@mui/internal-bundle-size-checker 1.0.0
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/.eslintrc.cjs +14 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/bin/bundle-size-checker.js +3 -0
- package/package.json +45 -0
- package/src/browser.d.ts +2 -0
- package/src/browser.js +2 -0
- package/src/cli.js +448 -0
- package/src/configLoader.js +189 -0
- package/src/defineConfig.js +15 -0
- package/src/fetchSnapshot.js +36 -0
- package/src/formatUtils.js +28 -0
- package/src/index.js +20 -0
- package/src/renderMarkdownReport.js +200 -0
- package/src/sizeDiff.js +199 -0
- package/src/types.d.ts +106 -0
- package/src/uploadSnapshot.js +72 -0
- package/src/worker.js +332 -0
- package/tsconfig.json +18 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rules: {
|
|
3
|
+
'import/prefer-default-export': 'off',
|
|
4
|
+
// Allow .js file extensions in import statements for ESM compatibility
|
|
5
|
+
'import/extensions': [
|
|
6
|
+
'error',
|
|
7
|
+
'ignorePackages',
|
|
8
|
+
{
|
|
9
|
+
js: 'always',
|
|
10
|
+
mjs: 'always',
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
};
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Material-UI SAS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Bundle Size Checker
|
|
2
|
+
|
|
3
|
+
A tool to check and track the bundle size of MUI packages.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Measures minified and gzipped bundle sizes of packages and components
|
|
8
|
+
- Compares bundle sizes between versions
|
|
9
|
+
- Generates markdown reports
|
|
10
|
+
- Uploads snapshots to S3 for persistent storage and comparison
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### CLI
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle-size-checker [options]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Options:
|
|
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
|
|
24
|
+
- `--output`, `-o`: Path to output the size snapshot JSON file
|
|
25
|
+
|
|
26
|
+
### Configuration
|
|
27
|
+
|
|
28
|
+
Create a `bundle-size-checker.config.js` or `bundle-size-checker.config.mjs` file:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { defineConfig } from '@mui/internal-bundle-size-checker';
|
|
32
|
+
|
|
33
|
+
export default defineConfig(async () => {
|
|
34
|
+
return {
|
|
35
|
+
entrypoints: [
|
|
36
|
+
// String entries (simple format)
|
|
37
|
+
'@mui/material', // Will bundle `import * as ... from '@mui/material'`
|
|
38
|
+
'@mui/material/Button', // Will bundle `import * as ... from '@mui/material/Button'`
|
|
39
|
+
'@mui/material#Button', // Will bundle `import { Button } from '@mui/material'`
|
|
40
|
+
|
|
41
|
+
// Object entries (advanced format)
|
|
42
|
+
{
|
|
43
|
+
id: 'custom-button',
|
|
44
|
+
code: `import Button from '@mui/material/Button'; console.log(Button);`,
|
|
45
|
+
},
|
|
46
|
+
// Object entries with import and importedNames
|
|
47
|
+
{
|
|
48
|
+
id: 'material-button-icons',
|
|
49
|
+
import: '@mui/material',
|
|
50
|
+
importedNames: ['Button', 'IconButton'],
|
|
51
|
+
},
|
|
52
|
+
// Object entry with custom externals
|
|
53
|
+
{
|
|
54
|
+
id: 'custom-externals',
|
|
55
|
+
import: '@mui/material',
|
|
56
|
+
importedNames: ['Button'],
|
|
57
|
+
externals: ['react', 'react-dom', '@emotion/styled'],
|
|
58
|
+
},
|
|
59
|
+
// Object entry that automatically extracts externals from package.json peer dependencies
|
|
60
|
+
{
|
|
61
|
+
id: 'auto-externals',
|
|
62
|
+
import: '@mui/material',
|
|
63
|
+
importedNames: ['Button'],
|
|
64
|
+
// When externals is not specified, peer dependencies will be automatically excluded
|
|
65
|
+
},
|
|
66
|
+
// ...
|
|
67
|
+
],
|
|
68
|
+
// Optional upload configuration
|
|
69
|
+
upload: {
|
|
70
|
+
project: 'organization/repository',
|
|
71
|
+
branch: 'main', // Optional, defaults to current git branch
|
|
72
|
+
isPullRequest: false, // Optional, defaults to false
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### S3 Upload
|
|
79
|
+
|
|
80
|
+
When the `upload` configuration is provided, the snapshot will be uploaded to S3 after generation.
|
|
81
|
+
|
|
82
|
+
The snapshot will be uploaded to:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
s3://mui-org-ci/artifacts/{project}/{commit-sha}/size-snapshot.json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The following tags will be applied:
|
|
89
|
+
|
|
90
|
+
- `isPullRequest`: 'yes' or 'no'
|
|
91
|
+
- `branch`: The branch name
|
|
92
|
+
|
|
93
|
+
Required AWS environment variables:
|
|
94
|
+
|
|
95
|
+
- `AWS_ACCESS_KEY_ID` or `AWS_ACCESS_KEY_ID_ARTIFACTS`
|
|
96
|
+
- `AWS_SECRET_ACCESS_KEY` or `AWS_SECRET_ACCESS_KEY_ARTIFACTS`
|
|
97
|
+
- `AWS_REGION` or `AWS_REGION_ARTIFACTS` (defaults to 'eu-central-1')
|
|
98
|
+
|
|
99
|
+
If the upload fails, the CLI will exit with an error code.
|
|
100
|
+
|
|
101
|
+
## API
|
|
102
|
+
|
|
103
|
+
The library exports the following functions:
|
|
104
|
+
|
|
105
|
+
- `defineConfig`: Helper for defining configuration with TypeScript support
|
|
106
|
+
- `loadConfig`: Loads configuration from file
|
|
107
|
+
- `calculateSizeDiff`: Calculates size differences between snapshots
|
|
108
|
+
- `renderMarkdownReport`: Generates markdown reports from size comparisons
|
|
109
|
+
- `fetchSnapshot`: Fetches size snapshots from S3
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mui/internal-bundle-size-checker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bundle size checker for MUI packages.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bundle-size-checker": "./bin/bundle-size-checker.js"
|
|
9
|
+
},
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./browser": "./src/browser.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@aws-sdk/client-s3": "^3.515.0",
|
|
18
|
+
"@aws-sdk/credential-providers": "^3.787.0",
|
|
19
|
+
"chalk": "^5.4.1",
|
|
20
|
+
"compression-webpack-plugin": "^10.0.0",
|
|
21
|
+
"css-loader": "^7.1.2",
|
|
22
|
+
"env-ci": "^11.1.0",
|
|
23
|
+
"execa": "^7.2.0",
|
|
24
|
+
"fast-glob": "^3.3.2",
|
|
25
|
+
"file-loader": "^6.2.0",
|
|
26
|
+
"fs-extra": "^11.2.0",
|
|
27
|
+
"micromatch": "^4.0.8",
|
|
28
|
+
"piscina": "^4.2.1",
|
|
29
|
+
"terser-webpack-plugin": "^5.3.10",
|
|
30
|
+
"webpack": "^5.90.3",
|
|
31
|
+
"webpack-bundle-analyzer": "^4.10.1",
|
|
32
|
+
"yargs": "^17.7.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/env-ci": "^3.1.4",
|
|
36
|
+
"@types/fs-extra": "^11.0.4",
|
|
37
|
+
"@types/micromatch": "^4.0.9",
|
|
38
|
+
"@types/webpack": "^5.28.5",
|
|
39
|
+
"@types/webpack-bundle-analyzer": "^4.7.0",
|
|
40
|
+
"@types/yargs": "^17.0.33"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typescript": "tsc -p tsconfig.json"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/browser.d.ts
ADDED
package/src/browser.js
ADDED
package/src/cli.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import fse from 'fs-extra';
|
|
6
|
+
import yargs from 'yargs';
|
|
7
|
+
import Piscina from 'piscina';
|
|
8
|
+
import micromatch from 'micromatch';
|
|
9
|
+
import { loadConfig } from './configLoader.js';
|
|
10
|
+
import { uploadSnapshot } from './uploadSnapshot.js';
|
|
11
|
+
import { calculateSizeDiff } from './sizeDiff.js';
|
|
12
|
+
import { renderMarkdownReportContent, renderMarkdownReport } from './renderMarkdownReport.js';
|
|
13
|
+
import { fetchSnapshot } from './fetchSnapshot.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {import('./sizeDiff.js').SizeSnapshot} SizeSnapshot
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const MAX_CONCURRENCY = Math.min(8, os.cpus().length);
|
|
20
|
+
|
|
21
|
+
const rootDir = process.cwd();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes entries to ensure they have a consistent format and ids are unique
|
|
25
|
+
* @param {ObjectEntry[]} entries - The array of entries from the config
|
|
26
|
+
* @returns {ObjectEntry[]} - Normalized entries with uniqueness enforced
|
|
27
|
+
*/
|
|
28
|
+
function normalizeEntries(entries) {
|
|
29
|
+
const usedIds = new Set();
|
|
30
|
+
|
|
31
|
+
return entries.map((entry) => {
|
|
32
|
+
if (!entry.id) {
|
|
33
|
+
throw new Error('Object entries must have an id property');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!entry.code && !entry.import) {
|
|
37
|
+
throw new Error(`Entry "${entry.id}" must have either code or import property defined`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (usedIds.has(entry.id)) {
|
|
41
|
+
throw new Error(`Duplicate entry id found: "${entry.id}". Entry ids must be unique.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
usedIds.add(entry.id);
|
|
45
|
+
|
|
46
|
+
return entry;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* creates size snapshot for every bundle that built with webpack
|
|
52
|
+
* @param {CommandLineArgs} args
|
|
53
|
+
* @param {NormalizedBundleSizeCheckerConfig} config - The loaded configuration
|
|
54
|
+
* @returns {Promise<Array<[string, { parsed: number, gzip: number }]>>}
|
|
55
|
+
*/
|
|
56
|
+
async function getWebpackSizes(args, config) {
|
|
57
|
+
const worker = new Piscina({
|
|
58
|
+
filename: new URL('./worker.js', import.meta.url).href,
|
|
59
|
+
maxThreads: MAX_CONCURRENCY,
|
|
60
|
+
});
|
|
61
|
+
// Clean and recreate the build directory
|
|
62
|
+
const buildDir = path.join(rootDir, 'build');
|
|
63
|
+
await fse.emptyDir(buildDir);
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!config ||
|
|
67
|
+
!config.entrypoints ||
|
|
68
|
+
!Array.isArray(config.entrypoints) ||
|
|
69
|
+
config.entrypoints.length === 0
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'No valid configuration found. Create a bundle-size-checker.config.js or bundle-size-checker.config.mjs file with entrypoints array.',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Normalize and validate entries
|
|
77
|
+
const entries = normalizeEntries(config.entrypoints);
|
|
78
|
+
|
|
79
|
+
// Apply filters if provided
|
|
80
|
+
let validEntries = entries;
|
|
81
|
+
const filter = args.filter;
|
|
82
|
+
if (filter && filter.length > 0) {
|
|
83
|
+
validEntries = entries.filter((entry) => {
|
|
84
|
+
return filter.some((pattern) => {
|
|
85
|
+
if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
|
|
86
|
+
return micromatch.isMatch(entry.id, pattern, { nocase: true });
|
|
87
|
+
}
|
|
88
|
+
return entry.id.toLowerCase().includes(pattern.toLowerCase());
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (validEntries.length === 0) {
|
|
93
|
+
console.warn('Warning: No entries match the provided filter pattern(s).');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sizeArrays = await Promise.all(
|
|
98
|
+
validEntries.map((entry, index) =>
|
|
99
|
+
worker.run({ entry, args, index, total: validEntries.length }),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return sizeArrays.flat();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Main runner function
|
|
108
|
+
* @param {CommandLineArgs} argv - Command line arguments
|
|
109
|
+
*/
|
|
110
|
+
async function run(argv) {
|
|
111
|
+
const { analyze, accurateBundles, output, verbose, filter } = argv;
|
|
112
|
+
|
|
113
|
+
const snapshotDestPath = output ? path.resolve(output) : path.join(rootDir, 'size-snapshot.json');
|
|
114
|
+
|
|
115
|
+
const config = await loadConfig(rootDir);
|
|
116
|
+
|
|
117
|
+
// Pass the filter patterns to getWebpackSizes if provided
|
|
118
|
+
const webpackSizes = await getWebpackSizes({ analyze, accurateBundles, verbose, filter }, config);
|
|
119
|
+
const bundleSizes = Object.fromEntries(webpackSizes.sort((a, b) => a[0].localeCompare(b[0])));
|
|
120
|
+
|
|
121
|
+
// Ensure output directory exists
|
|
122
|
+
await fse.mkdirp(path.dirname(snapshotDestPath));
|
|
123
|
+
await fse.writeJSON(snapshotDestPath, bundleSizes, { spaces: 2 });
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.log(`Bundle size snapshot written to ${snapshotDestPath}`);
|
|
127
|
+
|
|
128
|
+
// Upload the snapshot if upload configuration is provided and not null
|
|
129
|
+
if (config && config.upload) {
|
|
130
|
+
try {
|
|
131
|
+
// eslint-disable-next-line no-console
|
|
132
|
+
console.log('Uploading bundle size snapshot to S3...');
|
|
133
|
+
const { key } = await uploadSnapshot(snapshotDestPath, config.upload);
|
|
134
|
+
// eslint-disable-next-line no-console
|
|
135
|
+
console.log(`Bundle size snapshot uploaded to S3 with key: ${key}`);
|
|
136
|
+
} catch (/** @type {any} */ error) {
|
|
137
|
+
console.error('Failed to upload bundle size snapshot:', error.message);
|
|
138
|
+
// Exit with error code to indicate failure
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves a file path that can be relative or absolute
|
|
146
|
+
* @param {string} filePath - The file path to resolve
|
|
147
|
+
* @returns {string} The resolved absolute path
|
|
148
|
+
*/
|
|
149
|
+
function resolveFilePath(filePath) {
|
|
150
|
+
if (path.isAbsolute(filePath)) {
|
|
151
|
+
return filePath;
|
|
152
|
+
}
|
|
153
|
+
return path.resolve(rootDir, filePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Checks if a string is a URL
|
|
158
|
+
* @param {string} str - The string to check
|
|
159
|
+
* @returns {boolean} Whether the string is a URL
|
|
160
|
+
*/
|
|
161
|
+
function isUrl(str) {
|
|
162
|
+
try {
|
|
163
|
+
// eslint-disable-next-line no-new
|
|
164
|
+
new URL(str);
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Loads a snapshot from a URL (http:, https:, or file: scheme)
|
|
173
|
+
* @param {string} source - The source URL
|
|
174
|
+
* @returns {Promise<SizeSnapshot>} The loaded snapshot
|
|
175
|
+
*/
|
|
176
|
+
async function loadSnapshot(source) {
|
|
177
|
+
// Check if it's a valid URL
|
|
178
|
+
if (!isUrl(source)) {
|
|
179
|
+
throw new Error(`Invalid URL: ${source}. Use file:, http:, or https: schemes.`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (source.startsWith('file:')) {
|
|
183
|
+
// Handle file: URL
|
|
184
|
+
// Remove file: prefix and handle the rest as a file path
|
|
185
|
+
// For file:///absolute/path
|
|
186
|
+
let filePath = source.substring(source.indexOf('file:') + 5);
|
|
187
|
+
|
|
188
|
+
// Remove leading slashes for absolute paths on this machine
|
|
189
|
+
while (
|
|
190
|
+
filePath.startsWith('/') &&
|
|
191
|
+
!path.isAbsolute(filePath.substring(1)) &&
|
|
192
|
+
filePath.length > 1
|
|
193
|
+
) {
|
|
194
|
+
filePath = filePath.substring(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Now resolve the path
|
|
198
|
+
filePath = resolveFilePath(filePath);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
return await fse.readJSON(filePath);
|
|
202
|
+
} catch (/** @type {any} */ error) {
|
|
203
|
+
throw new Error(`Failed to read snapshot from ${filePath}: ${error.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// HTTP/HTTPS URL - fetch directly
|
|
208
|
+
const response = await fetch(source);
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new Error(`Failed to fetch snapshot from ${source}: ${response.statusText}`);
|
|
211
|
+
}
|
|
212
|
+
const body = await response.json();
|
|
213
|
+
return body;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handler for the diff command
|
|
218
|
+
* @param {DiffCommandArgs} argv - Command line arguments
|
|
219
|
+
*/
|
|
220
|
+
async function diffHandler(argv) {
|
|
221
|
+
const { base, head = 'file:./size-snapshot.json', output, reportUrl } = argv;
|
|
222
|
+
|
|
223
|
+
if (!base) {
|
|
224
|
+
console.error('The --base option is required');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// Load snapshots
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
console.log(`Loading base snapshot from ${base}...`);
|
|
232
|
+
const baseSnapshot = await loadSnapshot(base);
|
|
233
|
+
|
|
234
|
+
// eslint-disable-next-line no-console
|
|
235
|
+
console.log(`Loading head snapshot from ${head}...`);
|
|
236
|
+
const headSnapshot = await loadSnapshot(head);
|
|
237
|
+
|
|
238
|
+
// Calculate diff
|
|
239
|
+
const comparison = calculateSizeDiff(baseSnapshot, headSnapshot);
|
|
240
|
+
|
|
241
|
+
// Output
|
|
242
|
+
if (output === 'markdown') {
|
|
243
|
+
// Generate markdown with optional report URL
|
|
244
|
+
let markdownContent = renderMarkdownReportContent(comparison);
|
|
245
|
+
|
|
246
|
+
// Add report URL if provided
|
|
247
|
+
if (reportUrl) {
|
|
248
|
+
markdownContent += `\n\n[Details of bundle changes](${reportUrl})`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.log(markdownContent);
|
|
253
|
+
} else {
|
|
254
|
+
// Default JSON output
|
|
255
|
+
// eslint-disable-next-line no-console
|
|
256
|
+
console.log(JSON.stringify(comparison, null, 2));
|
|
257
|
+
}
|
|
258
|
+
} catch (/** @type {any} */ error) {
|
|
259
|
+
console.error(`Error: ${error.message}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Fetches GitHub PR information
|
|
266
|
+
* @param {string} owner - Repository owner
|
|
267
|
+
* @param {string} repo - Repository name
|
|
268
|
+
* @param {number} prNumber - Pull request number
|
|
269
|
+
* @returns {Promise<PrInfo>} PR information
|
|
270
|
+
*/
|
|
271
|
+
async function fetchPrInfo(owner, repo, prNumber) {
|
|
272
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
// eslint-disable-next-line no-console
|
|
276
|
+
console.log(`Fetching PR info from ${url}...`);
|
|
277
|
+
const response = await fetch(url);
|
|
278
|
+
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
throw new Error(`GitHub API request failed: ${response.statusText} (${response.status})`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return await response.json();
|
|
284
|
+
} catch (/** @type {any} */ error) {
|
|
285
|
+
console.error(`Failed to fetch PR info: ${error.message}`);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Handler for the pr command
|
|
292
|
+
* @param {PrCommandArgs} argv - Command line arguments
|
|
293
|
+
*/
|
|
294
|
+
async function prHandler(argv) {
|
|
295
|
+
const { prNumber, circleci, output } = argv;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// Load the config to get the repository information
|
|
299
|
+
const config = await loadConfig(rootDir);
|
|
300
|
+
|
|
301
|
+
if (!config.upload) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
'Upload is not configured. Please enable it in your bundle-size-checker config.',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Extract owner and repo from repository config
|
|
308
|
+
const [owner, repo] = config.upload.repo.split('/');
|
|
309
|
+
|
|
310
|
+
if (!owner || !repo) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Invalid repository format in config: ${config.upload.repo}. Expected format: "owner/repo"`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Fetch PR information from GitHub
|
|
317
|
+
const prInfo = await fetchPrInfo(owner, repo, prNumber);
|
|
318
|
+
|
|
319
|
+
// Generate the report
|
|
320
|
+
// eslint-disable-next-line no-console
|
|
321
|
+
console.log('Generating bundle size report...');
|
|
322
|
+
const report = await renderMarkdownReport(prInfo, circleci);
|
|
323
|
+
|
|
324
|
+
// Output
|
|
325
|
+
if (output === 'markdown') {
|
|
326
|
+
// eslint-disable-next-line no-console
|
|
327
|
+
console.log(report);
|
|
328
|
+
} else {
|
|
329
|
+
// For JSON we need to load the snapshots and calculate differences
|
|
330
|
+
const baseCommit = prInfo.base.sha;
|
|
331
|
+
const prCommit = prInfo.head.sha;
|
|
332
|
+
|
|
333
|
+
// eslint-disable-next-line no-console
|
|
334
|
+
console.log(`Fetching base snapshot for commit ${baseCommit}...`);
|
|
335
|
+
// eslint-disable-next-line no-console
|
|
336
|
+
console.log(`Fetching PR snapshot for commit ${prCommit}...`);
|
|
337
|
+
|
|
338
|
+
const [baseSnapshot, prSnapshot] = await Promise.all([
|
|
339
|
+
fetchSnapshot(config.upload.repo, baseCommit).catch(() => ({})),
|
|
340
|
+
fetchSnapshot(config.upload.repo, prCommit).catch(() => ({})),
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
const comparison = calculateSizeDiff(baseSnapshot, prSnapshot);
|
|
344
|
+
// eslint-disable-next-line no-console
|
|
345
|
+
console.log(JSON.stringify(comparison, null, 2));
|
|
346
|
+
}
|
|
347
|
+
} catch (/** @type {any} */ error) {
|
|
348
|
+
console.error(`Error: ${error.message}`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
yargs(process.argv.slice(2))
|
|
354
|
+
// @ts-expect-error
|
|
355
|
+
.command({
|
|
356
|
+
command: '$0',
|
|
357
|
+
describe: 'Saves a size snapshot in size-snapshot.json',
|
|
358
|
+
builder: (cmdYargs) => {
|
|
359
|
+
return cmdYargs
|
|
360
|
+
.option('analyze', {
|
|
361
|
+
default: false,
|
|
362
|
+
describe: 'Creates a webpack-bundle-analyzer report for each bundle.',
|
|
363
|
+
type: 'boolean',
|
|
364
|
+
})
|
|
365
|
+
.option('accurateBundles', {
|
|
366
|
+
default: false,
|
|
367
|
+
describe: 'Displays used bundles accurately at the cost of more CPU cycles.',
|
|
368
|
+
type: 'boolean',
|
|
369
|
+
})
|
|
370
|
+
.option('verbose', {
|
|
371
|
+
default: false,
|
|
372
|
+
describe: 'Show more detailed information during compilation.',
|
|
373
|
+
type: 'boolean',
|
|
374
|
+
})
|
|
375
|
+
.option('output', {
|
|
376
|
+
alias: 'o',
|
|
377
|
+
describe:
|
|
378
|
+
'Path to output the size snapshot JSON file (defaults to size-snapshot.json in current directory).',
|
|
379
|
+
type: 'string',
|
|
380
|
+
})
|
|
381
|
+
.option('filter', {
|
|
382
|
+
alias: 'F',
|
|
383
|
+
describe: 'Filter entry points by glob pattern(s) applied to their IDs',
|
|
384
|
+
type: 'array',
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
handler: run,
|
|
388
|
+
})
|
|
389
|
+
// @ts-expect-error
|
|
390
|
+
.command({
|
|
391
|
+
command: 'diff',
|
|
392
|
+
describe: 'Compare two bundle size snapshots',
|
|
393
|
+
builder: (cmdYargs) => {
|
|
394
|
+
return cmdYargs
|
|
395
|
+
.option('base', {
|
|
396
|
+
describe: 'Base snapshot URL (file:, http:, or https: scheme)',
|
|
397
|
+
type: 'string',
|
|
398
|
+
demandOption: true,
|
|
399
|
+
})
|
|
400
|
+
.option('head', {
|
|
401
|
+
describe:
|
|
402
|
+
'Head snapshot URL (file:, http:, or https: scheme), defaults to file:./size-snapshot.json',
|
|
403
|
+
type: 'string',
|
|
404
|
+
default: 'file:./size-snapshot.json',
|
|
405
|
+
})
|
|
406
|
+
.option('output', {
|
|
407
|
+
alias: 'o',
|
|
408
|
+
describe: 'Output format (json or markdown)',
|
|
409
|
+
type: 'string',
|
|
410
|
+
choices: ['json', 'markdown'],
|
|
411
|
+
default: 'json',
|
|
412
|
+
})
|
|
413
|
+
.option('reportUrl', {
|
|
414
|
+
describe: 'URL to the detailed report (optional)',
|
|
415
|
+
type: 'string',
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
handler: diffHandler,
|
|
419
|
+
})
|
|
420
|
+
// @ts-expect-error
|
|
421
|
+
.command({
|
|
422
|
+
command: 'pr <prNumber>',
|
|
423
|
+
describe: 'Generate a bundle size report for a GitHub pull request',
|
|
424
|
+
builder: (cmdYargs) => {
|
|
425
|
+
return cmdYargs
|
|
426
|
+
.positional('prNumber', {
|
|
427
|
+
describe: 'GitHub pull request number',
|
|
428
|
+
type: 'number',
|
|
429
|
+
demandOption: true,
|
|
430
|
+
})
|
|
431
|
+
.option('output', {
|
|
432
|
+
alias: 'o',
|
|
433
|
+
describe: 'Output format (json or markdown)',
|
|
434
|
+
type: 'string',
|
|
435
|
+
choices: ['json', 'markdown'],
|
|
436
|
+
default: 'markdown', // Default to markdown for PR reports
|
|
437
|
+
})
|
|
438
|
+
.option('circleci', {
|
|
439
|
+
describe: 'CircleCI build number for the report URL (optional)',
|
|
440
|
+
type: 'string',
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
handler: prHandler,
|
|
444
|
+
})
|
|
445
|
+
.help()
|
|
446
|
+
.strict(true)
|
|
447
|
+
.version(false)
|
|
448
|
+
.parse();
|