@mui/internal-bundle-size-checker 1.0.9-canary.3 → 1.0.9-canary.31
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 +5 -2
- package/package.json +14 -24
- package/src/{viteBuilder.js → builder.js} +42 -21
- package/src/cli.js +118 -54
- package/src/configLoader.js +5 -2
- package/src/fetchSnapshot.js +4 -60
- package/src/fetchSnapshotWithFallback.js +34 -0
- package/src/git.js +45 -0
- package/src/github.js +11 -0
- package/src/index.js +2 -0
- package/src/renderMarkdownReport.js +30 -9
- package/src/renderMarkdownReport.test.js +54 -16
- package/src/types.d.ts +7 -22
- package/src/uploadSnapshot.js +2 -2
- package/src/worker.js +6 -13
- package/tsconfig.json +2 -2
- 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
|
|
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.31",
|
|
4
4
|
"description": "Bundle size checker for MUI packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -19,36 +19,26 @@
|
|
|
19
19
|
"./browser": "./src/browser.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@aws-sdk/client-s3": "^3.
|
|
23
|
-
"@aws-sdk/credential-providers": "^3.
|
|
24
|
-
"@
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"css-loader": "^7.1.2",
|
|
31
|
-
"env-ci": "^11.1.0",
|
|
32
|
-
"execa": "^7.2.0",
|
|
33
|
-
"fast-glob": "^3.3.2",
|
|
34
|
-
"file-loader": "^6.2.0",
|
|
22
|
+
"@aws-sdk/client-s3": "^3.864.0",
|
|
23
|
+
"@aws-sdk/credential-providers": "^3.864.0",
|
|
24
|
+
"@octokit/auth-action": "^6.0.1",
|
|
25
|
+
"@octokit/rest": "^22.0.0",
|
|
26
|
+
"chalk": "^5.5.0",
|
|
27
|
+
"env-ci": "^11.1.1",
|
|
28
|
+
"execa": "^9.6.0",
|
|
29
|
+
"git-url-parse": "^16.1.0",
|
|
35
30
|
"micromatch": "^4.0.8",
|
|
36
|
-
"piscina": "^
|
|
37
|
-
"rollup-plugin-visualizer": "^6.0.
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"webpack": "^5.90.3",
|
|
41
|
-
"webpack-bundle-analyzer": "^4.10.1",
|
|
42
|
-
"yargs": "^17.7.2"
|
|
31
|
+
"piscina": "^5.1.3",
|
|
32
|
+
"rollup-plugin-visualizer": "^6.0.3",
|
|
33
|
+
"vite": "^7.1.2",
|
|
34
|
+
"yargs": "^18.0.0"
|
|
43
35
|
},
|
|
44
36
|
"devDependencies": {
|
|
45
37
|
"@types/env-ci": "^3.1.4",
|
|
46
38
|
"@types/micromatch": "^4.0.9",
|
|
47
|
-
"@types/webpack": "^5.28.5",
|
|
48
|
-
"@types/webpack-bundle-analyzer": "^4.7.0",
|
|
49
39
|
"@types/yargs": "^17.0.33"
|
|
50
40
|
},
|
|
51
|
-
"gitSha": "
|
|
41
|
+
"gitSha": "2f3a57356b8d7dac3a6970285fa99d43fd6125d2",
|
|
52
42
|
"scripts": {
|
|
53
43
|
"typescript": "tsc -p tsconfig.json",
|
|
54
44
|
"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<
|
|
32
|
+
* @returns {Promise<import('vite').InlineConfig>}
|
|
33
33
|
*/
|
|
34
34
|
async function createViteConfig(entry, args) {
|
|
35
35
|
const entryName = entry.id;
|
|
@@ -70,7 +70,7 @@ 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: {
|
|
@@ -100,6 +100,11 @@ 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: {
|
|
@@ -132,7 +137,7 @@ async function createViteConfig(entry, args) {
|
|
|
132
137
|
],
|
|
133
138
|
};
|
|
134
139
|
|
|
135
|
-
return
|
|
140
|
+
return configuration;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
/**
|
|
@@ -173,32 +178,45 @@ function walkDependencyTree(chunkKey, manifest, visited = new Set()) {
|
|
|
173
178
|
|
|
174
179
|
/**
|
|
175
180
|
* Process vite output to extract bundle sizes
|
|
176
|
-
* @param {
|
|
181
|
+
* @param {import('vite').Rollup.RollupOutput['output']} output - The Vite output
|
|
177
182
|
* @param {string} entryName - The entry name
|
|
178
183
|
* @returns {Promise<Map<string, { parsed: number, gzip: number }>>} - Map of bundle names to size information
|
|
179
184
|
*/
|
|
180
|
-
async function processBundleSizes(
|
|
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
|
|
183
|
-
|
|
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[
|
|
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(
|
|
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
|
|
201
|
-
|
|
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);
|
|
@@ -206,7 +224,7 @@ async function processBundleSizes(outDir, entryName) {
|
|
|
206
224
|
const gzipSize = Buffer.byteLength(gzipBuffer);
|
|
207
225
|
|
|
208
226
|
// Use chunk key as the name, or fallback to entry name for main chunk
|
|
209
|
-
const chunkName =
|
|
227
|
+
const chunkName = chunk.name === '_virtual_entry' ? entryName : chunkKey;
|
|
210
228
|
return /** @type {const} */ ([chunkName, { parsed, gzip: gzipSize }]);
|
|
211
229
|
});
|
|
212
230
|
|
|
@@ -220,14 +238,17 @@ async function processBundleSizes(outDir, entryName) {
|
|
|
220
238
|
* @param {CommandLineArgs} args - Command line arguments
|
|
221
239
|
* @returns {Promise<Map<string, { parsed: number, gzip: number }>>}
|
|
222
240
|
*/
|
|
223
|
-
export async function
|
|
241
|
+
export async function getBundleSizes(entry, args) {
|
|
224
242
|
// Create vite configuration
|
|
225
|
-
const
|
|
226
|
-
const outDir = path.join(rootDir, 'build', entry.id);
|
|
243
|
+
const configuration = await createViteConfig(entry, args);
|
|
227
244
|
|
|
228
245
|
// Run vite build
|
|
229
|
-
await build(configuration);
|
|
246
|
+
const { output } = /** @type {import('vite').Rollup.RollupOutput} */ (await build(configuration));
|
|
247
|
+
const manifestChunk = output.find((chunk) => chunk.fileName === '.vite/manifest.json');
|
|
248
|
+
if (!manifestChunk) {
|
|
249
|
+
throw new Error(`Manifest file not found in output for entry: ${entry.id}`);
|
|
250
|
+
}
|
|
230
251
|
|
|
231
252
|
// Process the output to get bundle sizes
|
|
232
|
-
return processBundleSizes(
|
|
253
|
+
return processBundleSizes(output, entry.id);
|
|
233
254
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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
9
|
import { loadConfig } from './configLoader.js';
|
|
10
10
|
import { uploadSnapshot } from './uploadSnapshot.js';
|
|
11
|
+
import { renderMarkdownReport } from './renderMarkdownReport.js';
|
|
12
|
+
import { octokit } from './github.js';
|
|
13
|
+
import { getCurrentRepoInfo } from './git.js';
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
|
|
@@ -19,12 +22,12 @@ const DEFAULT_CONCURRENCY = os.availableParallelism();
|
|
|
19
22
|
const rootDir = process.cwd();
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
* creates size snapshot for every bundle
|
|
25
|
+
* creates size snapshot for every bundle
|
|
23
26
|
* @param {CommandLineArgs} args
|
|
24
27
|
* @param {NormalizedBundleSizeCheckerConfig} config - The loaded configuration
|
|
25
28
|
* @returns {Promise<Array<[string, { parsed: number, gzip: number }]>>}
|
|
26
29
|
*/
|
|
27
|
-
async function
|
|
30
|
+
async function getBundleSizes(args, config) {
|
|
28
31
|
const worker = new Piscina({
|
|
29
32
|
filename: new URL('./worker.js', import.meta.url).href,
|
|
30
33
|
maxThreads: args.concurrency || DEFAULT_CONCURRENCY,
|
|
@@ -72,6 +75,42 @@ async function getWebpackSizes(args, config) {
|
|
|
72
75
|
return sizeArrays.flat();
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Report command handler
|
|
80
|
+
* @param {ReportCommandArgs} argv - Command line arguments
|
|
81
|
+
*/
|
|
82
|
+
async function reportCommand(argv) {
|
|
83
|
+
const { pr, owner: argOwner, repo: argRepo } = argv;
|
|
84
|
+
|
|
85
|
+
// Get current repo info and coerce with provided arguments
|
|
86
|
+
const currentRepo = await getCurrentRepoInfo();
|
|
87
|
+
const owner = argOwner ?? currentRepo.owner;
|
|
88
|
+
const repo = argRepo ?? currentRepo.repo;
|
|
89
|
+
|
|
90
|
+
if (typeof pr !== 'number') {
|
|
91
|
+
throw new Error('Invalid pull request number. Please provide a valid --pr option.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate that both owner and repo are available
|
|
95
|
+
if (!owner || !repo) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'Repository owner and name are required. Please provide --owner and --repo options, or run this command from within a git repository.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fetch PR information
|
|
102
|
+
const { data: prInfo } = await octokit.pulls.get({
|
|
103
|
+
owner,
|
|
104
|
+
repo,
|
|
105
|
+
pull_number: pr,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Generate and print the markdown report
|
|
109
|
+
const report = await renderMarkdownReport(prInfo);
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.log(report);
|
|
112
|
+
}
|
|
113
|
+
|
|
75
114
|
/**
|
|
76
115
|
* Main runner function
|
|
77
116
|
* @param {CommandLineArgs} argv - Command line arguments
|
|
@@ -86,12 +125,14 @@ async function run(argv) {
|
|
|
86
125
|
// eslint-disable-next-line no-console
|
|
87
126
|
console.log(`Starting bundle size snapshot creation with ${concurrency} workers...`);
|
|
88
127
|
|
|
89
|
-
const
|
|
90
|
-
const
|
|
128
|
+
const bundleSizes = await getBundleSizes(argv, config);
|
|
129
|
+
const sortedBundleSizes = Object.fromEntries(
|
|
130
|
+
bundleSizes.sort((a, b) => a[0].localeCompare(b[0])),
|
|
131
|
+
);
|
|
91
132
|
|
|
92
133
|
// Ensure output directory exists
|
|
93
134
|
await fs.mkdir(path.dirname(snapshotDestPath), { recursive: true });
|
|
94
|
-
await fs.writeFile(snapshotDestPath, JSON.stringify(
|
|
135
|
+
await fs.writeFile(snapshotDestPath, JSON.stringify(sortedBundleSizes, null, 2));
|
|
95
136
|
|
|
96
137
|
// eslint-disable-next-line no-console
|
|
97
138
|
console.log(`Bundle size snapshot written to ${snapshotDestPath}`);
|
|
@@ -109,56 +150,79 @@ async function run(argv) {
|
|
|
109
150
|
// Exit with error code to indicate failure
|
|
110
151
|
process.exit(1);
|
|
111
152
|
}
|
|
153
|
+
} else {
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.log('No upload configuration provided, skipping upload.');
|
|
112
156
|
}
|
|
113
157
|
}
|
|
114
158
|
|
|
115
159
|
yargs(process.argv.slice(2))
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
.command(
|
|
161
|
+
/** @type {import('yargs').CommandModule<{}, CommandLineArgs>} */ ({
|
|
162
|
+
command: '$0',
|
|
163
|
+
describe: 'Saves a size snapshot in size-snapshot.json',
|
|
164
|
+
builder: (cmdYargs) => {
|
|
165
|
+
return cmdYargs
|
|
166
|
+
.option('analyze', {
|
|
167
|
+
default: false,
|
|
168
|
+
describe: 'Creates a report for each bundle.',
|
|
169
|
+
type: 'boolean',
|
|
170
|
+
})
|
|
171
|
+
.option('verbose', {
|
|
172
|
+
default: false,
|
|
173
|
+
describe: 'Show more detailed information during compilation.',
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
})
|
|
176
|
+
.option('debug', {
|
|
177
|
+
default: false,
|
|
178
|
+
describe:
|
|
179
|
+
'Build with readable output (no name mangling or whitespace collapse, but still tree-shake).',
|
|
180
|
+
type: 'boolean',
|
|
181
|
+
})
|
|
182
|
+
.option('output', {
|
|
183
|
+
alias: 'o',
|
|
184
|
+
describe:
|
|
185
|
+
'Path to output the size snapshot JSON file (defaults to size-snapshot.json in current directory).',
|
|
186
|
+
type: 'string',
|
|
187
|
+
})
|
|
188
|
+
.option('filter', {
|
|
189
|
+
alias: 'F',
|
|
190
|
+
describe: 'Filter entry points by glob pattern(s) applied to their IDs',
|
|
191
|
+
type: 'array',
|
|
192
|
+
})
|
|
193
|
+
.option('concurrency', {
|
|
194
|
+
alias: 'c',
|
|
195
|
+
describe: 'Number of workers to use for parallel processing',
|
|
196
|
+
type: 'number',
|
|
197
|
+
default: DEFAULT_CONCURRENCY,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
handler: run,
|
|
201
|
+
}),
|
|
202
|
+
)
|
|
203
|
+
.command(
|
|
204
|
+
/** @type {import('yargs').CommandModule<{}, ReportCommandArgs>} */ ({
|
|
205
|
+
command: 'report',
|
|
206
|
+
describe: 'Generate a markdown report for a pull request',
|
|
207
|
+
builder: (cmdYargs) => {
|
|
208
|
+
return cmdYargs
|
|
209
|
+
.option('pr', {
|
|
210
|
+
describe: 'Pull request number',
|
|
211
|
+
type: 'number',
|
|
212
|
+
demandOption: true,
|
|
213
|
+
})
|
|
214
|
+
.option('owner', {
|
|
215
|
+
describe: 'Repository owner (defaults to current git repo owner)',
|
|
216
|
+
type: 'string',
|
|
217
|
+
})
|
|
218
|
+
.option('repo', {
|
|
219
|
+
describe: 'Repository name (defaults to current git repo name)',
|
|
220
|
+
type: 'string',
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
handler: reportCommand,
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
162
226
|
.help()
|
|
163
227
|
.strict(true)
|
|
164
228
|
.version(false)
|
package/src/configLoader.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Utility to load the bundle-size-checker configuration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import path from 'path';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
7
|
import envCi from 'env-ci';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -29,6 +29,9 @@ async function loadConfigFile(configPath) {
|
|
|
29
29
|
resolvedConfig = await config;
|
|
30
30
|
} else if (typeof config === 'function') {
|
|
31
31
|
resolvedConfig = await config();
|
|
32
|
+
} else {
|
|
33
|
+
// Handle plain config objects
|
|
34
|
+
resolvedConfig = config;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
return resolvedConfig;
|
package/src/fetchSnapshot.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
// This file must be importable in the browser
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
*
|
|
3
5
|
* @param {string} repo - The name of the repository e.g. 'mui/material-ui'
|
|
4
6
|
* @param {string} sha - The commit SHA
|
|
5
|
-
* @returns {Promise<import('./sizeDiff').SizeSnapshot>} - The size snapshot data
|
|
7
|
+
* @returns {Promise<import('./sizeDiff.js').SizeSnapshot>} - The size snapshot data
|
|
6
8
|
*/
|
|
7
9
|
export async function fetchSnapshot(repo, sha) {
|
|
8
10
|
const urlsToTry = [
|
|
@@ -25,7 +27,7 @@ export async function fetchSnapshot(repo, sha) {
|
|
|
25
27
|
continue;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
return response.json();
|
|
30
|
+
return /** @type {Promise<any>} */ (response.json());
|
|
29
31
|
} catch (error) {
|
|
30
32
|
lastError = error;
|
|
31
33
|
continue;
|
|
@@ -34,61 +36,3 @@ export async function fetchSnapshot(repo, sha) {
|
|
|
34
36
|
|
|
35
37
|
throw new Error(`Failed to fetch snapshot`, { cause: lastError });
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Gets parent commits for a given commit SHA using GitHub API
|
|
40
|
-
* @param {string} repo - Repository name (e.g., 'mui/material-ui')
|
|
41
|
-
* @param {string} commit - The commit SHA to start from
|
|
42
|
-
* @param {number} depth - How many commits to retrieve (including the starting commit)
|
|
43
|
-
* @returns {Promise<string[]>} Array of commit SHAs in chronological order (excluding the starting commit)
|
|
44
|
-
*/
|
|
45
|
-
async function getParentCommits(repo, commit, depth = 4) {
|
|
46
|
-
try {
|
|
47
|
-
const response = await fetch(
|
|
48
|
-
`https://api.github.com/repos/${repo}/commits?sha=${commit}&per_page=${depth}`,
|
|
49
|
-
);
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
throw new Error(`GitHub API request failed: ${response.status}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** @type {{ sha: string }[]} */
|
|
55
|
-
const commits = await response.json();
|
|
56
|
-
// Skip the first commit (which is the starting commit) and return the rest
|
|
57
|
-
return commits.slice(1).map((commitDetails) => commitDetails.sha);
|
|
58
|
-
} catch (/** @type {any} */ error) {
|
|
59
|
-
console.warn(`Failed to get parent commits for ${commit}: ${error.message}`);
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Attempts to fetch a snapshot with fallback to parent commits
|
|
66
|
-
* @param {string} repo - Repository name
|
|
67
|
-
* @param {string} commit - The commit SHA to start from
|
|
68
|
-
* @param {number} [fallbackDepth=3] - How many parent commits to try as fallback
|
|
69
|
-
* @returns {Promise<{snapshot: import('./sizeDiff').SizeSnapshot | null, actualCommit: string | null}>}
|
|
70
|
-
*/
|
|
71
|
-
export async function fetchSnapshotWithFallback(repo, commit, fallbackDepth = 3) {
|
|
72
|
-
// Try the original commit first
|
|
73
|
-
try {
|
|
74
|
-
const snapshot = await fetchSnapshot(repo, commit);
|
|
75
|
-
return { snapshot, actualCommit: commit };
|
|
76
|
-
} catch (/** @type {any} */ error) {
|
|
77
|
-
// fallthrough to parent commits if the snapshot for the original commit fails
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Get parent commits and try each one
|
|
81
|
-
const parentCommits = await getParentCommits(repo, commit, fallbackDepth + 1);
|
|
82
|
-
|
|
83
|
-
for (const parentCommit of parentCommits) {
|
|
84
|
-
try {
|
|
85
|
-
// eslint-disable-next-line no-await-in-loop
|
|
86
|
-
const snapshot = await fetchSnapshot(repo, parentCommit);
|
|
87
|
-
return { snapshot, actualCommit: parentCommit };
|
|
88
|
-
} catch {
|
|
89
|
-
// fallthrough to the next parent commit if fetching fails
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { snapshot: null, actualCommit: null };
|
|
94
|
-
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
2
|
+
import { getParentCommits } from './git.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Attempts to fetch a snapshot with fallback to parent commits
|
|
6
|
+
* @param {string} repo - Repository name
|
|
7
|
+
* @param {string} commit - The commit SHA to start from
|
|
8
|
+
* @param {number} [fallbackDepth=3] - How many parent commits to try as fallback
|
|
9
|
+
* @returns {Promise<{snapshot: import('./sizeDiff.js').SizeSnapshot | null, actualCommit: string | null}>}
|
|
10
|
+
*/
|
|
11
|
+
export async function fetchSnapshotWithFallback(repo, commit, fallbackDepth = 3) {
|
|
12
|
+
// Try the original commit first
|
|
13
|
+
try {
|
|
14
|
+
const snapshot = await fetchSnapshot(repo, commit);
|
|
15
|
+
return { snapshot, actualCommit: commit };
|
|
16
|
+
} catch (/** @type {any} */ error) {
|
|
17
|
+
// fallthrough to parent commits if the snapshot for the original commit fails
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get parent commits and try each one
|
|
21
|
+
const parentCommits = await getParentCommits(repo, commit, fallbackDepth);
|
|
22
|
+
|
|
23
|
+
for (const parentCommit of parentCommits) {
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line no-await-in-loop
|
|
26
|
+
const snapshot = await fetchSnapshot(repo, parentCommit);
|
|
27
|
+
return { snapshot, actualCommit: parentCommit };
|
|
28
|
+
} catch {
|
|
29
|
+
// fallthrough to the next parent commit if fetching fails
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { snapshot: null, actualCommit: null };
|
|
34
|
+
}
|
package/src/git.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import gitUrlParse from 'git-url-parse';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gets parent commits for a given commit SHA using git CLI
|
|
6
|
+
* @param {string} repo - Repository name (e.g., 'mui/material-ui') - ignored for git CLI
|
|
7
|
+
* @param {string} commit - The commit SHA to start from
|
|
8
|
+
* @param {number} depth - How many commits to retrieve (including the starting commit)
|
|
9
|
+
* @returns {Promise<string[]>} Array of commit SHAs in chronological order (excluding the starting commit)
|
|
10
|
+
*/
|
|
11
|
+
export async function getParentCommits(repo, commit, depth = 3) {
|
|
12
|
+
const { stdout } = await execa('git', ['rev-list', `--max-count=${depth}`, '--skip=1', commit]);
|
|
13
|
+
return stdout.trim().split('\n').filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compares two commits and returns merge base information using git CLI
|
|
18
|
+
* @param {string} base - Base commit SHA
|
|
19
|
+
* @param {string} head - Head commit SHA
|
|
20
|
+
* @returns {Promise<string>} Object with merge base commit info
|
|
21
|
+
*/
|
|
22
|
+
export async function getMergeBase(base, head) {
|
|
23
|
+
const { stdout } = await execa('git', ['merge-base', base, head]);
|
|
24
|
+
return stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the current repository owner and name from git remote
|
|
29
|
+
* @returns {Promise<{owner: string | null, repo: string | null}>}
|
|
30
|
+
*/
|
|
31
|
+
export async function getCurrentRepoInfo() {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
34
|
+
const parsed = gitUrlParse(stdout.trim());
|
|
35
|
+
return {
|
|
36
|
+
owner: parsed.owner,
|
|
37
|
+
repo: parsed.name,
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
owner: null,
|
|
42
|
+
repo: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/github.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { Octokit } from '@octokit/rest';
|
|
4
|
+
import { createActionAuth } from '@octokit/auth-action';
|
|
5
|
+
|
|
6
|
+
// Create and export Octokit instance
|
|
7
|
+
/** @type {import('@octokit/rest').Octokit} */
|
|
8
|
+
export const octokit = new Octokit({
|
|
9
|
+
authStrategy: process.env.GITHUB_TOKEN ? createActionAuth : undefined,
|
|
10
|
+
userAgent: 'bundle-size-checker',
|
|
11
|
+
});
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { calculateSizeDiff } from './sizeDiff.js';
|
|
8
|
-
import { fetchSnapshot
|
|
8
|
+
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
9
9
|
import { displayPercentFormatter, byteSizeChangeFormatter } from './formatUtils.js';
|
|
10
|
+
import { octokit } from './github.js';
|
|
11
|
+
import { getCurrentRepoInfo, getMergeBase } from './git.js';
|
|
12
|
+
import { fetchSnapshotWithFallback } from './fetchSnapshotWithFallback.js';
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Generates a symbol based on the relative change value.
|
|
@@ -96,11 +99,13 @@ function formatMarkdownTable(columns, data) {
|
|
|
96
99
|
const separators = alignments.map((align) => {
|
|
97
100
|
switch (align) {
|
|
98
101
|
case 'center':
|
|
99
|
-
return '
|
|
102
|
+
return ':---------:';
|
|
100
103
|
case 'right':
|
|
101
104
|
return '----------:';
|
|
105
|
+
case 'left':
|
|
106
|
+
return ':----------';
|
|
102
107
|
default:
|
|
103
|
-
return '
|
|
108
|
+
return '-----------';
|
|
104
109
|
}
|
|
105
110
|
});
|
|
106
111
|
table += `|${separators.join('|')}|\n`;
|
|
@@ -140,9 +145,9 @@ export function renderMarkdownReportContent(
|
|
|
140
145
|
|
|
141
146
|
markdownContent += formatMarkdownTable(
|
|
142
147
|
[
|
|
143
|
-
{ field: 'id', header: 'Bundle' },
|
|
144
|
-
{ field: 'parsed', header: 'Parsed
|
|
145
|
-
{ field: 'gzip', header: 'Gzip
|
|
148
|
+
{ field: 'id', header: 'Bundle', align: 'left' },
|
|
149
|
+
{ field: 'parsed', header: 'Parsed size', align: 'right' },
|
|
150
|
+
{ field: 'gzip', header: 'Gzip size', align: 'right' },
|
|
146
151
|
],
|
|
147
152
|
trackedEntries.map(({ id, parsed, gzip }) => ({
|
|
148
153
|
id,
|
|
@@ -224,11 +229,27 @@ function getDetailsUrl(prInfo, options = {}) {
|
|
|
224
229
|
export async function renderMarkdownReport(prInfo, circleciBuildNumber, options = {}) {
|
|
225
230
|
let markdownContent = '';
|
|
226
231
|
|
|
227
|
-
const baseCommit = prInfo.base.sha;
|
|
228
232
|
const prCommit = prInfo.head.sha;
|
|
229
233
|
const repo = prInfo.base.repo.full_name;
|
|
230
234
|
const { fallbackDepth = 3 } = options;
|
|
231
235
|
|
|
236
|
+
const [owner, repoName] = repo.split('/');
|
|
237
|
+
|
|
238
|
+
const currentRepo = await getCurrentRepoInfo();
|
|
239
|
+
|
|
240
|
+
let baseCommit;
|
|
241
|
+
if (owner === currentRepo.owner && repoName === currentRepo.repo) {
|
|
242
|
+
baseCommit = await getMergeBase(prInfo.base.sha, prCommit);
|
|
243
|
+
} else {
|
|
244
|
+
const { data } = await octokit.repos.compareCommits({
|
|
245
|
+
owner,
|
|
246
|
+
repo: repoName,
|
|
247
|
+
base: prInfo.base.sha,
|
|
248
|
+
head: prCommit,
|
|
249
|
+
});
|
|
250
|
+
baseCommit = data.merge_base_commit.sha;
|
|
251
|
+
}
|
|
252
|
+
|
|
232
253
|
const [baseResult, prSnapshot] = await Promise.all([
|
|
233
254
|
fetchSnapshotWithFallback(repo, baseCommit, fallbackDepth),
|
|
234
255
|
fetchSnapshot(repo, prCommit),
|
|
@@ -237,9 +258,9 @@ export async function renderMarkdownReport(prInfo, circleciBuildNumber, options
|
|
|
237
258
|
const { snapshot: baseSnapshot, actualCommit: actualBaseCommit } = baseResult;
|
|
238
259
|
|
|
239
260
|
if (!baseSnapshot) {
|
|
240
|
-
markdownContent += `_:no_entry_sign: No bundle size snapshot found for base
|
|
261
|
+
markdownContent += `_:no_entry_sign: No bundle size snapshot found for merge base ${baseCommit} or any of its ${fallbackDepth} parent commits._\n\n`;
|
|
241
262
|
} else if (actualBaseCommit !== baseCommit) {
|
|
242
|
-
markdownContent += `_:information_source: Using snapshot from parent commit ${actualBaseCommit} (fallback from ${baseCommit})._\n\n`;
|
|
263
|
+
markdownContent += `_:information_source: Using snapshot from parent commit ${actualBaseCommit} (fallback from merge base ${baseCommit})._\n\n`;
|
|
243
264
|
}
|
|
244
265
|
|
|
245
266
|
const sizeDiff = calculateSizeDiff(baseSnapshot ?? {}, prSnapshot);
|
|
@@ -2,13 +2,30 @@
|
|
|
2
2
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
3
3
|
import { renderMarkdownReport } from './renderMarkdownReport.js';
|
|
4
4
|
import * as fetchSnapshotModule from './fetchSnapshot.js';
|
|
5
|
+
import * as fetchSnapshotWithFallbackModule from './fetchSnapshotWithFallback.js';
|
|
5
6
|
|
|
6
7
|
// Mock the fetchSnapshot module
|
|
7
8
|
vi.mock('./fetchSnapshot.js');
|
|
9
|
+
// Mock the fetchSnapshotWithFallback module
|
|
10
|
+
vi.mock('./fetchSnapshotWithFallback.js');
|
|
11
|
+
// Mock the @octokit/rest module
|
|
12
|
+
vi.mock('@octokit/rest', () => ({
|
|
13
|
+
Octokit: vi.fn(() => ({
|
|
14
|
+
repos: {
|
|
15
|
+
compareCommits: vi.fn(),
|
|
16
|
+
listCommits: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
pulls: {
|
|
19
|
+
get: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
8
23
|
|
|
9
24
|
describe('renderMarkdownReport', () => {
|
|
10
25
|
const mockFetchSnapshot = vi.mocked(fetchSnapshotModule.fetchSnapshot);
|
|
11
|
-
const mockFetchSnapshotWithFallback = vi.mocked(
|
|
26
|
+
const mockFetchSnapshotWithFallback = vi.mocked(
|
|
27
|
+
fetchSnapshotWithFallbackModule.fetchSnapshotWithFallback,
|
|
28
|
+
);
|
|
12
29
|
|
|
13
30
|
/** @type {PrInfo} */
|
|
14
31
|
const mockPrInfo = {
|
|
@@ -24,9 +41,26 @@ describe('renderMarkdownReport', () => {
|
|
|
24
41
|
},
|
|
25
42
|
};
|
|
26
43
|
|
|
27
|
-
beforeEach(() => {
|
|
44
|
+
beforeEach(async () => {
|
|
28
45
|
mockFetchSnapshot.mockClear();
|
|
29
46
|
mockFetchSnapshotWithFallback.mockClear();
|
|
47
|
+
|
|
48
|
+
// Import and mock the octokit instance after mocking the module
|
|
49
|
+
const { octokit } = await import('./github.js');
|
|
50
|
+
|
|
51
|
+
// Set up default mock for compareCommits to return the base commit SHA
|
|
52
|
+
vi.mocked(octokit.repos.compareCommits).mockResolvedValue(
|
|
53
|
+
/** @type {any} */ ({
|
|
54
|
+
data: {
|
|
55
|
+
merge_base_commit: {
|
|
56
|
+
sha: mockPrInfo.base.sha,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Clear any previous mock calls
|
|
63
|
+
vi.mocked(octokit.repos.compareCommits).mockClear();
|
|
30
64
|
});
|
|
31
65
|
|
|
32
66
|
it('should generate markdown report with size increases', async () => {
|
|
@@ -109,7 +143,7 @@ describe('renderMarkdownReport', () => {
|
|
|
109
143
|
const result = await renderMarkdownReport(mockPrInfo);
|
|
110
144
|
|
|
111
145
|
expect(result).toContain(
|
|
112
|
-
'No bundle size snapshot found for base
|
|
146
|
+
'No bundle size snapshot found for merge base abc123 or any of its 3 parent commits.',
|
|
113
147
|
);
|
|
114
148
|
});
|
|
115
149
|
|
|
@@ -324,8 +358,8 @@ describe('renderMarkdownReport', () => {
|
|
|
324
358
|
});
|
|
325
359
|
|
|
326
360
|
expect(result).toMatchInlineSnapshot(`
|
|
327
|
-
"| Bundle | Parsed
|
|
328
|
-
|
|
361
|
+
"| Bundle | Parsed size | Gzip size |
|
|
362
|
+
|:----------|----------:|----------:|
|
|
329
363
|
| @mui/material/Button/index.js | 🔺+400B<sup>(+2.67%)</sup> | 🔺+100B<sup>(+2.22%)</sup> |
|
|
330
364
|
| @mui/material/TextField/index.js | 🔺+200B<sup>(+0.91%)</sup> | 🔺+100B<sup>(+1.54%)</sup> |
|
|
331
365
|
|
|
@@ -359,8 +393,8 @@ describe('renderMarkdownReport', () => {
|
|
|
359
393
|
});
|
|
360
394
|
|
|
361
395
|
expect(result).toMatchInlineSnapshot(`
|
|
362
|
-
"| Bundle | Parsed
|
|
363
|
-
|
|
396
|
+
"| Bundle | Parsed size | Gzip size |
|
|
397
|
+
|:----------|----------:|----------:|
|
|
364
398
|
| @mui/material/Button/index.js | 🔺+500B<sup>(+3.33%)</sup> | 🔺+150B<sup>(+3.33%)</sup> |
|
|
365
399
|
| @mui/material/TextField/index.js | 🔺+300B<sup>(+1.36%)</sup> | 🔺+150B<sup>(+2.31%)</sup> |
|
|
366
400
|
|
|
@@ -394,8 +428,8 @@ describe('renderMarkdownReport', () => {
|
|
|
394
428
|
});
|
|
395
429
|
|
|
396
430
|
expect(result).toMatchInlineSnapshot(`
|
|
397
|
-
"| Bundle | Parsed
|
|
398
|
-
|
|
431
|
+
"| Bundle | Parsed size | Gzip size |
|
|
432
|
+
|:----------|----------:|----------:|
|
|
399
433
|
| @mui/material/Button/index.js | 🔺+400B<sup>(+2.67%)</sup> | 🔺+100B<sup>(+2.22%)</sup> |
|
|
400
434
|
|
|
401
435
|
|
|
@@ -428,8 +462,8 @@ describe('renderMarkdownReport', () => {
|
|
|
428
462
|
});
|
|
429
463
|
|
|
430
464
|
expect(result).toMatchInlineSnapshot(`
|
|
431
|
-
"| Bundle | Parsed
|
|
432
|
-
|
|
465
|
+
"| Bundle | Parsed size | Gzip size |
|
|
466
|
+
|:----------|----------:|----------:|
|
|
433
467
|
| @mui/material/Button/index.js | 0B<sup>(0.00%)</sup> | 0B<sup>(0.00%)</sup> |
|
|
434
468
|
| @mui/material/TextField/index.js | 0B<sup>(0.00%)</sup> | 0B<sup>(0.00%)</sup> |
|
|
435
469
|
|
|
@@ -461,8 +495,8 @@ describe('renderMarkdownReport', () => {
|
|
|
461
495
|
});
|
|
462
496
|
|
|
463
497
|
expect(result).toMatchInlineSnapshot(`
|
|
464
|
-
"| Bundle | Parsed
|
|
465
|
-
|
|
498
|
+
"| Bundle | Parsed size | Gzip size |
|
|
499
|
+
|:----------|----------:|----------:|
|
|
466
500
|
| @mui/material/Button/index.js | 🔺+400B<sup>(+2.67%)</sup> | 🔺+100B<sup>(+2.22%)</sup> |
|
|
467
501
|
|
|
468
502
|
|
|
@@ -512,7 +546,9 @@ describe('renderMarkdownReport', () => {
|
|
|
512
546
|
|
|
513
547
|
const result = await renderMarkdownReport(mockPrInfo);
|
|
514
548
|
|
|
515
|
-
expect(result).toContain(
|
|
549
|
+
expect(result).toContain(
|
|
550
|
+
'Using snapshot from parent commit parent1 (fallback from merge base abc123)',
|
|
551
|
+
);
|
|
516
552
|
expect(result).toContain('baseCommit=parent1');
|
|
517
553
|
});
|
|
518
554
|
|
|
@@ -527,7 +563,7 @@ describe('renderMarkdownReport', () => {
|
|
|
527
563
|
const result = await renderMarkdownReport(mockPrInfo);
|
|
528
564
|
|
|
529
565
|
expect(result).toContain(
|
|
530
|
-
'No bundle size snapshot found for base
|
|
566
|
+
'No bundle size snapshot found for merge base abc123 or any of its 3 parent commits.',
|
|
531
567
|
);
|
|
532
568
|
});
|
|
533
569
|
|
|
@@ -548,7 +584,9 @@ describe('renderMarkdownReport', () => {
|
|
|
548
584
|
|
|
549
585
|
const result = await renderMarkdownReport(mockPrInfo, undefined, { fallbackDepth: 1 });
|
|
550
586
|
|
|
551
|
-
expect(result).toContain(
|
|
587
|
+
expect(result).toContain(
|
|
588
|
+
'Using snapshot from parent commit parent1 (fallback from merge base abc123)',
|
|
589
|
+
);
|
|
552
590
|
expect(mockFetchSnapshotWithFallback).toHaveBeenCalledWith('mui/material-ui', 'abc123', 1);
|
|
553
591
|
});
|
|
554
592
|
});
|
package/src/types.d.ts
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
// WebpackEntry type
|
|
2
|
-
interface WebpackEntry {
|
|
3
|
-
import: string;
|
|
4
|
-
importName?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// Webpack stats types
|
|
8
|
-
interface StatsAsset {
|
|
9
|
-
name: string;
|
|
10
|
-
size: number;
|
|
11
|
-
related?: {
|
|
12
|
-
find: (predicate: (asset: any) => boolean) => { size: number; type: string };
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface StatsChunkGroup {
|
|
17
|
-
name: string;
|
|
18
|
-
assets: Array<{ name: string; size: number }>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
1
|
// Upload configuration with optional properties
|
|
22
2
|
interface UploadConfig {
|
|
23
3
|
repo?: string; // The repository name (e.g., "mui/material-ui")
|
|
@@ -65,12 +45,17 @@ interface NormalizedBundleSizeCheckerConfig {
|
|
|
65
45
|
// Command line argument types
|
|
66
46
|
interface CommandLineArgs {
|
|
67
47
|
analyze?: boolean;
|
|
68
|
-
accurateBundles?: boolean;
|
|
69
48
|
output?: string;
|
|
70
49
|
verbose?: boolean;
|
|
71
50
|
filter?: string[];
|
|
72
51
|
concurrency?: number;
|
|
73
|
-
|
|
52
|
+
debug?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ReportCommandArgs {
|
|
56
|
+
pr?: number;
|
|
57
|
+
owner?: string;
|
|
58
|
+
repo?: string;
|
|
74
59
|
}
|
|
75
60
|
|
|
76
61
|
// Diff command argument types
|
package/src/uploadSnapshot.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import { S3Client, PutObjectCommand, PutObjectTaggingCommand } from '@aws-sdk/client-s3';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { fromEnv } from '@aws-sdk/credential-providers';
|
|
@@ -20,7 +20,7 @@ async function getCurrentCommitSHA() {
|
|
|
20
20
|
*/
|
|
21
21
|
function sanitizeS3TagString(str) {
|
|
22
22
|
// Replace disallowed characters with underscore
|
|
23
|
-
const safe = str.replace(/[^a-zA-Z0-9
|
|
23
|
+
const safe = str.replace(/[^a-zA-Z0-9 +\-=.:/@]+/g, '_');
|
|
24
24
|
// Truncate to max lengths (256 for value)
|
|
25
25
|
const maxLen = 256;
|
|
26
26
|
return safe.length > maxLen ? safe.substring(0, maxLen) : safe;
|
package/src/worker.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { pathToFileURL } from 'url';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs/promises';
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import * as module from 'module';
|
|
5
|
+
import * as module from 'node:module';
|
|
6
6
|
import { byteSizeFormatter } from './formatUtils.js';
|
|
7
|
-
import {
|
|
8
|
-
import { getViteSizes } from './viteBuilder.js';
|
|
7
|
+
import { getBundleSizes } from './builder.js';
|
|
9
8
|
|
|
10
9
|
const require = module.createRequire(import.meta.url);
|
|
11
10
|
|
|
@@ -83,12 +82,7 @@ export default async function getSizes({ entry, args, index, total }) {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
try {
|
|
86
|
-
|
|
87
|
-
if (args.vite) {
|
|
88
|
-
sizeMap = await getViteSizes(entry, args);
|
|
89
|
-
} else {
|
|
90
|
-
sizeMap = await getWebpackSizes(entry, args);
|
|
91
|
-
}
|
|
85
|
+
const sizeMap = await getBundleSizes(entry, args);
|
|
92
86
|
|
|
93
87
|
// Create a concise log message showing import details
|
|
94
88
|
let entryDetails = '';
|
|
@@ -112,7 +106,6 @@ export default async function getSizes({ entry, args, index, total }) {
|
|
|
112
106
|
${chalk.green('✓')} ${chalk.green.bold(`Completed ${index + 1}/${total}: [${entry.id}]`)}
|
|
113
107
|
${chalk.cyan('Import:')} ${entryDetails}
|
|
114
108
|
${chalk.cyan('Externals:')} ${entry.externals.join(', ')}
|
|
115
|
-
${chalk.cyan('Bundler:')} ${args.vite ? 'vite' : 'webpack'}
|
|
116
109
|
${chalk.cyan('Sizes:')} ${chalk.yellow(byteSizeFormatter.format(entrySize.parsed))} (${chalk.yellow(byteSizeFormatter.format(entrySize.gzip))} gzipped)
|
|
117
110
|
${args.analyze ? ` ${chalk.cyan('Analysis:')} ${chalk.underline(pathToFileURL(path.join(rootDir, 'build', `${entry.id}.html`)).href)}` : ''}
|
|
118
111
|
`.trim(),
|
package/tsconfig.json
CHANGED
package/src/webpackBuilder.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { promisify } from 'util';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import webpackCallbackBased from 'webpack';
|
|
4
|
-
import CompressionPlugin from 'compression-webpack-plugin';
|
|
5
|
-
import TerserPlugin from 'terser-webpack-plugin';
|
|
6
|
-
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @type {(options: webpackCallbackBased.Configuration) => Promise<webpackCallbackBased.Stats>}
|
|
11
|
-
*/
|
|
12
|
-
// @ts-expect-error Can't select the right overload
|
|
13
|
-
const webpack = promisify(webpackCallbackBased);
|
|
14
|
-
const rootDir = process.cwd();
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Creates webpack configuration for bundle size checking
|
|
19
|
-
* @param {ObjectEntry} entry - Entry point (string or object)
|
|
20
|
-
* @param {CommandLineArgs} args
|
|
21
|
-
* @returns {Promise<{configuration: import('webpack').Configuration, externalsArray: string[]}>}
|
|
22
|
-
*/
|
|
23
|
-
async function createWebpackConfig(entry, args) {
|
|
24
|
-
const analyzerMode = args.analyze ? 'static' : 'disabled';
|
|
25
|
-
const concatenateModules = !args.accurateBundles;
|
|
26
|
-
|
|
27
|
-
const entryName = entry.id;
|
|
28
|
-
let entryContent;
|
|
29
|
-
|
|
30
|
-
if (entry.code && (entry.import || entry.importedNames)) {
|
|
31
|
-
entryContent = entry.code;
|
|
32
|
-
} else if (entry.code) {
|
|
33
|
-
entryContent = entry.code;
|
|
34
|
-
} else if (entry.import) {
|
|
35
|
-
if (entry.importedNames && entry.importedNames.length > 0) {
|
|
36
|
-
// Generate named imports for each name in the importedNames array
|
|
37
|
-
const imports = entry.importedNames
|
|
38
|
-
.map((name) => `import { ${name} } from '${entry.import}';`)
|
|
39
|
-
.join('\n');
|
|
40
|
-
const logs = entry.importedNames.map((name) => `console.log(${name});`).join('\n');
|
|
41
|
-
entryContent = `${imports}\n${logs}`;
|
|
42
|
-
} else {
|
|
43
|
-
// Default to import * as if importedNames is not defined
|
|
44
|
-
entryContent = `import * as _ from '${entry.import}';\nconsole.log(_);`;
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
throw new Error(`Entry "${entry.id}" must have either code or import property defined`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Generate externals function from an array of package names
|
|
52
|
-
* @param {string[]} packages - Array of package names to exclude (defaults to react and react-dom)
|
|
53
|
-
* @returns {function} - Function to determine if a request should be treated as external
|
|
54
|
-
*/
|
|
55
|
-
function createExternalsFunction(packages = ['react', 'react-dom']) {
|
|
56
|
-
/**
|
|
57
|
-
* Check if a request should be treated as external
|
|
58
|
-
* Uses the new recommended format to avoid deprecation warnings
|
|
59
|
-
* @param {{ context: string, request: string }} params - Object containing context and request
|
|
60
|
-
* @param {Function} callback - Callback to handle the result
|
|
61
|
-
*/
|
|
62
|
-
return ({ request }, callback) => {
|
|
63
|
-
// Iterate through all packages and check if request is equal to or starts with package + '/'
|
|
64
|
-
for (const pkg of packages) {
|
|
65
|
-
if (request === pkg || request.startsWith(`${pkg}/`)) {
|
|
66
|
-
return callback(null, `commonjs ${request}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return callback();
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Use externals from the entry object
|
|
75
|
-
const externalsArray = entry.externals || ['react', 'react-dom'];
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @type {import('webpack').Configuration}
|
|
79
|
-
*/
|
|
80
|
-
const configuration = {
|
|
81
|
-
externals: [
|
|
82
|
-
// @ts-expect-error -- webpack types are not compatible with the current version
|
|
83
|
-
createExternalsFunction(externalsArray),
|
|
84
|
-
],
|
|
85
|
-
mode: 'production',
|
|
86
|
-
optimization: {
|
|
87
|
-
concatenateModules,
|
|
88
|
-
minimizer: [
|
|
89
|
-
new TerserPlugin({
|
|
90
|
-
test: /\.m?js(\?.*)?$/i,
|
|
91
|
-
// Avoid creating LICENSE.txt files for each module
|
|
92
|
-
// See https://github.com/webpack-contrib/terser-webpack-plugin#remove-comments
|
|
93
|
-
terserOptions: {
|
|
94
|
-
format: {
|
|
95
|
-
comments: false,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
extractComments: false,
|
|
99
|
-
}),
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
module: {
|
|
103
|
-
rules: [
|
|
104
|
-
{
|
|
105
|
-
test: /\.[jt]sx?$/,
|
|
106
|
-
include: rootDir,
|
|
107
|
-
exclude: /node_modules/,
|
|
108
|
-
use: {
|
|
109
|
-
loader: require.resolve('babel-loader'),
|
|
110
|
-
options: {
|
|
111
|
-
presets: [
|
|
112
|
-
require.resolve('@babel/preset-react'),
|
|
113
|
-
require.resolve('@babel/preset-typescript'),
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
test: /\.css$/,
|
|
120
|
-
use: [require.resolve('css-loader')],
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
test: /\.(png|svg|jpg|gif)$/,
|
|
124
|
-
use: [require.resolve('file-loader')],
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
output: {
|
|
129
|
-
filename: '[name].js',
|
|
130
|
-
library: {
|
|
131
|
-
// TODO: Use `type: 'module'` once it is supported (currently incompatible with `externals`)
|
|
132
|
-
name: 'M',
|
|
133
|
-
type: 'var',
|
|
134
|
-
// type: 'module',
|
|
135
|
-
},
|
|
136
|
-
path: path.join(rootDir, 'build'),
|
|
137
|
-
},
|
|
138
|
-
plugins: [
|
|
139
|
-
new CompressionPlugin({
|
|
140
|
-
filename: '[path][base][fragment].gz',
|
|
141
|
-
}),
|
|
142
|
-
new BundleAnalyzerPlugin({
|
|
143
|
-
analyzerMode,
|
|
144
|
-
// We create a report for each bundle so around 120 reports.
|
|
145
|
-
// Opening them all is spam.
|
|
146
|
-
// If opened with `webpack --config . --analyze` it'll still open one new tab though.
|
|
147
|
-
openAnalyzer: false,
|
|
148
|
-
// '[name].html' not supported: https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/12
|
|
149
|
-
reportFilename: `${entryName}.html`,
|
|
150
|
-
logLevel: 'warn',
|
|
151
|
-
}),
|
|
152
|
-
],
|
|
153
|
-
// A context to the current dir, which has a node_modules folder with workspace dependencies
|
|
154
|
-
context: rootDir,
|
|
155
|
-
entry: {
|
|
156
|
-
// This format is a data: url combined with inline matchResource to obtain a virtual entry.
|
|
157
|
-
// See https://github.com/webpack/webpack/issues/6437#issuecomment-874466638
|
|
158
|
-
// See https://webpack.js.org/api/module-methods/#import
|
|
159
|
-
// See https://webpack.js.org/api/loaders/#inline-matchresource
|
|
160
|
-
[entryName]: `./index.js!=!data:text/javascript;charset=utf-8;base64,${Buffer.from(entryContent.trim()).toString('base64')}`,
|
|
161
|
-
},
|
|
162
|
-
// TODO: 'browserslist:modern'
|
|
163
|
-
// See https://github.com/webpack/webpack/issues/14203
|
|
164
|
-
target: 'web',
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Return both the configuration and the externals array
|
|
168
|
-
return { configuration, externalsArray };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Process webpack stats to extract bundle sizes
|
|
173
|
-
* @param {import('webpack').Stats} webpackStats - The webpack stats object
|
|
174
|
-
* @returns {Map<string, { parsed: number, gzip: number }>} - Map of bundle names to size information
|
|
175
|
-
*/
|
|
176
|
-
function processBundleSizes(webpackStats) {
|
|
177
|
-
/** @type {Map<string, { parsed: number, gzip: number }>} */
|
|
178
|
-
const sizeMap = new Map();
|
|
179
|
-
|
|
180
|
-
if (!webpackStats) {
|
|
181
|
-
throw new Error('No webpack stats were returned');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (webpackStats.hasErrors()) {
|
|
185
|
-
const statsJson = webpackStats.toJson({
|
|
186
|
-
all: false,
|
|
187
|
-
entrypoints: true,
|
|
188
|
-
errors: true,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const entrypointKeys = statsJson.entrypoints ? Object.keys(statsJson.entrypoints) : [];
|
|
192
|
-
|
|
193
|
-
throw new Error(
|
|
194
|
-
`ERROR: The following errors occurred during bundling of ${entrypointKeys.join(', ')} with webpack: \n${(
|
|
195
|
-
statsJson.errors || []
|
|
196
|
-
)
|
|
197
|
-
.map((error) => {
|
|
198
|
-
return `${JSON.stringify(error, null, 2)}`;
|
|
199
|
-
})
|
|
200
|
-
.join('\n')}`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const stats = webpackStats.toJson({
|
|
205
|
-
all: false,
|
|
206
|
-
assets: true,
|
|
207
|
-
entrypoints: true,
|
|
208
|
-
relatedAssets: true,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
if (!stats.assets) {
|
|
212
|
-
return sizeMap;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const assets = new Map(stats.assets.map((asset) => [asset.name, asset]));
|
|
216
|
-
|
|
217
|
-
if (stats.entrypoints) {
|
|
218
|
-
Object.values(stats.entrypoints).forEach((entrypoint) => {
|
|
219
|
-
let parsedSize = 0;
|
|
220
|
-
let gzipSize = 0;
|
|
221
|
-
|
|
222
|
-
if (entrypoint.assets) {
|
|
223
|
-
entrypoint.assets.forEach(({ name, size }) => {
|
|
224
|
-
const asset = assets.get(name);
|
|
225
|
-
if (asset && asset.related) {
|
|
226
|
-
const gzippedAsset = asset.related.find((relatedAsset) => {
|
|
227
|
-
return relatedAsset.type === 'gzipped';
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (size !== undefined) {
|
|
231
|
-
parsedSize += size;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (gzippedAsset && gzippedAsset.size !== undefined) {
|
|
235
|
-
gzipSize += gzippedAsset.size;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!entrypoint.name) {
|
|
242
|
-
throw new Error('Entrypoint name is undefined');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
sizeMap.set(entrypoint.name, { parsed: parsedSize, gzip: gzipSize });
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return sizeMap;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Get sizes for a webpack bundle
|
|
254
|
-
* @param {ObjectEntry} entry - The entry configuration
|
|
255
|
-
* @param {CommandLineArgs} args - Command line arguments
|
|
256
|
-
* @returns {Promise<Map<string, { parsed: number, gzip: number }>>}
|
|
257
|
-
*/
|
|
258
|
-
export async function getWebpackSizes(entry, args) {
|
|
259
|
-
// Create webpack configuration
|
|
260
|
-
const { configuration } = await createWebpackConfig(entry, args);
|
|
261
|
-
|
|
262
|
-
// Run webpack
|
|
263
|
-
const webpackStats = await webpack(configuration);
|
|
264
|
-
|
|
265
|
-
// Process the webpack stats to get bundle sizes
|
|
266
|
-
return processBundleSizes(webpackStats);
|
|
267
|
-
}
|