Skip to content

Commit 154ad9b

Browse files
dgp1130vikerman
authored andcommitted
refactor(@angular-devkit/build-angular): move budget computations to be post-build
Refs #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).
1 parent cbe9efc commit 154ad9b

File tree

9 files changed

+675
-298
lines changed

9 files changed

+675
-298
lines changed

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,10 +553,14 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
553553
noEmitOnErrors: true,
554554
minimizer: [
555555
new HashedModuleIdsPlugin(),
556-
// TODO: check with Mike what this feature needs.
557-
new BundleBudgetPlugin({ budgets: buildOptions.budgets }),
558556
...extraMinimizers,
559-
],
557+
].concat(differentialLoadingMode ? [
558+
// Budgets are computed after differential builds, not via a plugin.
559+
// https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/src/browser/index.ts
560+
] : [
561+
// Non differential builds should be computed here, as a plugin.
562+
new BundleBudgetPlugin({ budgets: buildOptions.budgets }),
563+
]),
560564
},
561565
plugins: [
562566
// Always replace the context for the System.import in angular/core to prevent warnings.

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import * as path from 'path';
1010
import * as webpack from 'webpack';
1111
import {
12+
AnyComponentStyleBudgetChecker,
1213
PostcssCliResources,
1314
RawCssLoader,
1415
RemoveHashPlugin,
@@ -26,7 +27,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
2627
const { root, buildOptions } = wco;
2728
const entryPoints: { [key: string]: string[] } = {};
2829
const globalStylePaths: string[] = [];
29-
const extraPlugins = [];
30+
const extraPlugins: webpack.Plugin[] = [
31+
new AnyComponentStyleBudgetChecker(buildOptions.budgets),
32+
];
3033

3134
const cssSourceMap = buildOptions.sourceMap.styles;
3235

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { Compiler, Plugin } from 'webpack';
10+
import { Budget, Type } from '../../../src/browser/schema';
11+
import { ThresholdSeverity, calculateThresholds, checkThresholds } from '../utilities/bundle-calculator';
12+
13+
const PLUGIN_NAME = 'AnyComponentStyleBudgetChecker';
14+
15+
/**
16+
* Check budget sizes for component styles by emitting a warning or error if a
17+
* budget is exceeded by a particular component's styles.
18+
*/
19+
export class AnyComponentStyleBudgetChecker implements Plugin {
20+
private readonly budgets: Budget[];
21+
constructor(budgets: Budget[]) {
22+
this.budgets = budgets.filter((budget) => budget.type === Type.AnyComponentStyle);
23+
}
24+
25+
apply(compiler: Compiler) {
26+
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
27+
compilation.hooks.afterOptimizeChunkAssets.tap(PLUGIN_NAME, () => {
28+
// In AOT compilations component styles get processed in child compilations.
29+
// tslint:disable-next-line: no-any
30+
const parentCompilation = (compilation.compiler as any).parentCompilation;
31+
if (!parentCompilation) {
32+
return;
33+
}
34+
35+
const componentStyles = Object.keys(compilation.assets)
36+
.filter((name) => name.endsWith('.css'))
37+
.map((name) => ({
38+
size: compilation.assets[name].size(),
39+
label: name,
40+
}));
41+
const thresholds = flatMap(this.budgets, (budget) => calculateThresholds(budget));
42+
43+
for (const { size, label } of componentStyles) {
44+
for (const { severity, message } of checkThresholds(thresholds[Symbol.iterator](), size, label)) {
45+
switch (severity) {
46+
case ThresholdSeverity.Warning:
47+
compilation.warnings.push(message);
48+
break;
49+
case ThresholdSeverity.Error:
50+
compilation.errors.push(message);
51+
break;
52+
default:
53+
assertNever(severity);
54+
break;
55+
}
56+
}
57+
}
58+
});
59+
});
60+
}
61+
}
62+
63+
function assertNever(input: never): never {
64+
throw new Error(`Unexpected call to assertNever() with input: ${
65+
JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`);
66+
}
67+
68+
function flatMap<T, R>(list: T[], mapper: (item: T, index: number, array: T[]) => IterableIterator<R>): R[] {
69+
return ([] as R[]).concat(...list.map(mapper).map((iterator) => Array.from(iterator)));
70+
71+
}

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts

Lines changed: 13 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,7 @@
77
*/
88
import { Compiler, compilation } from 'webpack';
99
import { Budget, Type } from '../../browser/schema';
10-
import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator';
11-
import { formatSize } from '../utilities/stats';
12-
13-
interface Thresholds {
14-
maximumWarning?: number;
15-
maximumError?: number;
16-
minimumWarning?: number;
17-
minimumError?: number;
18-
warningLow?: number;
19-
warningHigh?: number;
20-
errorLow?: number;
21-
errorHigh?: number;
22-
}
10+
import { ThresholdSeverity, checkBudgets } from '../utilities/bundle-calculator';
2311

2412
export interface BundleBudgetPluginOptions {
2513
budgets: Budget[];
@@ -35,97 +23,22 @@ export class BundleBudgetPlugin {
3523
return;
3624
}
3725

38-
compiler.hooks.compilation.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
39-
compilation.hooks.afterOptimizeChunkAssets.tap('BundleBudgetPlugin', () => {
40-
// In AOT compilations component styles get processed in child compilations.
41-
// tslint:disable-next-line: no-any
42-
const parentCompilation = (compilation.compiler as any).parentCompilation;
43-
if (!parentCompilation) {
44-
return;
45-
}
46-
47-
const filteredBudgets = budgets.filter(budget => budget.type === Type.AnyComponentStyle);
48-
this.runChecks(filteredBudgets, compilation);
49-
});
50-
});
51-
5226
compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
53-
const filteredBudgets = budgets.filter(budget => budget.type !== Type.AnyComponentStyle);
54-
this.runChecks(filteredBudgets, compilation);
27+
this.runChecks(budgets, compilation);
5528
});
5629
}
5730

58-
private checkMinimum(threshold: number | undefined, size: Size, messages: string[]) {
59-
if (threshold && threshold > size.size) {
60-
const sizeDifference = formatSize(threshold - size.size);
61-
messages.push(`budgets, minimum exceeded for ${size.label}. `
62-
+ `Budget ${formatSize(threshold)} was not reached by ${sizeDifference}.`);
63-
}
64-
}
65-
66-
private checkMaximum(threshold: number | undefined, size: Size, messages: string[]) {
67-
if (threshold && threshold < size.size) {
68-
const sizeDifference = formatSize(size.size - threshold);
69-
messages.push(`budgets, maximum exceeded for ${size.label}. `
70-
+ `Budget ${formatSize(threshold)} was exceeded by ${sizeDifference}.`);
71-
}
72-
}
73-
74-
private calculate(budget: Budget): Thresholds {
75-
const thresholds: Thresholds = {};
76-
if (budget.maximumWarning) {
77-
thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 1);
78-
}
79-
80-
if (budget.maximumError) {
81-
thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 1);
82-
}
83-
84-
if (budget.minimumWarning) {
85-
thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, -1);
86-
}
87-
88-
if (budget.minimumError) {
89-
thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, -1);
90-
}
91-
92-
if (budget.warning) {
93-
thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, -1);
94-
}
95-
96-
if (budget.warning) {
97-
thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 1);
98-
}
99-
100-
if (budget.error) {
101-
thresholds.errorLow = calculateBytes(budget.error, budget.baseline, -1);
102-
}
103-
104-
if (budget.error) {
105-
thresholds.errorHigh = calculateBytes(budget.error, budget.baseline, 1);
106-
}
107-
108-
return thresholds;
109-
}
110-
11131
private runChecks(budgets: Budget[], compilation: compilation.Compilation) {
112-
budgets
113-
.map(budget => ({
114-
budget,
115-
thresholds: this.calculate(budget),
116-
sizes: calculateSizes(budget, compilation),
117-
}))
118-
.forEach(budgetCheck => {
119-
budgetCheck.sizes.forEach(size => {
120-
this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings);
121-
this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors);
122-
this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings);
123-
this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors);
124-
this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings);
125-
this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings);
126-
this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors);
127-
this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors);
128-
});
129-
});
32+
const stats = compilation.getStats().toJson();
33+
for (const { severity, message } of checkBudgets(budgets, stats)) {
34+
switch (severity) {
35+
case ThresholdSeverity.Warning:
36+
compilation.warnings.push(`budgets: ${message}`);
37+
break;
38+
case ThresholdSeverity.Error:
39+
compilation.errors.push(`budgets: ${message}`);
40+
break;
41+
}
42+
}
13043
}
13144
}

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
// Exports the webpack plugins we use internally.
10+
export { AnyComponentStyleBudgetChecker } from './any-component-style-budget-checker';
1011
export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss-webpack-plugin';
1112
export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
1213
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';

0 commit comments

Comments
 (0)