diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts index 29e5e17ac34e..442b80ebb34f 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts @@ -27,11 +27,13 @@ export function generateEntryPoints(appConfig: { const entryPoints = [ 'polyfills-nomodule-es5', + 'runtime', 'polyfills-es5', 'polyfills', 'sw-register', ...extraEntryPoints(appConfig.styles, 'styles'), ...extraEntryPoints(appConfig.scripts, 'scripts'), + 'vendor', 'main', ]; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts index 296b4f16b34f..ac2bc7aa8fe3 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts @@ -8,6 +8,7 @@ // tslint:disable // TODO: cleanup this file, it's copied as is from Angular CLI. import { tags, terminal } from '@angular-devkit/core'; +import * as path from 'path'; const { bold, green, red, reset, white, yellow } = terminal; @@ -23,27 +24,47 @@ export function formatSize(size: number): string { return `${+(size / Math.pow(1024, index)).toPrecision(3)} ${abbreviations[index]}`; } +export function generateBundleStats( + info: { + id: string | number; + size?: number; + files: string[]; + names?: string[]; + entry: boolean; + initial: boolean; + rendered?: boolean; + }, + colors: boolean, +): string { + const g = (x: string) => (colors ? bold(green(x)) : x); + const y = (x: string) => (colors ? bold(yellow(x)) : x); + + 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 {${y(info.id.toString())}} ${g(files)}${names}${size} ${initial}${flags}`; +} + +export function generateBuildStats(hash: string, time: number, colors: boolean): string { + const w = (x: string) => colors ? bold(white(x)) : x; + return `Date: ${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 ? reset(x) : x; const w = (x: string) => colors ? bold(white(x)) : x; - const g = (x: string) => colors ? bold(green(x)) : x; - const y = (x: string) => colors ? bold(yellow(x)) : x; const changedChunksStats = json.chunks .filter((chunk: any) => chunk.rendered) .map((chunk: any) => { const asset = json.assets.filter((x: any) => x.name == chunk.files[0])[0]; - const size = asset ? ` ${formatSize(asset.size)}` : ''; - const files = chunk.files.join(', '); - const names = chunk.names ? ` (${chunk.names.join(', ')})` : ''; - const initial = y(chunk.entry ? '[entry]' : chunk.initial ? '[initial]' : ''); - const flags = ['rendered', 'recorded'] - .map(f => f && chunk[f] ? g(` [${f}]`) : '') - .join(''); - - return `chunk {${y(chunk.id)}} ${g(files)}${names}${size} ${initial}${flags}`; + return generateBundleStats({ ...chunk, size: asset && asset.size }, colors); }); const unchangedChunkNumber = json.chunks.length - changedChunksStats.length; diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index f712a782c8d9..6ad5518856bb 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -51,6 +51,8 @@ import { import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker'; import { + generateBuildStats, + generateBundleStats, statsErrorsToString, statsToString, statsWarningsToString, @@ -64,7 +66,12 @@ import { normalizeSourceMaps, } from '../utils'; import { manglingDisabled } from '../utils/mangle-options'; -import { CacheKey, ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle'; +import { + CacheKey, + ProcessBundleFile, + ProcessBundleOptions, + ProcessBundleResult, +} from '../utils/process-bundle'; import { assertCompatibleAngularVersion } from '../utils/version'; import { generateBrowserWebpackConfigFromContext, @@ -202,9 +209,6 @@ export function buildWebpackBrowser( // Check Angular version. assertCompatibleAngularVersion(context.workspaceRoot, context.logger); - const loggingFn = - transforms.logging || createBrowserLoggingCallback(!!options.verbose, context.logger); - return from(initialize(options, context, host, transforms.webpackConfiguration)).pipe( // tslint:disable-next-line: no-big-function switchMap(({ config: configs, projectRoot }) => { @@ -222,6 +226,10 @@ export function buildWebpackBrowser( `); } + const useBundleDownleveling = + isDifferentialLoadingNeeded && !(fullDifferential || options.watch); + const startTime = Date.now(); + return from(configs).pipe( // the concurrency parameter (3rd parameter of mergeScan) is deliberately // set to 1 to make sure the build steps are executed in sequence. @@ -229,7 +237,13 @@ export function buildWebpackBrowser( (lastResult, config) => { // Make sure to only run the 2nd build step, if 1st one succeeded if (lastResult.success) { - return runWebpack(config, context, { logging: loggingFn }); + return runWebpack(config, context, { + logging: + transforms.logging || + (useBundleDownleveling + ? () => {} + : createBrowserLoggingCallback(!!options.verbose, context.logger)), + }); } else { return of(); } @@ -242,7 +256,19 @@ export function buildWebpackBrowser( switchMap(async buildEvents => { configs.length = 0; const success = buildEvents.every(r => r.success); - if (success) { + if (!success && useBundleDownleveling) { + // If using bundle downleveling then there is only one build + // If it fails show any diagnostic messages and bail + const webpackStats = buildEvents[0].webpackStats; + if (webpackStats && webpackStats.warnings.length > 0) { + context.logger.warn(statsWarningsToString(webpackStats, { colors: true })); + } + if (webpackStats && webpackStats.errors.length > 0) { + context.logger.error(statsErrorsToString(webpackStats, { colors: true })); + } + + return { success }; + } else if (success) { let noModuleFiles: EmittedFiles[] | undefined; let moduleFiles: EmittedFiles[] | undefined; let files: EmittedFiles[] | undefined; @@ -263,7 +289,7 @@ export function buildWebpackBrowser( noModuleFiles = secondBuild.emittedFiles; } } else if (isDifferentialLoadingNeeded && !fullDifferential) { - const { emittedFiles = [] } = firstBuild; + const { emittedFiles = [], webpackStats } = firstBuild; moduleFiles = []; noModuleFiles = []; @@ -342,7 +368,9 @@ export function buildWebpackBrowser( filename, code, map, - name: file.name, + // id is always present for non-assets + // tslint:disable-next-line: no-non-null-assertion + name: file.id!, optimizeOnly: true, }); @@ -356,7 +384,9 @@ export function buildWebpackBrowser( filename, code, map, - name: file.name, + // id is always present for non-assets + // tslint:disable-next-line: no-non-null-assertion + name: file.id!, runtime: file.file.startsWith('runtime'), ignoreOriginal: es5Polyfills, }); @@ -600,6 +630,73 @@ export function buildWebpackBrowser( } context.logger.info('ES5 bundle generation complete.'); + + type ArrayElement = A extends ReadonlyArray ? T : never; + function generateBundleInfoStats( + id: string | number, + bundle: ProcessBundleFile, + chunk: ArrayElement | undefined, + ): string { + return generateBundleStats( + { + id, + size: bundle.size, + files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], + names: chunk && chunk.names, + entry: !!chunk && chunk.names.includes('runtime'), + initial: !!chunk && chunk.initial, + rendered: true, + }, + true, + ); + } + + let bundleInfoText = ''; + const processedNames = new Set(); + for (const result of processResults) { + processedNames.add(result.name); + + const chunk = + webpackStats && + webpackStats.chunks && + webpackStats.chunks.find(c => result.name === c.id.toString()); + if (result.original) { + bundleInfoText += + '\n' + generateBundleInfoStats(result.name, result.original, chunk); + } + if (result.downlevel) { + bundleInfoText += + '\n' + generateBundleInfoStats(result.name, result.downlevel, chunk); + } + } + + if (webpackStats && webpackStats.chunks) { + for (const chunk of webpackStats.chunks) { + if (processedNames.has(chunk.id.toString())) { + continue; + } + + const asset = + webpackStats.assets && webpackStats.assets.find(a => a.name === chunk.files[0]); + bundleInfoText += + '\n' + generateBundleStats({ ...chunk, size: asset && asset.size }, true); + } + } + + bundleInfoText += + '\n' + + generateBuildStats( + (webpackStats && webpackStats.hash) || '', + Date.now() - startTime, + true, + ); + context.logger.info(bundleInfoText); + if (webpackStats && webpackStats.warnings.length > 0) { + context.logger.warn(statsWarningsToString(webpackStats, { colors: true })); + } + if (webpackStats && webpackStats.errors.length > 0) { + context.logger.error(statsErrorsToString(webpackStats, { colors: true })); + } } else { const { emittedFiles = [] } = firstBuild; files = emittedFiles.filter(x => x.name !== 'polyfills-es5'); diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index 28ec55d4cd96..1313660cc610 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -19,7 +19,7 @@ export interface ProcessBundleOptions { filename: string; code: string; map?: string; - name?: string; + name: string; sourceMaps?: boolean; hiddenSourceMaps?: boolean; vendorSourceMaps?: boolean; @@ -34,7 +34,7 @@ export interface ProcessBundleOptions { } export interface ProcessBundleResult { - name?: string; + name: string; integrity?: string; original?: ProcessBundleFile; downlevel?: ProcessBundleFile; diff --git a/tests/legacy-cli/e2e/tests/basic/scripts-array.ts b/tests/legacy-cli/e2e/tests/basic/scripts-array.ts index d6c8698e7532..fd7f869b6cc9 100644 --- a/tests/legacy-cli/e2e/tests/basic/scripts-array.ts +++ b/tests/legacy-cli/e2e/tests/basic/scripts-array.ts @@ -73,15 +73,15 @@ export default async function () { await expectFileToMatch( 'dist/test-project/index.html', oneLineTrim` + + - - - + `, ); diff --git a/tests/legacy-cli/e2e/tests/basic/styles-array.ts b/tests/legacy-cli/e2e/tests/basic/styles-array.ts index feea3e79acf8..72b082f3d18c 100644 --- a/tests/legacy-cli/e2e/tests/basic/styles-array.ts +++ b/tests/legacy-cli/e2e/tests/basic/styles-array.ts @@ -61,13 +61,13 @@ export default async function() { await expectFileToMatch( 'dist/test-project/index.html', oneLineTrim` + + - - - + `, ); diff --git a/tests/legacy-cli/e2e/tests/misc/support-safari-10.1.ts b/tests/legacy-cli/e2e/tests/misc/support-safari-10.1.ts index bf5dcd375ce2..f790ba83180e 100644 --- a/tests/legacy-cli/e2e/tests/misc/support-safari-10.1.ts +++ b/tests/legacy-cli/e2e/tests/misc/support-safari-10.1.ts @@ -44,15 +44,15 @@ export default async function () { } else { await expectFileToMatch('dist/test-project/index.html', oneLineTrim` + + - - - + `); }