Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): add bundle budget for component …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
alan-agius4 authored and vikerman committed Jul 25, 2019
1 parent 28bd549 commit bcf250c
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import { Compiler, compilation } from 'webpack';
import { Budget } from '../../browser/schema';
import { Budget, Type } from '../../browser/schema';
import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator';
import { formatSize } from '../utilities/stats';

Expand All @@ -30,33 +30,28 @@ export class BundleBudgetPlugin {

apply(compiler: Compiler): void {
const { budgets } = this.options;
compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => {
if (!budgets || budgets.length === 0) {
return;
}

budgets.map(budget => {
const thresholds = this.calculate(budget);

return {
budget,
thresholds,
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);
});
if (!budgets || budgets.length === 0) {
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);
});
}

Expand Down Expand Up @@ -116,4 +111,25 @@ export class BundleBudgetPlugin {

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);
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export function calculateSizes(budget: Budget, compilation: Compilation): Size[]
allScript: AllScriptCalculator,
any: AnyCalculator,
anyScript: AnyScriptCalculator,
anyComponentStyle: AnyComponentStyleCalculator,
bundle: BundleCalculator,
initial: InitialCalculator,
};

const ctor = calculatorMap[budget.type];
const calculator = new ctor(budget, compilation);

Expand Down Expand Up @@ -101,6 +103,20 @@ class AllCalculator extends Calculator {
}
}

/**
* Any components styles
*/
class AnyComponentStyleCalculator extends Calculator {
calculate() {
return Object.keys(this.compilation.assets)
.filter(key => key.endsWith('.css'))
.map(key => ({
size: this.compilation.assets[key].size(),
label: key,
}));
}
}

/**
* Any script, individually.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@
"allScript",
"any",
"anyScript",
"anyComponentStyle",
"bundle",
"initial"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,68 @@ describe('Browser Builder bundle budgets', () => {
await run.stop();
});

it(`shows warnings for large component css when using 'anyComponentStyle' when AOT`, async () => {
const overrides = {
aot: true,
optimization: true,
budgets: [{ type: 'anyComponentStyle', maximumWarning: '1b' }],
};

const cssContent = `
.foo { color: white; padding: 1px; }
.buz { color: white; padding: 2px; }
.bar { color: white; padding: 3px; }
`;

host.writeMultipleFiles({
'src/app/app.component.css': cssContent,
'src/assets/foo.css': cssContent,
'src/styles.css': cssContent,
});

const logger = new logging.Logger('');
const logs: string[] = [];
logger.subscribe(e => logs.push(e.message));

const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
const output = await run.result;
expect(output.success).toBe(true);
expect(logs.length).toBe(2);
expect(logs.join()).toMatch(/WARNING.+app\.component\.css/);
await run.stop();
});

it(`shows error for large component css when using 'anyComponentStyle' when AOT`, async () => {
const overrides = {
aot: true,
optimization: true,
budgets: [{ type: 'anyComponentStyle', maximumError: '1b' }],
};

const cssContent = `
.foo { color: white; padding: 1px; }
.buz { color: white; padding: 2px; }
.bar { color: white; padding: 3px; }
`;

host.writeMultipleFiles({
'src/app/app.component.css': cssContent,
'src/assets/foo.css': cssContent,
'src/styles.css': cssContent,
});

const logger = new logging.Logger('');
const logs: string[] = [];
logger.subscribe(e => logs.push(e.message));

const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
const output = await run.result;
expect(output.success).toBe(false);
expect(logs.length).toBe(2);
expect(logs.join()).toMatch(/ERROR.+app\.component\.css/);
await run.stop();
});

describe(`should ignore '.map' files`, () => {
it(`when 'bundle' budget`, async () => {
const overrides = {
Expand Down

0 comments on commit bcf250c

Please sign in to comment.