Skip to content

Commit 1216caf

Browse files
Copilothi-ogawa
andauthored
fix(rsc/cjs): unwrap default based on __cjs_module_runner_transform marker (#905)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hi-ogawa <[email protected]> Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent 5e245aa commit 1216caf

File tree

10 files changed

+235
-18
lines changed

10 files changed

+235
-18
lines changed

packages/plugin-rsc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@types/react": "^19.1.16",
5757
"@types/react-dom": "^19.1.9",
5858
"@vitejs/plugin-react": "workspace:*",
59+
"@vitejs/test-dep-cjs-and-esm": "./test-dep/cjs-and-esm",
5960
"react": "^19.1.1",
6061
"react-dom": "^19.1.1",
6162
"react-server-dom-webpack": "^19.1.1",

packages/plugin-rsc/src/plugins/cjs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export function cjsModuleRunnerPlugin(): Plugin[] {
6060
output.append(`
6161
;__vite_ssr_exportAll__(module.exports);
6262
export default module.exports;
63+
export const __cjs_module_runner_transform = true;
6364
`)
6465
return {
6566
code: output.toString(),

packages/plugin-rsc/src/transforms/cjs.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ if (true) {
3838
`
3939
expect(await testTransform(input)).toMatchInlineSnapshot(`
4040
"let exports = {}; const module = { exports };
41+
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
4142
if (true) {
42-
module.exports = ((await import('./cjs/use-sync-external-store.production.js')).default);
43+
module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.production.js')));
4344
} else {
44-
module.exports = ((await import('./cjs/use-sync-external-store.development.js')).default);
45+
module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.development.js')));
4546
}
4647
"
4748
`)
@@ -57,8 +58,9 @@ if (true) {
5758
`
5859
expect(await testTransform(input)).toMatchInlineSnapshot(`
5960
"let exports = {}; const module = { exports };
60-
const __cjs_to_esm_hoist_0 = (await import("react")).default;
61-
const __cjs_to_esm_hoist_1 = (await import("react-dom")).default;
61+
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
62+
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("react"));
63+
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("react-dom"));
6264
"production" !== process.env.NODE_ENV && (function() {
6365
var React = __cjs_to_esm_hoist_0;
6466
var ReactDOM = __cjs_to_esm_hoist_1;
@@ -82,12 +84,13 @@ function test() {
8284
`
8385
expect(await testTransform(input)).toMatchInlineSnapshot(`
8486
"let exports = {}; const module = { exports };
85-
const __cjs_to_esm_hoist_0 = (await import("te" + "st")).default;
86-
const __cjs_to_esm_hoist_1 = (await import("test")).default;
87-
const __cjs_to_esm_hoist_2 = (await import("test")).default;
88-
const x1 = ((await import("te" + "st")).default);
89-
const x2 = ((await import("test")).default)().test;
90-
console.log(((await import("test")).default))
87+
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
88+
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("te" + "st"));
89+
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("test"));
90+
const __cjs_to_esm_hoist_2 = __cjs_interop__(await import("test"));
91+
const x1 = (__cjs_interop__(await import("te" + "st")));
92+
const x2 = (__cjs_interop__(await import("test")))().test;
93+
console.log((__cjs_interop__(await import("test"))))
9194
9295
function test() {
9396
const y1 = __cjs_to_esm_hoist_0;
@@ -130,6 +133,7 @@ function test() {
130133
output.append(`
131134
;__vite_ssr_exportAll__(module.exports);
132135
export default module.exports;
136+
export const __cjs_module_runner_transform = true;
133137
`)
134138
return {
135139
code: output.toString(),
@@ -156,6 +160,7 @@ export default module.exports;
156160
"value": 3,
157161
},
158162
"depNamespace": {
163+
"__cjs_module_runner_transform": true,
159164
"a": "a",
160165
"b": "b",
161166
"default": {
@@ -164,6 +169,7 @@ export default module.exports;
164169
},
165170
},
166171
"depPrimitive": "[ok]",
172+
"dualLib": "ok",
167173
}
168174
`)
169175
})

packages/plugin-rsc/src/transforms/cjs.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import MagicString from 'magic-string'
33
import { analyze } from 'periscopic'
44
import { walk } from 'estree-walker'
55

6+
// TODO:
7+
// replacing require("xxx") into import("xxx") affects Vite's resolution.
8+
9+
// Runtime helper to handle CJS/ESM interop when transforming require() to import()
10+
// Only unwrap .default for modules that were transformed by this plugin (marked with __cjs_module_runner_transform)
11+
// This ensures we don't incorrectly unwrap .default on genuine ESM modules
12+
const CJS_INTEROP_HELPER = `function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }`
13+
614
export function transformCjsToEsm(
715
code: string,
816
ast: Program,
@@ -13,6 +21,7 @@ export function transformCjsToEsm(
1321
const parentNodes: Node[] = []
1422
const hoistedCodes: string[] = []
1523
let hoistIndex = 0
24+
1625
walk(ast, {
1726
enter(node) {
1827
parentNodes.push(node)
@@ -39,10 +48,14 @@ export function transformCjsToEsm(
3948
}
4049

4150
if (isTopLevel) {
42-
// top-level scope `require` to dynamic import
51+
// top-level scope `require` to dynamic import with interop
4352
// (this allows handling react development/production re-export within top-level if branch)
44-
output.update(node.start, node.callee.end, '((await import')
45-
output.appendRight(node.end, ').default)')
53+
output.update(
54+
node.start,
55+
node.callee.end,
56+
'(__cjs_interop__(await import',
57+
)
58+
output.appendRight(node.end, '))')
4659
} else {
4760
// hoist non top-level `require` to top-level
4861
const hoisted = `__cjs_to_esm_hoist_${hoistIndex}`
@@ -51,7 +64,7 @@ export function transformCjsToEsm(
5164
node.arguments[0]!.end,
5265
)
5366
hoistedCodes.push(
54-
`const ${hoisted} = (await import(${importee})).default;\n`,
67+
`const ${hoisted} = __cjs_interop__(await import(${importee}));\n`,
5568
)
5669
output.update(node.start, node.end, hoisted)
5770
hoistIndex++
@@ -65,6 +78,9 @@ export function transformCjsToEsm(
6578
for (const hoisted of hoistedCodes.reverse()) {
6679
output.prepend(hoisted)
6780
}
81+
if (output.hasChanged()) {
82+
output.prepend(`${CJS_INTEROP_HELPER}\n`)
83+
}
6884
// https://nodejs.org/docs/v22.19.0/api/modules.html#exports-shortcut
6985
output.prepend(`let exports = {}; const module = { exports };\n`)
7086
return { output }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const lib = require('@vitejs/test-dep-cjs-and-esm')
2+
module.exports = lib.ok

packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import depFn from './function.cjs'
44
import depPrimitive from './primitive.cjs'
55
import depExports from './exports.cjs'
66
import depFnRequire from './function-require.cjs'
7+
import dualLib from './dual-lib.cjs'
78
export {
89
depDefault,
910
depNamespace,
1011
depFn,
1112
depPrimitive,
1213
depExports,
1314
depFnRequire,
15+
dualLib,
1416
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports.ok = 'ok'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const ok = 'ok'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@vitejs/test-dep-cjs-and-esm",
3+
"private": true,
4+
"type": "module",
5+
"exports": {
6+
".": {
7+
"require": "./index.cjs",
8+
"default": "./index.mjs"
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)