@mui/internal-bundle-size-checker 1.0.9-canary.35 → 1.0.9-canary.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-bundle-size-checker",
3
- "version": "1.0.9-canary.35",
3
+ "version": "1.0.9-canary.37",
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": "ea7dfcb14068be0dddaa7827a0d294fc5de6e986",
41
+ "gitSha": "ae12666405d40653da881852ecb4d8f90065feeb",
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
@@ -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, { parsed: number, gzip: number }>>} - Map of bundle names to size information
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, { parsed: number, gzip: number }>>}
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, { parsed: number, gzip: number }]>>}
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, undefined, {
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))
@@ -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
- console.error(`Error loading config from ${configPath}:`, error);
40
- throw error; // Re-throw to indicate failure
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
- * @returns {ObjectEntry[]} - Normalized entries with uniqueness enforced
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
- return entries.map((entry) => {
92
- if (typeof entry === 'string') {
93
- // Transform string entries into object entries
94
- const [importSrc, importName] = entry.split('#');
95
- if (importName) {
96
- // For entries like '@mui/material#Button', create an object with import and importedNames
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
- if (!entry.id) {
112
- throw new Error('Object entries must have an id property');
113
- }
136
+ entry = { ...entry };
114
137
 
115
- if (!entry.code && !entry.import) {
116
- throw new Error(`Entry "${entry.id}" must have either code or import property defined`);
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
- return entry;
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
- * @returns {NormalizedBundleSizeCheckerConfig} Configuration with defaults applied
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,6 @@
2
2
 
3
3
  import defineConfig from './defineConfig.js';
4
4
  import { loadConfig } from './configLoader.js';
5
- import { calculateSizeDiff } from './sizeDiff.js';
6
5
  import { renderMarkdownReport } from './renderMarkdownReport.js';
7
- import { fetchSnapshot } from './fetchSnapshot.js';
8
6
 
9
- export { defineConfig, loadConfig, calculateSizeDiff, renderMarkdownReport, fetchSnapshot };
10
-
11
- /**
12
- * @typedef {import('./sizeDiff.js').Size} Size
13
- * @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
14
- * @typedef {import('./sizeDiff.js').ComparisonResult} ComparisonResult
15
- */
7
+ export { defineConfig, loadConfig, renderMarkdownReport };
@@ -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 { circleciBuildNumber, actualBaseCommit } = options;
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, circleciBuildNumber, options = {}) {
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, { circleciBuildNumber, actualBaseCommit })})`;
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**&emsp;**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, undefined, {
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, undefined, {
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, undefined, {
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, undefined, {
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, undefined, {
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, undefined, {
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, undefined, {
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, undefined, {
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, undefined, {
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, { parsed: number, gzip: number }]>>}
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