Skip to content

Commit 94910a7

Browse files
committed
fix: treat module as normal variable in ESM
1 parent ff7ac33 commit 94910a7

File tree

29 files changed

+239
-34
lines changed

29 files changed

+239
-34
lines changed

packages/core/src/config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ const composeFormatConfig = ({
602602
bundle?: boolean;
603603
umdName?: Rspack.LibraryName;
604604
}): EnvironmentConfig => {
605-
const jsParserOptions = {
605+
const jsParserOptions: Record<string, Rspack.JavascriptParserOptions> = {
606606
cjs: {
607607
requireResolve: false,
608608
requireDynamic: false,
@@ -611,11 +611,14 @@ const composeFormatConfig = ({
611611
esm: {
612612
importMeta: false,
613613
importDynamic: false,
614+
commonjs: {
615+
exports: 'skipInEsm',
616+
},
614617
},
615618
others: {
616619
worker: false,
617620
},
618-
} as const;
621+
};
619622

620623
// The built-in Rslib plugin will apply to all formats except the `mf` format.
621624
// The `mf` format functions more like an application than a library and requires additional webpack runtime.

packages/core/tests/__snapshots__/config.test.ts.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i
461461
typeReexportsPresence: 'tolerant',
462462
importMeta: false,
463463
importDynamic: false,
464+
commonjs: {
465+
exports: 'skipInEsm'
466+
},
464467
requireResolve: false,
465468
requireDynamic: false,
466469
requireAsExpression: false,
@@ -1158,6 +1161,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i
11581161
typeReexportsPresence: 'tolerant',
11591162
importMeta: false,
11601163
importDynamic: false,
1164+
commonjs: {
1165+
exports: 'skipInEsm'
1166+
},
11611167
requireResolve: false,
11621168
requireDynamic: false,
11631169
requireAsExpression: false,
@@ -3658,6 +3664,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i
36583664
"module": {
36593665
"parser": {
36603666
"javascript": {
3667+
"commonjs": {
3668+
"exports": "skipInEsm",
3669+
},
36613670
"importDynamic": false,
36623671
"importMeta": false,
36633672
"requireAsExpression": false,
@@ -3944,6 +3953,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i
39443953
"module": {
39453954
"parser": {
39463955
"javascript": {
3956+
"commonjs": {
3957+
"exports": "skipInEsm",
3958+
},
39473959
"importDynamic": false,
39483960
"importMeta": false,
39493961
"requireAsExpression": false,

pnpm-lock.yaml

Lines changed: 12 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/format/index.test.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,18 @@ test('throw error when using mf with `bundle: false`', async () => {
114114
);
115115
});
116116

117-
test("API plugin's api should be skipped in parser", async () => {
118-
const fixturePath = path.resolve(__dirname, 'api-plugin');
119-
const { entries } = await buildAndGetResults({
117+
test('ESM interop should be correct', async () => {
118+
const fixturePath = path.resolve(__dirname, 'esm-interop');
119+
const { entryFiles } = await buildAndGetResults({
120120
fixturePath,
121121
});
122122

123-
expect(entries.esm).toMatchInlineSnapshot(`
124-
"const a = require.cache;
125-
const b = require.extensions;
126-
const c = require.config;
127-
const d = require.version;
128-
const e = require.include;
129-
const f = require.onError;
130-
export { a, b, c, d, e, f };
131-
"
132-
`);
133-
134-
expect(entries.cjs).toContain('const a = require.cache;');
135-
expect(entries.cjs).toContain('const b = require.extensions;');
136-
expect(entries.cjs).toContain('const c = require.config;');
137-
expect(entries.cjs).toContain('const d = require.version;');
138-
expect(entries.cjs).toContain('const e = require.include;');
139-
expect(entries.cjs).toContain('const f = require.onError;');
123+
const cjsOutput = await import(entryFiles.cjs);
124+
expect(typeof cjsOutput.default.path1.basename).toBe('function');
125+
expect(cjsOutput.default.path1).toBe(cjsOutput.default.path2);
140126
});
141127

142-
test('ESM interop should be correct', async () => {
128+
test('`module` should be correctly handled by `parserOptions.commonjs.exports = "skipInEsm"`', async () => {
143129
const fixturePath = path.resolve(__dirname, 'esm-interop');
144130
const { entryFiles } = await buildAndGetResults({
145131
fixturePath,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import path from 'node:path';
2+
import { expect, test } from '@rstest/core';
3+
import { buildAndGetResults } from 'test-helper';
4+
5+
test("API plugin's api should be skipped in parser", async () => {
6+
const fixturePath = path.resolve(__dirname);
7+
const { entries } = await buildAndGetResults({
8+
fixturePath,
9+
});
10+
11+
expect(entries.esm).toMatchInlineSnapshot(`
12+
"const a = require.cache;
13+
const b = require.extensions;
14+
const c = require.config;
15+
const d = require.version;
16+
const e = require.include;
17+
const f = require.onError;
18+
export { a, b, c, d, e, f };
19+
"
20+
`);
21+
22+
expect(entries.cjs).toContain('const a = require.cache;');
23+
expect(entries.cjs).toContain('const b = require.extensions;');
24+
expect(entries.cjs).toContain('const c = require.config;');
25+
expect(entries.cjs).toContain('const d = require.version;');
26+
expect(entries.cjs).toContain('const e = require.include;');
27+
expect(entries.cjs).toContain('const f = require.onError;');
28+
});

tests/integration/format/api-plugin/package.json renamed to tests/integration/parser-javascript/api-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "api-plugin-test",
2+
"name": "parser-javascript-api-plugin-test",
33
"version": "1.0.0",
44
"private": true,
55
"type": "module"
File renamed without changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const value = () => 42;
2+
3+
// Make the export immutable
4+
Object.defineProperty(module, 'exports', {
5+
enumerable: true,
6+
get: value,
7+
});
File renamed without changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import path from 'node:path';
2+
import { expect, test } from '@rstest/core';
3+
import { buildAndGetResults, queryContent } from 'test-helper';
4+
5+
test('`module` variable should be preserved as-is by `javascript.commonjs.exports = "false"`', async () => {
6+
const fixturePath = path.resolve(__dirname);
7+
const { contents } = await buildAndGetResults({
8+
fixturePath,
9+
});
10+
11+
const esm1 = queryContent(contents.esm, 'm1.mjs', { basename: true });
12+
const esm2 = queryContent(contents.esm, 'm2.mjs', { basename: true });
13+
const cjs1 = queryContent(contents.cjs, 'm1.js', { basename: true });
14+
const cjs2 = queryContent(contents.cjs, 'm2.js', { basename: true });
15+
16+
expect(
17+
(
18+
await Promise.all([
19+
import(esm1.path),
20+
import(esm2.path),
21+
import(cjs1.path),
22+
import(cjs2.path),
23+
])
24+
).map((m) => m.value),
25+
).toEqual([42, 42, 42, 42]);
26+
27+
const checksM1 = [
28+
'if (module.children) module.children = module.children.filter((item)=>item.filename !== path);',
29+
'module.exports = original',
30+
'if (module.exports && module.exports.test) return module.exports.test()',
31+
];
32+
33+
const checksEsm2 = [
34+
'if (node_module.children) node_module.children = node_module.children.filter((item)=>item.filename !== path);',
35+
'node_module.exports = original',
36+
'if (node_module.exports && node_module.exports.test) return node_module.exports.test()',
37+
];
38+
39+
const checksCjs2 = [
40+
'if (external_node_module_default().children) external_node_module_default().children = external_node_module_default().children.filter((item)=>item.filename !== path);',
41+
'external_node_module_default().exports = original',
42+
'if (external_node_module_default().exports && external_node_module_default().exports.test) return external_node_module_default().exports.test()',
43+
];
44+
45+
for (const check of checksM1) {
46+
expect(esm1.content).toContain(check);
47+
expect(cjs1.content).toContain(check);
48+
}
49+
50+
for (const check of checksEsm2) {
51+
expect(esm2.content).toContain(check);
52+
}
53+
54+
for (const check of checksCjs2) {
55+
expect(cjs2.content).toContain(check);
56+
}
57+
});

0 commit comments

Comments
 (0)