cloudflare / cloudflare/vinext

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)

Open
#1,219 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## 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`:
```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`:
```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`:
```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`:
```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`:
```tsx
import { GoogleAnalytics } from '@next/third-parties/google';

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (



{children}


);
}
```

`app/page.tsx`:
```tsx
export default function Home() {
return

hi

;
}
```

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

## Scope

| Variant | Result |
| --- | --- |
| `` from `@next/third-parties/google` | **500** (this report) |
| `` 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:

```js
"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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.