Skip to content

Commit e3f4857

Browse files
committed
fix(core): improve builtin CSS compatibility
1 parent b630beb commit e3f4857

6 files changed

Lines changed: 106 additions & 6 deletions

File tree

e2e/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pnpm e2e
1717

1818
# Run specific test case, such as "css"
1919
pnpm e2e css
20+
21+
# Run CSS cases with pluginRspackBuiltinCss
22+
pnpm e2e:css-builtin
2023
```
2124

2225
## Debugging

e2e/cases/css/rspack-builtin-css/index.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ test('should use Rspack built-in CSS', async ({ build }) => {
1717
rsbuild.expectNoLog(COMPILE_WARNING);
1818
});
1919

20-
test('should use built-in style injection', async ({ build }) => {
21-
const rsbuild = await build({
20+
test('should use built-in style injection', async ({ page, buildPreview }) => {
21+
const rsbuild = await buildPreview({
2222
config: {
2323
output: {
2424
injectStyles: true,
@@ -29,6 +29,18 @@ test('should use built-in style injection', async ({ build }) => {
2929
const files = rsbuild.getDistFiles();
3030
const content = getFileContent(files, 'index.js');
3131
expect(content).toContain('color:red');
32+
await expect
33+
.poll(() =>
34+
page.evaluate(
35+
() =>
36+
(
37+
globalThis as typeof globalThis & {
38+
testStyles: { less: string; sass: string };
39+
}
40+
).testStyles,
41+
),
42+
)
43+
.toMatchObject({ less: expect.any(String), sass: expect.any(String) });
3244

3345
// should have no warnings
3446
rsbuild.expectNoLog(COMPILE_WARNING);

e2e/helper/jsApi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ const updateConfigForTest = async (originalConfig: RsbuildConfig, cwd: string =
2222
});
2323
const cwdPath = toPosixPath(cwd);
2424
const useRspackBuiltinCss =
25-
cwdPath.includes('/e2e/cases/css/') && !cwdPath.endsWith('/rspack-builtin-css');
25+
process.env.RSBUILD_E2E_RSPACK_BUILTIN_CSS === '1' &&
26+
cwdPath.includes('/e2e/cases/css/') &&
27+
!cwdPath.endsWith('/rspack-builtin-css');
2628

2729
const baseConfig: RsbuildConfig = {
2830
...(useRspackBuiltinCss ? { plugins: [pluginRspackBuiltinCss()] } : {}),

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"check-spell": "pnpx cspell && heading-case",
1212
"doc": "cd website && node --run dev",
1313
"e2e": "cd ./e2e && pnpm e2e",
14+
"e2e:css-builtin": "cross-env RSBUILD_E2E_RSPACK_BUILTIN_CSS=1 pnpm e2e css",
1415
"format": "oxfmt . && heading-case --write",
1516
"format:check": "oxfmt . --check",
1617
"lint": "rslint --type-check",

packages/core/src/plugins/rspackBuiltinCss.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,37 @@ const getExportsConvention = (
5454
return conventionMap[convention] ?? 'camel-case';
5555
};
5656

57+
const LOCAL_IDENT_HASH_REGEX =
58+
/\[(?:([^:\]]+):)?(?:(hash|contenthash|fullhash))(?::([a-z]+\d*))?(?::(\d+))?\]/i;
59+
const LOCAL_IDENT_HASH_REGEX_GLOBAL =
60+
/\[(?:([^:\]]+):)?(?:hash|contenthash|fullhash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi;
61+
62+
const getLocalIdentOptions = (
63+
localIdentName: string | undefined,
64+
): Partial<Rspack.CssModuleGeneratorOptions> => {
65+
if (localIdentName === undefined) {
66+
return {};
67+
}
68+
69+
const match = localIdentName.match(LOCAL_IDENT_HASH_REGEX);
70+
if (!match) {
71+
return { localIdentName };
72+
}
73+
74+
const [, hashFunction, , hashDigest, hashDigestLength] = match;
75+
76+
return {
77+
localIdentName: localIdentName.replace(
78+
LOCAL_IDENT_HASH_REGEX_GLOBAL,
79+
(_match, _hashFunction, currentHashName) =>
80+
currentHashName === 'fullhash' ? '[fullhash]' : '[hash]',
81+
),
82+
...(hashFunction ? { localIdentHashFunction: hashFunction } : {}),
83+
...(hashDigest ? { localIdentHashDigest: hashDigest } : {}),
84+
...(hashDigestLength ? { localIdentHashDigestLength: Number(hashDigestLength) } : {}),
85+
};
86+
};
87+
5788
const getResolveConfig = (rule: RspackChain.Rule<unknown>) =>
5889
(
5990
rule.resolve as RspackChain.Rule<unknown>['resolve'] & {
@@ -111,11 +142,17 @@ const applyCssModuleConfig = ({
111142
};
112143
const generatorOptions: Rspack.CssModuleGeneratorOptions = {
113144
...options.generator,
145+
// Rspack currently generates an invalid `exports` reference when style exports
146+
// with default CSS Module exports are concatenated. CommonJS output prevents
147+
// concatenation for these modules while preserving Rsbuild's default exports.
148+
...(config.output.injectStyles &&
149+
!cssModules.namedExport &&
150+
options.generator?.esModule === undefined
151+
? { esModule: false }
152+
: {}),
114153
exportsOnly: !emitCss,
115154
exportsConvention: getExportsConvention(cssModules.exportLocalsConvention),
116-
...(cssModules.localIdentName === undefined
117-
? {}
118-
: { localIdentName: cssModules.localIdentName }),
155+
...getLocalIdentOptions(cssModules.localIdentName),
119156
};
120157

121158
chain.module.parser.merge({

packages/core/tests/css.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,51 @@ describe('plugin-rspack-builtin-css', () => {
164164
expect(rspackConfig.module?.generator?.['css/auto']).not.toHaveProperty('localIdentName');
165165
});
166166

167+
it('should preserve default CSS Module exports when injecting styles', async () => {
168+
const rsbuild = await createRsbuild({
169+
config: {
170+
output: {
171+
injectStyles: true,
172+
},
173+
plugins: [pluginRspackBuiltinCss()],
174+
},
175+
});
176+
rstest.spyOn(rsbuild.logger, 'warn').mockImplementation(() => {});
177+
178+
const [rspackConfig] = await rsbuild.initConfigs();
179+
180+
expect(rspackConfig.module?.parser?.['css/auto']).toMatchObject({
181+
exportType: 'style',
182+
namedExports: false,
183+
});
184+
expect(rspackConfig.module?.generator?.['css/auto']).toMatchObject({
185+
esModule: false,
186+
});
187+
});
188+
189+
it('should translate hash options from cssModules.localIdentName', async () => {
190+
const rsbuild = await createRsbuild({
191+
config: {
192+
output: {
193+
cssModules: {
194+
localIdentName: '[name]__[local]--[sha256:hash:hex:4]',
195+
},
196+
},
197+
plugins: [pluginRspackBuiltinCss()],
198+
},
199+
});
200+
rstest.spyOn(rsbuild.logger, 'warn').mockImplementation(() => {});
201+
202+
const [rspackConfig] = await rsbuild.initConfigs();
203+
204+
expect(rspackConfig.module?.generator?.['css/auto']).toMatchObject({
205+
localIdentHashDigest: 'hex',
206+
localIdentHashDigestLength: 4,
207+
localIdentHashFunction: 'sha256',
208+
localIdentName: '[name]__[local]--[hash]',
209+
});
210+
});
211+
167212
it('should warn when CSS ?url imports are unsupported', async () => {
168213
const rsbuild = await createRsbuild({
169214
config: {

0 commit comments

Comments
 (0)