react-native-debug-toolkit 3.2.1 → 3.2.2

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 (66) hide show
  1. package/README.md +13 -2
  2. package/README.zh-CN.md +13 -2
  3. package/android/build.gradle +34 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitDevConnectModule.java +70 -0
  6. package/android/src/main/java/com/reactnativedebugtoolkit/ReactNativeDebugToolkitPackage.java +25 -0
  7. package/ios/DebugToolkitDevConnect.mm +67 -0
  8. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +18 -7
  9. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -1
  10. package/lib/commonjs/features/devConnect/DevConnectTab.js +232 -161
  11. package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
  12. package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -5
  13. package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -1
  14. package/lib/commonjs/features/devConnect/devConnectUtils.js +99 -15
  15. package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -1
  16. package/lib/commonjs/features/devConnect/index.js +39 -2
  17. package/lib/commonjs/features/devConnect/index.js.map +1 -1
  18. package/lib/commonjs/features/devConnect/nativeDevConnect.js +110 -0
  19. package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -0
  20. package/lib/commonjs/features/devConnect/platformDetect.js +7 -11
  21. package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -1
  22. package/lib/commonjs/utils/debugPreferences.js +43 -6
  23. package/lib/commonjs/utils/debugPreferences.js.map +1 -1
  24. package/lib/module/features/devConnect/DevConnectQrScanner.js +18 -7
  25. package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -1
  26. package/lib/module/features/devConnect/DevConnectTab.js +235 -164
  27. package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
  28. package/lib/module/features/devConnect/devConnectPreferences.js +33 -6
  29. package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -1
  30. package/lib/module/features/devConnect/devConnectUtils.js +94 -15
  31. package/lib/module/features/devConnect/devConnectUtils.js.map +1 -1
  32. package/lib/module/features/devConnect/index.js +11 -3
  33. package/lib/module/features/devConnect/index.js.map +1 -1
  34. package/lib/module/features/devConnect/nativeDevConnect.js +104 -0
  35. package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -0
  36. package/lib/module/features/devConnect/platformDetect.js +8 -12
  37. package/lib/module/features/devConnect/platformDetect.js.map +1 -1
  38. package/lib/module/utils/debugPreferences.js +43 -6
  39. package/lib/module/utils/debugPreferences.js.map +1 -1
  40. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +3 -2
  41. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -1
  42. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
  43. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +6 -0
  44. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -1
  45. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +18 -1
  46. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -1
  47. package/lib/typescript/src/features/devConnect/index.d.ts +2 -2
  48. package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -1
  49. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +17 -0
  50. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -0
  51. package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -1
  52. package/lib/typescript/src/features/devConnect/types.d.ts +3 -0
  53. package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -1
  54. package/lib/typescript/src/utils/debugPreferences.d.ts +2 -0
  55. package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
  56. package/package.json +4 -1
  57. package/react-native-debug-toolkit.podspec +18 -0
  58. package/src/features/devConnect/DevConnectQrScanner.tsx +20 -9
  59. package/src/features/devConnect/DevConnectTab.tsx +227 -105
  60. package/src/features/devConnect/devConnectPreferences.ts +50 -5
  61. package/src/features/devConnect/devConnectUtils.ts +122 -15
  62. package/src/features/devConnect/index.ts +13 -0
  63. package/src/features/devConnect/nativeDevConnect.ts +128 -0
  64. package/src/features/devConnect/platformDetect.ts +8 -13
  65. package/src/features/devConnect/types.ts +3 -0
  66. package/src/utils/debugPreferences.ts +49 -4
@@ -1,21 +1,39 @@
1
- const METRO_PORT = '8081';
1
+ export const DEFAULT_METRO_PORT = '8081';
2
+ export const DEFAULT_DAEMON_PORT = '3799';
2
3
 
3
4
  export interface MetroUrls {
4
5
  expUrl: string;
5
6
  httpUrl: string;
6
7
  }
7
8
 
9
+ export interface MetroTarget {
10
+ host: string;
11
+ port: string;
12
+ hostPort: string;
13
+ statusUrl: string;
14
+ }
15
+
16
+ export interface ParsedComputerTarget {
17
+ computerHost: string;
18
+ metroPort: string;
19
+ }
20
+
8
21
  export interface ParsedMetroQrPayload {
9
22
  computerHost: string;
23
+ metroPort: string;
10
24
  source: string;
11
25
  }
12
26
 
13
27
  function isValidIpv4(host: string): boolean {
14
28
  const parts = host.split('.');
15
- if (parts.length !== 4) return false;
29
+ if (parts.length !== 4) {
30
+ return false;
31
+ }
16
32
 
17
33
  return parts.every((part) => {
18
- if (!/^\d{1,3}$/.test(part)) return false;
34
+ if (!/^\d{1,3}$/.test(part)) {
35
+ return false;
36
+ }
19
37
  const value = Number(part);
20
38
  return value >= 0 && value <= 255 && String(value) === part;
21
39
  });
@@ -29,31 +47,120 @@ function toUrlInput(raw: string): string {
29
47
  return `http://${trimmed}`;
30
48
  }
31
49
 
32
- export function normalizeComputerHost(raw: string): string | null {
50
+ function parseHostAndPort(raw: string): { host: string; port: string } | null {
33
51
  const trimmed = raw.trim();
34
- if (!trimmed) return null;
52
+ if (!trimmed) {
53
+ return null;
54
+ }
35
55
 
36
56
  try {
37
57
  const parsed = new URL(toUrlInput(trimmed));
38
- const host = parsed.hostname.trim();
39
- return isValidIpv4(host) ? host : null;
58
+ return {
59
+ host: parsed.hostname.trim(),
60
+ port: parsed.port.trim(),
61
+ };
40
62
  } catch {
41
63
  return null;
42
64
  }
43
65
  }
44
66
 
45
- export function buildMetroUrls(rawHost: string): MetroUrls | null {
46
- const host = normalizeComputerHost(rawHost);
47
- if (!host) return null;
67
+ export function normalizeComputerHost(raw: string): string | null {
68
+ const parsed = parseHostAndPort(raw);
69
+ if (!parsed) {
70
+ return null;
71
+ }
72
+
73
+ return isValidIpv4(parsed.host) ? parsed.host : null;
74
+ }
75
+
76
+ export function normalizePort(raw: string): string | null {
77
+ const trimmed = raw.trim();
78
+ if (!/^\d+$/.test(trimmed)) {
79
+ return null;
80
+ }
81
+
82
+ const value = Number(trimmed);
83
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
84
+ return null;
85
+ }
86
+ return String(value);
87
+ }
88
+
89
+ export function parseComputerTarget(raw: string): ParsedComputerTarget | null {
90
+ const parsed = parseHostAndPort(raw);
91
+ if (!parsed || !isValidIpv4(parsed.host)) {
92
+ return null;
93
+ }
94
+
95
+ const metroPort = parsed.port
96
+ ? normalizePort(parsed.port)
97
+ : DEFAULT_METRO_PORT;
98
+ if (!metroPort) {
99
+ return null;
100
+ }
101
+
102
+ return {
103
+ computerHost: parsed.host,
104
+ metroPort,
105
+ };
106
+ }
107
+
108
+ function normalizeMetroHost(rawHost: string): string | null {
109
+ const parsed = parseHostAndPort(rawHost);
110
+ if (!parsed) {
111
+ return null;
112
+ }
113
+ if (parsed.host === 'localhost') {
114
+ return parsed.host;
115
+ }
116
+ return isValidIpv4(parsed.host) ? parsed.host : null;
117
+ }
118
+
119
+ export function buildMetroTarget(rawHost: string, rawPort = DEFAULT_METRO_PORT): MetroTarget | null {
120
+ const host = normalizeMetroHost(rawHost);
121
+ if (!host) {
122
+ return null;
123
+ }
124
+
125
+ const port = normalizePort(rawPort);
126
+ if (!port) {
127
+ return null;
128
+ }
129
+
130
+ return {
131
+ host,
132
+ port,
133
+ hostPort: `${host}:${port}`,
134
+ statusUrl: `http://${host}:${port}/status`,
135
+ };
136
+ }
137
+
138
+ export function buildMetroUrls(rawHost: string, rawPort = DEFAULT_METRO_PORT): MetroUrls | null {
139
+ const target = buildMetroTarget(rawHost, rawPort);
140
+ if (!target) {
141
+ return null;
142
+ }
48
143
 
49
144
  return {
50
- expUrl: `exp://${host}:${METRO_PORT}`,
51
- httpUrl: `http://${host}:${METRO_PORT}`,
145
+ expUrl: `exp://${target.hostPort}`,
146
+ httpUrl: `http://${target.hostPort}`,
52
147
  };
53
148
  }
54
149
 
55
150
  export function parseMetroQrPayload(payload: string): ParsedMetroQrPayload | null {
56
- const computerHost = normalizeComputerHost(payload);
57
- if (!computerHost) return null;
58
- return { computerHost, source: payload };
151
+ const target = parseComputerTarget(payload);
152
+ if (!target) {
153
+ return null;
154
+ }
155
+ return { ...target, source: payload };
156
+ }
157
+
158
+ export function buildDaemonDeviceHost(computerHost: string, daemonPort: string): string {
159
+ const host = normalizeComputerHost(computerHost);
160
+ if (!host) {
161
+ return '';
162
+ }
163
+
164
+ const port = normalizePort(daemonPort) ?? DEFAULT_DAEMON_PORT;
165
+ return port === DEFAULT_DAEMON_PORT ? host : `${host}:${port}`;
59
166
  }
@@ -1,6 +1,8 @@
1
1
  import { DevConnectTab } from './DevConnectTab';
2
2
  import { isCameraKitAvailable } from './cameraKit';
3
3
  import { loadDevConnectPreferences } from './devConnectPreferences';
4
+ import { DEFAULT_DAEMON_PORT, DEFAULT_METRO_PORT } from './devConnectUtils';
5
+ import { isNativeDevConnectAvailable } from './nativeDevConnect';
4
6
  import { isSimulator } from './platformDetect';
5
7
  import { daemonClient } from '../../utils/DaemonClient';
6
8
  import type { DebugFeature, DebugFeatureListener } from '../../types';
@@ -10,12 +12,17 @@ export type { DevConnectState } from './types';
10
12
  export {
11
13
  buildMetroUrls,
12
14
  normalizeComputerHost,
15
+ normalizePort,
16
+ parseComputerTarget,
13
17
  parseMetroQrPayload,
14
18
  } from './devConnectUtils';
15
19
  export {
16
20
  loadDevConnectPreferences,
17
21
  restoreDevConnectSettingsToDaemon,
18
22
  saveComputerHost,
23
+ saveComputerTarget,
24
+ saveDaemonPort,
25
+ saveMetroPort,
19
26
  } from './devConnectPreferences';
20
27
 
21
28
  export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
@@ -23,7 +30,10 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
23
30
  let state: DevConnectState = {
24
31
  isSimulator: isSimulator(),
25
32
  computerHost: '',
33
+ metroPort: DEFAULT_METRO_PORT,
34
+ daemonPort: DEFAULT_DAEMON_PORT,
26
35
  qrAvailable: isCameraKitAvailable(),
36
+ nativeMetroAvailable: isNativeDevConnectAvailable(),
27
37
  streaming: daemonClient.isConnected(),
28
38
  };
29
39
 
@@ -44,6 +54,9 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
44
54
  state = {
45
55
  ...state,
46
56
  computerHost: preferences.computerHost,
57
+ metroPort: preferences.metroPort,
58
+ daemonPort: preferences.daemonPort,
59
+ nativeMetroAvailable: isNativeDevConnectAvailable(),
47
60
  };
48
61
  notify();
49
62
  }).catch(() => {
@@ -0,0 +1,128 @@
1
+ import { DevSettings, NativeModules } from 'react-native';
2
+
3
+ import { buildMetroTarget } from './devConnectUtils';
4
+
5
+ interface DebugToolkitDevConnectNativeModule {
6
+ applyMetroHost: (hostPort: string) => Promise<{ hostPort?: string } | void>;
7
+ resetMetroHost: () => Promise<void>;
8
+ getMetroHost?: () => Promise<string | null>;
9
+ }
10
+
11
+ type MetroBundleFailureReason =
12
+ | 'invalid_target'
13
+ | 'native_unavailable'
14
+ | 'fetch_unavailable'
15
+ | 'metro_unreachable'
16
+ | 'native_error';
17
+
18
+ export type MetroBundleResult =
19
+ | { ok: true; hostPort: string }
20
+ | { ok: false; reason: MetroBundleFailureReason; error?: string; statusUrl?: string };
21
+
22
+ type FetchLike = (
23
+ url: string,
24
+ init: { method: string },
25
+ ) => Promise<{
26
+ ok?: boolean;
27
+ text?: () => Promise<string>;
28
+ }>;
29
+
30
+ function getNativeModule(): DebugToolkitDevConnectNativeModule | null {
31
+ const nativeModule = NativeModules.DebugToolkitDevConnect as Partial<DebugToolkitDevConnectNativeModule> | undefined;
32
+ if (
33
+ nativeModule &&
34
+ typeof nativeModule.applyMetroHost === 'function' &&
35
+ typeof nativeModule.resetMetroHost === 'function'
36
+ ) {
37
+ return nativeModule as DebugToolkitDevConnectNativeModule;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ export function isNativeDevConnectAvailable(): boolean {
43
+ return getNativeModule() !== null;
44
+ }
45
+
46
+ async function checkMetroStatus(statusUrl: string): Promise<MetroBundleResult | null> {
47
+ const fetchImpl = globalThis.fetch as FetchLike | undefined;
48
+ if (!fetchImpl) {
49
+ return {
50
+ ok: false,
51
+ reason: 'fetch_unavailable',
52
+ statusUrl,
53
+ error: 'global fetch is not available',
54
+ };
55
+ }
56
+
57
+ try {
58
+ const response = await fetchImpl(statusUrl, { method: 'GET' });
59
+ const body = typeof response.text === 'function' ? await response.text() : '';
60
+ if (response.ok === false || !body.includes('packager-status:running')) {
61
+ return {
62
+ ok: false,
63
+ reason: 'metro_unreachable',
64
+ statusUrl,
65
+ error: body || 'Metro status endpoint did not report running',
66
+ };
67
+ }
68
+ return null;
69
+ } catch (error) {
70
+ return {
71
+ ok: false,
72
+ reason: 'metro_unreachable',
73
+ statusUrl,
74
+ error: error instanceof Error ? error.message : String(error),
75
+ };
76
+ }
77
+ }
78
+
79
+ export async function applyMetroBundle(host: string, port: string): Promise<MetroBundleResult> {
80
+ const target = buildMetroTarget(host, port);
81
+ if (!target) {
82
+ return { ok: false, reason: 'invalid_target' };
83
+ }
84
+
85
+ const nativeModule = getNativeModule();
86
+ if (!nativeModule) {
87
+ return { ok: false, reason: 'native_unavailable' };
88
+ }
89
+
90
+ const statusError = await checkMetroStatus(target.statusUrl);
91
+ if (statusError) {
92
+ return statusError;
93
+ }
94
+
95
+ try {
96
+ const result = await nativeModule.applyMetroHost(target.hostPort);
97
+ DevSettings.reload?.('DebugToolkit DevConnect Metro host changed');
98
+ return {
99
+ ok: true,
100
+ hostPort: result && typeof result.hostPort === 'string' ? result.hostPort : target.hostPort,
101
+ };
102
+ } catch (error) {
103
+ return {
104
+ ok: false,
105
+ reason: 'native_error',
106
+ error: error instanceof Error ? error.message : String(error),
107
+ };
108
+ }
109
+ }
110
+
111
+ export async function resetMetroBundle(): Promise<MetroBundleResult | { ok: true }> {
112
+ const nativeModule = getNativeModule();
113
+ if (!nativeModule) {
114
+ return { ok: false, reason: 'native_unavailable' };
115
+ }
116
+
117
+ try {
118
+ await nativeModule.resetMetroHost();
119
+ DevSettings.reload?.('DebugToolkit DevConnect Metro host reset');
120
+ return { ok: true };
121
+ } catch (error) {
122
+ return {
123
+ ok: false,
124
+ reason: 'native_error',
125
+ error: error instanceof Error ? error.message : String(error),
126
+ };
127
+ }
128
+ }
@@ -1,25 +1,20 @@
1
- import { NativeModules, Platform } from 'react-native';
1
+ import { Platform } from 'react-native';
2
2
 
3
3
  export function isSimulator(): boolean {
4
4
  const { OS } = Platform;
5
+ const constants = (Platform.constants ?? {}) as Record<string, unknown>;
5
6
 
6
7
  if (OS === 'android') {
7
- const constants = Platform.constants as Record<string, unknown>;
8
- if (constants.isEmulator === true) return true;
9
- const model = String(constants.Model ?? '').toLowerCase();
8
+ if (constants.isEmulator === true) {
9
+ return true;
10
+ }
11
+ const model = String(constants.Model ?? constants.model ?? '').toLowerCase();
10
12
  return model.includes('sdk') || model.includes('emulator') || model.includes('google_sdk');
11
13
  }
12
14
 
13
15
  if (OS === 'ios') {
14
- // NativeModules.KCKFCSupportManager is absent on simulator,
15
- // but the most reliable check is the DeviceInfo model name.
16
- const deviceInfo = NativeModules.DeviceInfo
17
- ?? NativeModules.PlatformConstants;
18
- if (deviceInfo) {
19
- const model = String(deviceInfo.model ?? '').toLowerCase();
20
- if (model.includes('simulator')) return true;
21
- }
22
- return false;
16
+ const model = String(constants.model ?? '').toLowerCase();
17
+ return model.includes('simulator');
23
18
  }
24
19
 
25
20
  return false;
@@ -1,6 +1,9 @@
1
1
  export interface DevConnectState {
2
2
  isSimulator: boolean;
3
3
  computerHost: string;
4
+ metroPort: string;
5
+ daemonPort: string;
4
6
  qrAvailable: boolean;
7
+ nativeMetroAvailable: boolean;
5
8
  streaming: boolean;
6
9
  }
@@ -1,13 +1,31 @@
1
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
1
  type AsyncStorageLike = { getItem: (k: string) => Promise<string | null>; setItem: (k: string, v: string) => Promise<void> };
2
+ type NativePreferencesLike = { getPreference: (k: string) => Promise<string | null>; setPreference: (k: string, v: string) => Promise<void> };
3
3
 
4
4
  const memoryStore = new Map<string, string>();
5
5
 
6
6
  function loadAsyncStorage(): AsyncStorageLike | null {
7
7
  try {
8
- // eslint-disable-next-line @typescript-eslint/no-require-imports
9
8
  const mod = require('@react-native-async-storage/async-storage');
10
- if (mod && typeof mod.getItem === 'function') return mod;
9
+ if (mod && typeof mod.getItem === 'function') {
10
+ return mod;
11
+ }
12
+ return null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function loadNativePreferences(): NativePreferencesLike | null {
19
+ try {
20
+ const { NativeModules } = require('react-native') as { NativeModules?: { DebugToolkitDevConnect?: Partial<NativePreferencesLike> } };
21
+ const mod = NativeModules?.DebugToolkitDevConnect;
22
+ if (
23
+ mod &&
24
+ typeof mod.getPreference === 'function' &&
25
+ typeof mod.setPreference === 'function'
26
+ ) {
27
+ return mod as NativePreferencesLike;
28
+ }
11
29
  return null;
12
30
  } catch {
13
31
  return null;
@@ -20,6 +38,16 @@ export async function setPreference(key: string, value: string): Promise<void> {
20
38
  if (AsyncStorage) {
21
39
  try {
22
40
  await AsyncStorage.setItem(key, value);
41
+ return;
42
+ } catch {
43
+ // degrade to memory only
44
+ }
45
+ }
46
+
47
+ const nativePreferences = loadNativePreferences();
48
+ if (nativePreferences) {
49
+ try {
50
+ await nativePreferences.setPreference(key, value);
23
51
  } catch {
24
52
  // degrade to memory only
25
53
  }
@@ -31,11 +59,26 @@ export async function getPreference(key: string): Promise<string | null> {
31
59
  if (AsyncStorage) {
32
60
  try {
33
61
  const val = await AsyncStorage.getItem(key);
34
- if (val !== null) return val;
62
+ if (val !== null) {
63
+ return val;
64
+ }
35
65
  } catch {
36
66
  // fall through to memory
37
67
  }
38
68
  }
69
+
70
+ const nativePreferences = loadNativePreferences();
71
+ if (nativePreferences) {
72
+ try {
73
+ const val = await nativePreferences.getPreference(key);
74
+ if (val !== null) {
75
+ return val;
76
+ }
77
+ } catch {
78
+ // fall through to memory
79
+ }
80
+ }
81
+
39
82
  return memoryStore.get(key) ?? null;
40
83
  }
41
84
 
@@ -46,4 +89,6 @@ export const KEYS = {
46
89
  networkLogs: '@react_native_debug_toolkit/network_logs',
47
90
  trackLogs: '@react_native_debug_toolkit/track_logs',
48
91
  computerHost: '@react_native_debug_toolkit/computer_host',
92
+ metroPort: '@react_native_debug_toolkit/metro_port',
93
+ daemonPort: '@react_native_debug_toolkit/daemon_port',
49
94
  } as const;