react-native-debug-toolkit 3.3.4 → 3.3.8
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/README.md +48 -46
- package/README.zh-CN.md +48 -46
- package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitDevConnectModule.java +0 -187
- package/bin/debug-toolkit.js +0 -16
- package/ios/DebugToolkitDevConnect.h +0 -12
- package/ios/DebugToolkitDevConnect.mm +0 -321
- package/lib/commonjs/core/initialize.js +8 -1
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/devConnect/DevConnectTab.js +18 -470
- package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/commonjs/features/devConnect/devConnectPreferences.js +0 -12
- package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -1
- package/lib/commonjs/features/devConnect/devConnectUtils.js +2 -57
- package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -1
- package/lib/commonjs/features/devConnect/index.js +1 -23
- package/lib/commonjs/features/devConnect/index.js.map +1 -1
- package/lib/commonjs/features/devConnect/nativeDevConnect.js +1 -103
- package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/DebugView.js +2 -0
- package/lib/commonjs/ui/DebugView.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +22 -10
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/tabPersistence.js +17 -0
- package/lib/commonjs/ui/panel/tabPersistence.js.map +1 -0
- package/lib/commonjs/utils/createDebugTab.js +21 -0
- package/lib/commonjs/utils/createDebugTab.js.map +1 -0
- package/lib/commonjs/utils/debugPreferences.js +0 -1
- package/lib/commonjs/utils/debugPreferences.js.map +1 -1
- package/lib/module/core/initialize.js +8 -1
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectTab.js +21 -473
- package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/module/features/devConnect/devConnectPreferences.js +1 -12
- package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -1
- package/lib/module/features/devConnect/devConnectUtils.js +1 -53
- package/lib/module/features/devConnect/devConnectUtils.js.map +1 -1
- package/lib/module/features/devConnect/index.js +5 -9
- package/lib/module/features/devConnect/index.js.map +1 -1
- package/lib/module/features/devConnect/nativeDevConnect.js +1 -100
- package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/DebugView.js +2 -0
- package/lib/module/ui/DebugView.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +22 -10
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/tabPersistence.js +13 -0
- package/lib/module/ui/panel/tabPersistence.js.map +1 -0
- package/lib/module/utils/createDebugTab.js +17 -0
- package/lib/module/utils/createDebugTab.js.map +1 -0
- package/lib/module/utils/debugPreferences.js +0 -1
- package/lib/module/utils/debugPreferences.js.map +1 -1
- package/lib/typescript/src/core/initialize.d.ts +2 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +0 -2
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +0 -20
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/index.d.ts +2 -2
- package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +0 -25
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/types.d.ts +1 -3
- package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/DebugView.d.ts +4 -2
- package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/tabPersistence.d.ts +3 -0
- package/lib/typescript/src/ui/panel/tabPersistence.d.ts.map +1 -0
- package/lib/typescript/src/utils/createDebugTab.d.ts +18 -0
- package/lib/typescript/src/utils/createDebugTab.d.ts.map +1 -0
- package/lib/typescript/src/utils/debugPreferences.d.ts +0 -1
- package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
- package/package.json +2 -6
- package/src/core/initialize.ts +17 -1
- package/src/features/devConnect/DevConnectTab.tsx +17 -381
- package/src/features/devConnect/devConnectPreferences.ts +0 -15
- package/src/features/devConnect/devConnectUtils.ts +1 -81
- package/src/features/devConnect/index.ts +2 -9
- package/src/features/devConnect/nativeDevConnect.ts +1 -136
- package/src/features/devConnect/types.ts +1 -3
- package/src/index.ts +2 -0
- package/src/ui/DebugView.tsx +5 -1
- package/src/ui/panel/FloatPanelView.tsx +22 -10
- package/src/ui/panel/tabPersistence.ts +22 -0
- package/src/utils/createDebugTab.ts +32 -0
- package/src/utils/debugPreferences.ts +0 -1
- package/app.plugin.js +0 -51
- package/dev-client.js +0 -3
- package/scripts/bundle/android.js +0 -101
- package/scripts/bundle/cli.js +0 -57
- package/scripts/bundle/doctor.js +0 -38
- package/scripts/bundle/ios.js +0 -179
- package/scripts/bundle/setup.js +0 -39
- package/scripts/debug-bundle.gradle +0 -147
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DevConnectTab } from './DevConnectTab';
|
|
2
2
|
import { loadDevConnectPreferences } from './devConnectPreferences';
|
|
3
|
-
import { DEFAULT_DAEMON_PORT,
|
|
4
|
-
import { getDeviceLocalIp
|
|
3
|
+
import { DEFAULT_DAEMON_PORT, extractSubnetPrefix } from './devConnectUtils';
|
|
4
|
+
import { getDeviceLocalIp } from './nativeDevConnect';
|
|
5
5
|
import { isSimulator } from './platformDetect';
|
|
6
6
|
import { daemonClient } from '../../utils/DaemonClient';
|
|
7
7
|
import type { DebugFeature, DebugFeatureListener } from '../../types';
|
|
@@ -9,11 +9,9 @@ import type { DevConnectFeatureControls, DevConnectSettingsPatch, DevConnectStat
|
|
|
9
9
|
|
|
10
10
|
export type { DevConnectState } from './types';
|
|
11
11
|
export {
|
|
12
|
-
buildMetroUrls,
|
|
13
12
|
normalizeComputerHost,
|
|
14
13
|
normalizePort,
|
|
15
14
|
parseComputerTarget,
|
|
16
|
-
parseMetroQrPayload,
|
|
17
15
|
} from './devConnectUtils';
|
|
18
16
|
export {
|
|
19
17
|
loadDevConnectPreferences,
|
|
@@ -21,7 +19,6 @@ export {
|
|
|
21
19
|
saveComputerHost,
|
|
22
20
|
saveComputerTarget,
|
|
23
21
|
saveDaemonPort,
|
|
24
|
-
saveMetroPort,
|
|
25
22
|
} from './devConnectPreferences';
|
|
26
23
|
export { nativeIsDebugBuild } from './nativeDevConnect';
|
|
27
24
|
|
|
@@ -30,9 +27,7 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
|
|
|
30
27
|
let state: DevConnectState = {
|
|
31
28
|
isSimulator: isSimulator(),
|
|
32
29
|
computerHost: '',
|
|
33
|
-
metroPort: DEFAULT_METRO_PORT,
|
|
34
30
|
daemonPort: DEFAULT_DAEMON_PORT,
|
|
35
|
-
nativeMetroAvailable: isNativeDevConnectAvailable(),
|
|
36
31
|
streaming: daemonClient.isConnected(),
|
|
37
32
|
};
|
|
38
33
|
|
|
@@ -62,9 +57,7 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
|
|
|
62
57
|
state = {
|
|
63
58
|
...state,
|
|
64
59
|
computerHost: preferences.computerHost,
|
|
65
|
-
metroPort: preferences.metroPort,
|
|
66
60
|
daemonPort: preferences.daemonPort,
|
|
67
|
-
nativeMetroAvailable: isNativeDevConnectAvailable(),
|
|
68
61
|
};
|
|
69
62
|
|
|
70
63
|
if (!state.isSimulator) {
|
|
@@ -1,55 +1,14 @@
|
|
|
1
1
|
import { NativeModules } from 'react-native';
|
|
2
2
|
|
|
3
|
-
import { buildMetroTarget } from './devConnectUtils';
|
|
4
|
-
|
|
5
|
-
export interface NativeDiagnostics {
|
|
6
|
-
persistedMetroHost: string | null;
|
|
7
|
-
appDelegateClass: string;
|
|
8
|
-
// Metro host switching only works in Debug builds (RN strips the packager machinery in
|
|
9
|
-
// Release). False means the Remote JS Bundle controls should be disabled.
|
|
10
|
-
isDebugBuild: boolean;
|
|
11
|
-
hasEmbeddedBundle?: boolean;
|
|
12
|
-
embeddedFirstHookInstalled?: boolean;
|
|
13
|
-
packagerHookInstalled?: boolean;
|
|
14
|
-
bundleRootHookInstalled?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
3
|
interface DebugToolkitDevConnectNativeModule {
|
|
18
|
-
applyMetroHost: (hostPort: string) => Promise<{ hostPort?: string } | void>;
|
|
19
|
-
resetMetroHost: () => Promise<void>;
|
|
20
|
-
getMetroHost?: () => Promise<string | null>;
|
|
21
4
|
getLocalIp?: () => Promise<string | null>;
|
|
22
5
|
isDebugBuild?: () => Promise<boolean>;
|
|
23
6
|
getPreference?: (key: string) => Promise<string | null>;
|
|
24
|
-
getDiagnostics?: () => Promise<NativeDiagnostics>;
|
|
25
7
|
}
|
|
26
8
|
|
|
27
|
-
type MetroBundleFailureReason =
|
|
28
|
-
| 'invalid_target'
|
|
29
|
-
| 'native_unavailable'
|
|
30
|
-
| 'fetch_unavailable'
|
|
31
|
-
| 'metro_unreachable'
|
|
32
|
-
| 'native_error';
|
|
33
|
-
|
|
34
|
-
export type MetroBundleResult =
|
|
35
|
-
| { ok: true; hostPort: string }
|
|
36
|
-
| { ok: false; reason: MetroBundleFailureReason; error?: string; statusUrl?: string };
|
|
37
|
-
|
|
38
|
-
type FetchLike = (
|
|
39
|
-
url: string,
|
|
40
|
-
init: { method: string },
|
|
41
|
-
) => Promise<{
|
|
42
|
-
ok?: boolean;
|
|
43
|
-
text?: () => Promise<string>;
|
|
44
|
-
}>;
|
|
45
|
-
|
|
46
9
|
function getNativeModule(): DebugToolkitDevConnectNativeModule | null {
|
|
47
10
|
const nativeModule = NativeModules.DebugToolkitDevConnect as Partial<DebugToolkitDevConnectNativeModule> | undefined;
|
|
48
|
-
if (
|
|
49
|
-
nativeModule &&
|
|
50
|
-
typeof nativeModule.applyMetroHost === 'function' &&
|
|
51
|
-
typeof nativeModule.resetMetroHost === 'function'
|
|
52
|
-
) {
|
|
11
|
+
if (nativeModule && typeof nativeModule.isDebugBuild === 'function') {
|
|
53
12
|
return nativeModule as DebugToolkitDevConnectNativeModule;
|
|
54
13
|
}
|
|
55
14
|
return null;
|
|
@@ -59,88 +18,6 @@ export function isNativeDevConnectAvailable(): boolean {
|
|
|
59
18
|
return getNativeModule() !== null;
|
|
60
19
|
}
|
|
61
20
|
|
|
62
|
-
async function checkMetroStatus(statusUrl: string): Promise<MetroBundleResult | null> {
|
|
63
|
-
const fetchImpl = globalThis.fetch as FetchLike | undefined;
|
|
64
|
-
if (!fetchImpl) {
|
|
65
|
-
return {
|
|
66
|
-
ok: false,
|
|
67
|
-
reason: 'fetch_unavailable',
|
|
68
|
-
statusUrl,
|
|
69
|
-
error: 'global fetch is not available',
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const response = await fetchImpl(statusUrl, { method: 'GET' });
|
|
75
|
-
const body = typeof response.text === 'function' ? await response.text() : '';
|
|
76
|
-
if (response.ok === false || !body.includes('packager-status:running')) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
reason: 'metro_unreachable',
|
|
80
|
-
statusUrl,
|
|
81
|
-
error: body || 'Metro status endpoint did not report running',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
return {
|
|
87
|
-
ok: false,
|
|
88
|
-
reason: 'metro_unreachable',
|
|
89
|
-
statusUrl,
|
|
90
|
-
error: error instanceof Error ? error.message : String(error),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function applyMetroBundle(host: string, port: string): Promise<MetroBundleResult> {
|
|
96
|
-
const target = buildMetroTarget(host, port);
|
|
97
|
-
if (!target) {
|
|
98
|
-
return { ok: false, reason: 'invalid_target' };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const nativeModule = getNativeModule();
|
|
102
|
-
if (!nativeModule) {
|
|
103
|
-
return { ok: false, reason: 'native_unavailable' };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const statusError = await checkMetroStatus(target.statusUrl);
|
|
107
|
-
if (statusError) {
|
|
108
|
-
return statusError;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const result = await nativeModule.applyMetroHost(target.hostPort);
|
|
113
|
-
return {
|
|
114
|
-
ok: true,
|
|
115
|
-
hostPort: result && typeof result.hostPort === 'string' ? result.hostPort : target.hostPort,
|
|
116
|
-
};
|
|
117
|
-
} catch (error) {
|
|
118
|
-
return {
|
|
119
|
-
ok: false,
|
|
120
|
-
reason: 'native_error',
|
|
121
|
-
error: error instanceof Error ? error.message : String(error),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function resetMetroBundle(): Promise<MetroBundleResult | { ok: true }> {
|
|
127
|
-
const nativeModule = getNativeModule();
|
|
128
|
-
if (!nativeModule) {
|
|
129
|
-
return { ok: false, reason: 'native_unavailable' };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
await nativeModule.resetMetroHost();
|
|
134
|
-
return { ok: true };
|
|
135
|
-
} catch (error) {
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
reason: 'native_error',
|
|
139
|
-
error: error instanceof Error ? error.message : String(error),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
21
|
export async function getDeviceLocalIp(): Promise<string | null> {
|
|
145
22
|
const nativeModule = getNativeModule();
|
|
146
23
|
if (!nativeModule?.getLocalIp) {
|
|
@@ -166,15 +43,3 @@ export async function nativeIsDebugBuild(): Promise<boolean | null> {
|
|
|
166
43
|
return null;
|
|
167
44
|
}
|
|
168
45
|
}
|
|
169
|
-
|
|
170
|
-
export async function getNativeDiagnostics(): Promise<NativeDiagnostics | null> {
|
|
171
|
-
const nativeModule = getNativeModule();
|
|
172
|
-
if (!nativeModule?.getDiagnostics) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
176
|
-
return await nativeModule.getDiagnostics();
|
|
177
|
-
} catch {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
export interface DevConnectState {
|
|
2
2
|
isSimulator: boolean;
|
|
3
3
|
computerHost: string;
|
|
4
|
-
metroPort: string;
|
|
5
4
|
daemonPort: string;
|
|
6
5
|
subnetPrefix?: string;
|
|
7
|
-
nativeMetroAvailable: boolean;
|
|
8
6
|
streaming: boolean;
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
export type DevConnectSettingsPatch = Partial<
|
|
12
|
-
Pick<DevConnectState, 'computerHost' | '
|
|
10
|
+
Pick<DevConnectState, 'computerHost' | 'daemonPort'>
|
|
13
11
|
>;
|
|
14
12
|
|
|
15
13
|
export interface DevConnectFeatureControls {
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { useNavigationLogger } from './features/navigation/useNavigationLogger';
|
|
|
28
28
|
|
|
29
29
|
// Utilities
|
|
30
30
|
export { safeStringify } from './utils/safeStringify';
|
|
31
|
+
export { createDebugTab } from './utils/createDebugTab';
|
|
32
|
+
export type { CreateDebugTabOptions } from './utils/createDebugTab';
|
|
31
33
|
export { copyToComputer, logToComputer, fmt } from './utils/copyToComputer';
|
|
32
34
|
export type { CopyResult, CopyOptions, CopyMethod } from './utils/copyToComputer';
|
|
33
35
|
export { createDebugDeviceReport } from './utils/deviceReport';
|
package/src/ui/DebugView.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { DebugToolkitProvider } from '../core/DebugToolkitProvider';
|
|
|
3
3
|
import { initializeDebugToolkit } from '../core/initialize';
|
|
4
4
|
import type { FeatureConfigs } from '../core/initialize';
|
|
5
5
|
import { useNavigationLogger } from '../features/navigation/useNavigationLogger';
|
|
6
|
-
import type { EnvironmentConfig, NavigationContainerRef } from '../types';
|
|
6
|
+
import type { AnyDebugFeature, EnvironmentConfig, NavigationContainerRef } from '../types';
|
|
7
7
|
|
|
8
8
|
// --- Types ---
|
|
9
9
|
|
|
@@ -14,6 +14,8 @@ export interface DebugViewProps {
|
|
|
14
14
|
* Set to `false` to disable a feature.
|
|
15
15
|
*/
|
|
16
16
|
features?: Partial<FeatureConfigs>;
|
|
17
|
+
/** Custom tabs/features appended after built-in features. */
|
|
18
|
+
customFeatures?: AnyDebugFeature[];
|
|
17
19
|
/** Navigation container ref for route tracking. */
|
|
18
20
|
navigationRef?: React.RefObject<NavigationContainerRef | null>;
|
|
19
21
|
/** Environment configs for runtime host switching. */
|
|
@@ -38,6 +40,7 @@ function NavigationLoggerInner({
|
|
|
38
40
|
export function DebugView({
|
|
39
41
|
children,
|
|
40
42
|
features,
|
|
43
|
+
customFeatures,
|
|
41
44
|
navigationRef,
|
|
42
45
|
environments,
|
|
43
46
|
enabled,
|
|
@@ -66,6 +69,7 @@ export function DebugView({
|
|
|
66
69
|
let cancelled = false;
|
|
67
70
|
initializeDebugToolkit({
|
|
68
71
|
features: resolvedFeatures,
|
|
72
|
+
customFeatures,
|
|
69
73
|
enabled,
|
|
70
74
|
}).then((toolkit) => {
|
|
71
75
|
if (!cancelled) {
|
|
@@ -11,6 +11,7 @@ import { FloatIcon } from '../floating/FloatIcon';
|
|
|
11
11
|
import { DebugPanel } from './DebugPanel';
|
|
12
12
|
import { FeatureTabBar } from './FeatureTabBar';
|
|
13
13
|
import type { TabItem } from './FeatureTabBar';
|
|
14
|
+
import { resolveStoredTabIndex } from './tabPersistence';
|
|
14
15
|
import { useTabAnimation } from './useTabAnimation';
|
|
15
16
|
|
|
16
17
|
// ─── Error Boundary ────────────────────────────────────
|
|
@@ -53,25 +54,32 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
|
|
|
53
54
|
|
|
54
55
|
// Restore last tab on mount
|
|
55
56
|
useEffect(() => {
|
|
57
|
+
if (tabLoaded.current || features.length === 0) return;
|
|
56
58
|
let mounted = true;
|
|
57
59
|
getPreference(KEYS.lastTab).then((val) => {
|
|
58
|
-
if (!mounted ||
|
|
59
|
-
const idx =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
if (!mounted || tabLoaded.current) return;
|
|
61
|
+
const idx = resolveStoredTabIndex(features, val);
|
|
62
|
+
setActiveTab(idx);
|
|
63
|
+
const featureName = features[idx]?.name;
|
|
64
|
+
if (featureName && featureName !== val) {
|
|
65
|
+
setPreference(KEYS.lastTab, featureName);
|
|
63
66
|
}
|
|
67
|
+
tabLoaded.current = true;
|
|
64
68
|
});
|
|
65
69
|
return () => { mounted = false; };
|
|
66
|
-
}, []);
|
|
70
|
+
}, [features]);
|
|
67
71
|
|
|
68
72
|
const { contentOpacity, contentTranslateX, panHandlers, switchTab } = useTabAnimation({
|
|
69
73
|
activeTab,
|
|
70
74
|
tabCount: features.length,
|
|
71
75
|
onTabChange: useCallback((index: number) => {
|
|
76
|
+
tabLoaded.current = true;
|
|
72
77
|
setActiveTab(index);
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
const featureName = features[index]?.name;
|
|
79
|
+
if (featureName) {
|
|
80
|
+
setPreference(KEYS.lastTab, featureName);
|
|
81
|
+
}
|
|
82
|
+
}, [features]),
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
// Feature subscription → re-render on data changes
|
|
@@ -96,10 +104,14 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
|
|
|
96
104
|
// Clamp activeTab if features shrink
|
|
97
105
|
useEffect(() => {
|
|
98
106
|
if (features.length > 0 && activeTab >= features.length) {
|
|
107
|
+
tabLoaded.current = true;
|
|
99
108
|
setActiveTab(0);
|
|
100
|
-
|
|
109
|
+
const featureName = features[0]?.name;
|
|
110
|
+
if (featureName) {
|
|
111
|
+
setPreference(KEYS.lastTab, featureName);
|
|
112
|
+
}
|
|
101
113
|
}
|
|
102
|
-
}, [features
|
|
114
|
+
}, [features, activeTab]);
|
|
103
115
|
|
|
104
116
|
// Badge (first feature that returns one)
|
|
105
117
|
const envBadge = features.map((f) => f.badge?.()).find((b) => b != null) ?? null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnyDebugFeature } from '../../types';
|
|
2
|
+
|
|
3
|
+
export function resolveStoredTabIndex(
|
|
4
|
+
features: AnyDebugFeature[],
|
|
5
|
+
storedTab: string | null,
|
|
6
|
+
): number {
|
|
7
|
+
if (!storedTab) return 0;
|
|
8
|
+
|
|
9
|
+
const nameIndex = features.findIndex((feature) => feature.name === storedTab);
|
|
10
|
+
if (nameIndex >= 0) return nameIndex;
|
|
11
|
+
|
|
12
|
+
const legacyIndex = Number(storedTab);
|
|
13
|
+
if (
|
|
14
|
+
Number.isInteger(legacyIndex) &&
|
|
15
|
+
legacyIndex >= 0 &&
|
|
16
|
+
legacyIndex < features.length
|
|
17
|
+
) {
|
|
18
|
+
return legacyIndex;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { DebugFeature, DebugFeatureListener, DebugFeatureRenderProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface CreateDebugTabOptions<TSnapshot> {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
getSnapshot: () => TSnapshot;
|
|
8
|
+
render?: ComponentType<DebugFeatureRenderProps<TSnapshot>>;
|
|
9
|
+
setup?: () => void;
|
|
10
|
+
cleanup?: () => void;
|
|
11
|
+
clear?: () => void;
|
|
12
|
+
subscribe?: (listener: DebugFeatureListener) => () => void;
|
|
13
|
+
badge?: () => { label: string; color: string } | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const noop = () => {};
|
|
17
|
+
|
|
18
|
+
export function createDebugTab<TSnapshot>(
|
|
19
|
+
options: CreateDebugTabOptions<TSnapshot>,
|
|
20
|
+
): DebugFeature<TSnapshot> {
|
|
21
|
+
return {
|
|
22
|
+
name: options.name,
|
|
23
|
+
label: options.label,
|
|
24
|
+
setup: options.setup ?? noop,
|
|
25
|
+
cleanup: options.cleanup ?? noop,
|
|
26
|
+
getSnapshot: options.getSnapshot,
|
|
27
|
+
clear: options.clear,
|
|
28
|
+
subscribe: options.subscribe,
|
|
29
|
+
renderContent: options.render,
|
|
30
|
+
badge: options.badge,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -89,6 +89,5 @@ export const KEYS = {
|
|
|
89
89
|
networkLogs: '@react_native_debug_toolkit/network_logs',
|
|
90
90
|
trackLogs: '@react_native_debug_toolkit/track_logs',
|
|
91
91
|
computerHost: '@react_native_debug_toolkit/computer_host',
|
|
92
|
-
metroPort: '@react_native_debug_toolkit/metro_port',
|
|
93
92
|
daemonPort: '@react_native_debug_toolkit/daemon_port',
|
|
94
93
|
} as const;
|
package/app.plugin.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function markTestConfig(config) {
|
|
4
|
-
config.extra = config.extra || {};
|
|
5
|
-
config.extra.reactNativeDebugToolkit = {
|
|
6
|
-
...(config.extra.reactNativeDebugToolkit || {}),
|
|
7
|
-
embedBundle: true,
|
|
8
|
-
};
|
|
9
|
-
return config;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function loadConfigPlugins() {
|
|
13
|
-
try {
|
|
14
|
-
return require('@expo/config-plugins');
|
|
15
|
-
} catch {
|
|
16
|
-
throw new Error(
|
|
17
|
-
'react-native-debug-toolkit/dev-client requires @expo/config-plugins during Expo prebuild.',
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function withDebugToolkitDevClient(config, props = {}) {
|
|
23
|
-
if (!props.embedBundle) {
|
|
24
|
-
return config;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (props._testOnly) {
|
|
28
|
-
return markTestConfig(config);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const { withDangerousMod } = loadConfigPlugins();
|
|
32
|
-
const { setupIosBundle } = require('./scripts/bundle/ios');
|
|
33
|
-
const { setupAndroidBundle } = require('./scripts/bundle/android');
|
|
34
|
-
|
|
35
|
-
config = withDangerousMod(config, ['ios', async (modConfig) => {
|
|
36
|
-
setupIosBundle({
|
|
37
|
-
cwd: modConfig.modRequest.projectRoot,
|
|
38
|
-
iosTarget: props.iosTarget,
|
|
39
|
-
});
|
|
40
|
-
return modConfig;
|
|
41
|
-
}]);
|
|
42
|
-
|
|
43
|
-
config = withDangerousMod(config, ['android', async (modConfig) => {
|
|
44
|
-
setupAndroidBundle({ cwd: modConfig.modRequest.projectRoot });
|
|
45
|
-
return modConfig;
|
|
46
|
-
}]);
|
|
47
|
-
|
|
48
|
-
return config;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
module.exports = withDebugToolkitDevClient;
|
package/dev-client.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execFileSync } = require('child_process');
|
|
6
|
-
|
|
7
|
-
const BEGIN = '// react-native-debug-toolkit: begin debug bundle';
|
|
8
|
-
const END = '// react-native-debug-toolkit: end debug bundle';
|
|
9
|
-
const REL_SCRIPT = '../../node_modules/react-native-debug-toolkit/scripts/debug-bundle.gradle';
|
|
10
|
-
const EXPECTED_TASK = 'createDebugToolkitDebugJsAndAssets';
|
|
11
|
-
const GRADLE_TASKS_ARGS = [':app:tasks', '--all', '--no-daemon', '--console=plain'];
|
|
12
|
-
|
|
13
|
-
function findGradleFile(cwd) {
|
|
14
|
-
const groovy = path.join(cwd, 'android/app/build.gradle');
|
|
15
|
-
const kotlin = path.join(cwd, 'android/app/build.gradle.kts');
|
|
16
|
-
|
|
17
|
-
if (fs.existsSync(kotlin)) {
|
|
18
|
-
return { file: kotlin, kind: 'kotlin' };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (fs.existsSync(groovy)) {
|
|
22
|
-
return { file: groovy, kind: 'groovy' };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
throw new Error('android/app/build.gradle(.kts) not found.');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function block(kind) {
|
|
29
|
-
const applyLine = kind === 'kotlin'
|
|
30
|
-
? `apply(from = "${REL_SCRIPT}")`
|
|
31
|
-
: `apply from: "${REL_SCRIPT}"`;
|
|
32
|
-
|
|
33
|
-
return `${BEGIN}\n${applyLine}\n${END}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function removeBlock(content) {
|
|
37
|
-
return content.replace(
|
|
38
|
-
/\/\/ react-native-debug-toolkit: begin debug bundle\n[\s\S]*?\/\/ react-native-debug-toolkit: end debug bundle\n?/g,
|
|
39
|
-
'',
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function setupAndroidBundle(options) {
|
|
44
|
-
const gradle = findGradleFile(options.cwd);
|
|
45
|
-
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
46
|
-
|
|
47
|
-
if (content.includes(BEGIN)) {
|
|
48
|
-
return { ok: true, changed: false, file: gradle.file };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
fs.writeFileSync(gradle.file, `${content.trimEnd()}\n\n${block(gradle.kind)}\n`);
|
|
52
|
-
return { ok: true, changed: true, file: gradle.file };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function undoAndroidBundle(options) {
|
|
56
|
-
const gradle = findGradleFile(options.cwd);
|
|
57
|
-
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
58
|
-
const next = removeBlock(content).replace(/\n{3,}/g, '\n\n');
|
|
59
|
-
|
|
60
|
-
if (next === content) {
|
|
61
|
-
return { ok: true, changed: false, file: gradle.file };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
fs.writeFileSync(gradle.file, next);
|
|
65
|
-
return { ok: true, changed: true, file: gradle.file };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function checkAndroidBundle(options) {
|
|
69
|
-
const gradle = findGradleFile(options.cwd);
|
|
70
|
-
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
71
|
-
const hasMarker = content.includes(BEGIN) && content.includes('scripts/debug-bundle.gradle');
|
|
72
|
-
|
|
73
|
-
if (!hasMarker) {
|
|
74
|
-
return { ok: false, reason: 'marker_missing', file: gradle.file };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const androidDir = path.join(options.cwd, 'android');
|
|
78
|
-
const gradlew = path.join(androidDir, 'gradlew');
|
|
79
|
-
const command = fs.existsSync(gradlew) ? gradlew : 'gradle';
|
|
80
|
-
const runGradle = options.runGradle || ((cmd, args, opts) => execFileSync(cmd, args, {
|
|
81
|
-
cwd: opts.cwd,
|
|
82
|
-
encoding: 'utf8',
|
|
83
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
-
}));
|
|
85
|
-
const output = runGradle(command, GRADLE_TASKS_ARGS, { cwd: androidDir });
|
|
86
|
-
|
|
87
|
-
if (!String(output).includes(EXPECTED_TASK)) {
|
|
88
|
-
return { ok: false, reason: 'gradle_task_missing', file: gradle.file };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return { ok: true, file: gradle.file };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
module.exports = {
|
|
95
|
-
setupAndroidBundle,
|
|
96
|
-
undoAndroidBundle,
|
|
97
|
-
checkAndroidBundle,
|
|
98
|
-
BEGIN,
|
|
99
|
-
END,
|
|
100
|
-
EXPECTED_TASK,
|
|
101
|
-
};
|
package/scripts/bundle/cli.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function readOption(args, name) {
|
|
4
|
-
const index = args.indexOf(name);
|
|
5
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function hasFlag(args, name) {
|
|
9
|
-
return args.includes(name);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function parseCommon(args, cwd) {
|
|
13
|
-
return {
|
|
14
|
-
cwd,
|
|
15
|
-
platform: readOption(args, '--platform') || 'all',
|
|
16
|
-
undo: hasFlag(args, '--undo'),
|
|
17
|
-
check: hasFlag(args, '--check'),
|
|
18
|
-
iosTarget: readOption(args, '--ios-target'),
|
|
19
|
-
yes: hasFlag(args, '--yes'),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function runBundleCli(args, deps = {}) {
|
|
24
|
-
const cwd = deps.cwd || process.cwd();
|
|
25
|
-
const io = deps.io || {
|
|
26
|
-
writeOut: (value) => process.stdout.write(value),
|
|
27
|
-
writeErr: (value) => process.stderr.write(value),
|
|
28
|
-
};
|
|
29
|
-
const command = args[0];
|
|
30
|
-
|
|
31
|
-
if (command === 'setup-bundle') {
|
|
32
|
-
const setupBundle = deps.setupBundle || require('./setup').setupBundle;
|
|
33
|
-
await setupBundle(parseCommon(args.slice(1), cwd));
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (command === 'doctor-bundle') {
|
|
38
|
-
const doctorBundle = deps.doctorBundle || require('./doctor').doctorBundle;
|
|
39
|
-
await doctorBundle({
|
|
40
|
-
cwd,
|
|
41
|
-
platform: readOption(args.slice(1), '--platform') || 'all',
|
|
42
|
-
app: readOption(args.slice(1), '--app'),
|
|
43
|
-
apk: readOption(args.slice(1), '--apk'),
|
|
44
|
-
});
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (command === 'embed') {
|
|
49
|
-
io.writeErr('Unknown command: embed\nUse: debug-toolkit setup-bundle\n');
|
|
50
|
-
return 1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
io.writeErr(`Unknown command: ${command || '(none)'}\n`);
|
|
54
|
-
return 1;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { runBundleCli };
|
package/scripts/bundle/doctor.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execFileSync } = require('child_process');
|
|
6
|
-
|
|
7
|
-
function readZipEntriesWithUnzip(file) {
|
|
8
|
-
const output = execFileSync('unzip', ['-Z1', file], { encoding: 'utf8' });
|
|
9
|
-
return output.split(/\r?\n/).filter(Boolean);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function doctorIos(options) {
|
|
13
|
-
if (!options.app) throw new Error('--app is required for iOS doctor.');
|
|
14
|
-
const bundle = path.join(options.app, 'main.jsbundle');
|
|
15
|
-
if (!fs.existsSync(bundle)) {
|
|
16
|
-
throw new Error(`main.jsbundle not found in ${options.app}`);
|
|
17
|
-
}
|
|
18
|
-
return { ok: true, platform: 'ios', bundle };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function doctorAndroid(options) {
|
|
22
|
-
if (!options.apk) throw new Error('--apk is required for Android doctor.');
|
|
23
|
-
const readZipEntries = options.readZipEntries || readZipEntriesWithUnzip;
|
|
24
|
-
const entries = await readZipEntries(options.apk);
|
|
25
|
-
const bundle = entries.find((entry) => /^assets\/.+\.bundle$/.test(entry));
|
|
26
|
-
if (!bundle) {
|
|
27
|
-
throw new Error(`Android JS bundle not found in ${options.apk}`);
|
|
28
|
-
}
|
|
29
|
-
return { ok: true, platform: 'android', bundle };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function doctorBundle(options) {
|
|
33
|
-
if (options.platform === 'ios') return doctorIos(options);
|
|
34
|
-
if (options.platform === 'android') return doctorAndroid(options);
|
|
35
|
-
throw new Error(`Unsupported platform for doctor-bundle: ${options.platform}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
module.exports = { doctorBundle, readZipEntriesWithUnzip };
|