diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 76493a80f..bf96dcb6c 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -1523,6 +1523,14 @@ function defineTest(f: Fixture) { ).toHaveText('ok:browser') }) + test('cjs builtin interop', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('cjs-builtin-interop')).toHaveText( + 'cjs-builtin-interop: ok', + ) + }) + test('use cache function', async ({ page }) => { await page.goto(f.url()) await waitForHydration(page) diff --git a/packages/plugin-rsc/examples/basic/package.json b/packages/plugin-rsc/examples/basic/package.json index 57030661d..1a538b825 100644 --- a/packages/plugin-rsc/examples/basic/package.json +++ b/packages/plugin-rsc/examples/basic/package.json @@ -21,6 +21,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "latest", "@vitejs/plugin-rsc": "latest", + "@vitejs/test-dep-cjs-events-extend": "file:./test-dep/cjs-events-extend", "@vitejs/test-dep-client-in-server": "file:./test-dep/client-in-server", "@vitejs/test-dep-client-in-server2": "file:./test-dep/client-in-server2", "@vitejs/test-dep-css-in-server": "file:./test-dep/css-in-server", diff --git a/packages/plugin-rsc/examples/basic/src/routes/deps/cjs-builtin-interop/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/deps/cjs-builtin-interop/server.tsx new file mode 100644 index 000000000..9e0c6ddf4 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/deps/cjs-builtin-interop/server.tsx @@ -0,0 +1,10 @@ +// @ts-ignore +import * as testDep from '@vitejs/test-dep-cjs-events-extend' + +export function TestCjsBuiltinInterop() { + return ( +
+ cjs-builtin-interop: {testDep.test} +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx index 47415dcb7..2e6ef5e71 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -23,6 +23,7 @@ import { TestChunk2 } from './chunk2/server' import { ClientCounter, Hydrated } from './client' import { TestClientError } from './client-error/client' import { TestCssQueries } from './css-queries/server' +import { TestCjsBuiltinInterop } from './deps/cjs-builtin-interop/server' import { TestClientInServer } from './deps/client-in-server/server' import { TestServerInClient } from './deps/server-in-client/client' import { TestServerInServer } from './deps/server-in-server/server' @@ -123,6 +124,7 @@ export function Root(props: { url: URL }) { + ) diff --git a/packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend/index.js b/packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend/index.js new file mode 100644 index 000000000..01676475f --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend/index.js @@ -0,0 +1,10 @@ +const EventEmitter = require('node:events') + +class CustomEventEmitter extends EventEmitter { + constructor() { + super() + this.testValue = 'ok' + } +} + +module.exports.test = new CustomEventEmitter().testValue || 'ko' diff --git a/packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend/package.json b/packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend/package.json new file mode 100644 index 000000000..e4a1b4e06 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend/package.json @@ -0,0 +1,10 @@ +{ + "name": "@vitejs/test-dep-cjs-events-extend", + "version": "0.0.0", + "private": true, + "type": "commonjs", + "exports": "./index.js", + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/plugin-rsc/package.json b/packages/plugin-rsc/package.json index 267fab317..f411c2e13 100644 --- a/packages/plugin-rsc/package.json +++ b/packages/plugin-rsc/package.json @@ -58,6 +58,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "workspace:*", "@vitejs/test-dep-cjs-and-esm": "./test-dep/cjs-and-esm", + "@vitejs/test-dep-cjs-falsy-primitive": "./test-dep/cjs-falsy-primitive", "picocolors": "^1.1.1", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/packages/plugin-rsc/src/transforms/cjs.test.ts b/packages/plugin-rsc/src/transforms/cjs.test.ts index fa46caab8..8a71f2c53 100644 --- a/packages/plugin-rsc/src/transforms/cjs.test.ts +++ b/packages/plugin-rsc/src/transforms/cjs.test.ts @@ -44,7 +44,7 @@ if (true) { expect(await testTransform(input)).toMatchInlineSnapshot(` "let __filename = "/test.js"; let __dirname = "/"; let exports = {}; const module = { exports }; - function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; } + 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;} if (true) { module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.production.js'))); } else { @@ -69,7 +69,7 @@ if (true) { expect(await testTransform(input)).toMatchInlineSnapshot(` "let __filename = "/test.js"; let __dirname = "/"; let exports = {}; const module = { exports }; - function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; } + 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;} 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() { @@ -100,7 +100,7 @@ function test() { expect(await testTransform(input)).toMatchInlineSnapshot(` "let __filename = "/test.js"; let __dirname = "/"; let exports = {}; const module = { exports }; - function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; } + 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;} 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")); @@ -196,6 +196,12 @@ function test() { }, "depPrimitive": "[ok]", "dualLib": "ok", + "testExternalFalsyPrimitive": { + "ok": true, + }, + "testNodeBuiltins": { + "nodeEventsOk": true, + }, } `) }) diff --git a/packages/plugin-rsc/src/transforms/cjs.ts b/packages/plugin-rsc/src/transforms/cjs.ts index e48b3920c..766ab13a0 100644 --- a/packages/plugin-rsc/src/transforms/cjs.ts +++ b/packages/plugin-rsc/src/transforms/cjs.ts @@ -9,9 +9,23 @@ import { analyze } from 'periscopic' // 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) +// Unwrap .default for modules +// 1. if it was transformed by this plugin (marked with __cjs_module_runner_transform) +// 2. if all named exports point to .default properties (common CJS pattern) +// this is particularly important for Node built-in modules consumptions; +// where the built-in modules are not transformed by this plugin but still follow the CJS export pattern +// see [getESMFacade](https://github.com/nodejs/node/blob/f200685d9930404d610a52d9e06513bf0a821ed4/lib/internal/bootstrap/realm.js#L347-L360) +// // 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; }` +function __cjs_interop__(m: any) { + return m.__cjs_module_runner_transform || + ('default' in m && + Object.keys(m).every((k) => k === 'default' || m[k] === m.default[k])) + ? m.default + : m +} + +const CJS_INTEROP_HELPER = __cjs_interop__.toString().replace(/\n\s*/g, '') export function transformCjsToEsm( code: string, diff --git a/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs b/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs index 191b45b89..997d67b05 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs +++ b/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs @@ -2,10 +2,13 @@ import depDefault from './dep1.cjs' import * as depNamespace from './dep2.cjs' import dualLib from './dual-lib.cjs' import depExports from './exports.cjs' +import testExternalFalsyPrimitive from './external-falsy-primitive.cjs' import depFnRequire from './function-require.cjs' import depFn from './function.cjs' import cjsGlobals from './globals.cjs' +import testNodeBuiltins from './node-builtins.cjs' import depPrimitive from './primitive.cjs' + export { depDefault, depNamespace, @@ -15,4 +18,6 @@ export { depFnRequire, dualLib, cjsGlobals, + testNodeBuiltins, + testExternalFalsyPrimitive, } diff --git a/packages/plugin-rsc/src/transforms/fixtures/cjs/external-falsy-primitive.cjs b/packages/plugin-rsc/src/transforms/fixtures/cjs/external-falsy-primitive.cjs new file mode 100644 index 000000000..6343cbf56 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/cjs/external-falsy-primitive.cjs @@ -0,0 +1,3 @@ +const lib = require('@vitejs/test-dep-cjs-falsy-primitive') + +exports.ok = lib === false diff --git a/packages/plugin-rsc/src/transforms/fixtures/cjs/node-builtins.cjs b/packages/plugin-rsc/src/transforms/fixtures/cjs/node-builtins.cjs new file mode 100644 index 000000000..ab7677b36 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/cjs/node-builtins.cjs @@ -0,0 +1,10 @@ +const EventEmitter = require('node:events') + +class CustomEmitter extends EventEmitter { + constructor() { + super() + this.custom = true + } +} + +exports.nodeEventsOk = new CustomEmitter().custom diff --git a/packages/plugin-rsc/test-dep/cjs-falsy-primitive/index.cjs b/packages/plugin-rsc/test-dep/cjs-falsy-primitive/index.cjs new file mode 100644 index 000000000..f6b439dd6 --- /dev/null +++ b/packages/plugin-rsc/test-dep/cjs-falsy-primitive/index.cjs @@ -0,0 +1 @@ +module.exports = false diff --git a/packages/plugin-rsc/test-dep/cjs-falsy-primitive/package.json b/packages/plugin-rsc/test-dep/cjs-falsy-primitive/package.json new file mode 100644 index 000000000..e83c19bc4 --- /dev/null +++ b/packages/plugin-rsc/test-dep/cjs-falsy-primitive/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vitejs/test-dep-cjs-falsy-primitive", + "private": true, + "main": "./index.cjs" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6531eeaad..fa25fcb8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -488,6 +488,9 @@ importers: '@vitejs/test-dep-cjs-and-esm': specifier: ./test-dep/cjs-and-esm version: link:test-dep/cjs-and-esm + '@vitejs/test-dep-cjs-falsy-primitive': + specifier: ./test-dep/cjs-falsy-primitive + version: link:test-dep/cjs-falsy-primitive picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -531,6 +534,9 @@ importers: '@vitejs/plugin-rsc': specifier: latest version: link:../.. + '@vitejs/test-dep-cjs-events-extend': + specifier: file:./test-dep/cjs-events-extend + version: file:packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend(react@19.2.4) '@vitejs/test-dep-client-in-server': specifier: file:./test-dep/client-in-server version: file:packages/plugin-rsc/examples/basic/test-dep/client-in-server(react@19.2.4) @@ -2773,6 +2779,11 @@ packages: '@vitejs/release-scripts@1.6.0': resolution: {integrity: sha512-XV+w22Fvn+wqDtEkz8nQIJzvmRVSh90c2xvOO7cX9fkX8+39ZJpYRiXDIRJG1JRnF8khm1rHjulid+l+khc7TQ==} + '@vitejs/test-dep-cjs-events-extend@file:packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend': + resolution: {directory: packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend, type: directory} + peerDependencies: + react: '*' + '@vitejs/test-dep-cjs@file:packages/plugin-rsc/examples/basic/test-dep/cjs': resolution: {directory: packages/plugin-rsc/examples/basic/test-dep/cjs, type: directory} peerDependencies: @@ -6269,6 +6280,10 @@ snapshots: transitivePeerDependencies: - conventional-commits-filter + '@vitejs/test-dep-cjs-events-extend@file:packages/plugin-rsc/examples/basic/test-dep/cjs-events-extend(react@19.2.4)': + dependencies: + react: 19.2.4 + '@vitejs/test-dep-cjs@file:packages/plugin-rsc/examples/basic/test-dep/cjs(react@19.2.4)': dependencies: react: 19.2.4