bippy 0.6.1-dev.61475ed → 0.6.1-dev.b88fcb4

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 (64) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -48
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +45 -59
  5. package/dist/core.d.ts +45 -59
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +9 -0
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +9 -0
  11. package/dist/errors.d.cts +465 -0
  12. package/dist/errors.d.ts +465 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +11 -3
  15. package/dist/index.d.ts +11 -3
  16. package/dist/index.iife.js +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/install-hook-only.cjs +1 -1
  19. package/dist/install-hook-only.d.cts +9 -1
  20. package/dist/install-hook-only.d.ts +9 -1
  21. package/dist/install-hook-only.iife.js +1 -1
  22. package/dist/rdt-hook.cjs +1 -1
  23. package/dist/rdt-hook.js +1 -1
  24. package/dist/source.cjs +14 -4
  25. package/dist/source.d.cts +27 -9
  26. package/dist/source.d.ts +27 -9
  27. package/dist/source.js +14 -4
  28. package/package.json +22 -16
  29. package/src/core.ts +412 -316
  30. package/src/errors.ts +99 -0
  31. package/src/rdt-hook.ts +163 -151
  32. package/src/react-internals/generated/react-work-tags.ts +314 -0
  33. package/src/react-internals/index.ts +67 -0
  34. package/src/react-internals/semver.ts +78 -0
  35. package/src/react-internals/types.ts +269 -0
  36. package/src/source/constants.ts +1 -1
  37. package/src/source/error-stack.ts +11 -0
  38. package/src/source/get-display-name-from-source.ts +1 -1
  39. package/src/source/get-source.ts +4 -3
  40. package/src/source/index.ts +9 -1
  41. package/src/source/inspect-hooks.ts +199 -165
  42. package/src/source/owner-stack.ts +87 -113
  43. package/src/source/parse-debug-stack.ts +4 -4
  44. package/src/source/parse-hook-names.ts +1 -1
  45. package/src/source/parse-stack.ts +1 -1
  46. package/src/source/symbolication.ts +476 -88
  47. package/dist/get-source.cjs +0 -19
  48. package/dist/get-source.js +0 -19
  49. package/dist/react-refresh.cjs +0 -9
  50. package/dist/react-refresh.d.cts +0 -66
  51. package/dist/react-refresh.d.ts +0 -66
  52. package/dist/react-refresh.js +0 -9
  53. package/dist/unsubscribe.d.cts +0 -298
  54. package/dist/unsubscribe.d.ts +0 -298
  55. package/src/react-refresh/constants.ts +0 -9
  56. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  57. package/src/react-refresh/index.ts +0 -173
  58. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  59. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  60. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  61. package/src/react-refresh/types.ts +0 -7
  62. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  63. package/src/types.ts +0 -438
  64. package/src/unsubscribe.ts +0 -17
package/src/errors.ts ADDED
@@ -0,0 +1,99 @@
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 BippyInstrumentationError extends BippyError {
9
+ readonly callbackName: string;
10
+ readonly instrumentationName: string;
11
+
12
+ constructor(instrumentationName: string, callbackName: string, cause: unknown) {
13
+ super(
14
+ `Bippy instrumentation "${instrumentationName}" callback "${callbackName}" failed`,
15
+ cause,
16
+ );
17
+ this.name = "BippyInstrumentationError";
18
+ this.callbackName = callbackName;
19
+ this.instrumentationName = instrumentationName;
20
+ }
21
+ }
22
+
23
+ export class BippyReactDevToolsError extends BippyError {
24
+ readonly callbackName: string;
25
+
26
+ constructor(callbackName: string, cause: unknown) {
27
+ super(`React DevTools callback "${callbackName}" failed`, cause);
28
+ this.name = "BippyReactDevToolsError";
29
+ this.callbackName = callbackName;
30
+ }
31
+ }
32
+
33
+ export class BippyHookListenerError extends BippyError {
34
+ readonly listenerName: string;
35
+
36
+ constructor(listenerName: string, cause: unknown) {
37
+ super(`Bippy hook listener "${listenerName}" failed`, cause);
38
+ this.name = "BippyHookListenerError";
39
+ this.listenerName = listenerName;
40
+ }
41
+ }
42
+
43
+ export class BippyHookInstallationError extends BippyError {
44
+ constructor(cause: unknown) {
45
+ super("Bippy could not install the React DevTools hook", cause);
46
+ this.name = "BippyHookInstallationError";
47
+ }
48
+ }
49
+
50
+ export class BippyReactBuildError extends BippyError {
51
+ constructor(message: string, cause?: unknown) {
52
+ super(message, cause);
53
+ this.name = "BippyReactBuildError";
54
+ }
55
+ }
56
+
57
+ export class BippyHookInspectionError extends BippyError {
58
+ constructor(message: string, cause?: unknown) {
59
+ super(message, cause);
60
+ this.name = "BippyHookInspectionError";
61
+ }
62
+ }
63
+
64
+ export class BippyUnsupportedHookError extends BippyHookInspectionError {
65
+ constructor(message: string, cause?: unknown) {
66
+ super(message, cause);
67
+ this.name = "BippyUnsupportedHookError";
68
+ }
69
+ }
70
+
71
+ export class BippyHookRenderError extends BippyHookInspectionError {
72
+ constructor(message: string, cause?: unknown) {
73
+ super(message, cause);
74
+ this.name = "BippyHookRenderError";
75
+ }
76
+ }
77
+
78
+ export class BippySourceMapError extends BippyError {
79
+ constructor(message: string, cause?: unknown) {
80
+ super(message, cause);
81
+ this.name = "BippySourceMapError";
82
+ }
83
+ }
84
+
85
+ interface BippyErrorFactory {
86
+ (cause: unknown): BippyError;
87
+ }
88
+
89
+ export const runWithBippyError = <Result>(
90
+ callback: () => Result,
91
+ createError: BippyErrorFactory,
92
+ ): Result => {
93
+ try {
94
+ return callback();
95
+ } catch (cause) {
96
+ if (cause instanceof BippyError) throw cause;
97
+ throw createError(cause);
98
+ }
99
+ };
package/src/rdt-hook.ts CHANGED
@@ -1,28 +1,50 @@
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 {
4
+ BippyHookInstallationError,
5
+ BippyHookListenerError,
6
+ BippyReactBuildError,
7
+ runWithBippyError,
8
+ } from "./errors.js";
9
+ import type { ReactDevToolsGlobalHook, ReactRenderer } from "./react-internals/index.js";
10
+
11
+ export interface Unsubscribe extends Disposable {
12
+ (): void;
13
+ }
14
+
15
+ interface ActiveListener {
16
+ (): unknown;
17
+ }
18
+
19
+ interface RendererInjectListener {
20
+ (renderer: ReactRenderer): void;
21
+ }
22
+
23
+ interface RDTHookReplaceListener {
24
+ (rdtHook: ReactDevToolsGlobalHook): void;
25
+ }
8
26
 
9
27
  export const version = process.env.VERSION;
10
28
  export const BIPPY_INSTRUMENTATION_STRING = `bippy-${version}`;
11
29
 
12
30
  const objectDefineProperty = Object.defineProperty;
13
- // eslint-disable-next-line @typescript-eslint/unbound-method
14
- const objectHasOwnProperty = Object.prototype.hasOwnProperty;
15
31
 
16
- const NO_OP = () => {
17
- /**/
32
+ const noOp = (): void => {};
33
+
34
+ export const createUnsubscribe = (unsubscribe: () => void): Unsubscribe =>
35
+ Object.assign(unsubscribe, { [Symbol.dispose]: unsubscribe });
36
+
37
+ const runHookListener = (listenerName: string, callback: () => unknown): void => {
38
+ runWithBippyError(callback, (cause) => new BippyHookListenerError(listenerName, cause));
18
39
  };
19
40
 
20
41
  const checkDCE = (functionToCheck: unknown): void => {
21
42
  try {
22
43
  const code = Function.prototype.toString.call(functionToCheck);
23
- if (code.indexOf("^_^") > -1) {
44
+ if (code.includes("^_^")) {
45
+ // HACK: React DevTools reports failed dead-code elimination asynchronously.
24
46
  setTimeout(() => {
25
- throw new Error(
47
+ throw new BippyReactBuildError(
26
48
  "React is running in production mode, but dead code " +
27
49
  "elimination has not been applied. Read how to correctly " +
28
50
  "configure React for production: " +
@@ -39,62 +61,75 @@ export const isRealReactDevtools = (
39
61
  return Boolean(rdtHook && "getFiberRoots" in rdtHook);
40
62
  };
41
63
 
42
- let isReactRefreshOverride = false;
43
- let injectFnStr: string | undefined = undefined;
64
+ export const _onActiveListeners = new Set<ActiveListener>();
44
65
 
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();
66
+ export const _renderers = new Set<ReactRenderer>();
67
+
68
+ const rendererInjectListeners = new Set<RendererInjectListener>();
69
+ const rdtHookReplaceListeners = new Set<RDTHookReplaceListener>();
70
+ // HACK: Hook replacement can leave an old inject wrapper in the call chain, so notifications must be deduplicated.
71
+ const notifiedRenderers = new WeakSet<ReactRenderer>();
72
+ const notifyingInjectByHook = new WeakMap<
73
+ ReactDevToolsGlobalHook,
74
+ ReactDevToolsGlobalHook["inject"]
75
+ >();
76
+
77
+ const notifyActiveListeners = (): void => {
78
+ for (const listener of _onActiveListeners) {
79
+ runHookListener("onActive", listener);
51
80
  }
52
- // https://github.com/facebook/react/blob/8f8b336734d7c807f5aa11b0f31540e63302d789/packages/react-refresh/src/ReactFreshRuntime.js#L459
53
- return Boolean(injectFnStr?.includes("(injected)"));
54
81
  };
55
82
 
56
- export const _onActiveListeners = new Set<() => unknown>();
57
-
58
- export const _renderers = new Set<ReactRenderer>();
83
+ const notifyRendererInjectListeners = (renderer: ReactRenderer): void => {
84
+ for (const listener of rendererInjectListeners) {
85
+ runHookListener("onRendererInject", () => listener(renderer));
86
+ }
87
+ };
59
88
 
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;
89
+ const notifyRDTHookReplaceListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
90
+ for (const listener of rdtHookReplaceListeners) {
91
+ runHookListener("onRDTHookReplace", () => listener(rdtHook));
92
+ }
93
+ };
65
94
 
66
- const ensureInjectNotifiesListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
67
- if (rdtHook.inject === notifyingInject) return;
68
- const prevInject = rdtHook.inject;
95
+ const setRendererInjectDispatcher = (rdtHook: ReactDevToolsGlobalHook): void => {
96
+ if (rdtHook.inject === notifyingInjectByHook.get(rdtHook)) return;
97
+ const previousInject = rdtHook.inject;
69
98
  const nextInject = (renderer: ReactRenderer) => {
70
- const rendererId = prevInject.call(rdtHook, renderer);
99
+ const rendererId = previousInject.call(rdtHook, renderer);
71
100
  if (!notifiedRenderers.has(renderer)) {
72
101
  notifiedRenderers.add(renderer);
73
- for (const listener of rendererInjectListeners) {
74
- listener(renderer);
75
- }
102
+ notifyRendererInjectListeners(renderer);
76
103
  }
77
104
  return rendererId;
78
105
  };
79
106
  rdtHook.inject = nextInject;
80
- notifyingInject = nextInject;
107
+ notifyingInjectByHook.set(rdtHook, nextInject);
81
108
  };
82
109
 
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());
110
+ export const onRendererInject = (listener: RendererInjectListener): Unsubscribe => {
111
+ setRendererInjectDispatcher(getRDTHook());
91
112
  rendererInjectListeners.add(listener);
92
- return toUnsubscribe(() => {
113
+ return createUnsubscribe(() => {
93
114
  rendererInjectListeners.delete(listener);
94
115
  });
95
116
  };
96
117
 
97
- export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
118
+ export const onRDTHookReplace = (listener: RDTHookReplaceListener): Unsubscribe => {
119
+ rdtHookReplaceListeners.add(listener);
120
+ return createUnsubscribe(() => {
121
+ rdtHookReplaceListeners.delete(listener);
122
+ });
123
+ };
124
+
125
+ const getRendererMap = (rdtHook: ReactDevToolsGlobalHook): Map<number, ReactRenderer> => {
126
+ if (!(rdtHook.renderers instanceof Map)) {
127
+ rdtHook.renderers = new Map();
128
+ }
129
+ return rdtHook.renderers;
130
+ };
131
+
132
+ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
98
133
  if (onActive) {
99
134
  _onActiveListeners.add(onActive);
100
135
  }
@@ -105,20 +140,20 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
105
140
  _instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
106
141
  checkDCE,
107
142
  hasUnsupportedRendererAttached: false,
108
- inject(renderer) {
143
+ inject: (renderer) => {
109
144
  const nextRendererId = ++rendererIdCounter;
110
145
  renderers.set(nextRendererId, renderer);
111
146
  _renderers.add(renderer);
112
147
  if (!rdtHook._instrumentationIsActive) {
113
148
  rdtHook._instrumentationIsActive = true;
114
- _onActiveListeners.forEach((listener) => listener());
149
+ notifyActiveListeners();
115
150
  }
116
151
  return nextRendererId;
117
152
  },
118
- on: NO_OP,
119
- onCommitFiberRoot: NO_OP,
120
- onCommitFiberUnmount: NO_OP,
121
- onPostCommitFiberRoot: NO_OP,
153
+ on: noOp,
154
+ onCommitFiberRoot: noOp,
155
+ onCommitFiberUnmount: noOp,
156
+ onPostCommitFiberRoot: noOp,
122
157
  renderers,
123
158
  supportsFiber: true,
124
159
  supportsFlight: true,
@@ -134,137 +169,114 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
134
169
  if (newHook && typeof newHook === "object") {
135
170
  const ourRenderers = rdtHook.renderers;
136
171
  rdtHook = newHook;
172
+ const nextRenderers = getRendererMap(rdtHook);
137
173
  if (ourRenderers.size > 0) {
138
- ourRenderers.forEach((renderer, id) => {
174
+ ourRenderers.forEach((renderer, rendererId) => {
139
175
  _renderers.add(renderer);
140
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
141
- newHook.renderers.set(id, renderer);
176
+ nextRenderers.set(rendererId, renderer);
142
177
  });
178
+ }
179
+ if (ourRenderers.size > 0 || rdtHookReplaceListeners.size > 0) {
143
180
  patchRDTHook(onActive);
144
181
  }
182
+ notifyRDTHookReplaceListeners(rdtHook);
145
183
  }
146
184
  },
147
185
  });
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__") {
186
+ if (typeof window !== "undefined") {
187
+ // HACK: The DevTools extension uses hasOwnProperty to decide whether it may replace an earlier hook.
188
+ const originalWindowHasOwnProperty = window.hasOwnProperty.bind(window);
189
+ const originalHasOwnPropertyDescriptor = Object.getOwnPropertyDescriptor(
190
+ window,
191
+ "hasOwnProperty",
192
+ );
193
+ let didRunHasOwnPropertyHack = false;
194
+ const restoreWindowHasOwnProperty = (): void => {
195
+ if (originalHasOwnPropertyDescriptor) {
196
+ objectDefineProperty(window, "hasOwnProperty", originalHasOwnPropertyDescriptor);
197
+ } else {
198
+ Reflect.deleteProperty(window, "hasOwnProperty");
199
+ }
200
+ };
201
+ objectDefineProperty(window, "hasOwnProperty", {
202
+ configurable: true,
203
+ value: (...propertyKeys: [PropertyKey]) => {
204
+ if (!didRunHasOwnPropertyHack && propertyKeys[0] === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
205
+ didRunHasOwnPropertyHack = true;
206
+ restoreWindowHasOwnProperty();
158
207
  globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
159
- // special falsy value to know that we've already installed before
160
- hasRanHack = true;
161
- return -0;
208
+ return false;
162
209
  }
163
- } catch {}
164
- return originalWindowHasOwnProperty.apply(this, args);
165
- },
166
- writable: true,
167
- });
210
+ return originalWindowHasOwnProperty(...propertyKeys);
211
+ },
212
+ writable: true,
213
+ });
214
+ }
168
215
  } catch {
169
216
  patchRDTHook(onActive);
170
217
  }
171
218
  return rdtHook;
172
219
  };
173
220
 
174
- export const patchRDTHook = (onActive?: () => unknown): void => {
221
+ export const patchRDTHook = (onActive?: ActiveListener): void => {
175
222
  if (onActive) {
176
223
  _onActiveListeners.add(onActive);
177
224
  }
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
- };
225
+ let didNotifyActiveListeners = false;
226
+ const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
227
+ if (!rdtHook) return;
228
+ const renderers = getRendererMap(rdtHook);
229
+ if (!rdtHook._instrumentationSource) {
230
+ rdtHook.checkDCE = checkDCE;
231
+ rdtHook.supportsFiber = true;
232
+ rdtHook.supportsFlight = true;
233
+ rdtHook.hasUnsupportedRendererAttached = false;
234
+ rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
235
+ rdtHook._instrumentationIsActive = false;
236
+ // HACK: DevTools detection must happen after setting the source to avoid recursive patching.
237
+ const isReactDevtools = isRealReactDevtools(rdtHook);
238
+ if (!isReactDevtools) {
239
+ rdtHook.on = noOp;
223
240
  }
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?.();
241
+ if (renderers.size) {
242
+ renderers.forEach((renderer) => _renderers.add(renderer));
243
+ rdtHook._instrumentationIsActive = true;
244
+ notifyActiveListeners();
245
+ didNotifyActiveListeners = true;
231
246
  }
232
- } catch {}
247
+ const previousInject = rdtHook.inject;
248
+ rdtHook.inject = (renderer) => {
249
+ const rendererId = previousInject.call(rdtHook, renderer);
250
+ _renderers.add(renderer);
251
+ renderers.set(rendererId, renderer);
252
+ if (!rdtHook._instrumentationIsActive) {
253
+ rdtHook._instrumentationIsActive = true;
254
+ notifyActiveListeners();
255
+ }
256
+ return rendererId;
257
+ };
258
+ }
259
+ if (!didNotifyActiveListeners && (renderers.size || rdtHook._instrumentationIsActive)) {
260
+ if (onActive) runHookListener("onActive", onActive);
261
+ }
233
262
  };
234
263
 
235
264
  export const hasRDTHook = (): boolean => {
236
- return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
265
+ return Object.hasOwn(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
237
266
  };
238
267
 
239
268
  /**
240
269
  * Returns the current React DevTools global hook.
241
270
  */
242
- export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
271
+ export const getRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
243
272
  if (!hasRDTHook()) {
244
273
  return installRDTHook(onActive);
245
274
  }
246
275
 
247
276
  patchRDTHook(onActive);
248
- // must exist at this point
249
- return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ as ReactDevToolsGlobalHook;
277
+ return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? installRDTHook(onActive);
250
278
  };
251
279
 
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 {}
280
+ export const safelyInstallRDTHook = (): void => {
281
+ runWithBippyError(getRDTHook, (cause) => new BippyHookInstallationError(cause));
270
282
  };