react-native-bundle-discovery 2.6.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/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;