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
8 changes: 8 additions & 0 deletions packages/plugin-rsc/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-rsc/examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @ts-ignore
import * as testDep from '@vitejs/test-dep-cjs-events-extend'

export function TestCjsBuiltinInterop() {
return (
<div data-testid="cjs-builtin-interop">
cjs-builtin-interop: {testDep.test}
</div>
)
}
2 changes: 2 additions & 0 deletions packages/plugin-rsc/examples/basic/src/routes/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -123,6 +124,7 @@ export function Root(props: { url: URL }) {
<TestClientChunkServer />
<TestChunk2 />
<TestUseId />
<TestCjsBuiltinInterop />
</body>
</html>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@vitejs/test-dep-cjs-events-extend",
"version": "0.0.0",
"private": true,
"type": "commonjs",
"exports": "./index.js",
"peerDependencies": {
"react": "*"
}
}
1 change: 1 addition & 0 deletions packages/plugin-rsc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 9 additions & 3 deletions packages/plugin-rsc/src/transforms/cjs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -196,6 +196,12 @@ function test() {
},
"depPrimitive": "[ok]",
"dualLib": "ok",
"testExternalFalsyPrimitive": {
"ok": true,
},
"testNodeBuiltins": {
"nodeEventsOk": true,
},
}
`)
})
Expand Down
18 changes: 16 additions & 2 deletions packages/plugin-rsc/src/transforms/cjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,4 +18,6 @@ export {
depFnRequire,
dualLib,
cjsGlobals,
testNodeBuiltins,
testExternalFalsyPrimitive,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const lib = require('@vitejs/test-dep-cjs-falsy-primitive')

exports.ok = lib === false
10 changes: 10 additions & 0 deletions packages/plugin-rsc/src/transforms/fixtures/cjs/node-builtins.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const EventEmitter = require('node:events')

class CustomEmitter extends EventEmitter {
constructor() {
super()
this.custom = true
}
}

exports.nodeEventsOk = new CustomEmitter().custom
1 change: 1 addition & 0 deletions packages/plugin-rsc/test-dep/cjs-falsy-primitive/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = false
5 changes: 5 additions & 0 deletions packages/plugin-rsc/test-dep/cjs-falsy-primitive/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@vitejs/test-dep-cjs-falsy-primitive",
"private": true,
"main": "./index.cjs"
}
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading