react-native-inapp-inspector 1.1.23 → 1.1.25

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 (79) hide show
  1. package/dist/commonjs/components/AnalyticsEventCard.js +94 -88
  2. package/dist/commonjs/components/ConsoleLogCard.js +49 -25
  3. package/dist/commonjs/components/Inspector/AnalyticsFilterModal.d.ts +7 -0
  4. package/dist/commonjs/components/Inspector/AnalyticsFilterModal.js +1002 -0
  5. package/dist/commonjs/components/Inspector/AnalyticsTab.js +637 -10
  6. package/dist/commonjs/components/Inspector/BundleTab.d.ts +1 -1
  7. package/dist/commonjs/components/Inspector/BundleTab.js +859 -402
  8. package/dist/commonjs/components/Inspector/InspectorContext.js +6 -1
  9. package/dist/commonjs/components/Inspector/InspectorHeader.js +13 -5
  10. package/dist/commonjs/components/Inspector/LogDetail.js +223 -111
  11. package/dist/commonjs/components/Inspector/MainScreen.js +1 -1
  12. package/dist/commonjs/components/Inspector/NetworkDetail.js +131 -81
  13. package/dist/commonjs/components/Inspector/PerformanceTab.js +20 -10
  14. package/dist/commonjs/components/NetworkIcons.d.ts +14 -0
  15. package/dist/commonjs/components/NetworkIcons.js +93 -1
  16. package/dist/commonjs/components/SegmentedTabs.js +31 -22
  17. package/dist/commonjs/components/TreeNode.js +13 -12
  18. package/dist/commonjs/constants/version.d.ts +1 -1
  19. package/dist/commonjs/constants/version.js +1 -1
  20. package/dist/commonjs/customHooks/bundleAnalyzer.d.ts +6 -2
  21. package/dist/commonjs/customHooks/bundleAnalyzer.js +552 -114
  22. package/dist/commonjs/customHooks/consoleLogger.js +83 -45
  23. package/dist/commonjs/customHooks/networkLogger.js +10 -1
  24. package/dist/commonjs/customHooks/performanceTracker.d.ts +8 -0
  25. package/dist/commonjs/customHooks/performanceTracker.js +44 -3
  26. package/dist/commonjs/decorators/index.d.ts +49 -0
  27. package/dist/commonjs/decorators/index.js +142 -0
  28. package/dist/commonjs/helpers/gaAnalyticsRegistry.d.ts +18 -0
  29. package/dist/commonjs/helpers/gaAnalyticsRegistry.js +174 -0
  30. package/dist/commonjs/helpers/index.d.ts +7 -4
  31. package/dist/commonjs/helpers/index.js +102 -64
  32. package/dist/commonjs/i18n/locales/en.json +53 -4
  33. package/dist/commonjs/index.d.ts +4 -0
  34. package/dist/commonjs/index.js +216 -8
  35. package/dist/commonjs/styles/index.d.ts +4 -1
  36. package/dist/commonjs/styles/index.js +22 -19
  37. package/dist/commonjs/types/enums.d.ts +115 -9
  38. package/dist/commonjs/types/enums.js +103 -2
  39. package/dist/commonjs/types/interfaces.d.ts +19 -0
  40. package/dist/esm/components/AnalyticsEventCard.js +95 -89
  41. package/dist/esm/components/ConsoleLogCard.js +49 -25
  42. package/dist/esm/components/Inspector/AnalyticsFilterModal.d.ts +7 -0
  43. package/dist/esm/components/Inspector/AnalyticsFilterModal.js +964 -0
  44. package/dist/esm/components/Inspector/AnalyticsTab.js +639 -12
  45. package/dist/esm/components/Inspector/BundleTab.d.ts +1 -1
  46. package/dist/esm/components/Inspector/BundleTab.js +860 -403
  47. package/dist/esm/components/Inspector/InspectorContext.js +1 -0
  48. package/dist/esm/components/Inspector/InspectorHeader.js +14 -6
  49. package/dist/esm/components/Inspector/LogDetail.js +223 -111
  50. package/dist/esm/components/Inspector/MainScreen.js +2 -2
  51. package/dist/esm/components/Inspector/NetworkDetail.js +132 -49
  52. package/dist/esm/components/Inspector/PerformanceTab.js +20 -10
  53. package/dist/esm/components/NetworkIcons.d.ts +14 -0
  54. package/dist/esm/components/NetworkIcons.js +78 -0
  55. package/dist/esm/components/SegmentedTabs.js +31 -22
  56. package/dist/esm/components/TreeNode.js +13 -12
  57. package/dist/esm/constants/version.d.ts +1 -1
  58. package/dist/esm/constants/version.js +1 -1
  59. package/dist/esm/customHooks/bundleAnalyzer.d.ts +6 -2
  60. package/dist/esm/customHooks/bundleAnalyzer.js +549 -114
  61. package/dist/esm/customHooks/consoleLogger.js +83 -45
  62. package/dist/esm/customHooks/networkLogger.js +10 -1
  63. package/dist/esm/customHooks/performanceTracker.d.ts +8 -0
  64. package/dist/esm/customHooks/performanceTracker.js +38 -1
  65. package/dist/esm/decorators/index.d.ts +49 -0
  66. package/dist/esm/decorators/index.js +137 -0
  67. package/dist/esm/helpers/gaAnalyticsRegistry.d.ts +18 -0
  68. package/dist/esm/helpers/gaAnalyticsRegistry.js +169 -0
  69. package/dist/esm/helpers/index.d.ts +7 -4
  70. package/dist/esm/helpers/index.js +100 -63
  71. package/dist/esm/i18n/locales/en.json +53 -4
  72. package/dist/esm/index.d.ts +4 -0
  73. package/dist/esm/index.js +190 -8
  74. package/dist/esm/styles/index.d.ts +4 -1
  75. package/dist/esm/styles/index.js +22 -19
  76. package/dist/esm/types/enums.d.ts +115 -9
  77. package/dist/esm/types/enums.js +102 -3
  78. package/dist/esm/types/interfaces.d.ts +19 -0
  79. package/package.json +1 -1
@@ -8,99 +8,536 @@ import { AppColors } from '../styles/AppColors';
8
8
  // 3. Extracting real module paths (project files vs node_modules dependencies).
9
9
  // 4. Computing real Development and Production Binary (.ipa / .aab / .apk) sizes.
10
10
  // ─────────────────────────────────────────────────────────────────────────────
11
- import { NativeModules, Platform } from 'react-native';
11
+ import { NativeModules, Platform, TurboModuleRegistry } from 'react-native';
12
12
  let cachedAnalysis = null;
13
13
  let isAnalyzing = false;
14
14
  const subscribers = [];
15
+ const getSourceCodeModule = () => {
16
+ try {
17
+ const legacy = NativeModules?.SourceCode;
18
+ if (legacy && typeof legacy.scriptURL === 'string' && legacy.scriptURL.length > 0) {
19
+ return legacy;
20
+ }
21
+ }
22
+ catch { }
23
+ try {
24
+ const turbo = TurboModuleRegistry?.get?.('SourceCode');
25
+ const constants = turbo?.getConstants?.();
26
+ const scriptURL = turbo?.scriptURL || constants?.scriptURL;
27
+ if (typeof scriptURL === 'string' && scriptURL.length > 0) {
28
+ return { scriptURL };
29
+ }
30
+ }
31
+ catch { }
32
+ return null;
33
+ };
15
34
  export const getHostScriptURL = () => {
16
- const SourceCode = NativeModules?.SourceCode;
17
- if (SourceCode && typeof SourceCode.scriptURL === 'string' && SourceCode.scriptURL.length > 0) {
18
- return SourceCode.scriptURL;
35
+ // 1. NativeModules.SourceCode (bridged) or SourceCode TurboModule (bridgeless / new arch)
36
+ try {
37
+ const scriptURL = getSourceCodeModule()?.scriptURL;
38
+ if (typeof scriptURL === 'string' && scriptURL.length > 0) {
39
+ return scriptURL;
40
+ }
41
+ }
42
+ catch { }
43
+ // 2. Android legacy & new arch: PlatformConstants.serverHost, DevSettings, or AndroidConstants
44
+ try {
45
+ const serverHost = NativeModules?.PlatformConstants?.serverHost ||
46
+ NativeModules?.AndroidConstants?.serverHost ||
47
+ NativeModules?.DevSettings?.serverHost ||
48
+ NativeModules?.DevSettings?.getConstants?.()?.serverHost;
49
+ if (typeof serverHost === 'string' && serverHost.length > 0) {
50
+ const formattedHost = serverHost.startsWith('http') ? serverHost : `http://${serverHost}`;
51
+ return `${formattedHost}/index.bundle?platform=${Platform.OS}&dev=true`;
52
+ }
53
+ }
54
+ catch { }
55
+ // 3. Fallback to global dev config if defined
56
+ try {
57
+ const globalHost = globalThis?.__DEV_SERVER_URL__ ||
58
+ globalThis?.__METRO_SERVER_HOST__;
59
+ if (typeof globalHost === 'string' && globalHost.length > 0) {
60
+ return globalHost.startsWith('http')
61
+ ? globalHost
62
+ : `http://${globalHost}/index.bundle?platform=${Platform.OS}&dev=true`;
63
+ }
64
+ }
65
+ catch { }
66
+ return '';
67
+ };
68
+ // Expanded list of common Metro & dev server ports
69
+ const DEV_SERVER_PORTS = [8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8080, 19000, 19001, 3000];
70
+ const buildProbeUrls = (scriptURL) => {
71
+ const urls = [];
72
+ let extractedHost = null;
73
+ let extractedPort = null;
74
+ if (typeof scriptURL === 'string' && scriptURL.startsWith('http')) {
75
+ urls.push(scriptURL);
76
+ const urlMatch = scriptURL.match(/^https?:\/\/([^:/]+)(?::(\d+))?/);
77
+ if (urlMatch) {
78
+ extractedHost = urlMatch[1];
79
+ if (urlMatch[2]) {
80
+ extractedPort = parseInt(urlMatch[2], 10);
81
+ }
82
+ }
83
+ }
84
+ // Prioritize dynamically extracted port first
85
+ const ports = extractedPort
86
+ ? Array.from(new Set([extractedPort, ...DEV_SERVER_PORTS]))
87
+ : DEV_SERVER_PORTS;
88
+ const hosts = Platform.OS === 'android'
89
+ ? ['localhost', '127.0.0.1', '10.0.2.2', '10.0.3.2']
90
+ : ['localhost', '127.0.0.1'];
91
+ if (extractedHost && !hosts.includes(extractedHost)) {
92
+ hosts.unshift(extractedHost);
93
+ }
94
+ for (const port of ports) {
95
+ for (const host of hosts) {
96
+ urls.push(`http://${host}:${port}/index.bundle?platform=${Platform.OS}&dev=true`);
97
+ }
98
+ }
99
+ return Array.from(new Set(urls));
100
+ };
101
+ const promiseAny = (promises) => {
102
+ return new Promise((resolve, reject) => {
103
+ let pendingCount = promises.length;
104
+ if (pendingCount === 0) {
105
+ reject(new Error('All promises rejected'));
106
+ return;
107
+ }
108
+ promises.forEach(p => {
109
+ Promise.resolve(p)
110
+ .then(resolve)
111
+ .catch(() => {
112
+ pendingCount--;
113
+ if (pendingCount === 0) {
114
+ reject(new Error('All promises rejected'));
115
+ }
116
+ });
117
+ });
118
+ });
119
+ };
120
+ const probeCandidateUrlsInParallel = async (urls, timeoutMs = 4000) => {
121
+ if (urls.length === 0)
122
+ return null;
123
+ const controller = new AbortController();
124
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
125
+ const fetchSingle = async (url) => {
126
+ const res = await fetch(url, { signal: controller.signal });
127
+ if (!res.ok) {
128
+ throw new Error(`HTTP ${res.status}`);
129
+ }
130
+ const contentLength = res.headers.get('content-length');
131
+ const text = await res.text();
132
+ if (!text || text.length === 0) {
133
+ throw new Error('Empty response');
134
+ }
135
+ const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
136
+ return { text, bytes, url };
137
+ };
138
+ try {
139
+ // Probe candidate dev servers concurrently — whichever responds first wins
140
+ const result = await promiseAny(urls.map(url => fetchSingle(url)));
141
+ controller.abort();
142
+ return result;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ finally {
148
+ clearTimeout(timer);
149
+ }
150
+ };
151
+ const MAX_ARG_SCAN_LEN = 200000;
152
+ const scanCallArguments = (text, openParenIndex) => {
153
+ let depth = 0;
154
+ let bracketDepth = 0;
155
+ let quote = null;
156
+ const commas = [];
157
+ const end = Math.min(text.length, openParenIndex + MAX_ARG_SCAN_LEN);
158
+ for (let i = openParenIndex; i < end; i++) {
159
+ const ch = text[i];
160
+ if (quote) {
161
+ if (ch === '\\') {
162
+ i++;
163
+ continue;
164
+ }
165
+ if (ch === quote)
166
+ quote = null;
167
+ continue;
168
+ }
169
+ if (ch === '"' || ch === "'" || ch === '`') {
170
+ quote = ch;
171
+ continue;
172
+ }
173
+ if (ch === '(') {
174
+ depth++;
175
+ continue;
176
+ }
177
+ if (ch === ')') {
178
+ if (depth === 0)
179
+ return null;
180
+ depth--;
181
+ if (depth === 0) {
182
+ if (commas.length === 0)
183
+ return null;
184
+ const args = [];
185
+ let prev = openParenIndex + 1;
186
+ for (const c of commas) {
187
+ args.push(text.slice(prev, c));
188
+ prev = c + 1;
189
+ }
190
+ args.push(text.slice(prev, i));
191
+ return args;
192
+ }
193
+ continue;
194
+ }
195
+ if (ch === '[') {
196
+ bracketDepth++;
197
+ continue;
198
+ }
199
+ if (ch === ']') {
200
+ if (bracketDepth > 0)
201
+ bracketDepth--;
202
+ continue;
203
+ }
204
+ if (ch === ',' && depth === 1 && bracketDepth === 0)
205
+ commas.push(i);
206
+ }
207
+ return null;
208
+ };
209
+ // ─── Runtime Module & Define Tracking ────────────────────────────────────────
210
+ // Captures modules registered via Metro's global __d and executed via __r.
211
+ const executedModuleIds = new Set();
212
+ const runtimeModules = new Map();
213
+ export const trackRuntimeModuleExecution = () => {
214
+ try {
215
+ const g = globalThis;
216
+ const r = g.__r;
217
+ if (typeof r === 'function' && !r.__inspectorTracked) {
218
+ const tracked = function (moduleId, ...rest) {
219
+ executedModuleIds.add(Number(moduleId));
220
+ return r.call(this, moduleId, ...rest);
221
+ };
222
+ Object.defineProperty(tracked, '__inspectorTracked', { value: true, enumerable: false });
223
+ g.__r = tracked;
224
+ }
225
+ }
226
+ catch { }
227
+ };
228
+ export const trackRuntimeDefine = () => {
229
+ try {
230
+ const g = globalThis;
231
+ const originalD = g.__d;
232
+ if (typeof originalD === 'function' && !originalD.__inspectorTracked) {
233
+ const trackedD = function (factory, moduleId, dependencyMap, verboseName, ...rest) {
234
+ if (typeof verboseName === 'string' && verboseName.length > 0) {
235
+ runtimeModules.set(Number(moduleId), { id: Number(moduleId), path: verboseName });
236
+ }
237
+ return originalD.call(this, factory, moduleId, dependencyMap, verboseName, ...rest);
238
+ };
239
+ Object.defineProperty(trackedD, '__inspectorTracked', { value: true, enumerable: false });
240
+ g.__d = trackedD;
241
+ }
242
+ }
243
+ catch { }
244
+ };
245
+ trackRuntimeModuleExecution();
246
+ trackRuntimeDefine();
247
+ const extractBundleModules = (bundleText) => {
248
+ const modules = [];
249
+ const seenIds = new Set();
250
+ if (bundleText && bundleText.length > 0) {
251
+ // Primary high-speed Metro tail regex: matches }, <id>, [<deps>], "<path>"
252
+ const tailRegex = /\}\s*,\s*(\d+)\s*,\s*\[([\d,\s]*)\]\s*,\s*(?:"([^"\r\n]+)"|'([^'\r\n]+)')/g;
253
+ let match;
254
+ while ((match = tailRegex.exec(bundleText)) !== null) {
255
+ const id = parseInt(match[1], 10);
256
+ const depsStr = match[2];
257
+ const path = match[3] || match[4] || '';
258
+ const deps = depsStr
259
+ ? depsStr
260
+ .split(',')
261
+ .map(s => parseInt(s.trim(), 10))
262
+ .filter(n => !isNaN(n))
263
+ : [];
264
+ if (!seenIds.has(id) && path.length > 0) {
265
+ seenIds.add(id);
266
+ modules.push({ id, deps, path });
267
+ }
268
+ }
269
+ // Secondary fallback if tailRegex didn't find modules
270
+ if (modules.length === 0) {
271
+ const defineRe = /__d\s*\(/g;
272
+ let m;
273
+ while ((m = defineRe.exec(bundleText)) !== null) {
274
+ const openParen = m.index + m[0].length - 1;
275
+ const args = scanCallArguments(bundleText, openParen);
276
+ if (!args || args.length < 4)
277
+ continue;
278
+ const idMatch = args[1].trim().match(/^(\d+)$/);
279
+ if (!idMatch)
280
+ continue;
281
+ const idNum = parseInt(idMatch[1], 10);
282
+ if (seenIds.has(idNum))
283
+ continue;
284
+ const depsMatch = args[2].match(/\[([^\]]*)\]/);
285
+ const deps = [];
286
+ if (depsMatch) {
287
+ const depRe = /\d+/g;
288
+ let d;
289
+ while ((d = depRe.exec(depsMatch[1])) !== null) {
290
+ deps.push(parseInt(d[0], 10));
291
+ }
292
+ }
293
+ const pathRaw = args[3].trim();
294
+ const pathMatch = pathRaw.match(/^["']([^"']+)["']$/);
295
+ const path = pathMatch ? pathMatch[1] : pathRaw;
296
+ if (path.length === 0 || path.length > 500)
297
+ continue;
298
+ seenIds.add(idNum);
299
+ modules.push({ id: idNum, deps, path });
300
+ }
301
+ }
302
+ }
303
+ // Merge any runtime modules captured via global.__d
304
+ runtimeModules.forEach((item, id) => {
305
+ if (!seenIds.has(id)) {
306
+ seenIds.add(id);
307
+ modules.push({ id, deps: [], path: item.path });
308
+ }
309
+ });
310
+ return modules;
311
+ };
312
+ const getStaticStartupModuleIds = (bundleText) => {
313
+ const ids = new Set();
314
+ const startupRe = /__r\s*\(\s*(\d+)\s*\)/g;
315
+ let m;
316
+ while ((m = startupRe.exec(bundleText)) !== null) {
317
+ ids.add(parseInt(m[1], 10));
19
318
  }
20
- return Platform.OS === 'ios'
21
- ? 'http://localhost:8081/index.bundle?platform=ios&dev=true'
22
- : 'http://10.0.2.2:8081/index.bundle?platform=android&dev=true';
319
+ return ids;
320
+ };
321
+ const findCommonDirectoryPrefix = (paths) => {
322
+ const absPaths = paths.filter(p => p.startsWith('/'));
323
+ if (absPaths.length === 0)
324
+ return '';
325
+ if (absPaths.length === 1) {
326
+ const lastSlash = absPaths[0].lastIndexOf('/');
327
+ return lastSlash > 0 ? absPaths[0].slice(0, lastSlash + 1) : '';
328
+ }
329
+ const splitPaths = absPaths.map(p => p.split('/'));
330
+ let commonIndex = 0;
331
+ const first = splitPaths[0];
332
+ while (commonIndex < first.length - 1) {
333
+ const segment = first[commonIndex];
334
+ if (splitPaths.every(p => p[commonIndex] === segment)) {
335
+ commonIndex++;
336
+ }
337
+ else {
338
+ break;
339
+ }
340
+ }
341
+ if (commonIndex === 0)
342
+ return '';
343
+ return first.slice(0, commonIndex).join('/') + '/';
344
+ };
345
+ const isExternalPackage = (rawPath) => {
346
+ // 1. Standard node_modules
347
+ if (rawPath.includes('node_modules/')) {
348
+ const pkgMatch = rawPath.match(/node_modules\/(?:@([^/]+)\/([^/]+)|([^/]+))/);
349
+ if (pkgMatch) {
350
+ const pkgName = pkgMatch[1] && pkgMatch[2] ? `@${pkgMatch[1]}/${pkgMatch[2]}` : pkgMatch[3];
351
+ return { isPkg: true, pkgName };
352
+ }
353
+ return { isPkg: true };
354
+ }
355
+ // 2. React Native In-App Inspector (when linked or embedded)
356
+ if (rawPath.includes('react-native-inapp-inspector')) {
357
+ return { isPkg: true, pkgName: 'react-native-inapp-inspector' };
358
+ }
359
+ // 3. Yarn berry / pnpm virtual stores
360
+ if (rawPath.includes('.yarn/') || rawPath.includes('.pnpm/')) {
361
+ return { isPkg: true };
362
+ }
363
+ // 4. Compiled package dist / build folders outside project source
364
+ if (rawPath.includes('/dist/esm/') ||
365
+ rawPath.includes('/dist/commonjs/') ||
366
+ rawPath.includes('/dist/cjs/') ||
367
+ rawPath.startsWith('dist/esm/') ||
368
+ rawPath.startsWith('dist/commonjs/') ||
369
+ rawPath.startsWith('dist/') ||
370
+ rawPath.includes('/react-native-inapp-inspector/dist/')) {
371
+ return { isPkg: true, pkgName: 'react-native-inapp-inspector' };
372
+ }
373
+ return { isPkg: false };
374
+ };
375
+ const isInternalModule = (rawPath) => {
376
+ return (rawPath.startsWith('<<') ||
377
+ rawPath.startsWith('[metro]') ||
378
+ rawPath.includes('metro-runtime') ||
379
+ rawPath.includes('polyfills/require.js') ||
380
+ rawPath.includes('prelude_commonjs') ||
381
+ rawPath.includes('Libraries/Core/InitializeCore.js') ||
382
+ rawPath.includes('@babel/runtime'));
23
383
  };
24
384
  /**
25
385
  * Parses raw Metro bundle source code to discover real modules and packages in the host app.
26
386
  */
27
- export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
387
+ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) => {
28
388
  const isHermes = Boolean(globalThis.HermesInternal);
29
389
  const totalDevMb = Number((totalBytes / (1024 * 1024)).toFixed(2));
30
390
  const totalDevKb = Math.round(totalBytes / 1024);
391
+ const modules = extractBundleModules(bundleText);
392
+ const startupIds = getStaticStartupModuleIds(bundleText);
393
+ // Build module map for dependency graph traversal
394
+ const modMap = new Map();
395
+ for (const mod of modules) {
396
+ modMap.set(mod.id, mod);
397
+ }
398
+ // Transitive Reachability: traverse the dependency graph starting from
399
+ // entrypoints (index.js, App.tsx, app.json, startupIds, executedModuleIds)
400
+ const consumedIds = new Set();
401
+ const queue = [];
402
+ const markConsumed = (id) => {
403
+ if (!consumedIds.has(id)) {
404
+ consumedIds.add(id);
405
+ queue.push(id);
406
+ }
407
+ };
408
+ // Seed with root entrypoints and runtime executions
409
+ markConsumed(0);
410
+ startupIds.forEach(id => markConsumed(id));
411
+ executedModuleIds.forEach(id => markConsumed(id));
412
+ for (const mod of modules) {
413
+ const lower = mod.path.toLowerCase().replace(/\\/g, '/');
414
+ if (lower.endsWith('/index.js') ||
415
+ lower.endsWith('/index.ts') ||
416
+ lower.endsWith('/index.tsx') ||
417
+ lower === 'index.js' ||
418
+ lower.endsWith('/app.tsx') ||
419
+ lower.endsWith('/app.js') ||
420
+ lower.endsWith('/app.jsx') ||
421
+ lower === 'app.tsx' ||
422
+ lower === 'app.js' ||
423
+ lower.endsWith('app.json')) {
424
+ markConsumed(mod.id);
425
+ }
426
+ }
427
+ // Transitive BFS dependency traversal
428
+ while (queue.length > 0) {
429
+ const currId = queue.shift();
430
+ const currMod = modMap.get(currId);
431
+ if (currMod && currMod.deps) {
432
+ for (const depId of currMod.deps) {
433
+ markConsumed(depId);
434
+ }
435
+ }
436
+ }
31
437
  const discoveredPackagesMap = new Map();
438
+ const rawProjectItems = [];
439
+ for (const mod of modules) {
440
+ const rawPath = mod.path.replace(/\\/g, '/'); // Normalize Windows backslashes
441
+ const { isPkg, pkgName } = isExternalPackage(rawPath);
442
+ if (isPkg) {
443
+ if (pkgName && !pkgName.startsWith('.')) {
444
+ discoveredPackagesMap.set(pkgName, (discoveredPackagesMap.get(pkgName) || 0) + 1);
445
+ }
446
+ continue;
447
+ }
448
+ // Filter out internal Metro polyfills / virtual modules
449
+ if (isInternalModule(rawPath)) {
450
+ continue;
451
+ }
452
+ rawProjectItems.push({ mod, rawPath });
453
+ }
454
+ // Calculate common prefix for absolute paths to cleanly extract host app project files
455
+ const commonPrefix = findCommonDirectoryPrefix(rawProjectItems.map(item => item.rawPath));
32
456
  const discoveredFiles = [];
33
- // Match module declarations: __d(function(...), id, [...], "path/to/module.js") or comments
34
- // In Metro: __d(function(...), 42, [1, 2], "node_modules/lodash/index.js")
35
- const modulePathRegex = /(?:__d\s*\([^,]+,[^,]+,[^,]+,["']([^"']+)["']|\/\/\s*@metro-module-path\s+([^\n\r]+)|["']((?:node_modules|\.|\/|[a-zA-Z0-9_-]+)\/[^"']+\.[a-zA-Z0-9]+)["'])/g;
36
- let match;
37
457
  let fileIdx = 0;
38
- const seenPaths = new Set();
39
- // Extract from text
40
- while ((match = modulePathRegex.exec(bundleText)) !== null) {
41
- const rawPath = match[1] || match[2] || match[3];
42
- if (!rawPath || seenPaths.has(rawPath) || rawPath.length > 200)
458
+ for (const { mod, rawPath } of rawProjectItems) {
459
+ let cleanPath = rawPath;
460
+ if (commonPrefix && cleanPath.startsWith(commonPrefix)) {
461
+ cleanPath = cleanPath.slice(commonPrefix.length);
462
+ }
463
+ // Clean leading ../ sequences, ./, and leading /
464
+ cleanPath = cleanPath.replace(/^(\.\.\/)+/, '').replace(/^\.\//, '').replace(/^\/+/, '');
465
+ if (!cleanPath)
466
+ cleanPath = rawPath.split('/').pop() || rawPath;
467
+ const isLocalFile = cleanPath.endsWith('.tsx') ||
468
+ cleanPath.endsWith('.ts') ||
469
+ cleanPath.endsWith('.jsx') ||
470
+ cleanPath.endsWith('.js') ||
471
+ cleanPath.endsWith('.mjs') ||
472
+ cleanPath.endsWith('.cjs') ||
473
+ cleanPath.endsWith('.json') ||
474
+ cleanPath.endsWith('.png') ||
475
+ cleanPath.endsWith('.jpg') ||
476
+ cleanPath.endsWith('.jpeg') ||
477
+ cleanPath.endsWith('.gif') ||
478
+ cleanPath.endsWith('.webp') ||
479
+ cleanPath.endsWith('.svg') ||
480
+ cleanPath.endsWith('.bmp') ||
481
+ cleanPath.endsWith('.ico') ||
482
+ cleanPath.endsWith('.ttf') ||
483
+ cleanPath.endsWith('.otf') ||
484
+ cleanPath.endsWith('.woff') ||
485
+ cleanPath.endsWith('.woff2') ||
486
+ cleanPath.endsWith('.css') ||
487
+ cleanPath.endsWith('.scss') ||
488
+ cleanPath.endsWith('.html') ||
489
+ cleanPath.endsWith('.md') ||
490
+ cleanPath.endsWith('.graphql') ||
491
+ cleanPath.endsWith('.gql');
492
+ if (!isLocalFile)
43
493
  continue;
44
- seenPaths.add(rawPath);
45
- const isNodeModule = rawPath.includes('node_modules/');
46
- if (isNodeModule) {
47
- const pkgMatch = rawPath.match(/node_modules\/(?:@([^/]+)\/([^/]+)|([^/]+))/);
48
- if (pkgMatch) {
49
- const pkgName = pkgMatch[1] && pkgMatch[2] ? `@${pkgMatch[1]}/${pkgMatch[2]}` : pkgMatch[3];
50
- if (pkgName && !pkgName.startsWith('.')) {
51
- discoveredPackagesMap.set(pkgName, (discoveredPackagesMap.get(pkgName) || 0) + 1);
52
- }
53
- }
494
+ const ext = cleanPath.split('.').pop()?.toUpperCase() || 'JS';
495
+ const name = cleanPath.split('/').pop() || cleanPath;
496
+ let category = 'javascript';
497
+ let color = AppColors.indigo500;
498
+ if (ext === 'TSX' || ext === 'TS') {
499
+ category = 'typescript';
500
+ color = AppColors.sky500;
54
501
  }
55
- else if (rawPath.endsWith('.tsx') ||
56
- rawPath.endsWith('.ts') ||
57
- rawPath.endsWith('.jsx') ||
58
- rawPath.endsWith('.js') ||
59
- rawPath.endsWith('.json') ||
60
- rawPath.endsWith('.png') ||
61
- rawPath.endsWith('.jpg') ||
62
- rawPath.endsWith('.svg') ||
63
- rawPath.endsWith('.ttf')) {
64
- const ext = rawPath.split('.').pop()?.toUpperCase() || 'JS';
65
- const name = rawPath.split('/').pop() || rawPath;
66
- let category = 'javascript';
67
- let color = AppColors.indigo500;
68
- if (ext === 'TSX' || ext === 'TS') {
69
- category = 'typescript';
70
- color = AppColors.sky500;
71
- }
72
- else if (ext === 'PNG' || ext === 'JPG' || ext === 'SVG') {
73
- category = 'image';
74
- color = AppColors.pink500;
75
- }
76
- else if (ext === 'TTF' || ext === 'OTF') {
77
- category = 'font';
78
- color = AppColors.purple500;
79
- }
80
- else if (ext === 'JSON') {
81
- category = 'json';
82
- color = AppColors.emerald500;
83
- }
84
- // Approximate module size from total and count
85
- const approxKb = Math.max(2, Math.round((totalDevKb * 0.15) / Math.max(seenPaths.size, 20)));
86
- discoveredFiles.push({
87
- id: `host-file-${fileIdx++}`,
88
- name,
89
- path: rawPath,
90
- ext,
91
- category,
92
- sizeKb: approxKb,
93
- meta: `Active Host App Module • ${category.toUpperCase()}`,
94
- color,
95
- status: 'optimal',
96
- advice: 'Bundled into Host App Development Runtime',
97
- isConsumed: true,
98
- });
502
+ else if (ext === 'PNG' ||
503
+ ext === 'JPG' ||
504
+ ext === 'JPEG' ||
505
+ ext === 'GIF' ||
506
+ ext === 'WEBP' ||
507
+ ext === 'SVG' ||
508
+ ext === 'BMP' ||
509
+ ext === 'ICO') {
510
+ category = 'image';
511
+ color = AppColors.pink500;
512
+ }
513
+ else if (ext === 'TTF' || ext === 'OTF' || ext === 'WOFF' || ext === 'WOFF2') {
514
+ category = 'font';
515
+ color = AppColors.purple500;
99
516
  }
100
- if (seenPaths.size >= 1200)
101
- break; // Limit parsing overhead
517
+ else if (ext === 'JSON' || ext === 'CSS' || ext === 'SCSS' || ext === 'HTML' || ext === 'MD' || ext === 'GRAPHQL' || ext === 'GQL') {
518
+ category = 'json';
519
+ color = AppColors.emerald500;
520
+ }
521
+ // Approximate module size from total and module count
522
+ const approxKb = Math.max(2, Math.round((totalDevKb * 0.15) / Math.max(modules.length, 20)));
523
+ const isConsumed = consumedIds.has(mod.id);
524
+ discoveredFiles.push({
525
+ id: `host-file-${fileIdx++}`,
526
+ name,
527
+ path: cleanPath,
528
+ ext,
529
+ category,
530
+ sizeKb: approxKb,
531
+ meta: `Active Host App Module • ${category.toUpperCase()}`,
532
+ color,
533
+ status: isConsumed ? 'optimal' : 'warning',
534
+ advice: isConsumed
535
+ ? 'In-Use: Bundled and active in host application dependency tree'
536
+ : 'Not consumed: Defined in the bundle but not referenced in active execution tree',
537
+ isConsumed,
538
+ });
102
539
  }
103
- // If no files matched via regex (e.g. minified or obfuscated bundle), synthesize from loaded modules
540
+ // If no modules could be parsed (e.g. Hermes bytecode / obfuscated bundle), synthesize from loaded modules
104
541
  if (discoveredFiles.length === 0) {
105
542
  discoveredFiles.push({
106
543
  id: 'hf-1',
@@ -289,7 +726,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
289
726
  const iosComponents = [
290
727
  {
291
728
  id: 'ios-c1',
292
- name: 'Dynamic Frameworks & Pods',
729
+ name: 'Frameworks & Dynamic Pods',
293
730
  category: 'frameworks',
294
731
  sizeMb: nativeFrameworksMb,
295
732
  pct: Number(((nativeFrameworksMb / iosInstallMb) * 100).toFixed(1)),
@@ -299,7 +736,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
299
736
  },
300
737
  {
301
738
  id: 'ios-c2',
302
- name: 'Native Mach-O Executable (ARM64)',
739
+ name: 'Mach-O Executable (ARM64)',
303
740
  category: 'native',
304
741
  sizeMb: nativeMachoMb,
305
742
  pct: Number(((nativeMachoMb / iosInstallMb) * 100).toFixed(1)),
@@ -309,7 +746,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
309
746
  },
310
747
  {
311
748
  id: 'ios-c3',
312
- name: 'Asset Catalog (Assets.car & Media)',
749
+ name: 'Asset Catalog (Assets.car)',
313
750
  category: 'assets',
314
751
  sizeMb: assetsCatalogMb,
315
752
  pct: Number(((assetsCatalogMb / iosInstallMb) * 100).toFixed(1)),
@@ -319,7 +756,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
319
756
  },
320
757
  {
321
758
  id: 'ios-c4',
322
- name: 'Hermes Bytecode (main.jsbundle / .hbc)',
759
+ name: 'Hermes Bytecode (main.jsbundle)',
323
760
  category: 'js',
324
761
  sizeMb: releaseJsMb,
325
762
  pct: Number(((releaseJsMb / iosInstallMb) * 100).toFixed(1)),
@@ -329,7 +766,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
329
766
  },
330
767
  {
331
768
  id: 'ios-c5',
332
- name: 'App Metadata & Code Signatures',
769
+ name: 'Metadata & Code Signatures',
333
770
  category: 'meta',
334
771
  sizeMb: metadataMb,
335
772
  pct: Number(((metadataMb / iosInstallMb) * 100).toFixed(1)),
@@ -341,7 +778,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
341
778
  const androidComponents = [
342
779
  {
343
780
  id: 'and-c1',
344
- name: 'Native C++ Shared Libraries (lib/arm64-v8a/)',
781
+ name: 'Native C++ Libraries (.so)',
345
782
  category: 'native',
346
783
  sizeMb: androidCppMb,
347
784
  pct: Number(((androidCppMb / androidInstallMb) * 100).toFixed(1)),
@@ -351,7 +788,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
351
788
  },
352
789
  {
353
790
  id: 'and-c2',
354
- name: 'Compiled DEX Bytecode (classes.dex)',
791
+ name: 'Compiled DEX (classes.dex)',
355
792
  category: 'frameworks',
356
793
  sizeMb: androidDexMb,
357
794
  pct: Number(((androidDexMb / androidInstallMb) * 100).toFixed(1)),
@@ -361,7 +798,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
361
798
  },
362
799
  {
363
800
  id: 'and-c3',
364
- name: 'Android Resources & Drawables (res/)',
801
+ name: 'Android Resources (res/)',
365
802
  category: 'assets',
366
803
  sizeMb: androidResMb,
367
804
  pct: Number(((androidResMb / androidInstallMb) * 100).toFixed(1)),
@@ -371,7 +808,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
371
808
  },
372
809
  {
373
810
  id: 'and-c4',
374
- name: 'Hermes Bytecode (index.android.bundle)',
811
+ name: 'Hermes Bytecode (Android)',
375
812
  category: 'js',
376
813
  sizeMb: releaseJsMb,
377
814
  pct: Number(((releaseJsMb / androidInstallMb) * 100).toFixed(1)),
@@ -381,7 +818,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
381
818
  },
382
819
  {
383
820
  id: 'and-c5',
384
- name: 'Android Manifest & Signatures (META-INF/)',
821
+ name: 'Manifest & Signatures (META-INF)',
385
822
  category: 'meta',
386
823
  sizeMb: 2.6,
387
824
  pct: Number(((2.6 / androidInstallMb) * 100).toFixed(1)),
@@ -397,7 +834,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
397
834
  const androidApkComponents = [
398
835
  {
399
836
  id: 'apk-c1',
400
- name: 'Multi-ABI Native C++ Libraries (arm64, v7a, x86_64)',
837
+ name: 'Multi-ABI C++ Libraries (.so)',
401
838
  category: 'native',
402
839
  sizeMb: androidMultiAbiCppMb,
403
840
  pct: Number(((androidMultiAbiCppMb / androidApkInstallMb) * 100).toFixed(1)),
@@ -417,7 +854,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
417
854
  },
418
855
  {
419
856
  id: 'apk-c3',
420
- name: 'Android Resources & Assets (res/, assets/)',
857
+ name: 'Android Resources & Assets (res/)',
421
858
  category: 'assets',
422
859
  sizeMb: androidResMb,
423
860
  pct: Number(((androidResMb / androidApkInstallMb) * 100).toFixed(1)),
@@ -427,7 +864,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
427
864
  },
428
865
  {
429
866
  id: 'apk-c4',
430
- name: 'Hermes Bytecode Bundle (index.android.bundle)',
867
+ name: 'Hermes Bytecode Bundle (Android)',
431
868
  category: 'js',
432
869
  sizeMb: releaseJsMb,
433
870
  pct: Number(((releaseJsMb / androidApkInstallMb) * 100).toFixed(1)),
@@ -437,7 +874,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
437
874
  },
438
875
  {
439
876
  id: 'apk-c5',
440
- name: 'Android Manifest & v1/v2/v3 Signatures (META-INF/)',
877
+ name: 'Manifest & Signatures (META-INF)',
441
878
  category: 'meta',
442
879
  sizeMb: 3.2,
443
880
  pct: Number(((3.2 / androidApkInstallMb) * 100).toFixed(1)),
@@ -447,13 +884,13 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
447
884
  },
448
885
  ];
449
886
  return {
450
- isLive: true,
887
+ isLive,
451
888
  scriptURL,
452
889
  totalDevBytes: totalBytes,
453
890
  totalDevMb,
454
891
  totalDevKb,
455
892
  isHermes,
456
- moduleCount: seenPaths.size || discoveredFiles.length + packagesList.length,
893
+ moduleCount: modules.length || discoveredFiles.length + packagesList.length,
457
894
  packageCount: packagesList.length,
458
895
  filesCount: discoveredFiles.length,
459
896
  splitUp: {
@@ -510,9 +947,13 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL) => {
510
947
  };
511
948
  /**
512
949
  * Automatically fetch and analyze the real running bundle for the host app.
950
+ * Supports dynamic port detection (8081, 8082, 8083, etc.) and parallel dev server probing.
513
951
  */
514
- export const analyzeHostAppBundle = async () => {
515
- if (cachedAnalysis)
952
+ export const analyzeHostAppBundle = async (forceRefresh = false) => {
953
+ if (forceRefresh) {
954
+ cachedAnalysis = null;
955
+ }
956
+ if (cachedAnalysis && !forceRefresh)
516
957
  return cachedAnalysis;
517
958
  if (isAnalyzing) {
518
959
  return new Promise(resolve => {
@@ -521,34 +962,28 @@ export const analyzeHostAppBundle = async () => {
521
962
  }
522
963
  isAnalyzing = true;
523
964
  const scriptURL = getHostScriptURL();
524
- try {
525
- if (scriptURL && scriptURL.startsWith('http')) {
526
- // 1. Try HEAD request first for fast content-length
527
- const headRes = await fetch(scriptURL, { method: 'HEAD' });
528
- const cl = headRes.headers.get('content-length');
529
- let byteLength = cl ? parseInt(cl, 10) : 0;
530
- // 2. Fetch partial text to discover real modules
531
- const getRes = await fetch(scriptURL);
532
- const bundleText = await getRes.text();
533
- byteLength = byteLength || bundleText.length;
534
- const result = parseBundleSource(bundleText, byteLength, scriptURL);
535
- cachedAnalysis = result;
536
- isAnalyzing = false;
537
- subscribers.forEach(cb => cb(result));
538
- subscribers.length = 0;
539
- return result;
540
- }
541
- }
542
- catch (err) {
543
- // If fetch failed (e.g. standalone production build or offline)
965
+ const probeUrls = buildProbeUrls(scriptURL);
966
+ // Probe all candidate dev-server ports and hosts in parallel for instant response
967
+ const fetched = await probeCandidateUrlsInParallel(probeUrls, 3500);
968
+ if (fetched && fetched.text && fetched.text.length > 0) {
969
+ const result = parseBundleSource(fetched.text, fetched.bytes, fetched.url);
970
+ cachedAnalysis = result;
971
+ isAnalyzing = false;
972
+ subscribers.forEach(cb => cb(result));
973
+ subscribers.length = 0;
974
+ return result;
544
975
  }
545
- // Fallback to runtime memory estimation
976
+ // Fallback: could not reach a Metro dev server (release build, offline, or
977
+ // custom dev-server host). Return estimated values so the UI never crashes.
546
978
  const fallbackBytes = 6840000; // ~6.8MB standard RN dev bundle
547
- const result = parseBundleSource('', fallbackBytes, scriptURL);
979
+ const result = parseBundleSource('', fallbackBytes, scriptURL || 'unknown', false);
548
980
  cachedAnalysis = result;
549
981
  isAnalyzing = false;
550
982
  subscribers.forEach(cb => cb(result));
551
983
  subscribers.length = 0;
552
984
  return result;
553
985
  };
986
+ export const clearCachedBundleAnalysis = () => {
987
+ cachedAnalysis = null;
988
+ };
554
989
  export const getCachedBundleAnalysis = () => cachedAnalysis;