Skip to content

Commit a57f2dd

Browse files
ErleiXhi-ogawa
andauthored
fix(plugin-rsc): cjs to esm interop helper doesn't handle native/external cjs import properly (#1092)
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
1 parent 67295ce commit a57f2dd

File tree

15 files changed

+106
-5
lines changed

15 files changed

+106
-5
lines changed

packages/plugin-rsc/e2e/basic.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,14 @@ function defineTest(f: Fixture) {
15231523
).toHaveText('ok:browser')
15241524
})
15251525

1526+
test('cjs builtin interop', async ({ page }) => {
1527+
await page.goto(f.url())
1528+
await waitForHydration(page)
1529+
await expect(page.getByTestId('cjs-builtin-interop')).toHaveText(
1530+
'cjs-builtin-interop: ok',
1531+
)
1532+
})
1533+
15261534
test('use cache function', async ({ page }) => {
15271535
await page.goto(f.url())
15281536
await waitForHydration(page)

packages/plugin-rsc/examples/basic/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@types/react-dom": "^19.2.3",
2222
"@vitejs/plugin-react": "latest",
2323
"@vitejs/plugin-rsc": "latest",
24+
"@vitejs/test-dep-cjs-events-extend": "file:./test-dep/cjs-events-extend",
2425
"@vitejs/test-dep-client-in-server": "file:./test-dep/client-in-server",
2526
"@vitejs/test-dep-client-in-server2": "file:./test-dep/client-in-server2",
2627
"@vitejs/test-dep-css-in-server": "file:./test-dep/css-in-server",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @ts-ignore
2+
import * as testDep from '@vitejs/test-dep-cjs-events-extend'
3+
4+
export function TestCjsBuiltinInterop() {
5+
return (
6+
<div data-testid="cjs-builtin-interop">
7+
cjs-builtin-interop: {testDep.test}
8+
</div>
9+
)
10+
}

packages/plugin-rsc/examples/basic/src/routes/root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { TestChunk2 } from './chunk2/server'
2323
import { ClientCounter, Hydrated } from './client'
2424
import { TestClientError } from './client-error/client'
2525
import { TestCssQueries } from './css-queries/server'
26+
import { TestCjsBuiltinInterop } from './deps/cjs-builtin-interop/server'
2627
import { TestClientInServer } from './deps/client-in-server/server'
2728
import { TestServerInClient } from './deps/server-in-client/client'
2829
import { TestServerInServer } from './deps/server-in-server/server'
@@ -123,6 +124,7 @@ export function Root(props: { url: URL }) {
123124
<TestClientChunkServer />
124125
<TestChunk2 />
125126
<TestUseId />
127+
<TestCjsBuiltinInterop />
126128
</body>
127129
</html>
128130
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const EventEmitter = require('node:events')
2+
3+
class CustomEventEmitter extends EventEmitter {
4+
constructor() {
5+
super()
6+
this.testValue = 'ok'
7+
}
8+
}
9+
10+
module.exports.test = new CustomEventEmitter().testValue || 'ko'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "@vitejs/test-dep-cjs-events-extend",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "commonjs",
6+
"exports": "./index.js",
7+
"peerDependencies": {
8+
"react": "*"
9+
}
10+
}

packages/plugin-rsc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@types/react-dom": "^19.2.3",
5959
"@vitejs/plugin-react": "workspace:*",
6060
"@vitejs/test-dep-cjs-and-esm": "./test-dep/cjs-and-esm",
61+
"@vitejs/test-dep-cjs-falsy-primitive": "./test-dep/cjs-falsy-primitive",
6162
"picocolors": "^1.1.1",
6263
"react": "^19.2.4",
6364
"react-dom": "^19.2.4",

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ if (true) {
4444
expect(await testTransform(input)).toMatchInlineSnapshot(`
4545
"let __filename = "/test.js"; let __dirname = "/";
4646
let exports = {}; const module = { exports };
47-
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
47+
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "default" in m && Object.keys(m).every((k) => k === "default" || m[k] === m.default[k]) ? m.default : m;}
4848
if (true) {
4949
module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.production.js')));
5050
} else {
@@ -69,7 +69,7 @@ if (true) {
6969
expect(await testTransform(input)).toMatchInlineSnapshot(`
7070
"let __filename = "/test.js"; let __dirname = "/";
7171
let exports = {}; const module = { exports };
72-
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
72+
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "default" in m && Object.keys(m).every((k) => k === "default" || m[k] === m.default[k]) ? m.default : m;}
7373
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("react"));
7474
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("react-dom"));
7575
"production" !== process.env.NODE_ENV && (function() {
@@ -100,7 +100,7 @@ function test() {
100100
expect(await testTransform(input)).toMatchInlineSnapshot(`
101101
"let __filename = "/test.js"; let __dirname = "/";
102102
let exports = {}; const module = { exports };
103-
function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }
103+
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "default" in m && Object.keys(m).every((k) => k === "default" || m[k] === m.default[k]) ? m.default : m;}
104104
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("te" + "st"));
105105
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("test"));
106106
const __cjs_to_esm_hoist_2 = __cjs_interop__(await import("test"));
@@ -196,6 +196,12 @@ function test() {
196196
},
197197
"depPrimitive": "[ok]",
198198
"dualLib": "ok",
199+
"testExternalFalsyPrimitive": {
200+
"ok": true,
201+
},
202+
"testNodeBuiltins": {
203+
"nodeEventsOk": true,
204+
},
199205
}
200206
`)
201207
})

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,23 @@ import { analyze } from 'periscopic'
99
// replacing require("xxx") into import("xxx") affects Vite's resolution.
1010

1111
// Runtime helper to handle CJS/ESM interop when transforming require() to import()
12-
// Only unwrap .default for modules that were transformed by this plugin (marked with __cjs_module_runner_transform)
12+
// Unwrap .default for modules
13+
// 1. if it was transformed by this plugin (marked with __cjs_module_runner_transform)
14+
// 2. if all named exports point to .default properties (common CJS pattern)
15+
// this is particularly important for Node built-in modules consumptions;
16+
// where the built-in modules are not transformed by this plugin but still follow the CJS export pattern
17+
// see [getESMFacade](https://github.com/nodejs/node/blob/f200685d9930404d610a52d9e06513bf0a821ed4/lib/internal/bootstrap/realm.js#L347-L360)
18+
//
1319
// This ensures we don't incorrectly unwrap .default on genuine ESM modules
14-
const CJS_INTEROP_HELPER = `function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }`
20+
function __cjs_interop__(m: any) {
21+
return m.__cjs_module_runner_transform ||
22+
('default' in m &&
23+
Object.keys(m).every((k) => k === 'default' || m[k] === m.default[k]))
24+
? m.default
25+
: m
26+
}
27+
28+
const CJS_INTEROP_HELPER = __cjs_interop__.toString().replace(/\n\s*/g, '')
1529

1630
export function transformCjsToEsm(
1731
code: string,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import depDefault from './dep1.cjs'
22
import * as depNamespace from './dep2.cjs'
33
import dualLib from './dual-lib.cjs'
44
import depExports from './exports.cjs'
5+
import testExternalFalsyPrimitive from './external-falsy-primitive.cjs'
56
import depFnRequire from './function-require.cjs'
67
import depFn from './function.cjs'
78
import cjsGlobals from './globals.cjs'
9+
import testNodeBuiltins from './node-builtins.cjs'
810
import depPrimitive from './primitive.cjs'
11+
912
export {
1013
depDefault,
1114
depNamespace,
@@ -15,4 +18,6 @@ export {
1518
depFnRequire,
1619
dualLib,
1720
cjsGlobals,
21+
testNodeBuiltins,
22+
testExternalFalsyPrimitive,
1823
}

0 commit comments

Comments
 (0)