Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/plugin-rsc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "workspace:*",
"@vitejs/test-dep-cjs-and-esm": "./test-dep/cjs-and-esm",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-server-dom-webpack": "^19.1.1",
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-rsc/src/plugins/cjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function cjsModuleRunnerPlugin(): Plugin[] {
output.append(`
;__vite_ssr_exportAll__(module.exports);
export default module.exports;
export const __cjs_module_runner_transform = true;
`)
return {
code: output.toString(),
Expand Down
26 changes: 16 additions & 10 deletions packages/plugin-rsc/src/transforms/cjs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ if (true) {
`
expect(await testTransform(input)).toMatchInlineSnapshot(`
"let exports = {}; const module = { exports };
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
if (true) {
module.exports = ((await import('./cjs/use-sync-external-store.production.js')).default);
module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.production.js')));
} else {
module.exports = ((await import('./cjs/use-sync-external-store.development.js')).default);
module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.development.js')));
}
"
`)
Expand All @@ -57,8 +58,9 @@ if (true) {
`
expect(await testTransform(input)).toMatchInlineSnapshot(`
"let exports = {}; const module = { exports };
const __cjs_to_esm_hoist_0 = (await import("react")).default;
const __cjs_to_esm_hoist_1 = (await import("react-dom")).default;
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("react"));
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("react-dom"));
"production" !== process.env.NODE_ENV && (function() {
var React = __cjs_to_esm_hoist_0;
var ReactDOM = __cjs_to_esm_hoist_1;
Expand All @@ -82,12 +84,13 @@ function test() {
`
expect(await testTransform(input)).toMatchInlineSnapshot(`
"let exports = {}; const module = { exports };
const __cjs_to_esm_hoist_0 = (await import("te" + "st")).default;
const __cjs_to_esm_hoist_1 = (await import("test")).default;
const __cjs_to_esm_hoist_2 = (await import("test")).default;
const x1 = ((await import("te" + "st")).default);
const x2 = ((await import("test")).default)().test;
console.log(((await import("test")).default))
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("te" + "st"));
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("test"));
const __cjs_to_esm_hoist_2 = __cjs_interop__(await import("test"));
const x1 = (__cjs_interop__(await import("te" + "st")));
const x2 = (__cjs_interop__(await import("test")))().test;
console.log((__cjs_interop__(await import("test"))))

function test() {
const y1 = __cjs_to_esm_hoist_0;
Expand Down Expand Up @@ -130,6 +133,7 @@ function test() {
output.append(`
;__vite_ssr_exportAll__(module.exports);
export default module.exports;
export const __cjs_module_runner_transform = true;
`)
return {
code: output.toString(),
Expand All @@ -156,6 +160,7 @@ export default module.exports;
"value": 3,
},
"depNamespace": {
"__cjs_module_runner_transform": true,
"a": "a",
"b": "b",
"default": {
Expand All @@ -164,6 +169,7 @@ export default module.exports;
},
},
"depPrimitive": "[ok]",
"dualLib": "ok",
}
`)
})
Expand Down
24 changes: 20 additions & 4 deletions packages/plugin-rsc/src/transforms/cjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import MagicString from 'magic-string'
import { analyze } from 'periscopic'
import { walk } from 'estree-walker'

// TODO:
// replacing require("xxx") into import("xxx") affects Vite's resolution.

// Runtime helper to handle CJS/ESM interop when transforming require() to import()
// Only unwrap .default for modules that were transformed by this plugin (marked with __cjs_module_runner_transform)
// This ensures we don't incorrectly unwrap .default on genuine ESM modules
const CJS_INTEROP_HELPER = `function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }`

export function transformCjsToEsm(
code: string,
ast: Program,
Expand All @@ -13,6 +21,7 @@ export function transformCjsToEsm(
const parentNodes: Node[] = []
const hoistedCodes: string[] = []
let hoistIndex = 0

walk(ast, {
enter(node) {
parentNodes.push(node)
Expand All @@ -39,10 +48,14 @@ export function transformCjsToEsm(
}

if (isTopLevel) {
// top-level scope `require` to dynamic import
// top-level scope `require` to dynamic import with interop
// (this allows handling react development/production re-export within top-level if branch)
output.update(node.start, node.callee.end, '((await import')
output.appendRight(node.end, ').default)')
output.update(
node.start,
node.callee.end,
'(__cjs_interop__(await import',
)
output.appendRight(node.end, '))')
} else {
// hoist non top-level `require` to top-level
const hoisted = `__cjs_to_esm_hoist_${hoistIndex}`
Expand All @@ -51,7 +64,7 @@ export function transformCjsToEsm(
node.arguments[0]!.end,
)
hoistedCodes.push(
`const ${hoisted} = (await import(${importee})).default;\n`,
`const ${hoisted} = __cjs_interop__(await import(${importee}));\n`,
)
output.update(node.start, node.end, hoisted)
hoistIndex++
Expand All @@ -65,6 +78,9 @@ export function transformCjsToEsm(
for (const hoisted of hoistedCodes.reverse()) {
output.prepend(hoisted)
}
if (output.hasChanged()) {
output.prepend(`${CJS_INTEROP_HELPER}\n`)
}
// https://nodejs.org/docs/v22.19.0/api/modules.html#exports-shortcut
output.prepend(`let exports = {}; const module = { exports };\n`)
return { output }
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-rsc/src/transforms/fixtures/cjs/dual-lib.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const lib = require('@vitejs/test-dep-cjs-and-esm')
module.exports = lib.ok
2 changes: 2 additions & 0 deletions packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import depFn from './function.cjs'
import depPrimitive from './primitive.cjs'
import depExports from './exports.cjs'
import depFnRequire from './function-require.cjs'
import dualLib from './dual-lib.cjs'
export {
depDefault,
depNamespace,
depFn,
depPrimitive,
depExports,
depFnRequire,
dualLib,
}
1 change: 1 addition & 0 deletions packages/plugin-rsc/test-dep/cjs-and-esm/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.ok = 'ok'
1 change: 1 addition & 0 deletions packages/plugin-rsc/test-dep/cjs-and-esm/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ok = 'ok'
11 changes: 11 additions & 0 deletions packages/plugin-rsc/test-dep/cjs-and-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@vitejs/test-dep-cjs-and-esm",
"private": true,
"type": "module",
"exports": {
".": {
"require": "./index.cjs",
"default": "./index.mjs"
}
}
}
Loading
Loading