bippy 0.6.1-dev.fe75557 → 0.7.0-dev.07e6032

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 (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +223 -461
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/errors.d.cts +410 -0
  6. package/dist/errors.d.ts +410 -0
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +154 -3
  9. package/dist/index.d.ts +154 -3
  10. package/dist/index.js +1 -1
  11. package/dist/install-hook-only.cjs +1 -1
  12. package/dist/install-hook-only.d.cts +9 -1
  13. package/dist/install-hook-only.d.ts +9 -1
  14. package/dist/install-hook-only.js +1 -1
  15. package/dist/rdt-hook.cjs +1 -1
  16. package/dist/rdt-hook.js +1 -1
  17. package/dist/source.cjs +14 -5
  18. package/dist/source.d.cts +86 -69
  19. package/dist/source.d.ts +86 -69
  20. package/dist/source.js +14 -5
  21. package/package.json +26 -27
  22. package/src/core.ts +367 -685
  23. package/src/errors.ts +34 -0
  24. package/src/index.ts +1 -0
  25. package/src/install-hook-only.ts +6 -2
  26. package/src/rdt-hook.ts +177 -156
  27. package/src/react-internals/generated/react-work-tags.ts +318 -0
  28. package/src/react-internals/index.ts +73 -0
  29. package/src/react-internals/semver.ts +80 -0
  30. package/src/react-internals/types.ts +186 -0
  31. package/src/react.ts +76 -0
  32. package/src/source/constants.ts +1 -1
  33. package/src/source/error-stack.ts +11 -0
  34. package/src/source/get-display-name-from-source.ts +44 -40
  35. package/src/source/get-source.ts +43 -26
  36. package/src/source/index.ts +11 -1
  37. package/src/source/inspect-hooks.ts +220 -207
  38. package/src/source/owner-stack.ts +121 -174
  39. package/src/source/parse-debug-stack.ts +4 -4
  40. package/src/source/parse-hook-names.ts +14 -53
  41. package/src/source/parse-stack.ts +14 -31
  42. package/src/source/renderer-dispatchers.ts +30 -0
  43. package/src/source/symbolication.ts +709 -120
  44. package/dist/core.d.cts +0 -214
  45. package/dist/core.d.ts +0 -214
  46. package/dist/core2.d.cts +0 -3
  47. package/dist/core2.d.ts +0 -3
  48. package/dist/get-source.cjs +0 -19
  49. package/dist/get-source.js +0 -19
  50. package/dist/index.iife.js +0 -9
  51. package/dist/install-hook-only.iife.js +0 -9
  52. package/dist/react-refresh.cjs +0 -9
  53. package/dist/react-refresh.d.cts +0 -66
  54. package/dist/react-refresh.d.ts +0 -66
  55. package/dist/react-refresh.js +0 -9
  56. package/dist/unsubscribe.d.cts +0 -298
  57. package/dist/unsubscribe.d.ts +0 -298
  58. package/src/react-refresh/constants.ts +0 -9
  59. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  60. package/src/react-refresh/index.ts +0 -173
  61. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  62. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  63. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  64. package/src/react-refresh/types.ts +0 -7
  65. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  66. package/src/types.ts +0 -438
  67. package/src/unsubscribe.ts +0 -17
package/src/errors.ts ADDED
@@ -0,0 +1,34 @@
1
+ export class BippyError extends Error {
2
+ constructor(message: string, cause?: unknown) {
3
+ super(message, { cause });
4
+ this.name = "BippyError";
5
+ }
6
+ }
7
+
8
+ export class BippyHookInspectionError extends BippyError {
9
+ constructor(message: string, cause?: unknown) {
10
+ super(message, cause);
11
+ this.name = "BippyHookInspectionError";
12
+ }
13
+ }
14
+
15
+ export class BippyUnsupportedHookError extends BippyHookInspectionError {
16
+ constructor(message: string, cause?: unknown) {
17
+ super(message, cause);
18
+ this.name = "BippyUnsupportedHookError";
19
+ }
20
+ }
21
+
22
+ export class BippyHookRenderError extends BippyHookInspectionError {
23
+ constructor(message: string, cause?: unknown) {
24
+ super(message, cause);
25
+ this.name = "BippyHookRenderError";
26
+ }
27
+ }
28
+
29
+ export class BippySourceMapError extends BippyError {
30
+ constructor(message: string, cause?: unknown) {
31
+ super(message, cause);
32
+ this.name = "BippySourceMapError";
33
+ }
34
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import "./install-hook-only.js";
2
2
 
3
3
  export * from "./core.js";
4
+ export * from "./react.js";
@@ -1,3 +1,7 @@
1
- import { safelyInstallRDTHook } from "./rdt-hook.js";
1
+ import { getRDTHook } from "./rdt-hook.js";
2
2
 
3
- safelyInstallRDTHook();
3
+ // Importing bippy must never crash module evaluation, even when a foreign
4
+ // hook is frozen or otherwise rejects patching.
5
+ try {
6
+ getRDTHook();
7
+ } catch {}
package/src/rdt-hook.ts CHANGED
@@ -1,21 +1,32 @@
1
- // IMPORTANT:
2
- // this file is super important to load the __REACT_DEVTOOLS_GLOBAL_HOOK__ object
3
- // without this, we can't stub the React DevTools global hook, we don't have a way to instrument the application
4
- // make sure you import this file first before anything else (particularly React)
1
+ // This module must load before React so renderers can inject into the hook.
5
2
 
6
- import type { ReactDevToolsGlobalHook, ReactRenderer } from "./types.js";
7
- import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
3
+ import type { ReactDevToolsGlobalHook, ReactRenderer } from "./react-internals/index.js";
4
+
5
+ export interface Unsubscribe extends Disposable {
6
+ (): void;
7
+ }
8
+
9
+ interface ActiveListener {
10
+ (): unknown;
11
+ }
12
+
13
+ interface RendererInjectListener {
14
+ (renderer: ReactRenderer): void;
15
+ }
16
+
17
+ interface RDTHookReplaceListener {
18
+ (rdtHook: ReactDevToolsGlobalHook): void;
19
+ }
8
20
 
9
21
  export const version = process.env.VERSION;
10
22
  export const BIPPY_INSTRUMENTATION_STRING = `bippy-${version}`;
11
23
 
12
24
  const objectDefineProperty = Object.defineProperty;
13
- // eslint-disable-next-line @typescript-eslint/unbound-method
14
- const objectHasOwnProperty = Object.prototype.hasOwnProperty;
15
25
 
16
- const NO_OP = () => {
17
- /**/
18
- };
26
+ const noOp = (): void => {};
27
+
28
+ export const createUnsubscribe = (unsubscribe: () => void): Unsubscribe =>
29
+ Object.assign(unsubscribe, { [Symbol.dispose]: unsubscribe });
19
30
 
20
31
  const checkDCE = (functionToCheck: unknown): void => {
21
32
  try {
@@ -26,7 +37,7 @@ const checkDCE = (functionToCheck: unknown): void => {
26
37
  "React is running in production mode, but dead code " +
27
38
  "elimination has not been applied. Read how to correctly " +
28
39
  "configure React for production: " +
29
- "https://reactjs.org/link/perf-use-production-build",
40
+ "https://react.dev/link/perf-use-production-build",
30
41
  );
31
42
  });
32
43
  }
@@ -39,62 +50,104 @@ export const isRealReactDevtools = (
39
50
  return Boolean(rdtHook && "getFiberRoots" in rdtHook);
40
51
  };
41
52
 
42
- let isReactRefreshOverride = false;
43
- let injectFnStr: string | undefined = undefined;
44
-
53
+ // HACK: react-refresh's inject wrapper is the only stock hook whose parameter is named
54
+ // "injected", so its string form identifies hooks installed by react-refresh:
55
+ // https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshRuntime.js (injectIntoGlobalHook)
45
56
  export const isReactRefresh = (
46
57
  rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
47
58
  ): boolean => {
48
- if (isReactRefreshOverride) return true;
49
- if (rdtHook && typeof rdtHook.inject === "function") {
50
- injectFnStr = rdtHook.inject.toString();
59
+ if (!rdtHook || typeof rdtHook.inject !== "function") return false;
60
+ if (isRealReactDevtools(rdtHook)) return false;
61
+ try {
62
+ return Function.prototype.toString.call(rdtHook.inject).includes("(injected)");
63
+ } catch {
64
+ return false;
51
65
  }
52
- // https://github.com/facebook/react/blob/8f8b336734d7c807f5aa11b0f31540e63302d789/packages/react-refresh/src/ReactFreshRuntime.js#L459
53
- return Boolean(injectFnStr?.includes("(injected)"));
54
66
  };
55
67
 
56
- export const _onActiveListeners = new Set<() => unknown>();
68
+ export const _onActiveListeners = new Set<ActiveListener>();
57
69
 
58
70
  export const _renderers = new Set<ReactRenderer>();
59
71
 
60
- const rendererInjectListeners = new Set<(renderer: ReactRenderer) => void>();
61
- // re-wrapping inject (e.g. after the hook is replaced) leaves the old
62
- // wrapper in the call chain, so notifications are deduped per renderer
72
+ const rendererInjectListeners = new Set<RendererInjectListener>();
73
+ const rdtHookReplaceListeners = new Set<RDTHookReplaceListener>();
74
+ // HACK: Hook replacement can leave an old inject wrapper in the call chain, so notifications must be deduplicated.
63
75
  const notifiedRenderers = new WeakSet<ReactRenderer>();
64
- let notifyingInject: ReactDevToolsGlobalHook["inject"] | null = null;
76
+ const notifyingInjectByHook = new WeakMap<
77
+ ReactDevToolsGlobalHook,
78
+ ReactDevToolsGlobalHook["inject"]
79
+ >();
80
+
81
+ const notifyActiveListeners = (): void => {
82
+ for (const listener of _onActiveListeners) {
83
+ listener();
84
+ }
85
+ };
86
+
87
+ const notifyRendererInjectListeners = (renderer: ReactRenderer): void => {
88
+ for (const listener of rendererInjectListeners) {
89
+ listener(renderer);
90
+ }
91
+ };
65
92
 
66
- const ensureInjectNotifiesListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
67
- if (rdtHook.inject === notifyingInject) return;
68
- const prevInject = rdtHook.inject;
93
+ const notifyRDTHookReplaceListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
94
+ for (const listener of rdtHookReplaceListeners) {
95
+ listener(rdtHook);
96
+ }
97
+ };
98
+
99
+ const setRendererInjectDispatcher = (rdtHook: ReactDevToolsGlobalHook): void => {
100
+ if (rdtHook.inject === notifyingInjectByHook.get(rdtHook)) return;
101
+ const previousInject = rdtHook.inject;
69
102
  const nextInject = (renderer: ReactRenderer) => {
70
- const rendererId = prevInject.call(rdtHook, renderer);
103
+ const rendererId = previousInject.call(rdtHook, renderer);
71
104
  if (!notifiedRenderers.has(renderer)) {
72
105
  notifiedRenderers.add(renderer);
73
- for (const listener of rendererInjectListeners) {
74
- listener(renderer);
75
- }
106
+ notifyRendererInjectListeners(renderer);
76
107
  }
77
108
  return rendererId;
78
109
  };
79
110
  rdtHook.inject = nextInject;
80
- notifyingInject = nextInject;
111
+ notifyingInjectByHook.set(rdtHook, nextInject);
81
112
  };
82
113
 
83
- /**
84
- * Subscribes to future renderer injections into the DevTools hook. The
85
- * single shared inject wrapper lets multiple consumers (override methods,
86
- * react-refresh, user code) observe renderers without stacking patches
87
- * whose restore order matters. Returns an unsubscribe function.
88
- */
89
- export const onRendererInject = (listener: (renderer: ReactRenderer) => void): Unsubscribe => {
90
- ensureInjectNotifiesListeners(getRDTHook());
114
+ export const onRendererInject = (listener: RendererInjectListener): Unsubscribe => {
115
+ setRendererInjectDispatcher(getRDTHook());
91
116
  rendererInjectListeners.add(listener);
92
- return toUnsubscribe(() => {
117
+ return createUnsubscribe(() => {
93
118
  rendererInjectListeners.delete(listener);
94
119
  });
95
120
  };
96
121
 
97
- export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
122
+ export const onRDTHookReplace = (listener: RDTHookReplaceListener): Unsubscribe => {
123
+ rdtHookReplaceListeners.add(listener);
124
+ return createUnsubscribe(() => {
125
+ rdtHookReplaceListeners.delete(listener);
126
+ });
127
+ };
128
+
129
+ const getRendererMap = (rdtHook: ReactDevToolsGlobalHook): Map<number, ReactRenderer> => {
130
+ if (!(rdtHook.renderers instanceof Map)) {
131
+ rdtHook.renderers = new Map();
132
+ }
133
+ return rdtHook.renderers;
134
+ };
135
+
136
+ const trackInjectedRenderer = (
137
+ rdtHook: ReactDevToolsGlobalHook,
138
+ renderers: Map<number, ReactRenderer>,
139
+ rendererId: number,
140
+ renderer: ReactRenderer,
141
+ ): void => {
142
+ renderers.set(rendererId, renderer);
143
+ _renderers.add(renderer);
144
+ if (!rdtHook._instrumentationIsActive) {
145
+ rdtHook._instrumentationIsActive = true;
146
+ notifyActiveListeners();
147
+ }
148
+ };
149
+
150
+ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
98
151
  if (onActive) {
99
152
  _onActiveListeners.add(onActive);
100
153
  }
@@ -105,20 +158,15 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
105
158
  _instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
106
159
  checkDCE,
107
160
  hasUnsupportedRendererAttached: false,
108
- inject(renderer) {
161
+ inject: (renderer) => {
109
162
  const nextRendererId = ++rendererIdCounter;
110
- renderers.set(nextRendererId, renderer);
111
- _renderers.add(renderer);
112
- if (!rdtHook._instrumentationIsActive) {
113
- rdtHook._instrumentationIsActive = true;
114
- _onActiveListeners.forEach((listener) => listener());
115
- }
163
+ trackInjectedRenderer(rdtHook, renderers, nextRendererId, renderer);
116
164
  return nextRendererId;
117
165
  },
118
- on: NO_OP,
119
- onCommitFiberRoot: NO_OP,
120
- onCommitFiberUnmount: NO_OP,
121
- onPostCommitFiberRoot: NO_OP,
166
+ on: noOp,
167
+ onCommitFiberRoot: noOp,
168
+ onCommitFiberUnmount: noOp,
169
+ onPostCommitFiberRoot: noOp,
122
170
  renderers,
123
171
  supportsFiber: true,
124
172
  supportsFlight: true,
@@ -134,137 +182,110 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
134
182
  if (newHook && typeof newHook === "object") {
135
183
  const ourRenderers = rdtHook.renderers;
136
184
  rdtHook = newHook;
137
- if (ourRenderers.size > 0) {
138
- ourRenderers.forEach((renderer, id) => {
139
- _renderers.add(renderer);
140
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
141
- newHook.renderers.set(id, renderer);
142
- });
185
+ const nextRenderers = getRendererMap(rdtHook);
186
+ ourRenderers.forEach((renderer, rendererId) => {
187
+ _renderers.add(renderer);
188
+ nextRenderers.set(rendererId, renderer);
189
+ });
190
+ if (ourRenderers.size > 0 || rdtHookReplaceListeners.size > 0) {
143
191
  patchRDTHook(onActive);
144
192
  }
193
+ notifyRDTHookReplaceListeners(rdtHook);
145
194
  }
146
195
  },
147
196
  });
148
- // [!] this is a hack for chrome extensions - if we install before React DevTools, we could accidently prevent React DevTools from installing:
149
- // https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-devtools-extensions/src/contentScripts/installHook.js#L30C6-L30C27
150
- // eslint-disable-next-line @typescript-eslint/unbound-method
151
- const originalWindowHasOwnProperty = window.hasOwnProperty;
152
- let hasRanHack = false;
153
- objectDefineProperty(window, "hasOwnProperty", {
154
- configurable: true,
155
- value: function (this: unknown, ...args: [PropertyKey]) {
156
- try {
157
- if (!hasRanHack && args[0] === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
197
+ if (typeof window !== "undefined") {
198
+ // HACK: The DevTools extension uses hasOwnProperty to decide whether it may replace an earlier hook.
199
+ const originalWindowHasOwnProperty = window.hasOwnProperty.bind(window);
200
+ const originalHasOwnPropertyDescriptor = Object.getOwnPropertyDescriptor(
201
+ window,
202
+ "hasOwnProperty",
203
+ );
204
+ let didRunHasOwnPropertyHack = false;
205
+ const restoreWindowHasOwnProperty = (): void => {
206
+ if (originalHasOwnPropertyDescriptor) {
207
+ objectDefineProperty(window, "hasOwnProperty", originalHasOwnPropertyDescriptor);
208
+ } else {
209
+ Reflect.deleteProperty(window, "hasOwnProperty");
210
+ }
211
+ };
212
+ objectDefineProperty(window, "hasOwnProperty", {
213
+ configurable: true,
214
+ value: (propertyKey: PropertyKey) => {
215
+ if (!didRunHasOwnPropertyHack && propertyKey === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
216
+ didRunHasOwnPropertyHack = true;
217
+ restoreWindowHasOwnProperty();
158
218
  globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
159
- // special falsy value to know that we've already installed before
160
- hasRanHack = true;
161
- return -0;
219
+ return false;
162
220
  }
163
- } catch {}
164
- return originalWindowHasOwnProperty.apply(this, args);
165
- },
166
- writable: true,
167
- });
221
+ return originalWindowHasOwnProperty(propertyKey);
222
+ },
223
+ writable: true,
224
+ });
225
+ }
168
226
  } catch {
169
227
  patchRDTHook(onActive);
170
228
  }
171
229
  return rdtHook;
172
230
  };
173
231
 
174
- export const patchRDTHook = (onActive?: () => unknown): void => {
232
+ export const patchRDTHook = (onActive?: ActiveListener): void => {
175
233
  if (onActive) {
176
234
  _onActiveListeners.add(onActive);
177
235
  }
178
- try {
179
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
180
- if (!rdtHook) return;
181
- if (!rdtHook._instrumentationSource) {
182
- rdtHook.checkDCE = checkDCE;
183
- rdtHook.supportsFiber = true;
184
- rdtHook.supportsFlight = true;
185
- rdtHook.hasUnsupportedRendererAttached = false;
186
- rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
187
- rdtHook._instrumentationIsActive = false;
188
- // we need to be careful here (needs to be below _instrumentationSource) else it causes excessive recursion
189
- const isReactDevtools = isRealReactDevtools(rdtHook);
190
- if (!isReactDevtools) {
191
- rdtHook.on = NO_OP;
192
- }
193
- if (rdtHook.renderers.size) {
194
- rdtHook._instrumentationIsActive = true;
195
- _onActiveListeners.forEach((listener) => listener());
196
- return;
197
- }
198
- const prevInject = rdtHook.inject;
199
- const isRefresh = isReactRefresh(rdtHook);
200
- if (isRefresh && !isReactDevtools) {
201
- isReactRefreshOverride = true;
202
- // but since the underlying implementation doens't care,
203
- // it's ok: https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
204
- const injectedRendererId = rdtHook.inject({
205
- scheduleRefresh() {},
206
- } as unknown as ReactRenderer);
207
- if (injectedRendererId) {
208
- rdtHook._instrumentationIsActive = true;
209
- }
210
- }
211
- rdtHook.inject = (renderer) => {
212
- const rendererId = prevInject(renderer);
213
- _renderers.add(renderer);
214
- if (isRefresh) {
215
- // react refresh doesn't inject this properly
216
- // https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
217
- rdtHook.renderers.set(rendererId, renderer);
218
- }
219
- rdtHook._instrumentationIsActive = true;
220
- _onActiveListeners.forEach((listener) => listener());
221
- return rendererId;
222
- };
236
+ let didNotifyActiveListeners = false;
237
+ const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
238
+ if (!rdtHook) return;
239
+ const renderers = getRendererMap(rdtHook);
240
+ if (!rdtHook._instrumentationSource) {
241
+ rdtHook.checkDCE = checkDCE;
242
+ rdtHook.supportsFiber = true;
243
+ rdtHook.supportsFlight = true;
244
+ rdtHook.hasUnsupportedRendererAttached = false;
245
+ rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
246
+ rdtHook._instrumentationIsActive = false;
247
+ // HACK: DevTools detection must happen after setting the source to avoid recursive patching.
248
+ const isReactDevtools = isRealReactDevtools(rdtHook);
249
+ if (!isReactDevtools) {
250
+ rdtHook.on = noOp;
223
251
  }
224
- if (
225
- rdtHook.renderers.size ||
226
- rdtHook._instrumentationIsActive ||
227
- // depending on this to inject is unsafe, since inject could occur before and we wouldn't know
228
- isReactRefresh()
229
- ) {
230
- onActive?.();
252
+ if (renderers.size) {
253
+ renderers.forEach((renderer) => _renderers.add(renderer));
254
+ rdtHook._instrumentationIsActive = true;
255
+ notifyActiveListeners();
256
+ didNotifyActiveListeners = true;
257
+ } else if (!isReactDevtools && isReactRefresh(rdtHook)) {
258
+ // HACK: react-refresh's stub inject never records renderers, so a React app
259
+ // that injected before bippy loaded is undetectable through the renderers map.
260
+ // A react-refresh hook implies a dev renderer, so activate immediately.
261
+ rdtHook._instrumentationIsActive = true;
262
+ notifyActiveListeners();
263
+ didNotifyActiveListeners = true;
231
264
  }
232
- } catch {}
265
+ const previousInject = rdtHook.inject;
266
+ rdtHook.inject = (renderer) => {
267
+ const rendererId = previousInject.call(rdtHook, renderer);
268
+ trackInjectedRenderer(rdtHook, renderers, rendererId, renderer);
269
+ return rendererId;
270
+ };
271
+ }
272
+ if (!didNotifyActiveListeners && (renderers.size || rdtHook._instrumentationIsActive)) {
273
+ onActive?.();
274
+ }
233
275
  };
234
276
 
235
277
  export const hasRDTHook = (): boolean => {
236
- return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
278
+ return Object.hasOwn(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
237
279
  };
238
280
 
239
281
  /**
240
282
  * Returns the current React DevTools global hook.
241
283
  */
242
- export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
284
+ export const getRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
243
285
  if (!hasRDTHook()) {
244
286
  return installRDTHook(onActive);
245
287
  }
246
288
 
247
289
  patchRDTHook(onActive);
248
- // must exist at this point
249
- return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ as ReactDevToolsGlobalHook;
250
- };
251
-
252
- export const isClientEnvironment = (): boolean => {
253
- return Boolean(
254
- typeof window !== "undefined" &&
255
- // eslint-disable-next-line @typescript-eslint/unbound-method
256
- (window.document?.createElement || window.navigator?.product === "ReactNative"),
257
- );
258
- };
259
-
260
- /**
261
- * Usually used purely for side effect
262
- */
263
- export const safelyInstallRDTHook = () => {
264
- try {
265
- // __REACT_DEVTOOLS_GLOBAL_HOOK__ must exist before React is ever executed
266
- if (isClientEnvironment()) {
267
- getRDTHook();
268
- }
269
- } catch {}
290
+ return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? installRDTHook(onActive);
270
291
  };