Skip to content

Commit

Permalink
feat: add new output.emitCss config (#3967)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjiahan authored Nov 14, 2024
1 parent 425b3fc commit 917318d
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 96 deletions.
105 changes: 105 additions & 0 deletions e2e/cases/css/emit-css/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { build } from '@e2e/helper';
import { expect, test } from '@playwright/test';

test('should not emit CSS files when build node target', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
output: {
target: 'node',
},
},
});
const files = await rsbuild.unwrapOutputJSON();

// preserve CSS Modules mapping
const jsContent =
files[Object.keys(files).find((file) => file.endsWith('.js'))!];
expect(jsContent.includes('"title-class":')).toBeTruthy();

const cssFiles = Object.keys(files).filter((file) => file.endsWith('.css'));
expect(cssFiles).toHaveLength(0);
});

test('should allow to emit CSS with output.emitCss when build node target', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
output: {
target: 'node',
emitCss: true,
},
},
});
const files = await rsbuild.unwrapOutputJSON();

// preserve CSS Modules mapping
const jsContent =
files[Object.keys(files).find((file) => file.endsWith('.js'))!];
expect(jsContent.includes('"title-class":')).toBeTruthy();

const cssFiles = Object.keys(files).filter((file) => file.endsWith('.css'));
expect(cssFiles).toHaveLength(1);
});

test('should not emit CSS files when build web-worker target', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
output: {
target: 'web-worker',
},
},
});
const files = await rsbuild.unwrapOutputJSON();

// preserve CSS Modules mapping
const jsContent =
files[Object.keys(files).find((file) => file.endsWith('.js'))!];
expect(jsContent.includes('"title-class":')).toBeTruthy();

const cssFiles = Object.keys(files).filter((file) => file.endsWith('.css'));
expect(cssFiles).toHaveLength(0);
});

test('should allow to emit CSS with output.emitCss when build web-worker target', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
output: {
target: 'web-worker',
emitCss: true,
},
},
});
const files = await rsbuild.unwrapOutputJSON();

// preserve CSS Modules mapping
const jsContent =
files[Object.keys(files).find((file) => file.endsWith('.js'))!];
expect(jsContent.includes('"title-class":')).toBeTruthy();

const cssFiles = Object.keys(files).filter((file) => file.endsWith('.css'));
expect(cssFiles).toHaveLength(1);
});

test('should allow to disable CSS emit with output.emitCss when build web target', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
output: {
target: 'web',
emitCss: false,
},
},
});
const files = await rsbuild.unwrapOutputJSON();

// preserve CSS Modules mapping
const jsContent =
files[Object.keys(files).find((file) => file.endsWith('.js'))!];
expect(jsContent.includes('"title-class":')).toBeTruthy();

const cssFiles = Object.keys(files).filter((file) => file.endsWith('.css'));
expect(cssFiles).toHaveLength(0);
});
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './a.css';
import style from './b.module.scss';
import style from './b.module.css';

console.log(style);
44 changes: 0 additions & 44 deletions e2e/cases/css/ignore-css/removeCss.test.ts

This file was deleted.

5 changes: 0 additions & 5 deletions e2e/cases/css/ignore-css/rsbuild.config.ts

This file was deleted.

12 changes: 0 additions & 12 deletions e2e/cases/css/ignore-css/tsconfig.json

This file was deleted.

39 changes: 15 additions & 24 deletions packages/core/src/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,10 @@ import type {
PostCSSOptions,
RsbuildContext,
RsbuildPlugin,
RsbuildTarget,
Rspack,
RspackChain,
} from '../types';

export const isUseCssExtract = (
config: NormalizedEnvironmentConfig,
target: RsbuildTarget,
): boolean =>
!config.output.injectStyles && target !== 'node' && target !== 'web-worker';

const getCSSModulesLocalIdentName = (
config: NormalizedEnvironmentConfig,
isProd: boolean,
Expand Down Expand Up @@ -203,13 +196,13 @@ const getPostcssLoaderOptions = async ({
const getCSSLoaderOptions = ({
config,
importLoaders,
target,
localIdentName,
emitCss,
}: {
config: NormalizedEnvironmentConfig;
importLoaders: number;
target: RsbuildTarget;
localIdentName: string;
emitCss: boolean;
}) => {
const { cssModules } = config.output;

Expand All @@ -230,7 +223,7 @@ const getCSSLoaderOptions = ({

const cssLoaderOptions = normalizeCssLoaderOptions(
mergedCssLoaderOptions,
target !== 'web',
!emitCss,
);

return cssLoaderOptions;
Expand All @@ -247,31 +240,29 @@ async function applyCSSRule({
context: RsbuildContext;
utils: ModifyChainUtils;
}) {
// Check user config
const enableExtractCSS = isUseCssExtract(config, target);
const emitCss = config.output.emitCss ?? target === 'web';

// Create Rspack rule
// Order: style-loader/CssExtractRspackPlugin -> css-loader -> postcss-loader
if (target === 'web') {
// use CssExtractRspackPlugin loader
if (enableExtractCSS) {
rule
.use(CHAIN_ID.USE.MINI_CSS_EXTRACT)
.loader(getCssExtractPlugin().loader)
.options(config.tools.cssExtract.loaderOptions);
}
if (emitCss) {
// use style-loader
else {
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)
Expand All @@ -283,7 +274,7 @@ async function applyCSSRule({

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

if (target === 'web') {
if (emitCss) {
// `builtin:lightningcss-loader` is not supported when using webpack
if (
context.bundlerType === 'rspack' &&
Expand Down Expand Up @@ -337,8 +328,8 @@ async function applyCSSRule({
const cssLoaderOptions = getCSSLoaderOptions({
config,
importLoaders,
target,
localIdentName,
emitCss,
});
rule.use(CHAIN_ID.USE.CSS).options(cssLoaderOptions);

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/plugins/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
RsbuildContext,
RsbuildPlugin,
} from '../types';
import { isUseCssExtract } from './css';

function getPublicPath({
isProd,
Expand Down Expand Up @@ -141,7 +140,8 @@ export const pluginOutput = (): RsbuildPlugin => ({
}

// CSS output
if (isUseCssExtract(config, target)) {
const emitCss = config.output.emitCss ?? target === 'web';
if (!config.output.injectStyles && emitCss) {
const extractPluginOptions = config.tools.cssExtract.pluginOptions;

const cssPath = config.output.distPath.css;
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,8 @@ export interface PerformanceConfig {
/**
* Used to control resource `Prefetch`.
*
* Specifies that the user agent should preemptively fetch and cache the target resource as it is likely to be required for a followup navigation.
* Specifies that the user agent should preemptively fetch and cache the target resource as it
* is likely to be required for a followup navigation.
*/
prefetch?: true | PreloadOrPreFetchOption;

Expand Down Expand Up @@ -841,7 +842,8 @@ export interface OutputConfig {
*/
target?: RsbuildTarget;
/**
* At build time, prevent some `import` dependencies from being packed into bundles in your code, and instead fetch them externally at runtime.
* At build time, prevent some `import` dependencies from being packed into bundles in your code,
* and instead fetch them externally at runtime.
* For more information, please see: [Rspack Externals](https://rspack.dev/config/externals)
* @default undefined
*/
Expand Down Expand Up @@ -957,6 +959,13 @@ export interface OutputConfig {
* @default true
*/
emitAssets?: boolean;
/**
* Whether to emit CSS to the output bundles.
* If `false`, the CSS will not be extracted to separate files or injected into the JavaScript
* bundles via `output.injectStyles`.
* @default `true` when `output.target` is `web`, otherwise `false`
*/
emitCss?: boolean;
}

export interface NormalizedOutputConfig extends OutputConfig {
Expand Down
27 changes: 27 additions & 0 deletions website/docs/en/config/output/emit-css.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# output.emitCss

- **Type:** `boolean`
- **Default:** `true` when [output.target](/config/output/target) is `web`, otherwise `false`

Whether to emit CSS to the output bundles.

If `false`, the CSS will not be extracted to separate files or injected into the JavaScript bundles via [output.injectStyles](/config/output/inject-styles).

:::tip

When `output.emitCss` is `false`, the class name information of CSS Modules will still be injected into the JS bundles, which helps to ensure the correctness of CSS Modules class names in SSR.

:::

## Example

When building Node.js bundles, if you need to output CSS files, you can set `output.emitCss` to `true`:

```js
export default {
output: {
target: 'node',
emitCss: true,
},
};
```
7 changes: 4 additions & 3 deletions website/docs/en/config/output/target.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ When `target` is set to `'node'`, Rsbuild will:

- Set Rspack's [target](https://rspack.dev/config/target) to `'node'`.
- No HTML files will be generated, and HTML-related logic will not be executed, since HTML is not required by the Node.js environment.
- CSS code will not be bundled or extracted, but the id information of CSS Modules will be included in the bundle.
- The default code split strategy will be disabled, but dynamic import can still work.
- Disable the HMR.
- Adjust the default value of Browserslist to `['node >= 16']`.
- Set the default value of Browserslist to `['node >= 16']`.
- Set the default value of [output.emitCss](/config/output/emit-css) to `false`. This means that CSS code will not be extracted to separate files, but the id information of CSS Modules will be included in the bundle.

## Web Worker Target

Expand All @@ -80,8 +80,9 @@ When `target` is set to `'web-worker'`, Rsbuild will:

- Set Rspack's [target](https://rspack.dev/config/target) to `'webworker'`.
- No HTML files will be generated, and HTML-related logic will not be executed, since HTML is not required by the Web Worker environment.
- CSS code will not be bundled or extracted, but the id information of CSS Modules will be included in the bundle.
- CSS code will not be bundled or extracted, but the id information of CSS Modules will be included in the bundle (the default value of [output.emitCss](/config/output/emit-css) is `false`).
- The default code split strategy will be disabled, and **dynamic import can not work**, because the Web Worker only runs a single JavaScript file.
- Set the default value of [output.emitCss](/config/output/emit-css) to `false`. This means that CSS code will not be extracted to separate files, but the id information of CSS Modules will be included in the bundle.
- Disable the HMR.

For more information, please refer to: [Using Web Workers](/guide/basic/web-workers).
Expand Down
Loading

0 comments on commit 917318d

Please sign in to comment.