From 876a30a3aee27148bd230129c719e9ddf35aace4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 8 Oct 2025 10:05:11 +0900 Subject: [PATCH 1/4] refactor(rsc): move common code for `transformCjsToEsm` --- packages/plugin-rsc/src/plugins/cjs.ts | 8 ------ .../plugin-rsc/src/transforms/cjs.test.ts | 25 +++++++++++++++---- packages/plugin-rsc/src/transforms/cjs.ts | 11 ++++++++ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/plugin-rsc/src/plugins/cjs.ts b/packages/plugin-rsc/src/plugins/cjs.ts index 60af1e224..e955fb4ba 100644 --- a/packages/plugin-rsc/src/plugins/cjs.ts +++ b/packages/plugin-rsc/src/plugins/cjs.ts @@ -54,14 +54,6 @@ export function cjsModuleRunnerPlugin(): Plugin[] { const ast = await parseAstAsync(code) const result = transformCjsToEsm(code, ast) const output = result.output - // TODO: can we use cjs-module-lexer to properly define named exports? - // for re-exports, we need to eagerly transform dependencies though. - // https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/internal/modules/esm/translators.js#L382-L409 - output.append(` -;__vite_ssr_exportAll__(module.exports); -export default module.exports; -export const __cjs_module_runner_transform = true; -`) return { code: output.toString(), map: output.generateMap({ hires: 'boundary' }), diff --git a/packages/plugin-rsc/src/transforms/cjs.test.ts b/packages/plugin-rsc/src/transforms/cjs.test.ts index b4342448a..dc5a9406a 100644 --- a/packages/plugin-rsc/src/transforms/cjs.test.ts +++ b/packages/plugin-rsc/src/transforms/cjs.test.ts @@ -24,6 +24,10 @@ exports.ok = true; expect(await testTransform(input)).toMatchInlineSnapshot(` "let exports = {}; const module = { exports }; exports.ok = true; + + ;__vite_ssr_exportAll__(module.exports); + export default module.exports; + export const __cjs_module_runner_transform = true; " `) }) @@ -44,6 +48,10 @@ if (true) { } else { module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.development.js'))); } + + ;__vite_ssr_exportAll__(module.exports); + export default module.exports; + export const __cjs_module_runner_transform = true; " `) }) @@ -66,6 +74,10 @@ if (true) { var ReactDOM = __cjs_to_esm_hoist_1; exports.useSyncExternalStoreWithSelector = function () {} })() + + ;__vite_ssr_exportAll__(module.exports); + export default module.exports; + export const __cjs_module_runner_transform = true; " `) }) @@ -97,6 +109,10 @@ function test() { const y2 = __cjs_to_esm_hoist_1().test; consoe.log(__cjs_to_esm_hoist_2) } + + ;__vite_ssr_exportAll__(module.exports); + export default module.exports; + export const __cjs_module_runner_transform = true; " `) }) @@ -114,6 +130,10 @@ function test() { const require = () => {}; require("test"); } + + ;__vite_ssr_exportAll__(module.exports); + export default module.exports; + export const __cjs_module_runner_transform = true; " `) }) @@ -130,11 +150,6 @@ function test() { if (id.endsWith('.cjs')) { const ast = await parseAstAsync(code) const { output } = transformCjsToEsm(code, ast) - output.append(` -;__vite_ssr_exportAll__(module.exports); -export default module.exports; -export const __cjs_module_runner_transform = true; -`) return { code: output.toString(), map: output.generateMap({ hires: 'boundary' }), diff --git a/packages/plugin-rsc/src/transforms/cjs.ts b/packages/plugin-rsc/src/transforms/cjs.ts index fa80ec4b0..ba14faec6 100644 --- a/packages/plugin-rsc/src/transforms/cjs.ts +++ b/packages/plugin-rsc/src/transforms/cjs.ts @@ -75,6 +75,7 @@ export function transformCjsToEsm( parentNodes.pop()! }, }) + // TODO: prepend after shebang for (const hoisted of hoistedCodes.reverse()) { output.prepend(hoisted) } @@ -83,5 +84,15 @@ export function transformCjsToEsm( } // https://nodejs.org/docs/v22.19.0/api/modules.html#exports-shortcut output.prepend(`let exports = {}; const module = { exports };\n`) + + // TODO: can we use cjs-module-lexer to properly define named exports? + // for re-exports, we need to eagerly transform dependencies though. + // https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/internal/modules/esm/translators.js#L382-L409 + output.append(` +;__vite_ssr_exportAll__(module.exports); +export default module.exports; +export const __cjs_module_runner_transform = true; +`) + return { output } } From d5d37794f52b18dcd26d76e86cf632140cb22626 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 8 Oct 2025 10:11:57 +0900 Subject: [PATCH 2/4] fix(rsc/cjs): add `__filename` and `__dirname` --- packages/plugin-rsc/src/plugins/cjs.ts | 3 ++- .../plugin-rsc/src/transforms/cjs.test.ts | 19 ++++++++++++------- packages/plugin-rsc/src/transforms/cjs.ts | 11 +++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/src/plugins/cjs.ts b/packages/plugin-rsc/src/plugins/cjs.ts index e955fb4ba..6c3c3f534 100644 --- a/packages/plugin-rsc/src/plugins/cjs.ts +++ b/packages/plugin-rsc/src/plugins/cjs.ts @@ -6,6 +6,7 @@ import * as esModuleLexer from 'es-module-lexer' import { transformCjsToEsm } from '../transforms/cjs' import { createDebug } from '@hiogawa/utils' import { parseIdQuery } from './shared' +import { fileURLToPath, pathToFileURL } from 'node:url' const debug = createDebug('vite-rsc:cjs') @@ -52,7 +53,7 @@ export function cjsModuleRunnerPlugin(): Plugin[] { } const ast = await parseAstAsync(code) - const result = transformCjsToEsm(code, ast) + const result = transformCjsToEsm(code, ast, { id }) const output = result.output return { code: output.toString(), diff --git a/packages/plugin-rsc/src/transforms/cjs.test.ts b/packages/plugin-rsc/src/transforms/cjs.test.ts index dc5a9406a..108f49029 100644 --- a/packages/plugin-rsc/src/transforms/cjs.test.ts +++ b/packages/plugin-rsc/src/transforms/cjs.test.ts @@ -7,7 +7,7 @@ import path from 'node:path' describe(transformCjsToEsm, () => { async function testTransform(input: string) { const ast = await parseAstAsync(input) - const { output } = transformCjsToEsm(input, ast) + const { output } = transformCjsToEsm(input, ast, { id: '/test.js' }) if (!output.hasChanged()) { return } @@ -22,7 +22,8 @@ describe(transformCjsToEsm, () => { exports.ok = true; ` expect(await testTransform(input)).toMatchInlineSnapshot(` - "let exports = {}; const module = { exports }; + "let __filename = "/test.js"; let __dirname = "/"; + let exports = {}; const module = { exports }; exports.ok = true; ;__vite_ssr_exportAll__(module.exports); @@ -41,7 +42,8 @@ if (true) { } ` expect(await testTransform(input)).toMatchInlineSnapshot(` - "let exports = {}; const module = { exports }; + "let __filename = "/test.js"; let __dirname = "/"; + let exports = {}; const module = { exports }; function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; } if (true) { module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.production.js'))); @@ -65,7 +67,8 @@ if (true) { })() ` expect(await testTransform(input)).toMatchInlineSnapshot(` - "let exports = {}; const module = { exports }; + "let __filename = "/test.js"; let __dirname = "/"; + let exports = {}; const module = { exports }; 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")); @@ -95,7 +98,8 @@ function test() { } ` expect(await testTransform(input)).toMatchInlineSnapshot(` - "let exports = {}; const module = { exports }; + "let __filename = "/test.js"; let __dirname = "/"; + let exports = {}; const module = { exports }; 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")); @@ -125,7 +129,8 @@ function test() { } ` expect(await testTransform(input)).toMatchInlineSnapshot(` - "let exports = {}; const module = { exports }; + "let __filename = "/test.js"; let __dirname = "/"; + let exports = {}; const module = { exports }; { const require = () => {}; require("test"); @@ -149,7 +154,7 @@ function test() { async transform(code, id) { if (id.endsWith('.cjs')) { const ast = await parseAstAsync(code) - const { output } = transformCjsToEsm(code, ast) + const { output } = transformCjsToEsm(code, ast, { id }) return { code: output.toString(), map: output.generateMap({ hires: 'boundary' }), diff --git a/packages/plugin-rsc/src/transforms/cjs.ts b/packages/plugin-rsc/src/transforms/cjs.ts index ba14faec6..c5781fec7 100644 --- a/packages/plugin-rsc/src/transforms/cjs.ts +++ b/packages/plugin-rsc/src/transforms/cjs.ts @@ -2,6 +2,8 @@ import type { Program, Node } from 'estree' import MagicString from 'magic-string' import { analyze } from 'periscopic' import { walk } from 'estree-walker' +import { fileURLToPath, pathToFileURL } from 'node:url' +import path from 'node:path' // TODO: // replacing require("xxx") into import("xxx") affects Vite's resolution. @@ -14,6 +16,7 @@ const CJS_INTEROP_HELPER = `function __cjs_interop__(m) { return m.__cjs_module_ export function transformCjsToEsm( code: string, ast: Program, + options: { id: string }, ): { output: MagicString } { const output = new MagicString(code) const analyzed = analyze(ast) @@ -85,6 +88,14 @@ export function transformCjsToEsm( // https://nodejs.org/docs/v22.19.0/api/modules.html#exports-shortcut output.prepend(`let exports = {}; const module = { exports };\n`) + // https://nodejs.org/docs/v22.19.0/api/modules.html#the-module-scope + // https://github.com/vitest-dev/vitest/blob/965cefc19722a6c899cd1d3decb3cc33e72af696/packages/vite-node/src/client.ts#L548-L554 + const __filename = fileURLToPath(pathToFileURL(options.id).href) + const __dirname = path.dirname(__filename) + output.prepend( + `let __filename = ${JSON.stringify(__filename)}; let __dirname = ${JSON.stringify(__dirname)};\n`, + ) + // TODO: can we use cjs-module-lexer to properly define named exports? // for re-exports, we need to eagerly transform dependencies though. // https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/internal/modules/esm/translators.js#L382-L409 From 2ff33b773c4be58b3dbc9a911661510b7b331015 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 8 Oct 2025 10:14:03 +0900 Subject: [PATCH 3/4] chore: lint --- packages/plugin-rsc/src/plugins/cjs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugins/cjs.ts b/packages/plugin-rsc/src/plugins/cjs.ts index 6c3c3f534..73315615a 100644 --- a/packages/plugin-rsc/src/plugins/cjs.ts +++ b/packages/plugin-rsc/src/plugins/cjs.ts @@ -6,7 +6,6 @@ import * as esModuleLexer from 'es-module-lexer' import { transformCjsToEsm } from '../transforms/cjs' import { createDebug } from '@hiogawa/utils' import { parseIdQuery } from './shared' -import { fileURLToPath, pathToFileURL } from 'node:url' const debug = createDebug('vite-rsc:cjs') From 59ddbd209fe4ba467a359387367533470397d350 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 8 Oct 2025 10:17:09 +0900 Subject: [PATCH 4/4] test: unit --- packages/plugin-rsc/src/transforms/cjs.test.ts | 6 ++++++ packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs | 2 ++ packages/plugin-rsc/src/transforms/fixtures/cjs/globals.cjs | 1 + 3 files changed, 9 insertions(+) create mode 100644 packages/plugin-rsc/src/transforms/fixtures/cjs/globals.cjs diff --git a/packages/plugin-rsc/src/transforms/cjs.test.ts b/packages/plugin-rsc/src/transforms/cjs.test.ts index 108f49029..d14d29fd8 100644 --- a/packages/plugin-rsc/src/transforms/cjs.test.ts +++ b/packages/plugin-rsc/src/transforms/cjs.test.ts @@ -170,6 +170,12 @@ function test() { const mod = await runner.import('/entry.mjs') expect(mod).toMatchInlineSnapshot(` { + "cjsGlobals": { + "test": [ + "string", + "string", + ], + }, "depDefault": { "a": "a", "b": "b", diff --git a/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs b/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs index 191d29e4c..63c854c5a 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs +++ b/packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs @@ -5,6 +5,7 @@ import depPrimitive from './primitive.cjs' import depExports from './exports.cjs' import depFnRequire from './function-require.cjs' import dualLib from './dual-lib.cjs' +import cjsGlobals from './globals.cjs' export { depDefault, depNamespace, @@ -13,4 +14,5 @@ export { depExports, depFnRequire, dualLib, + cjsGlobals, } diff --git a/packages/plugin-rsc/src/transforms/fixtures/cjs/globals.cjs b/packages/plugin-rsc/src/transforms/fixtures/cjs/globals.cjs new file mode 100644 index 000000000..9a1c50f8c --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/cjs/globals.cjs @@ -0,0 +1 @@ +exports.test = [typeof __filename, typeof __dirname]