Skip to content

Commit 72ef689

Browse files
Updating per code review instructions and adding more unit tests
1 parent dd80e0f commit 72ef689

6 files changed

Lines changed: 153 additions & 47 deletions

File tree

packages/angular/build/src/builders/application/chunk-optimizer.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
BundleContextResult,
2727
InitialFileRecord,
2828
} from '../../tools/esbuild/bundler-context';
29-
import { createOutputFile } from '../../tools/esbuild/utils';
29+
import { createOutputFile, filterMetafile } from '../../tools/esbuild/utils';
3030
import { assertIsError } from '../../utils/error';
3131

3232
/**
@@ -288,17 +288,7 @@ export async function optimizeChunks(
288288
// Update the isolated browser metafile to reflect the optimized output.
289289
// Server outputs are excluded since chunk optimization only affects browser bundles.
290290
const serverOutputPaths = new Set(Object.keys(original.serverMetafile?.outputs ?? {}));
291-
const browserOutputs: Metafile['outputs'] = {};
292-
const browserInputs: Metafile['inputs'] = {};
293-
for (const [path, output] of Object.entries(newMetafile.outputs)) {
294-
if (!serverOutputPaths.has(path)) {
295-
browserOutputs[path] = output;
296-
for (const inputPath of Object.keys(output.inputs)) {
297-
browserInputs[inputPath] = newMetafile.inputs[inputPath];
298-
}
299-
}
300-
}
301-
original.browserMetafile = { inputs: browserInputs, outputs: browserOutputs };
291+
original.browserMetafile = filterMetafile(newMetafile, (path) => !serverOutputPaths.has(path));
302292

303293
// Remove used chunks and associated sourcemaps from the original result
304294
original.outputFiles = original.outputFiles.filter(

packages/angular/build/src/builders/application/execute-build.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,21 @@
77
*/
88

99
import { BuilderContext } from '@angular-devkit/architect';
10-
import type { Metafile } from 'esbuild';
1110
import { createAngularCompilation } from '../../tools/angular/compilation';
1211
import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
1312
import { generateBudgetStats } from '../../tools/esbuild/budget-stats';
1413
import {
1514
BuildOutputFileType,
1615
BundleContextResult,
1716
BundlerContext,
18-
InitialFileRecord,
1917
} from '../../tools/esbuild/bundler-context';
2018
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
2119
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
2220
import { extractLicenses } from '../../tools/esbuild/license-extractor';
2321
import { profileAsync } from '../../tools/esbuild/profiling';
2422
import {
2523
calculateEstimatedTransferSizes,
24+
filterMetafile,
2625
logBuildStats,
2726
transformSupportedBrowsersToTargets,
2827
} from '../../tools/esbuild/utils';
@@ -39,32 +38,6 @@ import { inlineI18n, loadActiveTranslations } from './i18n';
3938
import { NormalizedApplicationBuildOptions } from './options';
4039
import { createComponentStyleBundler, setupBundlerContexts } from './setup-bundling';
4140

42-
function filterMetafileByInitialFiles(
43-
metafile: Metafile,
44-
initialFiles: Map<string, InitialFileRecord>,
45-
): Metafile {
46-
const filteredOutputs: Metafile['outputs'] = {};
47-
const referencedInputs = new Set<string>();
48-
49-
for (const [path, output] of Object.entries(metafile.outputs)) {
50-
if (initialFiles.has(path)) {
51-
filteredOutputs[path] = output;
52-
for (const inputPath of Object.keys(output.inputs)) {
53-
referencedInputs.add(inputPath);
54-
}
55-
}
56-
}
57-
58-
const filteredInputs: Metafile['inputs'] = {};
59-
for (const [inputPath, input] of Object.entries(metafile.inputs)) {
60-
if (referencedInputs.has(inputPath)) {
61-
filteredInputs[inputPath] = input;
62-
}
63-
}
64-
65-
return { inputs: filteredInputs, outputs: filteredOutputs };
66-
}
67-
6841
// eslint-disable-next-line max-lines-per-function
6942
export async function executeBuild(
7043
options: NormalizedApplicationBuildOptions,
@@ -335,23 +308,39 @@ export async function executeBuild(
335308
if (options.stats) {
336309
executionResult.addOutputFile(
337310
'browser-stats.json',
338-
JSON.stringify(browserMetafile, null, 2),
311+
JSON.stringify(
312+
filterMetafile(browserMetafile, (path) => !initialFiles.has(path)),
313+
null,
314+
2,
315+
),
339316
BuildOutputFileType.Root,
340317
);
341318
executionResult.addOutputFile(
342319
'browser-initial-stats.json',
343-
JSON.stringify(filterMetafileByInitialFiles(browserMetafile, initialFiles), null, 2),
320+
JSON.stringify(
321+
filterMetafile(browserMetafile, (path) => initialFiles.has(path)),
322+
null,
323+
2,
324+
),
344325
BuildOutputFileType.Root,
345326
);
346327
if (ssrOutputEnabled && serverMetafile) {
347328
executionResult.addOutputFile(
348329
'server-stats.json',
349-
JSON.stringify(serverMetafile, null, 2),
330+
JSON.stringify(
331+
filterMetafile(serverMetafile, (path) => !initialFiles.has(path)),
332+
null,
333+
2,
334+
),
350335
BuildOutputFileType.Root,
351336
);
352337
executionResult.addOutputFile(
353338
'server-initial-stats.json',
354-
JSON.stringify(filterMetafileByInitialFiles(serverMetafile, initialFiles), null, 2),
339+
JSON.stringify(
340+
filterMetafile(serverMetafile, (path) => initialFiles.has(path)),
341+
null,
342+
2,
343+
),
355344
BuildOutputFileType.Root,
356345
);
357346
}

packages/angular/build/src/tools/esbuild/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,40 @@ import {
2929
PrerenderedRoutesRecord,
3030
} from './bundler-execution-result';
3131

32+
/**
33+
* Filters an esbuild metafile to only include outputs matching a given predicate,
34+
* along with the inputs referenced by those outputs.
35+
* @param metafile The esbuild metafile to filter.
36+
* @param predicate A function that receives an output path and returns `true` if the output
37+
* should be included.
38+
* @returns A new metafile containing only the matching outputs and their referenced inputs.
39+
*/
40+
export function filterMetafile(
41+
metafile: Metafile,
42+
predicate: (outputPath: string) => boolean,
43+
): Metafile {
44+
const filteredOutputs: Metafile['outputs'] = {};
45+
const referencedInputs = new Set<string>();
46+
47+
for (const [path, output] of Object.entries(metafile.outputs)) {
48+
if (predicate(path)) {
49+
filteredOutputs[path] = output;
50+
for (const inputPath of Object.keys(output.inputs)) {
51+
referencedInputs.add(inputPath);
52+
}
53+
}
54+
}
55+
56+
const filteredInputs: Metafile['inputs'] = {};
57+
for (const [inputPath, input] of Object.entries(metafile.inputs)) {
58+
if (referencedInputs.has(inputPath)) {
59+
filteredInputs[inputPath] = input;
60+
}
61+
}
62+
63+
return { inputs: filteredInputs, outputs: filteredOutputs };
64+
}
65+
3266
export function logBuildStats(
3367
metafile: Metafile,
3468
outputFiles: BuildOutputFile[],

packages/angular_devkit/build_angular/src/builders/browser/tests/options/stats-json_spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,67 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
5757
}
5858
});
5959

60+
it('browser-initial-stats.json contains only initial chunks', async () => {
61+
harness.useTarget('build', {
62+
...BASE_OPTIONS,
63+
statsJson: true,
64+
});
65+
66+
const { result } = await harness.executeOnce();
67+
expect(result?.success).toBe(true);
68+
69+
const raw = harness.readFile('dist/browser-initial-stats.json');
70+
const stats = JSON.parse(raw) as { chunks?: { initial?: boolean }[] };
71+
72+
for (const chunk of stats.chunks ?? []) {
73+
expect(chunk.initial)
74+
.withContext('browser-initial-stats.json should only contain initial chunks')
75+
.toBeTrue();
76+
}
77+
});
78+
79+
it('browser-stats.json contains only non-initial chunks', async () => {
80+
harness.useTarget('build', {
81+
...BASE_OPTIONS,
82+
statsJson: true,
83+
});
84+
85+
const { result } = await harness.executeOnce();
86+
expect(result?.success).toBe(true);
87+
88+
const raw = harness.readFile('dist/browser-stats.json');
89+
const stats = JSON.parse(raw) as { chunks?: { initial?: boolean }[] };
90+
91+
for (const chunk of stats.chunks ?? []) {
92+
expect(chunk.initial)
93+
.withContext('browser-stats.json should not contain initial chunks')
94+
.toBeFalse();
95+
}
96+
});
97+
98+
it('browser-stats.json and browser-initial-stats.json chunks have no overlap', async () => {
99+
harness.useTarget('build', {
100+
...BASE_OPTIONS,
101+
statsJson: true,
102+
});
103+
104+
const { result } = await harness.executeOnce();
105+
expect(result?.success).toBe(true);
106+
107+
type StatsJson = { chunks?: { id?: number | string; initial?: boolean }[] };
108+
const nonInitialStats = JSON.parse(harness.readFile('dist/browser-stats.json')) as StatsJson;
109+
const initialStats = JSON.parse(
110+
harness.readFile('dist/browser-initial-stats.json'),
111+
) as StatsJson;
112+
113+
const nonInitialIds = new Set((nonInitialStats.chunks ?? []).map((c) => c.id));
114+
for (const chunk of initialStats.chunks ?? []) {
115+
expect(nonInitialIds.has(chunk.id))
116+
.withContext(`Chunk '${chunk.id}' should not appear in both stats files`)
117+
.toBeFalse();
118+
}
119+
});
120+
60121
it('does not generate a Webpack Stats file in output when false', async () => {
61122
harness.useTarget('build', {
62123
...BASE_OPTIONS,

packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,35 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
243243
}
244244

245245
if (buildOptions.statsJson) {
246+
const getInitialChunkIds = (data: import('webpack').StatsCompilation): Set<string | number> =>
247+
new Set(
248+
data.chunks?.filter((c) => c.initial).flatMap((c) => (c.id != null ? [c.id] : [])) ?? [],
249+
);
250+
246251
extraPlugins.push(
247-
new JsonStatsPlugin(path.resolve(root, buildOptions.outputPath, 'browser-stats.json')),
252+
new JsonStatsPlugin(
253+
path.resolve(root, buildOptions.outputPath, 'browser-stats.json'),
254+
(data) => {
255+
const initialChunkIds = getInitialChunkIds(data);
256+
257+
return {
258+
...data,
259+
chunks: data.chunks?.filter((c) => !c.initial),
260+
assets: data.assets?.filter((a) => !a.chunks?.some((id) => initialChunkIds.has(id))),
261+
};
262+
},
263+
),
248264
new JsonStatsPlugin(
249265
path.resolve(root, buildOptions.outputPath, 'browser-initial-stats.json'),
266+
(data) => {
267+
const initialChunkIds = getInitialChunkIds(data);
268+
269+
return {
270+
...data,
271+
chunks: data.chunks?.filter((c) => c.initial),
272+
assets: data.assets?.filter((a) => a.chunks?.some((id) => initialChunkIds.has(id))),
273+
};
274+
},
250275
),
251276
);
252277
}

packages/angular_devkit/build_angular/src/tools/webpack/plugins/json-stats-plugin.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,24 @@ import { createWriteStream } from 'node:fs';
1010
import { mkdir } from 'node:fs/promises';
1111
import { dirname } from 'node:path';
1212
import { pipeline } from 'node:stream/promises';
13+
import type { StatsCompilation } from 'webpack';
1314
import { Compiler } from 'webpack';
1415
import { assertIsError } from '../../../utils/error';
1516
import { addError } from '../../../utils/webpack-diagnostics';
1617

1718
export class JsonStatsPlugin {
18-
constructor(private readonly statsOutputPath: string) {}
19+
constructor(
20+
private readonly statsOutputPath: string,
21+
private readonly transform?: (data: StatsCompilation) => StatsCompilation,
22+
) {}
1923

2024
apply(compiler: Compiler) {
2125
compiler.hooks.done.tapPromise('angular-json-stats', async (stats) => {
2226
const { stringifyChunked } = await import('@discoveryjs/json-ext');
23-
const data = stats.toJson('verbose');
27+
let data = stats.toJson('verbose');
28+
if (this.transform) {
29+
data = this.transform(data);
30+
}
2431

2532
try {
2633
await mkdir(dirname(this.statsOutputPath), { recursive: true });

0 commit comments

Comments
 (0)