@mui/internal-bundle-size-checker 1.0.9-canary.45 → 1.0.9-canary.47

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.45",
3
+ "version": "1.0.9-canary.47",
4
4
  "description": "Bundle size checker for MUI packages.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -37,7 +37,7 @@
37
37
  "@types/micromatch": "^4.0.9",
38
38
  "@types/yargs": "^17.0.33"
39
39
  },
40
- "gitSha": "110101a5bd808883977d6e52af0aeb8cb9ff701c",
40
+ "gitSha": "1e49380da7c1a731162a8815a0bc3d1c528fd360",
41
41
  "scripts": {
42
42
  "typescript": "tsc -p tsconfig.json",
43
43
  "test": "pnpm -w test --project @mui/internal-bundle-size-checker"
package/src/builder.js CHANGED
@@ -4,6 +4,7 @@ import * as zlib from 'node:zlib';
4
4
  import { promisify } from 'node:util';
5
5
  import { build, transformWithEsbuild } from 'vite';
6
6
  import { visualizer } from 'rollup-plugin-visualizer';
7
+ import { escapeFilename } from './strings.js';
7
8
 
8
9
  const gzipAsync = promisify(zlib.gzip);
9
10
 
@@ -25,13 +26,32 @@ const rootDir = process.cwd();
25
26
  * @typedef {Record<string, ManifestChunk>} Manifest
26
27
  */
27
28
 
29
+ /**
30
+ * Creates a simple string replacement plugin
31
+ * @param {Record<string, string>} replacements - Object with string replacements
32
+ * @returns {import('vite').Plugin}
33
+ */
34
+ function createReplacePlugin(replacements) {
35
+ return {
36
+ name: 'string-replace',
37
+ transform(code) {
38
+ let transformedCode = code;
39
+ for (const [search, replace] of Object.entries(replacements)) {
40
+ transformedCode = transformedCode.replaceAll(search, replace);
41
+ }
42
+ return transformedCode !== code ? transformedCode : null;
43
+ },
44
+ };
45
+ }
46
+
28
47
  /**
29
48
  * Creates vite configuration for bundle size checking
30
49
  * @param {ObjectEntry} entry - Entry point (string or object)
31
50
  * @param {CommandLineArgs} args
32
- * @returns {Promise<import('vite').InlineConfig>}
51
+ * @param {Record<string, string>} [replacements] - String replacements to apply
52
+ * @returns {Promise<{ config:import('vite').InlineConfig, treemapPath: string }>}
33
53
  */
34
- async function createViteConfig(entry, args) {
54
+ async function createViteConfig(entry, args, replacements = {}) {
35
55
  const entryName = entry.id;
36
56
  let entryContent;
37
57
 
@@ -59,12 +79,15 @@ async function createViteConfig(entry, args) {
59
79
  const externalsArray = entry.externals || ['react', 'react-dom'];
60
80
 
61
81
  // Ensure build directory exists
62
- const outDir = path.join(rootDir, 'build', entryName);
82
+ const outDir = path.join(rootDir, 'build', escapeFilename(entryName));
63
83
  await fs.mkdir(outDir, { recursive: true });
84
+
85
+ const treemapPath = path.join(outDir, 'treemap.html');
86
+
64
87
  /**
65
88
  * @type {import('vite').InlineConfig}
66
89
  */
67
- const configuration = {
90
+ const config = {
68
91
  configFile: false,
69
92
  root: rootDir,
70
93
 
@@ -73,15 +96,26 @@ async function createViteConfig(entry, args) {
73
96
  minify: args.debug ? 'esbuild' : true,
74
97
  outDir,
75
98
  emptyOutDir: true,
99
+ modulePreload: false,
76
100
  rollupOptions: {
77
- input: '/index.tsx',
101
+ input: {
102
+ ignore: '/ignore.ts',
103
+ bundle: '/entry.tsx',
104
+ },
105
+ output: {
106
+ // The output is for debugging purposes only. Remove all hashes to make it easier to compare two folders
107
+ // of build output.
108
+ entryFileNames: `assets/[name].js`,
109
+ chunkFileNames: `assets/[name].js`,
110
+ assetFileNames: `assets/[name].[ext]`,
111
+ },
78
112
  external: (id) => externalsArray.some((ext) => id === ext || id.startsWith(`${ext}/`)),
79
113
  plugins: [
80
114
  ...(args.analyze
81
115
  ? [
82
116
  // File sizes are not accurate, use it only for relative comparison
83
117
  visualizer({
84
- filename: `${outDir}.html`,
118
+ filename: treemapPath,
85
119
  title: `Bundle Size Analysis: ${entryName}`,
86
120
  projectRoot: rootDir,
87
121
  open: false,
@@ -113,11 +147,12 @@ async function createViteConfig(entry, args) {
113
147
  logLevel: args.verbose ? 'info' : 'silent',
114
148
  // Add plugins to handle virtual entry points
115
149
  plugins: [
150
+ createReplacePlugin(replacements),
116
151
  {
117
152
  name: 'virtual-entry',
118
153
  resolveId(id) {
119
- if (id === '/index.tsx') {
120
- return `\0virtual:index.tsx`;
154
+ if (id === '/ignore.ts') {
155
+ return `\0virtual:ignore.ts`;
121
156
  }
122
157
  if (id === '/entry.tsx') {
123
158
  return `\0virtual:entry.tsx`;
@@ -125,7 +160,9 @@ async function createViteConfig(entry, args) {
125
160
  return null;
126
161
  },
127
162
  load(id) {
128
- if (id === `\0virtual:index.tsx`) {
163
+ if (id === `\0virtual:ignore.ts`) {
164
+ // ignore chunk will contain the vite preload code, we can ignore this chunk in the output
165
+ // See https://github.com/vitejs/vite/issues/18551
129
166
  return transformWithEsbuild(`import('/entry.tsx').then(console.log)`, id);
130
167
  }
131
168
  if (id === `\0virtual:entry.tsx`) {
@@ -137,7 +174,7 @@ async function createViteConfig(entry, args) {
137
174
  ],
138
175
  };
139
176
 
140
- return configuration;
177
+ return { config, treemapPath };
141
178
  }
142
179
 
143
180
  /**
@@ -200,7 +237,7 @@ async function processBundleSizes(output, entryName) {
200
237
  const manifest = JSON.parse(manifestContent);
201
238
 
202
239
  // Find the main entry point JS file in the manifest
203
- const mainEntry = Object.entries(manifest).find(([_, entry]) => entry.name === '_virtual_entry');
240
+ const mainEntry = Object.entries(manifest).find(([_, entry]) => entry.name === 'bundle');
204
241
 
205
242
  if (!mainEntry) {
206
243
  throw new Error(`No main entry found in manifest for ${entryName}`);
@@ -217,18 +254,18 @@ async function processBundleSizes(output, entryName) {
217
254
  throw new Error(`Output chunk not found for ${chunk.file}`);
218
255
  }
219
256
  const fileContent = outputChunk.code;
257
+ if (chunk.name === 'preload-helper') {
258
+ // Skip the preload-helper chunk as it is not relevant for bundle size
259
+ return null;
260
+ }
220
261
 
221
262
  // Calculate sizes
222
263
  const parsed = Buffer.byteLength(fileContent);
223
264
  const gzipBuffer = await gzipAsync(fileContent, { level: zlib.constants.Z_BEST_COMPRESSION });
224
265
  const gzipSize = Buffer.byteLength(gzipBuffer);
225
266
 
226
- if (chunk.isEntry) {
227
- return null;
228
- }
229
-
230
267
  // Use chunk key as the name, or fallback to entry name for main chunk
231
- const chunkName = chunk.name === '_virtual_entry' ? entryName : chunk.name || chunkKey;
268
+ const chunkName = chunk.name === 'bundle' ? entryName : chunk.name || chunkKey;
232
269
  return /** @type {const} */ ([chunkName, { parsed, gzip: gzipSize }]);
233
270
  });
234
271
 
@@ -240,19 +277,22 @@ async function processBundleSizes(output, entryName) {
240
277
  * Get sizes for a vite bundle
241
278
  * @param {ObjectEntry} entry - The entry configuration
242
279
  * @param {CommandLineArgs} args - Command line arguments
243
- * @returns {Promise<Map<string, SizeSnapshotEntry>>}
280
+ * @param {Record<string, string>} [replacements] - String replacements to apply
281
+ * @returns {Promise<{ sizes: Map<string, SizeSnapshotEntry>, treemapPath: string }>}
244
282
  */
245
- export async function getBundleSizes(entry, args) {
283
+ export async function getBundleSizes(entry, args, replacements) {
246
284
  // Create vite configuration
247
- const configuration = await createViteConfig(entry, args);
285
+ const { config, treemapPath } = await createViteConfig(entry, args, replacements);
248
286
 
249
287
  // Run vite build
250
- const { output } = /** @type {import('vite').Rollup.RollupOutput} */ (await build(configuration));
288
+ const { output } = /** @type {import('vite').Rollup.RollupOutput} */ (await build(config));
251
289
  const manifestChunk = output.find((chunk) => chunk.fileName === '.vite/manifest.json');
252
290
  if (!manifestChunk) {
253
291
  throw new Error(`Manifest file not found in output for entry: ${entry.id}`);
254
292
  }
255
293
 
256
294
  // Process the output to get bundle sizes
257
- return processBundleSizes(output, entry.id);
295
+ const sizes = await processBundleSizes(output, entry.id);
296
+
297
+ return { sizes, treemapPath };
258
298
  }
package/src/cli.js CHANGED
@@ -7,6 +7,8 @@ import yargs from 'yargs';
7
7
  import { Piscina } from 'piscina';
8
8
  import micromatch from 'micromatch';
9
9
  import envCi from 'env-ci';
10
+ import { pathToFileURL } from 'node:url';
11
+ import chalk from 'chalk';
10
12
  import { loadConfig } from './configLoader.js';
11
13
  import { uploadSnapshot } from './uploadSnapshot.js';
12
14
  import { renderMarkdownReport } from './renderMarkdownReport.js';
@@ -100,7 +102,7 @@ async function getBundleSizes(args, config) {
100
102
 
101
103
  const sizeArrays = await Promise.all(
102
104
  validEntries.map((entry, index) =>
103
- worker.run({ entry, args, index, total: validEntries.length }),
105
+ worker.run({ entry, args, index, total: validEntries.length, replace: config.replace }),
104
106
  ),
105
107
  );
106
108
 
@@ -229,7 +231,9 @@ async function run(argv) {
229
231
  await fs.writeFile(snapshotDestPath, JSON.stringify(sortedBundleSizes, null, 2));
230
232
 
231
233
  // eslint-disable-next-line no-console
232
- console.log(`Bundle size snapshot written to ${snapshotDestPath}`);
234
+ console.log(
235
+ `Bundle size snapshot written to ${chalk.underline(pathToFileURL(snapshotDestPath))}`,
236
+ );
233
237
 
234
238
  // Upload the snapshot if upload configuration is provided and not null
235
239
  if (config && config.upload) {
@@ -225,6 +225,7 @@ async function applyConfigDefaults(config, configPath) {
225
225
  entrypoints: await normalizeEntries(config.entrypoints, configPath),
226
226
  upload: null, // Default to disabled
227
227
  comment: config.comment !== undefined ? config.comment : true, // Default to enabled
228
+ replace: config.replace || {}, // String replacements, default to empty object
228
229
  };
229
230
 
230
231
  // Handle different types of upload value
package/src/strings.js ADDED
@@ -0,0 +1,38 @@
1
+ const illegalRe = /[/?<>\\:*|"]/g;
2
+ // eslint-disable-next-line no-control-regex
3
+ const controlRe = /[\x00-\x1f\x80-\x9f]/g;
4
+ const reservedRe = /^\.+$/;
5
+ const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
6
+ const windowsTrailingRe = /[. ]+$/;
7
+
8
+ /**
9
+ * Inspired by https://github.com/parshap/node-sanitize-filename
10
+ *
11
+ * Replaces characters in strings that are illegal/unsafe for filenames.
12
+ * Unsafe characters are either removed or replaced by a substitute set
13
+ * in the optional `options` object.
14
+ *
15
+ * Illegal Characters on Various Operating Systems
16
+ * / ? < > \ : * | "
17
+ * https://kb.acronis.com/content/39790
18
+ *
19
+ * Unicode Control codes
20
+ * C0 0x00-0x1f & C1 (0x80-0x9f)
21
+ * http://en.wikipedia.org/wiki/C0_and_C1_control_codes
22
+ *
23
+ * Reserved filenames on Unix-based systems (".", "..")
24
+ * Reserved filenames in Windows ("CON", "PRN", "AUX", "NUL", "COM1",
25
+ * "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
26
+ * "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", and
27
+ * "LPT9") case-insesitively and with or without filename extensions.
28
+ * @param {string} input
29
+ */
30
+ export function escapeFilename(input, replacement = '_') {
31
+ const sanitized = input
32
+ .replace(illegalRe, replacement)
33
+ .replace(controlRe, replacement)
34
+ .replace(reservedRe, replacement)
35
+ .replace(windowsReservedRe, replacement)
36
+ .replace(windowsTrailingRe, replacement);
37
+ return sanitized;
38
+ }
package/src/types.d.ts CHANGED
@@ -32,6 +32,7 @@ interface BundleSizeCheckerConfigObject {
32
32
  entrypoints: EntryPoint[];
33
33
  upload?: UploadConfig | boolean | null;
34
34
  comment?: boolean; // Whether to post PR comments (defaults to true)
35
+ replace?: Record<string, string>; // String replacements to apply during bundling
35
36
  }
36
37
 
37
38
  type BundleSizeCheckerConfig =
@@ -44,6 +45,7 @@ interface NormalizedBundleSizeCheckerConfig {
44
45
  entrypoints: ObjectEntry[];
45
46
  upload: NormalizedUploadConfig | null; // null means upload is disabled
46
47
  comment: boolean; // Whether to post PR comments
48
+ replace: Record<string, string>; // String replacements to apply during bundling
47
49
  }
48
50
 
49
51
  // Command line argument types
package/src/worker.js CHANGED
@@ -1,13 +1,10 @@
1
1
  import { pathToFileURL } from 'node:url';
2
- import path from 'node:path';
3
2
  import fs from 'node:fs/promises';
4
3
  import chalk from 'chalk';
5
4
  import * as module from 'node:module';
6
5
  import { byteSizeFormatter } from './formatUtils.js';
7
6
  import { getBundleSizes } from './builder.js';
8
7
 
9
- const require = module.createRequire(import.meta.url);
10
-
11
8
  const rootDir = process.cwd();
12
9
 
13
10
  /**
@@ -20,11 +17,14 @@ async function getPeerDependencies(packageName) {
20
17
  /** @type {string | undefined} */
21
18
  let packageJsonPath;
22
19
 
20
+ const rootDirUrl = pathToFileURL(rootDir);
21
+
23
22
  if (module.findPackageJSON) {
24
23
  // findPackageJSON was added in: v23.2.0, v22.14.0
25
- packageJsonPath = module.findPackageJSON(packageName, `${rootDir}/_.js`);
24
+ packageJsonPath = module.findPackageJSON(packageName, `${rootDirUrl}/index.mjs`);
26
25
  } else {
27
26
  // Try to resolve packageName/package.json
27
+ const require = module.createRequire(`${rootDirUrl}/index.mjs`);
28
28
  packageJsonPath = require.resolve(`${packageName}/package.json`, {
29
29
  paths: [rootDir],
30
30
  });
@@ -56,10 +56,10 @@ async function getPeerDependencies(packageName) {
56
56
 
57
57
  /**
58
58
  * Get sizes for a bundle
59
- * @param {{ entry: ObjectEntry, args: CommandLineArgs, index: number, total: number }} options
59
+ * @param {{ entry: ObjectEntry, args: CommandLineArgs, index: number, total: number, replace?: Record<string, string> }} options
60
60
  * @returns {Promise<Array<[string, SizeSnapshotEntry]>>}
61
61
  */
62
- export default async function getSizes({ entry, args, index, total }) {
62
+ export default async function getSizes({ entry, args, index, total, replace }) {
63
63
  // eslint-disable-next-line no-console -- process monitoring
64
64
  console.log(chalk.blue(`Compiling ${index + 1}/${total}: ${chalk.bold(`[${entry.id}]`)}`));
65
65
 
@@ -82,7 +82,7 @@ export default async function getSizes({ entry, args, index, total }) {
82
82
  }
83
83
 
84
84
  try {
85
- const sizeMap = await getBundleSizes(entry, args);
85
+ const { sizes: sizeMap, treemapPath } = await getBundleSizes(entry, args, replace);
86
86
 
87
87
  // Create a concise log message showing import details
88
88
  let entryDetails = '';
@@ -107,7 +107,7 @@ ${chalk.green('✓')} ${chalk.green.bold(`Completed ${index + 1}/${total}: [${en
107
107
  ${chalk.cyan('Import:')} ${entryDetails}
108
108
  ${chalk.cyan('Externals:')} ${entry.externals.join(', ')}
109
109
  ${chalk.cyan('Sizes:')} ${chalk.yellow(byteSizeFormatter.format(entrySize.parsed))} (${chalk.yellow(byteSizeFormatter.format(entrySize.gzip))} gzipped)
110
- ${args.analyze ? ` ${chalk.cyan('Analysis:')} ${chalk.underline(pathToFileURL(path.join(rootDir, 'build', `${entry.id}.html`)).href)}` : ''}
110
+ ${args.analyze ? ` ${chalk.cyan('Analysis:')} ${chalk.underline(pathToFileURL(treemapPath).href)}` : ''}
111
111
  `.trim(),
112
112
  );
113
113