Skip to content

Commit

Permalink
feat(css): support css asset in bundleless mode and esm/cjs (#582)
Browse files Browse the repository at this point in the history
  • Loading branch information
SoonIter authored Dec 19, 2024
1 parent 8a9f9e3 commit 43c88d8
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 65 deletions.
1 change: 1 addition & 0 deletions examples/react-component-bundle-false/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
});
7 changes: 7 additions & 0 deletions examples/react-component-bundle-false/src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions examples/react-component-bundle-false/src/index.scss
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions examples/react-component-bundle-false/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const Counter: React.FC = () => {

return (
<div>
<h1 className="counter-title">React</h1>
<h2 className="counter-text">Counter: {count}</h2>
<CounterButton onClick={decrement} label="-" />
<CounterButton onClick={increment} label="+" />
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/asset/assetConfig.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down
81 changes: 81 additions & 0 deletions packages/core/src/css/LibCssExtractPlugin.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

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 };
32 changes: 0 additions & 32 deletions packages/core/src/css/RemoveCssExtractAssetPlugin.ts

This file was deleted.

10 changes: 2 additions & 8 deletions packages/core/src/css/cssConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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__';
Expand Down Expand Up @@ -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);
}
});
},
Expand Down
47 changes: 44 additions & 3 deletions packages/core/src/css/libCssExtractLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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<string, any>; __esModule: true }
Expand Down Expand Up @@ -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 = '';

Expand Down Expand Up @@ -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) {
Expand Down
45 changes: 45 additions & 0 deletions packages/core/src/css/utils.ts
Original file line number Diff line number Diff line change
@@ -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 };
25 changes: 13 additions & 12 deletions tests/e2e/react-component/index.pw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
3 changes: 0 additions & 3 deletions tests/integration/asset/limit/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ export default defineConfig({
distPath: {
root: './dist/esm/external-bundleless',
},
dataUriLimit: {
svg: 0,
},
},
}),
],
Expand Down
Loading

0 comments on commit 43c88d8

Please sign in to comment.