Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support output.manifest config #2280

Merged
merged 15 commits into from
May 8, 2024
Merged
47 changes: 47 additions & 0 deletions e2e/cases/output/manifest/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { build } from '@e2e/helper';
import { expect, test } from '@playwright/test';

const fixtures = __dirname;

test('output.manifest', async () => {
const rsbuild = await build({
cwd: fixtures,
rsbuildConfig: {
output: {
manifest: true,
legalComments: 'none',
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'))!];

expect(manifestContent).toBeDefined();

const manifest = JSON.parse(manifestContent);

// main.js、index.html
expect(Object.keys(manifest.allFiles).length).toBe(2);

expect(manifest.entries.index).toMatchObject({
initial: {
js: ['/static/js/index.js'],
css: [],
},
async: {
js: [],
css: [],
},
assets: [],
html: '/index.html',
});
});
1 change: 1 addition & 0 deletions e2e/cases/output/manifest/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('hello!');
1 change: 1 addition & 0 deletions packages/compat/webpack/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const webpackProvider: RsbuildProvider<'webpack'> = async ({
plugins.resourceHints(),
plugins.server(),
plugins.moduleFederation(),
plugins.manifest(),
]);

pluginManager.addPlugins(allPlugins);
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@types/ws": "^8.5.10",
"commander": "^12.0.0",
"connect-history-api-fallback": "^2.0.0",
"rspack-manifest-plugin": "5.0.0",
"dotenv": "16.4.5",
"dotenv-expand": "11.0.6",
"http-compression": "1.0.19",
Expand Down
1 change: 1 addition & 0 deletions packages/core/prebundle.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default {
'dotenv-expand',
'ws',
'on-finished',
'rspack-manifest-plugin',
{
name: 'launch-editor-middleware',
ignoreDts: true,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const getDefaultOutputConfig = (): NormalizedOutputConfig => ({
legalComments: 'linked',
injectStyles: false,
minify: true,
manifest: false,
sourceMap: {
js: undefined,
css: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export const plugins = {
server: () => import('./server').then((m) => m.pluginServer()),
moduleFederation: () =>
import('./moduleFederation').then((m) => m.pluginModuleFederation()),
manifest: () => import('./manifest').then((m) => m.pluginManifest()),
};
145 changes: 145 additions & 0 deletions packages/core/src/plugins/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { Chunk } from '@rspack/core';
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[];
};

type FileDescriptor = {
chunk?: Chunk;
isInitial: boolean;
name: string;
path: string;
};

const generateManifest =
(htmlPaths: Record<string, string>) =>
(_seed: Record<string, any>, files: FileDescriptor[]) => {
const chunkEntries = new Map<string, FileDescriptor[]>();

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

const allFiles = files.map((file) => {
if (file.chunk) {
const names = recursiveChunkEntryNames(file.chunk);

for (const name of names) {
chunkEntries.set(name, [file, ...(chunkEntries.get(name) || [])]);
}
}

if (file.path.endsWith('.LICENSE.txt')) {
const sourceFilePath = file.path.split('.LICENSE.txt')[0];
licenseMap.set(sourceFilePath, file.path);
}
return file.path;
});

const entries: ManifestList['entries'] = {};

for (const [name, chunkFiles] of chunkEntries) {
const assets = new Set<string>();
const initialJS: string[] = [];
const asyncJS: string[] = [];
const initialCSS: string[] = [];
const asyncCSS: string[] = [];

for (const file of chunkFiles) {
if (file.isInitial) {
if (file.path.endsWith('.css')) {
initialCSS.push(file.path);
} else {
initialJS.push(file.path);
}
} else {
if (file.path.endsWith('.css')) {
asyncCSS.push(file.path);
} else {
asyncJS.push(file.path);
}
}

const relatedLICENSE = licenseMap.get(file.path);

if (relatedLICENSE) {
assets.add(relatedLICENSE);
}

for (const auxiliaryFile of file.chunk!.auxiliaryFiles) {
assets.add(auxiliaryFile);
}
}

entries[name] = {
initial: {
js: initialJS,
css: initialCSS,
},
async: {
js: asyncJS,
css: asyncCSS,
},
assets: Array.from(assets),
html: files.find((f) => f.name === htmlPaths[name])?.path,
};
}

return {
allFiles,
entries,
};
};

export const pluginManifest = (): RsbuildPlugin => ({
name: 'rsbuild:manifest',

setup(api) {
api.modifyBundlerChain(async (chain, { CHAIN_ID }) => {
const htmlPaths = api.getHTMLPaths();

const {
output: { manifest },
} = api.getNormalizedConfig();

if (manifest === false) {
return;
}

const fileName =
typeof manifest === 'string' ? manifest : 'manifest.json';

const { RspackManifestPlugin } = await import(
'../../compiled/rspack-manifest-plugin'
);

chain.plugin(CHAIN_ID.PLUGIN.MANIFEST).use(RspackManifestPlugin, [
{
fileName,
generate: generateManifest(htmlPaths),
},
]);
});
},
});
1 change: 1 addition & 0 deletions packages/core/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const rspackProvider: RsbuildProvider = async ({
plugins.performance(),
plugins.server(),
plugins.moduleFederation(),
plugins.manifest(),
import('./plugins/rspackProfile').then((m) => m.pluginRspackProfile()),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ function generateLinks(
): HtmlWebpackPlugin.HtmlTagObject[] {
// get all chunks
const extractedChunks = extractChunks({
// @ts-expect-error compilation type mismatch
compilation,
includeType: options.type,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
*/

import type { PreloadOrPreFetchOption } from '@rsbuild/shared';
import type { Compilation } from '@rspack/core';
import type { Chunk } from 'webpack';
import type { ChunkGroup } from './extractChunks';
import type { Chunk, ChunkGroup, Compilation } from '@rspack/core';
import type { BeforeAssetTagGenerationHtmlPluginData } from './type';

interface DoesChunkBelongToHtmlOptions {
Expand All @@ -39,7 +37,7 @@ function recursiveChunkGroup(
return parents.flatMap((chunkParent) => recursiveChunkGroup(chunkParent));
}

function recursiveChunkEntryNames(chunk: Chunk): string[] {
export function recursiveChunkEntryNames(chunk: Chunk): string[] {
const isChunkName = (name: string | undefined): name is string =>
Boolean(name);

Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/rspack/preload/helpers/extractChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
*/

import type { PreloadIncludeType } from '@rsbuild/shared';
import type { Chunk, Compilation } from 'webpack';

export type ChunkGroup = Compilation['chunkGroups'][0];
import type { Chunk, ChunkGroup, Compilation } from '@rspack/core';

interface ExtractChunks {
compilation: Compilation;
Expand All @@ -30,6 +28,8 @@ function isAsync(chunk: Chunk | ChunkGroup): boolean {
return !chunk.canBeInitial();
}
if ('isInitial' in chunk) {
// compat webpack
// @ts-expect-error
return !chunk.isInitial();
}
// compat rspack
Expand Down Expand Up @@ -64,21 +64,22 @@ export function extractChunks({
// Every asset, regardless of which chunk it's in.
// Wrap it in a single, "pseudo-chunk" return value.
// Note: webpack5 will extract license default, we do not need to preload them
const licenseAssets = [...compilation.assetsInfo.values()]
// @ts-expect-error
const licenseAssets = [...(compilation.assetsInfo?.values() || [])]
.map((info) => {
if (info.related?.license) {
return info.related.license;
}
return false;
})
.filter(Boolean);

return [
{
// @ts-expect-error ignore ts check for files
files: Object.keys(compilation.assets).filter(
(t) => !licenseAssets.includes(t),
),
},
} as Chunk,
];
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/tests/__snapshots__/inspect.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ exports[`inspectConfig > should print plugin names when inspect config 1`] = `
"rsbuild:performance",
"rsbuild:server",
"rsbuild:module-federation",
"rsbuild:manifest",
"rsbuild:rspack-profile",
]
`;
4 changes: 4 additions & 0 deletions packages/shared/src/types/config/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ export interface OutputConfig {
* Whether to disable code minification in production build.
*/
minify?: Minify;
/**
* Whether to generate manifest file.
*/
manifest?: string | boolean;
/**
* Whether to generate source map files, and which format of source map to generate
*/
Expand Down
Loading
Loading