Skip to content

Commit 43c88d8

Browse files
authored
feat(css): support css asset in bundleless mode and esm/cjs (#582)
1 parent 8a9f9e3 commit 43c88d8

File tree

13 files changed

+211
-65
lines changed

13 files changed

+211
-65
lines changed

examples/react-component-bundle-false/rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default defineConfig({
3232
],
3333
output: {
3434
target: 'web',
35+
assetPrefix: 'auto', // TODO: move this line to packages/core/src/asset/assetConfig.ts
3536
},
3637
plugins: [pluginReact(), pluginSass()],
3738
});
Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
.counter-title {
2+
width: 100px;
3+
height: 100px;
4+
background: no-repeat url('./assets/logo.svg');
5+
background-size: cover;
6+
}
7+
18
.counter-text {
29
font-size: 50px;
310
}

examples/react-component-bundle-false/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const Counter: React.FC = () => {
88

99
return (
1010
<div>
11+
<h1 className="counter-title">React</h1>
1112
<h2 className="counter-text">Counter: {count}</h2>
1213
<CounterButton onClick={decrement} label="-" />
1314
<CounterButton onClick={increment} label="+" />

packages/core/src/asset/assetConfig.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RsbuildConfig } from '@rsbuild/core';
22
import type { Format } from '../types';
33

4+
// TODO: asset config document
45
export const composeAssetConfig = (
56
bundle: boolean,
67
format: Format,
@@ -14,8 +15,13 @@ export const composeAssetConfig = (
1415
},
1516
};
1617
}
17-
// TODO: bundleless
18-
return {};
18+
19+
return {
20+
output: {
21+
dataUriLimit: 0, // default: no inline asset
22+
// assetPrefix: 'auto', // TODO: will turn on this with js support together in the future
23+
},
24+
};
1925
}
2026

2127
// mf and umd etc
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { type Rspack, rspack } from '@rsbuild/core';
2+
import { RSLIB_CSS_ENTRY_FLAG } from './cssConfig';
3+
import {
4+
ABSOLUTE_PUBLIC_PATH,
5+
AUTO_PUBLIC_PATH,
6+
SINGLE_DOT_PATH_SEGMENT,
7+
} from './libCssExtractLoader';
8+
import { getUndoPath } from './utils';
9+
10+
const pluginName = 'LIB_CSS_EXTRACT_PLUGIN';
11+
12+
type Options = Record<string, unknown>;
13+
14+
class LibCssExtractPlugin implements Rspack.RspackPluginInstance {
15+
readonly name: string = pluginName;
16+
options: Options;
17+
constructor(options?: Options) {
18+
this.options = options ?? {};
19+
}
20+
21+
apply(compiler: Rspack.Compiler): void {
22+
// 1. mark and remove the normal css asset
23+
// 2. preserve CSS Modules asset
24+
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
25+
compilation.hooks.chunkAsset.tap(pluginName, (_chunk, filename) => {
26+
const asset = compilation.getAsset(filename);
27+
if (!asset) {
28+
return;
29+
}
30+
const needRemove = Boolean(asset.name.match(RSLIB_CSS_ENTRY_FLAG));
31+
if (needRemove) {
32+
compilation.deleteAsset(filename);
33+
}
34+
});
35+
});
36+
37+
/**
38+
* The following code is modified based on
39+
* https://github.com/webpack-contrib/mini-css-extract-plugin/blob/3effaa0319bad5cc1bf0ae760553bf7abcbc35a4/src/index.js#L1597
40+
*
41+
* replace publicPath placeholders of miniCssExtractLoader
42+
*/
43+
compiler.hooks.make.tap(pluginName, (compilation) => {
44+
compilation.hooks.processAssets.tap(pluginName, (assets) => {
45+
const chunkAsset = Object.keys(assets).filter((name) =>
46+
/\.css/.test(name),
47+
);
48+
for (const name of chunkAsset) {
49+
compilation.updateAsset(name, (old) => {
50+
const oldSource = old.source().toString();
51+
const replaceSource = new rspack.sources.ReplaceSource(old);
52+
53+
function replace(searchValue: string, replaceValue: string) {
54+
let start = oldSource.indexOf(searchValue);
55+
while (start !== -1) {
56+
replaceSource.replace(
57+
start,
58+
start + searchValue.length - 1,
59+
replaceValue,
60+
);
61+
start = oldSource.indexOf(searchValue, start + 1);
62+
}
63+
}
64+
65+
replace(ABSOLUTE_PUBLIC_PATH, '');
66+
replace(SINGLE_DOT_PATH_SEGMENT, '.');
67+
const undoPath = getUndoPath(
68+
name,
69+
compilation.outputOptions.path!,
70+
false,
71+
);
72+
replace(AUTO_PUBLIC_PATH, undoPath);
73+
74+
return replaceSource;
75+
});
76+
}
77+
});
78+
});
79+
}
80+
}
81+
export { LibCssExtractPlugin };

packages/core/src/css/RemoveCssExtractAssetPlugin.ts

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

packages/core/src/css/cssConfig.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
RsbuildPlugin,
77
} from '@rsbuild/core';
88
import { CSS_EXTENSIONS_PATTERN } from '../constant';
9-
import { RemoveCssExtractAssetPlugin } from './RemoveCssExtractAssetPlugin';
9+
import { LibCssExtractPlugin } from './LibCssExtractPlugin';
1010
const require = createRequire(import.meta.url);
1111

1212
export const RSLIB_CSS_ENTRY_FLAG = '__rslib_css__';
@@ -138,13 +138,7 @@ const pluginLibCss = (rootDir: string): RsbuildPlugin => ({
138138
if (isUsingCssExtract) {
139139
const cssExtract = CHAIN_ID.PLUGIN.MINI_CSS_EXTRACT;
140140
config.plugins.delete(cssExtract);
141-
config
142-
.plugin(RemoveCssExtractAssetPlugin.name)
143-
.use(RemoveCssExtractAssetPlugin, [
144-
{
145-
include: new RegExp(`^${RSLIB_CSS_ENTRY_FLAG}`),
146-
},
147-
]);
141+
config.plugin(LibCssExtractPlugin.name).use(LibCssExtractPlugin);
148142
}
149143
});
150144
},

packages/core/src/css/libCssExtractLoader.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
* https://github.com/web-infra-dev/rspack/blob/0a89e433a9f8596a7c6c4326542f168b5982d2da/packages/rspack/src/builtin-plugin/css-extract/loader.ts
44
* 1. remove hmr/webpack runtime
55
* 2. add `this.emitFile` to emit css files
6-
* 3. add `import './[name].css';`
6+
* 3. add `import './[name].css';` to js module
77
*/
88
import path, { extname } from 'node:path';
99
import type { Rspack } from '@rsbuild/core';
1010

11+
export const BASE_URI = 'webpack://';
12+
export const MODULE_TYPE = 'css/mini-extract';
13+
export const AUTO_PUBLIC_PATH = '__mini_css_extract_plugin_public_path_auto__';
14+
export const ABSOLUTE_PUBLIC_PATH: string = `${BASE_URI}/mini-css-extract-plugin/`;
15+
export const SINGLE_DOT_PATH_SEGMENT =
16+
'__mini_css_extract_plugin_single_dot_path_segment__';
17+
1118
interface DependencyDescription {
1219
identifier: string;
1320
content: string;
@@ -20,7 +27,11 @@ interface DependencyDescription {
2027
filepath: string;
2128
}
2229

30+
// https://github.com/web-infra-dev/rspack/blob/c0986d39b7d647682f10fcef5bbade39fd016eca/packages/rspack/src/config/types.ts#L10
31+
type Filename = string | ((pathData: any, assetInfo?: any) => string);
32+
2333
export interface CssExtractRspackLoaderOptions {
34+
publicPath?: string | ((resourcePath: string, context: string) => string);
2435
emit?: boolean;
2536
esModule?: boolean;
2637
layer?: string;
@@ -29,7 +40,7 @@ export interface CssExtractRspackLoaderOptions {
2940
rootDir?: string;
3041
}
3142

32-
const PLUGIN_NAME = 'LIB_CSS_EXTRACT_LOADER';
43+
const LOADER_NAME = 'LIB_CSS_EXTRACT_LOADER';
3344

3445
function stringifyLocal(value: any) {
3546
return typeof value === 'function' ? value.toString() : JSON.stringify(value);
@@ -77,6 +88,34 @@ export const pitch: Rspack.LoaderDefinition['pitch'] = function (
7788
const filepath = this.resourcePath;
7889
const rootDir = options.rootDir ?? this.rootContext;
7990

91+
let { publicPath } = this._compilation!.outputOptions;
92+
93+
if (typeof options.publicPath === 'string') {
94+
// eslint-disable-next-line prefer-destructuring
95+
publicPath = options.publicPath;
96+
} else if (typeof options.publicPath === 'function') {
97+
publicPath = options.publicPath(this.resourcePath, this.rootContext);
98+
}
99+
100+
if (publicPath === 'auto') {
101+
publicPath = AUTO_PUBLIC_PATH;
102+
}
103+
104+
let publicPathForExtract: Filename | undefined;
105+
106+
if (typeof publicPath === 'string') {
107+
const isAbsolutePublicPath = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/.test(publicPath);
108+
109+
publicPathForExtract = isAbsolutePublicPath
110+
? publicPath
111+
: `${ABSOLUTE_PUBLIC_PATH}${publicPath.replace(
112+
/\./g,
113+
SINGLE_DOT_PATH_SEGMENT,
114+
)}`;
115+
} else {
116+
publicPathForExtract = publicPath;
117+
}
118+
80119
const handleExports = (
81120
originalExports:
82121
| { default: Record<string, any>; __esModule: true }
@@ -196,7 +235,7 @@ export const pitch: Rspack.LoaderDefinition['pitch'] = function (
196235
return '';
197236
})();
198237

199-
let resultSource = `// extracted by ${PLUGIN_NAME}`;
238+
let resultSource = `// extracted by ${LOADER_NAME}`;
200239

201240
let importCssFiles = '';
202241

@@ -249,6 +288,8 @@ export const pitch: Rspack.LoaderDefinition['pitch'] = function (
249288
`${this.resourcePath}.webpack[javascript/auto]!=!!!${request}`,
250289
{
251290
layer: options.layer,
291+
publicPath: publicPathForExtract,
292+
baseUri: `${BASE_URI}/`,
252293
},
253294
(error, exports) => {
254295
if (error) {

packages/core/src/css/utils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* This function is copied from
3+
* https://github.com/webpack-contrib/mini-css-extract-plugin/blob/3effaa0319bad5cc1bf0ae760553bf7abcbc35a4/src/utils.js#L169
4+
* linted by biome
5+
*/
6+
function getUndoPath(
7+
filename: string,
8+
outputPathArg: string,
9+
enforceRelative: boolean,
10+
): string {
11+
let depth = -1;
12+
let append = '';
13+
14+
let outputPath = outputPathArg.replace(/[\\/]$/, '');
15+
16+
for (const part of filename.split(/[/\\]+/)) {
17+
if (part === '..') {
18+
if (depth > -1) {
19+
depth--;
20+
} else {
21+
const i = outputPath.lastIndexOf('/');
22+
const j = outputPath.lastIndexOf('\\');
23+
const pos = i < 0 ? j : j < 0 ? i : Math.max(i, j);
24+
25+
if (pos < 0) {
26+
return `${outputPath}/`;
27+
}
28+
29+
append = `${outputPath.slice(pos + 1)}/${append}`;
30+
31+
outputPath = outputPath.slice(0, pos);
32+
}
33+
} else if (part !== '.') {
34+
depth++;
35+
}
36+
}
37+
38+
return depth > 0
39+
? `${'../'.repeat(depth)}${append}`
40+
: enforceRelative
41+
? `./${append}`
42+
: append;
43+
}
44+
45+
export { getUndoPath };

tests/e2e/react-component/index.pw.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,36 +58,37 @@ test('should render example "react-component-bundle" successfully', async ({
5858
await rsbuild.close();
5959
});
6060

61-
test('should render example "react-component-umd" successfully', async ({
61+
test('should render example "react-component-bundle-false" successfully', async ({
6262
page,
6363
}) => {
64-
const umdPath = path.resolve(
65-
getCwdByExample('react-component-umd'),
66-
'./dist/umd/index.js',
67-
);
68-
fs.mkdirSync(path.resolve(__dirname, './public/umd'), { recursive: true });
69-
fs.copyFileSync(umdPath, path.resolve(__dirname, './public/umd/index.js'));
70-
7164
const rsbuild = await dev({
7265
cwd: __dirname,
7366
page,
74-
environment: ['umd'],
67+
environment: ['bundleFalse'],
7568
});
7669

7770
await counterCompShouldWork(page);
71+
await styleShouldWork(page);
72+
await assetShouldWork(page);
7873
await rsbuild.close();
7974
});
8075

81-
test('should render example "react-component-bundle-false" successfully', async ({
76+
test('should render example "react-component-umd" successfully', async ({
8277
page,
8378
}) => {
79+
const umdPath = path.resolve(
80+
getCwdByExample('react-component-umd'),
81+
'./dist/umd/index.js',
82+
);
83+
fs.mkdirSync(path.resolve(__dirname, './public/umd'), { recursive: true });
84+
fs.copyFileSync(umdPath, path.resolve(__dirname, './public/umd/index.js'));
85+
8486
const rsbuild = await dev({
8587
cwd: __dirname,
8688
page,
87-
environment: ['bundleFalse'],
89+
environment: ['umd'],
8890
});
8991

9092
await counterCompShouldWork(page);
91-
await styleShouldWork(page);
9293
await rsbuild.close();
9394
});

tests/integration/asset/limit/rslib.config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ export default defineConfig({
3737
distPath: {
3838
root: './dist/esm/external-bundleless',
3939
},
40-
dataUriLimit: {
41-
svg: 0,
42-
},
4340
},
4441
}),
4542
],

0 commit comments

Comments
 (0)