From 94910a75515a32b7473c6874576dc9a59b0f962c Mon Sep 17 00:00:00 2001 From: fi3ework Date: Mon, 29 Sep 2025 19:48:40 +0800 Subject: [PATCH] fix: treat `module` as normal variable in ESM --- packages/core/src/config.ts | 7 ++- .../tests/__snapshots__/config.test.ts.snap | 12 ++++ pnpm-lock.yaml | 22 +++---- tests/integration/format/index.test.ts | 28 +++------ .../api-plugin/index.test.ts | 28 +++++++++ .../api-plugin/package.json | 2 +- .../api-plugin/rslib.config.ts | 0 .../api-plugin/src/cjsFile.cjs | 7 +++ .../api-plugin/src/index.js | 0 .../parser-javascript/module/index.test.ts | 57 +++++++++++++++++++ .../parser-javascript/module/package.json | 5 ++ .../parser-javascript/module/rslib.config.ts | 25 ++++++++ .../parser-javascript/module/src/cjsFile.cjs | 7 +++ .../parser-javascript/module/src/module1.js | 36 ++++++++++++ .../parser-javascript/module/src/module2.js | 37 ++++++++++++ .../require/import-dynamic/index.ts | 0 .../require/import-dynamic/package.json | 0 .../require/import-dynamic/rslib.config.ts | 0 .../require/index.test.ts | 0 .../require/require-as-expression/index.js | 0 .../require-as-expression/package.json | 0 .../require-as-expression/rslib.config.ts | 0 .../require/require-dynamic/index.ts | 0 .../require/require-dynamic/package.json | 0 .../require/require-dynamic/rslib.config.ts | 0 .../require/require-resolve/index.ts | 0 .../require/require-resolve/other.ts | 0 .../require/require-resolve/package.json | 0 .../require/require-resolve/rslib.config.ts | 0 29 files changed, 239 insertions(+), 34 deletions(-) create mode 100644 tests/integration/parser-javascript/api-plugin/index.test.ts rename tests/integration/{format => parser-javascript}/api-plugin/package.json (57%) rename tests/integration/{format => parser-javascript}/api-plugin/rslib.config.ts (100%) create mode 100644 tests/integration/parser-javascript/api-plugin/src/cjsFile.cjs rename tests/integration/{format => parser-javascript}/api-plugin/src/index.js (100%) create mode 100644 tests/integration/parser-javascript/module/index.test.ts create mode 100644 tests/integration/parser-javascript/module/package.json create mode 100644 tests/integration/parser-javascript/module/rslib.config.ts create mode 100644 tests/integration/parser-javascript/module/src/cjsFile.cjs create mode 100644 tests/integration/parser-javascript/module/src/module1.js create mode 100644 tests/integration/parser-javascript/module/src/module2.js rename tests/integration/{ => parser-javascript}/require/import-dynamic/index.ts (100%) rename tests/integration/{ => parser-javascript}/require/import-dynamic/package.json (100%) rename tests/integration/{ => parser-javascript}/require/import-dynamic/rslib.config.ts (100%) rename tests/integration/{ => parser-javascript}/require/index.test.ts (100%) rename tests/integration/{ => parser-javascript}/require/require-as-expression/index.js (100%) rename tests/integration/{ => parser-javascript}/require/require-as-expression/package.json (100%) rename tests/integration/{ => parser-javascript}/require/require-as-expression/rslib.config.ts (100%) rename tests/integration/{ => parser-javascript}/require/require-dynamic/index.ts (100%) rename tests/integration/{ => parser-javascript}/require/require-dynamic/package.json (100%) rename tests/integration/{ => parser-javascript}/require/require-dynamic/rslib.config.ts (100%) rename tests/integration/{ => parser-javascript}/require/require-resolve/index.ts (100%) rename tests/integration/{ => parser-javascript}/require/require-resolve/other.ts (100%) rename tests/integration/{ => parser-javascript}/require/require-resolve/package.json (100%) rename tests/integration/{ => parser-javascript}/require/require-resolve/rslib.config.ts (100%) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 1816871e2..864ffe8a9 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -602,7 +602,7 @@ const composeFormatConfig = ({ bundle?: boolean; umdName?: Rspack.LibraryName; }): EnvironmentConfig => { - const jsParserOptions = { + const jsParserOptions: Record = { cjs: { requireResolve: false, requireDynamic: false, @@ -611,11 +611,14 @@ const composeFormatConfig = ({ esm: { importMeta: false, importDynamic: false, + commonjs: { + exports: 'skipInEsm', + }, }, others: { worker: false, }, - } as const; + }; // The built-in Rslib plugin will apply to all formats except the `mf` format. // The `mf` format functions more like an application than a library and requires additional webpack runtime. diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 5e6ea22ec..44ee503eb 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -461,6 +461,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i typeReexportsPresence: 'tolerant', importMeta: false, importDynamic: false, + commonjs: { + exports: 'skipInEsm' + }, requireResolve: false, requireDynamic: false, requireAsExpression: false, @@ -1158,6 +1161,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i typeReexportsPresence: 'tolerant', importMeta: false, importDynamic: false, + commonjs: { + exports: 'skipInEsm' + }, requireResolve: false, requireDynamic: false, requireAsExpression: false, @@ -3658,6 +3664,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i "module": { "parser": { "javascript": { + "commonjs": { + "exports": "skipInEsm", + }, "importDynamic": false, "importMeta": false, "requireAsExpression": false, @@ -3944,6 +3953,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i "module": { "parser": { "javascript": { + "commonjs": { + "exports": "skipInEsm", + }, "importDynamic": false, "importMeta": false, "requireAsExpression": false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dfd67af0..031b7e953 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -907,8 +907,6 @@ importers: specifier: ^4.17.20 version: 4.17.20 - tests/integration/format/api-plugin: {} - tests/integration/format/cjs-static-export: {} tests/integration/format/default: {} @@ -963,6 +961,18 @@ importers: tests/integration/output/chunkFileName-single: {} + tests/integration/parser-javascript/api-plugin: {} + + tests/integration/parser-javascript/module: {} + + tests/integration/parser-javascript/require/import-dynamic: {} + + tests/integration/parser-javascript/require/require-as-expression: {} + + tests/integration/parser-javascript/require/require-dynamic: {} + + tests/integration/parser-javascript/require/require-resolve: {} + tests/integration/plugins/basic: {} tests/integration/plugins/mf-dev: {} @@ -1043,14 +1053,6 @@ importers: tests/integration/redirect/style: {} - tests/integration/require/import-dynamic: {} - - tests/integration/require/require-as-expression: {} - - tests/integration/require/require-dynamic: {} - - tests/integration/require/require-resolve: {} - tests/integration/resolve/data-url: {} tests/integration/resolve/false: {} diff --git a/tests/integration/format/index.test.ts b/tests/integration/format/index.test.ts index aa58afcfb..e786f544b 100644 --- a/tests/integration/format/index.test.ts +++ b/tests/integration/format/index.test.ts @@ -114,32 +114,18 @@ test('throw error when using mf with `bundle: false`', async () => { ); }); -test("API plugin's api should be skipped in parser", async () => { - const fixturePath = path.resolve(__dirname, 'api-plugin'); - const { entries } = await buildAndGetResults({ +test('ESM interop should be correct', async () => { + const fixturePath = path.resolve(__dirname, 'esm-interop'); + const { entryFiles } = await buildAndGetResults({ fixturePath, }); - expect(entries.esm).toMatchInlineSnapshot(` - "const a = require.cache; - const b = require.extensions; - const c = require.config; - const d = require.version; - const e = require.include; - const f = require.onError; - export { a, b, c, d, e, f }; - " - `); - - expect(entries.cjs).toContain('const a = require.cache;'); - expect(entries.cjs).toContain('const b = require.extensions;'); - expect(entries.cjs).toContain('const c = require.config;'); - expect(entries.cjs).toContain('const d = require.version;'); - expect(entries.cjs).toContain('const e = require.include;'); - expect(entries.cjs).toContain('const f = require.onError;'); + const cjsOutput = await import(entryFiles.cjs); + expect(typeof cjsOutput.default.path1.basename).toBe('function'); + expect(cjsOutput.default.path1).toBe(cjsOutput.default.path2); }); -test('ESM interop should be correct', async () => { +test('`module` should be correctly handled by `parserOptions.commonjs.exports = "skipInEsm"`', async () => { const fixturePath = path.resolve(__dirname, 'esm-interop'); const { entryFiles } = await buildAndGetResults({ fixturePath, diff --git a/tests/integration/parser-javascript/api-plugin/index.test.ts b/tests/integration/parser-javascript/api-plugin/index.test.ts new file mode 100644 index 000000000..37f303747 --- /dev/null +++ b/tests/integration/parser-javascript/api-plugin/index.test.ts @@ -0,0 +1,28 @@ +import path from 'node:path'; +import { expect, test } from '@rstest/core'; +import { buildAndGetResults } from 'test-helper'; + +test("API plugin's api should be skipped in parser", async () => { + const fixturePath = path.resolve(__dirname); + const { entries } = await buildAndGetResults({ + fixturePath, + }); + + expect(entries.esm).toMatchInlineSnapshot(` + "const a = require.cache; + const b = require.extensions; + const c = require.config; + const d = require.version; + const e = require.include; + const f = require.onError; + export { a, b, c, d, e, f }; + " + `); + + expect(entries.cjs).toContain('const a = require.cache;'); + expect(entries.cjs).toContain('const b = require.extensions;'); + expect(entries.cjs).toContain('const c = require.config;'); + expect(entries.cjs).toContain('const d = require.version;'); + expect(entries.cjs).toContain('const e = require.include;'); + expect(entries.cjs).toContain('const f = require.onError;'); +}); diff --git a/tests/integration/format/api-plugin/package.json b/tests/integration/parser-javascript/api-plugin/package.json similarity index 57% rename from tests/integration/format/api-plugin/package.json rename to tests/integration/parser-javascript/api-plugin/package.json index bb5846304..18b55a7c4 100644 --- a/tests/integration/format/api-plugin/package.json +++ b/tests/integration/parser-javascript/api-plugin/package.json @@ -1,5 +1,5 @@ { - "name": "api-plugin-test", + "name": "parser-javascript-api-plugin-test", "version": "1.0.0", "private": true, "type": "module" diff --git a/tests/integration/format/api-plugin/rslib.config.ts b/tests/integration/parser-javascript/api-plugin/rslib.config.ts similarity index 100% rename from tests/integration/format/api-plugin/rslib.config.ts rename to tests/integration/parser-javascript/api-plugin/rslib.config.ts diff --git a/tests/integration/parser-javascript/api-plugin/src/cjsFile.cjs b/tests/integration/parser-javascript/api-plugin/src/cjsFile.cjs new file mode 100644 index 000000000..4f9bb5ea9 --- /dev/null +++ b/tests/integration/parser-javascript/api-plugin/src/cjsFile.cjs @@ -0,0 +1,7 @@ +const value = () => 42; + +// Make the export immutable +Object.defineProperty(module, 'exports', { + enumerable: true, + get: value, +}); diff --git a/tests/integration/format/api-plugin/src/index.js b/tests/integration/parser-javascript/api-plugin/src/index.js similarity index 100% rename from tests/integration/format/api-plugin/src/index.js rename to tests/integration/parser-javascript/api-plugin/src/index.js diff --git a/tests/integration/parser-javascript/module/index.test.ts b/tests/integration/parser-javascript/module/index.test.ts new file mode 100644 index 000000000..af2789822 --- /dev/null +++ b/tests/integration/parser-javascript/module/index.test.ts @@ -0,0 +1,57 @@ +import path from 'node:path'; +import { expect, test } from '@rstest/core'; +import { buildAndGetResults, queryContent } from 'test-helper'; + +test('`module` variable should be preserved as-is by `javascript.commonjs.exports = "false"`', async () => { + const fixturePath = path.resolve(__dirname); + const { contents } = await buildAndGetResults({ + fixturePath, + }); + + const esm1 = queryContent(contents.esm, 'm1.mjs', { basename: true }); + const esm2 = queryContent(contents.esm, 'm2.mjs', { basename: true }); + const cjs1 = queryContent(contents.cjs, 'm1.js', { basename: true }); + const cjs2 = queryContent(contents.cjs, 'm2.js', { basename: true }); + + expect( + ( + await Promise.all([ + import(esm1.path), + import(esm2.path), + import(cjs1.path), + import(cjs2.path), + ]) + ).map((m) => m.value), + ).toEqual([42, 42, 42, 42]); + + const checksM1 = [ + 'if (module.children) module.children = module.children.filter((item)=>item.filename !== path);', + 'module.exports = original', + 'if (module.exports && module.exports.test) return module.exports.test()', + ]; + + const checksEsm2 = [ + 'if (node_module.children) node_module.children = node_module.children.filter((item)=>item.filename !== path);', + 'node_module.exports = original', + 'if (node_module.exports && node_module.exports.test) return node_module.exports.test()', + ]; + + const checksCjs2 = [ + 'if (external_node_module_default().children) external_node_module_default().children = external_node_module_default().children.filter((item)=>item.filename !== path);', + 'external_node_module_default().exports = original', + 'if (external_node_module_default().exports && external_node_module_default().exports.test) return external_node_module_default().exports.test()', + ]; + + for (const check of checksM1) { + expect(esm1.content).toContain(check); + expect(cjs1.content).toContain(check); + } + + for (const check of checksEsm2) { + expect(esm2.content).toContain(check); + } + + for (const check of checksCjs2) { + expect(cjs2.content).toContain(check); + } +}); diff --git a/tests/integration/parser-javascript/module/package.json b/tests/integration/parser-javascript/module/package.json new file mode 100644 index 000000000..f94029d8a --- /dev/null +++ b/tests/integration/parser-javascript/module/package.json @@ -0,0 +1,5 @@ +{ + "name": "parser-javascript-module-test", + "version": "1.0.0", + "private": true +} diff --git a/tests/integration/parser-javascript/module/rslib.config.ts b/tests/integration/parser-javascript/module/rslib.config.ts new file mode 100644 index 000000000..eafd4f0ec --- /dev/null +++ b/tests/integration/parser-javascript/module/rslib.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + // ESM + generateBundleEsmConfig({ + source: { + entry: { + m1: './src/module1.js', + m2: './src/module2.js', + }, + }, + }), + // CJS + generateBundleCjsConfig({ + source: { + entry: { + m1: './src/module1.js', + m2: './src/module2.js', + }, + }, + }), + ], +}); diff --git a/tests/integration/parser-javascript/module/src/cjsFile.cjs b/tests/integration/parser-javascript/module/src/cjsFile.cjs new file mode 100644 index 000000000..4f9bb5ea9 --- /dev/null +++ b/tests/integration/parser-javascript/module/src/cjsFile.cjs @@ -0,0 +1,7 @@ +const value = () => 42; + +// Make the export immutable +Object.defineProperty(module, 'exports', { + enumerable: true, + get: value, +}); diff --git a/tests/integration/parser-javascript/module/src/module1.js b/tests/integration/parser-javascript/module/src/module1.js new file mode 100644 index 000000000..8f594ccb7 --- /dev/null +++ b/tests/integration/parser-javascript/module/src/module1.js @@ -0,0 +1,36 @@ +import value from './cjsFile.cjs'; +export { value }; + +export function foo() { + console.log(module); + if (module.children) { + module.children = module.children.filter((item) => item.filename !== path); + } +} + +export function bar() { + const original = module.exports; + + try { + module.exports = { + ...module.exports, + test: () => 'ok', + }; + + return module.exports.test(); + } finally { + module.exports = original; + } +} + +export function baz() { + if (typeof module === 'undefined') { + return undefined; + } + + if (module.exports && module.exports.test) { + return module.exports.test(); + } + + return undefined; +} diff --git a/tests/integration/parser-javascript/module/src/module2.js b/tests/integration/parser-javascript/module/src/module2.js new file mode 100644 index 000000000..995f32db6 --- /dev/null +++ b/tests/integration/parser-javascript/module/src/module2.js @@ -0,0 +1,37 @@ +import module from 'node:module'; +import value from './cjsFile.cjs'; +export { value }; + +export function foo() { + console.log(module); + if (module.children) { + module.children = module.children.filter((item) => item.filename !== path); + } +} + +export function bar() { + const original = module.exports; + + try { + module.exports = { + ...module.exports, + test: () => 'ok', + }; + + return module.exports.test(); + } finally { + module.exports = original; + } +} + +export function baz() { + if (typeof module === 'undefined') { + return undefined; + } + + if (module.exports && module.exports.test) { + return module.exports.test(); + } + + return undefined; +} diff --git a/tests/integration/require/import-dynamic/index.ts b/tests/integration/parser-javascript/require/import-dynamic/index.ts similarity index 100% rename from tests/integration/require/import-dynamic/index.ts rename to tests/integration/parser-javascript/require/import-dynamic/index.ts diff --git a/tests/integration/require/import-dynamic/package.json b/tests/integration/parser-javascript/require/import-dynamic/package.json similarity index 100% rename from tests/integration/require/import-dynamic/package.json rename to tests/integration/parser-javascript/require/import-dynamic/package.json diff --git a/tests/integration/require/import-dynamic/rslib.config.ts b/tests/integration/parser-javascript/require/import-dynamic/rslib.config.ts similarity index 100% rename from tests/integration/require/import-dynamic/rslib.config.ts rename to tests/integration/parser-javascript/require/import-dynamic/rslib.config.ts diff --git a/tests/integration/require/index.test.ts b/tests/integration/parser-javascript/require/index.test.ts similarity index 100% rename from tests/integration/require/index.test.ts rename to tests/integration/parser-javascript/require/index.test.ts diff --git a/tests/integration/require/require-as-expression/index.js b/tests/integration/parser-javascript/require/require-as-expression/index.js similarity index 100% rename from tests/integration/require/require-as-expression/index.js rename to tests/integration/parser-javascript/require/require-as-expression/index.js diff --git a/tests/integration/require/require-as-expression/package.json b/tests/integration/parser-javascript/require/require-as-expression/package.json similarity index 100% rename from tests/integration/require/require-as-expression/package.json rename to tests/integration/parser-javascript/require/require-as-expression/package.json diff --git a/tests/integration/require/require-as-expression/rslib.config.ts b/tests/integration/parser-javascript/require/require-as-expression/rslib.config.ts similarity index 100% rename from tests/integration/require/require-as-expression/rslib.config.ts rename to tests/integration/parser-javascript/require/require-as-expression/rslib.config.ts diff --git a/tests/integration/require/require-dynamic/index.ts b/tests/integration/parser-javascript/require/require-dynamic/index.ts similarity index 100% rename from tests/integration/require/require-dynamic/index.ts rename to tests/integration/parser-javascript/require/require-dynamic/index.ts diff --git a/tests/integration/require/require-dynamic/package.json b/tests/integration/parser-javascript/require/require-dynamic/package.json similarity index 100% rename from tests/integration/require/require-dynamic/package.json rename to tests/integration/parser-javascript/require/require-dynamic/package.json diff --git a/tests/integration/require/require-dynamic/rslib.config.ts b/tests/integration/parser-javascript/require/require-dynamic/rslib.config.ts similarity index 100% rename from tests/integration/require/require-dynamic/rslib.config.ts rename to tests/integration/parser-javascript/require/require-dynamic/rslib.config.ts diff --git a/tests/integration/require/require-resolve/index.ts b/tests/integration/parser-javascript/require/require-resolve/index.ts similarity index 100% rename from tests/integration/require/require-resolve/index.ts rename to tests/integration/parser-javascript/require/require-resolve/index.ts diff --git a/tests/integration/require/require-resolve/other.ts b/tests/integration/parser-javascript/require/require-resolve/other.ts similarity index 100% rename from tests/integration/require/require-resolve/other.ts rename to tests/integration/parser-javascript/require/require-resolve/other.ts diff --git a/tests/integration/require/require-resolve/package.json b/tests/integration/parser-javascript/require/require-resolve/package.json similarity index 100% rename from tests/integration/require/require-resolve/package.json rename to tests/integration/parser-javascript/require/require-resolve/package.json diff --git a/tests/integration/require/require-resolve/rslib.config.ts b/tests/integration/parser-javascript/require/require-resolve/rslib.config.ts similarity index 100% rename from tests/integration/require/require-resolve/rslib.config.ts rename to tests/integration/parser-javascript/require/require-resolve/rslib.config.ts