@teardown/dev-client 2.0.44
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/android/build.gradle.kts +34 -0
- package/android/react-native.config.js +10 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/com/teardown/devclient/DevSettingsModule.kt +130 -0
- package/android/src/main/java/com/teardown/devclient/ShakeDetectorModule.kt +118 -0
- package/android/src/main/java/com/teardown/devclient/TeardownDevClientPackage.kt +23 -0
- package/ios/TeardownDevClient/DevSettingsModule.swift +135 -0
- package/ios/TeardownDevClient/ShakeDetector.swift +102 -0
- package/ios/TeardownDevClient/TeardownDevClient.h +14 -0
- package/ios/TeardownDevClient/TeardownDevClient.mm +42 -0
- package/ios/TeardownDevClient.podspec +23 -0
- package/package.json +56 -0
- package/src/components/dev-menu/dev-menu.tsx +254 -0
- package/src/components/dev-menu/index.ts +5 -0
- package/src/components/error-overlay/error-overlay.tsx +256 -0
- package/src/components/error-overlay/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/components/splash-screen/index.ts +5 -0
- package/src/components/splash-screen/splash-screen.tsx +99 -0
- package/src/dev-client-provider.tsx +204 -0
- package/src/hooks/index.ts +24 -0
- package/src/hooks/use-bundler-status.ts +139 -0
- package/src/hooks/use-dev-menu.ts +306 -0
- package/src/hooks/use-splash-screen.ts +177 -0
- package/src/index.ts +77 -0
- package/src/native/dev-settings.ts +132 -0
- package/src/native/index.ts +16 -0
- package/src/native/shake-detector.ts +105 -0
- package/src/types.ts +235 -0
- package/src/utils/bundler-url.ts +103 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/platform.ts +64 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevClientProvider
|
|
3
|
+
*
|
|
4
|
+
* Root provider component that wraps the application and provides
|
|
5
|
+
* development client functionality including dev menu and splash screen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { createContext, type PropsWithChildren, useContext, useMemo } from "react";
|
|
9
|
+
import { DevMenu } from "./components/dev-menu";
|
|
10
|
+
import { SplashScreen } from "./components/splash-screen";
|
|
11
|
+
import { useBundlerStatus } from "./hooks/use-bundler-status";
|
|
12
|
+
import { useDevMenu } from "./hooks/use-dev-menu";
|
|
13
|
+
import { useSplashScreen } from "./hooks/use-splash-screen";
|
|
14
|
+
import type { DevClientConfig, DevClientContextValue } from "./types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default configuration
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_CONFIG: DevClientConfig = {
|
|
20
|
+
enableShakeGesture: true,
|
|
21
|
+
enableThreeFingerTap: true,
|
|
22
|
+
splash: {
|
|
23
|
+
backgroundColor: "#ffffff",
|
|
24
|
+
minDisplayTime: 500,
|
|
25
|
+
fadeOutDuration: 300,
|
|
26
|
+
autoHide: true,
|
|
27
|
+
},
|
|
28
|
+
verbose: __DEV__,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* DevClient context
|
|
33
|
+
*/
|
|
34
|
+
const DevClientContext = createContext<DevClientContextValue | null>(null);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* DevClientProvider props
|
|
38
|
+
*/
|
|
39
|
+
export interface DevClientProviderProps extends PropsWithChildren {
|
|
40
|
+
/** Configuration options */
|
|
41
|
+
config?: DevClientConfig;
|
|
42
|
+
|
|
43
|
+
/** Whether to show splash screen (default: true in dev) */
|
|
44
|
+
showSplash?: boolean;
|
|
45
|
+
|
|
46
|
+
/** Whether to enable dev menu (default: true in dev) */
|
|
47
|
+
enableDevMenu?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* DevClientProvider component
|
|
52
|
+
*
|
|
53
|
+
* Wraps the application and provides development client functionality.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* import { DevClientProvider } from '@teardown/dev-client';
|
|
58
|
+
*
|
|
59
|
+
* function Root() {
|
|
60
|
+
* return (
|
|
61
|
+
* <DevClientProvider
|
|
62
|
+
* config={{
|
|
63
|
+
* enableShakeGesture: true,
|
|
64
|
+
* splash: {
|
|
65
|
+
* backgroundColor: '#000000',
|
|
66
|
+
* logo: require('./assets/logo.png'),
|
|
67
|
+
* },
|
|
68
|
+
* }}
|
|
69
|
+
* >
|
|
70
|
+
* <App />
|
|
71
|
+
* </DevClientProvider>
|
|
72
|
+
* );
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function DevClientProvider(props: DevClientProviderProps): React.JSX.Element {
|
|
77
|
+
const { children, config: userConfig, showSplash = __DEV__, enableDevMenu = __DEV__ } = props;
|
|
78
|
+
|
|
79
|
+
// Merge config with defaults
|
|
80
|
+
const config: DevClientConfig = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
...DEFAULT_CONFIG,
|
|
83
|
+
...userConfig,
|
|
84
|
+
splash: {
|
|
85
|
+
...DEFAULT_CONFIG.splash,
|
|
86
|
+
...userConfig?.splash,
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
[userConfig]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Bundler status
|
|
93
|
+
const bundlerStatus = useBundlerStatus({
|
|
94
|
+
bundlerUrl: config.bundlerUrl,
|
|
95
|
+
enablePolling: __DEV__,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Dev menu
|
|
99
|
+
const devMenu = useDevMenu({
|
|
100
|
+
enableShakeGesture: enableDevMenu && config.enableShakeGesture,
|
|
101
|
+
bundlerStatus,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Splash screen
|
|
105
|
+
const splashScreen = useSplashScreen({
|
|
106
|
+
config: config.splash,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Context value
|
|
110
|
+
const contextValue: DevClientContextValue = useMemo(
|
|
111
|
+
() => ({
|
|
112
|
+
config,
|
|
113
|
+
devMenu: {
|
|
114
|
+
state: devMenu.state,
|
|
115
|
+
actions: devMenu.actions,
|
|
116
|
+
defaultItems: devMenu.defaultMenuItems,
|
|
117
|
+
},
|
|
118
|
+
splashScreen: {
|
|
119
|
+
isVisible: splashScreen.isVisible,
|
|
120
|
+
hide: splashScreen.hide,
|
|
121
|
+
show: splashScreen.show,
|
|
122
|
+
},
|
|
123
|
+
bundlerStatus,
|
|
124
|
+
}),
|
|
125
|
+
[config, devMenu, splashScreen, bundlerStatus]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<DevClientContext.Provider value={contextValue}>
|
|
130
|
+
{children}
|
|
131
|
+
|
|
132
|
+
{/* Dev Menu */}
|
|
133
|
+
{enableDevMenu && (
|
|
134
|
+
<DevMenu
|
|
135
|
+
visible={devMenu.state.isVisible}
|
|
136
|
+
onDismiss={devMenu.actions.hide}
|
|
137
|
+
items={[...devMenu.defaultMenuItems, ...(config.customMenuItems ?? [])]}
|
|
138
|
+
bundlerStatus={bundlerStatus}
|
|
139
|
+
isLoading={devMenu.state.isLoading}
|
|
140
|
+
testID="teardown-dev-menu"
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Splash Screen */}
|
|
145
|
+
{showSplash && (
|
|
146
|
+
<SplashScreen
|
|
147
|
+
visible={splashScreen.isVisible}
|
|
148
|
+
backgroundColor={splashScreen.config.backgroundColor}
|
|
149
|
+
logo={splashScreen.config.logo}
|
|
150
|
+
resizeMode={splashScreen.config.resizeMode}
|
|
151
|
+
opacity={splashScreen.opacity}
|
|
152
|
+
isFading={splashScreen.isFading}
|
|
153
|
+
testID="teardown-splash-screen"
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
</DevClientContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Hook to access dev client context
|
|
162
|
+
*
|
|
163
|
+
* @returns Dev client context value
|
|
164
|
+
* @throws Error if used outside DevClientProvider
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```tsx
|
|
168
|
+
* function MyComponent() {
|
|
169
|
+
* const { devMenu, splashScreen } = useDevClient();
|
|
170
|
+
*
|
|
171
|
+
* return (
|
|
172
|
+
* <Button
|
|
173
|
+
* title="Open Dev Menu"
|
|
174
|
+
* onPress={devMenu.actions.show}
|
|
175
|
+
* />
|
|
176
|
+
* );
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export function useDevClient(): DevClientContextValue {
|
|
181
|
+
const context = useContext(DevClientContext);
|
|
182
|
+
|
|
183
|
+
if (!context) {
|
|
184
|
+
throw new Error("useDevClient must be used within a DevClientProvider");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return context;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Hook to access dev menu (convenience wrapper)
|
|
192
|
+
*/
|
|
193
|
+
export function useDevClientMenu() {
|
|
194
|
+
const { devMenu } = useDevClient();
|
|
195
|
+
return devMenu;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Hook to access splash screen (convenience wrapper)
|
|
200
|
+
*/
|
|
201
|
+
export function useDevClientSplash() {
|
|
202
|
+
const { splashScreen } = useDevClient();
|
|
203
|
+
return splashScreen;
|
|
204
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createBundlerStatus,
|
|
7
|
+
type UseBundlerStatusOptions,
|
|
8
|
+
useBundlerStatus,
|
|
9
|
+
} from "./use-bundler-status";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
createInitialDevMenuState,
|
|
13
|
+
type DefaultMenuItemHandlers,
|
|
14
|
+
getDefaultMenuItems,
|
|
15
|
+
type UseDevMenuOptions,
|
|
16
|
+
type UseDevMenuReturn,
|
|
17
|
+
useDevMenu,
|
|
18
|
+
} from "./use-dev-menu";
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
type UseSplashScreenOptions,
|
|
22
|
+
type UseSplashScreenReturn,
|
|
23
|
+
useSplashScreen,
|
|
24
|
+
} from "./use-splash-screen";
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundler status hook
|
|
3
|
+
*
|
|
4
|
+
* Tracks connection status to the Metro bundler.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
import type { BundlerStatus } from "../types";
|
|
9
|
+
import { getBundlerUrl, isValidBundlerUrl } from "../utils/bundler-url";
|
|
10
|
+
import { isEmulator as checkIsEmulator, currentPlatform } from "../utils/platform";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default bundler status
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_STATUS: BundlerStatus = {
|
|
16
|
+
connected: false,
|
|
17
|
+
url: null,
|
|
18
|
+
error: null,
|
|
19
|
+
lastConnectedAt: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a bundler status object
|
|
24
|
+
*
|
|
25
|
+
* @param overrides - Partial status to override defaults
|
|
26
|
+
* @returns Complete bundler status object
|
|
27
|
+
*/
|
|
28
|
+
export function createBundlerStatus(overrides: Partial<BundlerStatus> = {}): BundlerStatus {
|
|
29
|
+
return {
|
|
30
|
+
...DEFAULT_STATUS,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for useBundlerStatus hook
|
|
37
|
+
*/
|
|
38
|
+
export interface UseBundlerStatusOptions {
|
|
39
|
+
/** Custom bundler URL override */
|
|
40
|
+
bundlerUrl?: string;
|
|
41
|
+
|
|
42
|
+
/** Custom port */
|
|
43
|
+
port?: number;
|
|
44
|
+
|
|
45
|
+
/** Polling interval in ms (default: 5000) */
|
|
46
|
+
pollInterval?: number;
|
|
47
|
+
|
|
48
|
+
/** Enable automatic polling (default: true in __DEV__) */
|
|
49
|
+
enablePolling?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook to track Metro bundler connection status
|
|
54
|
+
*
|
|
55
|
+
* @param options - Configuration options
|
|
56
|
+
* @returns Current bundler status
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* function MyComponent() {
|
|
61
|
+
* const status = useBundlerStatus();
|
|
62
|
+
*
|
|
63
|
+
* if (!status.connected) {
|
|
64
|
+
* return <Text>Connecting to bundler...</Text>;
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* return <Text>Connected to {status.url}</Text>;
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function useBundlerStatus(options: UseBundlerStatusOptions = {}): BundlerStatus {
|
|
72
|
+
const { bundlerUrl, port, pollInterval = 5000, enablePolling = __DEV__ } = options;
|
|
73
|
+
|
|
74
|
+
const [status, setStatus] = useState<BundlerStatus>(DEFAULT_STATUS);
|
|
75
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
76
|
+
|
|
77
|
+
const checkConnection = useCallback(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const isEmu = await checkIsEmulator();
|
|
80
|
+
const url =
|
|
81
|
+
bundlerUrl && isValidBundlerUrl(bundlerUrl)
|
|
82
|
+
? bundlerUrl
|
|
83
|
+
: getBundlerUrl({
|
|
84
|
+
platform: currentPlatform,
|
|
85
|
+
isEmulator: isEmu,
|
|
86
|
+
port,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Check bundler status endpoint
|
|
90
|
+
const statusUrl = `${url}/status`;
|
|
91
|
+
const response = await fetch(statusUrl, {
|
|
92
|
+
method: "GET",
|
|
93
|
+
headers: { Accept: "application/json" },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (response.ok) {
|
|
97
|
+
setStatus({
|
|
98
|
+
connected: true,
|
|
99
|
+
url,
|
|
100
|
+
error: null,
|
|
101
|
+
lastConnectedAt: new Date(),
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
setStatus((prev) => ({
|
|
105
|
+
...prev,
|
|
106
|
+
connected: false,
|
|
107
|
+
url,
|
|
108
|
+
error: `Bundler returned status ${response.status}`,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
113
|
+
setStatus((prev) => ({
|
|
114
|
+
...prev,
|
|
115
|
+
connected: false,
|
|
116
|
+
error: errorMessage,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
}, [bundlerUrl, port]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
// Initial check
|
|
123
|
+
checkConnection();
|
|
124
|
+
|
|
125
|
+
// Set up polling if enabled
|
|
126
|
+
if (enablePolling && pollInterval > 0) {
|
|
127
|
+
intervalRef.current = setInterval(checkConnection, pollInterval);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
if (intervalRef.current) {
|
|
132
|
+
clearInterval(intervalRef.current);
|
|
133
|
+
intervalRef.current = null;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [checkConnection, enablePolling, pollInterval]);
|
|
137
|
+
|
|
138
|
+
return status;
|
|
139
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev menu state management hook
|
|
3
|
+
*
|
|
4
|
+
* Provides state and actions for the developer menu.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useMemo, useReducer } from "react";
|
|
8
|
+
import {
|
|
9
|
+
getDevSettings,
|
|
10
|
+
openDebugger,
|
|
11
|
+
reload,
|
|
12
|
+
setHotLoadingEnabled,
|
|
13
|
+
toggleElementInspector,
|
|
14
|
+
} from "../native/dev-settings";
|
|
15
|
+
import { ShakeDetector } from "../native/shake-detector";
|
|
16
|
+
import type { BundlerStatus, DevMenuActions, DevMenuItem, DevMenuState } from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default bundler status
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_BUNDLER_STATUS: BundlerStatus = {
|
|
22
|
+
connected: false,
|
|
23
|
+
url: null,
|
|
24
|
+
error: null,
|
|
25
|
+
lastConnectedAt: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default dev menu state
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_STATE: DevMenuState = {
|
|
32
|
+
isVisible: false,
|
|
33
|
+
isLoading: false,
|
|
34
|
+
bundlerStatus: DEFAULT_BUNDLER_STATUS,
|
|
35
|
+
hotReloadEnabled: true,
|
|
36
|
+
fastRefreshEnabled: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create initial dev menu state
|
|
41
|
+
*
|
|
42
|
+
* @param overrides - Partial state to override defaults
|
|
43
|
+
* @returns Complete dev menu state
|
|
44
|
+
*/
|
|
45
|
+
export function createInitialDevMenuState(overrides: Partial<DevMenuState> = {}): DevMenuState {
|
|
46
|
+
return {
|
|
47
|
+
...DEFAULT_STATE,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Action types for dev menu reducer
|
|
54
|
+
*/
|
|
55
|
+
type DevMenuAction =
|
|
56
|
+
| { type: "SHOW" }
|
|
57
|
+
| { type: "HIDE" }
|
|
58
|
+
| { type: "TOGGLE" }
|
|
59
|
+
| { type: "SET_LOADING"; payload: boolean }
|
|
60
|
+
| { type: "SET_BUNDLER_STATUS"; payload: BundlerStatus }
|
|
61
|
+
| { type: "SET_HOT_RELOAD"; payload: boolean }
|
|
62
|
+
| { type: "SET_FAST_REFRESH"; payload: boolean }
|
|
63
|
+
| { type: "UPDATE_FROM_NATIVE"; payload: Partial<DevMenuState> };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Dev menu state reducer
|
|
67
|
+
*/
|
|
68
|
+
function devMenuReducer(state: DevMenuState, action: DevMenuAction): DevMenuState {
|
|
69
|
+
switch (action.type) {
|
|
70
|
+
case "SHOW":
|
|
71
|
+
return { ...state, isVisible: true };
|
|
72
|
+
case "HIDE":
|
|
73
|
+
return { ...state, isVisible: false };
|
|
74
|
+
case "TOGGLE":
|
|
75
|
+
return { ...state, isVisible: !state.isVisible };
|
|
76
|
+
case "SET_LOADING":
|
|
77
|
+
return { ...state, isLoading: action.payload };
|
|
78
|
+
case "SET_BUNDLER_STATUS":
|
|
79
|
+
return { ...state, bundlerStatus: action.payload };
|
|
80
|
+
case "SET_HOT_RELOAD":
|
|
81
|
+
return { ...state, hotReloadEnabled: action.payload };
|
|
82
|
+
case "SET_FAST_REFRESH":
|
|
83
|
+
return { ...state, fastRefreshEnabled: action.payload };
|
|
84
|
+
case "UPDATE_FROM_NATIVE":
|
|
85
|
+
return { ...state, ...action.payload };
|
|
86
|
+
default:
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Options for default menu item handlers
|
|
93
|
+
*/
|
|
94
|
+
export interface DefaultMenuItemHandlers {
|
|
95
|
+
onReload: () => Promise<void>;
|
|
96
|
+
onOpenDebugger: () => Promise<void>;
|
|
97
|
+
onToggleInspector: () => Promise<void>;
|
|
98
|
+
onTogglePerfMonitor: () => Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get default dev menu items
|
|
103
|
+
*
|
|
104
|
+
* @param handlers - Action handlers for menu items
|
|
105
|
+
* @returns Array of default menu items
|
|
106
|
+
*/
|
|
107
|
+
export function getDefaultMenuItems(handlers: DefaultMenuItemHandlers): DevMenuItem[] {
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
id: "reload",
|
|
111
|
+
label: "Reload",
|
|
112
|
+
onPress: handlers.onReload,
|
|
113
|
+
devOnly: false,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "debugger",
|
|
117
|
+
label: "Open Debugger",
|
|
118
|
+
onPress: handlers.onOpenDebugger,
|
|
119
|
+
devOnly: true,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "inspector",
|
|
123
|
+
label: "Toggle Element Inspector",
|
|
124
|
+
onPress: handlers.onToggleInspector,
|
|
125
|
+
devOnly: true,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "perf-monitor",
|
|
129
|
+
label: "Toggle Performance Monitor",
|
|
130
|
+
onPress: handlers.onTogglePerfMonitor,
|
|
131
|
+
devOnly: true,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Options for useDevMenu hook
|
|
138
|
+
*/
|
|
139
|
+
export interface UseDevMenuOptions {
|
|
140
|
+
/** Enable shake gesture to open menu (default: true) */
|
|
141
|
+
enableShakeGesture?: boolean;
|
|
142
|
+
|
|
143
|
+
/** Custom bundler status (optional, otherwise uses internal tracking) */
|
|
144
|
+
bundlerStatus?: BundlerStatus;
|
|
145
|
+
|
|
146
|
+
/** Initial visibility state */
|
|
147
|
+
initialVisible?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Hook return type
|
|
152
|
+
*/
|
|
153
|
+
export interface UseDevMenuReturn {
|
|
154
|
+
/** Current dev menu state */
|
|
155
|
+
state: DevMenuState;
|
|
156
|
+
|
|
157
|
+
/** Actions to control the dev menu */
|
|
158
|
+
actions: DevMenuActions;
|
|
159
|
+
|
|
160
|
+
/** Default menu items */
|
|
161
|
+
defaultMenuItems: DevMenuItem[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Hook for managing dev menu state
|
|
166
|
+
*
|
|
167
|
+
* @param options - Configuration options
|
|
168
|
+
* @returns State, actions, and default menu items
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```tsx
|
|
172
|
+
* function DevMenuContainer() {
|
|
173
|
+
* const { state, actions, defaultMenuItems } = useDevMenu();
|
|
174
|
+
*
|
|
175
|
+
* return (
|
|
176
|
+
* <DevMenu
|
|
177
|
+
* visible={state.isVisible}
|
|
178
|
+
* onDismiss={actions.hide}
|
|
179
|
+
* items={defaultMenuItems}
|
|
180
|
+
* />
|
|
181
|
+
* );
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export function useDevMenu(options: UseDevMenuOptions = {}): UseDevMenuReturn {
|
|
186
|
+
const { enableShakeGesture = true, bundlerStatus, initialVisible = false } = options;
|
|
187
|
+
|
|
188
|
+
const [state, dispatch] = useReducer(
|
|
189
|
+
devMenuReducer,
|
|
190
|
+
createInitialDevMenuState({
|
|
191
|
+
isVisible: initialVisible,
|
|
192
|
+
bundlerStatus: bundlerStatus ?? DEFAULT_BUNDLER_STATUS,
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Update bundler status when prop changes
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (bundlerStatus) {
|
|
199
|
+
dispatch({ type: "SET_BUNDLER_STATUS", payload: bundlerStatus });
|
|
200
|
+
}
|
|
201
|
+
}, [bundlerStatus]);
|
|
202
|
+
|
|
203
|
+
// Fetch initial settings from native
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const fetchSettings = async () => {
|
|
206
|
+
try {
|
|
207
|
+
const settings = await getDevSettings();
|
|
208
|
+
dispatch({
|
|
209
|
+
type: "UPDATE_FROM_NATIVE",
|
|
210
|
+
payload: {
|
|
211
|
+
hotReloadEnabled: settings.isHotLoadingEnabled,
|
|
212
|
+
fastRefreshEnabled: settings.isFastRefreshEnabled,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
} catch {
|
|
216
|
+
// Ignore errors - native module might not be available
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (__DEV__) {
|
|
221
|
+
fetchSettings();
|
|
222
|
+
}
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
// Set up shake gesture listener
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!enableShakeGesture || !__DEV__) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const handleShake = () => {
|
|
232
|
+
dispatch({ type: "TOGGLE" });
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
ShakeDetector.on("shake", handleShake);
|
|
236
|
+
ShakeDetector.start();
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
ShakeDetector.off("shake", handleShake);
|
|
240
|
+
ShakeDetector.stop();
|
|
241
|
+
};
|
|
242
|
+
}, [enableShakeGesture]);
|
|
243
|
+
|
|
244
|
+
// Actions
|
|
245
|
+
const actions: DevMenuActions = useMemo(
|
|
246
|
+
() => ({
|
|
247
|
+
show: () => dispatch({ type: "SHOW" }),
|
|
248
|
+
hide: () => dispatch({ type: "HIDE" }),
|
|
249
|
+
toggle: () => dispatch({ type: "TOGGLE" }),
|
|
250
|
+
|
|
251
|
+
reload: async () => {
|
|
252
|
+
dispatch({ type: "SET_LOADING", payload: true });
|
|
253
|
+
try {
|
|
254
|
+
reload();
|
|
255
|
+
} finally {
|
|
256
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
openDebugger: async () => {
|
|
261
|
+
dispatch({ type: "SET_LOADING", payload: true });
|
|
262
|
+
try {
|
|
263
|
+
openDebugger();
|
|
264
|
+
} finally {
|
|
265
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
266
|
+
dispatch({ type: "HIDE" });
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
toggleElementInspector: async () => {
|
|
271
|
+
toggleElementInspector();
|
|
272
|
+
dispatch({ type: "HIDE" });
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
togglePerfMonitor: async () => {
|
|
276
|
+
// Performance monitor toggle is handled by native
|
|
277
|
+
// Just hide the menu
|
|
278
|
+
dispatch({ type: "HIDE" });
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
setHotReloadEnabled: async (enabled: boolean) => {
|
|
282
|
+
setHotLoadingEnabled(enabled);
|
|
283
|
+
dispatch({ type: "SET_HOT_RELOAD", payload: enabled });
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
[]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Default menu items
|
|
290
|
+
const defaultMenuItems = useMemo(
|
|
291
|
+
() =>
|
|
292
|
+
getDefaultMenuItems({
|
|
293
|
+
onReload: actions.reload,
|
|
294
|
+
onOpenDebugger: actions.openDebugger,
|
|
295
|
+
onToggleInspector: actions.toggleElementInspector,
|
|
296
|
+
onTogglePerfMonitor: actions.togglePerfMonitor,
|
|
297
|
+
}),
|
|
298
|
+
[actions]
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
state,
|
|
303
|
+
actions,
|
|
304
|
+
defaultMenuItems,
|
|
305
|
+
};
|
|
306
|
+
}
|