@mui/internal-bundle-size-checker 1.0.9-canary.34 → 1.0.9-canary.36
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 +2 -2
- package/src/builder.js +10 -6
- package/src/cli.js +117 -2
- package/src/configLoader.js +113 -42
- package/src/github.js +1 -0
- package/src/index.js +1 -10
- package/src/notifyPr.js +81 -0
- package/src/renderMarkdownReport.js +3 -8
- package/src/renderMarkdownReport.test.js +9 -48
- package/src/sizeDiff.js +1 -5
- package/src/types.d.ts +9 -1
- package/src/worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-bundle-size-checker",
|
|
3
|
-
"version": "1.0.9-canary.
|
|
3
|
+
"version": "1.0.9-canary.36",
|
|
4
4
|
"description": "Bundle size checker for MUI packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/micromatch": "^4.0.9",
|
|
39
39
|
"@types/yargs": "^17.0.33"
|
|
40
40
|
},
|
|
41
|
-
"gitSha": "
|
|
41
|
+
"gitSha": "8414c835c34f6f7df03a518b69f1662e0aeec0a6",
|
|
42
42
|
"scripts": {
|
|
43
43
|
"typescript": "tsc -p tsconfig.json",
|
|
44
44
|
"test": "pnpm -w test --project @mui/internal-bundle-size-checker"
|
package/src/builder.js
CHANGED
|
@@ -75,7 +75,7 @@ async function createViteConfig(entry, args) {
|
|
|
75
75
|
emptyOutDir: true,
|
|
76
76
|
rollupOptions: {
|
|
77
77
|
input: '/index.tsx',
|
|
78
|
-
external: externalsArray,
|
|
78
|
+
external: (id) => externalsArray.some((ext) => id === ext || id.startsWith(`${ext}/`)),
|
|
79
79
|
plugins: [
|
|
80
80
|
...(args.analyze
|
|
81
81
|
? [
|
|
@@ -108,7 +108,7 @@ async function createViteConfig(entry, args) {
|
|
|
108
108
|
},
|
|
109
109
|
|
|
110
110
|
define: {
|
|
111
|
-
'process.env.NODE_ENV': JSON.stringify(
|
|
111
|
+
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
112
112
|
},
|
|
113
113
|
logLevel: args.verbose ? 'info' : 'silent',
|
|
114
114
|
// Add plugins to handle virtual entry points
|
|
@@ -180,7 +180,7 @@ function walkDependencyTree(chunkKey, manifest, visited = new Set()) {
|
|
|
180
180
|
* Process vite output to extract bundle sizes
|
|
181
181
|
* @param {import('vite').Rollup.RollupOutput['output']} output - The Vite output
|
|
182
182
|
* @param {string} entryName - The entry name
|
|
183
|
-
* @returns {Promise<Map<string,
|
|
183
|
+
* @returns {Promise<Map<string, SizeSnapshotEntry>>} - Map of bundle names to size information
|
|
184
184
|
*/
|
|
185
185
|
async function processBundleSizes(output, entryName) {
|
|
186
186
|
const chunksByFileName = new Map(output.map((chunk) => [chunk.fileName, chunk]));
|
|
@@ -223,20 +223,24 @@ async function processBundleSizes(output, entryName) {
|
|
|
223
223
|
const gzipBuffer = await gzipAsync(fileContent, { level: zlib.constants.Z_BEST_COMPRESSION });
|
|
224
224
|
const gzipSize = Buffer.byteLength(gzipBuffer);
|
|
225
225
|
|
|
226
|
+
if (chunk.isEntry) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
226
230
|
// Use chunk key as the name, or fallback to entry name for main chunk
|
|
227
|
-
const chunkName = chunk.name === '_virtual_entry' ? entryName : chunkKey;
|
|
231
|
+
const chunkName = chunk.name === '_virtual_entry' ? entryName : chunk.name || chunkKey;
|
|
228
232
|
return /** @type {const} */ ([chunkName, { parsed, gzip: gzipSize }]);
|
|
229
233
|
});
|
|
230
234
|
|
|
231
235
|
const chunkEntries = await Promise.all(chunkPromises);
|
|
232
|
-
return new Map(chunkEntries);
|
|
236
|
+
return new Map(/** @type {[string, SizeSnapshotEntry][]} */ (chunkEntries.filter(Boolean)));
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
/**
|
|
236
240
|
* Get sizes for a vite bundle
|
|
237
241
|
* @param {ObjectEntry} entry - The entry configuration
|
|
238
242
|
* @param {CommandLineArgs} args - Command line arguments
|
|
239
|
-
* @returns {Promise<Map<string,
|
|
243
|
+
* @returns {Promise<Map<string, SizeSnapshotEntry>>}
|
|
240
244
|
*/
|
|
241
245
|
export async function getBundleSizes(entry, args) {
|
|
242
246
|
// Create vite configuration
|
package/src/cli.js
CHANGED
|
@@ -6,11 +6,28 @@ import fs from 'node:fs/promises';
|
|
|
6
6
|
import yargs from 'yargs';
|
|
7
7
|
import { Piscina } from 'piscina';
|
|
8
8
|
import micromatch from 'micromatch';
|
|
9
|
+
import envCi from 'env-ci';
|
|
9
10
|
import { loadConfig } from './configLoader.js';
|
|
10
11
|
import { uploadSnapshot } from './uploadSnapshot.js';
|
|
11
12
|
import { renderMarkdownReport } from './renderMarkdownReport.js';
|
|
12
13
|
import { octokit } from './github.js';
|
|
13
14
|
import { getCurrentRepoInfo } from './git.js';
|
|
15
|
+
import { notifyPr } from './notifyPr.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
*/
|
|
19
|
+
function getCiInfo() {
|
|
20
|
+
const ciInfo = envCi();
|
|
21
|
+
if (!ciInfo.isCi) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
switch (ciInfo.name) {
|
|
25
|
+
case 'CircleCI':
|
|
26
|
+
return ciInfo;
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported CI environment: ${ciInfo.name}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
|
|
@@ -25,7 +42,7 @@ const rootDir = process.cwd();
|
|
|
25
42
|
* creates size snapshot for every bundle
|
|
26
43
|
* @param {CommandLineArgs} args
|
|
27
44
|
* @param {NormalizedBundleSizeCheckerConfig} config - The loaded configuration
|
|
28
|
-
* @returns {Promise<Array<[string,
|
|
45
|
+
* @returns {Promise<Array<[string, SizeSnapshotEntry]>>}
|
|
29
46
|
*/
|
|
30
47
|
async function getBundleSizes(args, config) {
|
|
31
48
|
const worker = new Piscina({
|
|
@@ -75,6 +92,51 @@ async function getBundleSizes(args, config) {
|
|
|
75
92
|
return sizeArrays.flat();
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Posts initial "in progress" PR comment with CircleCI build information
|
|
97
|
+
* @returns {Promise<void>}
|
|
98
|
+
*/
|
|
99
|
+
async function postInitialPrComment() {
|
|
100
|
+
// /** @type {envCi.CircleCiEnv} */
|
|
101
|
+
const ciInfo = getCiInfo();
|
|
102
|
+
|
|
103
|
+
if (!ciInfo || !ciInfo.isPr) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// In CI PR builds, all required info must be present
|
|
108
|
+
if (!ciInfo.slug || !ciInfo.pr) {
|
|
109
|
+
throw new Error('PR commenting enabled but repository information missing in CI PR build');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const prNumber = Number(ciInfo.pr);
|
|
113
|
+
const circleBuildNum = process.env.CIRCLE_BUILD_NUM;
|
|
114
|
+
const circleBuildUrl = process.env.CIRCLE_BUILD_URL;
|
|
115
|
+
|
|
116
|
+
if (!circleBuildNum || !circleBuildUrl) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
'PR commenting enabled but CircleCI environment variables missing in CI PR build',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.log('Posting initial PR comment...');
|
|
125
|
+
|
|
126
|
+
const initialComment = `## Bundle size report
|
|
127
|
+
|
|
128
|
+
Bundle size will be reported once [CircleCI build #${circleBuildNum}](${circleBuildUrl}) finishes.`;
|
|
129
|
+
|
|
130
|
+
await notifyPr(ciInfo.slug, prNumber, 'bundle-size-report', initialComment);
|
|
131
|
+
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
console.log(`Initial PR comment posted for PR #${prNumber}`);
|
|
134
|
+
} catch (/** @type {any} */ error) {
|
|
135
|
+
console.error('Failed to post initial PR comment:', error.message);
|
|
136
|
+
// Don't fail the build for comment failures
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
78
140
|
/**
|
|
79
141
|
* Report command handler
|
|
80
142
|
* @param {ReportCommandArgs} argv - Command line arguments
|
|
@@ -114,7 +176,7 @@ async function reportCommand(argv) {
|
|
|
114
176
|
};
|
|
115
177
|
|
|
116
178
|
// Generate and print the markdown report
|
|
117
|
-
const report = await renderMarkdownReport(prInfo,
|
|
179
|
+
const report = await renderMarkdownReport(prInfo, {
|
|
118
180
|
getMergeBase: getMergeBaseFromGithubApi,
|
|
119
181
|
});
|
|
120
182
|
// eslint-disable-next-line no-console
|
|
@@ -132,6 +194,11 @@ async function run(argv) {
|
|
|
132
194
|
|
|
133
195
|
const config = await loadConfig(rootDir);
|
|
134
196
|
|
|
197
|
+
// Post initial PR comment if enabled and in CI environment
|
|
198
|
+
if (config && config.comment) {
|
|
199
|
+
await postInitialPrComment();
|
|
200
|
+
}
|
|
201
|
+
|
|
135
202
|
// eslint-disable-next-line no-console
|
|
136
203
|
console.log(`Starting bundle size snapshot creation with ${concurrency} workers...`);
|
|
137
204
|
|
|
@@ -164,6 +231,54 @@ async function run(argv) {
|
|
|
164
231
|
// eslint-disable-next-line no-console
|
|
165
232
|
console.log('No upload configuration provided, skipping upload.');
|
|
166
233
|
}
|
|
234
|
+
|
|
235
|
+
// Post PR comment if enabled and in CI environment
|
|
236
|
+
if (config && config.comment) {
|
|
237
|
+
const ciInfo = getCiInfo();
|
|
238
|
+
|
|
239
|
+
// Skip silently if not in CI or not a PR
|
|
240
|
+
if (!ciInfo || !ciInfo.isPr) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// In CI PR builds, all required info must be present
|
|
245
|
+
if (!ciInfo.slug || !ciInfo.pr) {
|
|
246
|
+
throw new Error('PR commenting enabled but repository information missing in CI PR build');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const prNumber = Number(ciInfo.pr);
|
|
250
|
+
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.log('Generating PR comment with bundle size changes...');
|
|
253
|
+
|
|
254
|
+
// Get tracked bundles from config
|
|
255
|
+
const trackedBundles = config.entrypoints
|
|
256
|
+
.filter((entry) => entry.track === true)
|
|
257
|
+
.map((entry) => entry.id);
|
|
258
|
+
|
|
259
|
+
// Get PR info for renderMarkdownReport
|
|
260
|
+
const { data: prInfo } = await octokit.pulls.get({
|
|
261
|
+
owner: ciInfo.slug.split('/')[0],
|
|
262
|
+
repo: ciInfo.slug.split('/')[1],
|
|
263
|
+
pull_number: prNumber,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Generate markdown report
|
|
267
|
+
const report = await renderMarkdownReport(prInfo, {
|
|
268
|
+
track: trackedBundles.length > 0 ? trackedBundles : undefined,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Post or update PR comment
|
|
272
|
+
await notifyPr(
|
|
273
|
+
ciInfo.slug,
|
|
274
|
+
prNumber,
|
|
275
|
+
'bundle-size-report',
|
|
276
|
+
`## Bundle size report\n\n${report}`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// eslint-disable-next-line no-console
|
|
280
|
+
console.log(`PR comment posted/updated for PR #${prNumber}`);
|
|
281
|
+
}
|
|
167
282
|
}
|
|
168
283
|
|
|
169
284
|
yargs(process.argv.slice(2))
|
package/src/configLoader.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Utility to load the bundle-size-checker configuration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import fs from 'node:fs';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import envCi from 'env-ci';
|
|
8
|
+
import * as module from 'node:module';
|
|
9
|
+
import * as url from 'node:url';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Attempts to load and parse a single config file
|
|
@@ -14,10 +16,6 @@ import envCi from 'env-ci';
|
|
|
14
16
|
*/
|
|
15
17
|
async function loadConfigFile(configPath) {
|
|
16
18
|
try {
|
|
17
|
-
if (!fs.existsSync(configPath)) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
19
|
// Dynamic import for ESM
|
|
22
20
|
const configUrl = new URL(`file://${configPath}`);
|
|
23
21
|
const { default: config } = await import(configUrl.href);
|
|
@@ -35,9 +33,12 @@ async function loadConfigFile(configPath) {
|
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
return resolvedConfig;
|
|
38
|
-
} catch (error) {
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
} catch (/** @type {any} */ error) {
|
|
37
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw error;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -80,59 +81,128 @@ export function applyUploadConfigDefaults(uploadConfig, ciInfo) {
|
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @param {{ [s: string]: any; } | ArrayLike<any>} exportsObj
|
|
86
|
+
* @returns {string[]} Array of export paths
|
|
87
|
+
*/
|
|
88
|
+
function findExports(exportsObj) {
|
|
89
|
+
const paths = [];
|
|
90
|
+
for (const [key, value] of Object.entries(exportsObj)) {
|
|
91
|
+
if (key.startsWith('.')) {
|
|
92
|
+
paths.push(key);
|
|
93
|
+
} else {
|
|
94
|
+
paths.push(...findExports(value));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return paths;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {import("fs").PathLike | fs.FileHandle} pkgJson
|
|
102
|
+
* @returns {Promise<string[]>}
|
|
103
|
+
*/
|
|
104
|
+
async function findExportedPaths(pkgJson) {
|
|
105
|
+
const pkgContent = await fs.readFile(pkgJson, 'utf8');
|
|
106
|
+
const { exports = {} } = JSON.parse(pkgContent);
|
|
107
|
+
return findExports(exports);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Checks if the given import source is a top-level package
|
|
112
|
+
* @param {string} importSrc - The import source string
|
|
113
|
+
* @returns {boolean} - True if it's a top-level package, false otherwise
|
|
114
|
+
*/
|
|
115
|
+
function isPackageTopLevel(importSrc) {
|
|
116
|
+
const parts = importSrc.split('/');
|
|
117
|
+
return parts.length === 1 || (parts.length === 2 && parts[0].startsWith('@'));
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
/**
|
|
84
121
|
* Normalizes entries to ensure they have a consistent format and ids are unique
|
|
85
122
|
* @param {EntryPoint[]} entries - The array of entries from the config
|
|
86
|
-
* @
|
|
123
|
+
* @param {string} configPath - The path to the configuration file
|
|
124
|
+
* @returns {Promise<ObjectEntry[]>} - Normalized entries with uniqueness enforced
|
|
87
125
|
*/
|
|
88
|
-
function normalizeEntries(entries) {
|
|
126
|
+
async function normalizeEntries(entries, configPath) {
|
|
89
127
|
const usedIds = new Set();
|
|
90
128
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
entry = {
|
|
98
|
-
id: entry,
|
|
99
|
-
import: importSrc,
|
|
100
|
-
importedNames: [importName],
|
|
101
|
-
};
|
|
102
|
-
} else {
|
|
103
|
-
// For entries like '@mui/material', create an object with import only
|
|
104
|
-
entry = {
|
|
105
|
-
id: entry,
|
|
106
|
-
import: importSrc,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
129
|
+
const result = (
|
|
130
|
+
await Promise.all(
|
|
131
|
+
entries.map(async (entry) => {
|
|
132
|
+
if (typeof entry === 'string') {
|
|
133
|
+
entry = { id: entry };
|
|
134
|
+
}
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
throw new Error('Object entries must have an id property');
|
|
113
|
-
}
|
|
136
|
+
entry = { ...entry };
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
if (!entry.id) {
|
|
139
|
+
throw new Error('Object entries must have an id property');
|
|
140
|
+
}
|
|
118
141
|
|
|
142
|
+
if (!entry.code && !entry.import) {
|
|
143
|
+
// Transform string entries into object entries
|
|
144
|
+
const [importSrc, importName] = entry.id.split('#');
|
|
145
|
+
entry.import = importSrc;
|
|
146
|
+
if (importName) {
|
|
147
|
+
entry.importedNames = [importName];
|
|
148
|
+
}
|
|
149
|
+
if (isPackageTopLevel(entry.import) && !entry.importedNames) {
|
|
150
|
+
entry.track = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (entry.expand) {
|
|
155
|
+
if (!entry.import || !isPackageTopLevel(entry.import)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Entry "${entry.id}": expand can only be used with top-level package imports`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (!module.findPackageJSON) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Your Node.js version doesn't support `module.findPackageJSON`, which is required to expand entries.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const pkgJson = module.findPackageJSON(entry.import, url.pathToFileURL(configPath));
|
|
166
|
+
if (!pkgJson) {
|
|
167
|
+
throw new Error(`Can't find package.json for entry "${entry.id}".`);
|
|
168
|
+
}
|
|
169
|
+
const exportedPaths = await findExportedPaths(pkgJson);
|
|
170
|
+
|
|
171
|
+
const expandedEntries = [];
|
|
172
|
+
for (const exportPath of exportedPaths) {
|
|
173
|
+
const importSrc = entry.import + exportPath.slice(1);
|
|
174
|
+
expandedEntries.push({
|
|
175
|
+
id: importSrc,
|
|
176
|
+
import: importSrc,
|
|
177
|
+
track: isPackageTopLevel(importSrc),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return expandedEntries;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return [entry];
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
).flat();
|
|
187
|
+
|
|
188
|
+
for (const entry of result) {
|
|
119
189
|
if (usedIds.has(entry.id)) {
|
|
120
190
|
throw new Error(`Duplicate entry id found: "${entry.id}". Entry ids must be unique.`);
|
|
121
191
|
}
|
|
122
|
-
|
|
123
192
|
usedIds.add(entry.id);
|
|
193
|
+
}
|
|
124
194
|
|
|
125
|
-
|
|
126
|
-
});
|
|
195
|
+
return result;
|
|
127
196
|
}
|
|
128
197
|
|
|
129
198
|
/**
|
|
130
199
|
* Apply default values to the configuration using CI environment
|
|
131
200
|
* @param {BundleSizeCheckerConfigObject} config - The loaded configuration
|
|
132
|
-
* @
|
|
201
|
+
* @param {string} configPath - The path to the configuration file
|
|
202
|
+
* @returns {Promise<NormalizedBundleSizeCheckerConfig>} Configuration with defaults applied
|
|
133
203
|
* @throws {Error} If required fields are missing
|
|
134
204
|
*/
|
|
135
|
-
function applyConfigDefaults(config) {
|
|
205
|
+
async function applyConfigDefaults(config, configPath) {
|
|
136
206
|
// Get environment CI information
|
|
137
207
|
/** @type {{ branch?: string, isPr?: boolean, prBranch?: string, slug?: string}} */
|
|
138
208
|
const ciInfo = envCi();
|
|
@@ -148,8 +218,9 @@ function applyConfigDefaults(config) {
|
|
|
148
218
|
// Clone the config to avoid mutating the original
|
|
149
219
|
/** @type {NormalizedBundleSizeCheckerConfig} */
|
|
150
220
|
const result = {
|
|
151
|
-
entrypoints: normalizeEntries(config.entrypoints),
|
|
221
|
+
entrypoints: await normalizeEntries(config.entrypoints, configPath),
|
|
152
222
|
upload: null, // Default to disabled
|
|
223
|
+
comment: config.comment !== undefined ? config.comment : true, // Default to enabled
|
|
153
224
|
};
|
|
154
225
|
|
|
155
226
|
// Handle different types of upload value
|
|
@@ -198,7 +269,7 @@ export async function loadConfig(rootDir) {
|
|
|
198
269
|
const config = await loadConfigFile(configPath);
|
|
199
270
|
if (config) {
|
|
200
271
|
// Apply defaults and return the config
|
|
201
|
-
return applyConfigDefaults(config);
|
|
272
|
+
return applyConfigDefaults(config, configPath);
|
|
202
273
|
}
|
|
203
274
|
}
|
|
204
275
|
|
package/src/github.js
CHANGED
|
@@ -7,5 +7,6 @@ import { createActionAuth } from '@octokit/auth-action';
|
|
|
7
7
|
/** @type {import('@octokit/rest').Octokit} */
|
|
8
8
|
export const octokit = new Octokit({
|
|
9
9
|
authStrategy: process.env.GITHUB_TOKEN ? createActionAuth : undefined,
|
|
10
|
+
auth: process.env.DANGER_GITHUB_API_TOKEN,
|
|
10
11
|
userAgent: 'bundle-size-checker',
|
|
11
12
|
});
|
package/src/index.js
CHANGED
|
@@ -2,14 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
import defineConfig from './defineConfig.js';
|
|
4
4
|
import { loadConfig } from './configLoader.js';
|
|
5
|
-
import { calculateSizeDiff } from './sizeDiff.js';
|
|
6
|
-
import { renderMarkdownReport } from './renderMarkdownReport.js';
|
|
7
|
-
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
8
5
|
|
|
9
|
-
export { defineConfig, loadConfig
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {import('./sizeDiff.js').Size} Size
|
|
13
|
-
* @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
|
|
14
|
-
* @typedef {import('./sizeDiff.js').ComparisonResult} ComparisonResult
|
|
15
|
-
*/
|
|
6
|
+
export { defineConfig, loadConfig };
|
package/src/notifyPr.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { octokit } from './github.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursively searches for a comment containing the specified marker.
|
|
7
|
+
* Searches page-by-page (newest first) and stops when found or no more pages exist.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} owner - Repository owner
|
|
10
|
+
* @param {string} repoName - Repository name
|
|
11
|
+
* @param {number} prNumber - Pull request number
|
|
12
|
+
* @param {string} marker - HTML comment marker to search for
|
|
13
|
+
* @param {number} page - Current page number (default: 1)
|
|
14
|
+
*/
|
|
15
|
+
async function findCommentByMarker(owner, repoName, prNumber, marker, page = 1) {
|
|
16
|
+
const { data: comments } = await octokit.issues.listComments({
|
|
17
|
+
owner,
|
|
18
|
+
repo: repoName,
|
|
19
|
+
issue_number: prNumber,
|
|
20
|
+
sort: 'updated',
|
|
21
|
+
direction: 'desc',
|
|
22
|
+
per_page: 100,
|
|
23
|
+
page,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Base case: no comments on this page
|
|
27
|
+
if (comments.length <= 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Success case: found comment with marker
|
|
32
|
+
const foundComment = comments.find((comment) => comment.body && comment.body.includes(marker));
|
|
33
|
+
if (foundComment) {
|
|
34
|
+
return foundComment;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return findCommentByMarker(owner, repoName, prNumber, marker, page + 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates or updates a comment on a pull request with the specified content.
|
|
42
|
+
* Uses an HTML comment marker to identify and update existing comments.
|
|
43
|
+
* Searches page-by-page (newest first) and stops early when comment is found.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} repo - The repository in format "owner/repo"
|
|
46
|
+
* @param {number} prNumber - The pull request number
|
|
47
|
+
* @param {string} id - Unique identifier to mark the comment for future updates
|
|
48
|
+
* @param {string} content - The content to post or update in the comment
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
export async function notifyPr(repo, prNumber, id, content) {
|
|
52
|
+
const [owner, repoName] = repo.split('/');
|
|
53
|
+
|
|
54
|
+
if (!owner || !repoName) {
|
|
55
|
+
throw new Error(`Invalid repo format. Expected "owner/repo", got "${repo}"`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const marker = `<!-- bundle-size-checker-id: ${id} -->`;
|
|
59
|
+
const commentBody = `${marker}\n${content}`;
|
|
60
|
+
|
|
61
|
+
// Search for existing comment with our marker
|
|
62
|
+
const existingComment = await findCommentByMarker(owner, repoName, prNumber, marker);
|
|
63
|
+
|
|
64
|
+
if (existingComment) {
|
|
65
|
+
// Update existing comment
|
|
66
|
+
await octokit.issues.updateComment({
|
|
67
|
+
owner,
|
|
68
|
+
repo: repoName,
|
|
69
|
+
comment_id: existingComment.id,
|
|
70
|
+
body: commentBody,
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
// Create new comment
|
|
74
|
+
await octokit.issues.createComment({
|
|
75
|
+
owner,
|
|
76
|
+
repo: repoName,
|
|
77
|
+
issue_number: prNumber,
|
|
78
|
+
body: commentBody,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -196,12 +196,11 @@ export function renderMarkdownReportContent(
|
|
|
196
196
|
*
|
|
197
197
|
* @param {PrInfo} prInfo
|
|
198
198
|
* @param {Object} [options] - Optional parameters
|
|
199
|
-
* @param {string | null} [options.circleciBuildNumber] - The CircleCI build number
|
|
200
199
|
* @param {string | null} [options.actualBaseCommit] - The actual commit SHA used for comparison (may differ from prInfo.base.sha)
|
|
201
200
|
* @returns {URL}
|
|
202
201
|
*/
|
|
203
202
|
function getDetailsUrl(prInfo, options = {}) {
|
|
204
|
-
const {
|
|
203
|
+
const { actualBaseCommit } = options;
|
|
205
204
|
const detailedComparisonUrl = new URL(
|
|
206
205
|
`https://frontend-public.mui.com/size-comparison/${prInfo.base.repo.full_name}/diff`,
|
|
207
206
|
);
|
|
@@ -209,16 +208,12 @@ function getDetailsUrl(prInfo, options = {}) {
|
|
|
209
208
|
detailedComparisonUrl.searchParams.set('baseRef', prInfo.base.ref);
|
|
210
209
|
detailedComparisonUrl.searchParams.set('baseCommit', actualBaseCommit || prInfo.base.sha);
|
|
211
210
|
detailedComparisonUrl.searchParams.set('headCommit', prInfo.head.sha);
|
|
212
|
-
if (circleciBuildNumber) {
|
|
213
|
-
detailedComparisonUrl.searchParams.set('circleCIBuildNumber', circleciBuildNumber);
|
|
214
|
-
}
|
|
215
211
|
return detailedComparisonUrl;
|
|
216
212
|
}
|
|
217
213
|
|
|
218
214
|
/**
|
|
219
215
|
*
|
|
220
216
|
* @param {PrInfo} prInfo
|
|
221
|
-
* @param {string} [circleciBuildNumber] - The CircleCI build number
|
|
222
217
|
* @param {Object} [options] - Additional options
|
|
223
218
|
* @param {string[]} [options.track] - Array of bundle IDs to track
|
|
224
219
|
* @param {number} [options.fallbackDepth=3] - How many parent commits to try as fallback when base snapshot is missing
|
|
@@ -226,7 +221,7 @@ function getDetailsUrl(prInfo, options = {}) {
|
|
|
226
221
|
* @param {(base: string, head: string) => Promise<string>} [options.getMergeBase] - Custom function to get merge base commit
|
|
227
222
|
* @returns {Promise<string>} Markdown report
|
|
228
223
|
*/
|
|
229
|
-
export async function renderMarkdownReport(prInfo,
|
|
224
|
+
export async function renderMarkdownReport(prInfo, options = {}) {
|
|
230
225
|
let markdownContent = '';
|
|
231
226
|
|
|
232
227
|
const prCommit = prInfo.head.sha;
|
|
@@ -255,7 +250,7 @@ export async function renderMarkdownReport(prInfo, circleciBuildNumber, options
|
|
|
255
250
|
|
|
256
251
|
markdownContent += report;
|
|
257
252
|
|
|
258
|
-
markdownContent += `\n\n[Details of bundle changes](${getDetailsUrl(prInfo, {
|
|
253
|
+
markdownContent += `\n\n[Details of bundle changes](${getDetailsUrl(prInfo, { actualBaseCommit })})`;
|
|
259
254
|
|
|
260
255
|
return markdownContent;
|
|
261
256
|
}
|
|
@@ -248,39 +248,6 @@ describe('renderMarkdownReport', () => {
|
|
|
248
248
|
`);
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
it('should include CircleCI build number in details URL', async () => {
|
|
252
|
-
const baseSnapshot = {
|
|
253
|
-
'@mui/material/Button/index.js': { parsed: 15000, gzip: 4500 },
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const prSnapshot = {
|
|
257
|
-
'@mui/material/Button/index.js': { parsed: 15000, gzip: 4500 },
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
mockFetchSnapshotWithFallback.mockResolvedValueOnce({
|
|
261
|
-
snapshot: baseSnapshot,
|
|
262
|
-
actualCommit: 'abc123',
|
|
263
|
-
});
|
|
264
|
-
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
265
|
-
|
|
266
|
-
const result = await renderMarkdownReport(mockPrInfo, '12345');
|
|
267
|
-
|
|
268
|
-
expect(result).toContain('circleCIBuildNumber=12345');
|
|
269
|
-
expect(result).toMatchInlineSnapshot(`
|
|
270
|
-
"**Total Size Change:** 0B<sup>(0.00%)</sup> - **Total Gzip Change:** 0B<sup>(0.00%)</sup>
|
|
271
|
-
Files: 1 total (0 added, 0 removed, 0 changed)
|
|
272
|
-
|
|
273
|
-
<details>
|
|
274
|
-
<summary>Show details for 1 more bundle</summary>
|
|
275
|
-
|
|
276
|
-
**@mui/material/Button/index.js** **parsed:** 0B<sup>(0.00%)</sup> **gzip:** 0B<sup>(0.00%)</sup>
|
|
277
|
-
|
|
278
|
-
</details>
|
|
279
|
-
|
|
280
|
-
[Details of bundle changes](https://frontend-public.mui.com/size-comparison/mui/material-ui/diff?prNumber=42&baseRef=master&baseCommit=abc123&headCommit=def456&circleCIBuildNumber=12345)"
|
|
281
|
-
`);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
251
|
it('should handle no changes', async () => {
|
|
285
252
|
const baseSnapshot = {
|
|
286
253
|
'@mui/material/Button/index.js': { parsed: 15000, gzip: 4500 },
|
|
@@ -332,7 +299,7 @@ describe('renderMarkdownReport', () => {
|
|
|
332
299
|
});
|
|
333
300
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
334
301
|
|
|
335
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
302
|
+
const result = await renderMarkdownReport(mockPrInfo, {
|
|
336
303
|
track: ['@mui/material/Button/index.js', '@mui/material/TextField/index.js'],
|
|
337
304
|
});
|
|
338
305
|
|
|
@@ -367,7 +334,7 @@ describe('renderMarkdownReport', () => {
|
|
|
367
334
|
});
|
|
368
335
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
369
336
|
|
|
370
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
337
|
+
const result = await renderMarkdownReport(mockPrInfo, {
|
|
371
338
|
track: ['@mui/material/Button/index.js', '@mui/material/TextField/index.js'],
|
|
372
339
|
});
|
|
373
340
|
|
|
@@ -402,7 +369,7 @@ describe('renderMarkdownReport', () => {
|
|
|
402
369
|
});
|
|
403
370
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
404
371
|
|
|
405
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
372
|
+
const result = await renderMarkdownReport(mockPrInfo, {
|
|
406
373
|
track: ['@mui/material/Button/index.js'],
|
|
407
374
|
});
|
|
408
375
|
|
|
@@ -436,7 +403,7 @@ describe('renderMarkdownReport', () => {
|
|
|
436
403
|
});
|
|
437
404
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
438
405
|
|
|
439
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
406
|
+
const result = await renderMarkdownReport(mockPrInfo, {
|
|
440
407
|
track: ['@mui/material/Button/index.js', '@mui/material/TextField/index.js'],
|
|
441
408
|
});
|
|
442
409
|
|
|
@@ -469,7 +436,7 @@ describe('renderMarkdownReport', () => {
|
|
|
469
436
|
});
|
|
470
437
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
471
438
|
|
|
472
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
439
|
+
const result = await renderMarkdownReport(mockPrInfo, {
|
|
473
440
|
track: ['@mui/material/Button/index.js'],
|
|
474
441
|
});
|
|
475
442
|
|
|
@@ -500,7 +467,7 @@ describe('renderMarkdownReport', () => {
|
|
|
500
467
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
501
468
|
|
|
502
469
|
await expect(
|
|
503
|
-
renderMarkdownReport(mockPrInfo,
|
|
470
|
+
renderMarkdownReport(mockPrInfo, {
|
|
504
471
|
track: ['@mui/material/Button/index.js', '@mui/material/NonExistent/index.js'],
|
|
505
472
|
}),
|
|
506
473
|
).rejects.toThrow(
|
|
@@ -561,9 +528,7 @@ describe('renderMarkdownReport', () => {
|
|
|
561
528
|
});
|
|
562
529
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
563
530
|
|
|
564
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
565
|
-
fallbackDepth: 1,
|
|
566
|
-
});
|
|
531
|
+
const result = await renderMarkdownReport(mockPrInfo, { fallbackDepth: 1 });
|
|
567
532
|
|
|
568
533
|
expect(result).toContain(
|
|
569
534
|
'Using snapshot from parent commit parent1 (fallback from merge base abc123)',
|
|
@@ -609,9 +574,7 @@ describe('renderMarkdownReport', () => {
|
|
|
609
574
|
});
|
|
610
575
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
611
576
|
|
|
612
|
-
const result = await renderMarkdownReport(mockPrInfo,
|
|
613
|
-
getMergeBase: customGetMergeBase,
|
|
614
|
-
});
|
|
577
|
+
const result = await renderMarkdownReport(mockPrInfo, { getMergeBase: customGetMergeBase });
|
|
615
578
|
|
|
616
579
|
// Verify that custom getMergeBase was called instead of default
|
|
617
580
|
expect(customGetMergeBase).toHaveBeenCalledWith('abc123', 'def456');
|
|
@@ -631,9 +594,7 @@ describe('renderMarkdownReport', () => {
|
|
|
631
594
|
mockFetchSnapshot.mockResolvedValueOnce(prSnapshot);
|
|
632
595
|
|
|
633
596
|
await expect(
|
|
634
|
-
renderMarkdownReport(mockPrInfo,
|
|
635
|
-
getMergeBase: customGetMergeBase,
|
|
636
|
-
}),
|
|
597
|
+
renderMarkdownReport(mockPrInfo, { getMergeBase: customGetMergeBase }),
|
|
637
598
|
).rejects.toThrow('Custom merge base error');
|
|
638
599
|
|
|
639
600
|
// Verify that custom getMergeBase was called
|
package/src/sizeDiff.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @description Represents a single bundle size entry
|
|
3
|
-
* @typedef {Object} SizeSnapshotEntry
|
|
4
|
-
* @property {number} parsed
|
|
5
|
-
* @property {number} gzip
|
|
6
|
-
*
|
|
7
2
|
* @description Represents a single bundle size snapshot
|
|
8
3
|
* @typedef {Object.<string, SizeSnapshotEntry>} SizeSnapshot
|
|
9
4
|
*
|
|
@@ -36,6 +31,7 @@
|
|
|
36
31
|
* @property {number} fileCounts.total - Total number of files
|
|
37
32
|
*/
|
|
38
33
|
|
|
34
|
+
/** @type {SizeSnapshotEntry} */
|
|
39
35
|
const nullSnapshot = { parsed: 0, gzip: 0 };
|
|
40
36
|
|
|
41
37
|
/**
|
package/src/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ interface ObjectEntry {
|
|
|
21
21
|
import?: string; // Optional package name to import
|
|
22
22
|
importedNames?: string[]; // Optional array of named imports
|
|
23
23
|
externals?: string[]; // Optional array of packages to exclude from the bundle
|
|
24
|
+
track?: boolean; // Whether this bundle should be tracked in PR comments (defaults to false)
|
|
25
|
+
expand?: boolean; // Whether to expand the entry to include all exports
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
type EntryPoint = StringEntry | ObjectEntry;
|
|
@@ -29,6 +31,7 @@ type EntryPoint = StringEntry | ObjectEntry;
|
|
|
29
31
|
interface BundleSizeCheckerConfigObject {
|
|
30
32
|
entrypoints: EntryPoint[];
|
|
31
33
|
upload?: UploadConfig | boolean | null;
|
|
34
|
+
comment?: boolean; // Whether to post PR comments (defaults to true)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
type BundleSizeCheckerConfig =
|
|
@@ -40,6 +43,7 @@ type BundleSizeCheckerConfig =
|
|
|
40
43
|
interface NormalizedBundleSizeCheckerConfig {
|
|
41
44
|
entrypoints: ObjectEntry[];
|
|
42
45
|
upload: NormalizedUploadConfig | null; // null means upload is disabled
|
|
46
|
+
comment: boolean; // Whether to post PR comments
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
// Command line argument types
|
|
@@ -70,7 +74,6 @@ interface DiffCommandArgs {
|
|
|
70
74
|
interface PrCommandArgs {
|
|
71
75
|
prNumber: number;
|
|
72
76
|
output?: 'json' | 'markdown';
|
|
73
|
-
circleci?: string;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
interface PrInfo {
|
|
@@ -87,3 +90,8 @@ interface PrInfo {
|
|
|
87
90
|
sha: string;
|
|
88
91
|
};
|
|
89
92
|
}
|
|
93
|
+
|
|
94
|
+
interface SizeSnapshotEntry {
|
|
95
|
+
parsed: number;
|
|
96
|
+
gzip: number;
|
|
97
|
+
}
|
package/src/worker.js
CHANGED
|
@@ -57,7 +57,7 @@ async function getPeerDependencies(packageName) {
|
|
|
57
57
|
/**
|
|
58
58
|
* Get sizes for a bundle
|
|
59
59
|
* @param {{ entry: ObjectEntry, args: CommandLineArgs, index: number, total: number }} options
|
|
60
|
-
* @returns {Promise<Array<[string,
|
|
60
|
+
* @returns {Promise<Array<[string, SizeSnapshotEntry]>>}
|
|
61
61
|
*/
|
|
62
62
|
export default async function getSizes({ entry, args, index, total }) {
|
|
63
63
|
// eslint-disable-next-line no-console -- process monitoring
|