bippy 0.6.1-dev.3a24abf → 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 (48) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -1
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +39 -41
  5. package/dist/core.d.ts +39 -41
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +1 -1
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +1 -1
  11. package/dist/{types.d.cts → errors.d.cts} +205 -31
  12. package/dist/{types.d.ts → errors.d.ts} +205 -31
  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 +8 -0
  20. package/dist/install-hook-only.d.ts +8 -0
  21. package/dist/install-hook-only.iife.js +1 -1
  22. package/dist/install-hook-only.js +1 -1
  23. package/dist/rdt-hook.cjs +1 -1
  24. package/dist/rdt-hook.js +1 -1
  25. package/dist/source.cjs +13 -13
  26. package/dist/source.d.cts +27 -9
  27. package/dist/source.d.ts +27 -9
  28. package/dist/source.js +13 -13
  29. package/package.json +10 -6
  30. package/src/core.ts +269 -224
  31. package/src/errors.ts +99 -0
  32. package/src/rdt-hook.ts +154 -99
  33. package/src/react-internals/generated/react-work-tags.ts +314 -0
  34. package/src/react-internals/index.ts +67 -0
  35. package/src/react-internals/semver.ts +78 -0
  36. package/src/{types.ts → react-internals/types.ts} +13 -3
  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 +2 -2
  40. package/src/source/index.ts +9 -1
  41. package/src/source/inspect-hooks.ts +199 -165
  42. package/src/source/owner-stack.ts +26 -39
  43. package/src/source/parse-debug-stack.ts +4 -4
  44. package/src/source/parse-hook-names.ts +1 -1
  45. package/src/source/symbolication.ts +468 -101
  46. package/src/generated/react-work-tags.ts +0 -169
  47. package/src/react-internals.ts +0 -67
  48. 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,23 +1,50 @@
1
1
  // This module must load before React so renderers can inject into the hook.
2
2
 
3
- import type { ReactDevToolsGlobalHook, ReactRenderer } from "./types.js";
4
- 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
+ }
5
26
 
6
27
  export const version = process.env.VERSION;
7
28
  export const BIPPY_INSTRUMENTATION_STRING = `bippy-${version}`;
8
29
 
9
30
  const objectDefineProperty = Object.defineProperty;
10
- // eslint-disable-next-line @typescript-eslint/unbound-method
11
- const objectHasOwnProperty = Object.prototype.hasOwnProperty;
12
31
 
13
- const NO_OP = (): void => {};
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));
39
+ };
14
40
 
15
41
  const checkDCE = (functionToCheck: unknown): void => {
16
42
  try {
17
43
  const code = Function.prototype.toString.call(functionToCheck);
18
44
  if (code.includes("^_^")) {
45
+ // HACK: React DevTools reports failed dead-code elimination asynchronously.
19
46
  setTimeout(() => {
20
- throw new Error(
47
+ throw new BippyReactBuildError(
21
48
  "React is running in production mode, but dead code " +
22
49
  "elimination has not been applied. Read how to correctly " +
23
50
  "configure React for production: " +
@@ -34,49 +61,75 @@ export const isRealReactDevtools = (
34
61
  return Boolean(rdtHook && "getFiberRoots" in rdtHook);
35
62
  };
36
63
 
37
- export const _onActiveListeners = new Set<() => unknown>();
64
+ export const _onActiveListeners = new Set<ActiveListener>();
38
65
 
39
66
  export const _renderers = new Set<ReactRenderer>();
40
67
 
41
- const rendererInjectListeners = new Set<(renderer: ReactRenderer) => void>();
68
+ const rendererInjectListeners = new Set<RendererInjectListener>();
69
+ const rdtHookReplaceListeners = new Set<RDTHookReplaceListener>();
42
70
  // HACK: Hook replacement can leave an old inject wrapper in the call chain, so notifications must be deduplicated.
43
71
  const notifiedRenderers = new WeakSet<ReactRenderer>();
44
- let notifyingInject: ReactDevToolsGlobalHook["inject"] | null = null;
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);
80
+ }
81
+ };
45
82
 
46
- const ensureInjectNotifiesListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
47
- if (rdtHook.inject === notifyingInject) return;
48
- const prevInject = rdtHook.inject;
83
+ const notifyRendererInjectListeners = (renderer: ReactRenderer): void => {
84
+ for (const listener of rendererInjectListeners) {
85
+ runHookListener("onRendererInject", () => listener(renderer));
86
+ }
87
+ };
88
+
89
+ const notifyRDTHookReplaceListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
90
+ for (const listener of rdtHookReplaceListeners) {
91
+ runHookListener("onRDTHookReplace", () => listener(rdtHook));
92
+ }
93
+ };
94
+
95
+ const setRendererInjectDispatcher = (rdtHook: ReactDevToolsGlobalHook): void => {
96
+ if (rdtHook.inject === notifyingInjectByHook.get(rdtHook)) return;
97
+ const previousInject = rdtHook.inject;
49
98
  const nextInject = (renderer: ReactRenderer) => {
50
- const rendererId = prevInject.call(rdtHook, renderer);
99
+ const rendererId = previousInject.call(rdtHook, renderer);
51
100
  if (!notifiedRenderers.has(renderer)) {
52
101
  notifiedRenderers.add(renderer);
53
- for (const listener of rendererInjectListeners) {
54
- listener(renderer);
55
- }
102
+ notifyRendererInjectListeners(renderer);
56
103
  }
57
104
  return rendererId;
58
105
  };
59
106
  rdtHook.inject = nextInject;
60
- notifyingInject = nextInject;
107
+ notifyingInjectByHook.set(rdtHook, nextInject);
61
108
  };
62
109
 
63
- /** Subscribes to renderer injections without stacking competing hook patches. */
64
- export const onRendererInject = (listener: (renderer: ReactRenderer) => void): Unsubscribe => {
65
- ensureInjectNotifiesListeners(getRDTHook());
110
+ export const onRendererInject = (listener: RendererInjectListener): Unsubscribe => {
111
+ setRendererInjectDispatcher(getRDTHook());
66
112
  rendererInjectListeners.add(listener);
67
- return toUnsubscribe(() => {
113
+ return createUnsubscribe(() => {
68
114
  rendererInjectListeners.delete(listener);
69
115
  });
70
116
  };
71
117
 
72
- const ensureRendererMap = (rdtHook: ReactDevToolsGlobalHook): Map<number, ReactRenderer> => {
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> => {
73
126
  if (!(rdtHook.renderers instanceof Map)) {
74
127
  rdtHook.renderers = new Map();
75
128
  }
76
129
  return rdtHook.renderers;
77
130
  };
78
131
 
79
- export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
132
+ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
80
133
  if (onActive) {
81
134
  _onActiveListeners.add(onActive);
82
135
  }
@@ -87,20 +140,20 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
87
140
  _instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
88
141
  checkDCE,
89
142
  hasUnsupportedRendererAttached: false,
90
- inject(renderer) {
143
+ inject: (renderer) => {
91
144
  const nextRendererId = ++rendererIdCounter;
92
145
  renderers.set(nextRendererId, renderer);
93
146
  _renderers.add(renderer);
94
147
  if (!rdtHook._instrumentationIsActive) {
95
148
  rdtHook._instrumentationIsActive = true;
96
- _onActiveListeners.forEach((listener) => listener());
149
+ notifyActiveListeners();
97
150
  }
98
151
  return nextRendererId;
99
152
  },
100
- on: NO_OP,
101
- onCommitFiberRoot: NO_OP,
102
- onCommitFiberUnmount: NO_OP,
103
- onPostCommitFiberRoot: NO_OP,
153
+ on: noOp,
154
+ onCommitFiberRoot: noOp,
155
+ onCommitFiberUnmount: noOp,
156
+ onPostCommitFiberRoot: noOp,
104
157
  renderers,
105
158
  supportsFiber: true,
106
159
  supportsFlight: true,
@@ -116,92 +169,106 @@ export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHoo
116
169
  if (newHook && typeof newHook === "object") {
117
170
  const ourRenderers = rdtHook.renderers;
118
171
  rdtHook = newHook;
119
- const nextRenderers = ensureRendererMap(rdtHook);
172
+ const nextRenderers = getRendererMap(rdtHook);
120
173
  if (ourRenderers.size > 0) {
121
- ourRenderers.forEach((renderer, id) => {
174
+ ourRenderers.forEach((renderer, rendererId) => {
122
175
  _renderers.add(renderer);
123
- nextRenderers.set(id, renderer);
176
+ nextRenderers.set(rendererId, renderer);
124
177
  });
178
+ }
179
+ if (ourRenderers.size > 0 || rdtHookReplaceListeners.size > 0) {
125
180
  patchRDTHook(onActive);
126
181
  }
182
+ notifyRDTHookReplaceListeners(rdtHook);
127
183
  }
128
184
  },
129
185
  });
130
- // HACK: The DevTools extension uses hasOwnProperty to decide whether it may replace an earlier hook.
131
- // eslint-disable-next-line @typescript-eslint/unbound-method
132
- const originalWindowHasOwnProperty = window.hasOwnProperty;
133
- let hasRanHack = false;
134
- objectDefineProperty(window, "hasOwnProperty", {
135
- configurable: true,
136
- value: function (this: unknown, ...args: [PropertyKey]) {
137
- try {
138
- 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();
139
207
  globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
140
- hasRanHack = true;
141
- return -0;
208
+ return false;
142
209
  }
143
- } catch {}
144
- return originalWindowHasOwnProperty.apply(this, args);
145
- },
146
- writable: true,
147
- });
210
+ return originalWindowHasOwnProperty(...propertyKeys);
211
+ },
212
+ writable: true,
213
+ });
214
+ }
148
215
  } catch {
149
216
  patchRDTHook(onActive);
150
217
  }
151
218
  return rdtHook;
152
219
  };
153
220
 
154
- export const patchRDTHook = (onActive?: () => unknown): void => {
221
+ export const patchRDTHook = (onActive?: ActiveListener): void => {
155
222
  if (onActive) {
156
223
  _onActiveListeners.add(onActive);
157
224
  }
158
225
  let didNotifyActiveListeners = false;
159
- try {
160
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
161
- if (!rdtHook) return;
162
- const renderers = ensureRendererMap(rdtHook);
163
- if (!rdtHook._instrumentationSource) {
164
- rdtHook.checkDCE = checkDCE;
165
- rdtHook.supportsFiber = true;
166
- rdtHook.supportsFlight = true;
167
- rdtHook.hasUnsupportedRendererAttached = false;
168
- rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
169
- rdtHook._instrumentationIsActive = false;
170
- // HACK: DevTools detection must happen after setting the source to avoid recursive patching.
171
- const isReactDevtools = isRealReactDevtools(rdtHook);
172
- if (!isReactDevtools) {
173
- rdtHook.on = NO_OP;
174
- }
175
- if (renderers.size) {
176
- renderers.forEach((renderer) => _renderers.add(renderer));
177
- rdtHook._instrumentationIsActive = true;
178
- _onActiveListeners.forEach((listener) => listener());
179
- didNotifyActiveListeners = true;
180
- }
181
- const prevInject = rdtHook.inject;
182
- rdtHook.inject = (renderer) => {
183
- const rendererId = prevInject(renderer);
184
- _renderers.add(renderer);
185
- renderers.set(rendererId, renderer);
186
- rdtHook._instrumentationIsActive = true;
187
- _onActiveListeners.forEach((listener) => listener());
188
- return rendererId;
189
- };
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;
190
240
  }
191
- if (!didNotifyActiveListeners && (renderers.size || rdtHook._instrumentationIsActive)) {
192
- onActive?.();
241
+ if (renderers.size) {
242
+ renderers.forEach((renderer) => _renderers.add(renderer));
243
+ rdtHook._instrumentationIsActive = true;
244
+ notifyActiveListeners();
245
+ didNotifyActiveListeners = true;
193
246
  }
194
- } 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
+ }
195
262
  };
196
263
 
197
264
  export const hasRDTHook = (): boolean => {
198
- return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
265
+ return Object.hasOwn(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
199
266
  };
200
267
 
201
268
  /**
202
269
  * Returns the current React DevTools global hook.
203
270
  */
204
- export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
271
+ export const getRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
205
272
  if (!hasRDTHook()) {
206
273
  return installRDTHook(onActive);
207
274
  }
@@ -210,18 +277,6 @@ export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook =>
210
277
  return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? installRDTHook(onActive);
211
278
  };
212
279
 
213
- export const isClientEnvironment = (): boolean => {
214
- return Boolean(
215
- typeof window !== "undefined" &&
216
- // eslint-disable-next-line @typescript-eslint/unbound-method
217
- (window.document?.createElement || window.navigator?.product === "ReactNative"),
218
- );
219
- };
220
-
221
- export const safelyInstallRDTHook = () => {
222
- try {
223
- if (isClientEnvironment()) {
224
- getRDTHook();
225
- }
226
- } catch {}
280
+ export const safelyInstallRDTHook = (): void => {
281
+ runWithBippyError(getRDTHook, (cause) => new BippyHookInstallationError(cause));
227
282
  };