From 5996896076f350cd02e8c33823941b366e752043 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 16 Sep 2020 09:07:22 +0200 Subject: [PATCH] feat(@angular-devkit/build-angular): improve build stats output format With this change we also remove sourcemaps from build info to align with Webpack 5 output. --- package.json | 2 + .../angular_devkit/build_angular/BUILD.bazel | 2 + .../angular_devkit/build_angular/package.json | 1 + .../build_angular/src/browser/index.ts | 25 ++-- .../src/browser/specs/scripts-array_spec.ts | 8 +- .../build_angular/src/webpack/utils/stats.ts | 114 +++++++++++++----- yarn.lock | 10 ++ 7 files changed, 116 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 650e75ee2fea..2624e49bb479 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@types/resolve": "^1.17.1", "@types/rimraf": "^3.0.0", "@types/semver": "^7.0.0", + "@types/text-table": "^0.2.1", "@types/universal-analytics": "^0.4.2", "@types/uuid": "^8.0.0", "@types/webpack": "^4.41.22", @@ -215,6 +216,7 @@ "temp": "^0.9.0", "terser": "5.3.1", "terser-webpack-plugin": "4.2.1", + "text-table": "0.2.0", "through2": "^4.0.0", "tree-kill": "1.2.2", "ts-api-guardian": "0.5.0", diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index 62e54dc33166..83c3ab96d95e 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -127,6 +127,7 @@ ts_library( "@npm//@types/node", "@npm//@types/rimraf", "@npm//@types/semver", + "@npm//@types/text-table", "@npm//@types/webpack", "@npm//@types/webpack-dev-server", "@npm//@types/webpack-sources", @@ -182,6 +183,7 @@ ts_library( "@npm//stylus-loader", "@npm//terser", "@npm//terser-webpack-plugin", + "@npm//text-table", "@npm//tree-kill", "@npm//tslint", "@npm//typescript", diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 0f008d8ba5a3..d6c463bb1af4 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -68,6 +68,7 @@ "stylus-loader": "3.0.2", "terser": "5.3.1", "terser-webpack-plugin": "4.2.1", + "text-table": "0.2.0", "tree-kill": "1.2.2", "webpack": "4.44.1", "webpack-dev-middleware": "3.7.2", diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index 55572b282a49..254547a837d8 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -68,8 +68,10 @@ import { import { NgBuildAnalyticsPlugin } from '../webpack/plugins/analytics'; import { markAsyncChunksNonInitial } from '../webpack/utils/async-chunks'; import { + BundleStats, createWebpackLoggingCallback, generateBuildStats, + generateBuildStatsTable, generateBundleStats, statsErrorsToString, statsHasErrors, @@ -579,13 +581,11 @@ export function buildWebpackBrowser( type ArrayElement = A extends ReadonlyArray ? T : never; function generateBundleInfoStats( - id: string | number, bundle: ProcessBundleFile, chunk: ArrayElement | undefined, - ): string { + ): BundleStats { return generateBundleStats( { - id, size: bundle.size, files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], names: chunk && chunk.names, @@ -597,19 +597,17 @@ export function buildWebpackBrowser( ); } - let bundleInfoText = ''; + const bundleInfoStats: BundleStats[] = []; for (const result of processResults) { const chunk = webpackStats.chunks && webpackStats.chunks.find((chunk) => chunk.id.toString() === result.name); if (result.original) { - bundleInfoText += - '\n' + generateBundleInfoStats(result.name, result.original, chunk); + bundleInfoStats.push(generateBundleInfoStats(result.original, chunk)); } if (result.downlevel) { - bundleInfoText += - '\n' + generateBundleInfoStats(result.name, result.downlevel, chunk); + bundleInfoStats.push(generateBundleInfoStats(result.downlevel, chunk)); } } @@ -620,18 +618,19 @@ export function buildWebpackBrowser( for (const chunk of unprocessedChunks) { const asset = webpackStats.assets && webpackStats.assets.find(a => a.name === chunk.files[0]); - bundleInfoText += - '\n' + generateBundleStats({ ...chunk, size: asset && asset.size }, true); + bundleInfoStats.push(generateBundleStats({ ...chunk, size: asset && asset.size }, true)); } - bundleInfoText += + context.logger.info( '\n' + + generateBuildStatsTable(bundleInfoStats, colors.enabled) + + '\n\n' + generateBuildStats( (webpackStats && webpackStats.hash) || '', Date.now() - startTime, true, - ); - context.logger.info(bundleInfoText); + ), + ); // Check for budget errors and display them to the user. const budgets = options.budgets || []; diff --git a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts index 570b6dddc09d..29e004bf8428 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts @@ -170,9 +170,11 @@ describe('Browser Builder scripts array', () => { { logger }, ); - expect(logs.join('\n')).toMatch(/\(lazy-script\) 69 bytes.*\[entry].*\[rendered]/); - expect(logs.join('\n')).toMatch(/\(renamed-script\) 78 bytes.*\[entry].*\[rendered]/); - expect(logs.join('\n')).toMatch(/\(renamed-lazy-script\) 88 bytes.*\[entry].*\[rendered]/); + const joinedLogs = logs.join('\n'); + expect(joinedLogs).toMatch(/lazy-script.+69 bytes/); + expect(joinedLogs).toMatch(/renamed-script.+78 bytes/); + expect(joinedLogs).toMatch(/renamed-lazy-script.+88 bytes/); + expect(joinedLogs).not.toContain('Lazy Chunks'); }); it(`should error when a script doesn't exist`, async () => { diff --git a/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts b/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts index 95ce64de71f9..de1c2e6b5e60 100644 --- a/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts +++ b/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts @@ -10,7 +10,8 @@ import { logging, tags } from '@angular-devkit/core'; import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; import * as path from 'path'; -import { colors as ansiColors } from '../../utils/color'; +import * as textTable from 'text-table'; +import { colors as ansiColors, removeColor } from '../../utils/color'; export function formatSize(size: number): string { if (size <= 0) { @@ -23,9 +24,15 @@ export function formatSize(size: number): string { return `${+(size / Math.pow(1024, index)).toPrecision(3)} ${abbreviations[index]}`; } +export type BundleStatsData = [files: string, names: string, size: string]; + +export interface BundleStats { + initial: boolean; + stats: BundleStatsData; +}; + export function generateBundleStats( info: { - id: string | number; size?: number; files: string[]; names?: string[]; @@ -34,53 +41,100 @@ export function generateBundleStats( rendered?: boolean; }, colors: boolean, -): string { - const g = (x: string) => (colors ? ansiColors.bold.green(x) : x); - const y = (x: string) => (colors ? ansiColors.bold.yellow(x) : x); - - const id = info.id ? y(info.id.toString()) : ''; - const size = typeof info.size === 'number' ? ` ${formatSize(info.size)}` : ''; - const files = info.files.map(f => path.basename(f)).join(', '); - const names = info.names ? ` (${info.names.join(', ')})` : ''; - const initial = y(info.entry ? '[entry]' : info.initial ? '[initial]' : ''); - const flags = ['rendered', 'recorded'] - .map(f => (f && (info as any)[f] ? g(` [${f}]`) : '')) - .join(''); - - return `chunk {${id}} ${g(files)}${names}${size} ${initial}${flags}`; +): BundleStats { + const g = (x: string) => (colors ? ansiColors.greenBright(x) : x); + const c = (x: string) => (colors ? ansiColors.cyanBright(x) : x); + + const size = typeof info.size === 'number' ? formatSize(info.size) : '-'; + const files = info.files.filter(f => !f.endsWith('.map')).map(f => path.basename(f)).join(', '); + const names = info.names?.length ? info.names.join(', ') : '-'; + const initial = !!(info.entry || info.initial); + + return { + initial, + stats: [g(files), names, c(size)], + } +} + +export function generateBuildStatsTable(data: BundleStats[], colors: boolean): string { + const changedEntryChunksStats: BundleStatsData[] = []; + const changedLazyChunksStats: BundleStatsData[] = []; + for (const {initial, stats} of data) { + if (initial) { + changedEntryChunksStats.push(stats); + } else { + changedLazyChunksStats.push(stats); + } + } + + const bundleInfo: string[][] = []; + + const bold = (x: string) => colors ? ansiColors.bold(x) : x; + const dim = (x: string) => colors ? ansiColors.dim(x) : x; + + // Entry chunks + if (changedEntryChunksStats.length) { + bundleInfo.push( + ['Initial Chunk Files', 'Names', 'Size'].map(bold), + ...changedEntryChunksStats, + ); + } + + // Seperator + if (changedEntryChunksStats.length && changedLazyChunksStats.length) { + bundleInfo.push([]); + } + + // Lazy chunks + if (changedLazyChunksStats.length) { + bundleInfo.push( + ['Lazy Chunk Files', 'Names', 'Size'].map(bold), + ...changedLazyChunksStats, + ); + } + + return textTable(bundleInfo, { + hsep: dim(' | '), + stringLength: s => removeColor(s).length, + }); } export function generateBuildStats(hash: string, time: number, colors: boolean): string { const w = (x: string) => colors ? ansiColors.bold.white(x) : x; - return `Date: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms` + return `Build at: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms`; } export function statsToString(json: any, statsConfig: any) { 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 = json.chunks - .filter((chunk: any) => chunk.rendered) - .map((chunk: any) => { - const assets = json.assets.filter((asset: any) => chunk.files.indexOf(asset.name) != -1); - const summedSize = assets.filter((asset: any) => !asset.name.endsWith(".map")).reduce((total: number, asset: any) => { return total + asset.size }, 0); - return generateBundleStats({ ...chunk, size: summedSize }, colors); - }); + const changedChunksStats: BundleStats[] = []; + for (const chunk of json.chunks) { + if (!chunk.rendered) { + continue; + } + + const assets = json.assets.filter((asset: any) => chunk.files.includes(asset.name)); + const summedSize = assets.filter((asset: any) => !asset.name.endsWith(".map")).reduce((total: number, asset: any) => { return total + asset.size }, 0); + changedChunksStats.push(generateBundleStats({ ...chunk, size: summedSize }, colors)); + } const unchangedChunkNumber = json.chunks.length - changedChunksStats.length; + const statsTable = generateBuildStatsTable(changedChunksStats, colors); if (unchangedChunkNumber > 0) { return '\n' + rs(tags.stripIndents` - Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} + ${statsTable} + ${unchangedChunkNumber} unchanged chunks - ${changedChunksStats.join('\n')} - Time: ${w('' + json.time)}ms + + ${generateBuildStats(json.hash, json.time, colors)} `); } else { return '\n' + rs(tags.stripIndents` - ${changedChunksStats.join('\n')} - Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} - Time: ${w('' + json.time)}ms + ${statsTable} + + ${generateBuildStats(json.hash, json.time, colors)} `); } } diff --git a/yarn.lock b/yarn.lock index ffb7afbe3436..492a0de3d697 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1662,6 +1662,11 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== +"@types/text-table@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@types/text-table/-/text-table-0.2.1.tgz#39c4d4a058a82f677392dfd09976e83d9b4c9264" + integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== + "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -11550,6 +11555,11 @@ text-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== +text-table@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + through2@^2.0.0, through2@^2.0.2, through2@~2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"