@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 +12 -2
- package/build/builder.d.ts +25 -0
- package/build/ciReport.d.ts +32 -0
- package/build/cli.d.ts +3 -0
- package/build/configLoader.d.ts +34 -0
- package/build/defineConfig.d.ts +12 -0
- package/build/formatUtils.d.ts +6 -0
- package/build/git.d.ts +23 -0
- package/build/github.d.ts +2 -0
- package/build/index.d.ts +3 -0
- package/build/notifyPr.d.ts +12 -0
- package/build/strings.d.ts +23 -0
- package/build/syncPrComment.d.ts +13 -0
- package/build/uploadSnapshot.d.ts +11 -0
- package/build/worker.d.ts +15 -0
- package/package.json +28 -32
- package/src/{viteBuilder.js → builder.js} +108 -37
- package/src/ciReport.js +44 -0
- package/src/cli.js +87 -104
- package/src/configLoader.js +162 -45
- package/src/defineConfig.js +4 -0
- package/src/git.js +50 -0
- package/src/github.js +4 -1
- package/src/index.js +1 -10
- package/src/notifyPr.js +81 -0
- package/src/strings.js +38 -0
- package/src/syncPrComment.js +37 -0
- package/src/types.d.ts +53 -51
- package/src/uploadSnapshot.js +50 -56
- package/src/worker.js +18 -19
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +2 -2
- package/src/browser.d.ts +0 -2
- package/src/browser.js +0 -2
- package/src/fetchSnapshot.js +0 -96
- package/src/renderMarkdownReport.js +0 -263
- package/src/renderMarkdownReport.test.js +0 -587
- package/src/sizeDiff.js +0 -199
- package/src/webpackBuilder.js +0 -267
package/README.md
CHANGED
|
@@ -19,9 +19,12 @@ bundle-size-checker [options]
|
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
21
|
|
|
22
|
-
- `--analyze`: Creates a
|
|
23
|
-
- `--
|
|
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,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
|
+
}>;
|
package/build/index.d.ts
ADDED
|
@@ -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.
|
|
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
|
-
".":
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./build/index.d.ts",
|
|
22
|
+
"default": "./src/index.js"
|
|
23
|
+
},
|
|
18
24
|
"./package.json": "./package.json",
|
|
19
|
-
"./
|
|
25
|
+
"./ciReport": {
|
|
26
|
+
"types": "./build/ciReport.d.ts",
|
|
27
|
+
"default": "./src/ciReport.js"
|
|
28
|
+
}
|
|
20
29
|
},
|
|
21
30
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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": "^
|
|
39
|
-
"rollup-plugin-visualizer": "^
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
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": "
|
|
48
|
-
"@types/micromatch": "
|
|
49
|
-
"@types/
|
|
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": "
|
|
48
|
+
"gitSha": "8b0badde3f4948db33af81129bf69b51a619aa3a",
|
|
54
49
|
"scripts": {
|
|
55
|
-
"
|
|
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,
|
|
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
|
-
* @
|
|
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
|
|
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:
|
|
78
|
-
|
|
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:
|
|
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(
|
|
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 === '/
|
|
115
|
-
return `\0virtual:
|
|
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:
|
|
124
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
224
|
+
* @param {import('vite').Rollup.RollupOutput['output']} output - The Vite output
|
|
177
225
|
* @param {string} entryName - The entry name
|
|
178
|
-
* @returns {Promise<Map<string,
|
|
226
|
+
* @returns {Promise<Map<string, SizeSnapshotEntry>>} - Map of bundle names to size information
|
|
179
227
|
*/
|
|
180
|
-
async function processBundleSizes(
|
|
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
|
|
183
|
-
|
|
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[
|
|
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(
|
|
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
|
|
201
|
-
|
|
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 =
|
|
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
|
-
* @
|
|
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
|
|
289
|
+
export async function getBundleSizes(entry, args, replacements) {
|
|
224
290
|
// Create vite configuration
|
|
225
|
-
const {
|
|
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(
|
|
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
|
-
|
|
301
|
+
const sizes = await processBundleSizes(output, entry.id);
|
|
302
|
+
|
|
303
|
+
return { sizes, treemapPath };
|
|
233
304
|
}
|
package/src/ciReport.js
ADDED
|
@@ -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
|
+
*/
|