diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts index 6f61f2754a54..beaa682678be 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts @@ -8,6 +8,7 @@ import { Compiler, compilation } from 'webpack'; import { Budget, Type } from '../../browser/schema'; import { ThresholdSeverity, checkBudgets } from '../utilities/bundle-calculator'; +import { ProcessBundleResult } from '../../utils/process-bundle'; export interface BundleBudgetPluginOptions { budgets: Budget[]; @@ -29,8 +30,12 @@ export class BundleBudgetPlugin { } private runChecks(budgets: Budget[], compilation: compilation.Compilation) { + // No process bundle results because this plugin is only used when differential + // builds are disabled. + const processResults: ProcessBundleResult[] = []; + const stats = compilation.getStats().toJson(); - for (const { severity, message } of checkBudgets(budgets, stats)) { + for (const { severity, message } of checkBudgets(budgets, stats, processResults)) { switch (severity) { case ThresholdSeverity.Warning: compilation.warnings.push(`budgets: ${message}`); diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts index 2a3b3ff9d85f..af53ff7b31ac 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import * as webpack from 'webpack'; +import { ProcessBundleFile, ProcessBundleResult } from '../../../src/utils/process-bundle'; import { Budget, Type } from '../../browser/schema'; import { formatSize } from '../utilities/stats'; @@ -30,6 +31,11 @@ export enum ThresholdSeverity { Error = 'error', } +enum DifferentialBuildType { + ORIGINAL = 'es2015', + DOWNLEVEL = 'es5', +} + export function* calculateThresholds(budget: Budget): IterableIterator { if (budget.maximumWarning) { yield { @@ -98,6 +104,7 @@ export function* calculateThresholds(budget: Budget): IterableIterator = T extends Array ? U : never; +type Chunk = ArrayElement>; +type Asset = ArrayElement>; abstract class Calculator { constructor ( protected budget: Budget, - protected chunks: Exclude, - protected assets: Exclude, + protected chunks: Chunk[], + protected assets: Asset[], + protected processResults: ProcessBundleResult[], ) {} abstract calculate(): Size[]; + + /** Calculates the size of the given chunk for the provided build type. */ + protected calculateChunkSize( + chunk: Chunk, + buildType: DifferentialBuildType, + ): number { + // Look for a process result containing different builds for this chunk. + const processResult = this.processResults + .find((processResult) => processResult.name === chunk.id.toString()); + + if (processResult) { + // Found a differential build, use the correct size information. + const processResultFile = getDifferentialBuildResult( + processResult, buildType); + + return processResultFile && processResultFile.size || 0; + } else { + // No differential builds, get the chunk size by summing its assets. + return chunk.files + .filter((file) => !file.endsWith('.map')) + .map((file: string) => { + const asset = this.assets.find((asset) => asset.name === file); + if (!asset) { + throw new Error(`Could not find asset for file: ${file}`); + } + + return asset; + }) + .map((asset) => asset.size) + .reduce((l, r) => l + r); + } + } } /** @@ -149,22 +192,32 @@ class BundleCalculator extends Calculator { return []; } - const size: number = this.chunks - .filter(chunk => chunk.names.indexOf(budgetName) !== -1) - .reduce((files, chunk) => [...files, ...chunk.files], []) - .filter((file: string) => !file.endsWith('.map')) - .map((file: string) => { - const asset = this.assets.find((asset) => asset.name === file); - if (!asset) { - throw new Error(`Could not find asset for file: ${file}`); - } + // The chunk may or may not have differential builds. Compute the size for + // each then check afterwards if they are all the same. + const buildSizes = Object.values(DifferentialBuildType).map((buildType) => { + const size = this.chunks + .filter(chunk => chunk.names.indexOf(budgetName) !== -1) + .map((chunk) => this.calculateChunkSize(chunk, buildType)) + .reduce((l, r) => l + r); + + return {size, label: `${this.budget.name}-${buildType}`}; + }); + + // If there are multiple sizes, then there are differential builds. Display + // them all. + if (!allEquivalent(buildSizes.map((buildSize) => buildSize.size))) { + return buildSizes; + } - return asset; - }) - .map((asset) => asset.size) - .reduce((total: number, size: number) => total + size, 0); + if (buildSizes.length === 0) { + return []; + } - return [{size, label: this.budget.name}]; + // Only one size, must not be a differential build. + return [{ + label: this.budget.name, + size: buildSizes[0].size, + }]; } } @@ -173,22 +226,15 @@ class BundleCalculator extends Calculator { */ class InitialCalculator extends Calculator { calculate() { - const size = this.chunks - .filter(chunk => chunk.initial) - .reduce((files, chunk) => [...files, ...chunk.files], []) - .filter((file: string) => !file.endsWith('.map')) - .map((file: string) => { - const asset = this.assets.find((asset) => asset.name === file); - if (!asset) { - throw new Error(`Could not find asset for file: ${file}`); - } - - return asset; - }) - .map((asset) => asset.size) - .reduce((total: number, size: number) => total + size, 0); - - return [{size, label: 'initial'}]; + return Object.values(DifferentialBuildType).map((buildType) => { + return { + label: `initial-${buildType}`, + size: this.chunks + .filter(chunk => chunk.initial) + .map((chunk) => this.calculateChunkSize(chunk, buildType)) + .reduce((l, r) => l + r), + }; + }); } } @@ -287,13 +333,15 @@ function calculateBytes( } export function* checkBudgets( - budgets: Budget[], webpackStats: webpack.Stats.ToJsonOutput): - IterableIterator<{ severity: ThresholdSeverity, message: string }> { + budgets: Budget[], + webpackStats: webpack.Stats.ToJsonOutput, + processResults: ProcessBundleResult[], +): IterableIterator<{ severity: ThresholdSeverity, message: string }> { // Ignore AnyComponentStyle budgets as these are handled in `AnyComponentStyleBudgetChecker`. const computableBudgets = budgets.filter((budget) => budget.type !== Type.AnyComponentStyle); for (const budget of computableBudgets) { - const sizes = calculateSizes(budget, webpackStats); + const sizes = calculateSizes(budget, webpackStats, processResults); for (const { size, label } of sizes) { yield* checkThresholds(calculateThresholds(budget), size, label); } @@ -339,6 +387,32 @@ export function* checkThresholds(thresholds: IterableIterator, size: } } +/** Returns the {@link ProcessBundleFile} for the given {@link DifferentialBuildType}. */ +function getDifferentialBuildResult( + processResult: ProcessBundleResult, buildType: DifferentialBuildType): + ProcessBundleFile|null { + switch (buildType) { + case DifferentialBuildType.ORIGINAL: return processResult.original || null; + case DifferentialBuildType.DOWNLEVEL: return processResult.downlevel || null; + } +} + +/** Returns whether or not all items in the list are equivalent to each other. */ +function allEquivalent(items: T[]): boolean { + if (items.length === 0) { + return true; + } + + const first = items[0]; + for (const item of items.slice(1)) { + if (item !== first) { + return false; + } + } + + return true; +} + function assertNever(input: never): never { throw new Error(`Unexpected call to assertNever() with input: ${ JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index 4f2aae9dd94e..e4d27c43809b 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -639,7 +639,8 @@ export function buildWebpackBrowser( // Check for budget errors and display them to the user. const budgets = options.budgets || []; - for (const {severity, message} of checkBudgets(budgets, webpackStats)) { + const budgetFailures = checkBudgets(budgets, webpackStats, processResults); + for (const {severity, message} of budgetFailures) { switch (severity) { case ThresholdSeverity.Warning: webpackStats.warnings.push(message);