@mui/internal-bundle-size-checker 1.0.4 → 1.0.5-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -4
- package/src/cli.js +10 -272
- package/src/fetchSnapshot.js +58 -0
- package/src/index.js +2 -9
- package/src/renderMarkdownReport.js +72 -74
- package/src/renderMarkdownReport.test.js +559 -0
- package/src/types.d.ts +1 -58
- package/src/uploadSnapshot.js +15 -1
- package/src/viteBuilder.js +229 -0
- package/src/webpackBuilder.js +267 -0
- package/src/worker.js +51 -283
- package/vite.config.mts +5 -0
- package/build/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-bundle-size-checker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5-canary.1",
|
|
4
4
|
"description": "Bundle size checker for MUI packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -32,23 +32,25 @@
|
|
|
32
32
|
"execa": "^7.2.0",
|
|
33
33
|
"fast-glob": "^3.3.2",
|
|
34
34
|
"file-loader": "^6.2.0",
|
|
35
|
-
"fs-extra": "^11.2.0",
|
|
36
35
|
"micromatch": "^4.0.8",
|
|
37
36
|
"piscina": "^4.2.1",
|
|
37
|
+
"rollup-plugin-visualizer": "^6.0.1",
|
|
38
38
|
"terser-webpack-plugin": "^5.3.10",
|
|
39
|
+
"vite": "^6.3.5",
|
|
39
40
|
"webpack": "^5.90.3",
|
|
40
41
|
"webpack-bundle-analyzer": "^4.10.1",
|
|
41
42
|
"yargs": "^17.7.2"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/env-ci": "^3.1.4",
|
|
45
|
-
"@types/fs-extra": "^11.0.4",
|
|
46
46
|
"@types/micromatch": "^4.0.9",
|
|
47
47
|
"@types/webpack": "^5.28.5",
|
|
48
48
|
"@types/webpack-bundle-analyzer": "^4.7.0",
|
|
49
49
|
"@types/yargs": "^17.0.33"
|
|
50
50
|
},
|
|
51
|
+
"gitSha": "925798014aef249e6650d8fb67463a86ee675f50",
|
|
51
52
|
"scripts": {
|
|
52
|
-
"typescript": "tsc -p tsconfig.json"
|
|
53
|
+
"typescript": "tsc -p tsconfig.json",
|
|
54
|
+
"test": "pnpm -w test --project @mui/internal-bundle-size-checker"
|
|
53
55
|
}
|
|
54
56
|
}
|
package/src/cli.js
CHANGED
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
6
|
import yargs from 'yargs';
|
|
7
7
|
import Piscina from 'piscina';
|
|
8
8
|
import micromatch from 'micromatch';
|
|
9
9
|
import { loadConfig } from './configLoader.js';
|
|
10
10
|
import { uploadSnapshot } from './uploadSnapshot.js';
|
|
11
|
-
import { calculateSizeDiff } from './sizeDiff.js';
|
|
12
|
-
import { renderMarkdownReportContent, renderMarkdownReport } from './renderMarkdownReport.js';
|
|
13
|
-
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
|
|
@@ -34,7 +31,8 @@ async function getWebpackSizes(args, config) {
|
|
|
34
31
|
});
|
|
35
32
|
// Clean and recreate the build directory
|
|
36
33
|
const buildDir = path.join(rootDir, 'build');
|
|
37
|
-
await
|
|
34
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
35
|
+
await fs.mkdir(buildDir, { recursive: true });
|
|
38
36
|
|
|
39
37
|
if (
|
|
40
38
|
!config ||
|
|
@@ -92,8 +90,8 @@ async function run(argv) {
|
|
|
92
90
|
const bundleSizes = Object.fromEntries(webpackSizes.sort((a, b) => a[0].localeCompare(b[0])));
|
|
93
91
|
|
|
94
92
|
// Ensure output directory exists
|
|
95
|
-
await
|
|
96
|
-
await
|
|
93
|
+
await fs.mkdir(path.dirname(snapshotDestPath), { recursive: true });
|
|
94
|
+
await fs.writeFile(snapshotDestPath, JSON.stringify(bundleSizes, null, 2));
|
|
97
95
|
|
|
98
96
|
// eslint-disable-next-line no-console
|
|
99
97
|
console.log(`Bundle size snapshot written to ${snapshotDestPath}`);
|
|
@@ -114,215 +112,6 @@ async function run(argv) {
|
|
|
114
112
|
}
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
/**
|
|
118
|
-
* Resolves a file path that can be relative or absolute
|
|
119
|
-
* @param {string} filePath - The file path to resolve
|
|
120
|
-
* @returns {string} The resolved absolute path
|
|
121
|
-
*/
|
|
122
|
-
function resolveFilePath(filePath) {
|
|
123
|
-
if (path.isAbsolute(filePath)) {
|
|
124
|
-
return filePath;
|
|
125
|
-
}
|
|
126
|
-
return path.resolve(rootDir, filePath);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Checks if a string is a URL
|
|
131
|
-
* @param {string} str - The string to check
|
|
132
|
-
* @returns {boolean} Whether the string is a URL
|
|
133
|
-
*/
|
|
134
|
-
function isUrl(str) {
|
|
135
|
-
try {
|
|
136
|
-
// eslint-disable-next-line no-new
|
|
137
|
-
new URL(str);
|
|
138
|
-
return true;
|
|
139
|
-
} catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Loads a snapshot from a URL (http:, https:, or file: scheme)
|
|
146
|
-
* @param {string} source - The source URL
|
|
147
|
-
* @returns {Promise<SizeSnapshot>} The loaded snapshot
|
|
148
|
-
*/
|
|
149
|
-
async function loadSnapshot(source) {
|
|
150
|
-
// Check if it's a valid URL
|
|
151
|
-
if (!isUrl(source)) {
|
|
152
|
-
throw new Error(`Invalid URL: ${source}. Use file:, http:, or https: schemes.`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (source.startsWith('file:')) {
|
|
156
|
-
// Handle file: URL
|
|
157
|
-
// Remove file: prefix and handle the rest as a file path
|
|
158
|
-
// For file:///absolute/path
|
|
159
|
-
let filePath = source.substring(source.indexOf('file:') + 5);
|
|
160
|
-
|
|
161
|
-
// Remove leading slashes for absolute paths on this machine
|
|
162
|
-
while (
|
|
163
|
-
filePath.startsWith('/') &&
|
|
164
|
-
!path.isAbsolute(filePath.substring(1)) &&
|
|
165
|
-
filePath.length > 1
|
|
166
|
-
) {
|
|
167
|
-
filePath = filePath.substring(1);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Now resolve the path
|
|
171
|
-
filePath = resolveFilePath(filePath);
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
return await fse.readJSON(filePath);
|
|
175
|
-
} catch (/** @type {any} */ error) {
|
|
176
|
-
throw new Error(`Failed to read snapshot from ${filePath}: ${error.message}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// HTTP/HTTPS URL - fetch directly
|
|
181
|
-
const response = await fetch(source);
|
|
182
|
-
if (!response.ok) {
|
|
183
|
-
throw new Error(`Failed to fetch snapshot from ${source}: ${response.statusText}`);
|
|
184
|
-
}
|
|
185
|
-
const body = await response.json();
|
|
186
|
-
return body;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Handler for the diff command
|
|
191
|
-
* @param {DiffCommandArgs} argv - Command line arguments
|
|
192
|
-
*/
|
|
193
|
-
async function diffHandler(argv) {
|
|
194
|
-
const { base, head = 'file:./size-snapshot.json', output, reportUrl } = argv;
|
|
195
|
-
|
|
196
|
-
if (!base) {
|
|
197
|
-
console.error('The --base option is required');
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
// Load snapshots
|
|
203
|
-
// eslint-disable-next-line no-console
|
|
204
|
-
console.log(`Loading base snapshot from ${base}...`);
|
|
205
|
-
const baseSnapshot = await loadSnapshot(base);
|
|
206
|
-
|
|
207
|
-
// eslint-disable-next-line no-console
|
|
208
|
-
console.log(`Loading head snapshot from ${head}...`);
|
|
209
|
-
const headSnapshot = await loadSnapshot(head);
|
|
210
|
-
|
|
211
|
-
// Calculate diff
|
|
212
|
-
const comparison = calculateSizeDiff(baseSnapshot, headSnapshot);
|
|
213
|
-
|
|
214
|
-
// Output
|
|
215
|
-
if (output === 'markdown') {
|
|
216
|
-
// Generate markdown with optional report URL
|
|
217
|
-
let markdownContent = renderMarkdownReportContent(comparison);
|
|
218
|
-
|
|
219
|
-
// Add report URL if provided
|
|
220
|
-
if (reportUrl) {
|
|
221
|
-
markdownContent += `\n\n[Details of bundle changes](${reportUrl})`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// eslint-disable-next-line no-console
|
|
225
|
-
console.log(markdownContent);
|
|
226
|
-
} else {
|
|
227
|
-
// Default JSON output
|
|
228
|
-
// eslint-disable-next-line no-console
|
|
229
|
-
console.log(JSON.stringify(comparison, null, 2));
|
|
230
|
-
}
|
|
231
|
-
} catch (/** @type {any} */ error) {
|
|
232
|
-
console.error(`Error: ${error.message}`);
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Fetches GitHub PR information
|
|
239
|
-
* @param {string} owner - Repository owner
|
|
240
|
-
* @param {string} repo - Repository name
|
|
241
|
-
* @param {number} prNumber - Pull request number
|
|
242
|
-
* @returns {Promise<PrInfo>} PR information
|
|
243
|
-
*/
|
|
244
|
-
async function fetchPrInfo(owner, repo, prNumber) {
|
|
245
|
-
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`;
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
// eslint-disable-next-line no-console
|
|
249
|
-
console.log(`Fetching PR info from ${url}...`);
|
|
250
|
-
const response = await fetch(url);
|
|
251
|
-
|
|
252
|
-
if (!response.ok) {
|
|
253
|
-
throw new Error(`GitHub API request failed: ${response.statusText} (${response.status})`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return await response.json();
|
|
257
|
-
} catch (/** @type {any} */ error) {
|
|
258
|
-
console.error(`Failed to fetch PR info: ${error.message}`);
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Handler for the pr command
|
|
265
|
-
* @param {PrCommandArgs} argv - Command line arguments
|
|
266
|
-
*/
|
|
267
|
-
async function prHandler(argv) {
|
|
268
|
-
const { prNumber, circleci, output } = argv;
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
// Load the config to get the repository information
|
|
272
|
-
const config = await loadConfig(rootDir);
|
|
273
|
-
|
|
274
|
-
if (!config.upload) {
|
|
275
|
-
throw new Error(
|
|
276
|
-
'Upload is not configured. Please enable it in your bundle-size-checker config.',
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Extract owner and repo from repository config
|
|
281
|
-
const [owner, repo] = config.upload.repo.split('/');
|
|
282
|
-
|
|
283
|
-
if (!owner || !repo) {
|
|
284
|
-
throw new Error(
|
|
285
|
-
`Invalid repository format in config: ${config.upload.repo}. Expected format: "owner/repo"`,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Fetch PR information from GitHub
|
|
290
|
-
const prInfo = await fetchPrInfo(owner, repo, prNumber);
|
|
291
|
-
|
|
292
|
-
// Generate the report
|
|
293
|
-
// eslint-disable-next-line no-console
|
|
294
|
-
console.log('Generating bundle size report...');
|
|
295
|
-
const report = await renderMarkdownReport(prInfo, circleci);
|
|
296
|
-
|
|
297
|
-
// Output
|
|
298
|
-
if (output === 'markdown') {
|
|
299
|
-
// eslint-disable-next-line no-console
|
|
300
|
-
console.log(report);
|
|
301
|
-
} else {
|
|
302
|
-
// For JSON we need to load the snapshots and calculate differences
|
|
303
|
-
const baseCommit = prInfo.base.sha;
|
|
304
|
-
const prCommit = prInfo.head.sha;
|
|
305
|
-
|
|
306
|
-
// eslint-disable-next-line no-console
|
|
307
|
-
console.log(`Fetching base snapshot for commit ${baseCommit}...`);
|
|
308
|
-
// eslint-disable-next-line no-console
|
|
309
|
-
console.log(`Fetching PR snapshot for commit ${prCommit}...`);
|
|
310
|
-
|
|
311
|
-
const [baseSnapshot, prSnapshot] = await Promise.all([
|
|
312
|
-
fetchSnapshot(config.upload.repo, baseCommit).catch(() => ({})),
|
|
313
|
-
fetchSnapshot(config.upload.repo, prCommit).catch(() => ({})),
|
|
314
|
-
]);
|
|
315
|
-
|
|
316
|
-
const comparison = calculateSizeDiff(baseSnapshot, prSnapshot);
|
|
317
|
-
// eslint-disable-next-line no-console
|
|
318
|
-
console.log(JSON.stringify(comparison, null, 2));
|
|
319
|
-
}
|
|
320
|
-
} catch (/** @type {any} */ error) {
|
|
321
|
-
console.error(`Error: ${error.message}`);
|
|
322
|
-
process.exit(1);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
115
|
yargs(process.argv.slice(2))
|
|
327
116
|
// @ts-expect-error
|
|
328
117
|
.command({
|
|
@@ -345,6 +134,11 @@ yargs(process.argv.slice(2))
|
|
|
345
134
|
describe: 'Show more detailed information during compilation.',
|
|
346
135
|
type: 'boolean',
|
|
347
136
|
})
|
|
137
|
+
.option('vite', {
|
|
138
|
+
default: false,
|
|
139
|
+
describe: 'Use Vite instead of webpack for bundling.',
|
|
140
|
+
type: 'boolean',
|
|
141
|
+
})
|
|
348
142
|
.option('output', {
|
|
349
143
|
alias: 'o',
|
|
350
144
|
describe:
|
|
@@ -365,62 +159,6 @@ yargs(process.argv.slice(2))
|
|
|
365
159
|
},
|
|
366
160
|
handler: run,
|
|
367
161
|
})
|
|
368
|
-
// @ts-expect-error
|
|
369
|
-
.command({
|
|
370
|
-
command: 'diff',
|
|
371
|
-
describe: 'Compare two bundle size snapshots',
|
|
372
|
-
builder: (cmdYargs) => {
|
|
373
|
-
return cmdYargs
|
|
374
|
-
.option('base', {
|
|
375
|
-
describe: 'Base snapshot URL (file:, http:, or https: scheme)',
|
|
376
|
-
type: 'string',
|
|
377
|
-
demandOption: true,
|
|
378
|
-
})
|
|
379
|
-
.option('head', {
|
|
380
|
-
describe:
|
|
381
|
-
'Head snapshot URL (file:, http:, or https: scheme), defaults to file:./size-snapshot.json',
|
|
382
|
-
type: 'string',
|
|
383
|
-
default: 'file:./size-snapshot.json',
|
|
384
|
-
})
|
|
385
|
-
.option('output', {
|
|
386
|
-
alias: 'o',
|
|
387
|
-
describe: 'Output format (json or markdown)',
|
|
388
|
-
type: 'string',
|
|
389
|
-
choices: ['json', 'markdown'],
|
|
390
|
-
default: 'json',
|
|
391
|
-
})
|
|
392
|
-
.option('reportUrl', {
|
|
393
|
-
describe: 'URL to the detailed report (optional)',
|
|
394
|
-
type: 'string',
|
|
395
|
-
});
|
|
396
|
-
},
|
|
397
|
-
handler: diffHandler,
|
|
398
|
-
})
|
|
399
|
-
// @ts-expect-error
|
|
400
|
-
.command({
|
|
401
|
-
command: 'pr <prNumber>',
|
|
402
|
-
describe: 'Generate a bundle size report for a GitHub pull request',
|
|
403
|
-
builder: (cmdYargs) => {
|
|
404
|
-
return cmdYargs
|
|
405
|
-
.positional('prNumber', {
|
|
406
|
-
describe: 'GitHub pull request number',
|
|
407
|
-
type: 'number',
|
|
408
|
-
demandOption: true,
|
|
409
|
-
})
|
|
410
|
-
.option('output', {
|
|
411
|
-
alias: 'o',
|
|
412
|
-
describe: 'Output format (json or markdown)',
|
|
413
|
-
type: 'string',
|
|
414
|
-
choices: ['json', 'markdown'],
|
|
415
|
-
default: 'markdown', // Default to markdown for PR reports
|
|
416
|
-
})
|
|
417
|
-
.option('circleci', {
|
|
418
|
-
describe: 'CircleCI build number for the report URL (optional)',
|
|
419
|
-
type: 'string',
|
|
420
|
-
});
|
|
421
|
-
},
|
|
422
|
-
handler: prHandler,
|
|
423
|
-
})
|
|
424
162
|
.help()
|
|
425
163
|
.strict(true)
|
|
426
164
|
.version(false)
|
package/src/fetchSnapshot.js
CHANGED
|
@@ -34,3 +34,61 @@ export async function fetchSnapshot(repo, sha) {
|
|
|
34
34
|
|
|
35
35
|
throw new Error(`Failed to fetch snapshot`, { cause: lastError });
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets parent commits for a given commit SHA using GitHub API
|
|
40
|
+
* @param {string} repo - Repository name (e.g., 'mui/material-ui')
|
|
41
|
+
* @param {string} commit - The commit SHA to start from
|
|
42
|
+
* @param {number} depth - How many commits to retrieve (including the starting commit)
|
|
43
|
+
* @returns {Promise<string[]>} Array of commit SHAs in chronological order (excluding the starting commit)
|
|
44
|
+
*/
|
|
45
|
+
async function getParentCommits(repo, commit, depth = 4) {
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(
|
|
48
|
+
`https://api.github.com/repos/${repo}/commits?sha=${commit}&per_page=${depth}`,
|
|
49
|
+
);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`GitHub API request failed: ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @type {{ sha: string }[]} */
|
|
55
|
+
const commits = await response.json();
|
|
56
|
+
// Skip the first commit (which is the starting commit) and return the rest
|
|
57
|
+
return commits.slice(1).map((commitDetails) => commitDetails.sha);
|
|
58
|
+
} catch (/** @type {any} */ error) {
|
|
59
|
+
console.warn(`Failed to get parent commits for ${commit}: ${error.message}`);
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Attempts to fetch a snapshot with fallback to parent commits
|
|
66
|
+
* @param {string} repo - Repository name
|
|
67
|
+
* @param {string} commit - The commit SHA to start from
|
|
68
|
+
* @param {number} [fallbackDepth=3] - How many parent commits to try as fallback
|
|
69
|
+
* @returns {Promise<{snapshot: import('./sizeDiff').SizeSnapshot | null, actualCommit: string | null}>}
|
|
70
|
+
*/
|
|
71
|
+
export async function fetchSnapshotWithFallback(repo, commit, fallbackDepth = 3) {
|
|
72
|
+
// Try the original commit first
|
|
73
|
+
try {
|
|
74
|
+
const snapshot = await fetchSnapshot(repo, commit);
|
|
75
|
+
return { snapshot, actualCommit: commit };
|
|
76
|
+
} catch (/** @type {any} */ error) {
|
|
77
|
+
// fallthrough to parent commits if the snapshot for the original commit fails
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get parent commits and try each one
|
|
81
|
+
const parentCommits = await getParentCommits(repo, commit, fallbackDepth + 1);
|
|
82
|
+
|
|
83
|
+
for (const parentCommit of parentCommits) {
|
|
84
|
+
try {
|
|
85
|
+
// eslint-disable-next-line no-await-in-loop
|
|
86
|
+
const snapshot = await fetchSnapshot(repo, parentCommit);
|
|
87
|
+
return { snapshot, actualCommit: parentCommit };
|
|
88
|
+
} catch {
|
|
89
|
+
// fallthrough to the next parent commit if fetching fails
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { snapshot: null, actualCommit: null };
|
|
94
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import defineConfig from './defineConfig.js';
|
|
2
2
|
import { loadConfig } from './configLoader.js';
|
|
3
3
|
import { calculateSizeDiff } from './sizeDiff.js';
|
|
4
|
-
import { renderMarkdownReport
|
|
4
|
+
import { renderMarkdownReport } from './renderMarkdownReport.js';
|
|
5
5
|
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
6
6
|
|
|
7
|
-
export {
|
|
8
|
-
defineConfig,
|
|
9
|
-
loadConfig,
|
|
10
|
-
calculateSizeDiff,
|
|
11
|
-
renderMarkdownReport,
|
|
12
|
-
renderMarkdownReportContent,
|
|
13
|
-
fetchSnapshot,
|
|
14
|
-
};
|
|
7
|
+
export { defineConfig, loadConfig, calculateSizeDiff, renderMarkdownReport, fetchSnapshot };
|
|
15
8
|
|
|
16
9
|
/**
|
|
17
10
|
* @typedef {import('./sizeDiff.js').Size} Size
|
|
@@ -5,17 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { calculateSizeDiff } from './sizeDiff.js';
|
|
8
|
-
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
8
|
+
import { fetchSnapshot, fetchSnapshotWithFallback } from './fetchSnapshot.js';
|
|
9
9
|
import { displayPercentFormatter, byteSizeChangeFormatter } from './formatUtils.js';
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {'▲' | '▼'} symbol
|
|
13
|
-
* @param {KatexColor} color
|
|
14
|
-
* @returns
|
|
15
|
-
*/
|
|
16
|
-
function formatSymbol(symbol, color) {
|
|
17
|
-
return `<sup>\${\\tiny{\\color{${color}}${symbol}}}$</sup>`;
|
|
18
|
-
}
|
|
19
10
|
|
|
20
11
|
/**
|
|
21
12
|
* Generates a symbol based on the relative change value.
|
|
@@ -24,16 +15,16 @@ function formatSymbol(symbol, color) {
|
|
|
24
15
|
*/
|
|
25
16
|
function getChangeIcon(relative) {
|
|
26
17
|
if (relative === null) {
|
|
27
|
-
return
|
|
18
|
+
return '🔺';
|
|
28
19
|
}
|
|
29
20
|
if (relative === -1) {
|
|
30
|
-
return
|
|
21
|
+
return '▼';
|
|
31
22
|
}
|
|
32
23
|
if (relative < 0) {
|
|
33
|
-
return
|
|
24
|
+
return '▼';
|
|
34
25
|
}
|
|
35
26
|
if (relative > 0) {
|
|
36
|
-
return
|
|
27
|
+
return '🔺';
|
|
37
28
|
}
|
|
38
29
|
return ' ';
|
|
39
30
|
}
|
|
@@ -75,69 +66,66 @@ function generateEmphasizedChange({ id: bundle, parsed, gzip }) {
|
|
|
75
66
|
const changeParsed = formatChange(parsed.absoluteDiff, parsed.relativeDiff);
|
|
76
67
|
const changeGzip = formatChange(gzip.absoluteDiff, gzip.relativeDiff);
|
|
77
68
|
|
|
78
|
-
return `**${bundle}** **parsed
|
|
69
|
+
return `**${bundle}** **parsed:** ${changeParsed} **gzip:** ${changeGzip}`;
|
|
79
70
|
}
|
|
80
71
|
|
|
81
72
|
/**
|
|
82
73
|
* Generates a Markdown report for bundle size changes
|
|
83
74
|
* @param {ComparisonResult} comparison - Comparison result from calculateSizeDiff
|
|
84
75
|
* @param {Object} [options] - Additional options
|
|
85
|
-
* @param {
|
|
86
|
-
* @param {number} [options.
|
|
87
|
-
* @param {number} [options.gzipSizeChangeThreshold=100] - Threshold for gzipped size change by which to show the entry
|
|
76
|
+
* @param {string[]} [options.track] - Array of bundle IDs to track. If specified, totals will only include tracked bundles and all tracked bundles will be shown prominently
|
|
77
|
+
* @param {number} [options.maxDetailsLines=100] - Maximum number of bundles to show in details section
|
|
88
78
|
* @returns {string} Markdown report
|
|
89
79
|
*/
|
|
90
80
|
export function renderMarkdownReportContent(
|
|
91
81
|
comparison,
|
|
92
|
-
{
|
|
82
|
+
{ track = [], maxDetailsLines = 100 } = {},
|
|
93
83
|
) {
|
|
94
84
|
let markdownContent = '';
|
|
95
85
|
|
|
96
|
-
|
|
97
|
-
comparison.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (isSignificantChange && visibleEntries.length < visibleLimit) {
|
|
121
|
-
visibleEntries.push(entry);
|
|
122
|
-
} else {
|
|
123
|
-
hiddenEntries.push(entry);
|
|
124
|
-
}
|
|
86
|
+
if (track.length > 0) {
|
|
87
|
+
const entryMap = new Map(comparison.entries.map((entry) => [entry.id, entry]));
|
|
88
|
+
const trackedEntries = track.map((bundleId) => {
|
|
89
|
+
const trackedEntry = entryMap.get(bundleId);
|
|
90
|
+
if (!trackedEntry) {
|
|
91
|
+
throw new Error(`Tracked bundle not found in head snapshot: ${bundleId}`);
|
|
92
|
+
}
|
|
93
|
+
return trackedEntry;
|
|
94
|
+
});
|
|
95
|
+
// Show all tracked bundles directly (including unchanged ones)
|
|
96
|
+
const trackedChanges = trackedEntries.map(generateEmphasizedChange);
|
|
97
|
+
markdownContent += `${trackedChanges.join('\n')}`;
|
|
98
|
+
} else {
|
|
99
|
+
markdownContent += `**Total Size Change:** ${formatChange(
|
|
100
|
+
comparison.totals.totalParsed,
|
|
101
|
+
comparison.totals.totalParsedPercent,
|
|
102
|
+
)} - **Total Gzip Change:** ${formatChange(
|
|
103
|
+
comparison.totals.totalGzip,
|
|
104
|
+
comparison.totals.totalGzipPercent,
|
|
105
|
+
)}\n`;
|
|
106
|
+
|
|
107
|
+
markdownContent += `Files: ${comparison.fileCounts.total} total (${
|
|
108
|
+
comparison.fileCounts.added
|
|
109
|
+
} added, ${comparison.fileCounts.removed} removed, ${comparison.fileCounts.changed} changed)\n\n`;
|
|
125
110
|
}
|
|
126
111
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
112
|
+
// Show all entries in details section, not just changed ones
|
|
113
|
+
// Filter out tracked bundles to avoid duplication
|
|
114
|
+
const trackedIdSet = new Set(track);
|
|
115
|
+
const detailsEntries = comparison.entries.filter((entry) => !trackedIdSet.has(entry.id));
|
|
116
|
+
|
|
117
|
+
// Cap at maxDetailsLines bundles to avoid overly large reports
|
|
118
|
+
const cappedEntries = detailsEntries.slice(0, maxDetailsLines);
|
|
119
|
+
const hasMore = detailsEntries.length > maxDetailsLines;
|
|
120
|
+
|
|
121
|
+
if (cappedEntries.length > 0) {
|
|
122
|
+
const allChanges = cappedEntries.map(generateEmphasizedChange);
|
|
123
|
+
const bundleWord = cappedEntries.length === 1 ? 'bundle' : 'bundles';
|
|
124
|
+
const summaryText = hasMore
|
|
125
|
+
? `Show details for ${cappedEntries.length} more ${bundleWord} (${detailsEntries.length - maxDetailsLines} more not shown)`
|
|
126
|
+
: `Show details for ${cappedEntries.length} more ${bundleWord}`;
|
|
127
|
+
markdownContent += `<details>\n<summary>${summaryText}</summary>\n\n`;
|
|
128
|
+
markdownContent += `${allChanges.join('\n')}\n\n`;
|
|
141
129
|
markdownContent += `</details>`;
|
|
142
130
|
}
|
|
143
131
|
|
|
@@ -147,16 +135,19 @@ export function renderMarkdownReportContent(
|
|
|
147
135
|
/**
|
|
148
136
|
*
|
|
149
137
|
* @param {PrInfo} prInfo
|
|
150
|
-
* @param {
|
|
138
|
+
* @param {Object} [options] - Optional parameters
|
|
139
|
+
* @param {string | null} [options.circleciBuildNumber] - The CircleCI build number
|
|
140
|
+
* @param {string | null} [options.actualBaseCommit] - The actual commit SHA used for comparison (may differ from prInfo.base.sha)
|
|
151
141
|
* @returns {URL}
|
|
152
142
|
*/
|
|
153
|
-
function getDetailsUrl(prInfo,
|
|
143
|
+
function getDetailsUrl(prInfo, options = {}) {
|
|
144
|
+
const { circleciBuildNumber, actualBaseCommit } = options;
|
|
154
145
|
const detailedComparisonUrl = new URL(
|
|
155
146
|
`https://frontend-public.mui.com/size-comparison/${prInfo.base.repo.full_name}/diff`,
|
|
156
147
|
);
|
|
157
148
|
detailedComparisonUrl.searchParams.set('prNumber', String(prInfo.number));
|
|
158
149
|
detailedComparisonUrl.searchParams.set('baseRef', prInfo.base.ref);
|
|
159
|
-
detailedComparisonUrl.searchParams.set('baseCommit', prInfo.base.sha);
|
|
150
|
+
detailedComparisonUrl.searchParams.set('baseCommit', actualBaseCommit || prInfo.base.sha);
|
|
160
151
|
detailedComparisonUrl.searchParams.set('headCommit', prInfo.head.sha);
|
|
161
152
|
if (circleciBuildNumber) {
|
|
162
153
|
detailedComparisonUrl.searchParams.set('circleCIBuildNumber', circleciBuildNumber);
|
|
@@ -168,33 +159,40 @@ function getDetailsUrl(prInfo, circleciBuildNumber) {
|
|
|
168
159
|
*
|
|
169
160
|
* @param {PrInfo} prInfo
|
|
170
161
|
* @param {string} [circleciBuildNumber] - The CircleCI build number
|
|
162
|
+
* @param {Object} [options] - Additional options
|
|
163
|
+
* @param {string[]} [options.track] - Array of bundle IDs to track
|
|
164
|
+
* @param {number} [options.fallbackDepth=3] - How many parent commits to try as fallback when base snapshot is missing
|
|
165
|
+
* @param {number} [options.maxDetailsLines=100] - Maximum number of bundles to show in details section
|
|
171
166
|
* @returns {Promise<string>} Markdown report
|
|
172
167
|
*/
|
|
173
|
-
export async function renderMarkdownReport(prInfo, circleciBuildNumber) {
|
|
168
|
+
export async function renderMarkdownReport(prInfo, circleciBuildNumber, options = {}) {
|
|
174
169
|
let markdownContent = '';
|
|
175
170
|
|
|
176
171
|
const baseCommit = prInfo.base.sha;
|
|
177
172
|
const prCommit = prInfo.head.sha;
|
|
178
173
|
const repo = prInfo.base.repo.full_name;
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}),
|
|
174
|
+
const { fallbackDepth = 3 } = options;
|
|
175
|
+
|
|
176
|
+
const [baseResult, prSnapshot] = await Promise.all([
|
|
177
|
+
fetchSnapshotWithFallback(repo, baseCommit, fallbackDepth),
|
|
184
178
|
fetchSnapshot(repo, prCommit),
|
|
185
179
|
]);
|
|
186
180
|
|
|
181
|
+
const { snapshot: baseSnapshot, actualCommit: actualBaseCommit } = baseResult;
|
|
182
|
+
|
|
187
183
|
if (!baseSnapshot) {
|
|
188
|
-
markdownContent += `_:no_entry_sign: No bundle size snapshot found for base commit ${baseCommit}._\n\n`;
|
|
184
|
+
markdownContent += `_:no_entry_sign: No bundle size snapshot found for base commit ${baseCommit} or any of its ${fallbackDepth} parent commits._\n\n`;
|
|
185
|
+
} else if (actualBaseCommit !== baseCommit) {
|
|
186
|
+
markdownContent += `_:information_source: Using snapshot from parent commit ${actualBaseCommit} (fallback from ${baseCommit})._\n\n`;
|
|
189
187
|
}
|
|
190
188
|
|
|
191
189
|
const sizeDiff = calculateSizeDiff(baseSnapshot ?? {}, prSnapshot);
|
|
192
190
|
|
|
193
|
-
const report = renderMarkdownReportContent(sizeDiff);
|
|
191
|
+
const report = renderMarkdownReportContent(sizeDiff, options);
|
|
194
192
|
|
|
195
193
|
markdownContent += report;
|
|
196
194
|
|
|
197
|
-
markdownContent += `\n\n[Details of bundle changes](${getDetailsUrl(prInfo, circleciBuildNumber)})`;
|
|
195
|
+
markdownContent += `\n\n[Details of bundle changes](${getDetailsUrl(prInfo, { circleciBuildNumber, actualBaseCommit })})`;
|
|
198
196
|
|
|
199
197
|
return markdownContent;
|
|
200
198
|
}
|