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
78 changes: 78 additions & 0 deletions e2e/cases/output/manifest-generate/index.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
1 change: 1 addition & 0 deletions e2e/cases/output/manifest-generate/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('hello!');
87 changes: 54 additions & 33 deletions packages/core/src/plugins/manifest.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) =>
(htmlPaths: Record<string, string>, manifestOptions: ManifestObjectConfig) =>
(_seed: Record<string, any>, files: FileDescriptor[]) => {
const chunkEntries = new Map<string, FileDescriptor[]>();

Expand All @@ -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<string>();
Expand Down Expand Up @@ -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',
Expand All @@ -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'
Expand All @@ -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),
},
]);
});
Expand Down
50 changes: 48 additions & 2 deletions packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
};

export type ManifestConfig = string | boolean | ManifestObjectConfig;

export interface OutputConfig {
/**
* Specify build target to run in specified environment.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down
Loading