@mui/internal-bundle-size-checker 1.0.9-canary.4 → 1.0.9-canary.40

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/README.md CHANGED
@@ -19,9 +19,12 @@ bundle-size-checker [options]
19
19
 
20
20
  Options:
21
21
 
22
- - `--analyze`: Creates a webpack-bundle-analyzer report for each bundle
23
- - `--accurateBundles`: Displays used bundles accurately at the cost of more CPU cycles
22
+ - `--analyze`: Creates a report for each bundle (using rollup-plugin-visualizer)
23
+ - `--debug`: Build with readable output (no name mangling or whitespace collapse, but still tree-shake)
24
+ - `--verbose`: Show more detailed information during compilation
24
25
  - `--output`, `-o`: Path to output the size snapshot JSON file
26
+ - `--filter`, `-F`: Filter entry points by glob pattern(s) applied to their IDs
27
+ - `--concurrency`, `-c`: Number of workers to use for parallel processing
25
28
 
26
29
  ### Configuration
27
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-bundle-size-checker",
3
- "version": "1.0.9-canary.4",
3
+ "version": "1.0.9-canary.40",
4
4
  "description": "Bundle size checker for MUI packages.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -19,38 +19,25 @@
19
19
  "./browser": "./src/browser.js"
20
20
  },
21
21
  "dependencies": {
22
- "@aws-sdk/client-s3": "^3.515.0",
23
- "@aws-sdk/credential-providers": "^3.787.0",
24
- "@babel/core": "^7.27.4",
22
+ "@aws-sdk/client-s3": "^3.883.0",
23
+ "@aws-sdk/credential-providers": "^3.883.0",
25
24
  "@octokit/rest": "^22.0.0",
26
- "@babel/preset-react": "^7.18.6",
27
- "@babel/preset-typescript": "^7.27.1",
28
- "babel-loader": "^10.0.0",
29
- "chalk": "^5.4.1",
30
- "compression-webpack-plugin": "^10.0.0",
31
- "css-loader": "^7.1.2",
32
- "env-ci": "^11.1.0",
33
- "execa": "^7.2.0",
34
- "fast-glob": "^3.3.2",
35
- "file-loader": "^6.2.0",
25
+ "chalk": "^5.6.0",
26
+ "env-ci": "^11.2.0",
27
+ "execa": "^9.6.0",
36
28
  "git-url-parse": "^16.1.0",
37
29
  "micromatch": "^4.0.8",
38
- "piscina": "^4.2.1",
39
- "rollup-plugin-visualizer": "^6.0.1",
40
- "terser-webpack-plugin": "^5.3.10",
41
- "vite": "^6.3.5",
42
- "webpack": "^5.90.3",
43
- "webpack-bundle-analyzer": "^4.10.1",
44
- "yargs": "^17.7.2"
30
+ "piscina": "^5.1.3",
31
+ "rollup-plugin-visualizer": "^6.0.3",
32
+ "vite": "^7.1.4",
33
+ "yargs": "^18.0.0"
45
34
  },
46
35
  "devDependencies": {
47
36
  "@types/env-ci": "^3.1.4",
48
37
  "@types/micromatch": "^4.0.9",
49
- "@types/webpack": "^5.28.5",
50
- "@types/webpack-bundle-analyzer": "^4.7.0",
51
38
  "@types/yargs": "^17.0.33"
52
39
  },
53
- "gitSha": "e8405929964e683cd126735235fa15d68c4c33f4",
40
+ "gitSha": "f9fb2233d5f6d847f6205b345bb829a712eea199",
54
41
  "scripts": {
55
42
  "typescript": "tsc -p tsconfig.json",
56
43
  "test": "pnpm -w test --project @mui/internal-bundle-size-checker"
@@ -1,7 +1,7 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
3
- import * as zlib from 'zlib';
4
- import { promisify } from 'util';
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import * as zlib from 'node:zlib';
4
+ import { promisify } from 'node:util';
5
5
  import { build, transformWithEsbuild } from 'vite';
6
6
  import { visualizer } from 'rollup-plugin-visualizer';
7
7
 
@@ -29,7 +29,7 @@ const rootDir = process.cwd();
29
29
  * Creates vite configuration for bundle size checking
30
30
  * @param {ObjectEntry} entry - Entry point (string or object)
31
31
  * @param {CommandLineArgs} args
32
- * @returns {Promise<{configuration: import('vite').InlineConfig, externalsArray: string[]}>}
32
+ * @returns {Promise<import('vite').InlineConfig>}
33
33
  */
34
34
  async function createViteConfig(entry, args) {
35
35
  const entryName = entry.id;
@@ -70,12 +70,12 @@ async function createViteConfig(entry, args) {
70
70
 
71
71
  build: {
72
72
  write: true,
73
- minify: true,
73
+ minify: args.debug ? 'esbuild' : true,
74
74
  outDir,
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
  ? [
@@ -100,10 +100,15 @@ async function createViteConfig(entry, args) {
100
100
 
101
101
  esbuild: {
102
102
  legalComments: 'none',
103
+ ...(args.debug && {
104
+ minifyIdentifiers: false,
105
+ minifyWhitespace: false,
106
+ minifySyntax: true, // This enables tree-shaking and other safe optimizations
107
+ }),
103
108
  },
104
109
 
105
110
  define: {
106
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
111
+ 'process.env.NODE_ENV': JSON.stringify('production'),
107
112
  },
108
113
  logLevel: args.verbose ? 'info' : 'silent',
109
114
  // Add plugins to handle virtual entry points
@@ -132,7 +137,7 @@ async function createViteConfig(entry, args) {
132
137
  ],
133
138
  };
134
139
 
135
- return { configuration, externalsArray };
140
+ return configuration;
136
141
  }
137
142
 
138
143
  /**
@@ -173,61 +178,81 @@ function walkDependencyTree(chunkKey, manifest, visited = new Set()) {
173
178
 
174
179
  /**
175
180
  * Process vite output to extract bundle sizes
176
- * @param {string} outDir - The output directory
181
+ * @param {import('vite').Rollup.RollupOutput['output']} output - The Vite output
177
182
  * @param {string} entryName - The entry name
178
- * @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
179
184
  */
180
- async function processBundleSizes(outDir, entryName) {
185
+ async function processBundleSizes(output, entryName) {
186
+ const chunksByFileName = new Map(output.map((chunk) => [chunk.fileName, chunk]));
187
+
181
188
  // Read the manifest file to find the generated chunks
182
- const manifestPath = path.join(outDir, '.vite/manifest.json');
183
- const manifestContent = await fs.readFile(manifestPath, 'utf8');
189
+ const manifestChunk = chunksByFileName.get('.vite/manifest.json');
190
+ if (manifestChunk?.type !== 'asset') {
191
+ throw new Error(`Manifest file not found in output for entry: ${entryName}`);
192
+ }
193
+
194
+ const manifestContent =
195
+ typeof manifestChunk.source === 'string'
196
+ ? manifestChunk.source
197
+ : new TextDecoder().decode(manifestChunk.source);
198
+
184
199
  /** @type {Manifest} */
185
200
  const manifest = JSON.parse(manifestContent);
186
201
 
187
202
  // Find the main entry point JS file in the manifest
188
- const mainEntry = manifest['virtual:entry.tsx'];
203
+ const mainEntry = Object.entries(manifest).find(([_, entry]) => entry.name === '_virtual_entry');
189
204
 
190
205
  if (!mainEntry) {
191
206
  throw new Error(`No main entry found in manifest for ${entryName}`);
192
207
  }
193
208
 
194
209
  // Walk the dependency tree to get all chunks that are part of this entry
195
- const allChunks = walkDependencyTree('virtual:entry.tsx', manifest);
210
+ const allChunks = walkDependencyTree(mainEntry[0], manifest);
196
211
 
197
212
  // Process each chunk in the dependency tree in parallel
198
213
  const chunkPromises = Array.from(allChunks, async (chunkKey) => {
199
214
  const chunk = manifest[chunkKey];
200
- const filePath = path.join(outDir, chunk.file);
201
- const fileContent = await fs.readFile(filePath, 'utf8');
215
+ const outputChunk = chunksByFileName.get(chunk.file);
216
+ if (outputChunk?.type !== 'chunk') {
217
+ throw new Error(`Output chunk not found for ${chunk.file}`);
218
+ }
219
+ const fileContent = outputChunk.code;
202
220
 
203
221
  // Calculate sizes
204
222
  const parsed = Buffer.byteLength(fileContent);
205
223
  const gzipBuffer = await gzipAsync(fileContent, { level: zlib.constants.Z_BEST_COMPRESSION });
206
224
  const gzipSize = Buffer.byteLength(gzipBuffer);
207
225
 
226
+ if (chunk.isEntry) {
227
+ return null;
228
+ }
229
+
208
230
  // Use chunk key as the name, or fallback to entry name for main chunk
209
- const chunkName = chunkKey === 'virtual:entry.tsx' ? entryName : chunkKey;
231
+ const chunkName = chunk.name === '_virtual_entry' ? entryName : chunk.name || chunkKey;
210
232
  return /** @type {const} */ ([chunkName, { parsed, gzip: gzipSize }]);
211
233
  });
212
234
 
213
235
  const chunkEntries = await Promise.all(chunkPromises);
214
- return new Map(chunkEntries);
236
+ return new Map(/** @type {[string, SizeSnapshotEntry][]} */ (chunkEntries.filter(Boolean)));
215
237
  }
216
238
 
217
239
  /**
218
240
  * Get sizes for a vite bundle
219
241
  * @param {ObjectEntry} entry - The entry configuration
220
242
  * @param {CommandLineArgs} args - Command line arguments
221
- * @returns {Promise<Map<string, { parsed: number, gzip: number }>>}
243
+ * @returns {Promise<Map<string, SizeSnapshotEntry>>}
222
244
  */
223
- export async function getViteSizes(entry, args) {
245
+ export async function getBundleSizes(entry, args) {
224
246
  // Create vite configuration
225
- const { configuration } = await createViteConfig(entry, args);
226
- const outDir = path.join(rootDir, 'build', entry.id);
247
+ const configuration = await createViteConfig(entry, args);
227
248
 
228
249
  // Run vite build
229
- await build(configuration);
250
+ const { output } = /** @type {import('vite').Rollup.RollupOutput} */ (await build(configuration));
251
+ const manifestChunk = output.find((chunk) => chunk.fileName === '.vite/manifest.json');
252
+ if (!manifestChunk) {
253
+ throw new Error(`Manifest file not found in output for entry: ${entry.id}`);
254
+ }
230
255
 
231
256
  // Process the output to get bundle sizes
232
- return processBundleSizes(outDir, entry.id);
257
+ return processBundleSizes(output, entry.id);
233
258
  }
package/src/cli.js CHANGED
@@ -1,17 +1,33 @@
1
1
  // @ts-check
2
2
 
3
- import path from 'path';
4
- import os from 'os';
5
- import fs from 'fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ 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 { execa } from 'execa';
10
- import gitUrlParse from 'git-url-parse';
9
+ import envCi from 'env-ci';
11
10
  import { loadConfig } from './configLoader.js';
12
11
  import { uploadSnapshot } from './uploadSnapshot.js';
13
12
  import { renderMarkdownReport } from './renderMarkdownReport.js';
14
13
  import { octokit } from './github.js';
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
+ }
15
31
 
16
32
  /**
17
33
  * @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
@@ -23,32 +39,12 @@ const DEFAULT_CONCURRENCY = os.availableParallelism();
23
39
  const rootDir = process.cwd();
24
40
 
25
41
  /**
26
- * Gets the current repository owner and name from git remote
27
- * @returns {Promise<{owner: string | null, repo: string | null}>}
28
- */
29
- async function getCurrentRepoInfo() {
30
- try {
31
- const { stdout } = await execa('git', ['remote', 'get-url', 'origin']);
32
- const parsed = gitUrlParse(stdout.trim());
33
- return {
34
- owner: parsed.owner,
35
- repo: parsed.name,
36
- };
37
- } catch (error) {
38
- return {
39
- owner: null,
40
- repo: null,
41
- };
42
- }
43
- }
44
-
45
- /**
46
- * creates size snapshot for every bundle that built with webpack
42
+ * creates size snapshot for every bundle
47
43
  * @param {CommandLineArgs} args
48
44
  * @param {NormalizedBundleSizeCheckerConfig} config - The loaded configuration
49
- * @returns {Promise<Array<[string, { parsed: number, gzip: number }]>>}
45
+ * @returns {Promise<Array<[string, SizeSnapshotEntry]>>}
50
46
  */
51
- async function getWebpackSizes(args, config) {
47
+ async function getBundleSizes(args, config) {
52
48
  const worker = new Piscina({
53
49
  filename: new URL('./worker.js', import.meta.url).href,
54
50
  maxThreads: args.concurrency || DEFAULT_CONCURRENCY,
@@ -96,6 +92,51 @@ async function getWebpackSizes(args, config) {
96
92
  return sizeArrays.flat();
97
93
  }
98
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
+
99
140
  /**
100
141
  * Report command handler
101
142
  * @param {ReportCommandArgs} argv - Command line arguments
@@ -106,7 +147,7 @@ async function reportCommand(argv) {
106
147
  // Get current repo info and coerce with provided arguments
107
148
  const currentRepo = await getCurrentRepoInfo();
108
149
  const owner = argOwner ?? currentRepo.owner;
109
- const repo = argRepo ?? currentRepo.repo;
150
+ const repo = argRepo ?? currentRepo.name;
110
151
 
111
152
  if (typeof pr !== 'number') {
112
153
  throw new Error('Invalid pull request number. Please provide a valid --pr option.');
@@ -126,8 +167,18 @@ async function reportCommand(argv) {
126
167
  pull_number: pr,
127
168
  });
128
169
 
170
+ const getMergeBaseFromGithubApi = async (
171
+ /** @type {string} */ base,
172
+ /** @type {string} */ head,
173
+ ) => {
174
+ const { data } = await octokit.repos.compareCommits({ owner, repo, base, head });
175
+ return data.merge_base_commit.sha;
176
+ };
177
+
129
178
  // Generate and print the markdown report
130
- const report = await renderMarkdownReport(prInfo);
179
+ const report = await renderMarkdownReport(prInfo, {
180
+ getMergeBase: getMergeBaseFromGithubApi,
181
+ });
131
182
  // eslint-disable-next-line no-console
132
183
  console.log(report);
133
184
  }
@@ -143,15 +194,22 @@ async function run(argv) {
143
194
 
144
195
  const config = await loadConfig(rootDir);
145
196
 
197
+ // Post initial PR comment if enabled and in CI environment
198
+ if (config && config.comment) {
199
+ await postInitialPrComment();
200
+ }
201
+
146
202
  // eslint-disable-next-line no-console
147
203
  console.log(`Starting bundle size snapshot creation with ${concurrency} workers...`);
148
204
 
149
- const webpackSizes = await getWebpackSizes(argv, config);
150
- const bundleSizes = Object.fromEntries(webpackSizes.sort((a, b) => a[0].localeCompare(b[0])));
205
+ const bundleSizes = await getBundleSizes(argv, config);
206
+ const sortedBundleSizes = Object.fromEntries(
207
+ bundleSizes.sort((a, b) => a[0].localeCompare(b[0])),
208
+ );
151
209
 
152
210
  // Ensure output directory exists
153
211
  await fs.mkdir(path.dirname(snapshotDestPath), { recursive: true });
154
- await fs.writeFile(snapshotDestPath, JSON.stringify(bundleSizes, null, 2));
212
+ await fs.writeFile(snapshotDestPath, JSON.stringify(sortedBundleSizes, null, 2));
155
213
 
156
214
  // eslint-disable-next-line no-console
157
215
  console.log(`Bundle size snapshot written to ${snapshotDestPath}`);
@@ -169,6 +227,57 @@ async function run(argv) {
169
227
  // Exit with error code to indicate failure
170
228
  process.exit(1);
171
229
  }
230
+ } else {
231
+ // eslint-disable-next-line no-console
232
+ console.log('No upload configuration provided, skipping upload.');
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}`);
172
281
  }
173
282
  }
174
283
 
@@ -181,12 +290,7 @@ yargs(process.argv.slice(2))
181
290
  return cmdYargs
182
291
  .option('analyze', {
183
292
  default: false,
184
- describe: 'Creates a webpack-bundle-analyzer report for each bundle.',
185
- type: 'boolean',
186
- })
187
- .option('accurateBundles', {
188
- default: false,
189
- describe: 'Displays used bundles accurately at the cost of more CPU cycles.',
293
+ describe: 'Creates a report for each bundle.',
190
294
  type: 'boolean',
191
295
  })
192
296
  .option('verbose', {
@@ -194,9 +298,10 @@ yargs(process.argv.slice(2))
194
298
  describe: 'Show more detailed information during compilation.',
195
299
  type: 'boolean',
196
300
  })
197
- .option('vite', {
301
+ .option('debug', {
198
302
  default: false,
199
- describe: 'Use Vite instead of webpack for bundling.',
303
+ describe:
304
+ 'Build with readable output (no name mangling or whitespace collapse, but still tree-shake).',
200
305
  type: 'boolean',
201
306
  })
202
307
  .option('output', {