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