From 9545676bec661e441b770c2523819e052e72a12f Mon Sep 17 00:00:00 2001 From: Troy Steuwer Date: Sun, 1 Mar 2026 18:13:39 -0500 Subject: [PATCH 1/2] feat(@angular/build): Support splitting browser and server stats json files for easier consumption This feature supports splitting out the browser and server stats json files so it's easier to inspect the bundle in various analyzers. Today, everything gets dumped into a single file and it's nearly impossible to use without hours of fix -> remove unused browser chunks -> analyze and starting the loop all over again. This feature implements the feature request I made in #28185, along with another developers request to see a stats json file for just the initial page bundle. I've tested this out in my own repository and it's already helped an incredible amount. Fixes #28185 #28671 --- .../src/builders/application/execute-build.ts | 16 +++++-- .../src/builders/application/schema.json | 2 +- .../angular/build/src/tools/esbuild/utils.ts | 44 +++++++++++++++++++ .../src/builders/browser-esbuild/schema.json | 2 +- .../src/builders/browser/schema.json | 2 +- .../builders/browser/specs/stats-json_spec.ts | 6 +-- .../src/builders/server/schema.json | 2 +- .../src/tools/webpack/configs/common.ts | 7 ++- 8 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index aaddc5b6ef7e..d844ab0814c6 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -20,6 +20,7 @@ import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; import { extractLicenses } from '../../tools/esbuild/license-extractor'; import { profileAsync } from '../../tools/esbuild/profiling'; import { + buildMetafileForType, calculateEstimatedTransferSizes, logBuildStats, transformSupportedBrowsersToTargets, @@ -301,13 +302,22 @@ export async function executeBuild( BuildOutputFileType.Root, ); + const ssrOutputEnabled: boolean = !!ssrOptions; + // Write metafile if stats option is enabled if (options.stats) { executionResult.addOutputFile( - 'stats.json', - JSON.stringify(metafile, null, 2), + 'browser-stats.json', + JSON.stringify(buildMetafileForType(metafile, 'browser', outputFiles), null, 2), BuildOutputFileType.Root, ); + if (ssrOutputEnabled) { + executionResult.addOutputFile( + 'server-stats.json', + JSON.stringify(buildMetafileForType(metafile, 'server', outputFiles), null, 2), + BuildOutputFileType.Root, + ); + } } if (!jsonLogs) { @@ -322,7 +332,7 @@ export async function executeBuild( colors, changedFiles, estimatedTransferSizes, - !!ssrOptions, + ssrOutputEnabled, verbose, ), ); diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 5498a21fe004..76203437650b 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -535,7 +535,7 @@ }, "statsJson": { "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed with https://esbuild.github.io/analyze/.", + "description": "Generates a 'browser-stats.json' (and 'server-stats.json' when SSR is enabled) file which can be analyzed with https://esbuild.github.io/analyze/.", "default": false }, "budgets": { diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts index 2730dafae97c..9132822fda28 100644 --- a/packages/angular/build/src/tools/esbuild/utils.ts +++ b/packages/angular/build/src/tools/esbuild/utils.ts @@ -29,6 +29,50 @@ import { PrerenderedRoutesRecord, } from './bundler-execution-result'; +export function buildMetafileForType( + metafile: Metafile, + type: 'browser' | 'server', + outputFiles: BuildOutputFile[], +): Metafile { + const outputPathsForType = new Set( + outputFiles + .filter(({ type: fileType }) => { + const isServerFile = + fileType === BuildOutputFileType.ServerApplication || + fileType === BuildOutputFileType.ServerRoot; + + return type === 'server' ? isServerFile : !isServerFile; + }) + .map(({ path }) => path), + ); + + const filteredOutputs: Metafile['outputs'] = {}; + for (const [outputPath, output] of Object.entries(metafile.outputs)) { + if (outputPathsForType.has(outputPath)) { + filteredOutputs[outputPath] = output; + } + } + + const referencedInputs = new Set(); + for (const output of Object.values(filteredOutputs)) { + for (const inputPath of Object.keys(output.inputs)) { + referencedInputs.add(inputPath); + } + } + + const filteredInputs: Metafile['inputs'] = {}; + for (const [inputPath, input] of Object.entries(metafile.inputs)) { + if (referencedInputs.has(inputPath)) { + filteredInputs[inputPath] = input; + } + } + + return { + inputs: filteredInputs, + outputs: filteredOutputs, + }; +} + export function logBuildStats( metafile: Metafile, outputFiles: BuildOutputFile[], diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/schema.json b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/schema.json index edb91222d954..5db2b753d7bf 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/schema.json @@ -406,7 +406,7 @@ }, "statsJson": { "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", + "description": "Generates a 'browser-stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, "budgets": { diff --git a/packages/angular_devkit/build_angular/src/builders/browser/schema.json b/packages/angular_devkit/build_angular/src/builders/browser/schema.json index 301eeafcc4f1..bf73adeabb29 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/browser/schema.json @@ -394,7 +394,7 @@ }, "statsJson": { "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", + "description": "Generates a 'browser-stats.json' (and 'server-stats.json' when SSR is enabled) file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, "budgets": { diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/stats-json_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/stats-json_spec.ts index 175b61d7ca12..2bbe9e52e72d 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/stats-json_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/stats-json_spec.ts @@ -21,13 +21,13 @@ describe('Browser Builder stats json', () => { it('works', async () => { const { files } = await browserBuild(architect, host, target, { statsJson: true }); - expect('stats.json' in files).toBe(true); + expect('browser-stats.json' in files).toBe(true); }); it('works with profile flag', async () => { const { files } = await browserBuild(architect, host, target, { statsJson: true }); - expect('stats.json' in files).toBe(true); - const stats = JSON.parse(await files['stats.json']); + expect('browser-stats.json' in files).toBe(true); + const stats = JSON.parse(await files['browser-stats.json']); expect(stats.chunks[0].modules[0].profile.building).toBeDefined(); }); }); diff --git a/packages/angular_devkit/build_angular/src/builders/server/schema.json b/packages/angular_devkit/build_angular/src/builders/server/schema.json index 2375e1176add..99115adb333c 100644 --- a/packages/angular_devkit/build_angular/src/builders/server/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/server/schema.json @@ -208,7 +208,7 @@ }, "statsJson": { "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", + "description": "Generates a 'browser-stats.json' and 'server-stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, "watch": { diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts index b18c8ebc9be9..8375751e9403 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts @@ -83,9 +83,8 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise Date: Sun, 1 Mar 2026 19:16:38 -0500 Subject: [PATCH 2/2] fix(@angular/build): Fixing the missing browser initial stats file that was in my main branch off of @angular/angular-cli repository This addresses an issue of including the (browser|server)-initial-stats.json file that was in my original repository. It also fixes unit tests that were also addressed in the original repo. --- .../src/builders/application/execute-build.ts | 22 +++++++++++++++++-- .../angular/build/src/tools/esbuild/utils.ts | 14 +++++++++--- .../browser/tests/options/stats-json_spec.ts | 20 ++++++++++++----- .../src/tools/webpack/configs/common.ts | 3 +++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index d844ab0814c6..62f8d948566d 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -308,13 +308,31 @@ export async function executeBuild( if (options.stats) { executionResult.addOutputFile( 'browser-stats.json', - JSON.stringify(buildMetafileForType(metafile, 'browser', outputFiles), null, 2), + JSON.stringify(buildMetafileForType(metafile, 'browser', false, outputFiles), null, 2), + BuildOutputFileType.Root, + ); + executionResult.addOutputFile( + 'browser-initial-stats.json', + JSON.stringify( + buildMetafileForType(metafile, 'browser', true, outputFiles, initialFiles), + null, + 2, + ), BuildOutputFileType.Root, ); if (ssrOutputEnabled) { executionResult.addOutputFile( 'server-stats.json', - JSON.stringify(buildMetafileForType(metafile, 'server', outputFiles), null, 2), + JSON.stringify(buildMetafileForType(metafile, 'server', false, outputFiles), null, 2), + BuildOutputFileType.Root, + ); + executionResult.addOutputFile( + 'server-initial-stats.json', + JSON.stringify( + buildMetafileForType(metafile, 'server', true, outputFiles, initialFiles), + null, + 2, + ), BuildOutputFileType.Root, ); } diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts index 9132822fda28..c27fda8f275a 100644 --- a/packages/angular/build/src/tools/esbuild/utils.ts +++ b/packages/angular/build/src/tools/esbuild/utils.ts @@ -32,8 +32,12 @@ import { export function buildMetafileForType( metafile: Metafile, type: 'browser' | 'server', + initial: boolean, outputFiles: BuildOutputFile[], + initialFiles?: Map, ): Metafile { + const isServer = type === 'server'; + const outputPathsForType = new Set( outputFiles .filter(({ type: fileType }) => { @@ -41,16 +45,20 @@ export function buildMetafileForType( fileType === BuildOutputFileType.ServerApplication || fileType === BuildOutputFileType.ServerRoot; - return type === 'server' ? isServerFile : !isServerFile; + return isServer ? isServerFile : !isServerFile; }) .map(({ path }) => path), ); const filteredOutputs: Metafile['outputs'] = {}; for (const [outputPath, output] of Object.entries(metafile.outputs)) { - if (outputPathsForType.has(outputPath)) { - filteredOutputs[outputPath] = output; + if (!outputPathsForType.has(outputPath)) { + continue; + } + if (initial && !initialFiles?.has(outputPath)) { + continue; } + filteredOutputs[outputPath] = output; } const referencedInputs = new Set(); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/stats-json_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/stats-json_spec.ts index 609e21fcef5a..2a901997e333 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/stats-json_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/stats-json_spec.ts @@ -26,12 +26,18 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - if (harness.expectFile('dist/stats.json').toExist()) { - const content = harness.readFile('dist/stats.json'); + if (harness.expectFile('dist/browser-stats.json').toExist()) { + const content = harness.readFile('dist/browser-stats.json'); expect(() => JSON.parse(content)) .withContext('Expected Webpack Stats file to be valid JSON.') .not.toThrow(); } + if (harness.expectFile('dist/browser-initial-stats.json').toExist()) { + const initialContent = harness.readFile('dist/browser-initial-stats.json'); + expect(() => JSON.parse(initialContent)) + .withContext('Expected Webpack Stats file to be valid JSON.') + .not.toThrow(); + } }); // TODO: Investigate why this profiling object is no longer present in Webpack 5.90.3+ and if this should even be tested @@ -45,8 +51,8 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - if (harness.expectFile('dist/stats.json').toExist()) { - const stats = JSON.parse(harness.readFile('dist/stats.json')); + if (harness.expectFile('dist/browser-stats.json').toExist()) { + const stats = JSON.parse(harness.readFile('dist/browser-stats.json')); expect(stats?.chunks?.[0]?.modules?.[0]?.profile?.building).toBeDefined(); } }); @@ -61,7 +67,8 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - harness.expectFile('dist/stats.json').toNotExist(); + harness.expectFile('dist/browser-stats.json').toNotExist(); + harness.expectFile('dist/browser-initial-stats.json').toNotExist(); }); it('does not generate a Webpack Stats file in output when not present', async () => { @@ -73,7 +80,8 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { expect(result?.success).toBe(true); - harness.expectFile('dist/stats.json').toNotExist(); + harness.expectFile('dist/browser-stats.json').toNotExist(); + harness.expectFile('dist/browser-initial-stats.json').toNotExist(); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts index 8375751e9403..37c7423c566d 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts @@ -245,6 +245,9 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise