Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/preact-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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FunctionComponent } from 'preact';
import logo from '../../assets/logo.svg';
import styles from './index.module.scss';

interface CounterButtonProps {
Expand All @@ -10,7 +11,12 @@ export const CounterButton: FunctionComponent<CounterButtonProps> = ({
onClick,
label,
}) => (
<button type="button" className={styles.button} onClick={onClick}>
<button
type="button"
className={`${styles.button} counter-button`}
onClick={onClick}
>
<img src={logo} alt="react" />
{label}
</button>
);
5 changes: 5 additions & 0 deletions examples/preact-component-bundle-false/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}

declare module '*.svg' {
const url: string;
export default url;
}
7 changes: 7 additions & 0 deletions examples/preact-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;
}
4 changes: 3 additions & 1 deletion examples/react-component-bundle-false/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default defineConfig({
{
format: 'esm',
bundle: false,
syntax: ['es5'],
dts: true,
output: {
distPath: {
Expand All @@ -22,6 +23,7 @@ export default defineConfig({
{
format: 'cjs',
bundle: false,
syntax: ['es5'],
dts: true,
output: {
distPath: {
Expand All @@ -31,8 +33,8 @@ export default defineConfig({
},
],
output: {
minify: false,
target: 'web',
assetPrefix: 'auto', // TODO: move this line to packages/core/src/asset/assetConfig.ts
},
plugins: [pluginReact(), pluginSass()],
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type React from 'react';
import logo from '../../assets/logo.svg';
import styles from './index.module.scss';

interface CounterButtonProps {
Expand All @@ -10,7 +11,12 @@ export const CounterButton: React.FC<CounterButtonProps> = ({
onClick,
label,
}) => (
<button type="button" className={styles.button} onClick={onClick}>
<button
type="button"
className={`${styles.button} counter-button`}
onClick={onClick}
>
<img src={logo} alt="react" />
{label}
</button>
);
5 changes: 5 additions & 0 deletions examples/react-component-bundle-false/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}

declare module '*.svg' {
const url: string;
export default url;
}
1 change: 0 additions & 1 deletion examples/react-component-bundle/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export default defineConfig({
],
output: {
target: 'web',
assetPrefix: 'auto', // TODO: move this line to packages/core/src/asset/assetConfig.ts
},
plugins: [pluginReact(), pluginSass()],
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type React from 'react';
import logo from '../../assets/logo.svg';
import styles from './index.module.scss';

interface CounterButtonProps {
Expand All @@ -10,7 +11,12 @@ export const CounterButton: React.FC<CounterButtonProps> = ({
onClick,
label,
}) => (
<button type="button" className={styles.button} onClick={onClick}>
<button
type="button"
className={`${styles.button} counter-button`}
onClick={onClick}
>
<img src={logo} alt="react" />
{label}
</button>
);
5 changes: 5 additions & 0 deletions examples/react-component-bundle/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}

declare module '*.svg' {
const url: string;
export default url;
}
162 changes: 162 additions & 0 deletions packages/core/src/asset/LibAssetExtractPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import assert from 'node:assert';
import { type Rspack, rspack } from '@rsbuild/core';
import { getUndoPath } from '../css/utils';

/**
* these codes is written according to
* https://github.com/web-infra-dev/rspack/blob/61f0cd2b4e313445a9d3329ca71240e99edfb352/crates/rspack_plugin_asset/src/lib.rs#L531
*/

// 1. bundleless: single file
const BUNDLELESS_ASSET_PATTERN: RegExp =
/__webpack_require__\.p\s\+\s["'](.+)["']/g;
const RSLIB_NAMESPACE_OBJECT = '__rslib_asset__';
const esmSingleFileTemplate = (
url: string,
) => `import ${RSLIB_NAMESPACE_OBJECT} from '${url}';
export default ${RSLIB_NAMESPACE_OBJECT};`;
const cjsSingleFileTemplate = (url: string) =>
`module.exports = require('${url}');`;

function extractAssetFilenames(content: string): string[] {
return [...content.matchAll(BUNDLELESS_ASSET_PATTERN)]
.map((i) => {
return i?.[1];
})
.filter(Boolean) as string[];
}

// 2. bundle
// remove __webpack_require__.p and __webpack_require__.g
const WEBPACK_REQUIRE_G_IIFE =
/\(\(\)\s*=>\s*{.*?__webpack_require__\.g\s\=.*?}\)\(\);/gms;
const WEBPACK_REQUIRE_G_IIFE_ES5 =
/!function\(\)\s*\s*{.*?__webpack_require__\.g\s\=.*?}\(\);/gms;
const WEBPACK_REQUIRE_P_IIFE =
/\(\(\)\s*=>\s*{.*?__webpack_require__\.p\s\=.*?}\)\(\);/gms;
const WEBPACK_REQUIRE_P_IIFE_ES5 =
/!function\(\)\s*\s*{.*?__webpack_require__\.p\s\=.*?}\(\);/gms;

// 2.1 bundle: concatenated
const CONCATENATED_PATTERN: RegExp =
/(const|var)\s(\w+)\s=\s\(?__webpack_require__\.p\s\+\s["'](.+)["']\)?/g;
const concatenatedEsmReplaceTemplate = (variableName: string, url: string) =>
`import ${variableName} from '${url}';`;
const concatenatedCjsReplaceTemplate = (
declarationKind: string,
variableName: string,
url: string,
) => `${declarationKind} ${variableName} = require('${url}');`;

// 2.2 bundle: not concatenated, in __webpack_require__.m
const NOT_CONCATENATED_PATTERN: RegExp =
/module\.exports = __webpack_require__\.p\s\+\s["'](.+)["']/g;
const nonConcatenatedReplaceTemplate = (url: string) =>
`module.exports = require('${url}');`;

const pluginName = 'LIB_ASSET_EXTRACT_PLUGIN';

type Options = {
// bundle and isUsingSvgr options: just for perf, in bundleless we can replace the entire file directly
bundle: boolean;
isUsingSvgr: boolean;
};

class LibAssetExtractPlugin implements Rspack.RspackPluginInstance {
readonly name: string = pluginName;
options: Options;
constructor(options: Options) {
this.options = options;
}

apply(compiler: Rspack.Compiler): void {
compiler.hooks.make.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(pluginName, (assets) => {
const chunkAsset = Object.keys(assets).filter((name) =>
/js$/.test(name),
);
const isEsmFormat = compilation.options.output.module;
const canEntireFileReplacedDirectly =
!this.options.bundle && !this.options.isUsingSvgr;
for (const name of chunkAsset) {
const undoPath = getUndoPath(
name,
compilation.outputOptions.path!,
true,
);
compilation.updateAsset(name, (old) => {
const oldSource = old.source().toString();

// bundleless
if (canEntireFileReplacedDirectly) {
const assetFilenames = extractAssetFilenames(oldSource);
// asset/resource
if (assetFilenames.length === 0) {
return old;
}
assert(
assetFilenames.length === 1,
`in bundleless mode, each asset file should only generate one js module, but generated ${assetFilenames}, ${oldSource}`,
);
const assetFilename = assetFilenames[0];
let newSource = '';
const url = `${undoPath}${assetFilename}`;

if (isEsmFormat) {
newSource = esmSingleFileTemplate(url);
} else {
newSource = cjsSingleFileTemplate(url);
}
return new rspack.sources.RawSource(newSource);
}

// bundle
const newSource = new rspack.sources.ReplaceSource(old);
function replace(
pattern: RegExp,
replacer: (match: RegExpMatchArray) => string,
) {
const matches = oldSource.matchAll(pattern);
for (const match of matches) {
const replaced = replacer(match);
newSource.replace(
match.index,
match.index + match[0].length - 1,
replaced,
);
}
}

const supportsArrowFunction =
compilation.options.output.environment?.arrowFunction;
supportsArrowFunction
? replace(WEBPACK_REQUIRE_G_IIFE, () => '')
: replace(WEBPACK_REQUIRE_G_IIFE_ES5, () => '');
supportsArrowFunction
? replace(WEBPACK_REQUIRE_P_IIFE, () => '')
: replace(WEBPACK_REQUIRE_P_IIFE_ES5, () => '');

replace(CONCATENATED_PATTERN, (match) => {
const declarationKind = match[1];
const variableName = match[2];
const url = `${undoPath}${match[3]}`;
return isEsmFormat
? concatenatedEsmReplaceTemplate(variableName!, url)
: concatenatedCjsReplaceTemplate(
declarationKind!,
variableName!,
url,
);
});
replace(NOT_CONCATENATED_PATTERN, (match) => {
const url = `${undoPath}${match[1]}`;
return nonConcatenatedReplaceTemplate(url);
});
return newSource;
});
}
});
});
}
}
export { LibAssetExtractPlugin };
55 changes: 51 additions & 4 deletions packages/core/src/asset/assetConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
import type { EnvironmentConfig } from '@rsbuild/core';
import type { EnvironmentConfig, RsbuildPlugin } from '@rsbuild/core';
import type { Format } from '../types';
import { LibAssetExtractPlugin } from './LibAssetExtractPlugin';

const PLUGIN_NAME = 'rsbuild:lib-asset';

const RSBUILD_SVGR_PLUGIN_NAME = 'rsbuild:svgr';
const pluginLibAsset = ({ bundle }: { bundle: boolean }): RsbuildPlugin => ({
name: PLUGIN_NAME,
pre: [RSBUILD_SVGR_PLUGIN_NAME],
setup(api) {
api.modifyBundlerChain((config, { CHAIN_ID }) => {
const isUsingSvgr = Boolean(
config.module
.rule(CHAIN_ID.RULE.SVG)
.oneOf(CHAIN_ID.RULE.SVG)
.uses.has(CHAIN_ID.USE.SVGR),
);
if (isUsingSvgr && !bundle) {
// in bundleless, only support transform the svg asset to mixedImport svgr file
// remove issuer to make every svg asset is transformed
const rule = config.module
.rule(CHAIN_ID.RULE.SVG)
.oneOf(CHAIN_ID.ONE_OF.SVG);
rule.issuer([]);
}
config
.plugin(LibAssetExtractPlugin.name)
.use(LibAssetExtractPlugin, [{ bundle, isUsingSvgr }]);

if (bundle) {
// preserve './' in css url
// https://github.com/web-infra-dev/rspack/pull/8946
config.plugins.get(CHAIN_ID.PLUGIN.MINI_CSS_EXTRACT)?.tap((options) => {
if (bundle) {
return [
{
...options[0],
enforceRelative: true,
},
];
}
return options;
});
}
});
},
});

// TODO: asset config document
export const composeAssetConfig = (
Expand All @@ -11,16 +57,17 @@ export const composeAssetConfig = (
return {
output: {
dataUriLimit: 0, // default: no inline asset
// assetPrefix: 'auto', // TODO: will turn on this with js support together in the future
assetPrefix: 'auto',
},
plugins: [pluginLibAsset({ bundle: true })],
};
}

return {
output: {
dataUriLimit: 0, // default: no inline asset
// assetPrefix: 'auto', // TODO: will turn on this with js support together in the future
assetPrefix: 'auto',
},
plugins: [pluginLibAsset({ bundle: false })],
};
}

Expand Down
Loading
Loading