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)) {