diff --git a/e2e/cases/output/manifest-async-chunks/index.test.ts b/e2e/cases/output/manifest-async-chunks/index.test.ts index edbc7098ef..6e9616f257 100644 --- a/e2e/cases/output/manifest-async-chunks/index.test.ts +++ b/e2e/cases/output/manifest-async-chunks/index.test.ts @@ -9,7 +9,6 @@ test('should generate manifest for async chunks correctly', async () => { rsbuildConfig: { output: { manifest: true, - legalComments: 'none', filenameHash: false, }, performance: { diff --git a/e2e/cases/output/manifest-generate/index.test.ts b/e2e/cases/output/manifest-generate/index.test.ts index 219d8854c2..1fae1dbb9c 100644 --- a/e2e/cases/output/manifest-generate/index.test.ts +++ b/e2e/cases/output/manifest-generate/index.test.ts @@ -15,7 +15,6 @@ const rsbuildConfig: RsbuildConfig = { }; }, }, - legalComments: 'none', sourceMap: false, filenameHash: false, }, diff --git a/e2e/cases/output/manifest/index.test.ts b/e2e/cases/output/manifest/index.test.ts index 2af039acc9..9c4d444c48 100644 --- a/e2e/cases/output/manifest/index.test.ts +++ b/e2e/cases/output/manifest/index.test.ts @@ -1,17 +1,16 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { build, dev } from '@e2e/helper'; +import { build, dev, rspackOnlyTest } from '@e2e/helper'; import { expect, test } from '@playwright/test'; const fixtures = __dirname; -test('output.manifest', async () => { +test('should generate manifest file in output', async () => { const rsbuild = await build({ cwd: fixtures, rsbuildConfig: { output: { manifest: true, - legalComments: 'none', filenameHash: false, }, performance: { @@ -42,13 +41,12 @@ test('output.manifest', async () => { }); }); -test('output.manifest set a path', async () => { +test('should generate manifest file at specified path', async () => { await build({ cwd: fixtures, rsbuildConfig: { output: { manifest: './custom/my-manifest.json', - legalComments: 'none', filenameHash: false, }, performance: { @@ -69,7 +67,7 @@ test('output.manifest set a path', async () => { expect(Object.keys(parsed.allFiles).length).toBe(2); }); -test('output.manifest when target is node', async () => { +test('should generate manifest file when target is node', async () => { const rsbuild = await build({ cwd: fixtures, rsbuildConfig: { @@ -79,14 +77,8 @@ test('output.manifest when target is node', async () => { }, target: 'node', manifest: true, - legalComments: 'none', filenameHash: false, }, - performance: { - chunkSplit: { - strategy: 'all-in-one', - }, - }, }, }); @@ -109,7 +101,7 @@ test('output.manifest when target is node', async () => { }); }); -test('output.manifest should always write to disk when dev', async ({ +test('should always write manifest to disk when in dev mode', async ({ page, }) => { const rsbuild = await dev({ @@ -121,7 +113,6 @@ test('output.manifest should always write to disk when dev', async ({ root: 'dist-dev', }, manifest: true, - legalComments: 'none', filenameHash: false, }, performance: { @@ -141,3 +132,75 @@ test('output.manifest should always write to disk when dev', async ({ await rsbuild.close(); }); + +test('should allow to filter files in manifest', async () => { + const rsbuild = await build({ + cwd: fixtures, + rsbuildConfig: { + output: { + manifest: { + filter: (file) => file.name.endsWith('.js'), + }, + filenameHash: false, + }, + performance: { + chunkSplit: { + strategy: 'all-in-one', + }, + }, + }, + }); + + const files = await rsbuild.unwrapOutputJSON(); + + const manifestContent = + files[Object.keys(files).find((file) => file.endsWith('manifest.json'))!]; + const manifest = JSON.parse(manifestContent); + + // main.js + expect(Object.keys(manifest.allFiles).length).toBe(1); + + expect(manifest.entries.index).toMatchObject({ + initial: { + js: ['/static/js/index.js'], + }, + }); +}); + +rspackOnlyTest( + 'should allow to include license files in manifest', + async () => { + const rsbuild = await build({ + cwd: fixtures, + rsbuildConfig: { + output: { + manifest: { + filter: () => true, + }, + filenameHash: false, + }, + performance: { + chunkSplit: { + strategy: 'all-in-one', + }, + }, + }, + }); + + const files = await rsbuild.unwrapOutputJSON(); + + const manifestContent = + files[Object.keys(files).find((file) => file.endsWith('manifest.json'))!]; + const manifest = JSON.parse(manifestContent); + + expect(Object.keys(manifest.allFiles).length).toBe(3); + + expect(manifest.entries.index).toMatchObject({ + initial: { + js: ['/static/js/index.js'], + }, + html: ['/index.html'], + assets: ['/static/js/index.js.LICENSE.txt'], + }); + }, +); diff --git a/e2e/cases/output/manifest/src/index.ts b/e2e/cases/output/manifest/src/index.ts index ddc67c9b01..a74715686c 100644 --- a/e2e/cases/output/manifest/src/index.ts +++ b/e2e/cases/output/manifest/src/index.ts @@ -1 +1,3 @@ -console.log('hello!'); +import React from 'react'; + +console.log('hello!', React); diff --git a/packages/core/src/plugins/manifest.ts b/packages/core/src/plugins/manifest.ts index 7fb2749301..33d8da54e6 100644 --- a/packages/core/src/plugins/manifest.ts +++ b/packages/core/src/plugins/manifest.ts @@ -1,4 +1,4 @@ -import type { FileDescriptor } from 'rspack-manifest-plugin'; +import type { FileDescriptor } from '../../compiled/rspack-manifest-plugin'; import { isObject } from '../helpers'; import { recursiveChunkEntryNames } from '../rspack/preload/helpers'; import type { @@ -175,9 +175,15 @@ export const pluginManifest = (): RsbuildPlugin => ({ ); const { htmlPaths } = environment; + // Exclude `*.LICENSE.txt` files by default + const filter = + manifestOptions.filter ?? + ((file: FileDescriptor) => !file.name.endsWith('.LICENSE.txt')); + chain.plugin(CHAIN_ID.PLUGIN.MANIFEST).use(RspackManifestPlugin, [ { fileName: manifestOptions.filename, + filter, writeToFileEmit: isDev && writeToDisk !== true, generate: generateManifest(htmlPaths, manifestOptions), }, diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 7a8573b18f..544d571cf1 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -19,6 +19,7 @@ import type { Filter as ProxyFilter, } from '../../compiled/http-proxy-middleware/index.js'; import type RspackChain from '../../compiled/rspack-chain/index.js'; +import type { FileDescriptor } from '../../compiled/rspack-manifest-plugin'; import type { BundleAnalyzerPlugin } from '../../compiled/webpack-bundle-analyzer/index.js'; import type { ModifyBundlerChainUtils, @@ -926,9 +927,15 @@ export type ManifestObjectConfig = { * A custom function to generate the content of the manifest file. */ generate?: (params: { - files: import('rspack-manifest-plugin').FileDescriptor[]; + files: FileDescriptor[]; manifestData: ManifestData; }) => Record; + /** + * Allows you to filter the files included in the manifest. + * The function receives a `file` parameter and returns `true` to keep the file, or `false` to exclude it. + * @default (file: FileDescriptor) => !file.name.endsWith('.LICENSE.txt') + */ + filter?: (file: FileDescriptor) => boolean; }; export type ManifestConfig = string | boolean | ManifestObjectConfig; diff --git a/website/docs/en/config/output/manifest.mdx b/website/docs/en/config/output/manifest.mdx index dfc73897df..edde16c85a 100644 --- a/website/docs/en/config/output/manifest.mdx +++ b/website/docs/en/config/output/manifest.mdx @@ -184,3 +184,57 @@ const files = [ }, ]; ``` + +### filter + +- **Type:** + +```ts +type ManifestFilter = (file: FileDescriptor) => boolean; +``` + +- **Default:** `file => !file.name.endsWith('.LICENSE.txt')` +- **Version:** `>= 1.2.0` + +Allows you to filter the files included in the manifest. The function receives a `file` parameter and returns `true` to keep the file, or `false` to exclude it. + +By default, `*.LICENSE.txt` files are excluded from the manifest, as these license files are only used to declare open source licenses and are not used at runtime. + +For example, to only keep `*.js` files: + +```ts title="rsbuild.config.ts" +export default { + output: { + manifest: { + filter: (file) => file.name.endsWith('.js'), + }, + }, +}; +``` + +The generated manifest file will only include `*.js` files: + +```json title="dist/manifest.json" +{ + "allFiles": ["/static/js/index.[hash].js"], + "entries": { + "index": { + "initial": { + "js": ["/static/js/index.[hash].js"] + } + } + } +} +``` + +Or include all files: + +```ts title="rsbuild.config.ts" +export default { + output: { + manifest: { + filter: () => true, + }, + }, +}; +``` diff --git a/website/docs/zh/config/output/manifest.mdx b/website/docs/zh/config/output/manifest.mdx index 2ca6757ba7..447dcf7c15 100644 --- a/website/docs/zh/config/output/manifest.mdx +++ b/website/docs/zh/config/output/manifest.mdx @@ -184,3 +184,57 @@ const files = [ }, ]; ``` + +### filter + +- **类型:** + +```ts +type ManifestFilter = (file: FileDescriptor) => boolean; +``` + +- **默认值:** `file => !file.name.endsWith('.LICENSE.txt')` +- **版本:** `>= 1.2.0` + +允许你过滤包含在 manifest 中的文件。该函数接收一个 `file` 参数,返回 `true` 表示保留该文件,返回 `false` 表示不保留该文件。 + +默认情况下,`*.LICENSE.txt` 文件不会被包含在 manifest 文件中,因为这些许可证文件仅用于声明开源协议,不会在运行时被使用。 + +例如,仅保留 `*.js` 文件: + +```ts title="rsbuild.config.ts" +export default { + output: { + manifest: { + filter: (file) => file.name.endsWith('.js'), + }, + }, +}; +``` + +生成的 manifest 文件中仅会包含 `*.js` 文件: + +```json title="dist/manifest.json" +{ + "allFiles": ["/static/js/index.[hash].js"], + "entries": { + "index": { + "initial": { + "js": ["/static/js/index.[hash].js"] + } + } + } +} +``` + +或者是包含所有文件: + +```ts title="rsbuild.config.ts" +export default { + output: { + manifest: { + filter: () => true, + }, + }, +}; +```