[6a3a178] | 1 | "use strict";
|
---|
| 2 | /**
|
---|
| 3 | * @license
|
---|
| 4 | * Copyright Google LLC All Rights Reserved.
|
---|
| 5 | *
|
---|
| 6 | * Use of this source code is governed by an MIT-style license that can be
|
---|
| 7 | * found in the LICENSE file at https://angular.io/license
|
---|
| 8 | */
|
---|
| 9 | Object.defineProperty(exports, "__esModule", { value: true });
|
---|
| 10 | exports.checkThresholds = exports.checkBudgets = exports.calculateThresholds = exports.ThresholdSeverity = void 0;
|
---|
| 11 | const path_1 = require("path");
|
---|
| 12 | const schema_1 = require("../browser/schema");
|
---|
| 13 | const stats_1 = require("../webpack/utils/stats");
|
---|
| 14 | var ThresholdType;
|
---|
| 15 | (function (ThresholdType) {
|
---|
| 16 | ThresholdType["Max"] = "maximum";
|
---|
| 17 | ThresholdType["Min"] = "minimum";
|
---|
| 18 | })(ThresholdType || (ThresholdType = {}));
|
---|
| 19 | var ThresholdSeverity;
|
---|
| 20 | (function (ThresholdSeverity) {
|
---|
| 21 | ThresholdSeverity["Warning"] = "warning";
|
---|
| 22 | ThresholdSeverity["Error"] = "error";
|
---|
| 23 | })(ThresholdSeverity = exports.ThresholdSeverity || (exports.ThresholdSeverity = {}));
|
---|
| 24 | var DifferentialBuildType;
|
---|
| 25 | (function (DifferentialBuildType) {
|
---|
| 26 | DifferentialBuildType["ORIGINAL"] = "original";
|
---|
| 27 | DifferentialBuildType["DOWNLEVEL"] = "downlevel";
|
---|
| 28 | })(DifferentialBuildType || (DifferentialBuildType = {}));
|
---|
| 29 | function* calculateThresholds(budget) {
|
---|
| 30 | if (budget.maximumWarning) {
|
---|
| 31 | yield {
|
---|
| 32 | limit: calculateBytes(budget.maximumWarning, budget.baseline, 1),
|
---|
| 33 | type: ThresholdType.Max,
|
---|
| 34 | severity: ThresholdSeverity.Warning,
|
---|
| 35 | };
|
---|
| 36 | }
|
---|
| 37 | if (budget.maximumError) {
|
---|
| 38 | yield {
|
---|
| 39 | limit: calculateBytes(budget.maximumError, budget.baseline, 1),
|
---|
| 40 | type: ThresholdType.Max,
|
---|
| 41 | severity: ThresholdSeverity.Error,
|
---|
| 42 | };
|
---|
| 43 | }
|
---|
| 44 | if (budget.minimumWarning) {
|
---|
| 45 | yield {
|
---|
| 46 | limit: calculateBytes(budget.minimumWarning, budget.baseline, -1),
|
---|
| 47 | type: ThresholdType.Min,
|
---|
| 48 | severity: ThresholdSeverity.Warning,
|
---|
| 49 | };
|
---|
| 50 | }
|
---|
| 51 | if (budget.minimumError) {
|
---|
| 52 | yield {
|
---|
| 53 | limit: calculateBytes(budget.minimumError, budget.baseline, -1),
|
---|
| 54 | type: ThresholdType.Min,
|
---|
| 55 | severity: ThresholdSeverity.Error,
|
---|
| 56 | };
|
---|
| 57 | }
|
---|
| 58 | if (budget.warning) {
|
---|
| 59 | yield {
|
---|
| 60 | limit: calculateBytes(budget.warning, budget.baseline, -1),
|
---|
| 61 | type: ThresholdType.Min,
|
---|
| 62 | severity: ThresholdSeverity.Warning,
|
---|
| 63 | };
|
---|
| 64 | yield {
|
---|
| 65 | limit: calculateBytes(budget.warning, budget.baseline, 1),
|
---|
| 66 | type: ThresholdType.Max,
|
---|
| 67 | severity: ThresholdSeverity.Warning,
|
---|
| 68 | };
|
---|
| 69 | }
|
---|
| 70 | if (budget.error) {
|
---|
| 71 | yield {
|
---|
| 72 | limit: calculateBytes(budget.error, budget.baseline, -1),
|
---|
| 73 | type: ThresholdType.Min,
|
---|
| 74 | severity: ThresholdSeverity.Error,
|
---|
| 75 | };
|
---|
| 76 | yield {
|
---|
| 77 | limit: calculateBytes(budget.error, budget.baseline, 1),
|
---|
| 78 | type: ThresholdType.Max,
|
---|
| 79 | severity: ThresholdSeverity.Error,
|
---|
| 80 | };
|
---|
| 81 | }
|
---|
| 82 | }
|
---|
| 83 | exports.calculateThresholds = calculateThresholds;
|
---|
| 84 | /**
|
---|
| 85 | * Calculates the sizes for bundles in the budget type provided.
|
---|
| 86 | */
|
---|
| 87 | function calculateSizes(budget, stats, processResults) {
|
---|
| 88 | if (budget.type === schema_1.Type.AnyComponentStyle) {
|
---|
| 89 | // Component style size information is not available post-build, this must
|
---|
| 90 | // be checked mid-build via the `AnyComponentStyleBudgetChecker` plugin.
|
---|
| 91 | throw new Error('Can not calculate size of AnyComponentStyle. Use `AnyComponentStyleBudgetChecker` instead.');
|
---|
| 92 | }
|
---|
| 93 | const calculatorMap = {
|
---|
| 94 | all: AllCalculator,
|
---|
| 95 | allScript: AllScriptCalculator,
|
---|
| 96 | any: AnyCalculator,
|
---|
| 97 | anyScript: AnyScriptCalculator,
|
---|
| 98 | bundle: BundleCalculator,
|
---|
| 99 | initial: InitialCalculator,
|
---|
| 100 | };
|
---|
| 101 | const ctor = calculatorMap[budget.type];
|
---|
| 102 | const { chunks, assets } = stats;
|
---|
| 103 | if (!chunks) {
|
---|
| 104 | throw new Error('Webpack stats output did not include chunk information.');
|
---|
| 105 | }
|
---|
| 106 | if (!assets) {
|
---|
| 107 | throw new Error('Webpack stats output did not include asset information.');
|
---|
| 108 | }
|
---|
| 109 | const calculator = new ctor(budget, chunks, assets, processResults);
|
---|
| 110 | return calculator.calculate();
|
---|
| 111 | }
|
---|
| 112 | class Calculator {
|
---|
| 113 | constructor(budget, chunks, assets, processResults) {
|
---|
| 114 | this.budget = budget;
|
---|
| 115 | this.chunks = chunks;
|
---|
| 116 | this.assets = assets;
|
---|
| 117 | this.processResults = processResults;
|
---|
| 118 | }
|
---|
| 119 | /** Calculates the size of the given chunk for the provided build type. */
|
---|
| 120 | calculateChunkSize(chunk, buildType) {
|
---|
| 121 | // Look for a process result containing different builds for this chunk.
|
---|
| 122 | const processResult = this.processResults.find((processResult) => { var _a; return processResult.name === ((_a = chunk.id) === null || _a === void 0 ? void 0 : _a.toString()); });
|
---|
| 123 | if (processResult) {
|
---|
| 124 | // Found a differential build, use the correct size information.
|
---|
| 125 | const processResultFile = getDifferentialBuildResult(processResult, buildType);
|
---|
| 126 | return (processResultFile && processResultFile.size) || 0;
|
---|
| 127 | }
|
---|
| 128 | else {
|
---|
| 129 | // No differential builds, get the chunk size by summing its assets.
|
---|
| 130 | if (!chunk.files) {
|
---|
| 131 | return 0;
|
---|
| 132 | }
|
---|
| 133 | return chunk.files
|
---|
| 134 | .filter((file) => !file.endsWith('.map'))
|
---|
| 135 | .map((file) => {
|
---|
| 136 | const asset = this.assets.find((asset) => asset.name === file);
|
---|
| 137 | if (!asset) {
|
---|
| 138 | throw new Error(`Could not find asset for file: ${file}`);
|
---|
| 139 | }
|
---|
| 140 | return asset.size;
|
---|
| 141 | })
|
---|
| 142 | .reduce((l, r) => l + r, 0);
|
---|
| 143 | }
|
---|
| 144 | }
|
---|
| 145 | getAssetSize(asset) {
|
---|
| 146 | if (asset.name.endsWith('.js')) {
|
---|
| 147 | const processResult = this.processResults.find((processResult) => processResult.original && path_1.basename(processResult.original.filename) === asset.name);
|
---|
| 148 | if (processResult === null || processResult === void 0 ? void 0 : processResult.original) {
|
---|
| 149 | return processResult.original.size;
|
---|
| 150 | }
|
---|
| 151 | }
|
---|
| 152 | return asset.size;
|
---|
| 153 | }
|
---|
| 154 | }
|
---|
| 155 | /**
|
---|
| 156 | * A named bundle.
|
---|
| 157 | */
|
---|
| 158 | class BundleCalculator extends Calculator {
|
---|
| 159 | calculate() {
|
---|
| 160 | const budgetName = this.budget.name;
|
---|
| 161 | if (!budgetName) {
|
---|
| 162 | return [];
|
---|
| 163 | }
|
---|
| 164 | const buildTypeLabels = getBuildTypeLabels(this.processResults);
|
---|
| 165 | // The chunk may or may not have differential builds. Compute the size for
|
---|
| 166 | // each then check afterwards if they are all the same.
|
---|
| 167 | const buildSizes = Object.values(DifferentialBuildType).map((buildType) => {
|
---|
| 168 | const size = this.chunks
|
---|
| 169 | .filter((chunk) => { var _a; return (_a = chunk === null || chunk === void 0 ? void 0 : chunk.names) === null || _a === void 0 ? void 0 : _a.includes(budgetName); })
|
---|
| 170 | .map((chunk) => this.calculateChunkSize(chunk, buildType))
|
---|
| 171 | .reduce((l, r) => l + r, 0);
|
---|
| 172 | return { size, label: `bundle ${this.budget.name}-${buildTypeLabels[buildType]}` };
|
---|
| 173 | });
|
---|
| 174 | // If this bundle was not actually generated by a differential build, then
|
---|
| 175 | // merge the results into a single value.
|
---|
| 176 | if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) {
|
---|
| 177 | return mergeDifferentialBuildSizes(buildSizes, budgetName);
|
---|
| 178 | }
|
---|
| 179 | else {
|
---|
| 180 | return buildSizes;
|
---|
| 181 | }
|
---|
| 182 | }
|
---|
| 183 | }
|
---|
| 184 | /**
|
---|
| 185 | * The sum of all initial chunks (marked as initial).
|
---|
| 186 | */
|
---|
| 187 | class InitialCalculator extends Calculator {
|
---|
| 188 | calculate() {
|
---|
| 189 | const buildTypeLabels = getBuildTypeLabels(this.processResults);
|
---|
| 190 | const buildSizes = Object.values(DifferentialBuildType).map((buildType) => {
|
---|
| 191 | return {
|
---|
| 192 | label: `bundle initial-${buildTypeLabels[buildType]}`,
|
---|
| 193 | size: this.chunks
|
---|
| 194 | .filter((chunk) => chunk.initial)
|
---|
| 195 | .map((chunk) => this.calculateChunkSize(chunk, buildType))
|
---|
| 196 | .reduce((l, r) => l + r, 0),
|
---|
| 197 | };
|
---|
| 198 | });
|
---|
| 199 | // If this bundle was not actually generated by a differential build, then
|
---|
| 200 | // merge the results into a single value.
|
---|
| 201 | if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) {
|
---|
| 202 | return mergeDifferentialBuildSizes(buildSizes, 'initial');
|
---|
| 203 | }
|
---|
| 204 | else {
|
---|
| 205 | return buildSizes;
|
---|
| 206 | }
|
---|
| 207 | }
|
---|
| 208 | }
|
---|
| 209 | /**
|
---|
| 210 | * The sum of all the scripts portions.
|
---|
| 211 | */
|
---|
| 212 | class AllScriptCalculator extends Calculator {
|
---|
| 213 | calculate() {
|
---|
| 214 | const size = this.assets
|
---|
| 215 | .filter((asset) => asset.name.endsWith('.js'))
|
---|
| 216 | .map((asset) => this.getAssetSize(asset))
|
---|
| 217 | .reduce((total, size) => total + size, 0);
|
---|
| 218 | return [{ size, label: 'total scripts' }];
|
---|
| 219 | }
|
---|
| 220 | }
|
---|
| 221 | /**
|
---|
| 222 | * All scripts and assets added together.
|
---|
| 223 | */
|
---|
| 224 | class AllCalculator extends Calculator {
|
---|
| 225 | calculate() {
|
---|
| 226 | const size = this.assets
|
---|
| 227 | .filter((asset) => !asset.name.endsWith('.map'))
|
---|
| 228 | .map((asset) => this.getAssetSize(asset))
|
---|
| 229 | .reduce((total, size) => total + size, 0);
|
---|
| 230 | return [{ size, label: 'total' }];
|
---|
| 231 | }
|
---|
| 232 | }
|
---|
| 233 | /**
|
---|
| 234 | * Any script, individually.
|
---|
| 235 | */
|
---|
| 236 | class AnyScriptCalculator extends Calculator {
|
---|
| 237 | calculate() {
|
---|
| 238 | return this.assets
|
---|
| 239 | .filter((asset) => asset.name.endsWith('.js'))
|
---|
| 240 | .map((asset) => ({
|
---|
| 241 | size: this.getAssetSize(asset),
|
---|
| 242 | label: asset.name,
|
---|
| 243 | }));
|
---|
| 244 | }
|
---|
| 245 | }
|
---|
| 246 | /**
|
---|
| 247 | * Any script or asset (images, css, etc).
|
---|
| 248 | */
|
---|
| 249 | class AnyCalculator extends Calculator {
|
---|
| 250 | calculate() {
|
---|
| 251 | return this.assets
|
---|
| 252 | .filter((asset) => !asset.name.endsWith('.map'))
|
---|
| 253 | .map((asset) => ({
|
---|
| 254 | size: this.getAssetSize(asset),
|
---|
| 255 | label: asset.name,
|
---|
| 256 | }));
|
---|
| 257 | }
|
---|
| 258 | }
|
---|
| 259 | /**
|
---|
| 260 | * Calculate the bytes given a string value.
|
---|
| 261 | */
|
---|
| 262 | function calculateBytes(input, baseline, factor = 1) {
|
---|
| 263 | const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/);
|
---|
| 264 | if (!matches) {
|
---|
| 265 | return NaN;
|
---|
| 266 | }
|
---|
| 267 | const baselineBytes = (baseline && calculateBytes(baseline)) || 0;
|
---|
| 268 | let value = Number(matches[1]);
|
---|
| 269 | switch (matches[2] && matches[2].toLowerCase()) {
|
---|
| 270 | case '%':
|
---|
| 271 | value = (baselineBytes * value) / 100;
|
---|
| 272 | break;
|
---|
| 273 | case 'kb':
|
---|
| 274 | value *= 1024;
|
---|
| 275 | break;
|
---|
| 276 | case 'mb':
|
---|
| 277 | value *= 1024 * 1024;
|
---|
| 278 | break;
|
---|
| 279 | case 'gb':
|
---|
| 280 | value *= 1024 * 1024 * 1024;
|
---|
| 281 | break;
|
---|
| 282 | }
|
---|
| 283 | if (baselineBytes === 0) {
|
---|
| 284 | return value;
|
---|
| 285 | }
|
---|
| 286 | return baselineBytes + value * factor;
|
---|
| 287 | }
|
---|
| 288 | function* checkBudgets(budgets, webpackStats, processResults) {
|
---|
| 289 | // Ignore AnyComponentStyle budgets as these are handled in `AnyComponentStyleBudgetChecker`.
|
---|
| 290 | const computableBudgets = budgets.filter((budget) => budget.type !== schema_1.Type.AnyComponentStyle);
|
---|
| 291 | for (const budget of computableBudgets) {
|
---|
| 292 | const sizes = calculateSizes(budget, webpackStats, processResults);
|
---|
| 293 | for (const { size, label } of sizes) {
|
---|
| 294 | yield* checkThresholds(calculateThresholds(budget), size, label);
|
---|
| 295 | }
|
---|
| 296 | }
|
---|
| 297 | }
|
---|
| 298 | exports.checkBudgets = checkBudgets;
|
---|
| 299 | function* checkThresholds(thresholds, size, label) {
|
---|
| 300 | for (const threshold of thresholds) {
|
---|
| 301 | switch (threshold.type) {
|
---|
| 302 | case ThresholdType.Max: {
|
---|
| 303 | if (size <= threshold.limit) {
|
---|
| 304 | continue;
|
---|
| 305 | }
|
---|
| 306 | const sizeDifference = stats_1.formatSize(size - threshold.limit);
|
---|
| 307 | yield {
|
---|
| 308 | severity: threshold.severity,
|
---|
| 309 | message: `${label} exceeded maximum budget. Budget ${stats_1.formatSize(threshold.limit)} was not met by ${sizeDifference} with a total of ${stats_1.formatSize(size)}.`,
|
---|
| 310 | };
|
---|
| 311 | break;
|
---|
| 312 | }
|
---|
| 313 | case ThresholdType.Min: {
|
---|
| 314 | if (size >= threshold.limit) {
|
---|
| 315 | continue;
|
---|
| 316 | }
|
---|
| 317 | const sizeDifference = stats_1.formatSize(threshold.limit - size);
|
---|
| 318 | yield {
|
---|
| 319 | severity: threshold.severity,
|
---|
| 320 | message: `${label} failed to meet minimum budget. Budget ${stats_1.formatSize(threshold.limit)} was not met by ${sizeDifference} with a total of ${stats_1.formatSize(size)}.`,
|
---|
| 321 | };
|
---|
| 322 | break;
|
---|
| 323 | }
|
---|
| 324 | default: {
|
---|
| 325 | throw new Error(`Unexpected threshold type: ${ThresholdType[threshold.type]}`);
|
---|
| 326 | }
|
---|
| 327 | }
|
---|
| 328 | }
|
---|
| 329 | }
|
---|
| 330 | exports.checkThresholds = checkThresholds;
|
---|
| 331 | /** Returns the {@link ProcessBundleFile} for the given {@link DifferentialBuildType}. */
|
---|
| 332 | function getDifferentialBuildResult(processResult, buildType) {
|
---|
| 333 | switch (buildType) {
|
---|
| 334 | case DifferentialBuildType.ORIGINAL:
|
---|
| 335 | return processResult.original || null;
|
---|
| 336 | case DifferentialBuildType.DOWNLEVEL:
|
---|
| 337 | return processResult.downlevel || null;
|
---|
| 338 | }
|
---|
| 339 | }
|
---|
| 340 | /**
|
---|
| 341 | * Merges the given differential builds into a single, non-differential value.
|
---|
| 342 | *
|
---|
| 343 | * Preconditions: All the sizes should be equivalent, or else they represent
|
---|
| 344 | * differential builds.
|
---|
| 345 | */
|
---|
| 346 | function mergeDifferentialBuildSizes(buildSizes, mergeLabel) {
|
---|
| 347 | if (buildSizes.length === 0) {
|
---|
| 348 | return [];
|
---|
| 349 | }
|
---|
| 350 | // Only one size.
|
---|
| 351 | return [
|
---|
| 352 | {
|
---|
| 353 | label: mergeLabel,
|
---|
| 354 | size: buildSizes[0].size,
|
---|
| 355 | },
|
---|
| 356 | ];
|
---|
| 357 | }
|
---|
| 358 | /** Returns whether or not all items in the list are equivalent to each other. */
|
---|
| 359 | function allEquivalent(items) {
|
---|
| 360 | return new Set(items).size < 2;
|
---|
| 361 | }
|
---|
| 362 | function getBuildTypeLabels(processResults) {
|
---|
| 363 | var _a, _b, _c;
|
---|
| 364 | const fileNameSuffixRegExp = /\-(es20\d{2}|esnext)\./;
|
---|
| 365 | const originalFileName = (_b = (_a = processResults.find(({ original }) => (original === null || original === void 0 ? void 0 : original.filename) && fileNameSuffixRegExp.test(original.filename))) === null || _a === void 0 ? void 0 : _a.original) === null || _b === void 0 ? void 0 : _b.filename;
|
---|
| 366 | let originalSuffix;
|
---|
| 367 | if (originalFileName) {
|
---|
| 368 | originalSuffix = (_c = fileNameSuffixRegExp.exec(originalFileName)) === null || _c === void 0 ? void 0 : _c[1];
|
---|
| 369 | }
|
---|
| 370 | return {
|
---|
| 371 | [DifferentialBuildType.DOWNLEVEL]: 'es5',
|
---|
| 372 | [DifferentialBuildType.ORIGINAL]: originalSuffix || 'es2015',
|
---|
| 373 | };
|
---|
| 374 | }
|
---|