@mui/internal-bundle-size-checker 1.0.9-canary.8 → 1.0.9-canary.80

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
 
@@ -63,6 +66,13 @@ export default defineConfig(async () => {
63
66
  importedNames: ['Button'],
64
67
  // When externals is not specified, peer dependencies will be automatically excluded
65
68
  },
69
+ // Expand a package into one entry per export from its package.json
70
+ { id: '@mui/material', expand: true },
71
+ // Expand with glob exclusions (matched against the export subpath, e.g. `styles/colors`)
72
+ {
73
+ id: '@mui/material',
74
+ expand: { exclude: ['styles/**', 'internal/*'] },
75
+ },
66
76
  // ...
67
77
  ],
68
78
  // Optional upload configuration
@@ -0,0 +1,25 @@
1
+ export type ObjectEntry = import('./types.js').ObjectEntry;
2
+ export type CommandLineArgs = import('./types.js').CommandLineArgs;
3
+ export type SizeSnapshotEntry = import('./types.js').SizeSnapshotEntry;
4
+ export type ManifestChunk = {
5
+ file: string;
6
+ name?: string;
7
+ src?: string;
8
+ css?: string[];
9
+ isEntry?: boolean;
10
+ isDynamicEntry?: boolean;
11
+ imports?: string[];
12
+ dynamicImports?: string[];
13
+ };
14
+ export type Manifest = Record<string, ManifestChunk>;
15
+ /**
16
+ * Get sizes for a vite bundle
17
+ * @param {ObjectEntry} entry - The entry configuration
18
+ * @param {CommandLineArgs} args - Command line arguments
19
+ * @param {Record<string, string>} [replacements] - String replacements to apply
20
+ * @returns {Promise<{ sizes: Map<string, SizeSnapshotEntry>, treemapPath: string }>}
21
+ */
22
+ export declare function getBundleSizes(entry: ObjectEntry, args: CommandLineArgs, replacements?: Record<string, string>): Promise<{
23
+ sizes: Map<string, SizeSnapshotEntry>;
24
+ treemapPath: string;
25
+ }>;
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod/v4';
2
+ /**
3
+ * Creates a CI report upload schema for a specific report type.
4
+ * Common fields (commitSha, repo, branch, prNumber) are shared across all report types.
5
+ * @param {string} type - The report type literal (e.g. 'size-snapshot')
6
+ * @param {number} version - The schema version number
7
+ * @param {z.ZodType} reportSchema - Zod schema for the report payload
8
+ */
9
+ export declare function ciReportUploadSchema(type: string, version: number, reportSchema: z.ZodType): z.ZodObject<{
10
+ version: z.ZodLiteral<number>;
11
+ timestamp: z.ZodNumber;
12
+ commitSha: z.ZodString;
13
+ repo: z.ZodString;
14
+ reportType: z.ZodLiteral<string>;
15
+ prNumber: z.ZodOptional<z.ZodNumber>;
16
+ branch: z.ZodString;
17
+ report: z.ZodType<any, any, z.core.$ZodTypeInternals<any, any>>;
18
+ }, z.core.$strip>;
19
+ export declare const sizeSnapshotUploadSchema: z.ZodObject<{
20
+ version: z.ZodLiteral<number>;
21
+ timestamp: z.ZodNumber;
22
+ commitSha: z.ZodString;
23
+ repo: z.ZodString;
24
+ reportType: z.ZodLiteral<string>;
25
+ prNumber: z.ZodOptional<z.ZodNumber>;
26
+ branch: z.ZodString;
27
+ report: z.ZodType<any, any, z.core.$ZodTypeInternals<any, any>>;
28
+ }, z.core.$strip>;
29
+ export type SizeSnapshotUpload = z.infer<typeof sizeSnapshotUploadSchema>;
30
+ /**
31
+ * @typedef {z.infer<typeof sizeSnapshotUploadSchema>} SizeSnapshotUpload
32
+ */
package/build/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type CommandLineArgs = import('./types.js').CommandLineArgs;
2
+ export type NormalizedBundleSizeCheckerConfig = import('./types.js').NormalizedBundleSizeCheckerConfig;
3
+ export type SizeSnapshotEntry = import('./types.js').SizeSnapshotEntry;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Utility to load the bundle-size-checker configuration
3
+ */
4
+ export type BundleSizeCheckerConfigObject = import('./types.js').BundleSizeCheckerConfigObject;
5
+ export type UploadConfig = import('./types.js').UploadConfig;
6
+ export type NormalizedUploadConfig = import('./types.js').NormalizedUploadConfig;
7
+ export type EntryPoint = import('./types.js').EntryPoint;
8
+ export type ObjectEntry = import('./types.js').ObjectEntry;
9
+ export type NormalizedBundleSizeCheckerConfig = import('./types.js').NormalizedBundleSizeCheckerConfig;
10
+ /**
11
+ * Validates and normalizes an upload configuration object
12
+ * @param {UploadConfig} uploadConfig - The upload configuration to normalize
13
+ * @param {Object} ciInfo - CI environment information
14
+ * @param {string} [ciInfo.branch] - Branch name from CI environment
15
+ * @param {boolean} [ciInfo.isPr] - Whether this is a pull request from CI environment
16
+ * @param {string} [ciInfo.prBranch] - PR branch name from CI environment
17
+ * @param {string} [ciInfo.slug] - Repository slug from CI environment
18
+ * @param {string} [ciInfo.pr] - Pull request number from CI environment
19
+ * @returns {NormalizedUploadConfig} - Normalized upload config
20
+ * @throws {Error} If required fields are missing
21
+ */
22
+ export declare function applyUploadConfigDefaults(uploadConfig: UploadConfig, ciInfo: {
23
+ branch?: string;
24
+ isPr?: boolean;
25
+ prBranch?: string;
26
+ slug?: string;
27
+ pr?: string;
28
+ }): NormalizedUploadConfig;
29
+ /**
30
+ * Attempts to load the config file from the given directory
31
+ * @param {string} rootDir - The directory to search for the config file
32
+ * @returns {Promise<NormalizedBundleSizeCheckerConfig>} A promise that resolves to the normalized config object
33
+ */
34
+ export declare function loadConfig(rootDir: string): Promise<NormalizedBundleSizeCheckerConfig>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @typedef {import('./types.js').BundleSizeCheckerConfig} BundleSizeCheckerConfig
3
+ */
4
+ export type BundleSizeCheckerConfig = import('./types.js').BundleSizeCheckerConfig;
5
+ /**
6
+ * Define a configuration for the bundle size checker.
7
+ * This is just a pass-through function for better TypeScript typing.
8
+ *
9
+ * @param {BundleSizeCheckerConfig} config - Configuration object
10
+ * @returns {BundleSizeCheckerConfig} The configuration object
11
+ */
12
+ export default function defineConfig(config: BundleSizeCheckerConfig): BundleSizeCheckerConfig;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Format utilities for consistent display of sizes and percentages
3
+ */
4
+ export declare const byteSizeFormatter: Intl.NumberFormat;
5
+ export declare const byteSizeChangeFormatter: Intl.NumberFormat;
6
+ export declare const displayPercentFormatter: Intl.NumberFormat;
package/build/git.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Gets parent commits for a given commit SHA using git CLI
3
+ * @param {string} repo - Repository name (e.g., 'mui/material-ui') - ignored for git CLI
4
+ * @param {string} commit - The commit SHA to start from
5
+ * @param {number} depth - How many commits to retrieve (including the starting commit)
6
+ * @returns {Promise<string[]>} Array of commit SHAs in chronological order (excluding the starting commit)
7
+ */
8
+ export declare function getParentCommits(repo: string, commit: string, depth?: number): Promise<string[]>;
9
+ /**
10
+ * Compares two commits and returns merge base information using git CLI
11
+ * @param {string} base - Base commit SHA
12
+ * @param {string} head - Head commit SHA
13
+ * @returns {Promise<string>} Object with merge base commit info
14
+ */
15
+ export declare function getMergeBase(base: string, head: string): Promise<string>;
16
+ /**
17
+ * Gets the current repository owner and name from git remote
18
+ * @returns {Promise<{owner: string | null, name: string | null}>}
19
+ */
20
+ export declare function getCurrentRepoInfo(): Promise<{
21
+ owner: string | null;
22
+ name: string | null;
23
+ }>;
@@ -0,0 +1,2 @@
1
+ /** @type {import('@octokit/rest').Octokit} */
2
+ export declare const octokit: import('@octokit/rest').Octokit;
@@ -0,0 +1,3 @@
1
+ import defineConfig from './defineConfig.js';
2
+ import { loadConfig } from './configLoader.js';
3
+ export { defineConfig, loadConfig };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Creates or updates a comment on a pull request with the specified content.
3
+ * Uses an HTML comment marker to identify and update existing comments.
4
+ * Searches page-by-page (newest first) and stops early when comment is found.
5
+ *
6
+ * @param {string} repo - The repository in format "owner/repo"
7
+ * @param {number} prNumber - The pull request number
8
+ * @param {string} id - Unique identifier to mark the comment for future updates
9
+ * @param {string} content - The content to post or update in the comment
10
+ * @returns {Promise<void>}
11
+ */
12
+ export declare function notifyPr(repo: string, prNumber: number, id: string, content: string): Promise<void>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Inspired by https://github.com/parshap/node-sanitize-filename
3
+ *
4
+ * Replaces characters in strings that are illegal/unsafe for filenames.
5
+ * Unsafe characters are either removed or replaced by a substitute set
6
+ * in the optional `options` object.
7
+ *
8
+ * Illegal Characters on Various Operating Systems
9
+ * / ? < > \ : * | "
10
+ * https://kb.acronis.com/content/39790
11
+ *
12
+ * Unicode Control codes
13
+ * C0 0x00-0x1f & C1 (0x80-0x9f)
14
+ * http://en.wikipedia.org/wiki/C0_and_C1_control_codes
15
+ *
16
+ * Reserved filenames on Unix-based systems (".", "..")
17
+ * Reserved filenames in Windows ("CON", "PRN", "AUX", "NUL", "COM1",
18
+ * "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
19
+ * "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", and
20
+ * "LPT9") case-insesitively and with or without filename extensions.
21
+ * @param {string} input
22
+ */
23
+ export declare function escapeFilename(input: string, replacement?: string): string;
@@ -0,0 +1,13 @@
1
+ export type SyncPrCommentResult = {
2
+ success: boolean;
3
+ skipped?: boolean;
4
+ };
5
+ /**
6
+ * @typedef {{ success: boolean, skipped?: boolean }} SyncPrCommentResult
7
+ */
8
+ /**
9
+ * Syncs a PR comment via the dashboard API.
10
+ * @param {string} repo - Repository in owner/repo format
11
+ * @returns {Promise<SyncPrCommentResult>}
12
+ */
13
+ export declare function syncPrComment(repo: string): Promise<SyncPrCommentResult>;
@@ -0,0 +1,11 @@
1
+ export type NormalizedUploadConfig = import('./types.js').NormalizedUploadConfig;
2
+ /**
3
+ * Uploads the size snapshot to S3
4
+ * @param {string} snapshotPath - The path to the size snapshot JSON file
5
+ * @param {NormalizedUploadConfig} uploadConfig - The normalized upload configuration
6
+ * @param {string} [commitSha] - Optional commit SHA (defaults to current Git HEAD)
7
+ * @returns {Promise<{key:string}>}
8
+ */
9
+ export declare function uploadSnapshot(snapshotPath: string, uploadConfig: NormalizedUploadConfig, commitSha?: string): Promise<{
10
+ key: string;
11
+ }>;
@@ -0,0 +1,15 @@
1
+ export type ObjectEntry = import('./types.js').ObjectEntry;
2
+ export type CommandLineArgs = import('./types.js').CommandLineArgs;
3
+ export type SizeSnapshotEntry = import('./types.js').SizeSnapshotEntry;
4
+ /**
5
+ * Get sizes for a bundle
6
+ * @param {{ entry: ObjectEntry, args: CommandLineArgs, index: number, total: number, replace?: Record<string, string> }} options
7
+ * @returns {Promise<Array<[string, SizeSnapshotEntry]>>}
8
+ */
9
+ export default function getSizes({ entry, args, index, total, replace }: {
10
+ entry: ObjectEntry;
11
+ args: CommandLineArgs;
12
+ index: number;
13
+ total: number;
14
+ replace?: Record<string, string>;
15
+ }): Promise<Array<[string, SizeSnapshotEntry]>>;
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@mui/internal-bundle-size-checker",
3
- "version": "1.0.9-canary.8",
3
+ "version": "1.0.9-canary.80",
4
+ "author": "MUI Team",
4
5
  "description": "Bundle size checker for MUI packages.",
6
+ "license": "MIT",
5
7
  "type": "module",
6
8
  "main": "./src/index.js",
9
+ "types": "./build/index.d.ts",
7
10
  "bin": {
8
11
  "bundle-size-checker": "./bin/bundle-size-checker.js"
9
12
  },
@@ -14,45 +17,38 @@
14
17
  },
15
18
  "sideEffects": false,
16
19
  "exports": {
17
- ".": "./src/index.js",
20
+ ".": {
21
+ "types": "./build/index.d.ts",
22
+ "default": "./src/index.js"
23
+ },
18
24
  "./package.json": "./package.json",
19
- "./browser": "./src/browser.js"
25
+ "./ciReport": {
26
+ "types": "./build/ciReport.d.ts",
27
+ "default": "./src/ciReport.js"
28
+ }
20
29
  },
21
30
  "dependencies": {
22
- "@aws-sdk/client-s3": "^3.515.0",
23
- "@aws-sdk/credential-providers": "^3.787.0",
24
- "@babel/core": "^7.27.4",
25
- "@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",
31
+ "@octokit/rest": "^22.0.1",
32
+ "chalk": "^5.6.2",
33
+ "env-ci": "^11.2.0",
34
+ "execa": "^9.6.1",
36
35
  "git-url-parse": "^16.1.0",
37
36
  "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"
37
+ "piscina": "^5.1.4",
38
+ "rollup-plugin-visualizer": "^7.0.1",
39
+ "vite": "^8.0.11",
40
+ "yargs": "^18.0.0",
41
+ "zod": "^4.4.3"
45
42
  },
46
43
  "devDependencies": {
47
- "@types/env-ci": "^3.1.4",
48
- "@types/micromatch": "^4.0.9",
49
- "@types/webpack": "^5.28.5",
50
- "@types/webpack-bundle-analyzer": "^4.7.0",
51
- "@types/yargs": "^17.0.33"
44
+ "@types/env-ci": "3.1.4",
45
+ "@types/micromatch": "4.0.10",
46
+ "@types/yargs": "17.0.35"
52
47
  },
53
- "gitSha": "0812f9aed28e33d8d0713ddfb3131825b2321867",
48
+ "gitSha": "8b0badde3f4948db33af81129bf69b51a619aa3a",
54
49
  "scripts": {
55
- "typescript": "tsc -p tsconfig.json",
56
- "test": "pnpm -w test --project @mui/internal-bundle-size-checker"
50
+ "build": "tsgo -p tsconfig.build.json",
51
+ "test": "pnpm -w test --project @mui/internal-bundle-size-checker",
52
+ "typescript": "tsgo -noEmit"
57
53
  }
58
54
  }
@@ -1,9 +1,16 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
3
- import * as zlib from 'zlib';
4
- import { promisify } from 'util';
5
- import { build, transformWithEsbuild } from 'vite';
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
+ import { build, transformWithOxc } from 'vite';
6
6
  import { visualizer } from 'rollup-plugin-visualizer';
7
+ import { escapeFilename } from './strings.js';
8
+
9
+ /**
10
+ * @typedef {import('./types.js').ObjectEntry} ObjectEntry
11
+ * @typedef {import('./types.js').CommandLineArgs} CommandLineArgs
12
+ * @typedef {import('./types.js').SizeSnapshotEntry} SizeSnapshotEntry
13
+ */
7
14
 
8
15
  const gzipAsync = promisify(zlib.gzip);
9
16
 
@@ -25,13 +32,32 @@ const rootDir = process.cwd();
25
32
  * @typedef {Record<string, ManifestChunk>} Manifest
26
33
  */
27
34
 
35
+ /**
36
+ * Creates a simple string replacement plugin
37
+ * @param {Record<string, string>} replacements - Object with string replacements
38
+ * @returns {import('vite').Plugin}
39
+ */
40
+ function createReplacePlugin(replacements) {
41
+ return {
42
+ name: 'string-replace',
43
+ transform(code) {
44
+ let transformedCode = code;
45
+ for (const [search, replace] of Object.entries(replacements)) {
46
+ transformedCode = transformedCode.replaceAll(search, replace);
47
+ }
48
+ return transformedCode !== code ? transformedCode : null;
49
+ },
50
+ };
51
+ }
52
+
28
53
  /**
29
54
  * Creates vite configuration for bundle size checking
30
55
  * @param {ObjectEntry} entry - Entry point (string or object)
31
56
  * @param {CommandLineArgs} args
32
- * @returns {Promise<{configuration: import('vite').InlineConfig, externalsArray: string[]}>}
57
+ * @param {Record<string, string>} [replacements] - String replacements to apply
58
+ * @returns {Promise<{ config:import('vite').InlineConfig, treemapPath: string }>}
33
59
  */
34
- async function createViteConfig(entry, args) {
60
+ async function createViteConfig(entry, args, replacements = {}) {
35
61
  const entryName = entry.id;
36
62
  let entryContent;
37
63
 
@@ -59,29 +85,43 @@ async function createViteConfig(entry, args) {
59
85
  const externalsArray = entry.externals || ['react', 'react-dom'];
60
86
 
61
87
  // Ensure build directory exists
62
- const outDir = path.join(rootDir, 'build', entryName);
88
+ const outDir = path.join(rootDir, 'build', escapeFilename(entryName));
63
89
  await fs.mkdir(outDir, { recursive: true });
90
+
91
+ const treemapPath = path.join(outDir, 'treemap.html');
92
+
64
93
  /**
65
94
  * @type {import('vite').InlineConfig}
66
95
  */
67
- const configuration = {
96
+ const config = {
68
97
  configFile: false,
69
98
  root: rootDir,
70
99
 
71
100
  build: {
72
101
  write: true,
73
- minify: true,
102
+ minify: args.debug ? 'esbuild' : true,
74
103
  outDir,
75
104
  emptyOutDir: true,
105
+ modulePreload: false,
76
106
  rollupOptions: {
77
- input: '/index.tsx',
78
- external: externalsArray,
107
+ input: {
108
+ ignore: '/ignore.ts',
109
+ bundle: '/entry.tsx',
110
+ },
111
+ output: {
112
+ // The output is for debugging purposes only. Remove all hashes to make it easier to compare two folders
113
+ // of build output.
114
+ entryFileNames: `assets/[name].js`,
115
+ chunkFileNames: `assets/[name].js`,
116
+ assetFileNames: `assets/[name].[ext]`,
117
+ },
118
+ external: (id) => externalsArray.some((ext) => id === ext || id.startsWith(`${ext}/`)),
79
119
  plugins: [
80
120
  ...(args.analyze
81
121
  ? [
82
122
  // File sizes are not accurate, use it only for relative comparison
83
123
  visualizer({
84
- filename: `${outDir}.html`,
124
+ filename: treemapPath,
85
125
  title: `Bundle Size Analysis: ${entryName}`,
86
126
  projectRoot: rootDir,
87
127
  open: false,
@@ -100,19 +140,25 @@ async function createViteConfig(entry, args) {
100
140
 
101
141
  esbuild: {
102
142
  legalComments: 'none',
143
+ ...(args.debug && {
144
+ minifyIdentifiers: false,
145
+ minifyWhitespace: false,
146
+ minifySyntax: true, // This enables tree-shaking and other safe optimizations
147
+ }),
103
148
  },
104
149
 
105
150
  define: {
106
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
151
+ 'process.env.NODE_ENV': JSON.stringify('production'),
107
152
  },
108
153
  logLevel: args.verbose ? 'info' : 'silent',
109
154
  // Add plugins to handle virtual entry points
110
155
  plugins: [
156
+ createReplacePlugin(replacements),
111
157
  {
112
158
  name: 'virtual-entry',
113
159
  resolveId(id) {
114
- if (id === '/index.tsx') {
115
- return `\0virtual:index.tsx`;
160
+ if (id === '/ignore.ts') {
161
+ return `\0virtual:ignore.ts`;
116
162
  }
117
163
  if (id === '/entry.tsx') {
118
164
  return `\0virtual:entry.tsx`;
@@ -120,11 +166,13 @@ async function createViteConfig(entry, args) {
120
166
  return null;
121
167
  },
122
168
  load(id) {
123
- if (id === `\0virtual:index.tsx`) {
124
- return transformWithEsbuild(`import('/entry.tsx').then(console.log)`, id);
169
+ if (id === `\0virtual:ignore.ts`) {
170
+ // ignore chunk will contain the vite preload code, we can ignore this chunk in the output
171
+ // See https://github.com/vitejs/vite/issues/18551
172
+ return transformWithOxc(`import('/entry.tsx').then(console.log)`, id);
125
173
  }
126
174
  if (id === `\0virtual:entry.tsx`) {
127
- return transformWithEsbuild(entryContent, id);
175
+ return transformWithOxc(entryContent, id);
128
176
  }
129
177
  return null;
130
178
  },
@@ -132,7 +180,7 @@ async function createViteConfig(entry, args) {
132
180
  ],
133
181
  };
134
182
 
135
- return { configuration, externalsArray };
183
+ return { config, treemapPath };
136
184
  }
137
185
 
138
186
  /**
@@ -173,32 +221,49 @@ function walkDependencyTree(chunkKey, manifest, visited = new Set()) {
173
221
 
174
222
  /**
175
223
  * Process vite output to extract bundle sizes
176
- * @param {string} outDir - The output directory
224
+ * @param {import('vite').Rollup.RollupOutput['output']} output - The Vite output
177
225
  * @param {string} entryName - The entry name
178
- * @returns {Promise<Map<string, { parsed: number, gzip: number }>>} - Map of bundle names to size information
226
+ * @returns {Promise<Map<string, SizeSnapshotEntry>>} - Map of bundle names to size information
179
227
  */
180
- async function processBundleSizes(outDir, entryName) {
228
+ async function processBundleSizes(output, entryName) {
229
+ const chunksByFileName = new Map(output.map((chunk) => [chunk.fileName, chunk]));
230
+
181
231
  // 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');
232
+ const manifestChunk = chunksByFileName.get('.vite/manifest.json');
233
+ if (manifestChunk?.type !== 'asset') {
234
+ throw new Error(`Manifest file not found in output for entry: ${entryName}`);
235
+ }
236
+
237
+ const manifestContent =
238
+ typeof manifestChunk.source === 'string'
239
+ ? manifestChunk.source
240
+ : new TextDecoder().decode(manifestChunk.source);
241
+
184
242
  /** @type {Manifest} */
185
243
  const manifest = JSON.parse(manifestContent);
186
244
 
187
245
  // Find the main entry point JS file in the manifest
188
- const mainEntry = manifest['virtual:entry.tsx'];
246
+ const mainEntry = Object.entries(manifest).find(([_, entry]) => entry.name === 'bundle');
189
247
 
190
248
  if (!mainEntry) {
191
249
  throw new Error(`No main entry found in manifest for ${entryName}`);
192
250
  }
193
251
 
194
252
  // Walk the dependency tree to get all chunks that are part of this entry
195
- const allChunks = walkDependencyTree('virtual:entry.tsx', manifest);
253
+ const allChunks = walkDependencyTree(mainEntry[0], manifest);
196
254
 
197
255
  // Process each chunk in the dependency tree in parallel
198
256
  const chunkPromises = Array.from(allChunks, async (chunkKey) => {
199
257
  const chunk = manifest[chunkKey];
200
- const filePath = path.join(outDir, chunk.file);
201
- const fileContent = await fs.readFile(filePath, 'utf8');
258
+ const outputChunk = chunksByFileName.get(chunk.file);
259
+ if (outputChunk?.type !== 'chunk') {
260
+ throw new Error(`Output chunk not found for ${chunk.file}`);
261
+ }
262
+ const fileContent = outputChunk.code;
263
+ if (chunk.name === 'preload-helper') {
264
+ // Skip the preload-helper chunk as it is not relevant for bundle size
265
+ return null;
266
+ }
202
267
 
203
268
  // Calculate sizes
204
269
  const parsed = Buffer.byteLength(fileContent);
@@ -206,28 +271,34 @@ async function processBundleSizes(outDir, entryName) {
206
271
  const gzipSize = Buffer.byteLength(gzipBuffer);
207
272
 
208
273
  // Use chunk key as the name, or fallback to entry name for main chunk
209
- const chunkName = chunkKey === 'virtual:entry.tsx' ? entryName : chunkKey;
274
+ const chunkName = chunk.name === 'bundle' ? entryName : chunk.name || chunkKey;
210
275
  return /** @type {const} */ ([chunkName, { parsed, gzip: gzipSize }]);
211
276
  });
212
277
 
213
278
  const chunkEntries = await Promise.all(chunkPromises);
214
- return new Map(chunkEntries);
279
+ return new Map(/** @type {[string, SizeSnapshotEntry][]} */ (chunkEntries.filter(Boolean)));
215
280
  }
216
281
 
217
282
  /**
218
283
  * Get sizes for a vite bundle
219
284
  * @param {ObjectEntry} entry - The entry configuration
220
285
  * @param {CommandLineArgs} args - Command line arguments
221
- * @returns {Promise<Map<string, { parsed: number, gzip: number }>>}
286
+ * @param {Record<string, string>} [replacements] - String replacements to apply
287
+ * @returns {Promise<{ sizes: Map<string, SizeSnapshotEntry>, treemapPath: string }>}
222
288
  */
223
- export async function getViteSizes(entry, args) {
289
+ export async function getBundleSizes(entry, args, replacements) {
224
290
  // Create vite configuration
225
- const { configuration } = await createViteConfig(entry, args);
226
- const outDir = path.join(rootDir, 'build', entry.id);
291
+ const { config, treemapPath } = await createViteConfig(entry, args, replacements);
227
292
 
228
293
  // Run vite build
229
- await build(configuration);
294
+ const { output } = /** @type {import('vite').Rollup.RollupOutput} */ (await build(config));
295
+ const manifestChunk = output.find((chunk) => chunk.fileName === '.vite/manifest.json');
296
+ if (!manifestChunk) {
297
+ throw new Error(`Manifest file not found in output for entry: ${entry.id}`);
298
+ }
230
299
 
231
300
  // Process the output to get bundle sizes
232
- return processBundleSizes(outDir, entry.id);
301
+ const sizes = await processBundleSizes(output, entry.id);
302
+
303
+ return { sizes, treemapPath };
233
304
  }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ /**
4
+ * Creates a CI report upload schema for a specific report type.
5
+ * Common fields (commitSha, repo, branch, prNumber) are shared across all report types.
6
+ * @param {string} type - The report type literal (e.g. 'size-snapshot')
7
+ * @param {number} version - The schema version number
8
+ * @param {z.ZodType} reportSchema - Zod schema for the report payload
9
+ */
10
+ export function ciReportUploadSchema(type, version, reportSchema) {
11
+ return z.object({
12
+ version: z.literal(version),
13
+ timestamp: z.number(),
14
+ commitSha: z.string().regex(/^[0-9a-f]{40}$/, 'Must be a 40-character hex string'),
15
+ repo: z.string().includes('/', 'Must be in owner/repo format'),
16
+ reportType: z.literal(type),
17
+ prNumber: z.number().int().positive().optional(),
18
+ branch: z.string(),
19
+ report: reportSchema,
20
+ });
21
+ }
22
+
23
+ const sizeSnapshotEntrySchema = z.object({
24
+ parsed: z.number(),
25
+ gzip: z.number(),
26
+ });
27
+
28
+ const snapshotMetadataSchema = z.object({
29
+ trackedBundles: z.array(z.string()).optional(),
30
+ });
31
+
32
+ const sizeSnapshotSchema = z
33
+ .record(z.string(), sizeSnapshotEntrySchema)
34
+ .and(z.object({ _metadata: snapshotMetadataSchema }).partial());
35
+
36
+ export const sizeSnapshotUploadSchema = ciReportUploadSchema(
37
+ 'size-snapshot',
38
+ 1,
39
+ sizeSnapshotSchema,
40
+ );
41
+
42
+ /**
43
+ * @typedef {z.infer<typeof sizeSnapshotUploadSchema>} SizeSnapshotUpload
44
+ */