UNPKG

14.2 kBJavaScriptView Raw
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 */
9Object.defineProperty(exports, "__esModule", { value: true });
10exports.checkThresholds = exports.checkBudgets = exports.calculateThresholds = exports.ThresholdSeverity = void 0;
11const path_1 = require("path");
12const schema_1 = require("../browser/schema");
13const stats_1 = require("../webpack/utils/stats");
14var ThresholdType;
15(function (ThresholdType) {
16 ThresholdType["Max"] = "maximum";
17 ThresholdType["Min"] = "minimum";
18})(ThresholdType || (ThresholdType = {}));
19var ThresholdSeverity;
20(function (ThresholdSeverity) {
21 ThresholdSeverity["Warning"] = "warning";
22 ThresholdSeverity["Error"] = "error";
23})(ThresholdSeverity = exports.ThresholdSeverity || (exports.ThresholdSeverity = {}));
24var DifferentialBuildType;
25(function (DifferentialBuildType) {
26 DifferentialBuildType["ORIGINAL"] = "original";
27 DifferentialBuildType["DOWNLEVEL"] = "downlevel";
28})(DifferentialBuildType || (DifferentialBuildType = {}));
29function* 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}
83exports.calculateThresholds = calculateThresholds;
84/**
85 * Calculates the sizes for bundles in the budget type provided.
86 */
87function 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}
112class 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 */
158class 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 */
187class 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 */
212class 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 */
224class 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 */
236class 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 */
249class 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 */
262function 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}
288function* 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}
298exports.checkBudgets = checkBudgets;
299function* 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}
330exports.checkThresholds = checkThresholds;
331/** Returns the {@link ProcessBundleFile} for the given {@link DifferentialBuildType}. */
332function 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 */
346function 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. */
359function allEquivalent(items) {
360 return new Set(items).size < 2;
361}
362function 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}