Skip to content

Commit e133bd9

Browse files
committed
feat(core): support Vue with builtin CSS
1 parent e3f4857 commit e133bd9

7 files changed

Lines changed: 109 additions & 39 deletions

File tree

packages/core/src/plugins/rspackBuiltinCss.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const PLUGIN_RSPACK_BUILTIN_CSS_NAME = 'rsbuild:rspack-builtin-css';
88

99
type CssRuleType = 'css' | 'css/auto' | 'css/global' | 'css/module';
1010

11+
const CSS_MODULE_QUERY_REGEX = /[?&]module(?:&|=|$)/;
12+
1113
/**
1214
* Options for Rspack's built-in CSS parser that are not covered by
1315
* `output.cssModules`.
@@ -211,7 +213,6 @@ export const pluginRspackBuiltinCss = (
211213
let hasWarnedExportGlobals = false;
212214
let hasWarnedMode = false;
213215
let hasWarnedUrl = false;
214-
let hasWarnedVue = false;
215216

216217
api.modifyBundlerChain({
217218
order: 'post',
@@ -237,16 +238,6 @@ export const pluginRspackBuiltinCss = (
237238
'`output.cssModules.exportGlobals` is not supported by `pluginRspackBuiltinCss`. The value will be ignored.',
238239
);
239240
}
240-
if (
241-
!hasWarnedVue &&
242-
auto !== false &&
243-
api.isPluginExists('rsbuild:vue', { environment: environment.name })
244-
) {
245-
hasWarnedVue = true;
246-
api.logger.warn(
247-
'Vue SFC CSS Modules (`<style module>`) are not supported by `pluginRspackBuiltinCss`.',
248-
);
249-
}
250241
if (!hasWarnedUrl) {
251242
hasWarnedUrl = true;
252243
api.logger.warn(
@@ -285,6 +276,14 @@ export const pluginRspackBuiltinCss = (
285276
const isInline = String(branch.get('resourceQuery')) === String(INLINE_QUERY_REGEX);
286277

287278
if (!isInline && auto !== false) {
279+
const queryModuleRule = rule.oneOf(`${name}-query-module`).before(name);
280+
cloneRule({
281+
source: branch,
282+
target: queryModuleRule,
283+
legacyUseIds,
284+
});
285+
queryModuleRule.resourceQuery(CSS_MODULE_QUERY_REGEX).type(moduleRuleType);
286+
288287
const moduleRule = rule.oneOf(`${name}-module`).before(name);
289288
cloneRule({
290289
source: branch,

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,28 @@ exports[`plugin-rspack-builtin-css > should replace the default CSS pipeline and
433433
"resourceQuery": /\\[\\?&\\]raw\\(\\?:&\\|=\\|\\$\\)/,
434434
"type": "asset/source",
435435
},
436+
{
437+
"resolve": {
438+
"preferRelative": true,
439+
},
440+
"resourceQuery": /\\[\\?&\\]module\\(\\?:&\\|=\\|\\$\\)/,
441+
"sideEffects": true,
442+
"type": "css/global",
443+
"use": [
444+
{
445+
"loader": "builtin:lightningcss-loader",
446+
"options": {
447+
"errorRecovery": true,
448+
"targets": [
449+
"chrome >= 107",
450+
"edge >= 107",
451+
"firefox >= 104",
452+
"safari >= 16",
453+
],
454+
},
455+
},
456+
],
457+
},
436458
{
437459
"resolve": {
438460
"preferRelative": true,

packages/core/tests/css.test.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ describe('plugin-rspack-builtin-css', () => {
366366
(rule.type === 'css/auto' && rule.parser?.exportType !== 'text'),
367367
);
368368

369-
expect(styleRules).toHaveLength(2);
369+
expect(styleRules).toHaveLength(3);
370370
expect(
371371
styleRules?.every((rule) =>
372372
rule.use?.some((use) =>
@@ -376,6 +376,45 @@ describe('plugin-rspack-builtin-css', () => {
376376
).toBe(true);
377377
});
378378

379+
it('should add a CSS Modules oneOf rule for resource queries', async () => {
380+
const rsbuild = await createRsbuild({
381+
config: {
382+
plugins: [pluginRspackBuiltinCss()],
383+
},
384+
});
385+
rstest.spyOn(rsbuild.logger, 'warn').mockImplementation(() => {});
386+
387+
const [rspackConfig] = await rsbuild.initConfigs();
388+
const rules = matchRules(rspackConfig, 'a.vue.css');
389+
const oneOf = (rules[0] as { oneOf?: Rspack.RuleSetRule[] }).oneOf;
390+
const inlineRuleIndex = oneOf?.findIndex(
391+
(rule) => String(rule.resourceQuery) === String(/[?&]inline(?:&|=|$)/),
392+
);
393+
const queryModuleRuleIndex = oneOf?.findIndex(
394+
(rule) => String(rule.resourceQuery) === String(/[?&]module(?:&|=|$)/),
395+
);
396+
const fallbackRuleIndex = oneOf?.findIndex(
397+
(rule) => rule.type === 'css/auto' && !rule.resourceQuery && !rule.test,
398+
);
399+
const queryModuleRule = oneOf?.[queryModuleRuleIndex ?? -1];
400+
401+
expect(oneOf?.[inlineRuleIndex ?? -1]).toMatchObject({
402+
parser: {
403+
exportType: 'text',
404+
},
405+
type: 'css/auto',
406+
});
407+
expect(queryModuleRule).toMatchObject({
408+
sideEffects: true,
409+
type: 'css/module',
410+
});
411+
expect(
412+
(queryModuleRule?.resourceQuery as RegExp).test('?vue&type=style&module=&lang=css'),
413+
).toBe(true);
414+
expect(inlineRuleIndex).toBeLessThan(queryModuleRuleIndex!);
415+
expect(queryModuleRuleIndex).toBeLessThan(fallbackRuleIndex!);
416+
});
417+
379418
it('should warn about unsupported CSS Modules options', async () => {
380419
const rsbuild = await createRsbuild({
381420
config: {
@@ -403,27 +442,6 @@ describe('plugin-rspack-builtin-css', () => {
403442
'`output.cssModules.exportGlobals` is not supported by `pluginRspackBuiltinCss`. The value will be ignored.',
404443
);
405444
});
406-
407-
it('should warn when used with the Vue plugin', async () => {
408-
const rsbuild = await createRsbuild({
409-
config: {
410-
plugins: [
411-
{
412-
name: 'rsbuild:vue',
413-
setup() {},
414-
},
415-
pluginRspackBuiltinCss(),
416-
],
417-
},
418-
});
419-
const warn = rstest.spyOn(rsbuild.logger, 'warn').mockImplementation(() => {});
420-
421-
await rsbuild.initConfigs();
422-
423-
expect(warn).toHaveBeenCalledWith(
424-
'Vue SFC CSS Modules (`<style module>`) are not supported by `pluginRspackBuiltinCss`.',
425-
);
426-
});
427445
});
428446

429447
describe('plugin-css injectStyles', () => {

packages/plugin-vue/src/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { createRequire } from 'node:module';
2-
import type { EnvironmentConfig, RsbuildPlugin, Rspack } from '@rsbuild/core';
2+
import {
3+
type EnvironmentConfig,
4+
PLUGIN_RSPACK_BUILTIN_CSS_NAME,
5+
type RsbuildPlugin,
6+
type Rspack,
7+
} from '@rsbuild/core';
38
import { type VueLoaderOptions, VueLoaderPlugin } from 'rspack-vue-loader';
49
import { applySplitChunksRule } from './splitChunks.js';
510

@@ -55,7 +60,7 @@ export function pluginVue(options: PluginVueOptions = {}): RsbuildPlugin {
5560
const { test = /\.vue$/ } = options;
5661
const CSS_MODULES_REGEX = /\.modules?\.\w+$/i;
5762

58-
api.modifyEnvironmentConfig((config, { mergeEnvironmentConfig }) => {
63+
api.modifyEnvironmentConfig((config, { mergeEnvironmentConfig, name }) => {
5964
const extraConfig: EnvironmentConfig = {
6065
source: {
6166
define: {
@@ -72,7 +77,10 @@ export function pluginVue(options: PluginVueOptions = {}): RsbuildPlugin {
7277
const merged = mergeEnvironmentConfig(extraConfig, config);
7378

7479
// Support `<style module>`, `<style module="customName">` in Vue SFC
75-
if (merged.output.cssModules.auto === true) {
80+
if (
81+
merged.output.cssModules.auto === true &&
82+
!api.isPluginExists(PLUGIN_RSPACK_BUILTIN_CSS_NAME, { environment: name })
83+
) {
7684
merged.output.cssModules.auto = (path, query) => {
7785
// For Vue style block, the path might be like:
7886
// 1. `/path/to/Foo.vue`

packages/plugin-vue/tests/index.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createRsbuild } from '@rsbuild/core';
1+
import { createRsbuild, pluginRspackBuiltinCss, type Rspack } from '@rsbuild/core';
22
import { matchPlugin, matchRules } from '@scripts/test-helper';
33
import { pluginVue } from '../src';
44

@@ -42,6 +42,29 @@ describe('plugin-vue', () => {
4242
expect(matchPlugin(config[0], 'DefinePlugin')).toMatchSnapshot();
4343
});
4444

45+
it('should support Rspack built-in CSS rules', async () => {
46+
const rsbuild = await createRsbuild({
47+
config: {
48+
plugins: [pluginVue(), pluginRspackBuiltinCss()],
49+
},
50+
});
51+
const warn = rstest.spyOn(rsbuild.logger, 'warn').mockImplementation(() => {});
52+
53+
const [config] = await rsbuild.initConfigs();
54+
const cssRule = matchRules(config, 'a.vue.css')[0] as {
55+
oneOf?: Rspack.RuleSetRule[];
56+
};
57+
58+
expect(
59+
cssRule.oneOf?.some(
60+
(rule) =>
61+
rule.type === 'css/module' &&
62+
String(rule.resourceQuery) === String(/[?&]module(?:&|=|$)/),
63+
),
64+
).toBe(true);
65+
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('output.cssModules.auto'));
66+
});
67+
4568
it('should allow to custom test condition', async () => {
4669
const rsbuild = await createRsbuild({
4770
config: {

website/docs/en/guide/styling/rspack-builtin-css.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ The `parser` option supports `exportType`, `url`, `import`, `resolveImport`, `an
5656

5757
The `generator` option supports `esModule`, `localIdentHashDigest`, `localIdentHashDigestLength`, `localIdentHashFunction`, and `localIdentHashSalt`.
5858

59-
CSS `?url` imports and Vue SFC CSS Modules (`<style module>`) are also not supported.
59+
CSS `?url` imports are not supported.

website/docs/zh/guide/styling/rspack-builtin-css.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ export default {
5656

5757
`generator` 支持 `esModule``localIdentHashDigest``localIdentHashDigestLength``localIdentHashFunction``localIdentHashSalt`
5858

59-
暂不支持 CSS `?url` 导入和 Vue SFC CSS Modules(`<style module>`
59+
暂不支持 CSS `?url` 导入

0 commit comments

Comments
 (0)