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', }, });