react-native-debug-toolkit 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A dev-only floating debug panel for React Native with network, console, Zustand, navigation, and event logs",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -34,11 +34,11 @@
34
34
  "license": "MIT",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "https://github.com/zcj0808/react-native-debug-toolkit"
37
+ "url": "https://github.com/superzcj/react-native-debug-toolkit"
38
38
  },
39
- "homepage": "https://github.com/zcj0808/react-native-debug-toolkit#readme",
39
+ "homepage": "https://github.com/superzcj/react-native-debug-toolkit#readme",
40
40
  "bugs": {
41
- "url": "https://github.com/zcj0808/react-native-debug-toolkit/issues"
41
+ "url": "https://github.com/superzcj/react-native-debug-toolkit/issues"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@react-native-clipboard/clipboard": ">=1.0.0",
@@ -1,15 +1,12 @@
1
1
  import { NetworkLogTab } from './NetworkLogTab';
2
2
 
3
- export type { AxiosInstanceLike } from './networkInterceptor';
4
3
  import type { DebugFeature, NetworkLogEntry } from '../../types';
5
4
  import { createEventChannel } from '../../utils/createEventChannel';
6
5
  import { createPersistedObservableStore } from '../../utils/createPersistedObservableStore';
7
6
  import { KEYS } from '../../utils/debugPreferences';
8
7
  import { urlRewriter } from '../../utils/urlRewriterRegistry';
9
- import type { AxiosInstanceLike } from './networkInterceptor';
10
8
  import {
11
- startFetch,
12
- startAxios,
9
+ startXMLHttpRequest,
13
10
  resetInterceptors,
14
11
  } from './networkInterceptor';
15
12
 
@@ -46,8 +43,6 @@ export interface NetworkFeatureConfig {
46
43
  maxLogs?: number;
47
44
  /** URLs to filter out from logging */
48
45
  blacklist?: Array<string | RegExp>;
49
- /** Axios instance to intercept. Pass your axios.create() instance to capture axios requests. */
50
- axiosInstance?: AxiosInstanceLike;
51
46
  }
52
47
 
53
48
  export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeature<NetworkLogEntry[]> => {
@@ -59,8 +54,7 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
59
54
  });
60
55
  let initialized = false;
61
56
  let unsubscribeLogs: (() => void) | null = null;
62
- let stopFetchFn: (() => void) | null = null;
63
- let stopAxiosFn: (() => void) | null = null;
57
+ let stopXhrFn: (() => void) | null = null;
64
58
 
65
59
  const handleLog = (entry: NetworkLogPayload) => {
66
60
  if (isUrlBlacklisted(entry.request.url, blacklist)) {
@@ -78,10 +72,7 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
78
72
  return;
79
73
  }
80
74
  unsubscribeLogs = networkChannel.subscribe(handleLog);
81
- stopFetchFn = startFetch(emitNetworkLog);
82
- if (config?.axiosInstance) {
83
- stopAxiosFn = startAxios(config.axiosInstance, emitNetworkLog);
84
- }
75
+ stopXhrFn = startXMLHttpRequest(emitNetworkLog);
85
76
  initialized = true;
86
77
  },
87
78
  getSnapshot: () => logStore.getData(),
@@ -95,10 +86,8 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
95
86
  urlRewriter.set(null);
96
87
  unsubscribeLogs?.();
97
88
  unsubscribeLogs = null;
98
- stopFetchFn?.();
99
- stopFetchFn = null;
100
- stopAxiosFn?.();
101
- stopAxiosFn = null;
89
+ stopXhrFn?.();
90
+ stopXhrFn = null;
102
91
  logStore.clear();
103
92
  initialized = false;
104
93
  },
@@ -3,26 +3,8 @@ import { urlRewriter } from '../../utils/urlRewriterRegistry';
3
3
 
4
4
  type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
5
5
 
6
- // Intercepts fetch requests and optionally axios via interceptors.
7
- // Axios response data is captured directly through interceptor callbacks,
8
- // avoiding the unreliable XHR response interception in React Native.
9
-
10
- // ─── Minimal axios interface (no hard dependency) ──────
11
-
12
- export interface AxiosInstanceLike {
13
- interceptors: {
14
- request: {
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- use(onFulfilled?: (config: any) => any, onRejected?: (error: any) => any): number;
17
- eject(id: number): void;
18
- };
19
- response: {
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- use(onFulfilled?: (response: any) => any, onRejected?: (error: any) => any): number;
22
- eject(id: number): void;
23
- };
24
- };
25
- }
6
+ // Intercepts React Native's XMLHttpRequest transport layer.
7
+ // RN fetch and axios (default adapter) both go through XHR — one hook captures everything.
26
8
 
27
9
  // ─── Shared helpers ────────────────────────────────────
28
10
 
@@ -38,47 +20,31 @@ function rewriteUrl(url: string): string {
38
20
  }
39
21
  }
40
22
 
41
- function headersToObject(
42
- headers: Record<string, string> | Headers | undefined,
43
- ): Record<string, string> {
44
- const result: Record<string, string> = {};
45
- if (!headers) {
46
- return result;
47
- }
48
- if (typeof (headers as Headers).forEach === 'function') {
49
- (headers as Headers).forEach((value: string, key: string) => {
50
- result[key] = value;
51
- });
52
- return result;
23
+ function parseRawHeaders(rawHeaders: string | null | undefined): Record<string, string> | undefined {
24
+ if (!rawHeaders) {
25
+ return undefined;
53
26
  }
54
- Object.keys(headers).forEach((key) => {
55
- result[key] = (headers as Record<string, string>)[key]!;
56
- });
57
- return result;
58
- }
59
27
 
60
- function getRequestSnapshot(
61
- input: unknown,
62
- init?: RequestInit,
63
- ): NetworkLogEntry['request'] {
64
- const request = input instanceof Request ? input : null;
65
- return {
66
- url: typeof input === 'string' ? input : request?.url ?? String(input),
67
- method: (init?.method || request?.method || 'GET').toUpperCase(),
68
- headers: init?.headers
69
- ? headersToObject(init.headers as Record<string, string> | Headers)
70
- : request?.headers
71
- ? headersToObject(request.headers)
72
- : undefined,
73
- body: init?.body,
74
- };
28
+ const headers: Record<string, string> = {};
29
+ rawHeaders
30
+ .trim()
31
+ .split(/[\r\n]+/)
32
+ .forEach((line) => {
33
+ const separatorIndex = line.indexOf(':');
34
+ if (separatorIndex <= 0) {
35
+ return;
36
+ }
37
+ const key = line.slice(0, separatorIndex).trim();
38
+ const value = line.slice(separatorIndex + 1).trim();
39
+ if (key) {
40
+ headers[key] = value;
41
+ }
42
+ });
43
+ return Object.keys(headers).length > 0 ? headers : undefined;
75
44
  }
76
45
 
77
- async function parseResponseBody(response: Response): Promise<unknown> {
78
- const raw = await response.clone().text();
79
- if (!raw) {
80
- return undefined;
81
- }
46
+ function parseBodyText(raw: string): unknown {
47
+ if (!raw) return undefined;
82
48
  try {
83
49
  return JSON.parse(raw);
84
50
  } catch {
@@ -86,226 +52,235 @@ async function parseResponseBody(response: Response): Promise<unknown> {
86
52
  }
87
53
  }
88
54
 
89
- function normalizeAxiosHeaders(
90
- headers: unknown,
91
- ): Record<string, string> | undefined {
92
- if (!headers || typeof headers !== 'object') {
93
- return undefined;
94
- }
95
- // AxiosHeaders instance
96
- if (typeof (headers as Record<string, unknown>).forEach === 'function') {
97
- const result: Record<string, string> = {};
98
- (headers as { forEach(fn: (value: string, key: string) => void): void }).forEach(
99
- (value, key) => {
100
- result[key] = value;
101
- },
102
- );
103
- return Object.keys(result).length > 0 ? result : undefined;
55
+ function normalizeXhrResponseBody(xhr: XMLHttpRequestLike): unknown {
56
+ const text = safeRead(() => xhr.responseText);
57
+ if (typeof text === 'string' && text) {
58
+ return parseBodyText(text);
104
59
  }
105
- // Plain object
106
- const result: Record<string, string> = {};
107
- const obj = headers as Record<string, unknown>;
108
- for (const key of Object.keys(obj)) {
109
- const val = obj[key];
110
- if (typeof val === 'string') {
111
- result[key] = val;
112
- }
60
+
61
+ const response = safeRead(() => xhr.response);
62
+ if (response != null) {
63
+ return response;
113
64
  }
114
- return Object.keys(result).length > 0 ? result : undefined;
65
+
66
+ return undefined;
115
67
  }
116
68
 
117
- function buildFullUrl(config: {
118
- baseURL?: string;
119
- url?: string;
120
- }): string {
121
- const base = config.baseURL ?? '';
122
- const url = config.url ?? '';
123
- if (!base || url.startsWith('http')) {
124
- return url;
69
+ // ─── XMLHttpRequest interceptor ───────────────────────
70
+
71
+ interface XMLHttpRequestLike {
72
+ readyState: number;
73
+ DONE?: number;
74
+ status: number;
75
+ statusText?: string;
76
+ responseHeaders?: Record<string, string> | null;
77
+ responseType?: string;
78
+ response?: unknown;
79
+ responseText?: string;
80
+ responseURL?: string;
81
+ open(method: string, url: string, ...args: unknown[]): void;
82
+ send(body?: unknown): void;
83
+ setRequestHeader(header: string, value: string): void;
84
+ getAllResponseHeaders?(): string | null;
85
+ addEventListener(type: string, listener: () => void): void;
86
+ removeEventListener(type: string, listener: () => void): void;
87
+ }
88
+
89
+ type XMLHttpRequestConstructorLike = new () => XMLHttpRequestLike;
90
+
91
+ type XhrState = {
92
+ method: string;
93
+ url: string;
94
+ headers: Record<string, string>;
95
+ body?: unknown;
96
+ timestamp: number;
97
+ error?: string;
98
+ completed?: boolean;
99
+ };
100
+
101
+ let originalXMLHttpRequest: XMLHttpRequestConstructorLike | null = null;
102
+ let originalXhrOpen: XMLHttpRequestLike['open'] | null = null;
103
+ let originalXhrSend: XMLHttpRequestLike['send'] | null = null;
104
+ let originalXhrSetRequestHeader: XMLHttpRequestLike['setRequestHeader'] | null = null;
105
+ let xhrRefCount = 0;
106
+ const pendingXhrRequests = new WeakMap<XMLHttpRequestLike, XhrState>();
107
+
108
+ function safeRead<T>(read: () => T): T | undefined {
109
+ try {
110
+ return read();
111
+ } catch {
112
+ return undefined;
125
113
  }
126
- return base.replace(/\/$/, '') + '/' + url.replace(/^\//, '');
127
114
  }
128
115
 
129
- // ─── Fetch interceptor ─────────────────────────────────
116
+ function getGlobalXMLHttpRequest(): XMLHttpRequestConstructorLike | undefined {
117
+ return (globalThis as { XMLHttpRequest?: XMLHttpRequestConstructorLike }).XMLHttpRequest;
118
+ }
130
119
 
131
- let originalFetch: typeof globalThis.fetch | null = null;
132
- let fetchRefCount = 0;
120
+ function getXhrResponseHeaders(xhr: XMLHttpRequestLike): Record<string, string> | undefined {
121
+ const rawHeaders = safeRead(() => xhr.getAllResponseHeaders?.());
122
+ const parsedHeaders = parseRawHeaders(rawHeaders);
123
+ if (parsedHeaders) {
124
+ return parsedHeaders;
125
+ }
133
126
 
134
- function stopFetch(): void {
135
- fetchRefCount = Math.max(0, fetchRefCount - 1);
136
- if (fetchRefCount === 0 && originalFetch) {
137
- globalThis.fetch = originalFetch;
138
- originalFetch = null;
127
+ const headers = xhr.responseHeaders;
128
+ if (!headers) {
129
+ return undefined;
139
130
  }
131
+ return Object.keys(headers).length > 0 ? headers : undefined;
140
132
  }
141
133
 
142
- export function startFetch(emit: (entry: NetworkLogPayload) => void): () => void {
143
- fetchRefCount += 1;
144
- if (originalFetch) {
145
- return () => {
146
- stopFetch();
147
- };
134
+ function stopXMLHttpRequest(): void {
135
+ xhrRefCount = Math.max(0, xhrRefCount - 1);
136
+ if (xhrRefCount > 0 || !originalXMLHttpRequest) {
137
+ return;
148
138
  }
149
139
 
150
- originalFetch = globalThis.fetch;
151
-
152
- globalThis.fetch = async function interceptedFetch(
153
- input: Parameters<typeof fetch>[0],
154
- init?: Parameters<typeof fetch>[1],
155
- ) {
156
- const startTime = Date.now();
157
-
158
- let rewrittenInput: typeof input = input;
159
- if (urlRewriter.get()) {
160
- if (typeof input === 'string') {
161
- rewrittenInput = rewriteUrl(input);
162
- } else if (input instanceof Request) {
163
- rewrittenInput = new Request(rewriteUrl(input.url), input as RequestInit);
164
- }
140
+ const CurrentXMLHttpRequest = getGlobalXMLHttpRequest();
141
+ if (CurrentXMLHttpRequest) {
142
+ if (originalXhrOpen) {
143
+ CurrentXMLHttpRequest.prototype.open = originalXhrOpen;
165
144
  }
166
-
167
- const request = getRequestSnapshot(rewrittenInput, init);
168
-
169
- try {
170
- const response = await originalFetch!.call(globalThis, rewrittenInput, init);
171
- const duration = Date.now() - startTime;
172
-
173
- try {
174
- const data = await parseResponseBody(response);
175
- emit({
176
- timestamp: startTime,
177
- duration,
178
- request,
179
- response: { status: response.status, statusText: response.statusText, data },
180
- });
181
- } catch {
182
- emit({
183
- timestamp: startTime,
184
- duration,
185
- request,
186
- response: { status: response.status, statusText: response.statusText },
187
- });
188
- }
189
-
190
- return response;
191
- } catch (error) {
192
- emit({
193
- timestamp: startTime,
194
- duration: Date.now() - startTime,
195
- request,
196
- error: error instanceof Error ? error.message : String(error),
197
- });
198
- throw error;
145
+ if (originalXhrSend) {
146
+ CurrentXMLHttpRequest.prototype.send = originalXhrSend;
199
147
  }
200
- };
148
+ if (originalXhrSetRequestHeader) {
149
+ CurrentXMLHttpRequest.prototype.setRequestHeader = originalXhrSetRequestHeader;
150
+ }
151
+ }
201
152
 
202
- return () => {
203
- stopFetch();
204
- };
153
+ originalXMLHttpRequest = null;
154
+ originalXhrOpen = null;
155
+ originalXhrSend = null;
156
+ originalXhrSetRequestHeader = null;
205
157
  }
206
158
 
207
- // ─── Axios interceptor ─────────────────────────────────
208
-
209
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
210
- const pendingAxiosRequests = new WeakMap<any, { startTime: number; timestamp: number }>();
211
-
212
- export function startAxios(
213
- axiosInstance: AxiosInstanceLike,
159
+ export function startXMLHttpRequest(
214
160
  emit: (entry: NetworkLogPayload) => void,
215
161
  ): () => void {
216
- const requestInterceptorId = axiosInstance.interceptors.request.use(
217
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
- (config: any) => {
219
- const now = Date.now();
220
- pendingAxiosRequests.set(config, { startTime: now, timestamp: now });
221
-
222
- if (urlRewriter.get() && config.url) {
223
- const fullUrl = buildFullUrl(config);
224
- const rewritten = rewriteUrl(fullUrl);
225
- if (rewritten !== fullUrl) {
226
- config.url = rewritten;
227
- if (config.baseURL) {
228
- config.baseURL = undefined;
229
- }
230
- }
231
- }
162
+ const CurrentXMLHttpRequest = getGlobalXMLHttpRequest();
163
+ if (!CurrentXMLHttpRequest) {
164
+ return () => {};
165
+ }
166
+
167
+ xhrRefCount += 1;
168
+ if (originalXMLHttpRequest) {
169
+ return () => {
170
+ stopXMLHttpRequest();
171
+ };
172
+ }
173
+
174
+ originalXMLHttpRequest = CurrentXMLHttpRequest;
175
+ originalXhrOpen = CurrentXMLHttpRequest.prototype.open;
176
+ originalXhrSend = CurrentXMLHttpRequest.prototype.send;
177
+ originalXhrSetRequestHeader = CurrentXMLHttpRequest.prototype.setRequestHeader;
178
+
179
+ CurrentXMLHttpRequest.prototype.open = function interceptedOpen(
180
+ this: XMLHttpRequestLike,
181
+ method: string,
182
+ url: string,
183
+ ...args: unknown[]
184
+ ) {
185
+ const rewrittenUrl = urlRewriter.get() ? rewriteUrl(url) : url;
186
+ pendingXhrRequests.set(this, {
187
+ method: (method || 'GET').toUpperCase(),
188
+ url: rewrittenUrl,
189
+ headers: {},
190
+ timestamp: Date.now(),
191
+ });
192
+ return originalXhrOpen!.call(this, method, rewrittenUrl, ...args);
193
+ };
232
194
 
233
- return config;
234
- },
235
- );
195
+ CurrentXMLHttpRequest.prototype.setRequestHeader = function interceptedSetRequestHeader(
196
+ this: XMLHttpRequestLike,
197
+ header: string,
198
+ value: string,
199
+ ) {
200
+ const state = pendingXhrRequests.get(this);
201
+ if (state) {
202
+ state.headers[header] = value;
203
+ }
204
+ return originalXhrSetRequestHeader!.call(this, header, value);
205
+ };
236
206
 
237
- const responseInterceptorId = axiosInstance.interceptors.response.use(
238
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
239
- (response: any) => {
240
- const config = response.config;
241
- const pending = config ? pendingAxiosRequests.get(config) : undefined;
242
- const startTime = pending?.timestamp ?? Date.now();
207
+ CurrentXMLHttpRequest.prototype.send = function interceptedSend(
208
+ this: XMLHttpRequestLike,
209
+ body?: unknown,
210
+ ) {
211
+ const that = this;
212
+ const state = pendingXhrRequests.get(that) ?? {
213
+ method: 'GET',
214
+ url: '',
215
+ headers: {},
216
+ timestamp: Date.now(),
217
+ };
218
+ state.body = body;
219
+ state.timestamp = Date.now();
220
+ pendingXhrRequests.set(that, state);
221
+
222
+ const complete = () => {
223
+ const currentState = pendingXhrRequests.get(that);
224
+ if (!currentState || currentState.completed) {
225
+ return;
226
+ }
227
+ currentState.completed = true;
243
228
 
229
+ const headers = getXhrResponseHeaders(that);
244
230
  emit({
245
- timestamp: startTime,
246
- duration: Date.now() - startTime,
231
+ timestamp: currentState.timestamp,
232
+ duration: Date.now() - currentState.timestamp,
247
233
  request: {
248
- url: buildFullUrl(config),
249
- method: (config?.method ?? 'GET').toUpperCase(),
250
- headers: normalizeAxiosHeaders(config?.headers),
251
- body: config?.data,
234
+ url: currentState.url,
235
+ method: currentState.method,
236
+ headers: Object.keys(currentState.headers).length > 0
237
+ ? currentState.headers
238
+ : undefined,
239
+ body: currentState.body,
252
240
  },
253
241
  response: {
254
- status: response.status,
255
- statusText: response.statusText,
256
- headers: normalizeAxiosHeaders(response.headers),
257
- data: response.data,
258
- success: response.status >= 200 && response.status < 300,
242
+ status: that.status,
243
+ statusText: that.statusText,
244
+ headers,
245
+ data: normalizeXhrResponseBody(that),
246
+ success: that.status >= 200 && that.status < 300,
259
247
  },
248
+ error: currentState.error,
260
249
  });
250
+ };
261
251
 
262
- return response;
263
- },
264
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
- (error: any) => {
266
- const config = error.config;
267
- const pending = config ? pendingAxiosRequests.get(config) : undefined;
268
- const startTime = pending?.timestamp ?? Date.now();
269
-
270
- if (config) {
271
- emit({
272
- timestamp: startTime,
273
- duration: Date.now() - startTime,
274
- request: {
275
- url: buildFullUrl(config),
276
- method: (config.method ?? 'GET').toUpperCase(),
277
- headers: normalizeAxiosHeaders(config.headers),
278
- body: config.data,
279
- },
280
- response: error.response
281
- ? {
282
- status: error.response.status,
283
- statusText: error.response.statusText,
284
- headers: normalizeAxiosHeaders(error.response.headers),
285
- data: error.response.data,
286
- success: false,
287
- }
288
- : undefined,
289
- error: error.message ?? String(error),
290
- });
252
+ const markError = (message: string) => {
253
+ const currentState = pendingXhrRequests.get(that);
254
+ if (currentState) {
255
+ currentState.error = message;
291
256
  }
257
+ };
292
258
 
293
- return Promise.reject(error);
294
- },
295
- );
259
+ that.addEventListener('error', () => {
260
+ markError('Network Error');
261
+ });
262
+ that.addEventListener('timeout', () => {
263
+ markError('Timeout');
264
+ });
265
+ that.addEventListener('abort', () => {
266
+ markError('Aborted');
267
+ });
268
+ that.addEventListener('loadend', complete);
269
+
270
+ return originalXhrSend!.call(that, body);
271
+ };
296
272
 
297
273
  return () => {
298
- axiosInstance.interceptors.request.eject(requestInterceptorId);
299
- axiosInstance.interceptors.response.eject(responseInterceptorId);
274
+ stopXMLHttpRequest();
300
275
  };
301
276
  }
302
277
 
303
278
  // ─── Cleanup ───────────────────────────────────────────
304
279
 
305
280
  export function resetInterceptors(): void {
306
- if (originalFetch) {
307
- globalThis.fetch = originalFetch;
308
- originalFetch = null;
281
+ if (originalXMLHttpRequest) {
282
+ xhrRefCount = 1;
283
+ stopXMLHttpRequest();
309
284
  }
310
- fetchRefCount = 0;
285
+ xhrRefCount = 0;
311
286
  }
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ export type { InitializeOptions, FeatureConfigs } from './core/initialize';
8
8
 
9
9
  // Feature factories
10
10
  export { createNetworkFeature } from './features/network';
11
- export type { NetworkFeatureConfig, AxiosInstanceLike } from './features/network';
11
+ export type { NetworkFeatureConfig } from './features/network';
12
12
  export { createConsoleLogFeature } from './features/console';
13
13
  export type { ConsoleFeatureConfig } from './features/console';
14
14
  export { createZustandLogFeature, zustandLogMiddleware, addZustandLog } from './features/zustand';