bippy 0.6.1 → 0.7.0

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 -463
  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 +153 -3
  9. package/dist/index.d.ts +153 -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 +353 -685
  23. package/src/errors.ts +34 -0
  24. package/src/index.ts +1 -0
  25. package/src/install-hook-only.ts +2 -2
  26. package/src/rdt-hook.ts +160 -161
  27. package/src/react-internals/generated/react-work-tags.ts +318 -0
  28. package/src/react-internals/index.ts +67 -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 +119 -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,3 @@
1
- import { safelyInstallRDTHook } from "./rdt-hook.js";
1
+ import { getRDTHook } from "./rdt-hook.js";
2
2
 
3
- safelyInstallRDTHook();
3
+ getRDTHook();
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,89 @@ 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;
53
+ export const _onActiveListeners = new Set<ActiveListener>();
44
54
 
45
- export const isReactRefresh = (
46
- rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
47
- ): boolean => {
48
- if (isReactRefreshOverride) return true;
49
- if (rdtHook && typeof rdtHook.inject === "function") {
50
- injectFnStr = rdtHook.inject.toString();
55
+ export const _renderers = new Set<ReactRenderer>();
56
+
57
+ const rendererInjectListeners = new Set<RendererInjectListener>();
58
+ const rdtHookReplaceListeners = new Set<RDTHookReplaceListener>();
59
+ // HACK: Hook replacement can leave an old inject wrapper in the call chain, so notifications must be deduplicated.
60
+ const notifiedRenderers = new WeakSet<ReactRenderer>();
61
+ const notifyingInjectByHook = new WeakMap<
62
+ ReactDevToolsGlobalHook,
63
+ ReactDevToolsGlobalHook["inject"]
64
+ >();
65
+
66
+ const notifyActiveListeners = (): void => {
67
+ for (const listener of _onActiveListeners) {
68
+ listener();
51
69
  }
52
- // https://github.com/facebook/react/blob/8f8b336734d7c807f5aa11b0f31540e63302d789/packages/react-refresh/src/ReactFreshRuntime.js#L459
53
- return Boolean(injectFnStr?.includes("(injected)"));
54
70
  };
55
71
 
56
- export const _onActiveListeners = new Set<() => unknown>();
57
-
58
- export const _renderers = new Set<ReactRenderer>();
72
+ const notifyRendererInjectListeners = (renderer: ReactRenderer): void => {
73
+ for (const listener of rendererInjectListeners) {
74
+ listener(renderer);
75
+ }
76
+ };
59
77
 
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
63
- const notifiedRenderers = new WeakSet<ReactRenderer>();
64
- let notifyingInject: ReactDevToolsGlobalHook["inject"] | null = null;
78
+ const notifyRDTHookReplaceListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
79
+ for (const listener of rdtHookReplaceListeners) {
80
+ listener(rdtHook);
81
+ }
82
+ };
65
83
 
66
- const ensureInjectNotifiesListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
67
- if (rdtHook.inject === notifyingInject) return;
68
- const prevInject = rdtHook.inject;
84
+ const setRendererInjectDispatcher = (rdtHook: ReactDevToolsGlobalHook): void => {
85
+ if (rdtHook.inject === notifyingInjectByHook.get(rdtHook)) return;
86
+ const previousInject = rdtHook.inject;
69
87
  const nextInject = (renderer: ReactRenderer) => {
70
- const rendererId = prevInject.call(rdtHook, renderer);
88
+ const rendererId = previousInject.call(rdtHook, renderer);
71
89
  if (!notifiedRenderers.has(renderer)) {
72
90
  notifiedRenderers.add(renderer);
73
- for (const listener of rendererInjectListeners) {
74
- listener(renderer);
75
- }
91
+ notifyRendererInjectListeners(renderer);
76
92
  }
77
93
  return rendererId;
78
94
  };
79
95
  rdtHook.inject = nextInject;
80
- notifyingInject = nextInject;
96
+ notifyingInjectByHook.set(rdtHook, nextInject);
81
97
  };
82
98
 
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());
99
+ export const onRendererInject = (listener: RendererInjectListener): Unsubscribe => {
100
+ setRendererInjectDispatcher(getRDTHook());
91
101
  rendererInjectListeners.add(listener);
92
- return toUnsubscribe(() => {
102
+ return createUnsubscribe(() => {
93
103
  rendererInjectListeners.delete(listener);
94
104
  });
95
105
  };
96
106
 
97
- export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
107
+ export const onRDTHookReplace = (listener: RDTHookReplaceListener): Unsubscribe => {
108
+ rdtHookReplaceListeners.add(listener);
109
+ return createUnsubscribe(() => {
110
+ rdtHookReplaceListeners.delete(listener);
111
+ });
112
+ };
113
+
114
+ const getRendererMap = (rdtHook: ReactDevToolsGlobalHook): Map<number, ReactRenderer> => {
115
+ if (!(rdtHook.renderers instanceof Map)) {
116
+ rdtHook.renderers = new Map();
117
+ }
118
+ return rdtHook.renderers;
119
+ };
120
+
121
+ const trackInjectedRenderer = (
122
+ rdtHook: ReactDevToolsGlobalHook,
123
+ renderers: Map<number, ReactRenderer>,
124
+ rendererId: number,
125
+ renderer: ReactRenderer,
126
+ ): void => {
127
+ renderers.set(rendererId, renderer);
128
+ _renderers.add(renderer);
129
+ if (!rdtHook._instrumentationIsActive) {
130
+ rdtHook._instrumentationIsActive = true;
131
+ notifyActiveListeners();
132
+ }
133
+ };
134
+
135
+ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
98
136
  if (onActive) {
99
137
  _onActiveListeners.add(onActive);
100
138
  }
@@ -105,20 +143,15 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
105
143
  _instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
106
144
  checkDCE,
107
145
  hasUnsupportedRendererAttached: false,
108
- inject(renderer) {
146
+ inject: (renderer) => {
109
147
  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
- }
148
+ trackInjectedRenderer(rdtHook, renderers, nextRendererId, renderer);
116
149
  return nextRendererId;
117
150
  },
118
- on: NO_OP,
119
- onCommitFiberRoot: NO_OP,
120
- onCommitFiberUnmount: NO_OP,
121
- onPostCommitFiberRoot: NO_OP,
151
+ on: noOp,
152
+ onCommitFiberRoot: noOp,
153
+ onCommitFiberUnmount: noOp,
154
+ onPostCommitFiberRoot: noOp,
122
155
  renderers,
123
156
  supportsFiber: true,
124
157
  supportsFlight: true,
@@ -134,137 +167,103 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
134
167
  if (newHook && typeof newHook === "object") {
135
168
  const ourRenderers = rdtHook.renderers;
136
169
  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
- });
170
+ const nextRenderers = getRendererMap(rdtHook);
171
+ ourRenderers.forEach((renderer, rendererId) => {
172
+ _renderers.add(renderer);
173
+ nextRenderers.set(rendererId, renderer);
174
+ });
175
+ if (ourRenderers.size > 0 || rdtHookReplaceListeners.size > 0) {
143
176
  patchRDTHook(onActive);
144
177
  }
178
+ notifyRDTHookReplaceListeners(rdtHook);
145
179
  }
146
180
  },
147
181
  });
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__") {
182
+ if (typeof window !== "undefined") {
183
+ // HACK: The DevTools extension uses hasOwnProperty to decide whether it may replace an earlier hook.
184
+ const originalWindowHasOwnProperty = window.hasOwnProperty.bind(window);
185
+ const originalHasOwnPropertyDescriptor = Object.getOwnPropertyDescriptor(
186
+ window,
187
+ "hasOwnProperty",
188
+ );
189
+ let didRunHasOwnPropertyHack = false;
190
+ const restoreWindowHasOwnProperty = (): void => {
191
+ if (originalHasOwnPropertyDescriptor) {
192
+ objectDefineProperty(window, "hasOwnProperty", originalHasOwnPropertyDescriptor);
193
+ } else {
194
+ Reflect.deleteProperty(window, "hasOwnProperty");
195
+ }
196
+ };
197
+ objectDefineProperty(window, "hasOwnProperty", {
198
+ configurable: true,
199
+ value: (propertyKey: PropertyKey) => {
200
+ if (!didRunHasOwnPropertyHack && propertyKey === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
201
+ didRunHasOwnPropertyHack = true;
202
+ restoreWindowHasOwnProperty();
158
203
  globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
159
- // special falsy value to know that we've already installed before
160
- hasRanHack = true;
161
- return -0;
204
+ return false;
162
205
  }
163
- } catch {}
164
- return originalWindowHasOwnProperty.apply(this, args);
165
- },
166
- writable: true,
167
- });
206
+ return originalWindowHasOwnProperty(propertyKey);
207
+ },
208
+ writable: true,
209
+ });
210
+ }
168
211
  } catch {
169
212
  patchRDTHook(onActive);
170
213
  }
171
214
  return rdtHook;
172
215
  };
173
216
 
174
- export const patchRDTHook = (onActive?: () => unknown): void => {
217
+ export const patchRDTHook = (onActive?: ActiveListener): void => {
175
218
  if (onActive) {
176
219
  _onActiveListeners.add(onActive);
177
220
  }
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
- };
221
+ let didNotifyActiveListeners = false;
222
+ const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
223
+ if (!rdtHook) return;
224
+ const renderers = getRendererMap(rdtHook);
225
+ if (!rdtHook._instrumentationSource) {
226
+ rdtHook.checkDCE = checkDCE;
227
+ rdtHook.supportsFiber = true;
228
+ rdtHook.supportsFlight = true;
229
+ rdtHook.hasUnsupportedRendererAttached = false;
230
+ rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
231
+ rdtHook._instrumentationIsActive = false;
232
+ // HACK: DevTools detection must happen after setting the source to avoid recursive patching.
233
+ const isReactDevtools = isRealReactDevtools(rdtHook);
234
+ if (!isReactDevtools) {
235
+ rdtHook.on = noOp;
223
236
  }
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?.();
237
+ if (renderers.size) {
238
+ renderers.forEach((renderer) => _renderers.add(renderer));
239
+ rdtHook._instrumentationIsActive = true;
240
+ notifyActiveListeners();
241
+ didNotifyActiveListeners = true;
231
242
  }
232
- } catch {}
243
+ const previousInject = rdtHook.inject;
244
+ rdtHook.inject = (renderer) => {
245
+ const rendererId = previousInject.call(rdtHook, renderer);
246
+ trackInjectedRenderer(rdtHook, renderers, rendererId, renderer);
247
+ return rendererId;
248
+ };
249
+ }
250
+ if (!didNotifyActiveListeners && (renderers.size || rdtHook._instrumentationIsActive)) {
251
+ onActive?.();
252
+ }
233
253
  };
234
254
 
235
255
  export const hasRDTHook = (): boolean => {
236
- return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
256
+ return Object.hasOwn(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
237
257
  };
238
258
 
239
259
  /**
240
260
  * Returns the current React DevTools global hook.
241
261
  */
242
- export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
262
+ export const getRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
243
263
  if (!hasRDTHook()) {
244
264
  return installRDTHook(onActive);
245
265
  }
246
266
 
247
267
  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 {}
268
+ return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? installRDTHook(onActive);
270
269
  };