Skip to content

Commit e6e9a4e

Browse files
committed
test(css): make CSS modules actually concatenate and JS exports actually mangle
Previously the test had two latent bugs that the user caught on review: 1. CSS modules were NOT concatenated. The dynamic `import("./style.module.css?...")` calls in `index.js` created alternate access paths to the same modules, which webpack flags as "EXTERNAL MODULE" and excludes from the concatenation scope. The bundle showed every CSS module as a separate runtime module with full-string export keys. 2. JS export identifiers were NOT actually mangled — only the `canMangle: true` *capability* flag was asserted via `__webpack_exports_info__`. CSS modules only emit mangled identifiers inside the concatenation scope (CssGenerator.js #L472–497), so without (1), no mangling could happen. Restructure: - Switch to `target: "node"` and drop all dynamic imports — every reference to a CSS variant is now a static import, so all five modules inline into the entry scope. - Split into two entry files (`index-named.js` for `namedExports: true`, `index-default.js` for `namedExports: false`) selected per config. Mangling requires named imports specifically — a namespace import marks every export as observed-by-name and disables mangling — but named imports don't compile against a default-only module. The split avoids that conflict. - Drop the `re-exports.js` proxy module and the `__webpack_exports_info__.canMangle` assertions; replace with direct bundle-source inspection that proves: * No `EXTERNAL MODULE: css ./style.module.css?…` markers remain. * No `__webpack_require__("./style.module.css?…")` calls remain. * `__webpack_modules__` carries no CSS module entries. * Original JS export identifiers (`btnInfoIsDisabled`, `fooBar`, `btnInfo_isDisabled`, …) never appear as `const`/`let`/`var` declarations in the entry bundle — they have been replaced by mangled short names like `F0`, `bQ`, `zY`. Bundle inspection runs only on the CJS configs; mangling and concatenation behave identically across output modes, so we don't plumb up an `import.meta.url`-based `__dirname` polyfill for the ESM bundles. https://claude.ai/code/session_01Dp4wfcAdJPVdZaBpvD1QNZ
1 parent 1819638 commit e6e9a4e

5 files changed

Lines changed: 199 additions & 184 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Entry for `parser.namedExports: false` configs. The CSS modules
2+
// expose only `default`, so a default import is the only valid form.
3+
// Mangling does not apply here — there are no JS named exports for
4+
// CSS classes — but concatenation still does.
5+
6+
import asIsDefault from "./style.module.css?as-is";
7+
import camelCaseDefault from "./style.module.css?camel-case";
8+
import camelOnlyDefault from "./style.module.css?camel-case-only";
9+
import dashesDefault from "./style.module.css?dashes";
10+
import dashesOnlyDefault from "./style.module.css?dashes-only";
11+
12+
const matrixTitle = `output.module=${process.env.OUTPUT_MODULE}, namedExports=false`;
13+
14+
it(`should expose the right default-export shape per convention (${matrixTitle})`, () => {
15+
// "as-is": original CSS class names are exported, no aliases
16+
expect(asIsDefault["btn-info_is-disabled"]).toBeDefined();
17+
expect(asIsDefault["btn--info_is-disabled_1"]).toBeDefined();
18+
expect(asIsDefault.foo_bar).toBeDefined();
19+
expect(asIsDefault.simple).toBeDefined();
20+
expect(asIsDefault.class).toBeDefined();
21+
expect(asIsDefault["my-btn-info_is-disabled"]).toBe("value");
22+
expect(asIsDefault.foo).toBe("bar");
23+
expect(asIsDefault.btnInfoIsDisabled).toBeUndefined();
24+
expect(asIsDefault.fooBar).toBeUndefined();
25+
26+
// "camel-case": original AND camelCase aliases share the same value
27+
expect(camelCaseDefault["btn-info_is-disabled"]).toBe(
28+
camelCaseDefault.btnInfoIsDisabled
29+
);
30+
expect(camelCaseDefault["btn--info_is-disabled_1"]).toBe(
31+
camelCaseDefault.btnInfoIsDisabled1
32+
);
33+
expect(camelCaseDefault.foo_bar).toBe(camelCaseDefault.fooBar);
34+
expect(camelCaseDefault.foo).toBe("bar");
35+
expect(camelCaseDefault["my-btn-info_is-disabled"]).toBe("value");
36+
expect(camelCaseDefault.myBtnInfoIsDisabled).toBe("value");
37+
38+
// "camel-case-only": only the camelCase form exists
39+
expect(camelOnlyDefault.btnInfoIsDisabled).toBeDefined();
40+
expect(camelOnlyDefault.btnInfoIsDisabled1).toBeDefined();
41+
expect(camelOnlyDefault.fooBar).toBeDefined();
42+
expect(camelOnlyDefault["btn-info_is-disabled"]).toBeUndefined();
43+
expect(camelOnlyDefault["btn--info_is-disabled_1"]).toBeUndefined();
44+
expect(camelOnlyDefault.foo_bar).toBeUndefined();
45+
expect(camelOnlyDefault.myBtnInfoIsDisabled).toBe("value");
46+
expect(camelOnlyDefault["my-btn-info_is-disabled"]).toBeUndefined();
47+
48+
// "dashes": dashes -> camelCase, underscores preserved, original kept
49+
expect(dashesDefault["btn-info_is-disabled"]).toBe(
50+
dashesDefault.btnInfo_isDisabled
51+
);
52+
expect(dashesDefault.foo_bar).toBeDefined();
53+
expect(dashesDefault["my-btn-info_is-disabled"]).toBe("value");
54+
expect(dashesDefault.myBtnInfo_isDisabled).toBe("value");
55+
56+
// "dashes-only": only the dashes-converted form is exported
57+
expect(dashesOnlyDefault.btnInfo_isDisabled).toBeDefined();
58+
expect(dashesOnlyDefault["btn-info_is-disabled"]).toBeUndefined();
59+
expect(dashesOnlyDefault.myBtnInfo_isDisabled).toBe("value");
60+
expect(dashesOnlyDefault["my-btn-info_is-disabled"]).toBeUndefined();
61+
});
62+
63+
it(`should concatenate every CSS module out of __webpack_modules__ (${matrixTitle})`, () => {
64+
const cssModuleKeys = Object.keys(__webpack_modules__).filter((k) =>
65+
k.startsWith("./style.module.css")
66+
);
67+
expect(cssModuleKeys).toEqual([]);
68+
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Entry for `parser.namedExports: true` configs. Pure named imports
2+
// keep every CSS export mangleable — a namespace import would mark
3+
// them all as observed-by-name and disable mangling.
4+
5+
import {
6+
simple as asIsSimple,
7+
foo_bar as asIsFooBar,
8+
foo as asIsFoo
9+
} from "./style.module.css?as-is";
10+
import {
11+
simple as camelCaseSimple,
12+
fooBar as camelCaseFooBar,
13+
btnInfoIsDisabled as camelCaseBtnInfoIsDisabled,
14+
myBtnInfoIsDisabled as camelCaseMyBtnInfoIsDisabled
15+
} from "./style.module.css?camel-case";
16+
import {
17+
simple as camelOnlySimple,
18+
fooBar as camelOnlyFooBar,
19+
btnInfoIsDisabled as camelOnlyBtnInfoIsDisabled,
20+
btnInfoIsDisabled1 as camelOnlyBtnInfoIsDisabled1
21+
} from "./style.module.css?camel-case-only";
22+
import {
23+
simple as dashesSimple,
24+
btnInfo_isDisabled as dashesBtnInfo_isDisabled
25+
} from "./style.module.css?dashes";
26+
import {
27+
simple as dashesOnlySimple,
28+
btnInfo_isDisabled as dashesOnlyBtnInfo_isDisabled
29+
} from "./style.module.css?dashes-only";
30+
31+
const outputModule = process.env.OUTPUT_MODULE === true;
32+
const matrixTitle = `output.module=${process.env.OUTPUT_MODULE}, namedExports=true`;
33+
34+
it(`should resolve every named import per convention (${matrixTitle})`, () => {
35+
expect(typeof asIsSimple).toBe("string");
36+
expect(typeof asIsFooBar).toBe("string");
37+
expect(asIsFoo).toBe("bar");
38+
39+
expect(typeof camelCaseSimple).toBe("string");
40+
expect(typeof camelCaseFooBar).toBe("string");
41+
expect(typeof camelCaseBtnInfoIsDisabled).toBe("string");
42+
expect(camelCaseMyBtnInfoIsDisabled).toBe("value");
43+
44+
expect(typeof camelOnlySimple).toBe("string");
45+
expect(typeof camelOnlyFooBar).toBe("string");
46+
expect(typeof camelOnlyBtnInfoIsDisabled).toBe("string");
47+
expect(typeof camelOnlyBtnInfoIsDisabled1).toBe("string");
48+
49+
expect(typeof dashesSimple).toBe("string");
50+
expect(typeof dashesBtnInfo_isDisabled).toBe("string");
51+
52+
expect(typeof dashesOnlySimple).toBe("string");
53+
expect(typeof dashesOnlyBtnInfo_isDisabled).toBe("string");
54+
});
55+
56+
it(`should concatenate every CSS module out of __webpack_modules__ (${matrixTitle})`, () => {
57+
// `concatenateModules: true` plus only-static imports must inline
58+
// every CSS variant into the entry scope, so none of them remain
59+
// as their own runtime modules.
60+
const cssModuleKeys = Object.keys(__webpack_modules__).filter((k) =>
61+
k.startsWith("./style.module.css")
62+
);
63+
expect(cssModuleKeys).toEqual([]);
64+
});
65+
66+
if (!outputModule) {
67+
// CJS bundle — `__dirname` and `__non_webpack_require__("fs")` are
68+
// straightforward. Skip the source-text checks for ESM output to
69+
// avoid wiring up an `import.meta.url`-based __dirname polyfill;
70+
// the mangling/concatenation behavior we assert is identical
71+
// across output modes.
72+
it(`should not leave EXTERNAL MODULE markers for CSS in the bundle (${matrixTitle})`, () => {
73+
const fs = __non_webpack_require__("fs");
74+
const source = fs.readFileSync(`${__dirname}/bundle0.js`, "utf-8");
75+
76+
for (const convention of [
77+
"as-is",
78+
"camel-case",
79+
"camel-case-only",
80+
"dashes",
81+
"dashes-only"
82+
]) {
83+
expect(source).not.toContain(
84+
`EXTERNAL MODULE: css ./style.module.css?${convention}`
85+
);
86+
expect(source).not.toContain(
87+
`__webpack_require__("./style.module.css?${convention}")`
88+
);
89+
}
90+
});
91+
92+
it(`should mangle JS export identifiers in production (${matrixTitle})`, () => {
93+
const fs = __non_webpack_require__("fs");
94+
const source = fs.readFileSync(`${__dirname}/bundle0.js`, "utf-8");
95+
96+
// When CSS modules are concatenated, every named export becomes a
97+
// `const <identifier> = <value>;` declaration in the entry scope
98+
// (CssGenerator.js#L472–497). With
99+
// `mangleExports: "deterministic"`, that identifier is the mangled
100+
// used-name, so the original long names must NOT appear as
101+
// const/let/var bindings.
102+
const longCssExportNames = [
103+
"btnInfoIsDisabled",
104+
"btnInfoIsDisabled1",
105+
"btnInfo_isDisabled",
106+
"myBtnInfoIsDisabled",
107+
"fooBar"
108+
];
109+
for (const name of longCssExportNames) {
110+
const declRegex = new RegExp(`(?:const|let|var)\\s+${name}\\b`);
111+
expect(source).not.toMatch(declRegex);
112+
}
113+
});
114+
}

test/configCases/css/named-exports-mangling/index.js

Lines changed: 0 additions & 118 deletions
This file was deleted.

test/configCases/css/named-exports-mangling/re-exports.js

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)