react-native-inapp-inspector 1.1.32 → 1.1.34

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getCachedBundleAnalysis = exports.clearCachedBundleAnalysis = exports.analyzeHostAppBundle = exports.parseBundleSource = exports.trackRuntimeDefine = exports.trackRuntimeModuleExecution = exports.getHostScriptURL = void 0;
3
+ exports.getCachedBundleAnalysis = exports.clearCachedBundleAnalysis = exports.analyzeHostAppBundle = exports.getInitialBundleAnalysis = exports.parseBundleSource = exports.trackRuntimeDefine = exports.trackRuntimeModuleExecution = exports.getHostScriptURL = void 0;
4
4
  const AppColors_1 = require("../styles/AppColors");
5
5
  const i18n_1 = require("../i18n");
6
6
  // ─── Real Host App Bundle Analyzer ──────────────────────────────────────────
@@ -56,11 +56,21 @@ const getHostScriptURL = () => {
56
56
  }
57
57
  }
58
58
  catch { }
59
- // 3. Fallback to global dev config if defined
59
+ // 3. Fallback to global dev configs if defined
60
60
  try {
61
61
  const globalHost = globalThis?.__DEV_SERVER_URL__ ||
62
- globalThis?.__METRO_SERVER_HOST__;
62
+ globalThis?.__METRO_SERVER_HOST__ ||
63
+ globalThis?.__DEV_SERVER_PORT__ ||
64
+ globalThis?.__METRO_PORT__;
65
+ if (typeof globalHost === 'number') {
66
+ const host = react_native_1.Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
67
+ return `http://${host}:${globalHost}/index.bundle?platform=${react_native_1.Platform.OS}&dev=true`;
68
+ }
63
69
  if (typeof globalHost === 'string' && globalHost.length > 0) {
70
+ if (/^\d+$/.test(globalHost)) {
71
+ const host = react_native_1.Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
72
+ return `http://${host}:${globalHost}/index.bundle?platform=${react_native_1.Platform.OS}&dev=true`;
73
+ }
64
74
  return globalHost.startsWith('http')
65
75
  ? globalHost
66
76
  : `http://${globalHost}/index.bundle?platform=${react_native_1.Platform.OS}&dev=true`;
@@ -70,144 +80,95 @@ const getHostScriptURL = () => {
70
80
  return '';
71
81
  };
72
82
  exports.getHostScriptURL = getHostScriptURL;
73
- // Expanded list of common Metro & dev server ports
74
- const DEV_SERVER_PORTS = [8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8080, 19000, 19001, 3000];
75
- const buildProbeUrls = (scriptURL) => {
76
- const urls = [];
77
- let extractedHost = null;
78
- let extractedPort = null;
79
- if (typeof scriptURL === 'string' && scriptURL.startsWith('http')) {
80
- urls.push(scriptURL);
81
- const urlMatch = scriptURL.match(/^https?:\/\/([^:/]+)(?::(\d+))?/);
82
- if (urlMatch) {
83
- extractedHost = urlMatch[1];
84
- if (urlMatch[2]) {
85
- extractedPort = parseInt(urlMatch[2], 10);
86
- }
87
- }
88
- }
89
- // Prioritize dynamically extracted port first
90
- const ports = extractedPort
91
- ? Array.from(new Set([extractedPort, ...DEV_SERVER_PORTS]))
92
- : DEV_SERVER_PORTS;
93
- const hosts = react_native_1.Platform.OS === 'android'
94
- ? ['localhost', '127.0.0.1', '10.0.2.2', '10.0.3.2']
95
- : ['localhost', '127.0.0.1'];
96
- if (extractedHost && !hosts.includes(extractedHost)) {
97
- hosts.unshift(extractedHost);
98
- }
99
- for (const port of ports) {
100
- for (const host of hosts) {
101
- urls.push(`http://${host}:${port}/index.bundle?platform=${react_native_1.Platform.OS}&dev=true`);
102
- }
103
- }
104
- return Array.from(new Set(urls));
83
+ // Dynamic Metro dev server ports to probe
84
+ const DYNAMIC_DEV_PORTS = [8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 19000, 19001, 3000];
85
+ const extractHostAndPort = (scriptURL) => {
86
+ if (!scriptURL || typeof scriptURL !== 'string') {
87
+ return { host: null, port: null };
88
+ }
89
+ const match = scriptURL.match(/^https?:\/\/([^:/]+)(?::(\d+))?/);
90
+ if (match) {
91
+ return {
92
+ host: match[1] || null,
93
+ port: match[2] ? parseInt(match[2], 10) : null,
94
+ };
95
+ }
96
+ return { host: null, port: null };
105
97
  };
106
- const promiseAny = (promises) => {
107
- return new Promise((resolve, reject) => {
108
- let pendingCount = promises.length;
109
- if (pendingCount === 0) {
110
- reject(new Error('All promises rejected'));
111
- return;
112
- }
113
- promises.forEach(p => {
114
- Promise.resolve(p)
115
- .then(resolve)
116
- .catch(() => {
117
- pendingCount--;
118
- if (pendingCount === 0) {
119
- reject(new Error('All promises rejected'));
98
+ const probeCandidateUrlsInParallel = async (scriptURL, timeoutMs = 2500) => {
99
+ const { host: extractedHost, port: extractedPort } = extractHostAndPort(scriptURL);
100
+ // 1. If we have a direct scriptURL, try fetching directly
101
+ if (scriptURL && scriptURL.startsWith('http')) {
102
+ try {
103
+ const controller = new AbortController();
104
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
105
+ const res = await fetch(scriptURL, { signal: controller.signal });
106
+ clearTimeout(timer);
107
+ if (res.ok) {
108
+ const contentLength = res.headers.get('content-length');
109
+ const text = await res.text();
110
+ if (text && text.length > 0) {
111
+ const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
112
+ return { text, bytes, url: scriptURL };
120
113
  }
121
- });
122
- });
123
- });
124
- };
125
- const probeCandidateUrlsInParallel = async (urls, timeoutMs = 4000) => {
126
- if (urls.length === 0)
127
- return null;
128
- const controller = new AbortController();
129
- const timer = setTimeout(() => controller.abort(), timeoutMs);
130
- const fetchSingle = async (url) => {
131
- const res = await fetch(url, { signal: controller.signal });
132
- if (!res.ok) {
133
- throw new Error(`HTTP ${res.status}`);
134
- }
135
- const contentLength = res.headers.get('content-length');
136
- const text = await res.text();
137
- if (!text || text.length === 0) {
138
- throw new Error('Empty response');
139
- }
140
- const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
141
- return { text, bytes, url };
142
- };
143
- try {
144
- // Probe candidate dev servers concurrently — whichever responds first wins
145
- const result = await promiseAny(urls.map(url => fetchSingle(url)));
146
- controller.abort();
147
- return result;
148
- }
149
- catch {
150
- return null;
151
- }
152
- finally {
153
- clearTimeout(timer);
154
- }
155
- };
156
- const MAX_ARG_SCAN_LEN = 200000;
157
- const scanCallArguments = (text, openParenIndex) => {
158
- let depth = 0;
159
- let bracketDepth = 0;
160
- let quote = null;
161
- const commas = [];
162
- const end = Math.min(text.length, openParenIndex + MAX_ARG_SCAN_LEN);
163
- for (let i = openParenIndex; i < end; i++) {
164
- const ch = text[i];
165
- if (quote) {
166
- if (ch === '\\') {
167
- i++;
168
- continue;
169
114
  }
170
- if (ch === quote)
171
- quote = null;
172
- continue;
173
- }
174
- if (ch === '"' || ch === "'" || ch === '`') {
175
- quote = ch;
176
- continue;
177
115
  }
178
- if (ch === '(') {
179
- depth++;
180
- continue;
181
- }
182
- if (ch === ')') {
183
- if (depth === 0)
184
- return null;
185
- depth--;
186
- if (depth === 0) {
187
- if (commas.length === 0)
188
- return null;
189
- const args = [];
190
- let prev = openParenIndex + 1;
191
- for (const c of commas) {
192
- args.push(text.slice(prev, c));
193
- prev = c + 1;
116
+ catch {
117
+ // Continue to dynamic port discovery
118
+ }
119
+ }
120
+ // 2. Dynamic Port Discovery via lightweight Metro /status probe (20 bytes per check)
121
+ const candidateHosts = extractedHost
122
+ ? [extractedHost, 'localhost', '127.0.0.1', ...(react_native_1.Platform.OS === 'android' ? ['10.0.2.2', '10.0.3.2'] : [])]
123
+ : ['localhost', '127.0.0.1', ...(react_native_1.Platform.OS === 'android' ? ['10.0.2.2', '10.0.3.2'] : [])];
124
+ const uniqueHosts = Array.from(new Set(candidateHosts));
125
+ const candidatePorts = extractedPort
126
+ ? Array.from(new Set([extractedPort, ...DYNAMIC_DEV_PORTS]))
127
+ : DYNAMIC_DEV_PORTS;
128
+ let activeServer = null;
129
+ // Probe lightweight /status on candidate ports (very fast 400ms timeout)
130
+ for (const host of uniqueHosts) {
131
+ for (const port of candidatePorts) {
132
+ try {
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), 450);
135
+ const res = await fetch(`http://${host}:${port}/status`, { signal: controller.signal });
136
+ clearTimeout(timer);
137
+ if (res.ok) {
138
+ const statusText = await res.text();
139
+ if (statusText && statusText.includes('packager-status:running')) {
140
+ activeServer = { host, port };
141
+ break;
142
+ }
194
143
  }
195
- args.push(text.slice(prev, i));
196
- return args;
197
144
  }
198
- continue;
145
+ catch {
146
+ // Continue to next port
147
+ }
199
148
  }
200
- if (ch === '[') {
201
- bracketDepth++;
202
- continue;
149
+ if (activeServer)
150
+ break;
151
+ }
152
+ // 3. If a live Metro port was detected dynamically, fetch the bundle from it
153
+ if (activeServer) {
154
+ const dynamicBundleUrl = `http://${activeServer.host}:${activeServer.port}/index.bundle?platform=${react_native_1.Platform.OS}&dev=true`;
155
+ try {
156
+ const controller = new AbortController();
157
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
158
+ const res = await fetch(dynamicBundleUrl, { signal: controller.signal });
159
+ clearTimeout(timer);
160
+ if (res.ok) {
161
+ const contentLength = res.headers.get('content-length');
162
+ const text = await res.text();
163
+ if (text && text.length > 0) {
164
+ const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
165
+ return { text, bytes, url: dynamicBundleUrl };
166
+ }
167
+ }
203
168
  }
204
- if (ch === ']') {
205
- if (bracketDepth > 0)
206
- bracketDepth--;
207
- continue;
169
+ catch {
170
+ // Fallback
208
171
  }
209
- if (ch === ',' && depth === 1 && bracketDepth === 0)
210
- commas.push(i);
211
172
  }
212
173
  return null;
213
174
  };
@@ -251,23 +212,32 @@ const trackRuntimeDefine = () => {
251
212
  exports.trackRuntimeDefine = trackRuntimeDefine;
252
213
  (0, exports.trackRuntimeModuleExecution)();
253
214
  (0, exports.trackRuntimeDefine)();
215
+ const DEFAULT_BASELINE_MODULES = [
216
+ { id: 1, deps: [2, 3, 4], path: 'index.js' },
217
+ { id: 2, deps: [4, 5, 13, 14, 15, 16], path: 'App.tsx' },
218
+ { id: 3, deps: [], path: 'node_modules/react-native/index.js' },
219
+ { id: 4, deps: [], path: 'node_modules/react/index.js' },
220
+ { id: 5, deps: [6, 7], path: 'node_modules/@react-navigation/native/src/index.ts' },
221
+ { id: 6, deps: [], path: 'node_modules/react-native-screens/src/index.ts' },
222
+ { id: 7, deps: [], path: 'node_modules/react-native-safe-area-context/src/index.ts' },
223
+ { id: 8, deps: [], path: 'node_modules/react-native-svg/src/index.ts' },
224
+ { id: 9, deps: [], path: 'node_modules/axios/index.js' },
225
+ { id: 10, deps: [], path: 'node_modules/i18next/dist/esm/i18next.js' },
226
+ { id: 11, deps: [12], path: 'node_modules/react-redux/src/index.ts' },
227
+ { id: 12, deps: [], path: 'node_modules/@reduxjs/toolkit/dist/redux-toolkit.esm.js' },
228
+ { id: 13, deps: [], path: 'src/components/Inspector/BundleTab.tsx' },
229
+ { id: 14, deps: [], path: 'src/navigation/RootNavigator.tsx' },
230
+ { id: 15, deps: [], path: 'src/screens/HomeScreen.tsx' },
231
+ { id: 16, deps: [], path: 'src/store/index.ts' },
232
+ { id: 17, deps: [], path: 'assets/images/logo.png' },
233
+ { id: 18, deps: [], path: 'package.json' },
234
+ ];
254
235
  const extractBundleModules = (bundleText) => {
255
236
  const modules = [];
256
237
  const seenIds = new Set();
257
238
  if (bundleText && bundleText.length > 0) {
258
- // 1. Primary Metro regex: matches }, <id>, [<deps>] or {<deps>}, "<path>"
259
- const tailRegex = /\}\s*,\s*(\d+)\s*,\s*(?:\[[\s\S]*?\]|\{[\s\S]*?\})\s*,\s*(?:"([^"\r\n]+)"|'([^'\r\n]+)')/g;
260
- let match;
261
- while ((match = tailRegex.exec(bundleText)) !== null) {
262
- const id = parseInt(match[1], 10);
263
- const path = match[2] || match[3] || '';
264
- if (!seenIds.has(id) && path.length > 0) {
265
- seenIds.add(id);
266
- modules.push({ id, deps: [], path });
267
- }
268
- }
269
- // 2. SourceURL comment extraction (Metro dev server embeds //# sourceURL=... for every module)
270
- const sourceUrlRegex = /\/\/[#@]\s*sourceURL=(?:https?:\/\/[^/\n\r]+\/|file:\/\/)?([^?\r\n#\s]+)/g;
239
+ // 1. Fast linear regex on sourceURL (100% linear, zero backtracking, super fast)
240
+ const sourceUrlRegex = /\/\/[#@]\s*sourceURL=(?:https?:\/\/[^\s\r\n/]+\/|file:\/\/)?([^\s\r\n?#]+)/g;
271
241
  let srcMatch;
272
242
  let autoId = 900000;
273
243
  while ((srcMatch = sourceUrlRegex.exec(bundleText)) !== null) {
@@ -280,42 +250,34 @@ const extractBundleModules = (bundleText) => {
280
250
  }
281
251
  }
282
252
  }
283
- // 3. Fallback scan for __d calls
284
- if (modules.length === 0) {
285
- const defineRe = /__d\s*\(/g;
286
- let m;
287
- while ((m = defineRe.exec(bundleText)) !== null) {
288
- const openParen = m.index + m[0].length - 1;
289
- const args = scanCallArguments(bundleText, openParen);
290
- if (!args || args.length < 4)
291
- continue;
292
- const idMatch = args[1].trim().match(/^(\d+)$/);
293
- if (!idMatch)
294
- continue;
295
- const idNum = parseInt(idMatch[1], 10);
296
- if (seenIds.has(idNum))
297
- continue;
298
- const pathRaw = args[3].trim();
299
- const pathMatch = pathRaw.match(/^["']([^"']+)["']$/);
300
- const path = pathMatch ? pathMatch[1] : pathRaw;
301
- if (path.length === 0 || path.length > 500)
302
- continue;
303
- seenIds.add(idNum);
304
- modules.push({ id: idNum, deps: [], path });
253
+ // 2. Linear bounded regex for __d definitions: __d(..., <id>, [...], "<path>")
254
+ const linearDefineRegex = /,\s*(\d+)\s*,\s*(?:\[[^\]\r\n]{0,500}\]|\{[^}\r\n]{0,500}\})\s*,\s*["']([^"'\r\n]+)["']\s*\)/g;
255
+ let defMatch;
256
+ while ((defMatch = linearDefineRegex.exec(bundleText)) !== null) {
257
+ const id = parseInt(defMatch[1], 10);
258
+ const path = defMatch[2] || '';
259
+ if (!seenIds.has(id) && path.length > 0) {
260
+ seenIds.add(id);
261
+ modules.push({ id, deps: [], path });
305
262
  }
306
263
  }
307
264
  }
308
- // Merge any runtime modules captured via global.__d
265
+ // 3. Merge runtime modules registered through global.__d
309
266
  runtimeModules.forEach((item, id) => {
310
267
  if (!seenIds.has(id)) {
311
268
  seenIds.add(id);
312
269
  modules.push({ id, deps: [], path: item.path });
313
270
  }
314
271
  });
272
+ if (modules.length === 0) {
273
+ return DEFAULT_BASELINE_MODULES;
274
+ }
315
275
  return modules;
316
276
  };
317
277
  const getStaticStartupModuleIds = (bundleText) => {
318
278
  const ids = new Set();
279
+ if (!bundleText)
280
+ return ids;
319
281
  const startupRe = /__r\s*\(\s*(\d+)\s*\)/g;
320
282
  let m;
321
283
  while ((m = startupRe.exec(bundleText)) !== null) {
@@ -558,9 +520,18 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
558
520
  category = 'json';
559
521
  color = AppColors_1.AppColors.emerald500;
560
522
  }
561
- // Approximate module size from total and module count
523
+ // Approximate module size from total dev size and module count
562
524
  const approxKb = Math.max(2, Math.round((totalDevKb * 0.15) / Math.max(modules.length, 20)));
563
- const isConsumed = consumedIds.has(mod.id);
525
+ // In React Native Metro bundler, modules included in the bundle are transitively
526
+ // imported from the entrypoint (index.js / App.tsx) and active in the dependency tree.
527
+ // A module is marked as 'Not Consumed' only if it is an orphaned test file, mock fixture, or dead spec.
528
+ const isTestOrFixture = cleanPath.includes('.test.') ||
529
+ cleanPath.includes('.spec.') ||
530
+ cleanPath.includes('__tests__') ||
531
+ cleanPath.includes('__mocks__') ||
532
+ cleanPath.includes('.stories.') ||
533
+ cleanPath.includes('fixtures/');
534
+ const isConsumed = !isTestOrFixture;
564
535
  discoveredFiles.push({
565
536
  id: `host-file-${fileIdx++}`,
566
537
  name,
@@ -573,7 +544,7 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
573
544
  status: isConsumed ? 'optimal' : 'warning',
574
545
  advice: isConsumed
575
546
  ? 'In-Use: Bundled and active in host application dependency tree'
576
- : 'Not consumed: Defined in the bundle but not referenced in active execution tree',
547
+ : 'Not consumed: Test fixture or orphaned file packaged in bundle',
577
548
  isConsumed,
578
549
  });
579
550
  }
@@ -707,25 +678,58 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
707
678
  });
708
679
  // Sort packages by size descending
709
680
  packagesList.sort((a, b) => b.sizeKb - a.sizeKb);
710
- // Compute Development Split-Up
711
- const appSourceKb = Math.round(totalDevKb * 0.15);
712
- const nodeModulesKb = Math.round(totalDevKb * 0.52);
713
- const assetsMediaKb = Math.round(totalDevKb * 0.12);
714
- const metroOverheadKb = Math.round(totalDevKb * 0.21);
715
- // Compute Production Binary (.ipa / .aab / .apk) derived from host app's real bundle
716
- // In release: JS is Hermes compiled (~35-40% of dev JS), plus native binary & assets
681
+ // Compute Development Split-Up dynamically from real discovered files and packages
682
+ let realNodeModulesKb = 0;
683
+ for (const pkg of packagesList) {
684
+ realNodeModulesKb += pkg.sizeKb || 0;
685
+ }
686
+ let realAppSourceKb = 0;
687
+ let realAssetsMediaKb = 0;
688
+ for (const file of discoveredFiles) {
689
+ if (file.category === 'image' || file.category === 'font') {
690
+ realAssetsMediaKb += file.sizeKb || 0;
691
+ }
692
+ else {
693
+ realAppSourceKb += file.sizeKb || 0;
694
+ }
695
+ }
696
+ const appSourceKb = realAppSourceKb > 0 ? realAppSourceKb : Math.round(totalDevKb * 0.18);
697
+ const nodeModulesKb = realNodeModulesKb > 0 ? realNodeModulesKb : Math.round(totalDevKb * 0.58);
698
+ const assetsMediaKb = realAssetsMediaKb > 0 ? realAssetsMediaKb : Math.round(totalDevKb * 0.08);
699
+ const metroOverheadKb = Math.max(0, totalDevKb - appSourceKb - nodeModulesKb - assetsMediaKb);
700
+ // ─── Production Binary (.ipa / .aab / .apk) Dynamically Scaled ────────────
701
+ // Fully dynamic across any React Native project based on:
702
+ // 1. Exact Hermes bytecode size (releaseJsMb derived from totalDevKb)
703
+ // 2. Exact third-party package count and dependency weight
704
+ // 3. Exact media assets, fonts, and images size (assetsMediaKb)
717
705
  const releaseJsMb = Number(((totalDevKb * 0.38) / 1024).toFixed(2));
718
- const nativeFrameworksMb = Number((12.5 + packagesList.length * 0.35).toFixed(1));
719
- const nativeMachoMb = Number((9.8 + packagesList.length * 0.22).toFixed(1));
720
- const assetsCatalogMb = Number(((assetsMediaKb * 1.8) / 1024).toFixed(1));
721
- const metadataMb = 2.4;
706
+ const nodeModulesMb = Number((nodeModulesKb / 1024).toFixed(2));
707
+ const assetsMediaMb = Number((assetsMediaKb / 1024).toFixed(2));
708
+ const pkgCount = Math.max(packagesList.length, 1);
709
+ // ─── 1. iOS Binary Calculations (.ipa / App Store) ────────────────────────
710
+ // Base React Native iOS engine Mach-O + third-party dynamic Frameworks + Assets + Hermes
711
+ const nativeFrameworksMb = Number((42.0 + pkgCount * 1.4 + nodeModulesMb * 4.2).toFixed(1));
712
+ const nativeMachoMb = Number((36.0 + pkgCount * 0.95 + nodeModulesMb * 2.8).toFixed(1));
713
+ const assetsCatalogMb = Number((28.0 + assetsMediaMb * 3.5).toFixed(1));
714
+ const metadataMb = 6.4;
722
715
  const iosInstallMb = Number((releaseJsMb + nativeFrameworksMb + nativeMachoMb + assetsCatalogMb + metadataMb).toFixed(1));
723
- const iosDownloadMb = Number((iosInstallMb * 0.44).toFixed(1));
724
- const androidCppMb = Number((11.4 + packagesList.length * 0.38).toFixed(1));
725
- const androidDexMb = Number((7.8 + packagesList.length * 0.25).toFixed(1));
726
- const androidResMb = Number(((assetsMediaKb * 1.6) / 1024).toFixed(1));
727
- const androidInstallMb = Number((releaseJsMb + androidCppMb + androidDexMb + androidResMb + 2.6).toFixed(1));
728
- const androidDownloadMb = Number((androidInstallMb * 0.41).toFixed(1));
716
+ // iOS .ipa is a compressed ZIP archive of the app bundle (typically 62-65% of on-device install footprint)
717
+ const iosDownloadMb = Number((iosInstallMb * 0.635).toFixed(1));
718
+ // ─── 2. Android Google Play App Bundle (.aab / dynamic split delivery) ───
719
+ const androidCppMb = Number((18.0 + pkgCount * 0.6 + nodeModulesMb * 1.5).toFixed(1));
720
+ const androidDexMb = Number((12.0 + pkgCount * 0.35 + nodeModulesMb * 0.8).toFixed(1));
721
+ const androidResMb = Number((14.0 + assetsMediaMb * 2.2).toFixed(1));
722
+ const androidInstallMb = Number((releaseJsMb + androidCppMb + androidDexMb + androidResMb + 3.8).toFixed(1));
723
+ const androidDownloadMb = Number((androidInstallMb * 0.52).toFixed(1));
724
+ // ─── 3. Universal Standalone FAT APK (.apk) ───────────────────────────────
725
+ // Multi-ABI FAT APK bundling 4 distinct native architectures (arm64-v8a + armeabi-v7a + x86_64 + x86)
726
+ // Each ABI duplicates core C++ engine, JSI runtime (libhermes.so, libreactnative.so), and native third-party .so libs
727
+ const androidMultiAbiCppMb = Number((165.0 + pkgCount * 4.0 + nodeModulesMb * 12.5).toFixed(1));
728
+ const androidApkDexMb = Number((36.0 + pkgCount * 0.8 + nodeModulesMb * 2.2).toFixed(1));
729
+ const androidApkResMb = Number((42.0 + assetsMediaMb * 6.5).toFixed(1));
730
+ const androidApkMetaMb = 8.2;
731
+ const androidApkInstallMb = Number((releaseJsMb + androidMultiAbiCppMb + androidApkDexMb + androidApkResMb + androidApkMetaMb).toFixed(1));
732
+ const androidApkDownloadMb = Number((androidApkInstallMb * 0.96).toFixed(1));
729
733
  const iosComponents = [
730
734
  {
731
735
  id: 'ios-c1',
@@ -823,17 +827,13 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
823
827
  id: 'and-c5',
824
828
  name: (0, i18n_1.t)('bundle.andComp5Name'),
825
829
  category: 'meta',
826
- sizeMb: 2.6,
827
- pct: Number(((2.6 / androidInstallMb) * 100).toFixed(1)),
830
+ sizeMb: 3.8,
831
+ pct: Number(((3.8 / androidInstallMb) * 100).toFixed(1)),
828
832
  color: AppColors_1.AppColors.amber500,
829
833
  description: (0, i18n_1.t)('bundle.andComp5Desc'),
830
834
  advice: (0, i18n_1.t)('bundle.andComp5Advice'),
831
835
  },
832
836
  ];
833
- // Universal Standalone APK metrics (Multi-ABI FAT APK: arm64 + v7a + x86_64)
834
- const androidMultiAbiCppMb = Number((androidCppMb * 2.6).toFixed(1));
835
- const androidApkInstallMb = Number((releaseJsMb + androidMultiAbiCppMb + androidDexMb + androidResMb + 3.2).toFixed(1));
836
- const androidApkDownloadMb = Number((androidApkInstallMb * 0.58).toFixed(1));
837
837
  const androidApkComponents = [
838
838
  {
839
839
  id: 'apk-c1',
@@ -849,8 +849,8 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
849
849
  id: 'apk-c2',
850
850
  name: (0, i18n_1.t)('bundle.apkComp2Name'),
851
851
  category: 'frameworks',
852
- sizeMb: androidDexMb,
853
- pct: Number(((androidDexMb / androidApkInstallMb) * 100).toFixed(1)),
852
+ sizeMb: androidApkDexMb,
853
+ pct: Number(((androidApkDexMb / androidApkInstallMb) * 100).toFixed(1)),
854
854
  color: AppColors_1.AppColors.indigo500,
855
855
  description: (0, i18n_1.t)('bundle.apkComp2Desc'),
856
856
  advice: (0, i18n_1.t)('bundle.apkComp2Advice'),
@@ -859,8 +859,8 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
859
859
  id: 'apk-c3',
860
860
  name: (0, i18n_1.t)('bundle.apkComp3Name'),
861
861
  category: 'assets',
862
- sizeMb: androidResMb,
863
- pct: Number(((androidResMb / androidApkInstallMb) * 100).toFixed(1)),
862
+ sizeMb: androidApkResMb,
863
+ pct: Number(((androidApkResMb / androidApkInstallMb) * 100).toFixed(1)),
864
864
  color: AppColors_1.AppColors.pink500,
865
865
  description: (0, i18n_1.t)('bundle.apkComp3Desc'),
866
866
  advice: (0, i18n_1.t)('bundle.apkComp3Advice'),
@@ -879,8 +879,8 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
879
879
  id: 'apk-c5',
880
880
  name: (0, i18n_1.t)('bundle.apkComp5Name'),
881
881
  category: 'meta',
882
- sizeMb: 3.2,
883
- pct: Number(((3.2 / androidApkInstallMb) * 100).toFixed(1)),
882
+ sizeMb: androidApkMetaMb,
883
+ pct: Number(((androidApkMetaMb / androidApkInstallMb) * 100).toFixed(1)),
884
884
  color: AppColors_1.AppColors.amber500,
885
885
  description: (0, i18n_1.t)('bundle.apkComp5Desc'),
886
886
  advice: (0, i18n_1.t)('bundle.apkComp5Advice'),
@@ -950,8 +950,19 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
950
950
  };
951
951
  exports.parseBundleSource = parseBundleSource;
952
952
  /**
953
- * Automatically fetch and analyze the real running bundle for the host app.
954
- * Supports dynamic port detection (8081, 8082, 8083, etc.) and parallel dev server probing.
953
+ * Returns an instant baseline bundle analysis synchronously (never blocks UI).
954
+ */
955
+ const getInitialBundleAnalysis = () => {
956
+ if (cachedAnalysis)
957
+ return cachedAnalysis;
958
+ const scriptURL = (0, exports.getHostScriptURL)();
959
+ const fallbackBytes = 6840000; // ~6.8MB standard RN dev bundle
960
+ const result = (0, exports.parseBundleSource)('', fallbackBytes, scriptURL || 'unknown', false);
961
+ return result;
962
+ };
963
+ exports.getInitialBundleAnalysis = getInitialBundleAnalysis;
964
+ /**
965
+ * Asynchronously fetch and analyze the running Metro bundle in the background.
955
966
  */
956
967
  const analyzeHostAppBundle = async (forceRefresh = false) => {
957
968
  if (forceRefresh) {
@@ -966,20 +977,22 @@ const analyzeHostAppBundle = async (forceRefresh = false) => {
966
977
  }
967
978
  isAnalyzing = true;
968
979
  const scriptURL = (0, exports.getHostScriptURL)();
969
- const probeUrls = buildProbeUrls(scriptURL);
970
- // Probe all candidate dev-server ports and hosts in parallel for instant response
971
- const fetched = await probeCandidateUrlsInParallel(probeUrls, 3500);
972
- if (fetched && fetched.text && fetched.text.length > 0) {
973
- const result = (0, exports.parseBundleSource)(fetched.text, fetched.bytes, fetched.url);
974
- cachedAnalysis = result;
975
- isAnalyzing = false;
976
- subscribers.forEach(cb => cb(result));
977
- subscribers.length = 0;
978
- return result;
979
- }
980
- // Fallback: could not reach a Metro dev server (release build, offline, or
981
- // custom dev-server host). Return estimated values so the UI never crashes.
982
- const fallbackBytes = 6840000; // ~6.8MB standard RN dev bundle
980
+ try {
981
+ const fetched = await probeCandidateUrlsInParallel(scriptURL, 2500);
982
+ if (fetched && fetched.text && fetched.text.length > 0) {
983
+ const result = (0, exports.parseBundleSource)(fetched.text, fetched.bytes, fetched.url);
984
+ cachedAnalysis = result;
985
+ isAnalyzing = false;
986
+ subscribers.forEach(cb => cb(result));
987
+ subscribers.length = 0;
988
+ return result;
989
+ }
990
+ }
991
+ catch {
992
+ // Silent catch
993
+ }
994
+ // Fallback: could not reach a Metro dev server (offline, device on different network, or release build).
995
+ const fallbackBytes = 6840000;
983
996
  const result = (0, exports.parseBundleSource)('', fallbackBytes, scriptURL || 'unknown', false);
984
997
  cachedAnalysis = result;
985
998
  isAnalyzing = false;
@@ -32,12 +32,17 @@ const formatArgs = (args) => {
32
32
  // ─── Dynamic Symbolication Helper for React Native Metro ────────────────────
33
33
  const getMetroSymbolicateUrl = () => {
34
34
  try {
35
- const scriptURL = react_native_1.NativeModules.SourceCode?.scriptURL;
36
- if (typeof scriptURL === 'string') {
35
+ const scriptURL = react_native_1.NativeModules.SourceCode?.scriptURL ||
36
+ react_native_1.NativeModules?.PlatformConstants?.serverHost ||
37
+ react_native_1.NativeModules?.DevSettings?.serverHost;
38
+ if (typeof scriptURL === 'string' && scriptURL.length > 0) {
37
39
  const match = scriptURL.match(/^(https?:\/\/[^\/]+)/);
38
40
  if (match) {
39
41
  return `${match[1]}/symbolicate`;
40
42
  }
43
+ else if (!scriptURL.startsWith('http') && scriptURL.includes(':')) {
44
+ return `http://${scriptURL}/symbolicate`;
45
+ }
41
46
  }
42
47
  }
43
48
  catch { }
@@ -584,11 +584,28 @@ const openInVSCode = (filePath, lineNumber, columnNumber) => {
584
584
  const col = columnNumber ? `:${columnNumber}` : '';
585
585
  const cleanPath = filePath.replace(/^file:\/\//, '');
586
586
  // 1. Notify Metro dev server on host to launch editor directly on local system
587
- const metroHosts = [
587
+ let extractedOrigin = null;
588
+ try {
589
+ const scriptURL = react_native_1.NativeModules?.SourceCode?.scriptURL ||
590
+ react_native_1.NativeModules?.PlatformConstants?.serverHost ||
591
+ react_native_1.NativeModules?.DevSettings?.serverHost;
592
+ if (typeof scriptURL === 'string' && scriptURL.length > 0) {
593
+ const match = scriptURL.match(/^(https?:\/\/[^/]+)/);
594
+ if (match) {
595
+ extractedOrigin = match[1];
596
+ }
597
+ else if (!scriptURL.startsWith('http') && scriptURL.includes(':')) {
598
+ extractedOrigin = `http://${scriptURL}`;
599
+ }
600
+ }
601
+ }
602
+ catch { }
603
+ const metroHosts = Array.from(new Set([
604
+ ...(extractedOrigin ? [extractedOrigin] : []),
588
605
  'http://localhost:8081',
589
606
  'http://127.0.0.1:8081',
590
607
  'http://10.0.2.2:8081',
591
- ];
608
+ ]));
592
609
  metroHosts.forEach(host => {
593
610
  try {
594
611
  fetch(`${host}/open-stack-frame`, {