Skip to content

Commit 5f18644

Browse files
Copilothi-ogawa
andcommitted
fix(plugin-rsc): add CJS/ESM interop helper for require() transformation
- Add __cjs_interop__ runtime helper to handle ESM modules correctly - Check for __esModule flag and default export before accessing .default - Update all tests to reflect new transformation output - Fixes issue where require("esm-package") would fail when transformed Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
1 parent 4228f1c commit 5f18644

File tree

2 files changed

+36
-14
lines changed

2 files changed

+36
-14
lines changed

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

Lines changed: 13 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 && m.__esModule ? m.default : (m.default !== undefined ? 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 && m.__esModule ? m.default : (m.default !== undefined ? 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 && m.__esModule ? m.default : (m.default !== undefined ? 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;

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

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

6+
// Runtime helper to handle CJS/ESM interop when transforming require() to import()
7+
// This is needed because when CJS code does require("pkg"), it expects:
8+
// - For CJS modules: the module.exports value directly
9+
// - For ESM modules: Node.js actually returns the namespace object
10+
// Since we're transforming to dynamic import(), we need to handle both cases
11+
const CJS_INTEROP_HELPER = `function __cjs_interop__(m) { return m && m.__esModule ? m.default : (m.default !== undefined ? m.default : m); }`
12+
613
export function transformCjsToEsm(
714
code: string,
815
ast: Program,
@@ -13,6 +20,8 @@ export function transformCjsToEsm(
1320
const parentNodes: Node[] = []
1421
const hoistedCodes: string[] = []
1522
let hoistIndex = 0
23+
let needsInteropHelper = false
24+
1625
walk(ast, {
1726
enter(node) {
1827
parentNodes.push(node)
@@ -38,11 +47,17 @@ export function transformCjsToEsm(
3847
}
3948
}
4049

50+
needsInteropHelper = true
51+
4152
if (isTopLevel) {
42-
// top-level scope `require` to dynamic import
53+
// top-level scope `require` to dynamic import with interop
4354
// (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)')
55+
output.update(
56+
node.start,
57+
node.callee.end,
58+
'(__cjs_interop__(await import',
59+
)
60+
output.appendRight(node.end, '))')
4661
} else {
4762
// hoist non top-level `require` to top-level
4863
const hoisted = `__cjs_to_esm_hoist_${hoistIndex}`
@@ -51,7 +66,7 @@ export function transformCjsToEsm(
5166
node.arguments[0]!.end,
5267
)
5368
hoistedCodes.push(
54-
`const ${hoisted} = (await import(${importee})).default;\n`,
69+
`const ${hoisted} = __cjs_interop__(await import(${importee}));\n`,
5570
)
5671
output.update(node.start, node.end, hoisted)
5772
hoistIndex++
@@ -65,6 +80,10 @@ export function transformCjsToEsm(
6580
for (const hoisted of hoistedCodes.reverse()) {
6681
output.prepend(hoisted)
6782
}
83+
// Prepend interop helper if needed
84+
if (needsInteropHelper) {
85+
output.prepend(`${CJS_INTEROP_HELPER}\n`)
86+
}
6887
// https://nodejs.org/docs/v22.19.0/api/modules.html#exports-shortcut
6988
output.prepend(`let exports = {}; const module = { exports };\n`)
7089
return { output }

0 commit comments

Comments
 (0)