react-native-bundle-discovery 2.5.0 → 2.7.0-rc.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/README.md +178 -103
- package/dist/index.d.ts +262 -0
- package/dist/index.js +731 -0
- package/package.json +21 -10
- package/index.js +0 -301
- package/packageMetadata.js +0 -155
- package/parseUtils.js +0 -615
- package/webpack.js +0 -373
package/dist/index.js
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_buffer = require("node:buffer");
|
|
25
|
+
let node_fs = require("node:fs");
|
|
26
|
+
node_fs = __toESM(node_fs);
|
|
27
|
+
let node_path = require("node:path");
|
|
28
|
+
node_path = __toESM(node_path);
|
|
29
|
+
let chalk = require("chalk");
|
|
30
|
+
chalk = __toESM(chalk);
|
|
31
|
+
let acorn = require("acorn");
|
|
32
|
+
acorn = __toESM(acorn);
|
|
33
|
+
let acorn_walk = require("acorn-walk");
|
|
34
|
+
acorn_walk = __toESM(acorn_walk);
|
|
35
|
+
//#region package.json
|
|
36
|
+
var name = "react-native-bundle-discovery";
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/packageMetadata.ts
|
|
39
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
41
|
+
const DEFAULT_CONCURRENCY = 5;
|
|
42
|
+
const cache = /* @__PURE__ */ new Map();
|
|
43
|
+
/**
|
|
44
|
+
* `@babel/runtime` -> `@babel%2fruntime`
|
|
45
|
+
*/
|
|
46
|
+
function encodePackageName(name) {
|
|
47
|
+
return name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : name;
|
|
48
|
+
}
|
|
49
|
+
async function fetchJson(url, timeoutMs) {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
headers: { Accept: "application/json" },
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
53
|
+
});
|
|
54
|
+
if (res.status === 404) return null;
|
|
55
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText} (${url})`);
|
|
56
|
+
return res.json();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The version manifest (`/<name>/<version>`) has no publish date,
|
|
60
|
+
* so the packument is fetched once and only the fields we need are kept.
|
|
61
|
+
*/
|
|
62
|
+
async function fetchPackageSummary(name, { registry, timeoutMs }) {
|
|
63
|
+
const packument = await fetchJson(`${registry}/${encodePackageName(name)}`, timeoutMs);
|
|
64
|
+
if (!packument) return null;
|
|
65
|
+
const versions = {};
|
|
66
|
+
for (const [version, manifest] of Object.entries(packument.versions ?? {})) versions[version] = {
|
|
67
|
+
createdAt: packument.time?.[version] ?? null,
|
|
68
|
+
deprecated: manifest.deprecated || false
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
latestVersion: packument["dist-tags"]?.latest ?? null,
|
|
72
|
+
versions
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function getPackageSummary(name, { registry, timeoutMs }) {
|
|
76
|
+
const key = `${registry}|${name}`;
|
|
77
|
+
let promise = cache.get(key);
|
|
78
|
+
if (!promise) {
|
|
79
|
+
promise = fetchPackageSummary(name, {
|
|
80
|
+
registry,
|
|
81
|
+
timeoutMs
|
|
82
|
+
}).catch((error) => {
|
|
83
|
+
cache.delete(key);
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
cache.set(key, promise);
|
|
87
|
+
}
|
|
88
|
+
return promise;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns metadata of a specific (installed) package version, not the latest one.
|
|
92
|
+
*
|
|
93
|
+
* @returns `null` when the package/version is not found in the registry.
|
|
94
|
+
*/
|
|
95
|
+
async function getPackageMetadata(name, version, { registry = DEFAULT_REGISTRY, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
96
|
+
const summary = await getPackageSummary(name, {
|
|
97
|
+
registry,
|
|
98
|
+
timeoutMs
|
|
99
|
+
});
|
|
100
|
+
const info = summary?.versions[version];
|
|
101
|
+
if (!summary || !info) return null;
|
|
102
|
+
return {
|
|
103
|
+
createdAt: info.createdAt,
|
|
104
|
+
deprecated: info.deprecated,
|
|
105
|
+
isLatest: summary.latestVersion === version,
|
|
106
|
+
latestVersion: summary.latestVersion
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
110
|
+
const results = new Array(items.length);
|
|
111
|
+
let index = 0;
|
|
112
|
+
async function worker() {
|
|
113
|
+
while (index < items.length) {
|
|
114
|
+
const i = index++;
|
|
115
|
+
results[i] = await fn(items[i], i);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Adds a `metadata` field to every package of the report.
|
|
123
|
+
* Failed lookups result in `metadata: null` and are passed to `onError`.
|
|
124
|
+
*/
|
|
125
|
+
async function withPackagesMetadata(packages, { registry = DEFAULT_REGISTRY, timeoutMs = DEFAULT_TIMEOUT_MS, concurrency = DEFAULT_CONCURRENCY, onError = () => {} } = {}) {
|
|
126
|
+
return mapWithConcurrency(packages, concurrency, async (pkg) => {
|
|
127
|
+
if (!pkg.version) return {
|
|
128
|
+
...pkg,
|
|
129
|
+
metadata: null
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
const metadata = await getPackageMetadata(pkg.name, pkg.version, {
|
|
133
|
+
registry,
|
|
134
|
+
timeoutMs
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
...pkg,
|
|
138
|
+
metadata
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
onError(error, pkg);
|
|
142
|
+
return {
|
|
143
|
+
...pkg,
|
|
144
|
+
metadata: null
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/parseUtils.ts
|
|
151
|
+
function isNumericId(node) {
|
|
152
|
+
return node.type === "Literal" && node.value !== null && node.value !== void 0 && Number.isInteger(node.value) && node.value >= 0;
|
|
153
|
+
}
|
|
154
|
+
function isModuleId(node) {
|
|
155
|
+
return node !== null && node.type === "Literal" && (isNumericId(node) || typeof node.value === "string");
|
|
156
|
+
}
|
|
157
|
+
function isModuleWrapper(node) {
|
|
158
|
+
return (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && !node.id || isModuleId(node) || node.type === "ArrayExpression" && node.elements.length > 1 && isModuleId(node.elements[0]);
|
|
159
|
+
}
|
|
160
|
+
function isModulesHash(node) {
|
|
161
|
+
return node !== null && node.type === "ObjectExpression" && node.properties.filter((property) => property.type !== "SpreadElement").map((node) => node.value).every(isModuleWrapper);
|
|
162
|
+
}
|
|
163
|
+
function isModulesArray(node) {
|
|
164
|
+
return node !== null && node.type === "ArrayExpression" && node.elements.every((elem) => !elem || isModuleWrapper(elem));
|
|
165
|
+
}
|
|
166
|
+
function isSimpleModulesList(node) {
|
|
167
|
+
return isModulesHash(node) || isModulesArray(node);
|
|
168
|
+
}
|
|
169
|
+
function isOptimizedModulesArray(node) {
|
|
170
|
+
return node !== null && node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression" && node.callee.object.callee.type === "Identifier" && node.callee.object.callee.name === "Array" && node.callee.object.arguments.length === 1 && node.callee.object.arguments[0].type !== "SpreadElement" && isNumericId(node.callee.object.arguments[0]) && node.callee.property.type === "Identifier" && node.callee.property.name === "concat" && node.arguments.length === 1 && isModulesArray(node.arguments[0]);
|
|
171
|
+
}
|
|
172
|
+
function isModulesList(node) {
|
|
173
|
+
return isSimpleModulesList(node) || isOptimizedModulesArray(node);
|
|
174
|
+
}
|
|
175
|
+
function getModuleLocation(node) {
|
|
176
|
+
return {
|
|
177
|
+
start: node.start,
|
|
178
|
+
end: node.end
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function getModulesLocations(node) {
|
|
182
|
+
if (node.type === "ObjectExpression") return node.properties.reduce((result, moduleNode) => {
|
|
183
|
+
if (moduleNode.type !== "Property") return result;
|
|
184
|
+
const moduleId = moduleNode.key.type === "Identifier" ? moduleNode.key.name : moduleNode.key.value;
|
|
185
|
+
if (moduleId === "undefined") return result;
|
|
186
|
+
result[moduleId] = getModuleLocation(moduleNode.value);
|
|
187
|
+
return result;
|
|
188
|
+
}, {});
|
|
189
|
+
const isOptimizedArray = node.type === "CallExpression";
|
|
190
|
+
if (node.type === "ArrayExpression" || isOptimizedArray) {
|
|
191
|
+
const minId = isOptimizedArray && node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression" && node.callee.object.arguments[0].type === "Literal" ? node.callee.object.arguments[0].value : 0;
|
|
192
|
+
return (isOptimizedArray ? node.arguments[0].type === "ArrayExpression" ? node.arguments[0].elements : [] : node.elements).reduce((result, moduleNode, i) => {
|
|
193
|
+
if (moduleNode) result[i + minId] = getModuleLocation(moduleNode);
|
|
194
|
+
return result;
|
|
195
|
+
}, {});
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
function getIIFECallExpression(node) {
|
|
200
|
+
if (node.expression.type === "CallExpression") return node.expression;
|
|
201
|
+
if (node.expression.type === "UnaryExpression" && node.expression.argument.type === "CallExpression") return node.expression.argument;
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
function callsModulesMap(node, modulesVariableName) {
|
|
205
|
+
let callsModule = false;
|
|
206
|
+
acorn_walk.simple(node, { CallExpression(callExpression) {
|
|
207
|
+
const { callee } = callExpression;
|
|
208
|
+
if (callee.type === "MemberExpression" && (callee.object.type === "Identifier" && callee.object.name === modulesVariableName || callee.object.type === "MemberExpression" && callee.object.object.type === "Identifier" && callee.object.object.name === modulesVariableName)) callsModule = true;
|
|
209
|
+
} });
|
|
210
|
+
return callsModule;
|
|
211
|
+
}
|
|
212
|
+
function hasWebpackModulesRuntime(body, modulesDeclaration, modulesVariableName) {
|
|
213
|
+
const declarationIndex = body.body.indexOf(modulesDeclaration);
|
|
214
|
+
return body.body.slice(declarationIndex + 1).some((statement) => callsModulesMap(statement, modulesVariableName));
|
|
215
|
+
}
|
|
216
|
+
function getWebpack5IIFEModulesCandidate(node, isTopLevel) {
|
|
217
|
+
if (node.arguments.length !== 0 || node.callee.type !== "FunctionExpression" && node.callee.type !== "ArrowFunctionExpression" || node.callee.params.length !== 0 || node.callee.body.type !== "BlockStatement") return null;
|
|
218
|
+
const firstVariableDeclaration = node.callee.body.body.find((node) => node.type === "VariableDeclaration");
|
|
219
|
+
if (firstVariableDeclaration) {
|
|
220
|
+
for (const declaration of firstVariableDeclaration.declarations) if (declaration.init && isModulesList(declaration.init)) {
|
|
221
|
+
const locations = getModulesLocations(declaration.init);
|
|
222
|
+
if (Object.keys(locations).length === 0) continue;
|
|
223
|
+
return {
|
|
224
|
+
locations,
|
|
225
|
+
hasWebpackRuntime: declaration.id.type === "Identifier" && hasWebpackModulesRuntime(node.callee.body, firstVariableDeclaration, declaration.id.name),
|
|
226
|
+
isTopLevel
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function selectWebpack5IIFECandidate(candidates) {
|
|
233
|
+
return candidates.find((candidate) => candidate.hasWebpackRuntime && candidate.isTopLevel) || candidates.find((candidate) => candidate.hasWebpackRuntime) || candidates.find((candidate) => candidate.isTopLevel) || null;
|
|
234
|
+
}
|
|
235
|
+
function selectWebpack5IIFEModulesLocations(candidates, expectedModuleIds) {
|
|
236
|
+
if (candidates.length === 0) return null;
|
|
237
|
+
if (expectedModuleIds?.length) {
|
|
238
|
+
const expectedIds = new Set(expectedModuleIds.map(String));
|
|
239
|
+
const rankedCandidates = candidates.map((candidate) => ({
|
|
240
|
+
candidate,
|
|
241
|
+
expectedIdsIntersection: Object.keys(candidate.locations).filter((moduleId) => expectedIds.has(moduleId)).length
|
|
242
|
+
})).toSorted((candidateA, candidateB) => candidateB.expectedIdsIntersection - candidateA.expectedIdsIntersection);
|
|
243
|
+
if (rankedCandidates[0].expectedIdsIntersection > 0) return selectWebpack5IIFECandidate(rankedCandidates.filter((rankedCandidate) => rankedCandidate.expectedIdsIntersection === rankedCandidates[0].expectedIdsIntersection).map((rankedCandidate) => rankedCandidate.candidate))?.locations || null;
|
|
244
|
+
}
|
|
245
|
+
return selectWebpack5IIFECandidate(candidates)?.locations || null;
|
|
246
|
+
}
|
|
247
|
+
function isChunkIds(node) {
|
|
248
|
+
return node.type === "ArrayExpression" && node.elements.every(isModuleId);
|
|
249
|
+
}
|
|
250
|
+
function mayBeAsyncChunkArguments(args) {
|
|
251
|
+
return args.length >= 2 && args[0] !== null && args[0].type !== "SpreadElement" && isChunkIds(args[0]);
|
|
252
|
+
}
|
|
253
|
+
/** Returns bundle source except modules */
|
|
254
|
+
function getBundleRuntime(content, modulesLocations) {
|
|
255
|
+
const sortedLocations = Object.values(modulesLocations || {}).toSorted((a, b) => a.start - b.start);
|
|
256
|
+
let result = "";
|
|
257
|
+
let lastIndex = 0;
|
|
258
|
+
for (const { start, end } of sortedLocations) {
|
|
259
|
+
result += content.slice(lastIndex, start);
|
|
260
|
+
lastIndex = end;
|
|
261
|
+
}
|
|
262
|
+
return result + content.slice(lastIndex);
|
|
263
|
+
}
|
|
264
|
+
function isAsyncChunkPushExpression(node) {
|
|
265
|
+
const { callee, arguments: args } = node;
|
|
266
|
+
return callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "push" && callee.object.type === "AssignmentExpression" && args.length === 1 && args[0].type === "ArrayExpression" && mayBeAsyncChunkArguments(args[0].elements) && isModulesList(args[0].elements[1]);
|
|
267
|
+
}
|
|
268
|
+
function isAsyncWebWorkerChunkExpression(node) {
|
|
269
|
+
const { callee, type, arguments: args } = node;
|
|
270
|
+
return type === "CallExpression" && callee.type === "MemberExpression" && args.length === 2 && args[0].type !== "SpreadElement" && isChunkIds(args[0]) && isModulesList(args[1]);
|
|
271
|
+
}
|
|
272
|
+
function parseBundle(bundlePath, opts) {
|
|
273
|
+
const { sourceType = "script", expectedModuleIds } = opts || {};
|
|
274
|
+
const content = node_fs.default.readFileSync(bundlePath, "utf8");
|
|
275
|
+
const ast = acorn.parse(content, {
|
|
276
|
+
sourceType,
|
|
277
|
+
ecmaVersion: "latest"
|
|
278
|
+
});
|
|
279
|
+
const topLevelIIFECalls = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const node of ast.body) if (node.type === "ExpressionStatement") {
|
|
281
|
+
const iifeCall = getIIFECallExpression(node);
|
|
282
|
+
if (iifeCall) topLevelIIFECalls.add(iifeCall);
|
|
283
|
+
}
|
|
284
|
+
const walkState = {
|
|
285
|
+
locations: null,
|
|
286
|
+
webpack5IIFECandidates: []
|
|
287
|
+
};
|
|
288
|
+
acorn_walk.recursive(ast, walkState, {
|
|
289
|
+
AssignmentExpression(node, state) {
|
|
290
|
+
if (state.locations) return;
|
|
291
|
+
const { left, right } = node;
|
|
292
|
+
if (left && left.type === "MemberExpression" && left.object && left.object.type === "Identifier" && left.object.name === "exports" && left.property && left.property.type === "Identifier" && left.property.name === "modules" && isModulesHash(right)) state.locations = getModulesLocations(right);
|
|
293
|
+
},
|
|
294
|
+
CallExpression(node, state, callback) {
|
|
295
|
+
if (state.locations) return;
|
|
296
|
+
const args = node.arguments;
|
|
297
|
+
const webpack5IIFEModulesCandidate = getWebpack5IIFEModulesCandidate(node, topLevelIIFECalls.has(node));
|
|
298
|
+
if (webpack5IIFEModulesCandidate) state.webpack5IIFECandidates.push(webpack5IIFEModulesCandidate);
|
|
299
|
+
if (node.callee.type === "FunctionExpression" && !node.callee.id && args.length === 1 && isSimpleModulesList(args[0])) {
|
|
300
|
+
state.locations = getModulesLocations(args[0]);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// webpackJsonp([<chunks>], <modules>, ...)
|
|
304
|
+
if (node.callee.type === "Identifier" && mayBeAsyncChunkArguments(args) && args[1].type !== "SpreadElement" && isModulesList(args[1])) {
|
|
305
|
+
state.locations = getModulesLocations(args[1]);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (isAsyncChunkPushExpression(node) && args[0].type === "ArrayExpression" && args[0].elements[1]) {
|
|
309
|
+
state.locations = getModulesLocations(args[0].elements[1]);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (isAsyncWebWorkerChunkExpression(node)) {
|
|
313
|
+
state.locations = getModulesLocations(args[1]);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
for (const arg of args) callback(arg, state);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
const modulesLocations = walkState.locations || selectWebpack5IIFEModulesLocations(walkState.webpack5IIFECandidates, expectedModuleIds);
|
|
320
|
+
const modules = {};
|
|
321
|
+
if (modulesLocations) for (const [id, loc] of Object.entries(modulesLocations)) modules[id] = content.slice(loc.start, loc.end);
|
|
322
|
+
return {
|
|
323
|
+
modules,
|
|
324
|
+
src: content,
|
|
325
|
+
runtimeSrc: getBundleRuntime(content, modulesLocations)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/webpack.ts
|
|
330
|
+
const NAME$1 = name;
|
|
331
|
+
const packageRegex = /(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)/g;
|
|
332
|
+
/**
|
|
333
|
+
* Simple Webpack/Rspack plugin
|
|
334
|
+
* to adapt stats object https://webpack.js.org/api/stats/
|
|
335
|
+
* to metro-stats.json format used by `react-native-bundle-discovery`
|
|
336
|
+
*/
|
|
337
|
+
var BundleDiscoveryPlugin = class {
|
|
338
|
+
options;
|
|
339
|
+
filename;
|
|
340
|
+
enabled;
|
|
341
|
+
fetchPackagesMetadata;
|
|
342
|
+
constructor({ filename, options, enabled, fetchPackagesMetadata } = {}) {
|
|
343
|
+
this.options = options;
|
|
344
|
+
this.filename = filename || "metro-stats.json";
|
|
345
|
+
this.enabled = enabled ?? true;
|
|
346
|
+
this.fetchPackagesMetadata = fetchPackagesMetadata ?? true;
|
|
347
|
+
}
|
|
348
|
+
extractSizesFromJsBundle(statAsset, options) {
|
|
349
|
+
const assetFile = node_path.default.join(options.output.path, statAsset.name);
|
|
350
|
+
try {
|
|
351
|
+
return parseBundle(assetFile, { sourceType: statAsset.info.javascriptModule ? "module" : "script" });
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
apply(compiler) {
|
|
357
|
+
if (!this.enabled) return;
|
|
358
|
+
compiler.hooks.done.tap("GenerateStatsJsonPlugin", (stats) => {
|
|
359
|
+
const json = stats.toJson({
|
|
360
|
+
version: true,
|
|
361
|
+
builtAt: true,
|
|
362
|
+
modules: true,
|
|
363
|
+
reasons: true,
|
|
364
|
+
assets: true,
|
|
365
|
+
ids: true,
|
|
366
|
+
...this.options,
|
|
367
|
+
all: false,
|
|
368
|
+
source: true
|
|
369
|
+
});
|
|
370
|
+
const statsJson = this.formatStat(json, this.extractSizesFromJsBundle(json.assets[0], compiler.options), compiler.options);
|
|
371
|
+
const outputPath = node_path.default.resolve(compiler.options.context, this.filename);
|
|
372
|
+
const keepAlive = setInterval(() => {}, 1e3);
|
|
373
|
+
this.writeReport(statsJson, outputPath).catch((error) => {
|
|
374
|
+
console.error(`${chalk.default.yellow(`[${NAME$1}]`)}: Failed to create JSON report: ${error.message}`);
|
|
375
|
+
}).finally(() => clearInterval(keepAlive));
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async writeReport(statsJson, outputPath) {
|
|
379
|
+
if (this.fetchPackagesMetadata) statsJson.packages = await withPackagesMetadata(statsJson.packages, { onError: (error, pkg) => {
|
|
380
|
+
console.warn(`${chalk.default.yellow(`[${NAME$1}]`)}: Failed to fetch metadata for ${pkg.name}@${pkg.version}: ${error.message}`);
|
|
381
|
+
} });
|
|
382
|
+
node_fs.default.writeFileSync(outputPath, JSON.stringify(statsJson, null, 2));
|
|
383
|
+
console.log(`${chalk.default.yellow(`[${NAME$1}]`)}: Saved stats to ${chalk.default.green(outputPath)}`);
|
|
384
|
+
}
|
|
385
|
+
readSource(filePath) {
|
|
386
|
+
const fallback = {
|
|
387
|
+
code: "",
|
|
388
|
+
lineCount: 0,
|
|
389
|
+
sizeInBytes: 0
|
|
390
|
+
};
|
|
391
|
+
try {
|
|
392
|
+
const code = node_fs.default.readFileSync(filePath, "utf-8");
|
|
393
|
+
const lineCount = code.split("\n").length;
|
|
394
|
+
const sizeInBytes = Buffer.byteLength(code, "utf-8");
|
|
395
|
+
return {
|
|
396
|
+
code: this.options?.source === false ? "" : code,
|
|
397
|
+
lineCount,
|
|
398
|
+
sizeInBytes
|
|
399
|
+
};
|
|
400
|
+
} catch {
|
|
401
|
+
return fallback;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
getPkgVersionFromPath(path) {
|
|
405
|
+
try {
|
|
406
|
+
const matches = [...path.matchAll(/\.pnpm\/([^/]+)\/node_modules/g)];
|
|
407
|
+
if (!matches.length) return null;
|
|
408
|
+
const versionMatch = matches[matches.length - 1][1].match(/^@?[^@]+@([^_]+)/);
|
|
409
|
+
return versionMatch ? versionMatch[1] : null;
|
|
410
|
+
} catch {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
getPackages(statsJson) {
|
|
415
|
+
const packagesMap = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const mdl of statsJson.modules) {
|
|
417
|
+
const filePath = mdl.nameForCondition;
|
|
418
|
+
if (!filePath) continue;
|
|
419
|
+
let lastMatch = null;
|
|
420
|
+
packageRegex.lastIndex = 0;
|
|
421
|
+
let match = packageRegex.exec(filePath);
|
|
422
|
+
while (match !== null) {
|
|
423
|
+
lastMatch = match;
|
|
424
|
+
match = packageRegex.exec(filePath);
|
|
425
|
+
}
|
|
426
|
+
if (!lastMatch) continue;
|
|
427
|
+
const packageName = lastMatch[1];
|
|
428
|
+
const matchIndex = lastMatch.index;
|
|
429
|
+
const cutIndex = matchIndex + (filePath[matchIndex] === "/" ? 1 : 0) + 13 + packageName.length;
|
|
430
|
+
const absolutePath = filePath.slice(0, cutIndex);
|
|
431
|
+
let version = "";
|
|
432
|
+
try {
|
|
433
|
+
version = require(node_path.default.join(absolutePath, "package.json")).version;
|
|
434
|
+
} catch (e) {
|
|
435
|
+
const verFromPath = this.getPkgVersionFromPath(absolutePath);
|
|
436
|
+
if (verFromPath) version = verFromPath;
|
|
437
|
+
else if (e?.code === "MODULE_NOT_FOUND") console.warn(`[${NAME$1}] getPackages: unexpected path resolution`, {
|
|
438
|
+
filePath,
|
|
439
|
+
absolutePath
|
|
440
|
+
});
|
|
441
|
+
else throw e;
|
|
442
|
+
}
|
|
443
|
+
if (!packagesMap.has(absolutePath)) packagesMap.set(absolutePath, {
|
|
444
|
+
name: packageName,
|
|
445
|
+
absolutePath,
|
|
446
|
+
version
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return Array.from(packagesMap.values());
|
|
450
|
+
}
|
|
451
|
+
getEntryFile(statsJson, rootFolder) {
|
|
452
|
+
const fallbackValue = node_path.default.join(rootFolder, "index.js");
|
|
453
|
+
return statsJson.modules.find((mdl) => !mdl.nameForCondition?.includes("node_modules/") && mdl.reasons?.length === 1 && mdl.reasons?.[0].type === "entry")?.nameForCondition || fallbackValue;
|
|
454
|
+
}
|
|
455
|
+
getModuleOutput(mdl, bundleModules) {
|
|
456
|
+
const jsCodeStrFromJSBundle = bundleModules?.modules?.[String(mdl.id)];
|
|
457
|
+
if (jsCodeStrFromJSBundle) {
|
|
458
|
+
const formatted = jsCodeStrFromJSBundle.startsWith("function(") && jsCodeStrFromJSBundle.endsWith("}") ? jsCodeStrFromJSBundle.replace("function(", "function module(") : jsCodeStrFromJSBundle;
|
|
459
|
+
return {
|
|
460
|
+
code: formatted,
|
|
461
|
+
lineCount: formatted.split("\n").length ?? 0,
|
|
462
|
+
sizeInBytes: Buffer.byteLength(jsCodeStrFromJSBundle, "utf-8")
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
code: mdl.source ?? "",
|
|
467
|
+
lineCount: mdl.source?.split("\n").length ?? 0,
|
|
468
|
+
sizeInBytes: mdl.size ?? 0
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
formatStat(statsJson, bundleModules, compilerOptions) {
|
|
472
|
+
const modulesDeps = this.buildImportsMapFromReasons(statsJson, compilerOptions);
|
|
473
|
+
const context = compilerOptions.context;
|
|
474
|
+
return {
|
|
475
|
+
kind: "webpack",
|
|
476
|
+
date: statsJson.builtAt || Date.now(),
|
|
477
|
+
entryPoint: this.getEntryFile(statsJson, context),
|
|
478
|
+
rootFolder: this.findCommonRoot(statsJson.modules.map((m) => m.nameForCondition).filter((p) => Boolean(p))) || context,
|
|
479
|
+
transformOptions: {
|
|
480
|
+
dev: compilerOptions.mode !== "production",
|
|
481
|
+
minify: compilerOptions.optimization?.minimize ?? false,
|
|
482
|
+
platform: compilerOptions.name ?? "ios"
|
|
483
|
+
},
|
|
484
|
+
modules: statsJson.modules.map((mdl) => {
|
|
485
|
+
if (mdl.type === "runtime modules") {
|
|
486
|
+
if (!bundleModules) return null;
|
|
487
|
+
const sourceAndOutput = {
|
|
488
|
+
code: bundleModules.runtimeSrc ?? "",
|
|
489
|
+
lineCount: bundleModules.runtimeSrc?.split("\n").length ?? 0,
|
|
490
|
+
sizeInBytes: Buffer.byteLength(bundleModules.runtimeSrc ?? "", "utf-8")
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
path: "__runtime__",
|
|
494
|
+
source: sourceAndOutput,
|
|
495
|
+
output: sourceAndOutput,
|
|
496
|
+
dependencies: []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (!mdl.nameForCondition) return null;
|
|
500
|
+
return {
|
|
501
|
+
path: mdl.nameForCondition,
|
|
502
|
+
source: this.readSource(mdl.nameForCondition),
|
|
503
|
+
output: this.getModuleOutput(mdl, bundleModules),
|
|
504
|
+
dependencies: modulesDeps.get(mdl.nameForCondition) ?? []
|
|
505
|
+
};
|
|
506
|
+
}).filter((e) => e !== null && Boolean(e.path)),
|
|
507
|
+
packages: this.getPackages(statsJson)
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
buildImportsMapFromReasons(statsJson, compilerOptions) {
|
|
511
|
+
const modules = Array.isArray(statsJson?.modules) ? statsJson.modules : [];
|
|
512
|
+
const importsMap = /* @__PURE__ */ new Map();
|
|
513
|
+
const getAbs = (p) => {
|
|
514
|
+
if (!p || typeof p !== "string") return null;
|
|
515
|
+
if (node_path.default.isAbsolute(p)) return p;
|
|
516
|
+
return compilerOptions?.context ? node_path.default.resolve(compilerOptions?.context, p) : p;
|
|
517
|
+
};
|
|
518
|
+
const getModulePath = (m) => m?.path || m?.nameForCondition || m?.identifier || null;
|
|
519
|
+
for (const depModule of modules) {
|
|
520
|
+
const depAbsolutePath = getAbs(getModulePath(depModule));
|
|
521
|
+
if (!depAbsolutePath) continue;
|
|
522
|
+
const reasons = Array.isArray(depModule.reasons) ? depModule.reasons : [];
|
|
523
|
+
for (const reason of reasons) {
|
|
524
|
+
if (reason?.type === "entry") continue;
|
|
525
|
+
const importerPath = getAbs(reason?.modulePath || reason?.moduleNameForCondition || reason?.resolvedModule || reason?.moduleIdentifier);
|
|
526
|
+
if (!importerPath) continue;
|
|
527
|
+
let deps = importsMap.get(importerPath);
|
|
528
|
+
if (!deps) {
|
|
529
|
+
deps = [];
|
|
530
|
+
importsMap.set(importerPath, deps);
|
|
531
|
+
}
|
|
532
|
+
if (!deps.some((d) => d.absolutePath === depAbsolutePath)) deps.push({
|
|
533
|
+
absolutePath: depAbsolutePath,
|
|
534
|
+
name: reason?.userRequest || ""
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return importsMap;
|
|
539
|
+
}
|
|
540
|
+
findCommonRoot(paths) {
|
|
541
|
+
if (!paths || paths.length === 0) return "";
|
|
542
|
+
if (paths.length === 1) {
|
|
543
|
+
const lastSlash = paths[0].lastIndexOf("/");
|
|
544
|
+
return lastSlash === -1 ? "" : paths[0].slice(0, lastSlash);
|
|
545
|
+
}
|
|
546
|
+
const first = paths[0];
|
|
547
|
+
let maxLen = first.length;
|
|
548
|
+
for (let i = 1; i < paths.length; i++) {
|
|
549
|
+
const current = paths[i];
|
|
550
|
+
const limit = Math.min(maxLen, current.length);
|
|
551
|
+
let j = 0;
|
|
552
|
+
while (j < limit && first.charCodeAt(j) === current.charCodeAt(j)) j++;
|
|
553
|
+
maxLen = j;
|
|
554
|
+
if (maxLen === 0) return "";
|
|
555
|
+
}
|
|
556
|
+
if (maxLen === first.length) {
|
|
557
|
+
const lastSlash = first.lastIndexOf("/");
|
|
558
|
+
return lastSlash === -1 ? "" : first.slice(0, lastSlash);
|
|
559
|
+
}
|
|
560
|
+
const commonSlice = first.slice(0, maxLen);
|
|
561
|
+
const lastSlash = commonSlice.lastIndexOf("/");
|
|
562
|
+
return lastSlash === -1 ? "" : commonSlice.slice(0, lastSlash);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/index.ts
|
|
567
|
+
const NAME = name;
|
|
568
|
+
function getDefault(module) {
|
|
569
|
+
return module.__esModule ? module.default : module;
|
|
570
|
+
}
|
|
571
|
+
function getDefaultSerializer() {
|
|
572
|
+
const metroPath = (0, node_path.parse)(require.resolve("metro/package.json")).dir;
|
|
573
|
+
const bundleToString = getDefault(require(`${metroPath}/src/lib/bundleToString.js`));
|
|
574
|
+
const baseJSBundle = getDefault(require(`${metroPath}/src/DeltaBundler/Serializers/baseJSBundle.js`));
|
|
575
|
+
return function defaultSerializer(entryPoint, preModules, graph, options) {
|
|
576
|
+
let bundle = baseJSBundle(entryPoint, preModules, graph, options);
|
|
577
|
+
if (typeof options?.sentryBundleCallback === "function") bundle = options.sentryBundleCallback(bundle);
|
|
578
|
+
return bundleToString(bundle).code;
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function getStringSizeInBytes(str) {
|
|
582
|
+
return node_buffer.Buffer.byteLength(str, "utf8");
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* `/Users/i/app/node_modules/metro/node_modules/@babel/runtime/helpers/createClass.js` -> `@babel/runtime`
|
|
586
|
+
* `/Users/i/app/node_modules/react/jsx-runtime.js` -> `react`
|
|
587
|
+
*/
|
|
588
|
+
function getPackageNameFromPath(path) {
|
|
589
|
+
const parts = path.split("node_modules/");
|
|
590
|
+
const lastPart = parts[parts.length - 1];
|
|
591
|
+
if (lastPart.startsWith("@")) return lastPart.split("/").slice(0, 2).join("/");
|
|
592
|
+
return lastPart.split("/")[0];
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* `/Users/i/app/node_modules/metro/node_modules/@babel/runtime/helpers/createClass.js` -> `/Users/i/app/node_modules/metro/node_modules/@babel/runtime`
|
|
596
|
+
* `/Users/i/app/node_modules/react/jsx-runtime.js` -> `/Users/i/app/node_modules/react`
|
|
597
|
+
*/
|
|
598
|
+
function getPackageAbsolutePath(path, pkgName) {
|
|
599
|
+
const parts = path.split("node_modules/");
|
|
600
|
+
parts[parts.length - 1] = pkgName;
|
|
601
|
+
return parts.join("node_modules/");
|
|
602
|
+
}
|
|
603
|
+
function toPackages(modules) {
|
|
604
|
+
const packages = /* @__PURE__ */ new Map();
|
|
605
|
+
modules.forEach((module) => {
|
|
606
|
+
if (!module.path.includes("node_modules/")) return;
|
|
607
|
+
const pkgName = getPackageNameFromPath(module.path);
|
|
608
|
+
const absolutePkgPath = getPackageAbsolutePath(module.path, pkgName);
|
|
609
|
+
if (!packages.has(absolutePkgPath)) packages.set(absolutePkgPath, {
|
|
610
|
+
name: pkgName,
|
|
611
|
+
absolutePath: absolutePkgPath,
|
|
612
|
+
version: require((0, node_path.resolve)(absolutePkgPath, "package.json")).version
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
return Array.from(packages.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
616
|
+
}
|
|
617
|
+
function toModuleStruct(m, includeCode) {
|
|
618
|
+
const sourceCode = m.getSource().toString("utf8");
|
|
619
|
+
const outputCode = m.output[0].data.code;
|
|
620
|
+
return {
|
|
621
|
+
path: m.path,
|
|
622
|
+
source: {
|
|
623
|
+
code: includeCode ? sourceCode : "",
|
|
624
|
+
lineCount: sourceCode.split("\n").length,
|
|
625
|
+
sizeInBytes: getStringSizeInBytes(sourceCode)
|
|
626
|
+
},
|
|
627
|
+
output: {
|
|
628
|
+
code: includeCode ? outputCode : "",
|
|
629
|
+
lineCount: m.output[0].data.lineCount,
|
|
630
|
+
sizeInBytes: getStringSizeInBytes(outputCode)
|
|
631
|
+
},
|
|
632
|
+
dependencies: Array.from(m?.dependencies?.values?.() ?? []).filter((e) => e.absolutePath).map((e) => ({
|
|
633
|
+
absolutePath: e.absolutePath,
|
|
634
|
+
name: e.data.name
|
|
635
|
+
}))
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
async function createJsonReport({ graph, entryPoint, includeEnvs, preModules, includeCode, outputJsonPath, rootFolder, silent, options, fetchPackagesMetadata }) {
|
|
639
|
+
const { processModuleFilter = () => true } = options || {};
|
|
640
|
+
const dependencies = Array.from(graph.dependencies.values()).filter(processModuleFilter);
|
|
641
|
+
let packages = toPackages(preModules).concat(toPackages(dependencies));
|
|
642
|
+
const modules = preModules.map((m) => toModuleStruct(m, includeCode)).concat(dependencies.map((m) => toModuleStruct(m, includeCode)));
|
|
643
|
+
if (fetchPackagesMetadata) packages = await withPackagesMetadata(packages, { onError: (error, pkg) => {
|
|
644
|
+
if (!silent) console.warn(`${chalk.default.yellow(`[${NAME}]`)}: Failed to fetch metadata for ${pkg.name}@${pkg.version}: ${error.message}`);
|
|
645
|
+
} });
|
|
646
|
+
const stats = {
|
|
647
|
+
date: Date.now(),
|
|
648
|
+
entryPoint,
|
|
649
|
+
transformOptions: graph.transformOptions,
|
|
650
|
+
envs: includeEnvs.reduce((acc, envName) => {
|
|
651
|
+
acc[envName] = process.env[envName];
|
|
652
|
+
return acc;
|
|
653
|
+
}, {}),
|
|
654
|
+
rootFolder,
|
|
655
|
+
packages,
|
|
656
|
+
modules
|
|
657
|
+
};
|
|
658
|
+
(0, node_fs.writeFileSync)(outputJsonPath, JSON.stringify(stats));
|
|
659
|
+
if (!silent) console.log(`${chalk.default.yellow(`[${NAME}]`)}: Saved stats to ${chalk.default.green(outputJsonPath)}`);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Creates a custom serializer function for Metro bundler, which generates a JSON report
|
|
663
|
+
* and optionally modifies the serialization process.
|
|
664
|
+
*
|
|
665
|
+
* @param options - Configuration options for the serializer.
|
|
666
|
+
* @returns A custom serializer function to be used by Metro.
|
|
667
|
+
* @throws Throws an error if the project root does not exist.
|
|
668
|
+
*/
|
|
669
|
+
function createSerializer({ serializer, projectRoot, outputJsonPath, includeCode = true, silent = false, includeEnvs = [], fetchPackagesMetadata = true } = {}) {
|
|
670
|
+
const mySerializer = serializer || getDefaultSerializer();
|
|
671
|
+
if (!(0, node_fs.existsSync)(projectRoot)) throw new Error(`[${NAME}]: Project root does not exist: ${projectRoot}`);
|
|
672
|
+
const myOutputJsonPath = outputJsonPath ?? (0, node_path.resolve)(projectRoot, "metro-stats.json");
|
|
673
|
+
function customSerializer(entryPoint, preModules, graph, options) {
|
|
674
|
+
const code = mySerializer(entryPoint, preModules, graph, options);
|
|
675
|
+
const keepAlive = setInterval(() => {}, 1e3);
|
|
676
|
+
createJsonReport({
|
|
677
|
+
graph,
|
|
678
|
+
entryPoint,
|
|
679
|
+
includeEnvs,
|
|
680
|
+
preModules,
|
|
681
|
+
includeCode,
|
|
682
|
+
outputJsonPath: myOutputJsonPath,
|
|
683
|
+
rootFolder: projectRoot,
|
|
684
|
+
silent,
|
|
685
|
+
options,
|
|
686
|
+
fetchPackagesMetadata
|
|
687
|
+
}).finally(() => clearInterval(keepAlive));
|
|
688
|
+
return code;
|
|
689
|
+
}
|
|
690
|
+
return customSerializer;
|
|
691
|
+
}
|
|
692
|
+
const empty = { type: "empty" };
|
|
693
|
+
/**
|
|
694
|
+
* Usage:
|
|
695
|
+
*
|
|
696
|
+
* ```js
|
|
697
|
+
* // metro.config.js
|
|
698
|
+
* const { createResolveRequest } = require("react-native-bundle-discovery");
|
|
699
|
+
* const resolveRequest = createResolveRequest({
|
|
700
|
+
* removePromisePolyfill: true,
|
|
701
|
+
* });
|
|
702
|
+
* const config = {
|
|
703
|
+
* resolver: {
|
|
704
|
+
* resolveRequest,
|
|
705
|
+
* },
|
|
706
|
+
* };
|
|
707
|
+
* ```
|
|
708
|
+
*/
|
|
709
|
+
const createResolveRequest = ({ removeNewRenderer = false, removeUTFSequence = false } = {}) => {
|
|
710
|
+
const resolveRequest = (context, ...rest) => {
|
|
711
|
+
const result = context.resolveRequest(context, ...rest);
|
|
712
|
+
if (context.dev) return result;
|
|
713
|
+
if (removeUTFSequence && result?.filePath?.endsWith("/react-native/Libraries/UTFSequence.js")) return empty;
|
|
714
|
+
if (removeNewRenderer && result?.filePath?.endsWith("/react-native/Libraries/Renderer/shims/ReactFabric.js")) return empty;
|
|
715
|
+
return result;
|
|
716
|
+
};
|
|
717
|
+
return resolveRequest;
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* @deprecated Use `createResolveRequest` instead.
|
|
721
|
+
*/
|
|
722
|
+
const createProcessModuleFilter = ({ removePromisePolyfill = false, removeUTFSequence = false } = {}) => (module) => {
|
|
723
|
+
if (removePromisePolyfill && module.path.endsWith("/react-native/Libraries/Promise.js")) return false;
|
|
724
|
+
if (removeUTFSequence && module.path.endsWith("/react-native/Libraries/UTFSequence.js")) return false;
|
|
725
|
+
return true;
|
|
726
|
+
};
|
|
727
|
+
//#endregion
|
|
728
|
+
exports.BundleDiscoveryPlugin = BundleDiscoveryPlugin;
|
|
729
|
+
exports.createProcessModuleFilter = createProcessModuleFilter;
|
|
730
|
+
exports.createResolveRequest = createResolveRequest;
|
|
731
|
+
exports.createSerializer = createSerializer;
|