react-native-bundle-discovery 2.4.0 → 2.5.0-rc.1
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 +29 -1
- package/index.js +32 -6
- package/package.json +1 -1
- package/packageMetadata.js +155 -0
- package/webpack.js +35 -9
package/README.md
CHANGED
|
@@ -24,7 +24,6 @@ There are two ways to install the package:
|
|
|
24
24
|
1. As in independent tool (UI + CLI)
|
|
25
25
|
2. Or as a [Rozenite](_rozenite/README.md) plugin (see: [_rozenite/README.md](_rozenite/README.md))
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
> 🚀 If you've migrated to [Re.Pack](https://re-pack.dev/), you can still use this package to analyze 📊 JS bundles 📦 generated by Re.Pack.
|
|
29
28
|
>
|
|
30
29
|
> 💡 Learn more: 📖 [Re.Pack.md](./Re.Pack.md)
|
|
@@ -90,6 +89,35 @@ npx react-native-bundle-discovery-cli metro-stats.json
|
|
|
90
89
|
|
|
91
90
|
# Display all packages in the bundle report
|
|
92
91
|
npx react-native-bundle-discovery-cli packages metro-stats.json [--sort size|name] [--format json|table|default]
|
|
92
|
+
|
|
93
|
+
# Display the heaviest modules in the bundle report (default limit: 50, --limit 0 shows all)
|
|
94
|
+
# --filter accepts plain text (case-insensitive) or a regexp: --filter '/\.json/i'
|
|
95
|
+
npx react-native-bundle-discovery-cli modules metro-stats.json [--limit 50] [--filter <text|/regexp/>] [--sort size|name] [--format json|table|default]
|
|
96
|
+
|
|
97
|
+
# Compare two bundle reports: total size, added/removed/changed modules and packages,
|
|
98
|
+
# version changes, new duplicates and new deprecated packages
|
|
99
|
+
npx react-native-bundle-discovery-cli compare --before main-stats.json --after pr-stats.json [--limit 50] [--format json|markdown|default]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
##### Bundle size checks in CI
|
|
103
|
+
|
|
104
|
+
`compare` exits with code `1` when one of the checks fails:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# --fail-on-increase: max growth, in bytes (50KB, 0.5MB, 51200) or percent of the "before" size (5%)
|
|
108
|
+
# --max-size: absolute limit for the "after" bundle
|
|
109
|
+
# --fail-on: fail when new duplicate or new deprecated packages appear
|
|
110
|
+
npx react-native-bundle-discovery-cli compare --before main-stats.json --after pr-stats.json \
|
|
111
|
+
--fail-on-increase 50KB \
|
|
112
|
+
--max-size 3MB \
|
|
113
|
+
--fail-on new-duplicates,new-deprecated
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
On GitHub Actions no extra setup is needed: the markdown report is added to the job summary
|
|
117
|
+
and failed checks are shown as error annotations. Use `--format markdown` to get the same report for a PR comment.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
- run: npx react-native-bundle-discovery-cli compare --before main-stats.json --after metro-stats.json --fail-on-increase 5%
|
|
93
121
|
```
|
|
94
122
|
|
|
95
123
|
#### UI package
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const { writeFileSync, existsSync } = require("fs");
|
|
|
3
3
|
const { Buffer } = require("buffer");
|
|
4
4
|
const chalk = require("chalk");
|
|
5
5
|
const BundleDiscoveryPlugin = require("./webpack").BundleDiscoveryPlugin;
|
|
6
|
+
const { withPackagesMetadata } = require("./packageMetadata");
|
|
6
7
|
const NAME = require("./package.json").name;
|
|
7
8
|
|
|
8
9
|
function getDefault(module) {
|
|
@@ -106,7 +107,7 @@ function toModuleStruct(m, includeCode) {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
function createJsonReport({
|
|
110
|
+
async function createJsonReport({
|
|
110
111
|
graph,
|
|
111
112
|
entryPoint,
|
|
112
113
|
includeEnvs,
|
|
@@ -116,12 +117,30 @@ function createJsonReport({
|
|
|
116
117
|
rootFolder,
|
|
117
118
|
silent,
|
|
118
119
|
options,
|
|
120
|
+
fetchPackagesMetadata,
|
|
119
121
|
}) {
|
|
120
122
|
const { processModuleFilter = () => true } = options || {};
|
|
121
123
|
const dependencies = Array.from(graph.dependencies.values()).filter(
|
|
122
124
|
processModuleFilter,
|
|
123
125
|
);
|
|
124
126
|
|
|
127
|
+
let packages = toPackages(preModules).concat(toPackages(dependencies));
|
|
128
|
+
const modules = preModules
|
|
129
|
+
.map((m) => toModuleStruct(m, includeCode))
|
|
130
|
+
.concat(dependencies.map((m) => toModuleStruct(m, includeCode)));
|
|
131
|
+
|
|
132
|
+
if (fetchPackagesMetadata) {
|
|
133
|
+
packages = await withPackagesMetadata(packages, {
|
|
134
|
+
onError: (error, pkg) => {
|
|
135
|
+
if (!silent) {
|
|
136
|
+
console.warn(
|
|
137
|
+
`${chalk.yellow(`[${NAME}]`)}: Failed to fetch metadata for ${pkg.name}@${pkg.version}: ${error.message}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
const stats = {
|
|
126
145
|
date: Date.now(),
|
|
127
146
|
entryPoint,
|
|
@@ -131,10 +150,8 @@ function createJsonReport({
|
|
|
131
150
|
return acc;
|
|
132
151
|
}, {}),
|
|
133
152
|
rootFolder,
|
|
134
|
-
packages
|
|
135
|
-
modules
|
|
136
|
-
.map((m) => toModuleStruct(m, includeCode))
|
|
137
|
-
.concat(dependencies.map((m) => toModuleStruct(m, includeCode))),
|
|
153
|
+
packages,
|
|
154
|
+
modules,
|
|
138
155
|
};
|
|
139
156
|
|
|
140
157
|
writeFileSync(outputJsonPath, JSON.stringify(stats));
|
|
@@ -156,6 +173,7 @@ function createJsonReport({
|
|
|
156
173
|
* @param {string} [options.outputJsonPath] - The path where the JSON report will be saved. Defaults to "metro-stats.json" in the project root.
|
|
157
174
|
* @param {boolean} [options.includeCode=true] - Whether to include the source and output code in the JSON report.
|
|
158
175
|
* @param {string[]} [options.includeEnvs=[]] - A list of environment variable names to include in the JSON report.
|
|
176
|
+
* @param {boolean} [options.fetchPackagesMetadata=true] - Whether to fetch packages metadata (publish date, deprecation, latest version) from the npm registry.
|
|
159
177
|
* @returns {Function} - A custom serializer function to be used by Metro.
|
|
160
178
|
* @throws {Error} - Throws an error if the project root does not exist.
|
|
161
179
|
*/
|
|
@@ -166,6 +184,7 @@ function createSerializer({
|
|
|
166
184
|
includeCode = true,
|
|
167
185
|
silent = false,
|
|
168
186
|
includeEnvs = [],
|
|
187
|
+
fetchPackagesMetadata = true,
|
|
169
188
|
} = {}) {
|
|
170
189
|
const mySerializer = serializer || getDefaultSerializer();
|
|
171
190
|
|
|
@@ -179,6 +198,12 @@ function createSerializer({
|
|
|
179
198
|
function customSerializer(entryPoint, preModules, graph, options) {
|
|
180
199
|
const code = mySerializer(entryPoint, preModules, graph, options);
|
|
181
200
|
|
|
201
|
+
// Keeps the Node.js process alive until the report is written
|
|
202
|
+
// (otherwise it can exit while metadata requests are still pending)
|
|
203
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
204
|
+
|
|
205
|
+
// Graph/modules are read synchronously before the first `await`,
|
|
206
|
+
// so the report is not affected by later Metro graph mutations
|
|
182
207
|
createJsonReport({
|
|
183
208
|
graph,
|
|
184
209
|
entryPoint,
|
|
@@ -189,7 +214,8 @@ function createSerializer({
|
|
|
189
214
|
rootFolder: projectRoot,
|
|
190
215
|
silent,
|
|
191
216
|
options,
|
|
192
|
-
|
|
217
|
+
fetchPackagesMetadata,
|
|
218
|
+
}).finally(() => clearInterval(keepAlive));
|
|
193
219
|
|
|
194
220
|
return code;
|
|
195
221
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 1_000;
|
|
3
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
4
|
+
|
|
5
|
+
// Simple in-memory cache: `${registry}|${name}` -> Promise<packument summary | null>
|
|
6
|
+
// Keeps promises (not values) so concurrent lookups of the same package share one request,
|
|
7
|
+
// and different versions of the same package share one packument.
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `@babel/runtime` -> `@babel%2fruntime`
|
|
12
|
+
*/
|
|
13
|
+
function encodePackageName(name) {
|
|
14
|
+
return name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fetchJson(url, timeoutMs) {
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
headers: { Accept: "application/json" },
|
|
20
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
21
|
+
});
|
|
22
|
+
if (res.status === 404) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(`${res.status} ${res.statusText} (${url})`);
|
|
27
|
+
}
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The version manifest (`/<name>/<version>`) has no publish date,
|
|
33
|
+
* so the packument is fetched once and only the fields we need are kept.
|
|
34
|
+
*/
|
|
35
|
+
async function fetchPackageSummary(name, { registry, timeoutMs }) {
|
|
36
|
+
const packument = await fetchJson(
|
|
37
|
+
`${registry}/${encodePackageName(name)}`,
|
|
38
|
+
timeoutMs,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!packument) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const versions = {};
|
|
46
|
+
for (const [version, manifest] of Object.entries(packument.versions ?? {})) {
|
|
47
|
+
versions[version] = {
|
|
48
|
+
createdAt: packument.time?.[version] ?? null,
|
|
49
|
+
deprecated: manifest.deprecated || false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
latestVersion: packument["dist-tags"]?.latest ?? null,
|
|
55
|
+
versions,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getPackageSummary(name, { registry, timeoutMs }) {
|
|
60
|
+
const key = `${registry}|${name}`;
|
|
61
|
+
|
|
62
|
+
if (!cache.has(key)) {
|
|
63
|
+
const promise = fetchPackageSummary(name, { registry, timeoutMs }).catch(
|
|
64
|
+
(error) => {
|
|
65
|
+
// Don't cache failures (network issues, timeouts), retry on next report
|
|
66
|
+
cache.delete(key);
|
|
67
|
+
throw error;
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
cache.set(key, promise);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return cache.get(key);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns metadata of a specific (installed) package version, not the latest one.
|
|
78
|
+
*
|
|
79
|
+
* @returns {Promise<{createdAt: string|null, deprecated: string|false, isLatest: boolean, latestVersion: string|null} | null>}
|
|
80
|
+
* `null` when the package/version is not found in the registry.
|
|
81
|
+
*/
|
|
82
|
+
async function getPackageMetadata(
|
|
83
|
+
name,
|
|
84
|
+
version,
|
|
85
|
+
{ registry = DEFAULT_REGISTRY, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
86
|
+
) {
|
|
87
|
+
const summary = await getPackageSummary(name, { registry, timeoutMs });
|
|
88
|
+
const info = summary?.versions[version];
|
|
89
|
+
|
|
90
|
+
if (!info) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
createdAt: info.createdAt,
|
|
96
|
+
deprecated: info.deprecated,
|
|
97
|
+
isLatest: summary.latestVersion === version,
|
|
98
|
+
latestVersion: summary.latestVersion,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
103
|
+
const results = new Array(items.length);
|
|
104
|
+
let index = 0;
|
|
105
|
+
|
|
106
|
+
async function worker() {
|
|
107
|
+
while (index < items.length) {
|
|
108
|
+
const i = index++;
|
|
109
|
+
results[i] = await fn(items[i], i);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await Promise.all(
|
|
114
|
+
Array.from({ length: Math.min(limit, items.length) }, worker),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Adds a `metadata` field to every package of the report.
|
|
122
|
+
* Failed lookups result in `metadata: null` and are passed to `onError`.
|
|
123
|
+
*
|
|
124
|
+
* @param {Array<{name: string, version: string}>} packages
|
|
125
|
+
*/
|
|
126
|
+
async function withPackagesMetadata(
|
|
127
|
+
packages,
|
|
128
|
+
{
|
|
129
|
+
registry = DEFAULT_REGISTRY,
|
|
130
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
131
|
+
concurrency = DEFAULT_CONCURRENCY,
|
|
132
|
+
onError = () => {},
|
|
133
|
+
} = {},
|
|
134
|
+
) {
|
|
135
|
+
return mapWithConcurrency(packages, concurrency, async (pkg) => {
|
|
136
|
+
if (!pkg.version) {
|
|
137
|
+
return { ...pkg, metadata: null };
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const metadata = await getPackageMetadata(pkg.name, pkg.version, {
|
|
141
|
+
registry,
|
|
142
|
+
timeoutMs,
|
|
143
|
+
});
|
|
144
|
+
return { ...pkg, metadata };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
onError(error, pkg);
|
|
147
|
+
return { ...pkg, metadata: null };
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
getPackageMetadata,
|
|
154
|
+
withPackagesMetadata,
|
|
155
|
+
};
|
package/webpack.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require("node:path");
|
|
|
4
4
|
const chalk = require("chalk");
|
|
5
5
|
|
|
6
6
|
const { parseBundle } = require("./parseUtils.js");
|
|
7
|
+
const { withPackagesMetadata } = require("./packageMetadata.js");
|
|
7
8
|
const NAME = require("./package.json").name;
|
|
8
9
|
const packageRegex = /(?:^|\/)node_modules\/((?:@[^\/]+\/)?[^\/]+)/g;
|
|
9
10
|
|
|
@@ -13,10 +14,11 @@ const packageRegex = /(?:^|\/)node_modules\/((?:@[^\/]+\/)?[^\/]+)/g;
|
|
|
13
14
|
* to metro-stats.json format used by `react-native-bundle-discovery`
|
|
14
15
|
*/
|
|
15
16
|
class BundleDiscoveryPlugin {
|
|
16
|
-
constructor({ filename, options, enabled } = {}) {
|
|
17
|
+
constructor({ filename, options, enabled, fetchPackagesMetadata } = {}) {
|
|
17
18
|
this.options = options;
|
|
18
19
|
this.filename = filename || "metro-stats.json";
|
|
19
20
|
this.enabled = enabled ?? true;
|
|
21
|
+
this.fetchPackagesMetadata = fetchPackagesMetadata ?? true;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
extractSizesFromJsBundle(statAsset, options) {
|
|
@@ -59,16 +61,39 @@ class BundleDiscoveryPlugin {
|
|
|
59
61
|
);
|
|
60
62
|
// Write the formatted stats to the specified output file
|
|
61
63
|
const outputPath = path.resolve(compiler.options.context, this.filename);
|
|
62
|
-
fs.writeFileSync(outputPath, JSON.stringify(statsJson, null, 2));
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
65
|
+
// Keeps the Node.js process alive until the report is written
|
|
66
|
+
// (otherwise it can exit while metadata requests are still pending)
|
|
67
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
68
|
+
|
|
69
|
+
this.writeReport(statsJson, outputPath)
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
console.error(
|
|
72
|
+
`${chalk.yellow(`[${NAME}]`)}: Failed to create JSON report: ${error.message}`,
|
|
73
|
+
);
|
|
74
|
+
})
|
|
75
|
+
.finally(() => clearInterval(keepAlive));
|
|
69
76
|
});
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
async writeReport(statsJson, outputPath) {
|
|
80
|
+
if (this.fetchPackagesMetadata) {
|
|
81
|
+
statsJson.packages = await withPackagesMetadata(statsJson.packages, {
|
|
82
|
+
onError: (error, pkg) => {
|
|
83
|
+
console.warn(
|
|
84
|
+
`${chalk.yellow(`[${NAME}]`)}: Failed to fetch metadata for ${pkg.name}@${pkg.version}: ${error.message}`,
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(outputPath, JSON.stringify(statsJson, null, 2));
|
|
91
|
+
|
|
92
|
+
console.log(
|
|
93
|
+
`${chalk.yellow(`[${NAME}]`)}: Saved stats to ${chalk.green(outputPath)}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
readSource(filePath) {
|
|
73
98
|
const fallback = { code: "", lineCount: 0, sizeInBytes: 0 };
|
|
74
99
|
try {
|
|
@@ -93,8 +118,9 @@ class BundleDiscoveryPlugin {
|
|
|
93
118
|
|
|
94
119
|
const lastPnpmFolder = matches[matches.length - 1][1];
|
|
95
120
|
|
|
96
|
-
//
|
|
97
|
-
|
|
121
|
+
// Folder format is `<name>@<version>[_<peers>]`, where `<name>` may be scoped
|
|
122
|
+
// (`@scope+name`, `@scope_name`), so skip the leading `@` of the scope
|
|
123
|
+
const versionMatch = lastPnpmFolder.match(/^@?[^@]+@([^_]+)/);
|
|
98
124
|
|
|
99
125
|
return versionMatch ? versionMatch[1] : null;
|
|
100
126
|
} catch {
|