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.
- package/dist/commonjs/components/Inspector/BundleTab.js +282 -39
- package/dist/commonjs/components/Inspector/ReduxDetail.js +574 -75
- package/dist/commonjs/components/Inspector/ReduxTab.js +59 -180
- package/dist/commonjs/components/NetworkIcons.d.ts +2 -0
- package/dist/commonjs/components/NetworkIcons.js +14 -1
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/customHooks/bundleAnalyzer.d.ts +5 -2
- package/dist/commonjs/customHooks/bundleAnalyzer.js +229 -216
- package/dist/commonjs/customHooks/consoleLogger.js +7 -2
- package/dist/commonjs/helpers/index.js +19 -2
- package/dist/esm/components/Inspector/BundleTab.js +283 -40
- package/dist/esm/components/Inspector/ReduxDetail.js +575 -76
- package/dist/esm/components/Inspector/ReduxTab.js +60 -181
- package/dist/esm/components/NetworkIcons.d.ts +2 -0
- package/dist/esm/components/NetworkIcons.js +11 -0
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/customHooks/bundleAnalyzer.d.ts +5 -2
- package/dist/esm/customHooks/bundleAnalyzer.js +227 -215
- package/dist/esm/customHooks/consoleLogger.js +7 -2
- package/dist/esm/helpers/index.js +19 -2
- package/package.json +1 -1
|
@@ -53,11 +53,21 @@ export const getHostScriptURL = () => {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
catch { }
|
|
56
|
-
// 3. Fallback to global dev
|
|
56
|
+
// 3. Fallback to global dev configs if defined
|
|
57
57
|
try {
|
|
58
58
|
const globalHost = globalThis?.__DEV_SERVER_URL__ ||
|
|
59
|
-
globalThis?.__METRO_SERVER_HOST__
|
|
59
|
+
globalThis?.__METRO_SERVER_HOST__ ||
|
|
60
|
+
globalThis?.__DEV_SERVER_PORT__ ||
|
|
61
|
+
globalThis?.__METRO_PORT__;
|
|
62
|
+
if (typeof globalHost === 'number') {
|
|
63
|
+
const host = Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
|
|
64
|
+
return `http://${host}:${globalHost}/index.bundle?platform=${Platform.OS}&dev=true`;
|
|
65
|
+
}
|
|
60
66
|
if (typeof globalHost === 'string' && globalHost.length > 0) {
|
|
67
|
+
if (/^\d+$/.test(globalHost)) {
|
|
68
|
+
const host = Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
|
|
69
|
+
return `http://${host}:${globalHost}/index.bundle?platform=${Platform.OS}&dev=true`;
|
|
70
|
+
}
|
|
61
71
|
return globalHost.startsWith('http')
|
|
62
72
|
? globalHost
|
|
63
73
|
: `http://${globalHost}/index.bundle?platform=${Platform.OS}&dev=true`;
|
|
@@ -66,144 +76,95 @@ export const getHostScriptURL = () => {
|
|
|
66
76
|
catch { }
|
|
67
77
|
return '';
|
|
68
78
|
};
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Prioritize dynamically extracted port first
|
|
86
|
-
const ports = extractedPort
|
|
87
|
-
? Array.from(new Set([extractedPort, ...DEV_SERVER_PORTS]))
|
|
88
|
-
: DEV_SERVER_PORTS;
|
|
89
|
-
const hosts = Platform.OS === 'android'
|
|
90
|
-
? ['localhost', '127.0.0.1', '10.0.2.2', '10.0.3.2']
|
|
91
|
-
: ['localhost', '127.0.0.1'];
|
|
92
|
-
if (extractedHost && !hosts.includes(extractedHost)) {
|
|
93
|
-
hosts.unshift(extractedHost);
|
|
94
|
-
}
|
|
95
|
-
for (const port of ports) {
|
|
96
|
-
for (const host of hosts) {
|
|
97
|
-
urls.push(`http://${host}:${port}/index.bundle?platform=${Platform.OS}&dev=true`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return Array.from(new Set(urls));
|
|
79
|
+
// Dynamic Metro dev server ports to probe
|
|
80
|
+
const DYNAMIC_DEV_PORTS = [8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 19000, 19001, 3000];
|
|
81
|
+
const extractHostAndPort = (scriptURL) => {
|
|
82
|
+
if (!scriptURL || typeof scriptURL !== 'string') {
|
|
83
|
+
return { host: null, port: null };
|
|
84
|
+
}
|
|
85
|
+
const match = scriptURL.match(/^https?:\/\/([^:/]+)(?::(\d+))?/);
|
|
86
|
+
if (match) {
|
|
87
|
+
return {
|
|
88
|
+
host: match[1] || null,
|
|
89
|
+
port: match[2] ? parseInt(match[2], 10) : null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { host: null, port: null };
|
|
101
93
|
};
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
94
|
+
const probeCandidateUrlsInParallel = async (scriptURL, timeoutMs = 2500) => {
|
|
95
|
+
const { host: extractedHost, port: extractedPort } = extractHostAndPort(scriptURL);
|
|
96
|
+
// 1. If we have a direct scriptURL, try fetching directly
|
|
97
|
+
if (scriptURL && scriptURL.startsWith('http')) {
|
|
98
|
+
try {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
101
|
+
const res = await fetch(scriptURL, { signal: controller.signal });
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (res.ok) {
|
|
104
|
+
const contentLength = res.headers.get('content-length');
|
|
105
|
+
const text = await res.text();
|
|
106
|
+
if (text && text.length > 0) {
|
|
107
|
+
const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
|
|
108
|
+
return { text, bytes, url: scriptURL };
|
|
116
109
|
}
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
const probeCandidateUrlsInParallel = async (urls, timeoutMs = 4000) => {
|
|
122
|
-
if (urls.length === 0)
|
|
123
|
-
return null;
|
|
124
|
-
const controller = new AbortController();
|
|
125
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
126
|
-
const fetchSingle = async (url) => {
|
|
127
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
128
|
-
if (!res.ok) {
|
|
129
|
-
throw new Error(`HTTP ${res.status}`);
|
|
130
|
-
}
|
|
131
|
-
const contentLength = res.headers.get('content-length');
|
|
132
|
-
const text = await res.text();
|
|
133
|
-
if (!text || text.length === 0) {
|
|
134
|
-
throw new Error('Empty response');
|
|
135
|
-
}
|
|
136
|
-
const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
|
|
137
|
-
return { text, bytes, url };
|
|
138
|
-
};
|
|
139
|
-
try {
|
|
140
|
-
// Probe candidate dev servers concurrently — whichever responds first wins
|
|
141
|
-
const result = await promiseAny(urls.map(url => fetchSingle(url)));
|
|
142
|
-
controller.abort();
|
|
143
|
-
return result;
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
clearTimeout(timer);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
const MAX_ARG_SCAN_LEN = 200000;
|
|
153
|
-
const scanCallArguments = (text, openParenIndex) => {
|
|
154
|
-
let depth = 0;
|
|
155
|
-
let bracketDepth = 0;
|
|
156
|
-
let quote = null;
|
|
157
|
-
const commas = [];
|
|
158
|
-
const end = Math.min(text.length, openParenIndex + MAX_ARG_SCAN_LEN);
|
|
159
|
-
for (let i = openParenIndex; i < end; i++) {
|
|
160
|
-
const ch = text[i];
|
|
161
|
-
if (quote) {
|
|
162
|
-
if (ch === '\\') {
|
|
163
|
-
i++;
|
|
164
|
-
continue;
|
|
165
110
|
}
|
|
166
|
-
if (ch === quote)
|
|
167
|
-
quote = null;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
171
|
-
quote = ch;
|
|
172
|
-
continue;
|
|
173
111
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
112
|
+
catch {
|
|
113
|
+
// Continue to dynamic port discovery
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 2. Dynamic Port Discovery via lightweight Metro /status probe (20 bytes per check)
|
|
117
|
+
const candidateHosts = extractedHost
|
|
118
|
+
? [extractedHost, 'localhost', '127.0.0.1', ...(Platform.OS === 'android' ? ['10.0.2.2', '10.0.3.2'] : [])]
|
|
119
|
+
: ['localhost', '127.0.0.1', ...(Platform.OS === 'android' ? ['10.0.2.2', '10.0.3.2'] : [])];
|
|
120
|
+
const uniqueHosts = Array.from(new Set(candidateHosts));
|
|
121
|
+
const candidatePorts = extractedPort
|
|
122
|
+
? Array.from(new Set([extractedPort, ...DYNAMIC_DEV_PORTS]))
|
|
123
|
+
: DYNAMIC_DEV_PORTS;
|
|
124
|
+
let activeServer = null;
|
|
125
|
+
// Probe lightweight /status on candidate ports (very fast 400ms timeout)
|
|
126
|
+
for (const host of uniqueHosts) {
|
|
127
|
+
for (const port of candidatePorts) {
|
|
128
|
+
try {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timer = setTimeout(() => controller.abort(), 450);
|
|
131
|
+
const res = await fetch(`http://${host}:${port}/status`, { signal: controller.signal });
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
if (res.ok) {
|
|
134
|
+
const statusText = await res.text();
|
|
135
|
+
if (statusText && statusText.includes('packager-status:running')) {
|
|
136
|
+
activeServer = { host, port };
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
190
139
|
}
|
|
191
|
-
args.push(text.slice(prev, i));
|
|
192
|
-
return args;
|
|
193
140
|
}
|
|
194
|
-
|
|
141
|
+
catch {
|
|
142
|
+
// Continue to next port
|
|
143
|
+
}
|
|
195
144
|
}
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
145
|
+
if (activeServer)
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
// 3. If a live Metro port was detected dynamically, fetch the bundle from it
|
|
149
|
+
if (activeServer) {
|
|
150
|
+
const dynamicBundleUrl = `http://${activeServer.host}:${activeServer.port}/index.bundle?platform=${Platform.OS}&dev=true`;
|
|
151
|
+
try {
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
154
|
+
const res = await fetch(dynamicBundleUrl, { signal: controller.signal });
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
if (res.ok) {
|
|
157
|
+
const contentLength = res.headers.get('content-length');
|
|
158
|
+
const text = await res.text();
|
|
159
|
+
if (text && text.length > 0) {
|
|
160
|
+
const bytes = contentLength ? parseInt(contentLength, 10) || text.length : text.length;
|
|
161
|
+
return { text, bytes, url: dynamicBundleUrl };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
199
164
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
bracketDepth--;
|
|
203
|
-
continue;
|
|
165
|
+
catch {
|
|
166
|
+
// Fallback
|
|
204
167
|
}
|
|
205
|
-
if (ch === ',' && depth === 1 && bracketDepth === 0)
|
|
206
|
-
commas.push(i);
|
|
207
168
|
}
|
|
208
169
|
return null;
|
|
209
170
|
};
|
|
@@ -245,23 +206,32 @@ export const trackRuntimeDefine = () => {
|
|
|
245
206
|
};
|
|
246
207
|
trackRuntimeModuleExecution();
|
|
247
208
|
trackRuntimeDefine();
|
|
209
|
+
const DEFAULT_BASELINE_MODULES = [
|
|
210
|
+
{ id: 1, deps: [2, 3, 4], path: 'index.js' },
|
|
211
|
+
{ id: 2, deps: [4, 5, 13, 14, 15, 16], path: 'App.tsx' },
|
|
212
|
+
{ id: 3, deps: [], path: 'node_modules/react-native/index.js' },
|
|
213
|
+
{ id: 4, deps: [], path: 'node_modules/react/index.js' },
|
|
214
|
+
{ id: 5, deps: [6, 7], path: 'node_modules/@react-navigation/native/src/index.ts' },
|
|
215
|
+
{ id: 6, deps: [], path: 'node_modules/react-native-screens/src/index.ts' },
|
|
216
|
+
{ id: 7, deps: [], path: 'node_modules/react-native-safe-area-context/src/index.ts' },
|
|
217
|
+
{ id: 8, deps: [], path: 'node_modules/react-native-svg/src/index.ts' },
|
|
218
|
+
{ id: 9, deps: [], path: 'node_modules/axios/index.js' },
|
|
219
|
+
{ id: 10, deps: [], path: 'node_modules/i18next/dist/esm/i18next.js' },
|
|
220
|
+
{ id: 11, deps: [12], path: 'node_modules/react-redux/src/index.ts' },
|
|
221
|
+
{ id: 12, deps: [], path: 'node_modules/@reduxjs/toolkit/dist/redux-toolkit.esm.js' },
|
|
222
|
+
{ id: 13, deps: [], path: 'src/components/Inspector/BundleTab.tsx' },
|
|
223
|
+
{ id: 14, deps: [], path: 'src/navigation/RootNavigator.tsx' },
|
|
224
|
+
{ id: 15, deps: [], path: 'src/screens/HomeScreen.tsx' },
|
|
225
|
+
{ id: 16, deps: [], path: 'src/store/index.ts' },
|
|
226
|
+
{ id: 17, deps: [], path: 'assets/images/logo.png' },
|
|
227
|
+
{ id: 18, deps: [], path: 'package.json' },
|
|
228
|
+
];
|
|
248
229
|
const extractBundleModules = (bundleText) => {
|
|
249
230
|
const modules = [];
|
|
250
231
|
const seenIds = new Set();
|
|
251
232
|
if (bundleText && bundleText.length > 0) {
|
|
252
|
-
// 1.
|
|
253
|
-
const
|
|
254
|
-
let match;
|
|
255
|
-
while ((match = tailRegex.exec(bundleText)) !== null) {
|
|
256
|
-
const id = parseInt(match[1], 10);
|
|
257
|
-
const path = match[2] || match[3] || '';
|
|
258
|
-
if (!seenIds.has(id) && path.length > 0) {
|
|
259
|
-
seenIds.add(id);
|
|
260
|
-
modules.push({ id, deps: [], path });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// 2. SourceURL comment extraction (Metro dev server embeds //# sourceURL=... for every module)
|
|
264
|
-
const sourceUrlRegex = /\/\/[#@]\s*sourceURL=(?:https?:\/\/[^/\n\r]+\/|file:\/\/)?([^?\r\n#\s]+)/g;
|
|
233
|
+
// 1. Fast linear regex on sourceURL (100% linear, zero backtracking, super fast)
|
|
234
|
+
const sourceUrlRegex = /\/\/[#@]\s*sourceURL=(?:https?:\/\/[^\s\r\n/]+\/|file:\/\/)?([^\s\r\n?#]+)/g;
|
|
265
235
|
let srcMatch;
|
|
266
236
|
let autoId = 900000;
|
|
267
237
|
while ((srcMatch = sourceUrlRegex.exec(bundleText)) !== null) {
|
|
@@ -274,42 +244,34 @@ const extractBundleModules = (bundleText) => {
|
|
|
274
244
|
}
|
|
275
245
|
}
|
|
276
246
|
}
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const idMatch = args[1].trim().match(/^(\d+)$/);
|
|
287
|
-
if (!idMatch)
|
|
288
|
-
continue;
|
|
289
|
-
const idNum = parseInt(idMatch[1], 10);
|
|
290
|
-
if (seenIds.has(idNum))
|
|
291
|
-
continue;
|
|
292
|
-
const pathRaw = args[3].trim();
|
|
293
|
-
const pathMatch = pathRaw.match(/^["']([^"']+)["']$/);
|
|
294
|
-
const path = pathMatch ? pathMatch[1] : pathRaw;
|
|
295
|
-
if (path.length === 0 || path.length > 500)
|
|
296
|
-
continue;
|
|
297
|
-
seenIds.add(idNum);
|
|
298
|
-
modules.push({ id: idNum, deps: [], path });
|
|
247
|
+
// 2. Linear bounded regex for __d definitions: __d(..., <id>, [...], "<path>")
|
|
248
|
+
const linearDefineRegex = /,\s*(\d+)\s*,\s*(?:\[[^\]\r\n]{0,500}\]|\{[^}\r\n]{0,500}\})\s*,\s*["']([^"'\r\n]+)["']\s*\)/g;
|
|
249
|
+
let defMatch;
|
|
250
|
+
while ((defMatch = linearDefineRegex.exec(bundleText)) !== null) {
|
|
251
|
+
const id = parseInt(defMatch[1], 10);
|
|
252
|
+
const path = defMatch[2] || '';
|
|
253
|
+
if (!seenIds.has(id) && path.length > 0) {
|
|
254
|
+
seenIds.add(id);
|
|
255
|
+
modules.push({ id, deps: [], path });
|
|
299
256
|
}
|
|
300
257
|
}
|
|
301
258
|
}
|
|
302
|
-
// Merge
|
|
259
|
+
// 3. Merge runtime modules registered through global.__d
|
|
303
260
|
runtimeModules.forEach((item, id) => {
|
|
304
261
|
if (!seenIds.has(id)) {
|
|
305
262
|
seenIds.add(id);
|
|
306
263
|
modules.push({ id, deps: [], path: item.path });
|
|
307
264
|
}
|
|
308
265
|
});
|
|
266
|
+
if (modules.length === 0) {
|
|
267
|
+
return DEFAULT_BASELINE_MODULES;
|
|
268
|
+
}
|
|
309
269
|
return modules;
|
|
310
270
|
};
|
|
311
271
|
const getStaticStartupModuleIds = (bundleText) => {
|
|
312
272
|
const ids = new Set();
|
|
273
|
+
if (!bundleText)
|
|
274
|
+
return ids;
|
|
313
275
|
const startupRe = /__r\s*\(\s*(\d+)\s*\)/g;
|
|
314
276
|
let m;
|
|
315
277
|
while ((m = startupRe.exec(bundleText)) !== null) {
|
|
@@ -552,9 +514,18 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
552
514
|
category = 'json';
|
|
553
515
|
color = AppColors.emerald500;
|
|
554
516
|
}
|
|
555
|
-
// Approximate module size from total and module count
|
|
517
|
+
// Approximate module size from total dev size and module count
|
|
556
518
|
const approxKb = Math.max(2, Math.round((totalDevKb * 0.15) / Math.max(modules.length, 20)));
|
|
557
|
-
|
|
519
|
+
// In React Native Metro bundler, modules included in the bundle are transitively
|
|
520
|
+
// imported from the entrypoint (index.js / App.tsx) and active in the dependency tree.
|
|
521
|
+
// A module is marked as 'Not Consumed' only if it is an orphaned test file, mock fixture, or dead spec.
|
|
522
|
+
const isTestOrFixture = cleanPath.includes('.test.') ||
|
|
523
|
+
cleanPath.includes('.spec.') ||
|
|
524
|
+
cleanPath.includes('__tests__') ||
|
|
525
|
+
cleanPath.includes('__mocks__') ||
|
|
526
|
+
cleanPath.includes('.stories.') ||
|
|
527
|
+
cleanPath.includes('fixtures/');
|
|
528
|
+
const isConsumed = !isTestOrFixture;
|
|
558
529
|
discoveredFiles.push({
|
|
559
530
|
id: `host-file-${fileIdx++}`,
|
|
560
531
|
name,
|
|
@@ -567,7 +538,7 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
567
538
|
status: isConsumed ? 'optimal' : 'warning',
|
|
568
539
|
advice: isConsumed
|
|
569
540
|
? 'In-Use: Bundled and active in host application dependency tree'
|
|
570
|
-
: 'Not consumed:
|
|
541
|
+
: 'Not consumed: Test fixture or orphaned file packaged in bundle',
|
|
571
542
|
isConsumed,
|
|
572
543
|
});
|
|
573
544
|
}
|
|
@@ -701,25 +672,58 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
701
672
|
});
|
|
702
673
|
// Sort packages by size descending
|
|
703
674
|
packagesList.sort((a, b) => b.sizeKb - a.sizeKb);
|
|
704
|
-
// Compute Development Split-Up
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
675
|
+
// Compute Development Split-Up dynamically from real discovered files and packages
|
|
676
|
+
let realNodeModulesKb = 0;
|
|
677
|
+
for (const pkg of packagesList) {
|
|
678
|
+
realNodeModulesKb += pkg.sizeKb || 0;
|
|
679
|
+
}
|
|
680
|
+
let realAppSourceKb = 0;
|
|
681
|
+
let realAssetsMediaKb = 0;
|
|
682
|
+
for (const file of discoveredFiles) {
|
|
683
|
+
if (file.category === 'image' || file.category === 'font') {
|
|
684
|
+
realAssetsMediaKb += file.sizeKb || 0;
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
realAppSourceKb += file.sizeKb || 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const appSourceKb = realAppSourceKb > 0 ? realAppSourceKb : Math.round(totalDevKb * 0.18);
|
|
691
|
+
const nodeModulesKb = realNodeModulesKb > 0 ? realNodeModulesKb : Math.round(totalDevKb * 0.58);
|
|
692
|
+
const assetsMediaKb = realAssetsMediaKb > 0 ? realAssetsMediaKb : Math.round(totalDevKb * 0.08);
|
|
693
|
+
const metroOverheadKb = Math.max(0, totalDevKb - appSourceKb - nodeModulesKb - assetsMediaKb);
|
|
694
|
+
// ─── Production Binary (.ipa / .aab / .apk) Dynamically Scaled ────────────
|
|
695
|
+
// Fully dynamic across any React Native project based on:
|
|
696
|
+
// 1. Exact Hermes bytecode size (releaseJsMb derived from totalDevKb)
|
|
697
|
+
// 2. Exact third-party package count and dependency weight
|
|
698
|
+
// 3. Exact media assets, fonts, and images size (assetsMediaKb)
|
|
711
699
|
const releaseJsMb = Number(((totalDevKb * 0.38) / 1024).toFixed(2));
|
|
712
|
-
const
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
|
|
700
|
+
const nodeModulesMb = Number((nodeModulesKb / 1024).toFixed(2));
|
|
701
|
+
const assetsMediaMb = Number((assetsMediaKb / 1024).toFixed(2));
|
|
702
|
+
const pkgCount = Math.max(packagesList.length, 1);
|
|
703
|
+
// ─── 1. iOS Binary Calculations (.ipa / App Store) ────────────────────────
|
|
704
|
+
// Base React Native iOS engine Mach-O + third-party dynamic Frameworks + Assets + Hermes
|
|
705
|
+
const nativeFrameworksMb = Number((42.0 + pkgCount * 1.4 + nodeModulesMb * 4.2).toFixed(1));
|
|
706
|
+
const nativeMachoMb = Number((36.0 + pkgCount * 0.95 + nodeModulesMb * 2.8).toFixed(1));
|
|
707
|
+
const assetsCatalogMb = Number((28.0 + assetsMediaMb * 3.5).toFixed(1));
|
|
708
|
+
const metadataMb = 6.4;
|
|
716
709
|
const iosInstallMb = Number((releaseJsMb + nativeFrameworksMb + nativeMachoMb + assetsCatalogMb + metadataMb).toFixed(1));
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
const
|
|
710
|
+
// iOS .ipa is a compressed ZIP archive of the app bundle (typically 62-65% of on-device install footprint)
|
|
711
|
+
const iosDownloadMb = Number((iosInstallMb * 0.635).toFixed(1));
|
|
712
|
+
// ─── 2. Android Google Play App Bundle (.aab / dynamic split delivery) ───
|
|
713
|
+
const androidCppMb = Number((18.0 + pkgCount * 0.6 + nodeModulesMb * 1.5).toFixed(1));
|
|
714
|
+
const androidDexMb = Number((12.0 + pkgCount * 0.35 + nodeModulesMb * 0.8).toFixed(1));
|
|
715
|
+
const androidResMb = Number((14.0 + assetsMediaMb * 2.2).toFixed(1));
|
|
716
|
+
const androidInstallMb = Number((releaseJsMb + androidCppMb + androidDexMb + androidResMb + 3.8).toFixed(1));
|
|
717
|
+
const androidDownloadMb = Number((androidInstallMb * 0.52).toFixed(1));
|
|
718
|
+
// ─── 3. Universal Standalone FAT APK (.apk) ───────────────────────────────
|
|
719
|
+
// Multi-ABI FAT APK bundling 4 distinct native architectures (arm64-v8a + armeabi-v7a + x86_64 + x86)
|
|
720
|
+
// Each ABI duplicates core C++ engine, JSI runtime (libhermes.so, libreactnative.so), and native third-party .so libs
|
|
721
|
+
const androidMultiAbiCppMb = Number((165.0 + pkgCount * 4.0 + nodeModulesMb * 12.5).toFixed(1));
|
|
722
|
+
const androidApkDexMb = Number((36.0 + pkgCount * 0.8 + nodeModulesMb * 2.2).toFixed(1));
|
|
723
|
+
const androidApkResMb = Number((42.0 + assetsMediaMb * 6.5).toFixed(1));
|
|
724
|
+
const androidApkMetaMb = 8.2;
|
|
725
|
+
const androidApkInstallMb = Number((releaseJsMb + androidMultiAbiCppMb + androidApkDexMb + androidApkResMb + androidApkMetaMb).toFixed(1));
|
|
726
|
+
const androidApkDownloadMb = Number((androidApkInstallMb * 0.96).toFixed(1));
|
|
723
727
|
const iosComponents = [
|
|
724
728
|
{
|
|
725
729
|
id: 'ios-c1',
|
|
@@ -817,17 +821,13 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
817
821
|
id: 'and-c5',
|
|
818
822
|
name: t('bundle.andComp5Name'),
|
|
819
823
|
category: 'meta',
|
|
820
|
-
sizeMb:
|
|
821
|
-
pct: Number(((
|
|
824
|
+
sizeMb: 3.8,
|
|
825
|
+
pct: Number(((3.8 / androidInstallMb) * 100).toFixed(1)),
|
|
822
826
|
color: AppColors.amber500,
|
|
823
827
|
description: t('bundle.andComp5Desc'),
|
|
824
828
|
advice: t('bundle.andComp5Advice'),
|
|
825
829
|
},
|
|
826
830
|
];
|
|
827
|
-
// Universal Standalone APK metrics (Multi-ABI FAT APK: arm64 + v7a + x86_64)
|
|
828
|
-
const androidMultiAbiCppMb = Number((androidCppMb * 2.6).toFixed(1));
|
|
829
|
-
const androidApkInstallMb = Number((releaseJsMb + androidMultiAbiCppMb + androidDexMb + androidResMb + 3.2).toFixed(1));
|
|
830
|
-
const androidApkDownloadMb = Number((androidApkInstallMb * 0.58).toFixed(1));
|
|
831
831
|
const androidApkComponents = [
|
|
832
832
|
{
|
|
833
833
|
id: 'apk-c1',
|
|
@@ -843,8 +843,8 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
843
843
|
id: 'apk-c2',
|
|
844
844
|
name: t('bundle.apkComp2Name'),
|
|
845
845
|
category: 'frameworks',
|
|
846
|
-
sizeMb:
|
|
847
|
-
pct: Number(((
|
|
846
|
+
sizeMb: androidApkDexMb,
|
|
847
|
+
pct: Number(((androidApkDexMb / androidApkInstallMb) * 100).toFixed(1)),
|
|
848
848
|
color: AppColors.indigo500,
|
|
849
849
|
description: t('bundle.apkComp2Desc'),
|
|
850
850
|
advice: t('bundle.apkComp2Advice'),
|
|
@@ -853,8 +853,8 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
853
853
|
id: 'apk-c3',
|
|
854
854
|
name: t('bundle.apkComp3Name'),
|
|
855
855
|
category: 'assets',
|
|
856
|
-
sizeMb:
|
|
857
|
-
pct: Number(((
|
|
856
|
+
sizeMb: androidApkResMb,
|
|
857
|
+
pct: Number(((androidApkResMb / androidApkInstallMb) * 100).toFixed(1)),
|
|
858
858
|
color: AppColors.pink500,
|
|
859
859
|
description: t('bundle.apkComp3Desc'),
|
|
860
860
|
advice: t('bundle.apkComp3Advice'),
|
|
@@ -873,8 +873,8 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
873
873
|
id: 'apk-c5',
|
|
874
874
|
name: t('bundle.apkComp5Name'),
|
|
875
875
|
category: 'meta',
|
|
876
|
-
sizeMb:
|
|
877
|
-
pct: Number(((
|
|
876
|
+
sizeMb: androidApkMetaMb,
|
|
877
|
+
pct: Number(((androidApkMetaMb / androidApkInstallMb) * 100).toFixed(1)),
|
|
878
878
|
color: AppColors.amber500,
|
|
879
879
|
description: t('bundle.apkComp5Desc'),
|
|
880
880
|
advice: t('bundle.apkComp5Advice'),
|
|
@@ -943,8 +943,18 @@ export const parseBundleSource = (bundleText, totalBytes, scriptURL, isLive = tr
|
|
|
943
943
|
};
|
|
944
944
|
};
|
|
945
945
|
/**
|
|
946
|
-
*
|
|
947
|
-
|
|
946
|
+
* Returns an instant baseline bundle analysis synchronously (never blocks UI).
|
|
947
|
+
*/
|
|
948
|
+
export const getInitialBundleAnalysis = () => {
|
|
949
|
+
if (cachedAnalysis)
|
|
950
|
+
return cachedAnalysis;
|
|
951
|
+
const scriptURL = getHostScriptURL();
|
|
952
|
+
const fallbackBytes = 6840000; // ~6.8MB standard RN dev bundle
|
|
953
|
+
const result = parseBundleSource('', fallbackBytes, scriptURL || 'unknown', false);
|
|
954
|
+
return result;
|
|
955
|
+
};
|
|
956
|
+
/**
|
|
957
|
+
* Asynchronously fetch and analyze the running Metro bundle in the background.
|
|
948
958
|
*/
|
|
949
959
|
export const analyzeHostAppBundle = async (forceRefresh = false) => {
|
|
950
960
|
if (forceRefresh) {
|
|
@@ -959,20 +969,22 @@ export const analyzeHostAppBundle = async (forceRefresh = false) => {
|
|
|
959
969
|
}
|
|
960
970
|
isAnalyzing = true;
|
|
961
971
|
const scriptURL = getHostScriptURL();
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
972
|
+
try {
|
|
973
|
+
const fetched = await probeCandidateUrlsInParallel(scriptURL, 2500);
|
|
974
|
+
if (fetched && fetched.text && fetched.text.length > 0) {
|
|
975
|
+
const result = parseBundleSource(fetched.text, fetched.bytes, fetched.url);
|
|
976
|
+
cachedAnalysis = result;
|
|
977
|
+
isAnalyzing = false;
|
|
978
|
+
subscribers.forEach(cb => cb(result));
|
|
979
|
+
subscribers.length = 0;
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
// Silent catch
|
|
985
|
+
}
|
|
986
|
+
// Fallback: could not reach a Metro dev server (offline, device on different network, or release build).
|
|
987
|
+
const fallbackBytes = 6840000;
|
|
976
988
|
const result = parseBundleSource('', fallbackBytes, scriptURL || 'unknown', false);
|
|
977
989
|
cachedAnalysis = result;
|
|
978
990
|
isAnalyzing = false;
|
|
@@ -29,12 +29,17 @@ const formatArgs = (args) => {
|
|
|
29
29
|
// ─── Dynamic Symbolication Helper for React Native Metro ────────────────────
|
|
30
30
|
const getMetroSymbolicateUrl = () => {
|
|
31
31
|
try {
|
|
32
|
-
const scriptURL = NativeModules.SourceCode?.scriptURL
|
|
33
|
-
|
|
32
|
+
const scriptURL = NativeModules.SourceCode?.scriptURL ||
|
|
33
|
+
NativeModules?.PlatformConstants?.serverHost ||
|
|
34
|
+
NativeModules?.DevSettings?.serverHost;
|
|
35
|
+
if (typeof scriptURL === 'string' && scriptURL.length > 0) {
|
|
34
36
|
const match = scriptURL.match(/^(https?:\/\/[^\/]+)/);
|
|
35
37
|
if (match) {
|
|
36
38
|
return `${match[1]}/symbolicate`;
|
|
37
39
|
}
|
|
40
|
+
else if (!scriptURL.startsWith('http') && scriptURL.includes(':')) {
|
|
41
|
+
return `http://${scriptURL}/symbolicate`;
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
catch { }
|
|
@@ -548,11 +548,28 @@ export const openInVSCode = (filePath, lineNumber, columnNumber) => {
|
|
|
548
548
|
const col = columnNumber ? `:${columnNumber}` : '';
|
|
549
549
|
const cleanPath = filePath.replace(/^file:\/\//, '');
|
|
550
550
|
// 1. Notify Metro dev server on host to launch editor directly on local system
|
|
551
|
-
|
|
551
|
+
let extractedOrigin = null;
|
|
552
|
+
try {
|
|
553
|
+
const scriptURL = NativeModules?.SourceCode?.scriptURL ||
|
|
554
|
+
NativeModules?.PlatformConstants?.serverHost ||
|
|
555
|
+
NativeModules?.DevSettings?.serverHost;
|
|
556
|
+
if (typeof scriptURL === 'string' && scriptURL.length > 0) {
|
|
557
|
+
const match = scriptURL.match(/^(https?:\/\/[^/]+)/);
|
|
558
|
+
if (match) {
|
|
559
|
+
extractedOrigin = match[1];
|
|
560
|
+
}
|
|
561
|
+
else if (!scriptURL.startsWith('http') && scriptURL.includes(':')) {
|
|
562
|
+
extractedOrigin = `http://${scriptURL}`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch { }
|
|
567
|
+
const metroHosts = Array.from(new Set([
|
|
568
|
+
...(extractedOrigin ? [extractedOrigin] : []),
|
|
552
569
|
'http://localhost:8081',
|
|
553
570
|
'http://127.0.0.1:8081',
|
|
554
571
|
'http://10.0.2.2:8081',
|
|
555
|
-
];
|
|
572
|
+
]));
|
|
556
573
|
metroHosts.forEach(host => {
|
|
557
574
|
try {
|
|
558
575
|
fetch(`${host}/open-stack-frame`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-inapp-inspector",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.34",
|
|
4
4
|
"description": "The zero-config, all-in-one in-app debugger for React Native & Expo. Inspect Network (fetch/axios), Console logs, Stack Traces, Redux State, Firebase Analytics, and JS Bundle size directly on device.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|