@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 +2 -2
- package/src/builder.js +61 -21
- package/src/cli.js +6 -2
- package/src/configLoader.js +1 -0
- package/src/strings.js +38 -0
- package/src/types.d.ts +2 -0
- package/src/worker.js +8 -8
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.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": "
|
|
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
|
-
* @
|
|
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
|
|
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:
|
|
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:
|
|
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 === '/
|
|
120
|
-
return `\0virtual:
|
|
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:
|
|
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
|
|
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 === '
|
|
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 === '
|
|
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
|
-
* @
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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) {
|
package/src/configLoader.js
CHANGED
|
@@ -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, `${
|
|
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(
|
|
110
|
+
${args.analyze ? ` ${chalk.cyan('Analysis:')} ${chalk.underline(pathToFileURL(treemapPath).href)}` : ''}
|
|
111
111
|
`.trim(),
|
|
112
112
|
);
|
|
113
113
|
|