Skip to content

vinext + @cloudflare/vite-plugin: TS-compiled "use client" CJS modules from node_modules don't get a Client Reference (SSR renders them as undefined/object) #1219

@eashish93

Description

@eashish93

Versions

  • vinext: 0.0.49
  • @cloudflare/vite-plugin: 1.37.0
  • @vitejs/plugin-rsc: 0.5.26
  • vite: 8.0.13
  • react / react-dom: 19.2.6
  • @next/third-parties: 16.2.6

Symptom

vinext dev returns HTTP 500 with:

[vite] Internal server error: Element type is invalid: expected a string
(for built-in components) or a class/function (for composite components)
but got: undefined.
    at renderElement (.../react-dom-server.edge.development...)
    at retryNode (...)
    at renderNodeDestructive (...)
    ...

(Some users see but got: object instead of undefined depending on which import is being resolved — same root cause.)

Minimal repro

package.json:

{
  "name": "vinext-repro",
  "private": true,
  "type": "module",
  "scripts": { "dev": "vinext dev --port 4030" },
  "dependencies": {
    "@next/third-parties": "^16.2.3",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "^1.36.4",
    "@vitejs/plugin-rsc": "^0.5.26",
    "vinext": "0.0.49",
    "vite": "^8.0.10",
    "wrangler": "^4.91.0"
  }
}

vite.config.ts:

import { cloudflare } from '@cloudflare/vite-plugin';
import { defineConfig } from 'vite';
import vinext from 'vinext';

export default defineConfig({
  plugins: [
    vinext(),
    cloudflare({
      inspectorPort: false,
      viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] },
    }),
  ],
});

wrangler.jsonc:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "vinext-repro",
  "main": "./worker/index.ts",
  "compatibility_date": "2026-04-21",
  "compatibility_flags": ["nodejs_compat"],
  "assets": { "binding": "ASSETS", "not_found_handling": "none" }
}

worker/index.ts:

import handler from 'vinext/server/app-router-entry';
export default {
  async fetch(request: Request, env: { ASSETS: Fetcher }, ctx: ExecutionContext) {
    return handler.fetch(request, env, ctx);
  },
};

app/layout.tsx:

import { GoogleAnalytics } from '@next/third-parties/google';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <GoogleAnalytics gaId="G-FAKEID000" />
        {children}
      </body>
    </html>
  );
}

app/page.tsx:

export default function Home() {
  return <h1>hi</h1>;
}

Reproduce:

bun install
bun run dev
curl http://localhost:4030/   # → 500

Scope

Variant Result
<GoogleAnalytics /> from @next/third-parties/google 500 (this report)
<Script /> directly from next/script (vinext shim) 200 ✅
<GoogleAnalytics /> with plain vite + vinext (no @cloudflare/vite-plugin) 200 ✅

So the bug only triggers when all three are present:

  1. The Cloudflare Vite plugin
  2. vinext's next/script alias path
  3. A use client module that is TypeScript-compiled CJS (the failing file is node_modules/@next/third-parties/dist/google/ga.js)

Diagnosis

@next/third-parties/dist/google/ga.js starts with:

"use strict";
'use client';
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoogleAnalytics = GoogleAnalytics;
// ...
const script_1 = __importDefault(require("next/script"));
// ...
return jsxs(Fragment, { children: [jsx(script_1.default, { ... }), ...] });

This is tsc-compiled output — common for any TS package targeting CJS. The "use strict"; is emitted by tsc before the user-authored 'use client';.

The interaction (best-effort theory; happy to hand off to maintainers):

  1. @vitejs/plugin-rsc's cjs-module-runner-transform (node_modules/@vitejs/plugin-rsc/dist/cjs-BdahOUyh.js) matches the file (it's in node_modules and contains require/exports) and rewrites require("next/script") to __cjs_interop__(await import("next/script")). This runs because @cloudflare/vite-plugin enables dev.moduleRunnerTransform in the rsc environment.

  2. @vitejs/plugin-rsc's rsc:use-client transform should have intercepted the file first and converted it into a client-reference proxy export. Plain next/script (vinext's ESM shim, where 'use client' is the first line) does get this treatment. The TS-compiled CJS variant of the same pattern apparently does not — the resulting module reaches SSR as a CJS export object whose named exports (GoogleAnalytics) are server-side functions trying to render a next/script import that has been wrapped through __cjs_interop__, returning either the raw namespace (→ "got: object") or undefined for the named/default field (→ "got: undefined").

Whether the root cause is:

  • the rsc:use-client plugin not transforming TS-compiled-CJS variants, or
  • the cjs-module-runner-transform plugin clobbering them before the use-client transform runs, or
  • vinext's next/script alias not registering a client-reference for this import path under the Cloudflare runner,

is best left to maintainers to confirm. The behavioural fact is that the same component import pattern fails for a node_modules CJS package and succeeds for ESM, only when the Cloudflare Vite plugin is active.

Workaround

Replace @next/third-parties usage with hand-written <script async src="..."> tags. (next/script itself works.)

Suggested test cases

  • Render any "use client" directive-prefixed CJS package from node_modules in an App Router server component under vinext + @cloudflare/vite-plugin.
  • Compare behaviour with and without @cloudflare/vite-plugin.
  • Compare behaviour for 'use client'-first vs "use strict";\n'use client'; source files.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions