1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
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 |
|
86 |
|
87 | function calculateSizes(budget, stats, processResults) {
|
88 | if (budget.type === schema_1.Type.AnyComponentStyle) {
|
89 |
|
90 |
|
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 |
|
120 | calculateChunkSize(chunk, buildType) {
|
121 |
|
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 |
|
125 | const processResultFile = getDifferentialBuildResult(processResult, buildType);
|
126 | return (processResultFile && processResultFile.size) || 0;
|
127 | }
|
128 | else {
|
129 |
|
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 |
|
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 |
|
166 |
|
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 |
|
175 |
|
176 | if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) {
|
177 | return mergeDifferentialBuildSizes(buildSizes, budgetName);
|
178 | }
|
179 | else {
|
180 | return buildSizes;
|
181 | }
|
182 | }
|
183 | }
|
184 |
|
185 |
|
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 |
|
200 |
|
201 | if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) {
|
202 | return mergeDifferentialBuildSizes(buildSizes, 'initial');
|
203 | }
|
204 | else {
|
205 | return buildSizes;
|
206 | }
|
207 | }
|
208 | }
|
209 |
|
210 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 | function mergeDifferentialBuildSizes(buildSizes, mergeLabel) {
|
347 | if (buildSizes.length === 0) {
|
348 | return [];
|
349 | }
|
350 |
|
351 | return [
|
352 | {
|
353 | label: mergeLabel,
|
354 | size: buildSizes[0].size,
|
355 | },
|
356 | ];
|
357 | }
|
358 |
|
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 | }
|