Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@
"allScript",
"any",
"anyScript",
"anyComponentStyle",
"bundle",
"initial"
]
Expand Down
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 @@ -7,7 +7,7 @@
*/

import { Architect } from '@angular-devkit/architect';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { join, logging, normalize, virtualFs } from '@angular-devkit/core';
import { BrowserBuilderOutput } from '../../src/browser';
import { createArchitect, host, ivyEnabled } from '../utils';

Expand Down Expand Up @@ -39,4 +39,28 @@ describe('Browser Builder AOT', () => {

await run.stop();
});

it('shows warnings for component styles', async () => {
const overrides = {
aot: true,
optimization: true,
};

host.writeMultipleFiles({
'src/app/app.component.css': `
.foo { color: white; padding: 1px; };
.buz { color: white; padding: 2px; };
`,
});

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.join()).toContain('WARNING in Invalid selector');
await run.stop();
});
});
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' }],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the plan for showing this warning overall? Will we update bundle budgets in 9 for all projects?

Copy link
Collaborator Author

@alan-agius4 alan-agius4 Jul 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best option would be to do it for 9 for existing projects since users won't typically run migrations on minor and for new projects we can do it right away.

  1. What should be the value maximumWarning and maximumError for new projects?
  2. I am guessing that for the migration we will not want to add maximumError as this will break exiting projects, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think it should be for 9 too. We should add the migration to this PR as well.

As for the default numbers, I'm not really sure. UTF-8 uses 1 to 4 bytes for each char, most commonly only 1 byte for US ASCII chars. So 1kb would be 1024 chars. Off the top of my head warning on 5kb and erroring on 20kb sounds within the realm of reasonable. @mgechev do you have a recommendation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ill start working on the migrations, and than we just change the number when we have more feedback from @mgechev.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good source for setting up good defaults will be http://bit.ly/perf-budget-calculator

I'd recommend bumping the error limit up with few extra 100s of KBs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alan-agius4 do we have a way forward with a global css bundle budget? Not asking to have it included in this PR, just asking if the changes in this PR would get in the way of that. I imagine a anyGlobalCss budget or something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global styles meaning styles in general or that are set globally ie imported via style.css?

For the later we already have the AnyCalculator which can be used to set budgets on global css.

However, if we'd want to add a budget for css within the component + global css we can do it and this PR will provide a way forward for that.

My concerns would be that if want to sum up css from all the components and global stylesheet, you might end up with a misleading budget. Let's say the result of total CSS is 500Kb, when having lazy loading this will never be loaded/parsed at once.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic budgets, such as LightWallet would be great in such scenario. In the same time, we might be able to estimate what would be loaded initially, right? In webpack we have this metadata in the compilation stats.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is indeed is the enabler for that :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have some budgets for initial chunks

};

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
17 changes: 13 additions & 4 deletions packages/ngtools/webpack/src/resource_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,19 @@ export class WebpackResourceLoader {
return new Promise((resolve, reject) => {
childCompiler.compile((err: Error, childCompilation: any) => {
// Resolve / reject the promise
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
const errorDetails = childCompilation.errors.map(function (error: any) {
return error.message + (error.error ? ':\n' + error.error : '');
}).join('\n');
const { warnings, errors } = childCompilation;

if (warnings && warnings.length) {
this._parentCompilation.warnings.push(...warnings);
}

if (errors && errors.length) {
this._parentCompilation.errors.push(...errors);

const errorDetails = errors
.map((error: any) => error.message + (error.error ? ':\n' + error.error : ''))
.join('\n');

reject(new Error('Child compilation failed:\n' + errorDetails));
} else if (err) {
reject(err);
Expand Down
5 changes: 5 additions & 0 deletions packages/schematics/angular/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul
type: 'initial',
maximumWarning: '2mb',
maximumError: '5mb',
},
{
type: 'anyComponentStyle',
maximumWarning: '6kb',
maximumError: '10kb',
}],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ import {
} from '@angular-devkit/core';
import { Rule, Tree, UpdateRecorder } from '@angular-devkit/schematics';
import {
appendValueInAstArray,
findPropertyInAstObject,
insertPropertyInAstObjectInOrder,
removePropertyInAstObject,
} from '../../utility/json-utils';

export const ANY_COMPONENT_STYLE_BUDGET = {
type: 'anyComponentStyle',
maximumWarning: '6kb',
};

export function UpdateWorkspaceConfig(): Rule {
return (tree: Tree) => {
let workspaceConfigPath = 'angular.json';
Expand Down Expand Up @@ -59,8 +65,9 @@ export function UpdateWorkspaceConfig(): Rule {
const builder = findPropertyInAstObject(buildTarget, 'builder');
// Projects who's build builder is not build-angular:browser
if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:browser') {
updateOption('styles', recorder, buildTarget);
updateOption('scripts', recorder, buildTarget);
updateStyleOrScriptOption('styles', recorder, buildTarget);
updateStyleOrScriptOption('scripts', recorder, buildTarget);
addAnyComponentStyleBudget(recorder, buildTarget);
}
}

Expand All @@ -69,8 +76,8 @@ export function UpdateWorkspaceConfig(): Rule {
const builder = findPropertyInAstObject(testTarget, 'builder');
// Projects who's build builder is not build-angular:browser
if (builder && builder.kind === 'string' && builder.value === '@angular-devkit/build-angular:karma') {
updateOption('styles', recorder, testTarget);
updateOption('scripts', recorder, testTarget);
updateStyleOrScriptOption('styles', recorder, testTarget);
updateStyleOrScriptOption('scripts', recorder, testTarget);
}
}
}
Expand All @@ -84,19 +91,21 @@ export function UpdateWorkspaceConfig(): Rule {
/**
* Helper to retreive all the options in various configurations
*/
function getAllOptions(builderConfig: JsonAstObject): JsonAstObject[] {
function getAllOptions(builderConfig: JsonAstObject, configurationsOnly = false): JsonAstObject[] {
const options = [];
const configurations = findPropertyInAstObject(builderConfig, 'configurations');
if (configurations && configurations.kind === 'object') {
options.push(...configurations.properties.map(x => x.value));
}

options.push(findPropertyInAstObject(builderConfig, 'options'));
if (!configurationsOnly) {
options.push(findPropertyInAstObject(builderConfig, 'options'));
}

return options.filter(o => o && o.kind === 'object') as JsonAstObject[];
}

function updateOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) {
function updateStyleOrScriptOption(property: 'scripts' | 'styles', recorder: UpdateRecorder, builderConfig: JsonAstObject) {
const options = getAllOptions(builderConfig);

for (const option of options) {
Expand All @@ -121,3 +130,42 @@ function updateOption(property: 'scripts' | 'styles', recorder: UpdateRecorder,
}
}
}

function addAnyComponentStyleBudget(recorder: UpdateRecorder, builderConfig: JsonAstObject) {
const options = getAllOptions(builderConfig, true);

for (const option of options) {
const aotOption = findPropertyInAstObject(option, 'aot');
if (!aotOption || aotOption.kind !== 'true') {
// AnyComponentStyle only works for AOT
continue;
}

const budgetOption = findPropertyInAstObject(option, 'budgets');
if (!budgetOption) {
// add
insertPropertyInAstObjectInOrder(recorder, option, 'budgets', [ANY_COMPONENT_STYLE_BUDGET], 14);
continue;
}

if (budgetOption.kind !== 'array') {
continue;
}

// if 'anyComponentStyle' budget already exists don't add.
const hasAnyComponentStyle = budgetOption.elements.some(node => {
if (!node || node.kind !== 'object') {
// skip non complex objects
return false;
}

const budget = findPropertyInAstObject(node, 'type');

return !!budget && budget.kind === 'string' && budget.value === 'anyComponentStyle';
});

if (!hasAnyComponentStyle) {
appendValueInAstArray(recorder, budgetOption, ANY_COMPONENT_STYLE_BUDGET, 16);
}
}
}
Loading