From 41ea985f9317b11cfa6627a2d3f6b34ff4dbc134 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 29 Jan 2024 16:33:17 +0000 Subject: [PATCH] fix(@angular-devkit/build-angular): display server bundles in build stats This commit adds server bundle information to the build logs Closes #25855 --- .../src/builders/application/execute-build.ts | 2 + .../builders/application/setup-bundling.ts | 8 +- .../browser/specs/scripts-array_spec.ts | 2 +- .../src/tools/esbuild/budget-stats.ts | 6 + .../src/tools/esbuild/bundler-context.ts | 7 ++ .../build_angular/src/tools/esbuild/utils.ts | 35 ++++-- .../src/tools/webpack/utils/stats.ts | 111 ++++++++++++------ tests/legacy-cli/e2e/tests/basic/build.ts | 8 +- .../e2e/tests/basic/styles-array.ts | 4 +- .../e2e/tests/build/progress-and-stats.ts | 8 +- 10 files changed, 125 insertions(+), 66 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts b/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts index aeef526f2834..b839376dae34 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts @@ -36,6 +36,7 @@ export async function executeBuild( assets, cacheOptions, prerenderOptions, + ssrOptions, } = options; // TODO: Consider integrating into watch mode. Would require full rebuild on target changes. @@ -188,6 +189,7 @@ export async function executeBuild( budgetFailures, changedFiles, estimatedTransferSizes, + !!ssrOptions, ); // Write metafile if stats option is enabled diff --git a/packages/angular_devkit/build_angular/src/builders/application/setup-bundling.ts b/packages/angular_devkit/build_angular/src/builders/application/setup-bundling.ts index dea588873038..498d72ffa5b5 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/setup-bundling.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/setup-bundling.ts @@ -103,7 +103,6 @@ export function setupBundlerContexts( nodeTargets, codeBundleCache, ), - () => false, ), ); @@ -116,12 +115,7 @@ export function setupBundlerContexts( if (serverPolyfillBundleOptions) { bundlerContexts.push( - new BundlerContext( - workspaceRoot, - !!options.watch, - serverPolyfillBundleOptions, - () => false, - ), + new BundlerContext(workspaceRoot, !!options.watch, serverPolyfillBundleOptions), ); } } diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/scripts-array_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/scripts-array_spec.ts index ee55a57b2fc4..caf18f8a2def 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/scripts-array_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/scripts-array_spec.ts @@ -143,6 +143,6 @@ describe('Browser Builder scripts array', () => { expect(joinedLogs).toMatch(/lazy-script.+\d+ bytes/); expect(joinedLogs).toMatch(/renamed-script.+\d+ bytes/); expect(joinedLogs).toMatch(/renamed-lazy-script.+\d+ bytes/); - expect(joinedLogs).not.toContain('Lazy Chunks'); + expect(joinedLogs).not.toContain('Lazy chunks'); }); }); diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/budget-stats.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/budget-stats.ts index 0737e9825f24..d5148ffed3f4 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/budget-stats.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/budget-stats.ts @@ -33,6 +33,12 @@ export function generateBudgetStats( continue; } + // Exclude server bundles + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((entry as any)['ng-platform-server']) { + continue; + } + const initialRecord = initialFiles.get(file); let name = initialRecord?.name; if (name === undefined && entry.entryPoint) { diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts index b278c2d50e8b..652ebf985ff0 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts @@ -221,6 +221,13 @@ export class BundlerContext { // For non-incremental builds, perform a single build result = await build(this.#esbuildOptions); } + + if (this.#esbuildOptions?.platform === 'node') { + for (const entry of Object.values(result.metafile.outputs)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (entry as any)['ng-platform-server'] = true; + } + } } catch (failure) { // Build failures will throw an exception which contains errors/warnings if (isEsBuildFailure(failure)) { diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts index 424e2bd0252e..92f2bf6c7cba 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/utils.ts @@ -17,7 +17,7 @@ import { coerce } from 'semver'; import { NormalizedOutputOptions } from '../../builders/application/options'; import { BudgetCalculatorResult } from '../../utils/bundle-calculator'; import { Spinner } from '../../utils/spinner'; -import { BundleStats, generateBuildStatsTable } from '../webpack/utils/stats'; +import { BundleStats, generateEsbuildBuildStatsTable } from '../webpack/utils/stats'; import { BuildOutputFile, BuildOutputFileType, InitialFileRecord } from './bundler-context'; import { BuildOutputAsset } from './bundler-execution-result'; @@ -28,14 +28,18 @@ export function logBuildStats( budgetFailures: BudgetCalculatorResult[] | undefined, changedFiles?: Set, estimatedTransferSizes?: Map, + ssrOutputEnabled?: boolean, ): void { - const stats: BundleStats[] = []; + const browserStats: BundleStats[] = []; + const serverStats: BundleStats[] = []; let unchangedCount = 0; + for (const [file, output] of Object.entries(metafile.outputs)) { // Only display JavaScript and CSS files - if (!file.endsWith('.js') && !file.endsWith('.css')) { + if (!/\.(?:css|m?js)$/.test(file)) { continue; } + // Skip internal component resources // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((output as any)['ng-component']) { @@ -48,6 +52,13 @@ export function logBuildStats( continue; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const isPlatformServer = (output as any)['ng-platform-server']; + if (isPlatformServer && !ssrOutputEnabled) { + // Only log server build stats when SSR is enabled. + continue; + } + let name = initial.get(file)?.name; if (name === undefined && output.entryPoint) { name = path @@ -56,22 +67,28 @@ export function logBuildStats( .replace(/[\\/.]/g, '-'); } - stats.push({ + const stat: BundleStats = { initial: initial.has(file), stats: [file, name ?? '-', output.bytes, estimatedTransferSizes?.get(file) ?? '-'], - }); + }; + + if (isPlatformServer) { + serverStats.push(stat); + } else { + browserStats.push(stat); + } } - if (stats.length > 0) { - const tableText = generateBuildStatsTable( - stats, + if (browserStats.length > 0 || serverStats.length > 0) { + const tableText = generateEsbuildBuildStatsTable( + [browserStats, serverStats], true, unchangedCount === 0, !!estimatedTransferSizes, budgetFailures, ); - logger.info('\n' + tableText + '\n'); + logger.info(tableText + '\n'); } else if (changedFiles !== undefined) { logger.info('\nNo output file changes.\n'); } diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/utils/stats.ts b/packages/angular_devkit/build_angular/src/tools/webpack/utils/stats.ts index 2a7d506712ec..4260e2e0dc9e 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/utils/stats.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/utils/stats.ts @@ -7,7 +7,7 @@ */ import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; -import { logging, tags } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; import assert from 'node:assert'; import * as path from 'node:path'; import { Configuration, StatsCompilation } from 'webpack'; @@ -75,6 +75,38 @@ function generateBundleStats(info: { }; } +export function generateEsbuildBuildStatsTable( + [browserStats, serverStats]: [browserStats: BundleStats[], serverStats: BundleStats[]], + colors: boolean, + showTotalSize: boolean, + showEstimatedTransferSize: boolean, + budgetFailures?: BudgetCalculatorResult[], +): string { + const bundleInfo = generateBuildStatsData( + browserStats, + colors, + showTotalSize, + showEstimatedTransferSize, + budgetFailures, + ); + + if (serverStats.length) { + const m = (x: string) => (colors ? ansiColors.magenta(x) : x); + if (browserStats.length) { + bundleInfo.unshift([m('Browser bundles')]); + // Add seperators between browser and server logs + bundleInfo.push([], []); + } + + bundleInfo.push( + [m('Server bundles')], + ...generateBuildStatsData(serverStats, colors, false, false, undefined), + ); + } + + return generateTableText(bundleInfo, colors); +} + export function generateBuildStatsTable( data: BundleStats[], colors: boolean, @@ -82,11 +114,34 @@ export function generateBuildStatsTable( showEstimatedTransferSize: boolean, budgetFailures?: BudgetCalculatorResult[], ): string { - const g = (x: string) => (colors ? ansiColors.greenBright(x) : x); - const c = (x: string) => (colors ? ansiColors.cyanBright(x) : x); + const bundleInfo = generateBuildStatsData( + data, + colors, + showTotalSize, + showEstimatedTransferSize, + budgetFailures, + ); + + return generateTableText(bundleInfo, colors); +} + +function generateBuildStatsData( + data: BundleStats[], + colors: boolean, + showTotalSize: boolean, + showEstimatedTransferSize: boolean, + budgetFailures?: BudgetCalculatorResult[], +): (string | number)[][] { + if (data.length === 0) { + return []; + } + + const g = (x: string) => (colors ? ansiColors.green(x) : x); + const c = (x: string) => (colors ? ansiColors.cyan(x) : x); const r = (x: string) => (colors ? ansiColors.redBright(x) : x); const y = (x: string) => (colors ? ansiColors.yellowBright(x) : x); const bold = (x: string) => (colors ? ansiColors.bold(x) : x); + const dim = (x: string) => (colors ? ansiColors.dim(x) : x); const getSizeColor = (name: string, file?: string, defaultColor = c) => { const severity = budgets.get(name) || (file && budgets.get(file)); @@ -138,7 +193,7 @@ export function generateBuildStatsTable( if (showEstimatedTransferSize) { data = [ g(files), - names, + dim(names), getRawSizeColor(typeof rawSize === 'number' ? formatSize(rawSize) : rawSize), c( typeof estimatedTransferSize === 'number' @@ -149,7 +204,7 @@ export function generateBuildStatsTable( } else { data = [ g(files), - names, + dim(names), getRawSizeColor(typeof rawSize === 'number' ? formatSize(rawSize) : rawSize), '', ]; @@ -172,17 +227,17 @@ export function generateBuildStatsTable( } const bundleInfo: (string | number)[][] = []; - const baseTitles = ['Names', 'Raw Size']; + const baseTitles = ['Names', 'Raw size']; const tableAlign: ('l' | 'r')[] = ['l', 'l', 'r']; if (showEstimatedTransferSize) { - baseTitles.push('Estimated Transfer Size'); + baseTitles.push('Estimated transfer size'); tableAlign.push('r'); } // Entry chunks if (changedEntryChunksStats.length) { - bundleInfo.push(['Initial Chunk Files', ...baseTitles].map(bold), ...changedEntryChunksStats); + bundleInfo.push(['Initial chunk files', ...baseTitles].map(bold), ...changedEntryChunksStats); if (showTotalSize) { bundleInfo.push([]); @@ -190,7 +245,7 @@ export function generateBuildStatsTable( const initialSizeTotalColor = getSizeColor('bundle initial', undefined, (x) => x); const totalSizeElements = [ ' ', - 'Initial Total', + 'Initial total', initialSizeTotalColor(formatSize(initialTotalRawSize)), ]; if (showEstimatedTransferSize) { @@ -211,10 +266,10 @@ export function generateBuildStatsTable( // Lazy chunks if (changedLazyChunksStats.length) { - bundleInfo.push(['Lazy Chunk Files', ...baseTitles].map(bold), ...changedLazyChunksStats); + bundleInfo.push(['Lazy chunk files', ...baseTitles].map(bold), ...changedLazyChunksStats); } - return generateTableText(bundleInfo, colors); + return bundleInfo; } function generateTableText(bundleInfo: (string | number)[][], colors: boolean): string { @@ -255,12 +310,6 @@ function generateTableText(bundleInfo: (string | number)[][], colors: boolean): return outputTable.join('\n'); } -function generateBuildStats(hash: string, time: number, colors: boolean): string { - const w = (x: string) => (colors ? ansiColors.bold.white(x) : x); - - return `Build at: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms`; -} - // We use this cache because we can have multiple builders running in the same process, // where each builder has different output path. @@ -279,6 +328,7 @@ function statsToString( const colors = statsConfig.colors; const rs = (x: string) => (colors ? ansiColors.reset(x) : x); + const w = (x: string) => (colors ? ansiColors.bold.white(x) : x); const changedChunksStats: BundleStats[] = []; let unchangedChunkNumber = 0; @@ -330,30 +380,13 @@ function statsToString( // In some cases we do things outside of webpack context // Such us index generation, service worker augmentation etc... // This will correct the time and include these. - const time = getBuildDuration(json); - if (unchangedChunkNumber > 0) { - return ( - '\n' + - rs(tags.stripIndents` - ${statsTable} - - ${unchangedChunkNumber} unchanged chunks - - ${generateBuildStats(json.hash || '', time, colors)} - `) - ); - } else { - return ( - '\n' + - rs(tags.stripIndents` - ${statsTable} - - ${generateBuildStats(json.hash || '', time, colors)} - `) - ); - } + return rs( + `\n${statsTable}\n\n` + + (unchangedChunkNumber > 0 ? `${unchangedChunkNumber} unchanged chunks\n\n` : '') + + `Build at: ${w(new Date().toISOString())} - Hash: ${w(json.hash || '')} - Time: ${w('' + time)}ms`, + ); } export function statsWarningsToString( diff --git a/tests/legacy-cli/e2e/tests/basic/build.ts b/tests/legacy-cli/e2e/tests/basic/build.ts index 173bbb40a0c5..e6c8ef5bf1e5 100644 --- a/tests/legacy-cli/e2e/tests/basic/build.ts +++ b/tests/legacy-cli/e2e/tests/basic/build.ts @@ -7,9 +7,9 @@ export default async function () { const { stdout: stdout1 } = await ng('build', '--configuration=development'); await expectFileToMatch('dist/test-project/browser/index.html', 'main.js'); - if (stdout1.includes('Estimated Transfer Size')) { + if (stdout1.includes('Estimated transfer size')) { throw new Error( - `Expected stdout not to contain 'Estimated Transfer Size' but it did.\n${stdout1}`, + `Expected stdout not to contain 'Estimated transfer size' but it did.\n${stdout1}`, ); } @@ -22,9 +22,9 @@ export default async function () { await expectFileToMatch('dist/test-project/browser/index.html', /main\.[a-zA-Z0-9]{16}\.js/); } - if (!stdout2.includes('Estimated Transfer Size')) { + if (!stdout2.includes('Estimated transfer size')) { throw new Error( - `Expected stdout to contain 'Estimated Transfer Size' but it did not.\n${stdout2}`, + `Expected stdout to contain 'Estimated transfer size' but it did not.\n${stdout2}`, ); } } diff --git a/tests/legacy-cli/e2e/tests/basic/styles-array.ts b/tests/legacy-cli/e2e/tests/basic/styles-array.ts index 800761c1698a..1639f8863aac 100644 --- a/tests/legacy-cli/e2e/tests/basic/styles-array.ts +++ b/tests/legacy-cli/e2e/tests/basic/styles-array.ts @@ -42,8 +42,8 @@ export default async function () { ); // Non injected styles should be listed under lazy chunk files - if (!/Lazy Chunk Files[\s\S]+renamed-lazy-style\.css/m.test(stdout)) { + if (!/Lazy chunk files[\s\S]+renamed-lazy-style\.css/m.test(stdout)) { console.log(stdout); - throw new Error(`Expected "renamed-lazy-style.css" to be listed under "Lazy Chunk Files".`); + throw new Error(`Expected "renamed-lazy-style.css" to be listed under "Lazy chunk files".`); } } diff --git a/tests/legacy-cli/e2e/tests/build/progress-and-stats.ts b/tests/legacy-cli/e2e/tests/build/progress-and-stats.ts index d1e2af1acbda..ec474bd4ce56 100644 --- a/tests/legacy-cli/e2e/tests/build/progress-and-stats.ts +++ b/tests/legacy-cli/e2e/tests/build/progress-and-stats.ts @@ -3,13 +3,13 @@ import { ng } from '../../utils/process'; export default async function () { const { stderr: stderrProgress, stdout } = await ng('build', '--progress'); - if (!stdout.includes('Initial Total')) { - throw new Error(`Expected stdout to contain 'Initial Total' but it did not.\n${stdout}`); + if (!stdout.includes('Initial total')) { + throw new Error(`Expected stdout to contain 'Initial total' but it did not.\n${stdout}`); } - if (!stdout.includes('Estimated Transfer Size')) { + if (!stdout.includes('Estimated transfer size')) { throw new Error( - `Expected stdout to contain 'Estimated Transfer Size' but it did not.\n${stdout}`, + `Expected stdout to contain 'Estimated transfer size' but it did not.\n${stdout}`, ); }