react-native-bundle-discovery-ui 2.8.0 → 2.9.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +12 -255
  3. package/assets/global.css +769 -53
  4. package/dist/bin.js +61 -20
  5. package/dist/client/issues.js +5 -0
  6. package/dist/client/pages/_common.js +258 -86
  7. package/dist/client/pages/_module.js +347 -0
  8. package/dist/client/pages/default.js +145 -62
  9. package/dist/client/pages/module.js +2 -309
  10. package/dist/client/pages/moduleDiff.js +76 -0
  11. package/dist/client/pages/package.js +172 -154
  12. package/dist/client/prepare.js +15 -0
  13. package/dist/client/queryHelpers.js +445 -81
  14. package/dist/client/views/{_theme.js → _colorScheme.js} +2 -9
  15. package/dist/client/views/_html.js +52 -0
  16. package/dist/client/views/codeDiff.js +66 -0
  17. package/dist/client/views/compare.js +237 -0
  18. package/dist/client/views/importGraph.js +255 -0
  19. package/dist/client/views/insights.js +80 -0
  20. package/dist/client/views/moduleOverview.js +134 -0
  21. package/dist/client/views/packageOverview.js +114 -0
  22. package/dist/client/views/persistedFilterInput.js +27 -0
  23. package/dist/client/views/reportBar.js +89 -0
  24. package/dist/client/views/treemap.js +598 -0
  25. package/dist/discoveryrc.js +26 -7
  26. package/dist/rsdoctor.js +4 -101
  27. package/dist/src-Bp8pPQrQ.js +1545 -0
  28. package/package.json +5 -3
  29. package/assets/highcharts.css +0 -1
  30. package/dist/client/_virtual/_rolldown/runtime.js +0 -37
  31. package/dist/client/views/foamtree.js +0 -103
  32. package/dist/client/views/highcharts.js +0 -33
  33. package/dist/src-DPAqywOJ.js +0 -201
  34. package/vendors/foamtree.js +0 -9008
  35. package/vendors/high-contrast-dark.js +0 -1
  36. package/vendors/highcharts-exporting.js +0 -13
  37. package/vendors/highcharts-networkgraph.js +0 -1
  38. package/vendors/highcharts.js +0 -10
@@ -0,0 +1,1545 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.js");
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let chalk = require("chalk");
5
+ chalk = require_rolldown_runtime.__toESM(chalk);
6
+ //#region ../shared/src/packageGroups.ts
7
+ const LODASH_FAMILY_GROUP = "lodash (please use only one)";
8
+ function getDuplicateGroupName(packageName) {
9
+ if (
10
+ packageName === "lodash" ||
11
+ packageName === "lodash-es" ||
12
+ packageName === "underscore" ||
13
+ packageName === "ramda" ||
14
+ packageName.startsWith("lodash.")
15
+ )
16
+ return LODASH_FAMILY_GROUP;
17
+ return packageName;
18
+ }
19
+ function getPackageGroups(report, sort = "size") {
20
+ const groupedPackages = report.packages.reduce((map, pkg) => {
21
+ const name = pkg?.name ?? "<unknown>";
22
+ const duplicateGroupName = getDuplicateGroupName(name);
23
+ const version = pkg?.version ?? "<unknown>";
24
+ const packagePath = pkg?.path ?? pkg?.absolutePath ?? "<unknown>";
25
+ const sizeInBytes = pkg?.sizeInBytes ?? 0;
26
+ let entries = map.get(duplicateGroupName);
27
+ if (!entries) {
28
+ entries = [];
29
+ map.set(duplicateGroupName, entries);
30
+ }
31
+ entries.push({
32
+ name,
33
+ version,
34
+ path: packagePath,
35
+ absolutePath: pkg?.absolutePath ?? packagePath,
36
+ sizeInBytes,
37
+ });
38
+ return map;
39
+ }, /* @__PURE__ */ new Map());
40
+ const packageGroups = Array.from(groupedPackages.entries()).map(
41
+ ([name, entries]) => {
42
+ return {
43
+ name,
44
+ entries,
45
+ totalSizeInBytes: entries.reduce(
46
+ (sum, entry) => sum + entry.sizeInBytes,
47
+ 0,
48
+ ),
49
+ };
50
+ },
51
+ );
52
+ if (sort === "name")
53
+ packageGroups.sort((a, b) => a.name.localeCompare(b.name));
54
+ else packageGroups.sort((a, b) => b.totalSizeInBytes - a.totalSizeInBytes);
55
+ return packageGroups;
56
+ }
57
+ //#endregion
58
+ //#region ../shared/src/esbuild.ts
59
+ const RNX_PRELUDE = "virtual:metro:__rnx_prelude__";
60
+ const packageRegex = /(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)/g;
61
+ const isEsbuildMetafile = (reportJson, reportPath, noLogs) => {
62
+ try {
63
+ if (
64
+ reportJson?.inputs &&
65
+ typeof reportJson.inputs === "object" &&
66
+ reportJson?.outputs &&
67
+ typeof reportJson.outputs === "object" &&
68
+ !Array.isArray(reportJson?.modules)
69
+ ) {
70
+ !noLogs &&
71
+ console.info(
72
+ `Detected ${chalk.default.red("esbuild metafile")} format in the input file: ${chalk.default.green(reportPath)}`,
73
+ );
74
+ return true;
75
+ }
76
+ return false;
77
+ } catch {
78
+ return false;
79
+ }
80
+ };
81
+ const isEsbuildMetafilePath = (reportPath) => {
82
+ try {
83
+ const reportJson = require(reportPath);
84
+ return isEsbuildMetafile(reportJson, reportPath, true);
85
+ } catch {
86
+ return false;
87
+ }
88
+ };
89
+ const isVirtual = (inputPath) =>
90
+ !(0, node_path.isAbsolute)(inputPath) && /^[\w-]+:/.test(inputPath);
91
+ /**
92
+ * Metafile paths are relative to esbuild's working directory, which is not stored in the file.
93
+ * Try the metafile folder (and its parents) and then `process.cwd()`; pick the first one where the inputs exist.
94
+ */
95
+ const getRootFolder = (metafile, reportPath) => {
96
+ const probe = Object.keys(metafile.inputs).find((p) => !isVirtual(p));
97
+ const candidates = [];
98
+ let dir = (0, node_path.dirname)(reportPath);
99
+ while (!candidates.includes(dir)) {
100
+ candidates.push(dir);
101
+ dir = (0, node_path.dirname)(dir);
102
+ }
103
+ candidates.push(process.cwd());
104
+ return (
105
+ (probe &&
106
+ candidates.find((root) =>
107
+ (0, node_fs.existsSync)((0, node_path.resolve)(root, probe)),
108
+ )) ||
109
+ (0, node_path.dirname)(reportPath)
110
+ );
111
+ };
112
+ const getPlatform = (paths) => {
113
+ const count = (platform) =>
114
+ paths.filter((p) => new RegExp(`\\.${platform}\\.[cm]?[jt]sx?$`).test(p))
115
+ .length;
116
+ return count("android") > count("ios") ? "android" : "ios";
117
+ };
118
+ const getPackages = (modulePaths) => {
119
+ const packages = /* @__PURE__ */ new Map();
120
+ for (const filePath of modulePaths) {
121
+ const lastMatch = [...filePath.matchAll(packageRegex)].pop();
122
+ if (!lastMatch) continue;
123
+ const name = lastMatch[1];
124
+ const end = filePath.indexOf(name, lastMatch.index) + name.length;
125
+ const absolutePath = filePath.slice(0, end);
126
+ if (packages.has(absolutePath)) continue;
127
+ let version = "";
128
+ try {
129
+ version = JSON.parse(
130
+ (0, node_fs.readFileSync)(
131
+ (0, node_path.resolve)(absolutePath, "package.json"),
132
+ "utf8",
133
+ ),
134
+ ).version;
135
+ } catch {}
136
+ packages.set(absolutePath, {
137
+ name,
138
+ absolutePath,
139
+ version,
140
+ });
141
+ }
142
+ return Array.from(packages.values()).sort((a, b) =>
143
+ a.name.localeCompare(b.name),
144
+ );
145
+ };
146
+ /**
147
+ * Converts an esbuild metafile (e.g. written by `@rnx-kit/metro-serializer-esbuild` with the `metafile` option)
148
+ * to the `metro-stats.json` format.
149
+ *
150
+ * Output sizes are `bytesInOutput` (after tree shaking and minification); modules removed by tree shaking are omitted.
151
+ * Code is not included: esbuild hoists all modules into a single scope.
152
+ */
153
+ const transformEsbuildMetafile = (metafile, reportPath) => {
154
+ const rootFolder = getRootFolder(metafile, reportPath);
155
+ const toAbsolute = (p) =>
156
+ isVirtual(p) ? p : (0, node_path.resolve)(rootFolder, p);
157
+ const outputs = Object.entries(metafile.outputs).filter(
158
+ ([outputPath]) => !outputPath.endsWith(".map"),
159
+ );
160
+ const bytesInOutput = /* @__PURE__ */ new Map();
161
+ for (const [, output] of outputs)
162
+ for (const [inputPath, { bytesInOutput: bytes }] of Object.entries(
163
+ output.inputs,
164
+ ))
165
+ bytesInOutput.set(inputPath, (bytesInOutput.get(inputPath) ?? 0) + bytes);
166
+ const bundledInputs = Object.keys(metafile.inputs).filter(
167
+ (p) => (bytesInOutput.get(p) ?? 0) > 0,
168
+ );
169
+ const bundled = new Set(bundledInputs);
170
+ let entryPoint = outputs.find(([, o]) => o.entryPoint)?.[1].entryPoint ?? "";
171
+ if (entryPoint === RNX_PRELUDE)
172
+ entryPoint = metafile.inputs[RNX_PRELUDE]?.imports.at(-1)?.path ?? "";
173
+ const modules = bundledInputs.map((inputPath) => {
174
+ const input = metafile.inputs[inputPath];
175
+ return {
176
+ path: toAbsolute(inputPath),
177
+ source: {
178
+ code: "",
179
+ lineCount: 0,
180
+ sizeInBytes: input.bytes,
181
+ },
182
+ output: {
183
+ code: "",
184
+ lineCount: 0,
185
+ sizeInBytes: bytesInOutput.get(inputPath) ?? 0,
186
+ },
187
+ dependencies: input.imports
188
+ .filter((imp) => !imp.external && bundled.has(imp.path))
189
+ .map((imp) => ({
190
+ absolutePath: toAbsolute(imp.path),
191
+ name: imp.original ?? imp.path,
192
+ })),
193
+ };
194
+ });
195
+ return {
196
+ date: Date.now(),
197
+ entryPoint: entryPoint ? toAbsolute(entryPoint) : "",
198
+ rootFolder,
199
+ transformOptions: {
200
+ dev: false,
201
+ minify: true,
202
+ platform: getPlatform(bundledInputs),
203
+ },
204
+ packages: getPackages(modules.map((m) => m.path)),
205
+ modules,
206
+ };
207
+ };
208
+ //#endregion
209
+ //#region ../shared/src/isRsdoctorReport.ts
210
+ const isRsdoctorReport = (reportJson, reportPath, noLogs) => {
211
+ try {
212
+ if (
213
+ Array.isArray(reportJson?.data?.moduleGraph?.modules) ||
214
+ Array.isArray(reportJson?.data?.packageGraph?.packages)
215
+ ) {
216
+ !noLogs &&
217
+ console.info(
218
+ `Detected ${chalk.default.red("rsdoctor")} format in the input file: ${chalk.default.green(reportPath)}`,
219
+ );
220
+ return true;
221
+ }
222
+ return false;
223
+ } catch {
224
+ return false;
225
+ }
226
+ };
227
+ const isRsdoctorReportPath = (reportPath) => {
228
+ try {
229
+ const reportJson = require(reportPath);
230
+ return isRsdoctorReport(reportJson, reportPath);
231
+ } catch {
232
+ return false;
233
+ }
234
+ };
235
+ //#endregion
236
+ //#region ../shared/src/rsdoctor.ts
237
+ const possibleEntryFiles = [
238
+ "index.js",
239
+ "index.jsx",
240
+ "index.ts",
241
+ "index.tsx",
242
+ "index.mjs",
243
+ "index.cjs",
244
+ ];
245
+ const doStructureCheck = (rsdoctorData) => {
246
+ if (!(
247
+ Array.isArray(rsdoctorData?.data?.configs) &&
248
+ rsdoctorData.data.configs.length === 1
249
+ ))
250
+ throw new Error(
251
+ `Unexpected number of "data.configs[]" in rsdoctor-data.json. Work only with single config. Found: ${rsdoctorData?.data?.configs?.length}`,
252
+ );
253
+ if (!Array.isArray(rsdoctorData?.data?.packageGraph?.packages))
254
+ throw new Error(
255
+ `Unexpected "data.packageGraph.packages[]" in rsdoctor-data.json. Expected array. Found: ${typeof rsdoctorData?.data?.packageGraph?.packages}`,
256
+ );
257
+ const firstConfig = rsdoctorData.data.configs[0];
258
+ if (!(firstConfig.name === "rspack" || firstConfig.name === "webpack"))
259
+ throw new Error(
260
+ `Unsupported bundler: ${firstConfig.name}@${firstConfig.version}. Only 'rspack' and 'webpack' are supported.`,
261
+ );
262
+ };
263
+ const getDependenciesMap = (rsdoctorData) => {
264
+ return new Map(
265
+ rsdoctorData.data.moduleGraph.dependencies.map((dep) => [dep.id, dep]),
266
+ );
267
+ };
268
+ const getEntryFile = (rsdoctorData) => {
269
+ const firstConfig = rsdoctorData.data.configs[0];
270
+ const main = firstConfig.config.entry?.main;
271
+ const entryFile =
272
+ typeof main === "string"
273
+ ? main
274
+ : typeof main?.import === "string"
275
+ ? main.import
276
+ : main?.import[0];
277
+ if (entryFile) return (0, node_path.resolve)(firstConfig.root, entryFile);
278
+ const pathsToCheck = possibleEntryFiles.map((f) =>
279
+ (0, node_path.resolve)(firstConfig.root, f),
280
+ );
281
+ const indexFile = rsdoctorData.data.moduleGraph.modules.find((mdl) =>
282
+ pathsToCheck.some((entry) => mdl.path === entry),
283
+ );
284
+ if (indexFile && indexFile.imported?.length === 0) return indexFile.path;
285
+ return `${firstConfig.root}/index.js`;
286
+ };
287
+ const transformRSDoctorData = (rsdoctorData) => {
288
+ doStructureCheck(rsdoctorData);
289
+ const tmpDependenciesMap = getDependenciesMap(rsdoctorData);
290
+ const firstConfig = rsdoctorData.data.configs[0];
291
+ return {
292
+ date: Date.now(),
293
+ entryPoint: getEntryFile(rsdoctorData),
294
+ rootFolder: firstConfig.root,
295
+ transformOptions: {
296
+ dev: firstConfig.config.mode !== "production",
297
+ minify: firstConfig.config.optimization?.minimize ?? false,
298
+ platform: firstConfig.config.name ?? "ios",
299
+ },
300
+ packages: rsdoctorData.data.packageGraph.packages.map((pkg) => ({
301
+ name: pkg.name,
302
+ absolutePath: pkg.root,
303
+ version: pkg.version,
304
+ source: { sizeInBytes: pkg.size?.sourceSize ?? 1 },
305
+ output: { sizeInBytes: pkg.size?.parsedSize ?? 1 },
306
+ })),
307
+ modules: rsdoctorData.data.moduleGraph.modules.map((mdl) => ({
308
+ path: mdl.path,
309
+ source: {
310
+ code: "",
311
+ lineCount: 0,
312
+ sizeInBytes: mdl.size?.sourceSize ?? 0,
313
+ },
314
+ output: {
315
+ code: "",
316
+ lineCount: 0,
317
+ sizeInBytes: mdl.size?.parsedSize ?? 0,
318
+ },
319
+ dependencies: mdl.dependencies
320
+ .map((depNumId) => {
321
+ const dep = tmpDependenciesMap.get(depNumId);
322
+ return dep
323
+ ? {
324
+ absolutePath: dep.resolvedRequest,
325
+ name: dep.request,
326
+ }
327
+ : null;
328
+ })
329
+ .filter((e) => e !== null),
330
+ })),
331
+ };
332
+ };
333
+ //#endregion
334
+ //#region ../shared/src/prepareReport.ts
335
+ function normalizeReportData(report, reportPath, noLogs) {
336
+ if (isRsdoctorReport(report, reportPath, noLogs))
337
+ return transformRSDoctorData(report);
338
+ if (isEsbuildMetafile(report, reportPath, noLogs))
339
+ return transformEsbuildMetafile(report, reportPath);
340
+ return report;
341
+ }
342
+ /** Modules that belong to the package itself (without its nested `node_modules`) */
343
+ function getPackageModules(report, pkg) {
344
+ const packagePrefix = `${pkg.absolutePath}/`;
345
+ return report.modules.filter(
346
+ (module) =>
347
+ module.path.startsWith(packagePrefix) &&
348
+ !module.path.slice(packagePrefix.length).includes("/node_modules/"),
349
+ );
350
+ }
351
+ function sumModulesSize(modules) {
352
+ return modules.reduce(
353
+ (sum, module) => sum + (module.output?.sizeInBytes ?? 0),
354
+ 0,
355
+ );
356
+ }
357
+ function prepareReport(_report, reportPath, noLogs) {
358
+ const report = normalizeReportData(_report, reportPath, noLogs);
359
+ const packages = report.packages.map((pkg) => {
360
+ const sizeInBytes = sumModulesSize(getPackageModules(report, pkg));
361
+ return {
362
+ ...pkg,
363
+ path: pkg.absolutePath.replace(`${report.rootFolder}/`, ""),
364
+ sizeInBytes,
365
+ };
366
+ });
367
+ return Object.assign(report, { packages });
368
+ }
369
+ //#endregion
370
+ //#region ../shared/src/recommendations/savings.ts
371
+ function getModulesSavings(modules) {
372
+ const unique = Array.from(new Set(modules));
373
+ return {
374
+ sizeInBytes: sumModulesSize(unique),
375
+ modules: unique.map((module) => module.path),
376
+ };
377
+ }
378
+ function getPackagesSavings(report, packages) {
379
+ return getModulesSavings(
380
+ packages.flatMap((pkg) => getPackageModules(report, pkg)),
381
+ );
382
+ }
383
+ //#endregion
384
+ //#region ../shared/src/recommendations/deprecated-packages.ts
385
+ function findDeprecatedPackages(report) {
386
+ const matches = /* @__PURE__ */ new Map();
387
+ for (const pkg of report.packages) {
388
+ const deprecated = pkg?.metadata?.deprecated;
389
+ if (!deprecated) continue;
390
+ const id = `${pkg.name}@${pkg.version}`;
391
+ if (!matches.has(id))
392
+ matches.set(id, {
393
+ id,
394
+ reason: typeof deprecated === "string" ? deprecated.trim() : null,
395
+ latestVersion: pkg.metadata?.latestVersion ?? null,
396
+ });
397
+ }
398
+ return Array.from(matches.values());
399
+ }
400
+ const recommendation$14 = {
401
+ id: "deprecated-packages",
402
+ title: "Replace deprecated packages",
403
+ check: (report) => {
404
+ const deprecatedPackages = findDeprecatedPackages(report);
405
+ if (deprecatedPackages.length === 0) return null;
406
+ const details = deprecatedPackages
407
+ .map(({ id, reason, latestVersion }) => {
408
+ const latest = latestVersion ? ` (latest: ${latestVersion})` : "";
409
+ return reason ? ` - ${id}${latest}: ${reason}` : ` - ${id}${latest}`;
410
+ })
411
+ .join("\n");
412
+ return {
413
+ message: `Detected ${deprecatedPackages.length} deprecated package(s) in the bundle:
414
+ ${details}
415
+
416
+ Deprecated packages are no longer maintained and may contain bugs or security issues.
417
+ Upgrade to a non-deprecated version or migrate to the recommended replacement.
418
+ `,
419
+ packages: deprecatedPackages.map(({ id }) => id),
420
+ docsUrl:
421
+ "https://docs.npmjs.com/deprecating-and-undeprecating-packages-or-package-versions",
422
+ modules: getModulesSavings(
423
+ report.packages
424
+ .filter((pkg) => pkg?.metadata?.deprecated)
425
+ .flatMap((pkg) => getPackageModules(report, pkg)),
426
+ ).modules,
427
+ };
428
+ },
429
+ };
430
+ //#endregion
431
+ //#region ../shared/src/compare.ts
432
+ const COMPARED_TRANSFORM_OPTIONS = [
433
+ "dev",
434
+ "minify",
435
+ "unstable_transformProfile",
436
+ ];
437
+ const CONTRIBUTORS_LIMIT = 5;
438
+ /**
439
+ * Makes paths comparable between machines and package managers:
440
+ * `/Users/i/app/src/App.js` -> `src/App.js`
441
+ * `/ci/app/node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/get.js` -> `node_modules/lodash/get.js`
442
+ */
443
+ function normalizePath(absolutePath, rootFolder) {
444
+ const value = absolutePath ?? "<unknown>";
445
+ const rootPrefix = rootFolder ? `${rootFolder}/` : null;
446
+ return (
447
+ rootPrefix && value.startsWith(rootPrefix)
448
+ ? value.slice(rootPrefix.length)
449
+ : value
450
+ ).replace(/node_modules\/\.pnpm\/[^/]+\/node_modules\//g, "node_modules/");
451
+ }
452
+ /**
453
+ * `node_modules/@babel/runtime/helpers/createClass.js` -> `@babel/runtime`
454
+ * `src/screens/Home/index.js` -> `src/screens`
455
+ */
456
+ function getContributorName(modulePath) {
457
+ const index = modulePath.lastIndexOf("node_modules/");
458
+ if (index !== -1) {
459
+ const parts = modulePath.slice(index + 13).split("/");
460
+ return parts[0].startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
461
+ }
462
+ return modulePath.split("/").slice(0, -1).slice(0, 2).join("/") || modulePath;
463
+ }
464
+ function getTotalSize(report) {
465
+ return report.modules.reduce(
466
+ (sum, module) => sum + (module?.output?.sizeInBytes ?? 0),
467
+ 0,
468
+ );
469
+ }
470
+ function getTransformOptionsDiff(before, after) {
471
+ if (!before.transformOptions || !after.transformOptions) return [];
472
+ return COMPARED_TRANSFORM_OPTIONS.filter(
473
+ (option) =>
474
+ before.transformOptions[option] !== after.transformOptions[option],
475
+ ).map((option) => ({
476
+ option,
477
+ before: before.transformOptions[option],
478
+ after: after.transformOptions[option],
479
+ }));
480
+ }
481
+ function getModuleSizes(report) {
482
+ const sizes = /* @__PURE__ */ new Map();
483
+ report.modules.forEach((module) => {
484
+ const modulePath = normalizePath(module?.path, report.rootFolder);
485
+ sizes.set(
486
+ modulePath,
487
+ (sizes.get(modulePath) ?? 0) + (module?.output?.sizeInBytes ?? 0),
488
+ );
489
+ });
490
+ return sizes;
491
+ }
492
+ function compareModules(before, after) {
493
+ const beforeSizes = getModuleSizes(before);
494
+ const afterSizes = getModuleSizes(after);
495
+ const added = [];
496
+ const removed = [];
497
+ const changed = [];
498
+ afterSizes.forEach((sizeInBytes, path) => {
499
+ const beforeSizeInBytes = beforeSizes.get(path);
500
+ if (beforeSizeInBytes === void 0) {
501
+ added.push({
502
+ path,
503
+ sizeInBytes,
504
+ deltaInBytes: sizeInBytes,
505
+ });
506
+ return;
507
+ }
508
+ if (beforeSizeInBytes !== sizeInBytes)
509
+ changed.push({
510
+ path,
511
+ beforeSizeInBytes,
512
+ afterSizeInBytes: sizeInBytes,
513
+ deltaInBytes: sizeInBytes - beforeSizeInBytes,
514
+ });
515
+ });
516
+ beforeSizes.forEach((sizeInBytes, path) => {
517
+ if (!afterSizes.has(path))
518
+ removed.push({
519
+ path,
520
+ sizeInBytes,
521
+ deltaInBytes: -sizeInBytes,
522
+ });
523
+ });
524
+ added.sort((a, b) => b.sizeInBytes - a.sizeInBytes);
525
+ removed.sort((a, b) => b.sizeInBytes - a.sizeInBytes);
526
+ changed.sort((a, b) => Math.abs(b.deltaInBytes) - Math.abs(a.deltaInBytes));
527
+ return {
528
+ added,
529
+ removed,
530
+ changed,
531
+ };
532
+ }
533
+ function getPackagesByName(report) {
534
+ const packages = /* @__PURE__ */ new Map();
535
+ report.packages.forEach((pkg) => {
536
+ const name = pkg?.name ?? "<unknown>";
537
+ let entry = packages.get(name);
538
+ if (!entry) {
539
+ entry = {
540
+ name,
541
+ versions: [],
542
+ sizeInBytes: 0,
543
+ };
544
+ packages.set(name, entry);
545
+ }
546
+ entry.versions.push(pkg?.version ?? "<unknown>");
547
+ entry.sizeInBytes += pkg?.sizeInBytes ?? 0;
548
+ });
549
+ packages.forEach((entry) => {
550
+ entry.versions = Array.from(new Set(entry.versions)).sort();
551
+ });
552
+ return packages;
553
+ }
554
+ function getNewDuplicates(before, after) {
555
+ const beforeGroups = new Map(
556
+ getPackageGroups(before).map((group) => [group.name, group]),
557
+ );
558
+ return getPackageGroups(after)
559
+ .filter(
560
+ (group) =>
561
+ group.entries.length > 1 &&
562
+ group.entries.length >
563
+ (beforeGroups.get(group.name)?.entries.length ?? 0),
564
+ )
565
+ .map((group) => ({
566
+ name: group.name,
567
+ count: group.entries.length,
568
+ sizeInBytes: group.totalSizeInBytes,
569
+ entries: group.entries.map((entry) => ({
570
+ name: entry.name,
571
+ version: entry.version,
572
+ path: normalizePath(entry.path, null),
573
+ })),
574
+ }));
575
+ }
576
+ function compareDeprecated(before, after) {
577
+ const beforeDeprecated = findDeprecatedPackages(before);
578
+ const afterDeprecated = findDeprecatedPackages(after);
579
+ const beforeIds = new Set(beforeDeprecated.map(({ id }) => id));
580
+ return {
581
+ beforeCount: beforeDeprecated.length,
582
+ afterCount: afterDeprecated.length,
583
+ added: afterDeprecated.filter(({ id }) => !beforeIds.has(id)),
584
+ };
585
+ }
586
+ function comparePackages(before, after) {
587
+ const beforePackages = getPackagesByName(before);
588
+ const afterPackages = getPackagesByName(after);
589
+ const added = [];
590
+ const removed = [];
591
+ const versionChanges = [];
592
+ afterPackages.forEach((pkg, name) => {
593
+ const beforePkg = beforePackages.get(name);
594
+ if (!beforePkg) {
595
+ added.push(pkg);
596
+ return;
597
+ }
598
+ if (beforePkg.versions.join() !== pkg.versions.join())
599
+ versionChanges.push({
600
+ name,
601
+ beforeVersions: beforePkg.versions,
602
+ afterVersions: pkg.versions,
603
+ deltaInBytes: pkg.sizeInBytes - beforePkg.sizeInBytes,
604
+ });
605
+ });
606
+ beforePackages.forEach((pkg, name) => {
607
+ if (!afterPackages.has(name)) removed.push(pkg);
608
+ });
609
+ added.sort((a, b) => b.sizeInBytes - a.sizeInBytes);
610
+ removed.sort((a, b) => b.sizeInBytes - a.sizeInBytes);
611
+ versionChanges.sort(
612
+ (a, b) => Math.abs(b.deltaInBytes) - Math.abs(a.deltaInBytes),
613
+ );
614
+ return {
615
+ added,
616
+ removed,
617
+ versionChanges,
618
+ newDuplicates: getNewDuplicates(before, after),
619
+ deprecated: compareDeprecated(before, after),
620
+ };
621
+ }
622
+ function getBiggestContributors(modules, packages) {
623
+ const deltas = /* @__PURE__ */ new Map();
624
+ [...modules.added, ...modules.removed, ...modules.changed].forEach(
625
+ (module) => {
626
+ const name = getContributorName(module.path);
627
+ deltas.set(name, (deltas.get(name) ?? 0) + module.deltaInBytes);
628
+ },
629
+ );
630
+ const addedNames = new Set(packages.added.map(({ name }) => name));
631
+ const versionChanges = new Map(
632
+ packages.versionChanges.map((change) => [change.name, change]),
633
+ );
634
+ return Array.from(deltas.entries())
635
+ .filter(([, deltaInBytes]) => deltaInBytes > 0)
636
+ .sort((a, b) => b[1] - a[1])
637
+ .slice(0, CONTRIBUTORS_LIMIT)
638
+ .map(([name, deltaInBytes]) => {
639
+ let note = null;
640
+ const change = versionChanges.get(name);
641
+ if (addedNames.has(name)) note = "new package";
642
+ else if (change)
643
+ note = `${change.beforeVersions.join(", ")} → ${change.afterVersions.join(", ")}`;
644
+ return {
645
+ name,
646
+ deltaInBytes,
647
+ note,
648
+ };
649
+ });
650
+ }
651
+ function withUniquePackages(report) {
652
+ const seen = /* @__PURE__ */ new Set();
653
+ return {
654
+ ...report,
655
+ packages: report.packages.filter((pkg) => {
656
+ const key = `${pkg?.name}@${pkg?.absolutePath ?? pkg?.path}`;
657
+ if (seen.has(key)) return false;
658
+ seen.add(key);
659
+ return true;
660
+ }),
661
+ };
662
+ }
663
+ function compareReports(_before, _after) {
664
+ const before = withUniquePackages(_before);
665
+ const after = withUniquePackages(_after);
666
+ const beforeSizeInBytes = getTotalSize(before);
667
+ const afterSizeInBytes = getTotalSize(after);
668
+ const modules = compareModules(before, after);
669
+ const packages = comparePackages(before, after);
670
+ return {
671
+ transformOptionsDiff: getTransformOptionsDiff(before, after),
672
+ summary: {
673
+ before: {
674
+ platform: before.transformOptions?.platform ?? null,
675
+ sizeInBytes: beforeSizeInBytes,
676
+ modules: before.modules.length,
677
+ packages: before.packages.length,
678
+ },
679
+ after: {
680
+ platform: after.transformOptions?.platform ?? null,
681
+ sizeInBytes: afterSizeInBytes,
682
+ modules: after.modules.length,
683
+ packages: after.packages.length,
684
+ },
685
+ deltaInBytes: afterSizeInBytes - beforeSizeInBytes,
686
+ deltaPercent:
687
+ beforeSizeInBytes > 0
688
+ ? ((afterSizeInBytes - beforeSizeInBytes) / beforeSizeInBytes) * 100
689
+ : null,
690
+ },
691
+ modules,
692
+ packages,
693
+ biggestContributors: getBiggestContributors(modules, packages),
694
+ };
695
+ }
696
+ function getBeforeModules(before, changed) {
697
+ const paths = new Set(changed.map((module) => module.path));
698
+ const result = {};
699
+ before.modules.forEach((module) => {
700
+ const modulePath = normalizePath(module?.path, before.rootFolder);
701
+ if (paths.has(modulePath) && !result[modulePath])
702
+ result[modulePath] = {
703
+ source: module.source?.code ?? "",
704
+ output: module.output?.code ?? "",
705
+ };
706
+ });
707
+ return result;
708
+ }
709
+ /** Attaches the diff against a "before" report (raw JSON of any supported format) */
710
+ function withComparison(report, before, beforePath) {
711
+ const beforeReport = prepareReport(before, beforePath, true);
712
+ const comparison = compareReports(
713
+ beforeReport,
714
+ prepareReport({ ...report }, "", true),
715
+ );
716
+ return Object.assign(report, {
717
+ comparison: {
718
+ ...comparison,
719
+ beforeFile: beforePath.split("/").pop() ?? beforePath,
720
+ beforeModules: getBeforeModules(beforeReport, comparison.modules.changed),
721
+ },
722
+ });
723
+ }
724
+ //#endregion
725
+ //#region ../shared/src/recommendations/dev-files.ts
726
+ const devFileNamePattern = /\b(development|debug|dev|storybook)\b/i;
727
+ function findDevFilesInBundle(report) {
728
+ return report.modules.filter((module) =>
729
+ devFileNamePattern.test(module?.path?.replace(report.rootFolder, "")),
730
+ );
731
+ }
732
+ const recommendation$13 = {
733
+ id: "dev-files-in-production-bundle",
734
+ title: "Exclude dev/debug files from production bundle",
735
+ check: (report) => {
736
+ const devFiles = findDevFilesInBundle(report);
737
+ if (devFiles.length === 0) return null;
738
+ const sample = devFiles
739
+ .slice(0, 5)
740
+ .map((module) => module.path.replace(report.rootFolder, ""))
741
+ .join("\n - ");
742
+ return {
743
+ message: `Detected ${devFiles.length} dev/debug file(s) in the production bundle.
744
+ Files matching development|debug|dev|storybook should be excluded from release builds.
745
+ Example paths:\n - ${sample}`,
746
+ packages: [],
747
+ docsUrl: null,
748
+ ...getModulesSavings(devFiles),
749
+ };
750
+ },
751
+ };
752
+ //#endregion
753
+ //#region ../shared/src/versions.ts
754
+ function getReactNativeVersion(packages) {
755
+ if (!Array.isArray(packages)) return;
756
+ return packages.find((pkg) => pkg?.name === "react-native")?.version;
757
+ }
758
+ function parseVersion(version) {
759
+ if (typeof version !== "string") return null;
760
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
761
+ if (!match) return null;
762
+ return {
763
+ major: Number(match[1]),
764
+ minor: Number(match[2]),
765
+ patch: Number(match[3]),
766
+ };
767
+ }
768
+ function isVersionGte(version, targetVersion) {
769
+ const current = parseVersion(version);
770
+ const target = parseVersion(targetVersion);
771
+ if (!current || !target) return false;
772
+ if (current.major !== target.major) return current.major > target.major;
773
+ if (current.minor !== target.minor) return current.minor > target.minor;
774
+ return current.patch >= target.patch;
775
+ }
776
+ function formatRnVersionToDocsFormat(version) {
777
+ return version.split(".").slice(0, 2).join(".");
778
+ }
779
+ //#endregion
780
+ //#region ../shared/src/recommendations/dev-only-packages.ts
781
+ const rn74PlusDevOnlyPackages = [
782
+ "prop-types",
783
+ "deprecated-react-native-prop-types",
784
+ ];
785
+ const devOnlyPackages = [
786
+ "redux-logger",
787
+ "redux-devtools",
788
+ "@redux-devtools/core",
789
+ "@redux-devtools/extension",
790
+ "redux-devtools-extension",
791
+ "@redux-devtools/instrument",
792
+ "@redux-devtools/remote-redux-devtools",
793
+ "@storybook/react",
794
+ "@storybook/react-native",
795
+ "@storybook/addon-actions",
796
+ "@storybook/addon-links",
797
+ "json-server",
798
+ "miragejs",
799
+ "@faker-js/faker",
800
+ "msw",
801
+ "eruda",
802
+ "vconsole",
803
+ "react-developer-tools",
804
+ "react-render-tracker",
805
+ "why-did-you-render",
806
+ "@welldone-software/why-did-you-render",
807
+ "use-what-changed",
808
+ "mobx-react-devtools",
809
+ "mobx-devtools",
810
+ "@tanstack/react-query-devtools",
811
+ "react-devtools-core",
812
+ "react-refresh",
813
+ ];
814
+ function findBundledDevPackages(report, _devOnlyPackages) {
815
+ const packages = report.packages;
816
+ const modules = report.modules;
817
+ const matches = /* @__PURE__ */ new Set();
818
+ const matchedModules = [];
819
+ for (const pkg of packages)
820
+ if (
821
+ _devOnlyPackages.includes(pkg?.name) &&
822
+ Number(pkg?.sizeInBytes ?? 0) > 1e3
823
+ )
824
+ matches.add(`${pkg.name}@${pkg.version}`);
825
+ for (const module of modules) {
826
+ const modulePath = module?.path;
827
+ if (typeof modulePath !== "string") continue;
828
+ for (const pkgName of _devOnlyPackages)
829
+ if (
830
+ modulePath.includes(`/node_modules/${pkgName}/`) ||
831
+ modulePath.endsWith(`/node_modules/${pkgName}`)
832
+ ) {
833
+ const pkgVersion = packages.find(
834
+ (pkg) => pkg?.name === pkgName,
835
+ )?.version;
836
+ matches.add(pkgVersion ? `${pkgName}@${pkgVersion}` : pkgName);
837
+ matchedModules.push(module);
838
+ }
839
+ }
840
+ return {
841
+ names: Array.from(matches),
842
+ modules: matchedModules,
843
+ };
844
+ }
845
+ const recommendation$12 = {
846
+ id: "dev-only-packages-in-production-bundle",
847
+ title: "Remove dev-only packages from production bundle",
848
+ check: (report) => {
849
+ const { names: bundledDevPackages, modules } = findBundledDevPackages(
850
+ report,
851
+ isVersionGte(getReactNativeVersion(report.packages), "0.74.0")
852
+ ? [...devOnlyPackages, ...rn74PlusDevOnlyPackages]
853
+ : devOnlyPackages,
854
+ );
855
+ if (bundledDevPackages.length === 0) return null;
856
+ return {
857
+ message: `Detected dev-only packages in the production bundle: ${bundledDevPackages.join(", ")}.
858
+
859
+ Move debug-only imports/usage behind \`__DEV__\` checks so Metro can exclude them from release builds, see example below:
860
+
861
+ \`\`\`js
862
+ // Init dev-only packages conditionally
863
+ if (__DEV__) {
864
+ const { whyDidYouRender } = require('@welldone-software/why-did-you-render');
865
+ whyDidYouRender(React, { trackAllPureComponents: true });
866
+ }
867
+
868
+ // Or PropTypes
869
+ if (__DEV__) {
870
+ const PropTypes = require('prop-types');
871
+ Calendar.propTypes = { minDate: PropTypes.string };
872
+ }
873
+ \`\`\`
874
+ `,
875
+ packages: bundledDevPackages,
876
+ docsUrl: "https://reactnative.dev/docs/global-__DEV__",
877
+ ...getModulesSavings(modules),
878
+ };
879
+ },
880
+ };
881
+ //#endregion
882
+ //#region ../shared/src/recommendations/duplicate-packages.ts
883
+ const cliPackageName = "react-native-bundle-discovery-cli";
884
+ const recommendation$11 = {
885
+ id: "duplicate-packages",
886
+ title: "Deduplicate repeated packages in the bundle",
887
+ check(report) {
888
+ const duplicateGroups = getPackageGroups(report, "size").filter(
889
+ (group) => group.entries.length > 1,
890
+ );
891
+ if (duplicateGroups.length === 0) return null;
892
+ return duplicateGroups.map((group) => {
893
+ const { name, entries } = group;
894
+ const versions = Array.from(
895
+ new Set(
896
+ entries.map(
897
+ (entry) => `${entry.name}@${entry.version} (${entry.path})`,
898
+ ),
899
+ ),
900
+ );
901
+ const packages = `${name === "lodash (please use only one)" ? "lodash" : name} x${entries.length}:\n - ${versions.join("\n - ")}`;
902
+ const hasDotLodash = entries.some((e) => e.name.startsWith("lodash."));
903
+ const hasESLodash = entries.some((e) => e.name === "lodash-es");
904
+ const hasSimpleLodash = entries.some((e) => e.name === "lodash");
905
+ const hasUnderscore = entries.some((e) => e.name === "underscore");
906
+ const hasRamda = entries.some((e) => e.name === "ramda");
907
+ const [kept, ...otherEntries] = [...entries].sort(
908
+ (a, b) => b.sizeInBytes - a.sizeInBytes,
909
+ );
910
+ const savings = getPackagesSavings(
911
+ report,
912
+ otherEntries.filter(
913
+ (entry) => entry.absolutePath !== kept.absolutePath,
914
+ ),
915
+ );
916
+ const message = [
917
+ hasDotLodash &&
918
+ hasSimpleLodash &&
919
+ "You can simply use `lodash/*` imports and remove `lodash.*` packages",
920
+ hasESLodash &&
921
+ hasSimpleLodash &&
922
+ "You don't need two copies of `lodash` and `lodash-es`! Just use one",
923
+ (hasSimpleLodash || hasESLodash || hasDotLodash) &&
924
+ hasUnderscore &&
925
+ "You have both `underscore` and `lodash` in your project. Consider using only one library to reduce bundle size.",
926
+ (hasSimpleLodash || hasESLodash || hasDotLodash) &&
927
+ hasRamda &&
928
+ "You have both `ramda` and `lodash` in your project. Consider using only one library to reduce bundle size.",
929
+ ]
930
+ .filter(Boolean)
931
+ .join("; ");
932
+ if (name === "lodash (please use only one)")
933
+ return {
934
+ packages,
935
+ message: `${message}
936
+
937
+ Or you can use \`patch-package\` to remove usage of specific package, example lodash:
938
+ \`\`\`diff
939
+ -const get = require('lodash.get');
940
+ +const get = require('lodash/get');
941
+ \`\`\``,
942
+ docsUrl: null,
943
+ ...savings,
944
+ };
945
+ return {
946
+ packages,
947
+ message: `Found ${duplicateGroups.length} duplicate packages.
948
+
949
+ Align versions or use dependency \`overrides\` (npm) / \`resolutions\` (yarn) to keep a single copy per package.
950
+
951
+ To see more use the next command: "${cliPackageName} packages <file>"`,
952
+ docsUrl: [
953
+ "https://docs.npmjs.com/cli/v10/configuring-npm/package-json#overrides",
954
+ "https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/",
955
+ ],
956
+ ...savings,
957
+ };
958
+ });
959
+ },
960
+ };
961
+ //#endregion
962
+ //#region ../shared/src/recommendations/linear-gradient.ts
963
+ const recommendation$10 = {
964
+ id: "linear-gradient-vs-background-image",
965
+ title:
966
+ "Replace third-party linear gradient libraries with built-in backgroundImage",
967
+ check(report) {
968
+ const packages = report.packages;
969
+ const gradientPkgs = packages.filter(
970
+ (pkg) =>
971
+ pkg?.name === "react-native-linear-gradient" ||
972
+ pkg?.name === "expo-linear-gradient",
973
+ );
974
+ if (gradientPkgs.length === 0) return null;
975
+ const reactNativeVersion = getReactNativeVersion(packages);
976
+ if (!reactNativeVersion || !isVersionGte(reactNativeVersion, "0.76.0"))
977
+ return null;
978
+ const isStableBackgroundImage = isVersionGte(reactNativeVersion, "0.87.0");
979
+ const backgroundImageProp = isStableBackgroundImage
980
+ ? "backgroundImage"
981
+ : "experimental_backgroundImage";
982
+ const docsAnchor = isStableBackgroundImage
983
+ ? "backgroundimage"
984
+ : "experimental_backgroundimage";
985
+ return {
986
+ message: `React Native ships built-in \`linear-gradient()\` support (starting React Native 0.76.x+).
987
+ You can remove ${gradientPkgs.map((e) => e.name).join(", ")}
988
+ and migrate to a simple View with a \`${backgroundImageProp}\` style prop.`,
989
+ packages: [
990
+ ...gradientPkgs.map((e) => `${e.name}@${e.version}`),
991
+ `react-native@${reactNativeVersion}`,
992
+ ],
993
+ docsUrl: `https://reactnative.dev/docs/${formatRnVersionToDocsFormat(reactNativeVersion)}/view-style-props#${docsAnchor}`,
994
+ ...getPackagesSavings(report, gradientPkgs),
995
+ };
996
+ },
997
+ };
998
+ //#endregion
999
+ //#region ../shared/src/recommendations/outdated-packages.ts
1000
+ const MAX_AGE_YEARS = 4;
1001
+ const MS_PER_YEAR = 315576e5;
1002
+ function findOutdatedPackages(report, now = Date.now()) {
1003
+ const matches = /* @__PURE__ */ new Map();
1004
+ for (const pkg of report.packages) {
1005
+ const metadata = pkg?.metadata;
1006
+ if (!metadata || metadata.isLatest || metadata.deprecated) continue;
1007
+ const createdAt = Date.parse(metadata.createdAt);
1008
+ if (Number.isNaN(createdAt)) continue;
1009
+ const ageInYears = (now - createdAt) / MS_PER_YEAR;
1010
+ if (ageInYears < MAX_AGE_YEARS) continue;
1011
+ const id = `${pkg.name}@${pkg.version}`;
1012
+ if (!matches.has(id))
1013
+ matches.set(id, {
1014
+ id,
1015
+ createdAt: metadata.createdAt,
1016
+ ageInYears,
1017
+ latestVersion: metadata.latestVersion ?? null,
1018
+ });
1019
+ }
1020
+ return Array.from(matches.values()).sort(
1021
+ (a, b) => b.ageInYears - a.ageInYears,
1022
+ );
1023
+ }
1024
+ const recommendation$9 = {
1025
+ id: "outdated-packages",
1026
+ title: "Update outdated packages",
1027
+ check: (report) => {
1028
+ const outdatedPackages = findOutdatedPackages(report);
1029
+ if (outdatedPackages.length === 0) return null;
1030
+ const details = outdatedPackages
1031
+ .map(({ id, createdAt, ageInYears, latestVersion }) => {
1032
+ return ` - ${id} (${`published ${createdAt.slice(0, 10)}, ${ageInYears.toFixed(1)} years ago`}${latestVersion ? `, latest: ${latestVersion}` : ""})`;
1033
+ })
1034
+ .join("\n");
1035
+ return {
1036
+ message: `Detected ${outdatedPackages.length} outdated package(s) in the bundle:
1037
+ ${details}
1038
+
1039
+ Old package versions miss bug fixes, performance and bundle size improvements, and may contain known security issues.
1040
+ Upgrade them to the latest version or consider a maintained alternative.
1041
+ `,
1042
+ packages: outdatedPackages.map(({ id }) => id),
1043
+ };
1044
+ },
1045
+ };
1046
+ //#endregion
1047
+ //#region ../shared/src/recommendations/package-json-files.ts
1048
+ function findBundledDependencyPackageJsonFiles(report) {
1049
+ return report.modules.filter((module) =>
1050
+ module?.path?.endsWith("package.json"),
1051
+ );
1052
+ }
1053
+ const recommendation$8 = {
1054
+ id: "bundled-package-json-files",
1055
+ title: "Avoid bundling dependency package.json files",
1056
+ check: (report) => {
1057
+ const packageJsonFiles = findBundledDependencyPackageJsonFiles(report);
1058
+ if (packageJsonFiles.length === 0) return null;
1059
+ const sample = packageJsonFiles
1060
+ .slice(0, 3)
1061
+ .map((module) => module.path)
1062
+ .join("\n - ");
1063
+ return {
1064
+ message: `Detected ${packageJsonFiles.length} dependency package.json file(s) in the bundle.
1065
+ They are often used to get only the package \`version/name\` but all other fields are not needed in the bundle.
1066
+ Remove package.json imports from app code and dependencies (use a \`patch-packages\` for node_modules if needed).
1067
+ Example paths:\n - ${sample}
1068
+ `,
1069
+ packages: [],
1070
+ docsUrl: null,
1071
+ ...getModulesSavings(packageJsonFiles),
1072
+ };
1073
+ },
1074
+ };
1075
+ //#endregion
1076
+ //#region ../shared/src/recommendations/radial-gradient.ts
1077
+ const recommendation$7 = {
1078
+ id: "radial-gradient-vs-background-image",
1079
+ title:
1080
+ "Replace third-party radial gradient libraries with built-in backgroundImage",
1081
+ check(report) {
1082
+ const packages = report.packages;
1083
+ const gradientPkgs = packages.filter(
1084
+ (pkg) =>
1085
+ pkg?.name === "react-native-radial-gradient" ||
1086
+ pkg?.name === "expo-radial-gradient",
1087
+ );
1088
+ if (gradientPkgs.length === 0) return null;
1089
+ const reactNativeVersion = getReactNativeVersion(packages);
1090
+ if (!reactNativeVersion || !isVersionGte(reactNativeVersion, "0.80.0"))
1091
+ return null;
1092
+ const isStableBackgroundImage = isVersionGte(reactNativeVersion, "0.87.0");
1093
+ const backgroundImageProp = isStableBackgroundImage
1094
+ ? "backgroundImage"
1095
+ : "experimental_backgroundImage";
1096
+ const docsAnchor = isStableBackgroundImage
1097
+ ? "backgroundimage"
1098
+ : "experimental_backgroundimage";
1099
+ return {
1100
+ message: `React Native ships built-in \`radial-gradient()\` support (starting React Native 0.80.x+).
1101
+ You can remove ${gradientPkgs.map((e) => e.name).join(", ")}
1102
+ and migrate to a simple View with a \`${backgroundImageProp}\` style prop.`,
1103
+ packages: [
1104
+ ...gradientPkgs.map((e) => `${e.name}@${e.version}`),
1105
+ `react-native@${reactNativeVersion}`,
1106
+ ],
1107
+ docsUrl: `https://reactnative.dev/docs/${formatRnVersionToDocsFormat(reactNativeVersion)}/view-style-props#${docsAnchor}`,
1108
+ ...getPackagesSavings(report, gradientPkgs),
1109
+ };
1110
+ },
1111
+ };
1112
+ //#endregion
1113
+ //#region ../shared/src/recommendations/rn-inline-plugin.ts
1114
+ const TARGET_MODULE_PATH =
1115
+ "react-native/Libraries/Components/Pressable/useAndroidRippleForView.js";
1116
+ const ANDROID_PLATFORM_CHECK_PATTERN = /['"]android['"]===\w\.default\.OS/;
1117
+ const DOCS_URL = "https://github.com/react/react-native/pull/57848";
1118
+ const SAVED_BYTES = {
1119
+ ios: Math.round(18186.24),
1120
+ android: Math.round(20080.64),
1121
+ };
1122
+ function findAffectedModules(report) {
1123
+ return report.modules.find((module) => {
1124
+ return (
1125
+ module?.path?.includes(TARGET_MODULE_PATH) &&
1126
+ ANDROID_PLATFORM_CHECK_PATTERN.test(module?.output?.code ?? "")
1127
+ );
1128
+ });
1129
+ }
1130
+ const recommendation$6 = {
1131
+ id: "rn-inline-platform-plugin",
1132
+ title: "Remove platform-specific dead code from production bundle",
1133
+ check: (report) => {
1134
+ const packages = report.packages;
1135
+ const reactNativeVersion = getReactNativeVersion(packages);
1136
+ if (
1137
+ isVersionGte(reactNativeVersion, "0.88.0") ||
1138
+ !isVersionGte(reactNativeVersion, "0.85.0")
1139
+ )
1140
+ return null;
1141
+ const affectedModule = findAffectedModules(report);
1142
+ if (!affectedModule) return null;
1143
+ return {
1144
+ message: `Detected dead code for ${report?.transformOptions?.platform === "android" ? "iOS" : "Android"} platform.
1145
+
1146
+ You can save bundle size by removing that code as ${report?.transformOptions?.platform === "android" ? "iOS" : "Android"} code won't be executed on ${report?.transformOptions?.platform === "android" ? "Android" : "iOS"} platform.
1147
+
1148
+ 🍏 iOS: -17.76 KB
1149
+ 🤖 Android: -19.61 KB
1150
+
1151
+ To fix the issue you can bump \`@react-native/babel-preset\` to \`0.88.x\` or \`rc\` (if \`0.88.x\` not released yet).`,
1152
+ packages: ["@react-native/babel-preset"],
1153
+ docsUrl: DOCS_URL,
1154
+ sizeInBytes: SAVED_BYTES[report?.transformOptions?.platform],
1155
+ modules: [affectedModule.path],
1156
+ };
1157
+ },
1158
+ };
1159
+ //#endregion
1160
+ //#region ../shared/src/recommendations/rn-issue-abort-controller.ts
1161
+ const recommendation$5 = {
1162
+ id: "rn-issue-abort-controller-polyfill",
1163
+ title: "Remove abort-controller polyfill on React Native 0.87+",
1164
+ check: (report) => {
1165
+ const packages = report.packages;
1166
+ const abortControllerNames = [
1167
+ "abort-controller",
1168
+ "abort-controller-x",
1169
+ "abort-controller-es5",
1170
+ "abortcontroller-polyfill",
1171
+ "@chainsafe/abort-controller",
1172
+ ];
1173
+ const abortControllerPkgs = packages.filter((pkg) =>
1174
+ abortControllerNames.includes(pkg?.name),
1175
+ );
1176
+ if (abortControllerPkgs.length === 0) return null;
1177
+ const reactNativeVersion = getReactNativeVersion(packages);
1178
+ if (!isVersionGte(reactNativeVersion, "0.87.0")) return null;
1179
+ return {
1180
+ message: `React Native 0.87.x+ includes built-in AbortController / AbortSignal support,
1181
+ so you can remove the \`${abortControllerPkgs.map((pkg) => pkg.name).join(", ")}\` polyfill(s) from the bundle.`,
1182
+ packages: [
1183
+ ...abortControllerPkgs.map((pkg) => `${pkg.name}@${pkg.version}`),
1184
+ `react-native@${reactNativeVersion}`,
1185
+ ],
1186
+ docsUrl:
1187
+ "https://github.com/react/react-native/commit/6f3375a140b10cffe9bed3dd72a017ece97bbbba",
1188
+ ...getPackagesSavings(report, abortControllerPkgs),
1189
+ };
1190
+ },
1191
+ };
1192
+ //#endregion
1193
+ //#region ../shared/src/recommendations/rn-issue-babel-runtime.ts
1194
+ const babelRuntimeInlineCode = [
1195
+ "function _interopRequireWildcard(",
1196
+ "function _callSuper(",
1197
+ "function _isNativeReflectConstruct(",
1198
+ "Boolean.prototype.valueOf.call(Reflect.construct(Boolean",
1199
+ "Object.defineProperty)&&Object.getOwnPropertyDescriptor(",
1200
+ ];
1201
+ function hasBabelRuntimeInlineCode(code) {
1202
+ return babelRuntimeInlineCode.some((chunk) => code.includes(chunk));
1203
+ }
1204
+ function hasInlinedBabelRuntimeHelpers(module) {
1205
+ const outputCode = module?.output?.code;
1206
+ const sourceCode = module?.source?.code;
1207
+ if (typeof outputCode !== "string" || typeof sourceCode !== "string")
1208
+ return false;
1209
+ return (
1210
+ hasBabelRuntimeInlineCode(outputCode) &&
1211
+ !hasBabelRuntimeInlineCode(sourceCode)
1212
+ );
1213
+ }
1214
+ const recommendation$4 = {
1215
+ id: "rn-issue-babel-runtime-inline-helpers",
1216
+ title: "Enable Babel runtime helpers deduplication",
1217
+ check: (report) => {
1218
+ const inlinedHelpersModules = report.modules.filter(
1219
+ hasInlinedBabelRuntimeHelpers,
1220
+ );
1221
+ if (inlinedHelpersModules.length === 0) return null;
1222
+ const babelRuntimeVersion = report?.packages?.find(
1223
+ (pkg) => pkg?.name === "@babel/runtime",
1224
+ )?.version;
1225
+ const runtimeVersionForConfig = babelRuntimeVersion ?? "x.x.x";
1226
+ return {
1227
+ message: `Bundle contains inlined Babel runtime helpers in ${inlinedHelpersModules.length} module(s).
1228
+
1229
+ To fix the issue, add the \`enableBabelRuntime\` option to your Babel config,
1230
+ where value is \`@babel/runtime\` version installed in your project:
1231
+
1232
+ \`\`\`js
1233
+ // babel.config.js
1234
+ module.exports = {
1235
+ presets: [
1236
+ [
1237
+ 'module:@react-native/babel-preset',
1238
+ { enableBabelRuntime: '${runtimeVersionForConfig}' }
1239
+ ],
1240
+ ],
1241
+ };
1242
+ \`\`\``,
1243
+ packages: babelRuntimeVersion
1244
+ ? [`@babel/runtime@${babelRuntimeVersion}`]
1245
+ : [],
1246
+ docsUrl: "https://github.com/react/react-native/issues/57123",
1247
+ modules: inlinedHelpersModules.map((module) => module.path),
1248
+ };
1249
+ },
1250
+ };
1251
+ //#endregion
1252
+ //#region ../shared/src/recommendations/rn-issue-hermesv1-profile.ts
1253
+ const filesToCheck = [
1254
+ "react-native/Libraries/Blob/File.js",
1255
+ "react-native/Libraries/Utilities/PixelRatio.js",
1256
+ ];
1257
+ const recommendation$3 = {
1258
+ id: "rn-issue-hermes-transform-profile",
1259
+ title: "Prefer hermes-stable transform profile for React Native 0.84+",
1260
+ check: (report) => {
1261
+ const reactNativeVersion = getReactNativeVersion(report?.packages);
1262
+ if (!isVersionGte(reactNativeVersion, "0.85.0")) return null;
1263
+ if (
1264
+ report.modules.some(
1265
+ (module) =>
1266
+ filesToCheck.some((file) => module?.path?.endsWith(file)) &&
1267
+ module?.output?.code?.includes("class "),
1268
+ )
1269
+ )
1270
+ return null;
1271
+ return {
1272
+ message: `Hermes V1 is enabled, but Babel is targeting ES5 because it uses the default transform profile.
1273
+
1274
+ To fix the issue, you can set the \`unstable_transformProfile\` option to \`hermes-stable\` in your Babel config:
1275
+
1276
+ \`\`\`js
1277
+ // babel.config.js
1278
+ module.exports = {
1279
+ presets: [
1280
+ [
1281
+ 'module:@react-native/babel-preset',
1282
+ { unstable_transformProfile: 'hermes-stable' },
1283
+ ],
1284
+ ],
1285
+ };
1286
+ \`\`\``,
1287
+ packages: [`react-native@${reactNativeVersion}`],
1288
+ docsUrl: [
1289
+ "https://reactnative.dev/blog/2026/02/11/react-native-0.84",
1290
+ "https://github.com/react/react-native/issues/57174",
1291
+ ],
1292
+ };
1293
+ },
1294
+ };
1295
+ //#endregion
1296
+ //#region ../shared/src/recommendations/rn-issue-promise-polyfill.ts
1297
+ const deadCode = "react-native/Libraries/Promise.js";
1298
+ const promiseTryWithResolversEntries = [
1299
+ "promise.try",
1300
+ "promise-try",
1301
+ "core-js/es/promise/try",
1302
+ "promise.withresolvers",
1303
+ "core-js/es/promise/with-resolvers",
1304
+ ];
1305
+ const promiseAnyAllSettledEntries = [
1306
+ "promise.any",
1307
+ "promise-any-polyfill",
1308
+ "core-js/es/promise/any",
1309
+ "promise.allsettled",
1310
+ "core-js/es/promise/all-settled",
1311
+ ];
1312
+ function findMatchingPromisePolyfills(report, entries) {
1313
+ const packages = report.packages;
1314
+ const modules = report.modules;
1315
+ const matches = /* @__PURE__ */ new Set();
1316
+ const matchedModules = [];
1317
+ const entriesCoreJs = entries.filter((entry) => entry.startsWith("core-js/"));
1318
+ const entriesNonCoreJs = entries.filter(
1319
+ (entry) => !entry.startsWith("core-js/"),
1320
+ );
1321
+ packages.forEach((pkg) => {
1322
+ if (entriesNonCoreJs.includes(pkg?.name)) {
1323
+ matches.add(pkg.name);
1324
+ matchedModules.push(...getPackageModules(report, pkg));
1325
+ }
1326
+ });
1327
+ modules.forEach((module) => {
1328
+ if (entriesCoreJs.some((entry) => module?.path?.includes(entry))) {
1329
+ matches.add(module.path);
1330
+ matchedModules.push(module);
1331
+ }
1332
+ });
1333
+ return {
1334
+ names: Array.from(matches),
1335
+ modules: matchedModules,
1336
+ };
1337
+ }
1338
+ const recommendation$2 = {
1339
+ id: "rn-issue-promise-polyfill",
1340
+ title: "Remove Promise polyfills",
1341
+ check: (report) => {
1342
+ if (report.kind === "webpack") return null;
1343
+ const packages = report.packages;
1344
+ const modules = report.modules;
1345
+ const reactNativeVersion = getReactNativeVersion(packages);
1346
+ const deadPromiseModules = modules.filter((module) =>
1347
+ module?.path?.includes(deadCode),
1348
+ );
1349
+ const hasDeadPromiseModule = deadPromiseModules.length > 0;
1350
+ const removablePolyfills = [];
1351
+ const removableModules = [...deadPromiseModules];
1352
+ const entriesToCheck = [
1353
+ isVersionGte(reactNativeVersion, "0.86.0") &&
1354
+ promiseTryWithResolversEntries,
1355
+ isVersionGte(reactNativeVersion, "0.75.0") && promiseAnyAllSettledEntries,
1356
+ ];
1357
+ for (const entries of entriesToCheck)
1358
+ if (entries) {
1359
+ const found = findMatchingPromisePolyfills(report, entries);
1360
+ removablePolyfills.push(...found.names);
1361
+ removableModules.push(...found.modules);
1362
+ }
1363
+ const uniquePolyfills = Array.from(new Set(removablePolyfills));
1364
+ if (!hasDeadPromiseModule && uniquePolyfills.length === 0) return null;
1365
+ const messageParts = [];
1366
+ if (hasDeadPromiseModule)
1367
+ messageParts.push(`Bundle contains \`react-native/Libraries/Promise.js\`.
1368
+ This file is dead code because Hermes already provides Promise out of the box.
1369
+ You can update 'metro.config.js' to remove this file from the bundle:
1370
+
1371
+ \`\`\`js
1372
+ const { createResolveRequest } = require('react-native-bundle-discovery');
1373
+ const resolveRequest = createResolveRequest({ removePromisePolyfill: true });
1374
+
1375
+ const config = {
1376
+ resolver: { resolveRequest },
1377
+ };
1378
+ \`\`\``);
1379
+ if (uniquePolyfills.length > 0)
1380
+ messageParts.push(`Bundle also includes Promise polyfill modules/packages that Hermes already supports natively.
1381
+ Consider removing: ${uniquePolyfills.join(", ")}.`);
1382
+ return {
1383
+ message: messageParts.join("\n\n"),
1384
+ packages: reactNativeVersion
1385
+ ? [`react-native@${reactNativeVersion}`]
1386
+ : [],
1387
+ docsUrl: [
1388
+ "https://github.com/react/react-native/issues/57702",
1389
+ "https://github.com/react/react-native/pull/57215",
1390
+ ],
1391
+ ...getModulesSavings(removableModules),
1392
+ };
1393
+ },
1394
+ };
1395
+ //#endregion
1396
+ //#region ../shared/src/recommendations/worklets-bundle-mode.ts
1397
+ const BUNDLE_MODE_MODULES_PATH =
1398
+ "node_modules/react-native-worklets/.worklets/";
1399
+ const recommendation$1 = {
1400
+ id: "worklets-bundle-mode",
1401
+ title: "Enable Bundle Mode in react-native-worklets",
1402
+ check: (report) => {
1403
+ const packages = report.packages;
1404
+ const workletsPkg = packages.find(
1405
+ (pkg) =>
1406
+ pkg?.name === "react-native-worklets" &&
1407
+ isVersionGte(pkg.version, "0.10.0"),
1408
+ );
1409
+ if (!workletsPkg) return null;
1410
+ const reactNativeVersion = getReactNativeVersion(packages);
1411
+ if (!isVersionGte(reactNativeVersion, "0.82.4")) return null;
1412
+ if (
1413
+ report.modules.some((module) =>
1414
+ module?.path?.includes(BUNDLE_MODE_MODULES_PATH),
1415
+ )
1416
+ )
1417
+ return null;
1418
+ return {
1419
+ message: `\`react-native-worklets\` 0.10.x+ supports Bundle Mode, which gives worklets access to the entire JavaScript bundle.
1420
+ Third-party libraries can be used in worklets without patching, and worklets benefit from pre-compiled bytecode instead of runtime evaluation.
1421
+ Consider enabling Bundle Mode (it will become the default way of using worklets in the future).
1422
+ More details: https://x.com/tell_me_mur/status/2105190960363880647`,
1423
+ packages: [
1424
+ `${workletsPkg.name}@${workletsPkg.version}`,
1425
+ `react-native@${reactNativeVersion}`,
1426
+ ],
1427
+ docsUrl:
1428
+ "https://docs.swmansion.com/react-native-worklets/docs/bundleMode/",
1429
+ };
1430
+ },
1431
+ };
1432
+ //#endregion
1433
+ //#region ../shared/src/recommendations/wrong-platform-files.ts
1434
+ const OTHER_PLATFORM = {
1435
+ ios: "android",
1436
+ android: "ios",
1437
+ };
1438
+ const PLATFORM_CHECK_PATTERN = /is[-_]?(android|ios)/gi;
1439
+ function findWrongPlatformFiles(report, otherPlatform) {
1440
+ return report.modules.filter((module) =>
1441
+ module.path
1442
+ .replace(`${report.rootFolder}/`, "")
1443
+ .replace(PLATFORM_CHECK_PATTERN, "")
1444
+ .includes(otherPlatform),
1445
+ );
1446
+ }
1447
+ //#endregion
1448
+ //#region ../shared/src/recommendations/index.ts
1449
+ const recommendations = [
1450
+ recommendation$10,
1451
+ recommendation$7,
1452
+ recommendation$11,
1453
+ recommendation$5,
1454
+ recommendation$4,
1455
+ recommendation$3,
1456
+ recommendation$6,
1457
+ recommendation$2,
1458
+ recommendation$12,
1459
+ recommendation$8,
1460
+ recommendation$13,
1461
+ recommendation$14,
1462
+ recommendation$9,
1463
+ recommendation$1,
1464
+ {
1465
+ id: "wrong-platform-files",
1466
+ title: "Exclude other platform files from the bundle",
1467
+ check: (report) => {
1468
+ const platform = report?.transformOptions?.platform;
1469
+ const otherPlatform = OTHER_PLATFORM[platform];
1470
+ if (!otherPlatform) return null;
1471
+ const wrongFiles = findWrongPlatformFiles(report, otherPlatform);
1472
+ if (wrongFiles.length === 0) return null;
1473
+ const sample = wrongFiles
1474
+ .slice(0, 5)
1475
+ .map((module) => module.path.replace(`${report.rootFolder}/`, ""))
1476
+ .join("\n - ");
1477
+ return {
1478
+ message: `Detected ${wrongFiles.length} file(s) with "${otherPlatform}" in the path in the ${platform} bundle.
1479
+ These files are likely not used on ${platform} and can be excluded to save bundle size.
1480
+ Example paths:\n - ${sample}`,
1481
+ packages: [],
1482
+ docsUrl: null,
1483
+ ...getModulesSavings(wrongFiles),
1484
+ };
1485
+ },
1486
+ },
1487
+ ];
1488
+ function collectRecommendations(report) {
1489
+ return recommendations.flatMap((recommendation) => {
1490
+ const finding = recommendation.check(report);
1491
+ if (!finding) return [];
1492
+ return (Array.isArray(finding) ? finding : [finding]).map((f) => {
1493
+ return {
1494
+ id: recommendation.id,
1495
+ title: recommendation.title,
1496
+ ...f,
1497
+ };
1498
+ });
1499
+ });
1500
+ }
1501
+ function withRecommendations(report) {
1502
+ return Object.assign(report, {
1503
+ recommendations:
1504
+ report.transformOptions?.dev === false
1505
+ ? collectRecommendations(prepareReport({ ...report }, "", true))
1506
+ : null,
1507
+ });
1508
+ }
1509
+ //#endregion
1510
+ Object.defineProperty(exports, "isEsbuildMetafilePath", {
1511
+ enumerable: true,
1512
+ get: function () {
1513
+ return isEsbuildMetafilePath;
1514
+ },
1515
+ });
1516
+ Object.defineProperty(exports, "isRsdoctorReportPath", {
1517
+ enumerable: true,
1518
+ get: function () {
1519
+ return isRsdoctorReportPath;
1520
+ },
1521
+ });
1522
+ Object.defineProperty(exports, "transformEsbuildMetafile", {
1523
+ enumerable: true,
1524
+ get: function () {
1525
+ return transformEsbuildMetafile;
1526
+ },
1527
+ });
1528
+ Object.defineProperty(exports, "transformRSDoctorData", {
1529
+ enumerable: true,
1530
+ get: function () {
1531
+ return transformRSDoctorData;
1532
+ },
1533
+ });
1534
+ Object.defineProperty(exports, "withComparison", {
1535
+ enumerable: true,
1536
+ get: function () {
1537
+ return withComparison;
1538
+ },
1539
+ });
1540
+ Object.defineProperty(exports, "withRecommendations", {
1541
+ enumerable: true,
1542
+ get: function () {
1543
+ return withRecommendations;
1544
+ },
1545
+ });