Skip to content
Merged
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
37 changes: 19 additions & 18 deletions e2e/cases/sri/enable-dev/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { dev } from '@e2e/helper';
import { expect, test } from '@playwright/test';
import { dev, rspackOnlyTest } from '@e2e/helper';
import { expect } from '@playwright/test';

test('generate integrity for script and style tags in dev build', async ({
page,
}) => {
const rsbuild = await dev({
cwd: __dirname,
page,
});
rspackOnlyTest(
'generate integrity for script and style tags in dev build',
async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
page,
});

const testEl = page.locator('#root');
await expect(testEl).toHaveText('Hello Rsbuild!');
const testEl = page.locator('#root');
await expect(testEl).toHaveText('Hello Rsbuild!');

expect(
await page.evaluate(
'document.querySelector("script")?.getAttribute("integrity")',
),
).toMatch(/sha384-[A-Za-z0-9+\/=]+/);
expect(
await page.evaluate(
'document.querySelector("script")?.getAttribute("integrity")',
),
).toMatch(/sha384-[A-Za-z0-9+\/=]+/);

await rsbuild.close();
});
await rsbuild.close();
},
);
37 changes: 20 additions & 17 deletions e2e/cases/sri/preload/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { build } from '@e2e/helper';
import { expect, test } from '@playwright/test';
import { build, rspackOnlyTest } from '@e2e/helper';
import { expect } from '@playwright/test';

test('generate integrity for preload tags in prod build', async ({ page }) => {
const rsbuild = await build({
cwd: __dirname,
page,
});
rspackOnlyTest(
'generate integrity for preload tags in prod build',
async ({ page }) => {
const rsbuild = await build({
cwd: __dirname,
page,
});

const files = await rsbuild.unwrapOutputJSON();
const html =
files[Object.keys(files).find((file) => file.endsWith('index.html'))!];
const files = await rsbuild.unwrapOutputJSON();
const html =
files[Object.keys(files).find((file) => file.endsWith('index.html'))!];

expect(html).toMatch(
/<link href="\/static\/js\/async\/foo\.\w{8}\.js" rel="preload" as="script" integrity="sha384-[A-Za-z0-9+\/=]+"/,
);
expect(html).toMatch(
/<link href="\/static\/js\/async\/foo\.\w{8}\.js" rel="preload" as="script" integrity="sha384-[A-Za-z0-9+\/=]+"/,
);

const testEl = page.locator('#root');
await expect(testEl).toHaveText('Hello Rsbuild!');
await rsbuild.close();
});
const testEl = page.locator('#root');
await expect(testEl).toHaveText('Hello Rsbuild!');
await rsbuild.close();
},
);
4 changes: 1 addition & 3 deletions packages/core/src/configChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,8 @@ export const CHAIN_ID = {
VUE_LOADER_PLUGIN: 'vue-loader-plugin',
/** ReactFastRefreshPlugin */
REACT_FAST_REFRESH: 'react-fast-refresh',
/** WebpackSRIPlugin */
/** SubresourceIntegrityPlugin */
SUBRESOURCE_INTEGRITY: 'subresource-integrity',
/** AutoSetRootFontSizePlugin */
AUTO_SET_ROOT_SIZE: 'auto-set-root-size',
},
/** Predefined minimizers */
MINIMIZER: {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/helpers/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function formatStats(
);

if (warnings.length) {
const title = color.bold(color.yellow('Compile Warning: \n'));
const title = color.bold(color.yellow('Compile warning: \n'));

return {
message: `${title}${warnings.join('\n\n')}\n`,
Expand Down
206 changes: 31 additions & 175 deletions packages/core/src/plugins/sri.ts
Original file line number Diff line number Diff line change
@@ -1,192 +1,48 @@
import type { Buffer } from 'node:buffer';
import crypto from 'node:crypto';
import { HTML_REGEX } from '../constants';
import { removeLeadingSlash } from '../helpers';
import { logger } from '../logger';
import type {
EnvironmentContext,
RsbuildPlugin,
Rspack,
SriAlgorithm,
SriOptions,
} from '../types';

const getAssetName = (url: string, assetPrefix: string) => {
if (url.startsWith(assetPrefix)) {
return removeLeadingSlash(url.replace(assetPrefix, ''));
}
return removeLeadingSlash(url);
};

const isSriLinkRel = (rel: string | boolean | null | undefined) => {
return (
typeof rel === 'string' &&
['stylesheet', 'preload', 'modulepreload'].includes(rel)
);
};
import path from 'node:path';
import { rspack } from '@rspack/core';
import { COMPILED_PATH } from '../constants';
import type { RsbuildPlugin } from '../types';

export const pluginSri = (): RsbuildPlugin => ({
name: 'rsbuild:sri',

setup(api) {
const placeholder = 'RSBUILD_INTEGRITY_PLACEHOLDER:';
api.modifyBundlerChain((chain, { environment, CHAIN_ID }) => {
if (api.context.bundlerType === 'webpack') {
return;
}

const { config, htmlPaths } = environment;

if (Object.keys(htmlPaths).length === 0) {
return;
}

const getAlgorithm = (environment: EnvironmentContext) => {
const { config } = environment;
const { sri } = config.security;
const enable =
sri.enable === 'auto' ? config.mode === 'production' : sri.enable;

if (!enable) {
return null;
return;
}

const { algorithm = 'sha384' }: SriOptions = sri;
return algorithm;
};

api.modifyHTMLTags({
// ensure `sri` can be applied to all tags
order: 'post',
handler(tags, { assetPrefix, environment }) {
const algorithm = getAlgorithm(environment);

if (!algorithm) {
return tags;
}

const allTags = [...tags.headTags, ...tags.bodyTags];

for (const tag of allTags) {
let url = '';

if (!tag.attrs) {
continue;
}

if (tag.tag === 'script' && typeof tag.attrs.src === 'string') {
url = tag.attrs.src;
} else if (
tag.tag === 'link' &&
isSriLinkRel(tag.attrs.rel) &&
typeof tag.attrs.href === 'string'
) {
url = tag.attrs.href;
}

if (!url) {
continue;
}

const assetName = getAssetName(url, assetPrefix);

if (!assetName) {
continue;
}

tag.attrs.integrity ??= `${placeholder}${assetName}`;
}

return tags;
},
});

const replaceIntegrity = (
htmlContent: string,
assets: Rspack.Assets,
algorithm: SriAlgorithm,
integrityCache: Map<string, string>,
) => {
const regex = /integrity="RSBUILD_INTEGRITY_PLACEHOLDER:([^"]+)"/g;
const matches = htmlContent.matchAll(regex);
let replacedHtml = htmlContent;

const calcIntegrity = (
algorithm: SriAlgorithm,
assetName: string,
data: Buffer,
) => {
if (integrityCache.has(assetName)) {
return integrityCache.get(assetName);
}

const hash = crypto
.createHash(algorithm)
.update(data as Uint8Array)
.digest()
.toString('base64');
const integrity = `${algorithm}-${hash}`;

integrityCache.set(assetName, integrity);

return integrity;
};

for (const match of matches) {
const assetName = match[1];
if (!assetName) {
continue;
}

if (assets[assetName]) {
const integrity = calcIntegrity(
algorithm,
assetName,
assets[assetName].buffer(),
);
replacedHtml = replacedHtml.replaceAll(
`integrity="${placeholder}${assetName}"`,
`integrity="${integrity}"`,
);
} else {
logger.debug(
`[rsbuild:sri] failed to generate integrity for ${assetName}.`,
);
// remove the integrity placeholder
replacedHtml = replacedHtml.replace(
`integrity="${placeholder}${assetName}"`,
'',
);
}
// SRI requires a cross-origin policy
const crossorigin = chain.output.get('crossOriginLoading');
if (crossorigin === false || crossorigin === undefined) {
chain.output.crossOriginLoading('anonymous');
}

return replacedHtml;
};

api.processAssets(
{
// use to final stage to get the final asset content
stage: 'report',
},
({ assets, sources, environment }) => {
const { htmlPaths } = environment;

if (Object.keys(htmlPaths).length === 0) {
return;
}

const algorithm = getAlgorithm(environment);
if (!algorithm) {
return;
}

const integrityCache = new Map<string, string>();

for (const asset of Object.keys(assets)) {
if (!HTML_REGEX.test(asset)) {
continue;
}

const htmlContent = assets[asset].source() as string;
if (!htmlContent.includes(placeholder)) {
continue;
}

assets[asset] = new sources.RawSource(
replaceIntegrity(htmlContent, assets, algorithm, integrityCache),
);
}
},
);
const { algorithm = 'sha384' } = sri;

chain
.plugin(CHAIN_ID.PLUGIN.SUBRESOURCE_INTEGRITY)
.use(rspack.experiments.SubresourceIntegrityPlugin, [
{
enabled: true,
hashFuncNames: [algorithm],
htmlPlugin: path.join(COMPILED_PATH, 'html-rspack-plugin/index.js'),
},
]);
});
},
});
18 changes: 9 additions & 9 deletions website/docs/en/config/tools/bundler-chain.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -217,17 +217,17 @@ For example, the `RULE.STYLUS` rule exists only when the Stylus plugin is regist

`PLUGIN.[ID]` can match a certain Rspack or webpack plugin.

| ID | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `PLUGIN.HTML` | correspond to `HtmlRspackPlugin`, you need to concat the entry name when using: `${PLUGIN.HTML}-${entryName}` |
| `PLUGIN.BUNDLE_ANALYZER` | correspond to `WebpackBundleAnalyzer` |
| `PLUGIN.VUE_LOADER_PLUGIN` | correspond to `VueLoaderPlugin` |
| ID | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PLUGIN.HTML` | correspond to [HtmlRspackPlugin](https://rspack.dev/plugins/rspack/html-rspack-plugin), you need to concat the entry name when using: `${PLUGIN.HTML}-${entryName}` |
| `PLUGIN.SUBRESOURCE_INTEGRITY` | correspond to [SubresourceIntegrityPlugin](https://rspack.dev/plugins/rspack/subresource-integrity-plugin) |
| `PLUGIN.BUNDLE_ANALYZER` | correspond to [BundleAnalyzerPlugin](https://github.com/webpack-contrib/webpack-bundle-analyzer) |

### CHAIN_ID.MINIMIZER

`MINIMIZER.[ID]` can match a certain minimizer.

| ID | Description |
| --------------- | ------------------------------------------------- |
| `MINIMIZER.JS` | correspond to `SwcJsMinimizerRspackPlugin` |
| `MINIMIZER.CSS` | correspond to `LightningCssMinimizerRspackPlugin` |
| ID | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `MINIMIZER.JS` | correspond to [SwcJsMinimizerRspackPlugin](https://rspack.dev/plugins/rspack/swc-js-minimizer-rspack-plugin) |
| `MINIMIZER.CSS` | correspond to [LightningCssMinimizerRspackPlugin](https://rspack.dev/plugins/rspack/lightning-css-minimizer-rspack-plugin) |
18 changes: 9 additions & 9 deletions website/docs/zh/config/tools/bundler-chain.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,17 @@ Rsbuild 中预先定义了一些常用的 Chain ID,你可以通过这些 ID

通过 `PLUGIN.[ID]` 可以匹配到特定的 Rspack 或 webpack plugin。

| ID | 描述 |
| -------------------------- | --------------------------------------------------------------------------------- |
| `PLUGIN.HTML` | 对应 `HtmlRspackPlugin`,使用时需要拼接 entry 名称:`${PLUGIN.HTML}-${entryName}` |
| `PLUGIN.BUNDLE_ANALYZER` | 对应 `WebpackBundleAnalyzer` |
| `PLUGIN.VUE_LOADER_PLUGIN` | 对应 `VueLoaderPlugin`(依赖 [Vue 插件](/plugins/list/plugin-vue)) |
| ID | 描述 |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `PLUGIN.HTML` | 对应 [HtmlRspackPlugin](https://rspack.dev/zh/plugins/rspack/html-rspack-plugin),使用时需要拼接 entry 名称:`${PLUGIN.HTML}-${entryName}` |
| `PLUGIN.SUBRESOURCE_INTEGRITY` | 对应 [SubresourceIntegrityPlugin](https://rspack.dev/zh/plugins/rspack/subresource-integrity-plugin) |
| `PLUGIN.BUNDLE_ANALYZER` | 对应 [BundleAnalyzerPlugin](https://github.com/webpack-contrib/webpack-bundle-analyzer) |

### CHAIN_ID.MINIMIZER

通过 `MINIMIZER.[ID]` 可以匹配到对应的压缩工具。

| ID | 描述 |
| --------------- | ---------------------------------------- |
| `MINIMIZER.JS` | 对应 `SwcJsMinimizerRspackPlugin` |
| `MINIMIZER.CSS` | 对应 `LightningCssMinimizerRspackPlugin` |
| ID | 描述 |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| `MINIMIZER.JS` | 对应 [SwcJsMinimizerRspackPlugin](https://rspack.dev/zh/plugins/rspack/swc-js-minimizer-rspack-plugin) |
| `MINIMIZER.CSS` | 对应 [LightningCssMinimizerRspackPlugin](https://rspack.dev/zh/plugins/rspack/lightning-css-minimizer-rspack-plugin) |
Loading