Skip to content

Commit

Permalink
feat(@angular/cli): Ability to specify budgets for your apps
Browse files Browse the repository at this point in the history
Closes #7139
  • Loading branch information
Brocco authored and hansl committed Jan 18, 2018
1 parent 23fee3d commit 9c871b0
Show file tree
Hide file tree
Showing 10 changed files with 621 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/documentation/stories.md
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
@@ -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
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
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
@@ -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 {

This comment has been minimized.

Copy link
@piecioshka

piecioshka Feb 17, 2018

Typo 😄

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
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
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

0 comments on commit 9c871b0

Please sign in to comment.