From 2d141fe3bc1efb9e254b15ce91ebc885a43c928a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 15 May 2023 12:35:16 -0400 Subject: [PATCH] feat(@angular-devkit/build-angular): show estimated transfer size with esbuild builder When using the esbuild-based browser application builder, the console build stats output will now show the estimated transfer size of JavaScript and CSS files when optimizations are enabled. This provides similar behavior to the default Webpack-based builder. --- .../src/builders/browser-esbuild/index.ts | 56 +++++++++++++++++-- tests/legacy-cli/e2e/tests/basic/build.ts | 14 +++-- .../e2e/tests/build/progress-and-stats.ts | 20 +++---- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts index d2e1afee2e00..be61739b076a 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts @@ -11,6 +11,8 @@ import type { BuildOptions, Metafile, OutputFile } from 'esbuild'; import { constants as fsConstants } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; +import { promisify } from 'node:util'; +import { brotliCompress } from 'node:zlib'; import { copyAssets } from '../../utils/copy-assets'; import { assertIsError } from '../../utils/error'; import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets'; @@ -33,6 +35,8 @@ import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin'; import { shutdownSassWorkerPool } from './stylesheets/sass-plugin'; import type { ChangedFiles } from './watcher'; +const compressAsync = promisify(brotliCompress); + interface RebuildState { rebuildContexts: BundlerContext[]; codeBundleCache?: SourceFileCache; @@ -259,7 +263,12 @@ async function execute( } } - logBuildStats(context, metafile, initialFiles); + // Calculate estimated transfer size if scripts are optimized + let estimatedTransferSizes; + if (optimizationOptions.scripts || optimizationOptions.styles.minify) { + estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles); + } + logBuildStats(context, metafile, initialFiles, estimatedTransferSizes); const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9; context.logger.info(`Application bundle generation complete. [${buildTime.toFixed(3)} seconds]`); @@ -700,7 +709,12 @@ export async function* buildEsbuildBrowserInternal( export default createBuilder(buildEsbuildBrowser); -function logBuildStats(context: BuilderContext, metafile: Metafile, initialFiles: FileInfo[]) { +function logBuildStats( + context: BuilderContext, + metafile: Metafile, + initialFiles: FileInfo[], + estimatedTransferSizes?: Map, +) { const initial = new Map(initialFiles.map((info) => [info.file, info.name])); const stats: BundleStats[] = []; for (const [file, output] of Object.entries(metafile.outputs)) { @@ -716,11 +730,45 @@ function logBuildStats(context: BuilderContext, metafile: Metafile, initialFiles stats.push({ initial: initial.has(file), - stats: [file, initial.get(file) ?? '-', output.bytes, ''], + stats: [ + file, + initial.get(file) ?? '-', + output.bytes, + estimatedTransferSizes?.get(file) ?? '-', + ], }); } - const tableText = generateBuildStatsTable(stats, true, true, false, undefined); + const tableText = generateBuildStatsTable(stats, true, true, !!estimatedTransferSizes, undefined); context.logger.info('\n' + tableText + '\n'); } + +async function calculateEstimatedTransferSizes(outputFiles: OutputFile[]) { + const sizes = new Map(); + + const pendingCompression = []; + for (const outputFile of outputFiles) { + // Only calculate JavaScript and CSS files + if (!outputFile.path.endsWith('.js') && !outputFile.path.endsWith('.css')) { + continue; + } + + // Skip compressing small files which may end being larger once compressed and will most likely not be + // compressed in actual transit. + if (outputFile.contents.byteLength < 1024) { + sizes.set(outputFile.path, outputFile.contents.byteLength); + continue; + } + + pendingCompression.push( + compressAsync(outputFile.contents).then((result) => + sizes.set(outputFile.path, result.byteLength), + ), + ); + } + + await Promise.all(pendingCompression); + + return sizes; +} diff --git a/tests/legacy-cli/e2e/tests/basic/build.ts b/tests/legacy-cli/e2e/tests/basic/build.ts index 51fcca4b9bcd..04d3af02c1b2 100644 --- a/tests/legacy-cli/e2e/tests/basic/build.ts +++ b/tests/legacy-cli/e2e/tests/basic/build.ts @@ -4,21 +4,27 @@ import { ng } from '../../utils/process'; export default async function () { // Development build - const { stdout } = await ng('build', '--configuration=development'); + const { stdout: stdout1 } = await ng('build', '--configuration=development'); await expectFileToMatch('dist/test-project/index.html', 'main.js'); - if (stdout.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${stdout}`, + `Expected stdout not to contain 'Estimated Transfer Size' but it did.\n${stdout1}`, ); } // Production build - await ng('build'); + const { stdout: stdout2 } = await ng('build'); if (getGlobalVariable('argv')['esbuild']) { // esbuild uses an 8 character hash await expectFileToMatch('dist/test-project/index.html', /main\.[a-zA-Z0-9]{8}\.js/); } else { await expectFileToMatch('dist/test-project/index.html', /main\.[a-zA-Z0-9]{16}\.js/); } + + if (!stdout2.includes('Estimated Transfer Size')) { + throw new Error( + `Expected stdout to contain 'Estimated Transfer Size' but it did not.\n${stdout2}`, + ); + } } 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 eb4c9147ef44..f0c7a9ba360f 100644 --- a/tests/legacy-cli/e2e/tests/build/progress-and-stats.ts +++ b/tests/legacy-cli/e2e/tests/build/progress-and-stats.ts @@ -2,11 +2,6 @@ import { getGlobalVariable } from '../../utils/env'; import { ng } from '../../utils/process'; export default async function () { - if (getGlobalVariable('argv')['esbuild']) { - // EXPERIMENTAL_ESBUILD: esbuild does not yet output build stats - return; - } - 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}`); @@ -18,11 +13,16 @@ export default async function () { ); } - const logs: string[] = [ - 'Browser application bundle generation complete', - 'Copying assets complete', - 'Index html generation complete', - ]; + let logs; + if (getGlobalVariable('argv')['esbuild']) { + logs = ['Application bundle generation complete.']; + } else { + logs = [ + 'Browser application bundle generation complete', + 'Copying assets complete', + 'Index html generation complete', + ]; + } for (const log of logs) { if (!stderrProgress.includes(log)) {