diff --git a/examples/react-component-bundle-false/rslib.config.ts b/examples/react-component-bundle-false/rslib.config.ts
index 915d00d80..ac017ae4f 100644
--- a/examples/react-component-bundle-false/rslib.config.ts
+++ b/examples/react-component-bundle-false/rslib.config.ts
@@ -32,6 +32,7 @@ export default defineConfig({
],
output: {
target: 'web',
+ assetPrefix: 'auto', // TODO: move this line to packages/core/src/asset/assetConfig.ts
},
plugins: [pluginReact(), pluginSass()],
});
diff --git a/examples/react-component-bundle-false/src/assets/logo.svg b/examples/react-component-bundle-false/src/assets/logo.svg
new file mode 100644
index 000000000..6b60c1042
--- /dev/null
+++ b/examples/react-component-bundle-false/src/assets/logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/examples/react-component-bundle-false/src/index.scss b/examples/react-component-bundle-false/src/index.scss
index 2e506a0ac..f4c82dce4 100644
--- a/examples/react-component-bundle-false/src/index.scss
+++ b/examples/react-component-bundle-false/src/index.scss
@@ -1,3 +1,10 @@
+.counter-title {
+ width: 100px;
+ height: 100px;
+ background: no-repeat url('./assets/logo.svg');
+ background-size: cover;
+}
+
.counter-text {
font-size: 50px;
}
diff --git a/examples/react-component-bundle-false/src/index.tsx b/examples/react-component-bundle-false/src/index.tsx
index b7e472bb2..b9ceb8718 100644
--- a/examples/react-component-bundle-false/src/index.tsx
+++ b/examples/react-component-bundle-false/src/index.tsx
@@ -8,6 +8,7 @@ export const Counter: React.FC = () => {
return (
+
React
Counter: {count}
diff --git a/packages/core/src/asset/assetConfig.ts b/packages/core/src/asset/assetConfig.ts
index 908913af9..fdbf763e8 100644
--- a/packages/core/src/asset/assetConfig.ts
+++ b/packages/core/src/asset/assetConfig.ts
@@ -1,6 +1,7 @@
import type { RsbuildConfig } from '@rsbuild/core';
import type { Format } from '../types';
+// TODO: asset config document
export const composeAssetConfig = (
bundle: boolean,
format: Format,
@@ -14,8 +15,13 @@ export const composeAssetConfig = (
},
};
}
- // TODO: bundleless
- return {};
+
+ return {
+ output: {
+ dataUriLimit: 0, // default: no inline asset
+ // assetPrefix: 'auto', // TODO: will turn on this with js support together in the future
+ },
+ };
}
// mf and umd etc
diff --git a/packages/core/src/css/LibCssExtractPlugin.ts b/packages/core/src/css/LibCssExtractPlugin.ts
new file mode 100644
index 000000000..d4588c63b
--- /dev/null
+++ b/packages/core/src/css/LibCssExtractPlugin.ts
@@ -0,0 +1,81 @@
+import { type Rspack, rspack } from '@rsbuild/core';
+import { RSLIB_CSS_ENTRY_FLAG } from './cssConfig';
+import {
+ ABSOLUTE_PUBLIC_PATH,
+ AUTO_PUBLIC_PATH,
+ SINGLE_DOT_PATH_SEGMENT,
+} from './libCssExtractLoader';
+import { getUndoPath } from './utils';
+
+const pluginName = 'LIB_CSS_EXTRACT_PLUGIN';
+
+type Options = Record;
+
+class LibCssExtractPlugin implements Rspack.RspackPluginInstance {
+ readonly name: string = pluginName;
+ options: Options;
+ constructor(options?: Options) {
+ this.options = options ?? {};
+ }
+
+ apply(compiler: Rspack.Compiler): void {
+ // 1. mark and remove the normal css asset
+ // 2. preserve CSS Modules asset
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
+ compilation.hooks.chunkAsset.tap(pluginName, (_chunk, filename) => {
+ const asset = compilation.getAsset(filename);
+ if (!asset) {
+ return;
+ }
+ const needRemove = Boolean(asset.name.match(RSLIB_CSS_ENTRY_FLAG));
+ if (needRemove) {
+ compilation.deleteAsset(filename);
+ }
+ });
+ });
+
+ /**
+ * The following code is modified based on
+ * https://github.com/webpack-contrib/mini-css-extract-plugin/blob/3effaa0319bad5cc1bf0ae760553bf7abcbc35a4/src/index.js#L1597
+ *
+ * replace publicPath placeholders of miniCssExtractLoader
+ */
+ compiler.hooks.make.tap(pluginName, (compilation) => {
+ compilation.hooks.processAssets.tap(pluginName, (assets) => {
+ const chunkAsset = Object.keys(assets).filter((name) =>
+ /\.css/.test(name),
+ );
+ for (const name of chunkAsset) {
+ compilation.updateAsset(name, (old) => {
+ const oldSource = old.source().toString();
+ const replaceSource = new rspack.sources.ReplaceSource(old);
+
+ function replace(searchValue: string, replaceValue: string) {
+ let start = oldSource.indexOf(searchValue);
+ while (start !== -1) {
+ replaceSource.replace(
+ start,
+ start + searchValue.length - 1,
+ replaceValue,
+ );
+ start = oldSource.indexOf(searchValue, start + 1);
+ }
+ }
+
+ replace(ABSOLUTE_PUBLIC_PATH, '');
+ replace(SINGLE_DOT_PATH_SEGMENT, '.');
+ const undoPath = getUndoPath(
+ name,
+ compilation.outputOptions.path!,
+ false,
+ );
+ replace(AUTO_PUBLIC_PATH, undoPath);
+
+ return replaceSource;
+ });
+ }
+ });
+ });
+ }
+}
+export { LibCssExtractPlugin };
diff --git a/packages/core/src/css/RemoveCssExtractAssetPlugin.ts b/packages/core/src/css/RemoveCssExtractAssetPlugin.ts
deleted file mode 100644
index 29481f79f..000000000
--- a/packages/core/src/css/RemoveCssExtractAssetPlugin.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { Rspack } from '@rsbuild/core';
-
-const pluginName = 'REMOVE_CSS_EXTRACT_ASSET_PLUGIN';
-
-type Options = {
- include: RegExp;
-};
-
-class RemoveCssExtractAssetPlugin implements Rspack.RspackPluginInstance {
- readonly name: string = pluginName;
- options: Options;
- constructor(options: Options) {
- this.options = options;
- }
-
- apply(compiler: Rspack.Compiler): void {
- const include = this.options.include;
- compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
- compilation.hooks.chunkAsset.tap(pluginName, (_chunk, filename) => {
- const asset = compilation.getAsset(filename);
- if (!asset) {
- return;
- }
- const needRemove = Boolean(asset.name.match(include));
- if (needRemove) {
- compilation.deleteAsset(filename);
- }
- });
- });
- }
-}
-export { RemoveCssExtractAssetPlugin };
diff --git a/packages/core/src/css/cssConfig.ts b/packages/core/src/css/cssConfig.ts
index b5e6ab94f..b57feac32 100644
--- a/packages/core/src/css/cssConfig.ts
+++ b/packages/core/src/css/cssConfig.ts
@@ -6,7 +6,7 @@ import type {
RsbuildPlugin,
} from '@rsbuild/core';
import { CSS_EXTENSIONS_PATTERN } from '../constant';
-import { RemoveCssExtractAssetPlugin } from './RemoveCssExtractAssetPlugin';
+import { LibCssExtractPlugin } from './LibCssExtractPlugin';
const require = createRequire(import.meta.url);
export const RSLIB_CSS_ENTRY_FLAG = '__rslib_css__';
@@ -138,13 +138,7 @@ const pluginLibCss = (rootDir: string): RsbuildPlugin => ({
if (isUsingCssExtract) {
const cssExtract = CHAIN_ID.PLUGIN.MINI_CSS_EXTRACT;
config.plugins.delete(cssExtract);
- config
- .plugin(RemoveCssExtractAssetPlugin.name)
- .use(RemoveCssExtractAssetPlugin, [
- {
- include: new RegExp(`^${RSLIB_CSS_ENTRY_FLAG}`),
- },
- ]);
+ config.plugin(LibCssExtractPlugin.name).use(LibCssExtractPlugin);
}
});
},
diff --git a/packages/core/src/css/libCssExtractLoader.ts b/packages/core/src/css/libCssExtractLoader.ts
index 2dec64580..6faa2aae9 100644
--- a/packages/core/src/css/libCssExtractLoader.ts
+++ b/packages/core/src/css/libCssExtractLoader.ts
@@ -3,11 +3,18 @@
* https://github.com/web-infra-dev/rspack/blob/0a89e433a9f8596a7c6c4326542f168b5982d2da/packages/rspack/src/builtin-plugin/css-extract/loader.ts
* 1. remove hmr/webpack runtime
* 2. add `this.emitFile` to emit css files
- * 3. add `import './[name].css';`
+ * 3. add `import './[name].css';` to js module
*/
import path, { extname } from 'node:path';
import type { Rspack } from '@rsbuild/core';
+export const BASE_URI = 'webpack://';
+export const MODULE_TYPE = 'css/mini-extract';
+export const AUTO_PUBLIC_PATH = '__mini_css_extract_plugin_public_path_auto__';
+export const ABSOLUTE_PUBLIC_PATH: string = `${BASE_URI}/mini-css-extract-plugin/`;
+export const SINGLE_DOT_PATH_SEGMENT =
+ '__mini_css_extract_plugin_single_dot_path_segment__';
+
interface DependencyDescription {
identifier: string;
content: string;
@@ -20,7 +27,11 @@ interface DependencyDescription {
filepath: string;
}
+// https://github.com/web-infra-dev/rspack/blob/c0986d39b7d647682f10fcef5bbade39fd016eca/packages/rspack/src/config/types.ts#L10
+type Filename = string | ((pathData: any, assetInfo?: any) => string);
+
export interface CssExtractRspackLoaderOptions {
+ publicPath?: string | ((resourcePath: string, context: string) => string);
emit?: boolean;
esModule?: boolean;
layer?: string;
@@ -29,7 +40,7 @@ export interface CssExtractRspackLoaderOptions {
rootDir?: string;
}
-const PLUGIN_NAME = 'LIB_CSS_EXTRACT_LOADER';
+const LOADER_NAME = 'LIB_CSS_EXTRACT_LOADER';
function stringifyLocal(value: any) {
return typeof value === 'function' ? value.toString() : JSON.stringify(value);
@@ -77,6 +88,34 @@ export const pitch: Rspack.LoaderDefinition['pitch'] = function (
const filepath = this.resourcePath;
const rootDir = options.rootDir ?? this.rootContext;
+ let { publicPath } = this._compilation!.outputOptions;
+
+ if (typeof options.publicPath === 'string') {
+ // eslint-disable-next-line prefer-destructuring
+ publicPath = options.publicPath;
+ } else if (typeof options.publicPath === 'function') {
+ publicPath = options.publicPath(this.resourcePath, this.rootContext);
+ }
+
+ if (publicPath === 'auto') {
+ publicPath = AUTO_PUBLIC_PATH;
+ }
+
+ let publicPathForExtract: Filename | undefined;
+
+ if (typeof publicPath === 'string') {
+ const isAbsolutePublicPath = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/.test(publicPath);
+
+ publicPathForExtract = isAbsolutePublicPath
+ ? publicPath
+ : `${ABSOLUTE_PUBLIC_PATH}${publicPath.replace(
+ /\./g,
+ SINGLE_DOT_PATH_SEGMENT,
+ )}`;
+ } else {
+ publicPathForExtract = publicPath;
+ }
+
const handleExports = (
originalExports:
| { default: Record; __esModule: true }
@@ -196,7 +235,7 @@ export const pitch: Rspack.LoaderDefinition['pitch'] = function (
return '';
})();
- let resultSource = `// extracted by ${PLUGIN_NAME}`;
+ let resultSource = `// extracted by ${LOADER_NAME}`;
let importCssFiles = '';
@@ -249,6 +288,8 @@ export const pitch: Rspack.LoaderDefinition['pitch'] = function (
`${this.resourcePath}.webpack[javascript/auto]!=!!!${request}`,
{
layer: options.layer,
+ publicPath: publicPathForExtract,
+ baseUri: `${BASE_URI}/`,
},
(error, exports) => {
if (error) {
diff --git a/packages/core/src/css/utils.ts b/packages/core/src/css/utils.ts
new file mode 100644
index 000000000..3fd765500
--- /dev/null
+++ b/packages/core/src/css/utils.ts
@@ -0,0 +1,45 @@
+/**
+ * This function is copied from
+ * https://github.com/webpack-contrib/mini-css-extract-plugin/blob/3effaa0319bad5cc1bf0ae760553bf7abcbc35a4/src/utils.js#L169
+ * linted by biome
+ */
+function getUndoPath(
+ filename: string,
+ outputPathArg: string,
+ enforceRelative: boolean,
+): string {
+ let depth = -1;
+ let append = '';
+
+ let outputPath = outputPathArg.replace(/[\\/]$/, '');
+
+ for (const part of filename.split(/[/\\]+/)) {
+ if (part === '..') {
+ if (depth > -1) {
+ depth--;
+ } else {
+ const i = outputPath.lastIndexOf('/');
+ const j = outputPath.lastIndexOf('\\');
+ const pos = i < 0 ? j : j < 0 ? i : Math.max(i, j);
+
+ if (pos < 0) {
+ return `${outputPath}/`;
+ }
+
+ append = `${outputPath.slice(pos + 1)}/${append}`;
+
+ outputPath = outputPath.slice(0, pos);
+ }
+ } else if (part !== '.') {
+ depth++;
+ }
+ }
+
+ return depth > 0
+ ? `${'../'.repeat(depth)}${append}`
+ : enforceRelative
+ ? `./${append}`
+ : append;
+}
+
+export { getUndoPath };
diff --git a/tests/e2e/react-component/index.pw.test.ts b/tests/e2e/react-component/index.pw.test.ts
index 54935d837..ff618c545 100644
--- a/tests/e2e/react-component/index.pw.test.ts
+++ b/tests/e2e/react-component/index.pw.test.ts
@@ -58,36 +58,37 @@ test('should render example "react-component-bundle" successfully', async ({
await rsbuild.close();
});
-test('should render example "react-component-umd" successfully', async ({
+test('should render example "react-component-bundle-false" successfully', async ({
page,
}) => {
- const umdPath = path.resolve(
- getCwdByExample('react-component-umd'),
- './dist/umd/index.js',
- );
- fs.mkdirSync(path.resolve(__dirname, './public/umd'), { recursive: true });
- fs.copyFileSync(umdPath, path.resolve(__dirname, './public/umd/index.js'));
-
const rsbuild = await dev({
cwd: __dirname,
page,
- environment: ['umd'],
+ environment: ['bundleFalse'],
});
await counterCompShouldWork(page);
+ await styleShouldWork(page);
+ await assetShouldWork(page);
await rsbuild.close();
});
-test('should render example "react-component-bundle-false" successfully', async ({
+test('should render example "react-component-umd" successfully', async ({
page,
}) => {
+ const umdPath = path.resolve(
+ getCwdByExample('react-component-umd'),
+ './dist/umd/index.js',
+ );
+ fs.mkdirSync(path.resolve(__dirname, './public/umd'), { recursive: true });
+ fs.copyFileSync(umdPath, path.resolve(__dirname, './public/umd/index.js'));
+
const rsbuild = await dev({
cwd: __dirname,
page,
- environment: ['bundleFalse'],
+ environment: ['umd'],
});
await counterCompShouldWork(page);
- await styleShouldWork(page);
await rsbuild.close();
});
diff --git a/tests/integration/asset/limit/rslib.config.ts b/tests/integration/asset/limit/rslib.config.ts
index 0d0ead07a..f472330cf 100644
--- a/tests/integration/asset/limit/rslib.config.ts
+++ b/tests/integration/asset/limit/rslib.config.ts
@@ -37,9 +37,6 @@ export default defineConfig({
distPath: {
root: './dist/esm/external-bundleless',
},
- dataUriLimit: {
- svg: 0,
- },
},
}),
],
diff --git a/tests/integration/style/sass/bundle-false/rslib.config.ts b/tests/integration/style/sass/bundle-false/rslib.config.ts
index 79c602f7e..7ff820973 100644
--- a/tests/integration/style/sass/bundle-false/rslib.config.ts
+++ b/tests/integration/style/sass/bundle-false/rslib.config.ts
@@ -11,8 +11,7 @@ export default defineConfig({
entry: {
index: [
'../__fixtures__/src/**/*.scss',
- // TODO: assets support
- // '../__fixtures__/foundation/logo.svg'
+ '../__fixtures__/foundation/logo.svg',
],
},
},
@@ -25,8 +24,6 @@ export default defineConfig({
],
output: {
target: 'web',
- // dataUriLimit: {
- // svg: 0
- // }
+ assetPrefix: 'auto',
},
});