Skip to content

Commit

Permalink
refactor(@angular-devkit/build-angular): move budget computations to …
Browse files Browse the repository at this point in the history
…be post-build

Refs angular#15792.

This provides access to all the size information necessary because all build steps have already completed. This commit is roughly a no-op because it simply moves the budget checks (for different builds) to be executed post-build.

The lone exception is the AnyComponentStyle budget. Component stylesheet files are not emitted after the build is completed, so there is no size information to work with. Instead, these budgets are checked during a separate plugin (exected for different builds **and** non-differential builds).
  • Loading branch information
dgp1130 committed Dec 9, 2019
1 parent b6863b6 commit d74b072
Show file tree
Hide file tree
Showing 9 changed files with 675 additions and 298 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -553,10 +553,14 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
noEmitOnErrors: true,
minimizer: [
new HashedModuleIdsPlugin(),
// TODO: check with Mike what this feature needs.
new BundleBudgetPlugin({ budgets: buildOptions.budgets }),
...extraMinimizers,
],
].concat(differentialLoadingMode ? [
// Budgets are computed after differential builds, not via a plugin.
// https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/src/browser/index.ts
] : [
// Non differential builds should be computed here, as a plugin.
new BundleBudgetPlugin({ budgets: buildOptions.budgets }),
]),
},
plugins: [
// Always replace the context for the System.import in angular/core to prevent warnings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import * as path from 'path';
import * as webpack from 'webpack';
import {
AnyComponentStyleBudgetChecker,
PostcssCliResources,
RawCssLoader,
RemoveHashPlugin,
Expand All @@ -26,7 +27,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
const { root, buildOptions } = wco;
const entryPoints: { [key: string]: string[] } = {};
const globalStylePaths: string[] = [];
const extraPlugins = [];
const extraPlugins: webpack.Plugin[] = [
new AnyComponentStyleBudgetChecker(buildOptions.budgets),
];

const cssSourceMap = buildOptions.sourceMap.styles;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { Compiler, Plugin } from 'webpack';
import { Budget, Type } from '../../../src/browser/schema';
import { ThresholdSeverity, calculateThresholds, checkThresholds } from '../utilities/bundle-calculator';

const PLUGIN_NAME = 'AnyComponentStyleBudgetChecker';

/**
* Check budget sizes for component styles by emitting a warning or error if a
* budget is exceeded by a particular component's styles.
*/
export class AnyComponentStyleBudgetChecker implements Plugin {
private readonly budgets: Budget[];
constructor(budgets: Budget[]) {
this.budgets = budgets.filter((budget) => budget.type === Type.AnyComponentStyle);
}

apply(compiler: Compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.afterOptimizeChunkAssets.tap(PLUGIN_NAME, () => {
// In AOT compilations component styles get processed in child compilations.
// tslint:disable-next-line: no-any
const parentCompilation = (compilation.compiler as any).parentCompilation;
if (!parentCompilation) {
return;
}

const componentStyles = Object.keys(compilation.assets)
.filter((name) => name.endsWith('.css'))
.map((name) => ({
size: compilation.assets[name].size(),
label: name,
}));
const thresholds = flatMap(this.budgets, (budget) => calculateThresholds(budget));

for (const { size, label } of componentStyles) {
for (const { severity, message } of checkThresholds(thresholds[Symbol.iterator](), size, label)) {
switch (severity) {
case ThresholdSeverity.Warning:
compilation.warnings.push(message);
break;
case ThresholdSeverity.Error:
compilation.errors.push(message);
break;
default:
assertNever(severity);
break;
}
}
}
});
});
}
}

function assertNever(input: never): never {
throw new Error(`Unexpected call to assertNever() with input: ${
JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`);
}

function flatMap<T, R>(list: T[], mapper: (item: T, index: number, array: T[]) => IterableIterator<R>): R[] {
return ([] as R[]).concat(...list.map(mapper).map((iterator) => Array.from(iterator)));

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,7 @@
*/
import { Compiler, compilation } from 'webpack';
import { Budget, Type } from '../../browser/schema';
import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator';
import { formatSize } from '../utilities/stats';

interface Thresholds {
maximumWarning?: number;
maximumError?: number;
minimumWarning?: number;
minimumError?: number;
warningLow?: number;
warningHigh?: number;
errorLow?: number;
errorHigh?: number;
}
import { ThresholdSeverity, checkBudgets } from '../utilities/bundle-calculator';

export interface BundleBudgetPluginOptions {
budgets: Budget[];
Expand All @@ -35,97 +23,22 @@ export class BundleBudgetPlugin {
return;
}

compiler.hooks.compilation.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
compilation.hooks.afterOptimizeChunkAssets.tap('BundleBudgetPlugin', () => {
// In AOT compilations component styles get processed in child compilations.
// tslint:disable-next-line: no-any
const parentCompilation = (compilation.compiler as any).parentCompilation;
if (!parentCompilation) {
return;
}

const filteredBudgets = budgets.filter(budget => budget.type === Type.AnyComponentStyle);
this.runChecks(filteredBudgets, compilation);
});
});

compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
const filteredBudgets = budgets.filter(budget => budget.type !== Type.AnyComponentStyle);
this.runChecks(filteredBudgets, compilation);
this.runChecks(budgets, compilation);
});
}

private checkMinimum(threshold: number | undefined, size: Size, messages: string[]) {
if (threshold && threshold > size.size) {
const sizeDifference = formatSize(threshold - size.size);
messages.push(`budgets, minimum exceeded for ${size.label}. `
+ `Budget ${formatSize(threshold)} was not reached by ${sizeDifference}.`);
}
}

private checkMaximum(threshold: number | undefined, size: Size, messages: string[]) {
if (threshold && threshold < size.size) {
const sizeDifference = formatSize(size.size - threshold);
messages.push(`budgets, maximum exceeded for ${size.label}. `
+ `Budget ${formatSize(threshold)} was exceeded by ${sizeDifference}.`);
}
}

private calculate(budget: Budget): Thresholds {
const thresholds: Thresholds = {};
if (budget.maximumWarning) {
thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 1);
}

if (budget.maximumError) {
thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 1);
}

if (budget.minimumWarning) {
thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, -1);
}

if (budget.minimumError) {
thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, -1);
}

if (budget.warning) {
thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, -1);
}

if (budget.warning) {
thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 1);
}

if (budget.error) {
thresholds.errorLow = calculateBytes(budget.error, budget.baseline, -1);
}

if (budget.error) {
thresholds.errorHigh = calculateBytes(budget.error, budget.baseline, 1);
}

return thresholds;
}

private runChecks(budgets: Budget[], compilation: compilation.Compilation) {
budgets
.map(budget => ({
budget,
thresholds: this.calculate(budget),
sizes: calculateSizes(budget, compilation),
}))
.forEach(budgetCheck => {
budgetCheck.sizes.forEach(size => {
this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings);
this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors);
this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings);
this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors);
this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings);
this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings);
this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors);
this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors);
});
});
const stats = compilation.getStats().toJson();
for (const { severity, message } of checkBudgets(budgets, stats)) {
switch (severity) {
case ThresholdSeverity.Warning:
compilation.warnings.push(`budgets: ${message}`);
break;
case ThresholdSeverity.Error:
compilation.errors.push(`budgets: ${message}`);
break;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

// Exports the webpack plugins we use internally.
export { AnyComponentStyleBudgetChecker } from './any-component-style-budget-checker';
export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss-webpack-plugin';
export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';
Expand Down
Loading

0 comments on commit d74b072

Please sign in to comment.