react-native-inapp-inspector 1.1.32 → 1.1.33

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
- }
178
- if (ch === '(') {
179
- depth++;
180
- continue;
181
115
  }
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) {
@@ -950,8 +912,19 @@ const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = true) =>
950
912
  };
951
913
  exports.parseBundleSource = parseBundleSource;
952
914
  /**
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.
915
+ * Returns an instant baseline bundle analysis synchronously (never blocks UI).
916
+ */
917
+ const getInitialBundleAnalysis = () => {
918
+ if (cachedAnalysis)
919
+ return cachedAnalysis;
920
+ const scriptURL = (0, exports.getHostScriptURL)();
921
+ const fallbackBytes = 6840000; // ~6.8MB standard RN dev bundle
922
+ const result = (0, exports.parseBundleSource)('', fallbackBytes, scriptURL || 'unknown', false);
923
+ return result;
924
+ };
925
+ exports.getInitialBundleAnalysis = getInitialBundleAnalysis;
926
+ /**
927
+ * Asynchronously fetch and analyze the running Metro bundle in the background.
955
928
  */
956
929
  const analyzeHostAppBundle = async (forceRefresh = false) => {
957
930
  if (forceRefresh) {
@@ -966,20 +939,22 @@ const analyzeHostAppBundle = async (forceRefresh = false) => {
966
939
  }
967
940
  isAnalyzing = true;
968
941
  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
942
+ try {
943
+ const fetched = await probeCandidateUrlsInParallel(scriptURL, 2500);
944
+ if (fetched && fetched.text && fetched.text.length > 0) {
945
+ const result = (0, exports.parseBundleSource)(fetched.text, fetched.bytes, fetched.url);
946
+ cachedAnalysis = result;
947
+ isAnalyzing = false;
948
+ subscribers.forEach(cb => cb(result));
949
+ subscribers.length = 0;
950
+ return result;
951
+ }
952
+ }
953
+ catch {
954
+ // Silent catch
955
+ }
956
+ // Fallback: could not reach a Metro dev server (offline, device on different network, or release build).
957
+ const fallbackBytes = 6840000;
983
958
  const result = (0, exports.parseBundleSource)('', fallbackBytes, scriptURL || 'unknown', false);
984
959
  cachedAnalysis = result;
985
960
  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`, {
@@ -9,7 +9,7 @@ import { AppFonts } from '../../styles/AppFonts';
9
9
  import { copyToClipboard } from '../../helpers';
10
10
  import { PackageIcon, SearchIcon, ClearIcon, CircleAlertIcon, LayersIcon, TimelineIcon, LiveStateIcon, StorageIcon, MetadataIcon, FolderIcon, FolderOpenIcon, TrashIcon, LightbulbIcon, SparkleIcon, ClockIcon, RefreshCcwIcon, ExternalLinkIcon, DownloadIcon, } from '../NetworkIcons';
11
11
  import Svg, { Path, Rect, Circle, Ellipse, G, Defs, LinearGradient, Stop, } from 'react-native-svg';
12
- import { analyzeHostAppBundle, getCachedBundleAnalysis, } from '../../customHooks/bundleAnalyzer';
12
+ import { analyzeHostAppBundle, getCachedBundleAnalysis, getInitialBundleAnalysis, getHostScriptURL, } from '../../customHooks/bundleAnalyzer';
13
13
  // ─── High-Fidelity Vector Package & NPM Logos ────────────────────────────────
14
14
  const PackageLogoRenderer = ({ name, size = 28 }) => {
15
15
  const lowerName = name.toLowerCase();
@@ -307,22 +307,25 @@ function buildBundleFileTree(files) {
307
307
  }
308
308
  export const downloadBundleFile = async (file, scriptURL) => {
309
309
  let content = '';
310
- // 1. Attempt to fetch real source code from Metro dev server if available
310
+ // 1. Attempt to fetch real source code from Metro dev server dynamically
311
311
  try {
312
- let devServerOrigin = 'http://localhost:8081';
313
- if (scriptURL && scriptURL.startsWith('http')) {
314
- const match = scriptURL.match(/^(https?:\/\/[^/]+)/);
312
+ let devServerOrigin = '';
313
+ const activeUrl = scriptURL || getHostScriptURL();
314
+ if (activeUrl && activeUrl.startsWith('http')) {
315
+ const match = activeUrl.match(/^(https?:\/\/[^/]+)/);
315
316
  if (match) {
316
317
  devServerOrigin = match[1];
317
318
  }
318
319
  }
319
- const cleanPath = file.path.replace(/^\/+/, '');
320
- const url = `${devServerOrigin}/${cleanPath}`;
321
- const res = await fetch(url);
322
- if (res.ok) {
323
- const text = await res.text();
324
- if (text && text.length > 0) {
325
- content = text;
320
+ if (devServerOrigin) {
321
+ const cleanPath = file.path.replace(/^\/+/, '');
322
+ const url = `${devServerOrigin}/${cleanPath}`;
323
+ const res = await fetch(url);
324
+ if (res.ok) {
325
+ const text = await res.text();
326
+ if (text && text.length > 0) {
327
+ content = text;
328
+ }
326
329
  }
327
330
  }
328
331
  }
@@ -483,22 +486,32 @@ const BundleTab = React.memo(() => {
483
486
  const [search, setSearch] = useState('');
484
487
  const [activeCategory, setActiveCategory] = useState('ALL');
485
488
  // ─── Live Host App Bundle Analysis (fetched from Metro / runtime) ─────────
486
- const [analysis, setAnalysis] = useState(() => getCachedBundleAnalysis());
487
- const [isAnalyzing, setIsAnalyzing] = useState(!getCachedBundleAnalysis());
489
+ const [analysis, setAnalysis] = useState(() => getCachedBundleAnalysis() || getInitialBundleAnalysis());
490
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
488
491
  const refreshAnalysis = useCallback(() => {
489
492
  setIsAnalyzing(true);
490
- analyzeHostAppBundle(true).then(result => {
493
+ analyzeHostAppBundle(true)
494
+ .then(result => {
491
495
  setAnalysis(result);
492
496
  setIsAnalyzing(false);
497
+ })
498
+ .catch(() => {
499
+ setIsAnalyzing(false);
493
500
  });
494
501
  }, []);
495
502
  useEffect(() => {
496
503
  let mounted = true;
497
- analyzeHostAppBundle().then(result => {
504
+ analyzeHostAppBundle()
505
+ .then(result => {
498
506
  if (mounted) {
499
507
  setAnalysis(result);
500
508
  setIsAnalyzing(false);
501
509
  }
510
+ })
511
+ .catch(() => {
512
+ if (mounted) {
513
+ setIsAnalyzing(false);
514
+ }
502
515
  });
503
516
  return () => {
504
517
  mounted = false;
@@ -752,21 +765,10 @@ const BundleTab = React.memo(() => {
752
765
  </TouchableScale>
753
766
  </View>
754
767
 
755
- {/* ─── Live Bundle Analysis Loading State ─── */}
756
- {isAnalyzing && (<View style={bundleStyles.analyzingContainer}>
757
- <ActivityIndicator size="small" color={AppColors.brandPurple}/>
758
- <Text style={bundleStyles.analyzingText}>
759
- {t('bundle.analyzingTitle')}
760
- </Text>
761
- <Text style={bundleStyles.analyzingHint}>
762
- {t('bundle.analyzingHint')}
763
- </Text>
764
- </View>)}
765
-
766
768
  {/* ══════════════════════════════════════════════════════════════════════ */}
767
769
  {/* ── 1. TAB: OVERVIEW & TREEMAP ───────────────────────────────────────── */}
768
770
  {/* ══════════════════════════════════════════════════════════════════════ */}
769
- {activeSubTab === 'overview' && !isAnalyzing && (<ScrollView style={{ flex: 1 }} contentContainerStyle={bundleStyles.contentContainer} keyboardShouldPersistTaps="handled">
771
+ {activeSubTab === 'overview' && (<ScrollView style={{ flex: 1 }} contentContainerStyle={bundleStyles.contentContainer} keyboardShouldPersistTaps="handled">
770
772
 
771
773
  {/* Hero Overview Card */}
772
774
  <View style={bundleStyles.heroCard}>
@@ -1089,7 +1091,7 @@ const BundleTab = React.memo(() => {
1089
1091
  {/* ══════════════════════════════════════════════════════════════════════ */}
1090
1092
  {/* ── 2. TAB: PRODUCTION BUILDS & PLATFORMS ───────────────────────────── */}
1091
1093
  {/* ══════════════════════════════════════════════════════════════════════ */}
1092
- {activeSubTab === 'production' && !isAnalyzing && (<ScrollView style={{ flex: 1 }} contentContainerStyle={bundleStyles.contentContainer} keyboardShouldPersistTaps="handled">
1094
+ {activeSubTab === 'production' && (<ScrollView style={{ flex: 1 }} contentContainerStyle={bundleStyles.contentContainer} keyboardShouldPersistTaps="handled">
1093
1095
 
1094
1096
  {/* Platform Segmented Switcher (Horizontally Scrollable) */}
1095
1097
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={bundleStyles.prodPlatformScroll}>
@@ -1317,7 +1319,7 @@ const BundleTab = React.memo(() => {
1317
1319
  {/* ══════════════════════════════════════════════════════════════════════ */}
1318
1320
  {/* ── 2. TAB: FILES BREAKDOWN ─────────────────────────────────────────── */}
1319
1321
  {/* ══════════════════════════════════════════════════════════════════════ */}
1320
- {activeSubTab === 'files' && !isAnalyzing && (<View style={{ flex: 1 }}>
1322
+ {activeSubTab === 'files' && (<View style={{ flex: 1 }}>
1321
1323
  {/* Search & Category Filter */}
1322
1324
  <View style={bundleStyles.filterContainer}>
1323
1325
  <View style={bundleStyles.searchRow}>
@@ -1511,7 +1513,7 @@ const BundleTab = React.memo(() => {
1511
1513
  {/* ══════════════════════════════════════════════════════════════════════ */}
1512
1514
  {/* ── 3. TAB: PACKAGES & NODE_MODULES ─────────────────────────────────── */}
1513
1515
  {/* ══════════════════════════════════════════════════════════════════════ */}
1514
- {activeSubTab === 'packages' && !isAnalyzing && (<View style={{ flex: 1 }}>
1516
+ {activeSubTab === 'packages' && (<View style={{ flex: 1 }}>
1515
1517
  <ScrollView style={{ flex: 1 }} contentContainerStyle={bundleStyles.contentContainer} keyboardShouldPersistTaps="handled">
1516
1518
 
1517
1519
  <View style={bundleStyles.filterContainer}>
@@ -1665,7 +1667,7 @@ const BundleTab = React.memo(() => {
1665
1667
  {/* ══════════════════════════════════════════════════════════════════════ */}
1666
1668
  {/* ── 4. TAB: MEDIA & ASSET AUDITOR ───────────────────────────────────── */}
1667
1669
  {/* ══════════════════════════════════════════════════════════════════════ */}
1668
- {activeSubTab === 'media' && !isAnalyzing && (<View style={{ flex: 1 }}>
1670
+ {activeSubTab === 'media' && (<View style={{ flex: 1 }}>
1669
1671
  <ScrollView style={{ flex: 1 }} contentContainerStyle={bundleStyles.contentContainer} keyboardShouldPersistTaps="handled">
1670
1672
 
1671
1673
  <View style={bundleStyles.tipsCard}>