Skip to content

Commit

Permalink
feat: reference style modules as URL assets (#4016)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjiahan authored Nov 19, 2024
1 parent a5a7361 commit 8594f32
Show file tree
Hide file tree
Showing 21 changed files with 293 additions and 127 deletions.
49 changes: 49 additions & 0 deletions e2e/cases/assets/styles-as-assets/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { build, rspackOnlyTest } from '@e2e/helper';
import { expect } from '@playwright/test';

rspackOnlyTest(
'should allow to use `new URL` to reference styles as assets',
async ({ page }) => {
const rsbuild = await build({
cwd: __dirname,
page,
});

const files = await rsbuild.unwrapOutputJSON();
const filenames = Object.keys(files);

const test1 = filenames.find((filename) =>
filename.includes('dist/static/assets/test1.css'),
);
const test2 = filenames.find((filename) =>
filename.includes('dist/static/assets/test2.less'),
);
const test3 = filenames.find((filename) =>
filename.includes('dist/static/assets/test3.scss'),
);
const test4 = filenames.find((filename) =>
filename.includes('dist/static/assets/test4.styl'),
);

expect(test1).toBeDefined();
expect(test2).toBeDefined();
expect(test3).toBeDefined();
expect(test4).toBeDefined();
expect(files[test1!]).toContain('body{color:red}');
expect(files[test2!]).toContain('& .foo');
expect(files[test3!]).toContain('& .foo');
expect(files[test4!]).toContain('& .foo');
expect(await page.evaluate('window.test1')).toBe(
`http://localhost:${rsbuild.port}/static/assets/test1.css`,
);
expect(await page.evaluate('window.test2')).toBe(
`http://localhost:${rsbuild.port}/static/assets/test2.less`,
);
expect(await page.evaluate('window.test3')).toBe(
`http://localhost:${rsbuild.port}/static/assets/test3.scss`,
);
expect(await page.evaluate('window.test4')).toBe(
`http://localhost:${rsbuild.port}/static/assets/test4.styl`,
);
},
);
11 changes: 11 additions & 0 deletions e2e/cases/assets/styles-as-assets/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from '@rsbuild/core';
import { pluginLess } from '@rsbuild/plugin-less';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginStylus } from '@rsbuild/plugin-stylus';

export default defineConfig({
plugins: [pluginLess(), pluginSass(), pluginStylus()],
output: {
filenameHash: false,
},
});
4 changes: 4 additions & 0 deletions e2e/cases/assets/styles-as-assets/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
window.test1 = new URL('./test1.css', import.meta.url).href;
window.test2 = new URL('./test2.less', import.meta.url).href;
window.test3 = new URL('./test3.scss', import.meta.url).href;
window.test4 = new URL('./test4.styl', import.meta.url).href;
3 changes: 3 additions & 0 deletions e2e/cases/assets/styles-as-assets/src/test1.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
color: red;
}
5 changes: 5 additions & 0 deletions e2e/cases/assets/styles-as-assets/src/test2.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
body {
& .foo {
color: blue;
}
}
5 changes: 5 additions & 0 deletions e2e/cases/assets/styles-as-assets/src/test3.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
body {
& .foo {
color: green;
}
}
5 changes: 5 additions & 0 deletions e2e/cases/assets/styles-as-assets/src/test4.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
body {
& .foo {
color: yellow;
}
}
12 changes: 12 additions & 0 deletions packages/compat/webpack/tests/__snapshots__/default.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ exports[`applyDefaultPlugins > should apply default plugins correctly 1`] = `
"test": /\\\\\\.m\\?js/,
},
{
"dependency": {
"not": "url",
},
"resolve": {
"preferRelative": true,
},
Expand Down Expand Up @@ -428,6 +431,9 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when produ
"test": /\\\\\\.m\\?js/,
},
{
"dependency": {
"not": "url",
},
"resolve": {
"preferRelative": true,
},
Expand Down Expand Up @@ -808,6 +814,9 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe
"test": /\\\\\\.m\\?js/,
},
{
"dependency": {
"not": "url",
},
"resolve": {
"preferRelative": true,
},
Expand Down Expand Up @@ -1135,6 +1144,9 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe
"test": /\\\\\\.m\\?js/,
},
{
"dependency": {
"not": "url",
},
"resolve": {
"preferRelative": true,
},
Expand Down
230 changes: 106 additions & 124 deletions packages/core/src/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import { getCssExtractPlugin } from '../pluginHelper';
import type {
CSSLoaderModulesMode,
CSSLoaderOptions,
ModifyChainUtils,
NormalizedEnvironmentConfig,
PostCSSLoaderOptions,
PostCSSOptions,
RsbuildContext,
RsbuildPlugin,
Rspack,
RspackChain,
} from '../types';

const getCSSModulesLocalIdentName = (
Expand Down Expand Up @@ -229,138 +226,123 @@ const getCSSLoaderOptions = ({
return cssLoaderOptions;
};

async function applyCSSRule({
rule,
config,
context,
utils: { target, isProd, CHAIN_ID, environment },
}: {
rule: RspackChain.Rule;
config: NormalizedEnvironmentConfig;
context: RsbuildContext;
utils: ModifyChainUtils;
}) {
const emitCss = config.output.emitCss ?? target === 'web';

// Create Rspack rule
// Order: style-loader/CssExtractRspackPlugin -> css-loader -> postcss-loader
if (emitCss) {
// use style-loader
if (config.output.injectStyles) {
const styleLoaderOptions = reduceConfigs({
initial: {},
config: config.tools.styleLoader,
});
rule
.use(CHAIN_ID.USE.STYLE)
.loader(getCompiledPath('style-loader'))
.options(styleLoaderOptions);
}
// use CssExtractRspackPlugin loader
else {
rule
.use(CHAIN_ID.USE.MINI_CSS_EXTRACT)
.loader(getCssExtractPlugin().loader)
.options(config.tools.cssExtract.loaderOptions);
}
} else {
rule
.use(CHAIN_ID.USE.IGNORE_CSS)
.loader(path.join(LOADER_PATH, 'ignoreCssLoader.cjs'));
}

// Number of loaders applied before css-loader for `@import` at-rules
let importLoaders = 0;

rule.use(CHAIN_ID.USE.CSS).loader(getCompiledPath('css-loader'));

if (emitCss) {
// `builtin:lightningcss-loader` is not supported when using webpack
if (
context.bundlerType === 'rspack' &&
config.tools.lightningcssLoader !== false
) {
importLoaders++;

const userOptions =
config.tools.lightningcssLoader === true
? {}
: config.tools.lightningcssLoader;

const initialOptions: Rspack.LightningcssLoaderOptions = {
targets: environment.browserslist,
};

if (config.mode === 'production' && config.output.injectStyles) {
initialOptions.minify = true;
}

const loaderOptions = reduceConfigs<Rspack.LightningcssLoaderOptions>({
initial: initialOptions,
config: userOptions,
});

rule
.use(CHAIN_ID.USE.LIGHTNINGCSS)
.loader('builtin:lightningcss-loader')
.options(loaderOptions);
}

const postcssLoaderOptions = await getPostcssLoaderOptions({
config,
root: context.rootPath,
});

// enable postcss-loader if using PostCSS plugins
if (
typeof postcssLoaderOptions.postcssOptions === 'function' ||
postcssLoaderOptions.postcssOptions?.plugins?.length
) {
importLoaders++;
rule
.use(CHAIN_ID.USE.POSTCSS)
.loader(getCompiledPath('postcss-loader'))
.options(postcssLoaderOptions);
}
}

const localIdentName = getCSSModulesLocalIdentName(config, isProd);
const cssLoaderOptions = getCSSLoaderOptions({
config,
importLoaders,
localIdentName,
emitCss,
});
rule.use(CHAIN_ID.USE.CSS).options(cssLoaderOptions);

// CSS imports should always be treated as sideEffects
rule.merge({ sideEffects: true });

// Enable preferRelative by default, which is consistent with the default behavior of css-loader
// see: https://github.com/webpack-contrib/css-loader/blob/579fc13/src/plugins/postcss-import-parser.js#L234
rule.resolve.preferRelative(true);
}

export const pluginCss = (): RsbuildPlugin => ({
name: 'rsbuild:css',
setup(api) {
api.modifyBundlerChain({
order: 'pre',
handler: async (chain, utils) => {
const rule = chain.module.rule(utils.CHAIN_ID.RULE.CSS);
const { config } = utils.environment;
handler: async (chain, { target, isProd, CHAIN_ID, environment }) => {
const rule = chain.module.rule(CHAIN_ID.RULE.CSS);
const { config } = environment;

rule
.test(CSS_REGEX)
// specify type to allow enabling Rspack `experiments.css`
.type('javascript/auto');

await applyCSSRule({
rule,
utils,
.type('javascript/auto')
// When using `new URL('./path/to/foo.css', import.meta.url)`,
// the module should be treated as an asset module rather than a JS module.
.dependency({ not: 'url' });

const emitCss = config.output.emitCss ?? target === 'web';

// Create Rspack rule
// Order: style-loader/CssExtractRspackPlugin -> css-loader -> postcss-loader
if (emitCss) {
// use style-loader
if (config.output.injectStyles) {
const styleLoaderOptions = reduceConfigs({
initial: {},
config: config.tools.styleLoader,
});
rule
.use(CHAIN_ID.USE.STYLE)
.loader(getCompiledPath('style-loader'))
.options(styleLoaderOptions);
}
// use CssExtractRspackPlugin loader
else {
rule
.use(CHAIN_ID.USE.MINI_CSS_EXTRACT)
.loader(getCssExtractPlugin().loader)
.options(config.tools.cssExtract.loaderOptions);
}
} else {
rule
.use(CHAIN_ID.USE.IGNORE_CSS)
.loader(path.join(LOADER_PATH, 'ignoreCssLoader.cjs'));
}

// Number of loaders applied before css-loader for `@import` at-rules
let importLoaders = 0;

rule.use(CHAIN_ID.USE.CSS).loader(getCompiledPath('css-loader'));

if (emitCss) {
// `builtin:lightningcss-loader` is not supported when using webpack
if (
api.context.bundlerType === 'rspack' &&
config.tools.lightningcssLoader !== false
) {
importLoaders++;

const userOptions =
config.tools.lightningcssLoader === true
? {}
: config.tools.lightningcssLoader;

const initialOptions: Rspack.LightningcssLoaderOptions = {
targets: environment.browserslist,
};

if (config.mode === 'production' && config.output.injectStyles) {
initialOptions.minify = true;
}

const loaderOptions =
reduceConfigs<Rspack.LightningcssLoaderOptions>({
initial: initialOptions,
config: userOptions,
});

rule
.use(CHAIN_ID.USE.LIGHTNINGCSS)
.loader('builtin:lightningcss-loader')
.options(loaderOptions);
}

const postcssLoaderOptions = await getPostcssLoaderOptions({
config,
root: api.context.rootPath,
});

// enable postcss-loader if using PostCSS plugins
if (
typeof postcssLoaderOptions.postcssOptions === 'function' ||
postcssLoaderOptions.postcssOptions?.plugins?.length
) {
importLoaders++;
rule
.use(CHAIN_ID.USE.POSTCSS)
.loader(getCompiledPath('postcss-loader'))
.options(postcssLoaderOptions);
}
}

const localIdentName = getCSSModulesLocalIdentName(config, isProd);
const cssLoaderOptions = getCSSLoaderOptions({
config,
context: api.context,
importLoaders,
localIdentName,
emitCss,
});
rule.use(CHAIN_ID.USE.CSS).options(cssLoaderOptions);

// CSS imports should always be treated as sideEffects
rule.merge({ sideEffects: true });

// Enable preferRelative by default, which is consistent with the default behavior of css-loader
// see: https://github.com/webpack-contrib/css-loader/blob/579fc13/src/plugins/postcss-import-parser.js#L234
rule.resolve.preferRelative(true);
},
});
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/tests/__snapshots__/builder.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ exports[`should use Rspack as the default bundler > apply Rspack correctly 1`] =
"test": /\\\\\\.m\\?js/,
},
{
"dependency": {
"not": "url",
},
"resolve": {
"preferRelative": true,
},
Expand Down
Loading

0 comments on commit 8594f32

Please sign in to comment.