mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 19:13:34 +08:00
Previous ``` packages/angular_devkit/build_angular ├── plugins │ └── webpack └── src ├── angular-cli-files │ ├── models │ │ └── webpack-configs │ ├── plugins │ └── utilities │ ├── index-file │ └── service-worker ├── app-shell ├── browser │ └── specs ├── dev-server ├── extract-i18n ├── karma ├── ng-packagr ├── protractor ├── server ├── tslint ├── utils └── webpack └── models ``` Now ``` packages/angular_devkit/build_angular ├── plugins └── src ├── app-shell ├── browser │ └── specs ├── dev-server ├── extract-i18n ├── karma ├── ng-packagr ├── protractor ├── server ├── tslint ├── utils │ └── index-file └── webpack ├── configs ├── plugins └── utils ```
422 lines
12 KiB
TypeScript
422 lines
12 KiB
TypeScript
/**
|
|
* @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 * as webpack from 'webpack';
|
|
import { Budget, Type } from '../browser/schema';
|
|
import { ProcessBundleFile, ProcessBundleResult } from '../utils/process-bundle';
|
|
import { formatSize } from '../webpack/utils/stats';
|
|
|
|
interface Size {
|
|
size: number;
|
|
label?: string;
|
|
}
|
|
|
|
interface Threshold {
|
|
limit: number;
|
|
type: ThresholdType;
|
|
severity: ThresholdSeverity;
|
|
}
|
|
|
|
enum ThresholdType {
|
|
Max = 'maximum',
|
|
Min = 'minimum',
|
|
}
|
|
|
|
export enum ThresholdSeverity {
|
|
Warning = 'warning',
|
|
Error = 'error',
|
|
}
|
|
|
|
enum DifferentialBuildType {
|
|
// FIXME: this should match the actual file suffix and not hardcoded.
|
|
ORIGINAL = 'es2015',
|
|
DOWNLEVEL = 'es5',
|
|
}
|
|
|
|
export function* calculateThresholds(budget: Budget): IterableIterator<Threshold> {
|
|
if (budget.maximumWarning) {
|
|
yield {
|
|
limit: calculateBytes(budget.maximumWarning, budget.baseline, 1),
|
|
type: ThresholdType.Max,
|
|
severity: ThresholdSeverity.Warning,
|
|
};
|
|
}
|
|
|
|
if (budget.maximumError) {
|
|
yield {
|
|
limit: calculateBytes(budget.maximumError, budget.baseline, 1),
|
|
type: ThresholdType.Max,
|
|
severity: ThresholdSeverity.Error,
|
|
};
|
|
}
|
|
|
|
if (budget.minimumWarning) {
|
|
yield {
|
|
limit: calculateBytes(budget.minimumWarning, budget.baseline, -1),
|
|
type: ThresholdType.Min,
|
|
severity: ThresholdSeverity.Warning,
|
|
};
|
|
}
|
|
|
|
if (budget.minimumError) {
|
|
yield {
|
|
limit: calculateBytes(budget.minimumError, budget.baseline, -1),
|
|
type: ThresholdType.Min,
|
|
severity: ThresholdSeverity.Error,
|
|
};
|
|
}
|
|
|
|
if (budget.warning) {
|
|
yield {
|
|
limit: calculateBytes(budget.warning, budget.baseline, -1),
|
|
type: ThresholdType.Min,
|
|
severity: ThresholdSeverity.Warning,
|
|
};
|
|
|
|
yield {
|
|
limit: calculateBytes(budget.warning, budget.baseline, 1),
|
|
type: ThresholdType.Max,
|
|
severity: ThresholdSeverity.Warning,
|
|
};
|
|
}
|
|
|
|
if (budget.error) {
|
|
yield {
|
|
limit: calculateBytes(budget.error, budget.baseline, -1),
|
|
type: ThresholdType.Min,
|
|
severity: ThresholdSeverity.Error,
|
|
};
|
|
|
|
yield {
|
|
limit: calculateBytes(budget.error, budget.baseline, 1),
|
|
type: ThresholdType.Max,
|
|
severity: ThresholdSeverity.Error,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates the sizes for bundles in the budget type provided.
|
|
*/
|
|
function calculateSizes(
|
|
budget: Budget,
|
|
stats: webpack.Stats.ToJsonOutput,
|
|
processResults: ProcessBundleResult[],
|
|
): Size[] {
|
|
if (budget.type === Type.AnyComponentStyle) {
|
|
// Component style size information is not available post-build, this must
|
|
// be checked mid-build via the `AnyComponentStyleBudgetChecker` plugin.
|
|
throw new Error('Can not calculate size of AnyComponentStyle. Use `AnyComponentStyleBudgetChecker` instead.');
|
|
}
|
|
|
|
type NonComponentStyleBudgetTypes = Exclude<Budget['type'], Type.AnyComponentStyle>;
|
|
type CalculatorTypes = { new(budget: Budget, chunks: Chunk[], assets: Asset[], processResults: ProcessBundleResult[]): Calculator };
|
|
const calculatorMap: Record<NonComponentStyleBudgetTypes, CalculatorTypes> = {
|
|
all: AllCalculator,
|
|
allScript: AllScriptCalculator,
|
|
any: AnyCalculator,
|
|
anyScript: AnyScriptCalculator,
|
|
bundle: BundleCalculator,
|
|
initial: InitialCalculator,
|
|
};
|
|
|
|
const ctor = calculatorMap[budget.type];
|
|
const {chunks, assets} = stats;
|
|
if (!chunks) {
|
|
throw new Error('Webpack stats output did not include chunk information.');
|
|
}
|
|
if (!assets) {
|
|
throw new Error('Webpack stats output did not include asset information.');
|
|
}
|
|
|
|
const calculator = new ctor(budget, chunks, assets, processResults);
|
|
|
|
return calculator.calculate();
|
|
}
|
|
|
|
type ArrayElement<T> = T extends Array<infer U> ? U : never;
|
|
type Chunk = ArrayElement<Exclude<webpack.Stats.ToJsonOutput['chunks'], undefined>>;
|
|
type Asset = ArrayElement<Exclude<webpack.Stats.ToJsonOutput['assets'], undefined>>;
|
|
abstract class Calculator {
|
|
constructor (
|
|
protected budget: Budget,
|
|
protected chunks: Chunk[],
|
|
protected assets: Asset[],
|
|
protected processResults: ProcessBundleResult[],
|
|
) {}
|
|
|
|
abstract calculate(): Size[];
|
|
|
|
/** Calculates the size of the given chunk for the provided build type. */
|
|
protected calculateChunkSize(
|
|
chunk: Chunk,
|
|
buildType: DifferentialBuildType,
|
|
): number {
|
|
// Look for a process result containing different builds for this chunk.
|
|
const processResult = this.processResults
|
|
.find((processResult) => processResult.name === chunk.id.toString());
|
|
|
|
if (processResult) {
|
|
// Found a differential build, use the correct size information.
|
|
const processResultFile = getDifferentialBuildResult(
|
|
processResult, buildType);
|
|
|
|
return processResultFile && processResultFile.size || 0;
|
|
} else {
|
|
// No differential builds, get the chunk size by summing its assets.
|
|
return chunk.files
|
|
.filter(file => !file.endsWith('.map'))
|
|
.map(file => {
|
|
const asset = this.assets.find((asset) => asset.name === file);
|
|
if (!asset) {
|
|
throw new Error(`Could not find asset for file: ${file}`);
|
|
}
|
|
|
|
return asset.size;
|
|
})
|
|
.reduce((l, r) => l + r, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A named bundle.
|
|
*/
|
|
class BundleCalculator extends Calculator {
|
|
calculate() {
|
|
const budgetName = this.budget.name;
|
|
if (!budgetName) {
|
|
return [];
|
|
}
|
|
|
|
// The chunk may or may not have differential builds. Compute the size for
|
|
// each then check afterwards if they are all the same.
|
|
const buildSizes = Object.values(DifferentialBuildType).map((buildType) => {
|
|
const size = this.chunks
|
|
.filter(chunk => chunk.names.indexOf(budgetName) !== -1)
|
|
.map(chunk => this.calculateChunkSize(chunk, buildType))
|
|
.reduce((l, r) => l + r, 0);
|
|
|
|
return {size, label: `bundle ${this.budget.name}-${buildType}`};
|
|
});
|
|
|
|
// If this bundle was not actually generated by a differential build, then
|
|
// merge the results into a single value.
|
|
if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) {
|
|
return mergeDifferentialBuildSizes(buildSizes, budgetName);
|
|
} else {
|
|
return buildSizes;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The sum of all initial chunks (marked as initial).
|
|
*/
|
|
class InitialCalculator extends Calculator {
|
|
calculate() {
|
|
const buildSizes = Object.values(DifferentialBuildType).map((buildType) => {
|
|
return {
|
|
label: `bundle initial-${buildType}`,
|
|
size: this.chunks
|
|
.filter(chunk => chunk.initial)
|
|
.map(chunk => this.calculateChunkSize(chunk, buildType))
|
|
.reduce((l, r) => l + r, 0),
|
|
};
|
|
});
|
|
|
|
// If this bundle was not actually generated by a differential build, then
|
|
// merge the results into a single value.
|
|
if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) {
|
|
return mergeDifferentialBuildSizes(buildSizes, 'initial');
|
|
} else {
|
|
return buildSizes;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The sum of all the scripts portions.
|
|
*/
|
|
class AllScriptCalculator extends Calculator {
|
|
calculate() {
|
|
const size = this.assets
|
|
.filter(asset => asset.name.endsWith('.js'))
|
|
.map(asset => asset.size)
|
|
.reduce((total: number, size: number) => total + size, 0);
|
|
|
|
return [{size, label: 'total scripts'}];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* All scripts and assets added together.
|
|
*/
|
|
class AllCalculator extends Calculator {
|
|
calculate() {
|
|
const size = this.assets
|
|
.filter(asset => !asset.name.endsWith('.map'))
|
|
.map(asset => asset.size)
|
|
.reduce((total: number, size: number) => total + size, 0);
|
|
|
|
return [{size, label: 'total'}];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Any script, individually.
|
|
*/
|
|
class AnyScriptCalculator extends Calculator {
|
|
calculate() {
|
|
return this.assets
|
|
.filter(asset => asset.name.endsWith('.js'))
|
|
.map(asset => ({
|
|
size: asset.size,
|
|
label: asset.name,
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Any script or asset (images, css, etc).
|
|
*/
|
|
class AnyCalculator extends Calculator {
|
|
calculate() {
|
|
return this.assets
|
|
.filter(asset => !asset.name.endsWith('.map'))
|
|
.map(asset => ({
|
|
size: asset.size,
|
|
label: asset.name,
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the bytes given a string value.
|
|
*/
|
|
function calculateBytes(
|
|
input: string,
|
|
baseline?: string,
|
|
factor: 1 | -1 = 1,
|
|
): number {
|
|
const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/);
|
|
if (!matches) {
|
|
return NaN;
|
|
}
|
|
|
|
const baselineBytes = baseline && calculateBytes(baseline) || 0;
|
|
|
|
let value = Number(matches[1]);
|
|
switch (matches[2] && matches[2].toLowerCase()) {
|
|
case '%':
|
|
value = baselineBytes * value / 100;
|
|
break;
|
|
case 'kb':
|
|
value *= 1024;
|
|
break;
|
|
case 'mb':
|
|
value *= 1024 * 1024;
|
|
break;
|
|
case 'gb':
|
|
value *= 1024 * 1024 * 1024;
|
|
break;
|
|
}
|
|
|
|
if (baselineBytes === 0) {
|
|
return value;
|
|
}
|
|
|
|
return baselineBytes + value * factor;
|
|
}
|
|
|
|
export function* checkBudgets(
|
|
budgets: Budget[],
|
|
webpackStats: webpack.Stats.ToJsonOutput,
|
|
processResults: ProcessBundleResult[],
|
|
): IterableIterator<{ severity: ThresholdSeverity, message: string }> {
|
|
// Ignore AnyComponentStyle budgets as these are handled in `AnyComponentStyleBudgetChecker`.
|
|
const computableBudgets = budgets.filter((budget) => budget.type !== Type.AnyComponentStyle);
|
|
|
|
for (const budget of computableBudgets) {
|
|
const sizes = calculateSizes(budget, webpackStats, processResults);
|
|
for (const { size, label } of sizes) {
|
|
yield* checkThresholds(calculateThresholds(budget), size, label);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function* checkThresholds(thresholds: IterableIterator<Threshold>, size: number, label?: string):
|
|
IterableIterator<{ severity: ThresholdSeverity, message: string }> {
|
|
for (const threshold of thresholds) {
|
|
switch (threshold.type) {
|
|
case ThresholdType.Max: {
|
|
if (size <= threshold.limit) {
|
|
continue;
|
|
}
|
|
|
|
const sizeDifference = formatSize(size - threshold.limit);
|
|
yield {
|
|
severity: threshold.severity,
|
|
message: `${label} exceeded maximum budget. Budget ${
|
|
formatSize(threshold.limit)} was not met by ${
|
|
sizeDifference} with a total of ${formatSize(size)}.`,
|
|
};
|
|
break;
|
|
}
|
|
case ThresholdType.Min: {
|
|
if (size >= threshold.limit) {
|
|
continue;
|
|
}
|
|
|
|
const sizeDifference = formatSize(threshold.limit - size);
|
|
yield {
|
|
severity: threshold.severity,
|
|
message: `${label} failed to meet minimum budget. Budget ${
|
|
formatSize(threshold.limit)} was not met by ${
|
|
sizeDifference} with a total of ${formatSize(size)}.`,
|
|
};
|
|
break;
|
|
} default: {
|
|
throw new Error(`Unexpected threshold type: ${ThresholdType[threshold.type]}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns the {@link ProcessBundleFile} for the given {@link DifferentialBuildType}. */
|
|
function getDifferentialBuildResult(
|
|
processResult: ProcessBundleResult, buildType: DifferentialBuildType):
|
|
ProcessBundleFile|null {
|
|
switch (buildType) {
|
|
case DifferentialBuildType.ORIGINAL: return processResult.original || null;
|
|
case DifferentialBuildType.DOWNLEVEL: return processResult.downlevel || null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges the given differential builds into a single, non-differential value.
|
|
*
|
|
* Preconditions: All the sizes should be equivalent, or else they represent
|
|
* differential builds.
|
|
*/
|
|
function mergeDifferentialBuildSizes(buildSizes: Size[], mergeLabel: string): Size[] {
|
|
if (buildSizes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Only one size.
|
|
return [{
|
|
label: mergeLabel,
|
|
size: buildSizes[0].size,
|
|
}];
|
|
}
|
|
|
|
/** Returns whether or not all items in the list are equivalent to each other. */
|
|
function allEquivalent<T>(items: Iterable<T>): boolean {
|
|
return new Set(items).size < 2;
|
|
}
|