Skip to content

fix(plugin-rsc): cjs to esm interop helper doesn't handle native/external cjs import properly#1092

Merged
hi-ogawa merged 7 commits intovitejs:mainfrom
ErleiX:fix-cjs-interop-helper
Feb 3, 2026
Merged

fix(plugin-rsc): cjs to esm interop helper doesn't handle native/external cjs import properly#1092
hi-ogawa merged 7 commits intovitejs:mainfrom
ErleiX:fix-cjs-interop-helper

Conversation

@ErleiX
Copy link
Contributor

@ErleiX ErleiX commented Feb 2, 2026

Description

This change added a reproduce test for the problem that discovered by issue #1087

The test would fail without this fix and passes with this fix.

When it fails, it is exactly the same error described in issue #1087

The fix is to leverage the Node cjs to esm wrapping behavior, where the wrapped esm namespace of the cjs module is a duplicate of the default export, with additional properties of the default and its named export.

The fix is also considered "safe" and won't break the consumer code, because the fix is inheriently checking that the default is equivalent to the namespace object by comparing all the properties.

reproduced call stack:
```text
[examples/basic:dev] 11:58:49 PM [vite] Internal server error: Class extends value [object Module] is not a constructor or null
at eval (...\vite-plugin-react\node_modules\.pnpm\@vitejs+test-dep-cjs-events_287fa3db95bd309409d973813ed1549a\node_modules\@vitejs\test-dep-cjs-events-extend\index.js:3:34)
at async ESModulesEvaluator.runInlinedModule (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:913:3)
at async ModuleRunner.directRequest (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:1146:59)
at async ModuleRunner.cachedRequest (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:1053:73)
at async eval (...\vite-plugin-react\packages\plugin-rsc\examples\basic\src\routes\cjs-builtin-interop\server.tsx:1:1)
at async ESModulesEvaluator.runInlinedModule (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:913:3)
at async ModuleRunner.directRequest (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:1146:59)
at async ModuleRunner.cachedRequest (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:1053:73)
at async eval (...\vite-plugin-react\packages\plugin-rsc\examples\basic\src\routes\root.tsx:59:1)
at async ESModulesEvaluator.runInlinedModule (.../vite-plugin-react/node_modules/.pnpm/vite@7.3.1_@types+node@24.1_8836aae8899929c20df59ea37bacc8d4/node_modules/vite/dist/node/module-runner.js:913:3)
```
…s import properly

this is to fix issue vitejs#1087

This change enhances the cjs import check to cover the common Node cjs
to esm wrapping behavior, where the esm wrapper namespace object is
equivalent to the default export except the type differences and the
default and named export of the default itself.

the reproduce test now passes with this fix.
@ErleiX ErleiX changed the title fix #1087 RSC: cjs to esm interop helper doesn't handle native/external cjs import properly fix: #1087 RSC: cjs to esm interop helper doesn't handle native/external cjs import properly Feb 2, 2026
@hi-ogawa hi-ogawa self-assigned this Feb 2, 2026
@hi-ogawa hi-ogawa changed the title fix: #1087 RSC: cjs to esm interop helper doesn't handle native/external cjs import properly fix(rsc): cjs to esm interop helper doesn't handle native/external cjs import properly Feb 3, 2026
@hi-ogawa
Copy link
Contributor

hi-ogawa commented Feb 3, 2026

Thanks for PR! Will take a look. Your test case looks good, but can you also explain what's the actual package you hit the issue?

@hi-ogawa hi-ogawa added the trigger: preview Trigger pkg.pr.new label Feb 3, 2026
@ErleiX
Copy link
Contributor Author

ErleiX commented Feb 3, 2026

Thanks for PR! Will take a look. Your test case looks good, but can you also explain what's the actual package you hit the issue?

sure thing! One of the real cases is that we have a dependency on the "ws" package, where the websocket/websocker-server.js is having exactly the same issue as the test case I added in the PR:

https://github.com/websockets/ws/blob/master/lib/websocket.js
https://github.com/websockets/ws/blob/master/lib/websocket-server.js

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@vitejs/plugin-react@1092
npm i https://pkg.pr.new/@vitejs/plugin-react-oxc@1092
npm i https://pkg.pr.new/@vitejs/plugin-rsc@1092
npm i https://pkg.pr.new/@vitejs/plugin-react-swc@1092

commit: 942df8e

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Feb 3, 2026

Thanks for reference. In this case, ws package is importing events, but what I normally expect is to externalize ws since they don't need to go through noExternal. RSC plugin does aggresive noExternal only for react dependencies to ensure single react instance, but ws shouldn't be included there. Can you explain why ws ended up transformed by RSC's cjs plugin?

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Feb 3, 2026

That said, this fixes genuine bugs so I'm happy to merge this. Can you test pkg.pr.new in #1092 (comment) to confirm if this fix is sufficient for your use case? (as I adjusted the interop slightly)

@ErleiX
Copy link
Contributor Author

ErleiX commented Feb 3, 2026

Thanks for reference. In this case, ws package is importing events, but what I normally expect is to externalize ws since they don't need to go through noExternal. RSC plugin does aggresive noExternal only for react dependencies to ensure single react instance, but ws shouldn't be included there. Can you explain why ws ended up transformed by RSC's cjs plugin?

One of the things the team decided is to bundling everything for ssr/rsc to make the server side deployment easier(and potentially better performance without having to resolve the external dependencies at runtime), that's why our vite config is very aggressive with "noExternal". I am happy to learn your perspective on what the benefits or trade-offs are by making dependencies like ws external

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Feb 3, 2026

Right, bundling server output is actually legitimate as far as I know, but that can be done at separate build pipeline after vite build. Alternatively, you can also configure noExternal only for vite build, but not on vite dev since inherently transforming all server dependencies can be slower than let node import it as external. This is possible by conditionally returning config via defineConfig(env => env.command === 'build' ? ... : ...) https://vite.dev/config/#conditional-config.

@ErleiX
Copy link
Contributor Author

ErleiX commented Feb 3, 2026

Right, bundling server output is actually legitimate as far as I know, but that can be done at separate build pipeline after vite build. Alternatively, you can also configure noExternal only for vite build, but not on vite dev since inherently transforming all server dependencies can be slower than let node import it as external. This is possible by conditionally returning config via defineConfig(env => env.command === 'build' ? ... : ...) https://vite.dev/config/#conditional-config.

that seems to be a better solution, let me try that out, thanks for the suggestion. I also realized that Vite dev and build modes are quite different during debugging of this issue, which makes me wonder what are other gotchas that we'd better to be aware and config dev mode intentionally different, if there are such, please kindly share, thanks!

@hi-ogawa hi-ogawa changed the title fix(rsc): cjs to esm interop helper doesn't handle native/external cjs import properly fix(plugin-rsc): cjs to esm interop helper doesn't handle native/external cjs import properly Feb 3, 2026
@ErleiX
Copy link
Contributor Author

ErleiX commented Feb 3, 2026

That said, this fixes genuine bugs so I'm happy to merge this. Can you test pkg.pr.new in #1092 (comment) to confirm if this fix is sufficient for your use case? (as I adjusted the interop slightly)

confirming that this fix is sufficient for my use case, please feel free to merge it

@hi-ogawa hi-ogawa merged commit a57f2dd into vitejs:main Feb 3, 2026
20 checks passed
@hi-ogawa
Copy link
Contributor

hi-ogawa commented Feb 3, 2026

Thanks for contribution!

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Feb 3, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trigger: preview Trigger pkg.pr.new

Projects

None yet

2 participants