Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(@angular/cli): Ability to specify budgets for your apps #8939

Merged
merged 1 commit into from
Jan 18, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/documentation/stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Angular Material](stories/include-angular-material)
- [AngularFire](stories/include-angularfire)
- [Bootstrap](stories/include-bootstrap)
- [Budgets](stories/budgets)
- [Font Awesome](stories/include-font-awesome)
- [Moving Into the CLI](stories/moving-into-the-cli)
- [Moving Out of the CLI](stories/moving-out-of-the-cli)
Expand Down
61 changes: 61 additions & 0 deletions docs/documentation/stories/budgets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Budgets

As applications grow in functionality, they also grow in size. Budgets is a feature in the
Angular CLI which allows you to set budget thresholds in your configuration to ensure parts
of your application stay within boundries which you set.

**.angular-cli.json**
```
{
...
apps: [
{
...
budgets: []
}
]
}
```

## Budget Definition

- type
- The type of budget.
- Possible values:
- bundle - The size of a specific bundle.
- initial - The initial size of the app.
- allScript - The size of all scripts.
- all - The size of the entire app.
- anyScript - The size of any one script.
- any - The size of any file.
- name
- The name of the bundle.
- Required only for type of "bundle"
- baseline
- The baseline size for comparison.
- maximumWarning
- The maximum threshold for warning relative to the baseline.
- maximumError
- The maximum threshold for error relative to the baseline.
- minimumWarning
- The minimum threshold for warning relative to the baseline.
- minimumError
- The minimum threshold for error relative to the baseline.
- warning
- The threshold for warning relative to the baseline (min & max).
- error
- The threshold for error relative to the baseline (min & max).

## Specifying sizes

Available formats:
123 - size in bytes
123b - size in bytes
123kb - size in kilobytes
123mb - size in megabytes
12% - percentage

## NOTES

All sizes are relative to baseline.
Percentages are not valid for baseline values.
47 changes: 47 additions & 0 deletions packages/@angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,53 @@
},
"default": []
},
"budgets": {
"type": "array",
"description": "Threshold definitions for bundle sizes.",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["bundle", "initial", "allScript", "all", "anyScript", "any"],
"description": "The type of budget"
},
"name": {
"type": "string",
"description": "The name of the bundle"
},
"baseline": {
"type": "string",
"description": "The baseline size for comparison."
},
"maximumWarning": {
"type": "string",
"description": "The maximum threshold for warning relative to the baseline."
},
"maximumError": {
"type": "string",
"description": "The maximum threshold for error relative to the baseline."
},
"minimumWarning": {
"type": "string",
"description": "The minimum threshold for warning relative to the baseline."
},
"minimumError": {
"type": "string",
"description": "The minimum threshold for error relative to the baseline."
},
"warning": {
"type": "string",
"description": "The threshold for warning relative to the baseline (min & max)."
},
"error": {
"type": "string",
"description": "The threshold for error relative to the baseline (min & max)."
}
}
},
"default": []
},
"deployUrl": {
"type": "string",
"description": "URL where files will be deployed."
Expand Down
5 changes: 5 additions & 0 deletions packages/@angular/cli/models/webpack-configs/production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as semver from 'semver';
import { stripIndent } from 'common-tags';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import { PurifyPlugin } from '@angular-devkit/build-optimizer';
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
import { StaticAssetPlugin } from '../../plugins/static-asset';
import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin';
import { WebpackConfigOptions } from '../webpack-config';
Expand Down Expand Up @@ -108,6 +109,10 @@ export function getProdConfig(wco: WebpackConfigOptions) {
}
}

extraPlugins.push(new BundleBudgetPlugin({
budgets: appConfig.budgets
}));

if (buildOptions.extractLicenses) {
extraPlugins.push(new LicenseWebpackPlugin({
pattern: /^(MIT|ISC|BSD.*)$/,
Expand Down
129 changes: 129 additions & 0 deletions packages/@angular/cli/plugins/bundle-budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @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 { Budget, calculateBytes, calculateSizes } from '../utilities/bundle-calculator';

interface Thresholds {
maximumWarning?: number;
maximumError?: number;
minimumWarning?: number;
minimumError?: number;
warningLow?: number;
warningHigh?: number;
errorLow?: number;
errorHigh?: number;
}

export interface BundleBudgetPluginOptions {
budgets: Budget[];
}

export class BundleBudgetPlugin {
constructor(private options: BundleBudgetPluginOptions) {}

apply(compiler: any): void {
const { budgets } = this.options;
compiler.plugin('after-emit', (compilation: any, cb: Function) => {
if (!budgets || budgets.length === 0) {
cb();
return;
}

budgets.map(budget => {
const thresholds = this.calcualte(budget);
return {
budget,
thresholds,
sizes: calculateSizes(budget, compilation)
};
})
.forEach(budgetCheck => {
budgetCheck.sizes.forEach(size => {
if (budgetCheck.thresholds.maximumWarning) {
if (budgetCheck.thresholds.maximumWarning < size.size) {
compilation.warnings.push(`budgets, maximum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.maximumError) {
if (budgetCheck.thresholds.maximumError < size.size) {
compilation.errors.push(`budgets, maximum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.minimumWarning) {
if (budgetCheck.thresholds.minimumWarning > size.size) {
compilation.warnings.push(`budgets, minimum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.minimumError) {
if (budgetCheck.thresholds.minimumError > size.size) {
compilation.errors.push(`budgets, minimum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.warningLow) {
if (budgetCheck.thresholds.warningLow > size.size) {
compilation.warnings.push(`budgets, minimum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.warningHigh) {
if (budgetCheck.thresholds.warningHigh < size.size) {
compilation.warnings.push(`budgets, maximum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.errorLow) {
if (budgetCheck.thresholds.errorLow > size.size) {
compilation.errors.push(`budgets, minimum exceeded for ${size.label}.`);
}
}
if (budgetCheck.thresholds.errorHigh) {
if (budgetCheck.thresholds.errorHigh < size.size) {
compilation.errors.push(`budgets, maximum exceeded for ${size.label}.`);
}
}
});

});
cb();
});
}

private calcualte(budget: Budget): Thresholds {
let thresholds: Thresholds = {};
if (budget.maximumWarning) {
thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 'pos');
}

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

if (budget.minimumWarning) {
thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, 'neg');
}

if (budget.minimumError) {
thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, 'neg');
}

if (budget.warning) {
thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, 'neg');
}

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

if (budget.error) {
thresholds.errorLow = calculateBytes(budget.error, budget.baseline, 'neg');
}

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

return thresholds;
}
}
1 change: 1 addition & 0 deletions packages/@angular/cli/plugins/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export { BaseHrefWebpackPlugin } from '../lib/base-href-webpack/base-href-webpack-plugin';
export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss-webpack-plugin';
export { GlobCopyWebpackPlugin, GlobCopyWebpackPluginOptions } from './glob-copy-webpack-plugin';
export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
export { NamedLazyChunksWebpackPlugin } from './named-lazy-chunks-webpack-plugin';
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';
export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin';
13 changes: 13 additions & 0 deletions packages/@angular/cli/tasks/eject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ class JsonWebpackSerializer {
};
}

private _bundleBudgetPluginSerialize(value: any): any {
console.log('VALUE!!!');
console.log(value);
let budgets = value.options.budgets;
return {
budgets
};
}

private _insertConcatAssetsWebpackPluginSerialize(value: any): any {
return value.entryNames;
}
Expand Down Expand Up @@ -200,6 +209,10 @@ class JsonWebpackSerializer {
args = this._globCopyWebpackPluginSerialize(plugin);
this._addImport('@angular/cli/plugins/webpack', 'GlobCopyWebpackPlugin');
break;
case angularCliPlugins.BundleBudgetPlugin:
args = this._bundleBudgetPluginSerialize(plugin);
this._addImport('@angular/cli/plugins/webpack', 'BundleBudgetPlugin');
break;
case angularCliPlugins.InsertConcatAssetsWebpackPlugin:
args = this._insertConcatAssetsWebpackPluginSerialize(plugin);
this._addImport('@angular/cli/plugins/webpack', 'InsertConcatAssetsWebpackPlugin');
Expand Down