Skip to content

Commit bcf250c

Browse files
alan-agius4vikerman
authored andcommitted
feat(@angular-devkit/build-angular): add bundle budget for component styles
It’s very easy to inadvertently import toplevel css in component styles. Since component css is standalone and self-contained, it will never be shared between components and remains as a single large bundle for each component. This in turn adds a large amount of code that must be processed and increases bundle size. Related to: TOOL-949
1 parent 28bd549 commit bcf250c

File tree

4 files changed

+121
-26
lines changed

4 files changed

+121
-26
lines changed

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

+42-26
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import { Compiler, compilation } from 'webpack';
9-
import { Budget } from '../../browser/schema';
9+
import { Budget, Type } from '../../browser/schema';
1010
import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator';
1111
import { formatSize } from '../utilities/stats';
1212

@@ -30,33 +30,28 @@ export class BundleBudgetPlugin {
3030

3131
apply(compiler: Compiler): void {
3232
const { budgets } = this.options;
33-
compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
34-
if (!budgets || budgets.length === 0) {
35-
return;
36-
}
3733

38-
budgets.map(budget => {
39-
const thresholds = this.calculate(budget);
40-
41-
return {
42-
budget,
43-
thresholds,
44-
sizes: calculateSizes(budget, compilation),
45-
};
46-
})
47-
.forEach(budgetCheck => {
48-
budgetCheck.sizes.forEach(size => {
49-
this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings);
50-
this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors);
51-
this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings);
52-
this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors);
53-
this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings);
54-
this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings);
55-
this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors);
56-
this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors);
57-
});
34+
if (!budgets || budgets.length === 0) {
35+
return;
36+
}
5837

59-
});
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+
52+
compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
53+
const filteredBudgets = budgets.filter(budget => budget.type !== Type.AnyComponentStyle);
54+
this.runChecks(filteredBudgets, compilation);
6055
});
6156
}
6257

@@ -116,4 +111,25 @@ export class BundleBudgetPlugin {
116111

117112
return thresholds;
118113
}
114+
115+
private runChecks(budgets: Budget[], compilation: compilation.Compilation) {
116+
budgets
117+
.map(budget => ({
118+
budget,
119+
thresholds: this.calculate(budget),
120+
sizes: calculateSizes(budget, compilation),
121+
}))
122+
.forEach(budgetCheck => {
123+
budgetCheck.sizes.forEach(size => {
124+
this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings);
125+
this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors);
126+
this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings);
127+
this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors);
128+
this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings);
129+
this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings);
130+
this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors);
131+
this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors);
132+
});
133+
});
134+
}
119135
}

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts

+16
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export function calculateSizes(budget: Budget, compilation: Compilation): Size[]
2525
allScript: AllScriptCalculator,
2626
any: AnyCalculator,
2727
anyScript: AnyScriptCalculator,
28+
anyComponentStyle: AnyComponentStyleCalculator,
2829
bundle: BundleCalculator,
2930
initial: InitialCalculator,
3031
};
32+
3133
const ctor = calculatorMap[budget.type];
3234
const calculator = new ctor(budget, compilation);
3335

@@ -101,6 +103,20 @@ class AllCalculator extends Calculator {
101103
}
102104
}
103105

106+
/**
107+
* Any components styles
108+
*/
109+
class AnyComponentStyleCalculator extends Calculator {
110+
calculate() {
111+
return Object.keys(this.compilation.assets)
112+
.filter(key => key.endsWith('.css'))
113+
.map(key => ({
114+
size: this.compilation.assets[key].size(),
115+
label: key,
116+
}));
117+
}
118+
}
119+
104120
/**
105121
* Any script, individually.
106122
*/

packages/angular_devkit/build_angular/src/browser/schema.json

+1
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@
482482
"allScript",
483483
"any",
484484
"anyScript",
485+
"anyComponentStyle",
485486
"bundle",
486487
"initial"
487488
]

packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts

+62
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,68 @@ describe('Browser Builder bundle budgets', () => {
6464
await run.stop();
6565
});
6666

67+
it(`shows warnings for large component css when using 'anyComponentStyle' when AOT`, async () => {
68+
const overrides = {
69+
aot: true,
70+
optimization: true,
71+
budgets: [{ type: 'anyComponentStyle', maximumWarning: '1b' }],
72+
};
73+
74+
const cssContent = `
75+
.foo { color: white; padding: 1px; }
76+
.buz { color: white; padding: 2px; }
77+
.bar { color: white; padding: 3px; }
78+
`;
79+
80+
host.writeMultipleFiles({
81+
'src/app/app.component.css': cssContent,
82+
'src/assets/foo.css': cssContent,
83+
'src/styles.css': cssContent,
84+
});
85+
86+
const logger = new logging.Logger('');
87+
const logs: string[] = [];
88+
logger.subscribe(e => logs.push(e.message));
89+
90+
const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
91+
const output = await run.result;
92+
expect(output.success).toBe(true);
93+
expect(logs.length).toBe(2);
94+
expect(logs.join()).toMatch(/WARNING.+app\.component\.css/);
95+
await run.stop();
96+
});
97+
98+
it(`shows error for large component css when using 'anyComponentStyle' when AOT`, async () => {
99+
const overrides = {
100+
aot: true,
101+
optimization: true,
102+
budgets: [{ type: 'anyComponentStyle', maximumError: '1b' }],
103+
};
104+
105+
const cssContent = `
106+
.foo { color: white; padding: 1px; }
107+
.buz { color: white; padding: 2px; }
108+
.bar { color: white; padding: 3px; }
109+
`;
110+
111+
host.writeMultipleFiles({
112+
'src/app/app.component.css': cssContent,
113+
'src/assets/foo.css': cssContent,
114+
'src/styles.css': cssContent,
115+
});
116+
117+
const logger = new logging.Logger('');
118+
const logs: string[] = [];
119+
logger.subscribe(e => logs.push(e.message));
120+
121+
const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
122+
const output = await run.result;
123+
expect(output.success).toBe(false);
124+
expect(logs.length).toBe(2);
125+
expect(logs.join()).toMatch(/ERROR.+app\.component\.css/);
126+
await run.stop();
127+
});
128+
67129
describe(`should ignore '.map' files`, () => {
68130
it(`when 'bundle' budget`, async () => {
69131
const overrides = {

0 commit comments

Comments
 (0)