From 3b6594d6f4c5a1b381669f9bcc75b67b22c3a2bb Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 29 Dec 2024 17:51:28 +0800 Subject: [PATCH] feat: allow to custom manifest JSON (#4289) --- .../output/manifest-generate/index.test.ts | 78 +++++++++++++++++ .../output/manifest-generate/src/index.ts | 1 + packages/core/src/plugins/manifest.ts | 87 ++++++++++++------- packages/core/src/types/config.ts | 50 ++++++++++- 4 files changed, 181 insertions(+), 35 deletions(-) create mode 100644 e2e/cases/output/manifest-generate/index.test.ts create mode 100644 e2e/cases/output/manifest-generate/src/index.ts diff --git a/e2e/cases/output/manifest-generate/index.test.ts b/e2e/cases/output/manifest-generate/index.test.ts new file mode 100644 index 0000000000..219d8854c2 --- /dev/null +++ b/e2e/cases/output/manifest-generate/index.test.ts @@ -0,0 +1,78 @@ +import { build, dev } from '@e2e/helper'; +import { expect, test } from '@playwright/test'; +import type { RsbuildConfig } from '@rsbuild/core'; + +const fixtures = __dirname; + +const rsbuildConfig: RsbuildConfig = { + output: { + manifest: { + filename: 'my-manifest.json', + generate: ({ files, manifestData }) => { + return { + filesCount: files.length, + data: manifestData, + }; + }, + }, + legalComments: 'none', + sourceMap: false, + filenameHash: false, + }, + performance: { + chunkSplit: { + strategy: 'all-in-one', + }, + }, +}; + +test('should allow to custom generate manifest data in production build', async () => { + const rsbuild = await build({ + cwd: fixtures, + rsbuildConfig, + }); + + const files = await rsbuild.unwrapOutputJSON(); + const manifestContent = + files[ + Object.keys(files).find((file) => file.endsWith('my-manifest.json'))! + ]; + const manifest = JSON.parse(manifestContent); + + expect(manifest.filesCount).toBe(2); + expect(manifest.data.allFiles.length).toBe(2); + expect(manifest.data.entries.index).toMatchObject({ + initial: { + js: ['/static/js/index.js'], + }, + html: ['/index.html'], + }); +}); + +test('should allow to custom generate manifest data in dev', async ({ + page, +}) => { + const rsbuild = await dev({ + cwd: fixtures, + page, + rsbuildConfig, + }); + + const files = await rsbuild.unwrapOutputJSON(); + const manifestContent = + files[ + Object.keys(files).find((file) => file.endsWith('my-manifest.json'))! + ]; + const manifest = JSON.parse(manifestContent); + + expect(manifest.filesCount).toBe(2); + expect(manifest.data.allFiles.length).toBe(2); + expect(manifest.data.entries.index).toMatchObject({ + initial: { + js: ['/static/js/index.js'], + }, + html: ['/index.html'], + }); + + await rsbuild.close(); +}); diff --git a/e2e/cases/output/manifest-generate/src/index.ts b/e2e/cases/output/manifest-generate/src/index.ts new file mode 100644 index 0000000000..ddc67c9b01 --- /dev/null +++ b/e2e/cases/output/manifest-generate/src/index.ts @@ -0,0 +1 @@ +console.log('hello!'); diff --git a/packages/core/src/plugins/manifest.ts b/packages/core/src/plugins/manifest.ts index 574e7f94fa..7fb2749301 100644 --- a/packages/core/src/plugins/manifest.ts +++ b/packages/core/src/plugins/manifest.ts @@ -1,34 +1,16 @@ import type { FileDescriptor } from 'rspack-manifest-plugin'; +import { isObject } from '../helpers'; import { recursiveChunkEntryNames } from '../rspack/preload/helpers'; -import type { RsbuildPlugin } from '../types'; - -type FilePath = string; - -type ManifestByEntry = { - initial?: { - js?: FilePath[]; - css?: FilePath[]; - }; - async?: { - js?: FilePath[]; - css?: FilePath[]; - }; - /** other assets (e.g. png、svg、source map) related to the current entry */ - assets?: FilePath[]; - html?: FilePath[]; -}; - -type ManifestList = { - entries: { - /** relate to rsbuild source.entry */ - [entryName: string]: ManifestByEntry; - }; - /** Flatten all assets */ - allFiles: FilePath[]; -}; +import type { + ManifestByEntry, + ManifestConfig, + ManifestData, + ManifestObjectConfig, + RsbuildPlugin, +} from '../types'; const generateManifest = - (htmlPaths: Record) => + (htmlPaths: Record, manifestOptions: ManifestObjectConfig) => (_seed: Record, files: FileDescriptor[]) => { const chunkEntries = new Map(); @@ -50,7 +32,7 @@ const generateManifest = return file.path; }); - const entries: ManifestList['entries'] = {}; + const entries: ManifestData['entries'] = {}; for (const [name, chunkFiles] of chunkEntries) { const assets = new Set(); @@ -126,11 +108,51 @@ const generateManifest = entries[name] = entryManifest; } - return { + const manifestData: ManifestData = { allFiles, entries, }; + + if (manifestOptions.generate) { + const generatedManifest = manifestOptions.generate({ + files, + manifestData, + }); + + if (isObject(generatedManifest)) { + return generatedManifest; + } + + throw new Error( + '[rsbuild:manifest] `manifest.generate` function must return a valid manifest object.', + ); + } + + return manifestData; + }; + +function normalizeManifestObjectConfig( + manifest?: ManifestConfig, +): ManifestObjectConfig { + if (typeof manifest === 'string') { + return { + filename: manifest, + }; + } + + const defaultOptions: ManifestObjectConfig = { + filename: 'manifest.json', + }; + + if (typeof manifest === 'boolean') { + return defaultOptions; + } + + return { + ...defaultOptions, + ...manifest, }; +} export const pluginManifest = (): RsbuildPlugin => ({ name: 'rsbuild:manifest', @@ -146,8 +168,7 @@ export const pluginManifest = (): RsbuildPlugin => ({ return; } - const fileName = - typeof manifest === 'string' ? manifest : 'manifest.json'; + const manifestOptions = normalizeManifestObjectConfig(manifest); const { RspackManifestPlugin } = await import( '../../compiled/rspack-manifest-plugin/index.js' @@ -156,9 +177,9 @@ export const pluginManifest = (): RsbuildPlugin => ({ chain.plugin(CHAIN_ID.PLUGIN.MANIFEST).use(RspackManifestPlugin, [ { - fileName, + fileName: manifestOptions.filename, writeToFileEmit: isDev && writeToDisk !== true, - generate: generateManifest(htmlPaths), + generate: generateManifest(htmlPaths, manifestOptions), }, ]); }); diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 2dc0896333..f809326f0f 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -889,6 +889,47 @@ export type InlineChunkConfig = | InlineChunkTest | { enable?: boolean | 'auto'; test: InlineChunkTest }; +export type ManifestByEntry = { + initial?: { + js?: string[]; + css?: string[]; + }; + async?: { + js?: string[]; + css?: string[]; + }; + /** other assets (e.g. png、svg、source map) related to the current entry */ + assets?: string[]; + html?: string[]; +}; + +export type ManifestData = { + entries: { + /** relate to Rsbuild's source.entry config */ + [entryName: string]: ManifestByEntry; + }; + /** Flatten all assets */ + allFiles: string[]; +}; + +export type ManifestObjectConfig = { + /** + * The filename or path of the manifest file. + * The manifest file will be emitted to the output directory. + * @default 'manifest.json' + */ + filename?: string; + /** + * A custom function to generate the content of the manifest file. + */ + generate?: (params: { + files: import('rspack-manifest-plugin').FileDescriptor[]; + manifestData: ManifestData; + }) => Record; +}; + +export type ManifestConfig = string | boolean | ManifestObjectConfig; + export interface OutputConfig { /** * Specify build target to run in specified environment. @@ -958,10 +999,14 @@ export interface OutputConfig { */ minify?: Minify; /** - * Whether to generate manifest file. + * Configure how to generate the manifest file. + * - `true`: Generate a manifest file named `manifest.json` in the output directory. + * - `false`: Do not generate the manifest file. + * - `string`: Generate a manifest file with the specified filename or path. + * - `object`: Generate a manifest file with the specified options. * @default false */ - manifest?: string | boolean; + manifest?: ManifestConfig; /** * Whether to generate source map files, and which format of source map to generate. * @@ -1037,6 +1082,7 @@ export interface NormalizedOutputConfig extends OutputConfig { filenameHash: boolean | string; assetPrefix: string; dataUriLimit: number | NormalizedDataUriLimit; + manifest: ManifestConfig; minify: Minify; inlineScripts: InlineChunkConfig; inlineStyles: InlineChunkConfig;