@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.
Files changed (32) hide show
  1. package/android/build.gradle.kts +34 -0
  2. package/android/react-native.config.js +10 -0
  3. package/android/src/main/AndroidManifest.xml +7 -0
  4. package/android/src/main/java/com/teardown/devclient/DevSettingsModule.kt +130 -0
  5. package/android/src/main/java/com/teardown/devclient/ShakeDetectorModule.kt +118 -0
  6. package/android/src/main/java/com/teardown/devclient/TeardownDevClientPackage.kt +23 -0
  7. package/ios/TeardownDevClient/DevSettingsModule.swift +135 -0
  8. package/ios/TeardownDevClient/ShakeDetector.swift +102 -0
  9. package/ios/TeardownDevClient/TeardownDevClient.h +14 -0
  10. package/ios/TeardownDevClient/TeardownDevClient.mm +42 -0
  11. package/ios/TeardownDevClient.podspec +23 -0
  12. package/package.json +56 -0
  13. package/src/components/dev-menu/dev-menu.tsx +254 -0
  14. package/src/components/dev-menu/index.ts +5 -0
  15. package/src/components/error-overlay/error-overlay.tsx +256 -0
  16. package/src/components/error-overlay/index.ts +5 -0
  17. package/src/components/index.ts +7 -0
  18. package/src/components/splash-screen/index.ts +5 -0
  19. package/src/components/splash-screen/splash-screen.tsx +99 -0
  20. package/src/dev-client-provider.tsx +204 -0
  21. package/src/hooks/index.ts +24 -0
  22. package/src/hooks/use-bundler-status.ts +139 -0
  23. package/src/hooks/use-dev-menu.ts +306 -0
  24. package/src/hooks/use-splash-screen.ts +177 -0
  25. package/src/index.ts +77 -0
  26. package/src/native/dev-settings.ts +132 -0
  27. package/src/native/index.ts +16 -0
  28. package/src/native/shake-detector.ts +105 -0
  29. package/src/types.ts +235 -0
  30. package/src/utils/bundler-url.ts +103 -0
  31. package/src/utils/index.ts +19 -0
  32. 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
+ }