bippy 0.7.2 → 0.7.3-dev.0c7e504

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.
package/src/react.ts CHANGED
@@ -1,76 +1,152 @@
1
1
  import "./install-hook-only.js";
2
- import * as React from "react";
3
- import { isFiber } from "./core.js";
2
+ import React from "react";
3
+ import { isFiber, traverseFiber } from "./core.js";
4
+ import { _renderers } from "./rdt-hook.js";
4
5
  import type { Fiber } from "./react-internals/index.js";
5
6
 
6
7
  export type { Fiber } from "./react-internals/index.js";
7
8
 
8
- const preserveState = (state: undefined): undefined => state;
9
- const readEmptySnapshot = (): undefined => undefined;
10
- const unsubscribeFromEmptyStore = (): void => {};
11
- const subscribeToEmptyStore = (): (() => void) => unsubscribeFromEmptyStore;
12
- const useSyncExternalStore: unknown = Reflect.get(React, "useSyncExternalStore");
9
+ interface RenderMarker {
10
+ (state: undefined): undefined;
11
+ }
13
12
 
14
- const captureFiberFromHook = (useCaptureHook: () => void): Fiber | null => {
15
- const originalBind = Function.prototype.bind;
16
- let capturedFiber: Fiber | null = null;
17
- // HACK: React binds hook callbacks to the rendering Fiber in production but exposes no public API for it.
13
+ interface ReducerQueue {
14
+ lastRenderedReducer: unknown;
15
+ }
16
+
17
+ interface FiberCapture {
18
+ fiber: Fiber;
19
+ queue: ReducerQueue | null;
20
+ }
21
+
22
+ const isReducerQueue = (value: unknown): value is ReducerQueue =>
23
+ typeof value === "object" && value !== null && "lastRenderedReducer" in value;
24
+
25
+ const hasRenderMarker = (fiber: Fiber, renderMarker: RenderMarker): boolean => {
26
+ for (let hook = fiber.memoizedState; hook; hook = hook.next) {
27
+ if (Array.isArray(hook.memoizedState) && hook.memoizedState[0] === renderMarker) return true;
28
+ }
29
+ return false;
30
+ };
31
+
32
+ const getReducerQueue = (fiber: Fiber, renderMarker: RenderMarker): ReducerQueue | null => {
33
+ for (let hook = fiber.memoizedState; hook; hook = hook.next) {
34
+ if (isReducerQueue(hook.queue) && hook.queue.lastRenderedReducer === renderMarker)
35
+ return hook.queue;
36
+ }
37
+ return null;
38
+ };
39
+
40
+ const getDevToolsFiber = (renderMarker: RenderMarker): Fiber | null => {
41
+ for (const renderer of _renderers) {
42
+ try {
43
+ const fiber = renderer.getCurrentFiber?.();
44
+ if (fiber && isFiber(fiber) && hasRenderMarker(fiber, renderMarker)) return fiber;
45
+ } catch {}
46
+ }
47
+ return null;
48
+ };
49
+
50
+ const captureReducerFiber = (
51
+ renderMarker: RenderMarker,
52
+ shouldCapture: boolean,
53
+ ): FiberCapture | null => {
54
+ let originalBind: typeof Function.prototype.bind | undefined;
55
+ if (shouldCapture) {
56
+ try {
57
+ originalBind = Function.prototype.bind;
58
+ } catch {}
59
+ }
60
+ if (!originalBind) {
61
+ React.useReducer(renderMarker, undefined);
62
+ return null;
63
+ }
64
+
65
+ let capture: FiberCapture | null = null;
66
+ let isCapturing = true;
67
+ // HACK: Production React binds a new reducer's Fiber and queue; validate the private reducer marker, not argument positions.
18
68
  const bindProxy = new Proxy(originalBind, {
19
- apply: (bind, functionToBind, boundArguments) => {
20
- const fiber = boundArguments[1];
21
- if (!capturedFiber && isFiber(fiber)) {
22
- capturedFiber = fiber;
69
+ apply: (bind, callback, boundArguments) => {
70
+ if (isCapturing && !capture) {
71
+ try {
72
+ const queue = boundArguments.find(
73
+ (argument): argument is ReducerQueue =>
74
+ isReducerQueue(argument) && argument.lastRenderedReducer === renderMarker,
75
+ );
76
+ if (queue) {
77
+ const fiber = boundArguments.find(
78
+ (argument): argument is Fiber => isFiber(argument) && isFiber(argument.return),
79
+ );
80
+ if (fiber) capture = { fiber, queue };
81
+ }
82
+ } catch {}
23
83
  }
24
- return Reflect.apply(bind, functionToBind, boundArguments);
84
+ return Reflect.apply(bind, callback, boundArguments);
25
85
  },
26
86
  });
27
- Reflect.set(Function.prototype, "bind", bindProxy);
28
87
 
29
88
  try {
30
- useCaptureHook();
89
+ try {
90
+ Reflect.set(Function.prototype, "bind", bindProxy);
91
+ } catch {}
92
+ React.useReducer(renderMarker, undefined);
93
+ return capture;
31
94
  } finally {
32
- if (Function.prototype.bind === bindProxy) {
33
- Reflect.set(Function.prototype, "bind", originalBind);
34
- }
95
+ isCapturing = false;
96
+ capture = null;
97
+ try {
98
+ if (Function.prototype.bind === bindProxy) {
99
+ Reflect.set(Function.prototype, "bind", originalBind);
100
+ }
101
+ } catch {}
35
102
  }
36
-
37
- return capturedFiber;
38
103
  };
39
104
 
40
- const useExternalStoreCapture = (): void => {
41
- Reflect.apply(useSyncExternalStore, React, [
42
- subscribeToEmptyStore,
43
- readEmptySnapshot,
44
- readEmptySnapshot,
45
- ]);
46
- };
47
-
48
- const useReducerCapture = (): void => {
49
- React.useReducer(preserveState, undefined);
105
+ // HACK: React <16.13 attaches hooks after render; the shared reducer queue validates the root-walk fallback.
106
+ const getRenderingFiberFromRoot = (
107
+ { fiber, queue }: FiberCapture,
108
+ renderMarker: RenderMarker,
109
+ ): Fiber | null => {
110
+ if (queue?.lastRenderedReducer !== renderMarker) return null;
111
+ let rootFiber = fiber;
112
+ while (rootFiber.return) rootFiber = rootFiber.return;
113
+ const root = rootFiber.stateNode;
114
+ if (typeof root !== "object" || root === null || !("current" in root) || !isFiber(root.current)) {
115
+ return null;
116
+ }
117
+ const renderingRoot = root.current.alternate;
118
+ return traverseFiber(renderingRoot, (candidate) => {
119
+ if (candidate !== fiber && candidate !== fiber.alternate) return false;
120
+ let parent = candidate;
121
+ while (parent.return) parent = parent.return;
122
+ return parent === renderingRoot;
123
+ });
50
124
  };
51
125
 
52
- const useFiberWithExternalStore = (): Fiber | undefined =>
53
- captureFiberFromHook(useExternalStoreCapture) ?? undefined;
54
-
55
- const useFiberWithReducer = (): Fiber | undefined => {
56
- const committedFiberRef = React.useRef<Fiber | null>(null);
57
- const renderedFiberRef = React.useRef<Fiber | null>(null);
58
- const hookFiber = captureFiberFromHook(useReducerCapture);
126
+ export const useFiber = (): Fiber | undefined => {
127
+ "use no memo";
128
+ const fiberRef = React.useRef<FiberCapture | null>(null);
129
+ const renderMarker: RenderMarker = (state) => state;
130
+ React.useMemo(() => renderMarker, [renderMarker]);
131
+ const devToolsFiber = fiberRef.current ? null : getDevToolsFiber(renderMarker);
132
+ const capture = captureReducerFiber(renderMarker, !fiberRef.current && !devToolsFiber);
133
+ if (devToolsFiber) {
134
+ fiberRef.current = {
135
+ fiber: devToolsFiber,
136
+ queue: getReducerQueue(devToolsFiber, renderMarker),
137
+ };
138
+ return devToolsFiber;
139
+ }
140
+ const knownCapture = capture ?? fiberRef.current;
141
+ if (!knownCapture) return undefined;
142
+ const knownFiber = knownCapture.fiber;
59
143
  const fiber =
60
- hookFiber ??
61
- (renderedFiberRef.current !== committedFiberRef.current
62
- ? renderedFiberRef.current
63
- : committedFiberRef.current?.alternate) ??
64
- committedFiberRef.current;
65
-
66
- renderedFiberRef.current = fiber;
67
-
68
- React.useEffect(() => {
69
- committedFiberRef.current = fiber;
70
- }, [fiber]);
71
-
144
+ capture?.fiber ??
145
+ (hasRenderMarker(knownFiber, renderMarker)
146
+ ? knownFiber
147
+ : knownFiber.alternate && hasRenderMarker(knownFiber.alternate, renderMarker)
148
+ ? knownFiber.alternate
149
+ : getRenderingFiberFromRoot(knownCapture, renderMarker));
150
+ if (fiber) fiberRef.current = { fiber, queue: knownCapture.queue };
72
151
  return fiber ?? undefined;
73
152
  };
74
-
75
- export const useFiber =
76
- typeof useSyncExternalStore === "function" ? useFiberWithExternalStore : useFiberWithReducer;
@@ -25,9 +25,7 @@ export const BUNDLED_FILE_PATTERN_REGEX =
25
25
 
26
26
  export const QUERY_PARAMETER_PATTERN_REGEX = /^\?[\w~.-]+(?:=[^&#]*)?(?:&[\w~.-]+(?:=[^&#]*)?)*$/;
27
27
 
28
- export const SERVER_FRAME_MARKER = "(at Server)";
29
-
30
- export const SERVER_ENV_PATTERN = /\(at [^)]+\)$/;
28
+ export const SERVER_ENV_PATTERN = /\[[^\]]+\]$/;
31
29
 
32
30
  export const REACT_STACK_BOTTOM_FRAME_PATTERNS = [
33
31
  "react_stack_bottom_frame",
@@ -6,6 +6,7 @@ import {
6
6
  getSourceFromSourceMap,
7
7
  getSourceMap,
8
8
  type SourceFetch,
9
+ type SourceMapRequestOptions,
9
10
  } from "./symbolication.js";
10
11
  import type { StackFrame } from "./parse-stack.js";
11
12
 
@@ -53,8 +54,9 @@ const extractComponentNameFromSource = (
53
54
 
54
55
  export const getDisplayNameFromSource = async (
55
56
  fiber: Fiber,
56
- cache = true,
57
- fetchFn?: SourceFetch,
57
+ shouldUseCache = true,
58
+ sourceFetch?: SourceFetch,
59
+ requestOptions: SourceMapRequestOptions = {},
58
60
  ): Promise<string | null> => {
59
61
  const stackFrame =
60
62
  getDefinitionFrameFromOwnedChild(fiber) ??
@@ -64,7 +66,12 @@ export const getDisplayNameFromSource = async (
64
66
  return getDisplayName(fiber.type);
65
67
  }
66
68
 
67
- const bundleSourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);
69
+ const bundleSourceMap = await getSourceMap(
70
+ stackFrame.fileName,
71
+ shouldUseCache,
72
+ sourceFetch,
73
+ requestOptions,
74
+ );
68
75
 
69
76
  if (!bundleSourceMap) {
70
77
  return getDisplayName(fiber.type);
@@ -19,6 +19,7 @@ import {
19
19
  getSourceMap,
20
20
  symbolicateStack,
21
21
  type SourceFetch,
22
+ type SourceMapRequestOptions,
22
23
  } from "./symbolication.js";
23
24
 
24
25
  export const hasDebugSource = (
@@ -67,10 +68,17 @@ const getUsageFrameFromDebugStack = (fiber: Fiber): StackFrame | null => {
67
68
  return null;
68
69
  };
69
70
 
71
+ export const getRawSource = (fiber: Fiber): FiberSource | null => {
72
+ if (hasDebugSource(fiber)) return fiber._debugSource;
73
+ const stackFrame = getUsageFrameFromDebugStack(fiber) ?? getDefinitionFrameFromOwnedChild(fiber);
74
+ return stackFrame ? toFiberSource(stackFrame) : null;
75
+ };
76
+
70
77
  const getSourceByComponentName = async (
71
78
  fiber: Fiber,
72
- cache: boolean,
73
- fetchFn: SourceFetch | undefined,
79
+ shouldUseCache: boolean,
80
+ sourceFetch: SourceFetch | undefined,
81
+ requestOptions: SourceMapRequestOptions,
74
82
  ): Promise<FiberSource | null> => {
75
83
  const functionName = getDisplayName(fiber.type);
76
84
  if (!functionName) return null;
@@ -83,7 +91,7 @@ const getSourceByComponentName = async (
83
91
  continue;
84
92
  }
85
93
  visitedFileNames.add(fileName);
86
- const sourceMap = await getSourceMap(fileName, cache, fetchFn);
94
+ const sourceMap = await getSourceMap(fileName, shouldUseCache, sourceFetch, requestOptions);
87
95
  if (!sourceMap) continue;
88
96
  const source = getSourceFromSourceMapByFunctionName(sourceMap, functionName);
89
97
  if (source && !source.isIgnoreListed) return toFiberSource(source);
@@ -119,30 +127,42 @@ const getSourceByComponentName = async (
119
127
  */
120
128
  export const getSource = async (
121
129
  fiber: Fiber,
122
- cache = true,
123
- fetchFn?: SourceFetch,
130
+ shouldUseCache = true,
131
+ sourceFetch?: SourceFetch,
132
+ requestOptions: SourceMapRequestOptions = {},
124
133
  ): Promise<FiberSource | null> => {
125
- if (hasDebugSource(fiber)) {
126
- return fiber._debugSource;
127
- }
128
-
129
- const debugStackFrame =
130
- getUsageFrameFromDebugStack(fiber) ?? getDefinitionFrameFromOwnedChild(fiber);
131
- if (debugStackFrame) {
132
- const [symbolicatedFrame] = await symbolicateStack([debugStackFrame], cache, fetchFn);
133
- const debugStackSource = toFiberSource(symbolicatedFrame);
134
- if (debugStackSource) {
135
- return debugStackSource;
136
- }
134
+ if (hasDebugSource(fiber)) return fiber._debugSource;
135
+ const rawSource = getRawSource(fiber);
136
+ if (rawSource) {
137
+ const [symbolicatedFrame] = await symbolicateStack(
138
+ [
139
+ {
140
+ columnNumber: rawSource.columnNumber,
141
+ fileName: rawSource.fileName,
142
+ functionName: rawSource.functionName,
143
+ lineNumber: rawSource.lineNumber,
144
+ },
145
+ ],
146
+ shouldUseCache,
147
+ sourceFetch,
148
+ requestOptions,
149
+ );
150
+ const symbolicatedSource = toFiberSource(symbolicatedFrame);
151
+ if (symbolicatedSource) return symbolicatedSource;
137
152
  }
138
153
 
139
- const parentStackFrames = await getParentStack(fiber, cache, fetchFn);
154
+ const parentStackFrames = await getParentStack(
155
+ fiber,
156
+ shouldUseCache,
157
+ sourceFetch,
158
+ requestOptions,
159
+ );
140
160
  for (const stackFrame of parentStackFrames) {
141
161
  if (stackFrame.fileName) {
142
162
  return toFiberSource(stackFrame);
143
163
  }
144
164
  }
145
- return getSourceByComponentName(fiber, cache, fetchFn);
165
+ return getSourceByComponentName(fiber, shouldUseCache, sourceFetch, requestOptions);
146
166
  };
147
167
 
148
168
  const getPathSegmentCount = (path: string): number => path.split("/").filter(Boolean).length;
@@ -1,5 +1,13 @@
1
- export { formatOwnerStack, getOwnerStack, getParentStack, hasDebugStack } from "./owner-stack.js";
2
- export { getSource, isSourceFile, normalizeFileName } from "./get-source.js";
1
+ export {
2
+ formatOwnerStack,
3
+ getDefinitionFrameFromOwnedChild,
4
+ getFallbackParentStack,
5
+ getOwnerStack,
6
+ getParentStack,
7
+ getRawOwnerStack,
8
+ hasDebugStack,
9
+ } from "./owner-stack.js";
10
+ export { getRawSource, getSource, isSourceFile, normalizeFileName } from "./get-source.js";
3
11
  export {
4
12
  getSourceContentFromSourceMap,
5
13
  getSourceFromSourceMap,
@@ -23,5 +31,11 @@ export {
23
31
  BippySourceMapError,
24
32
  BippyUnsupportedHookError,
25
33
  } from "../errors.js";
26
- export { getFiberHooks, type HookSource, type HooksNode, type HooksTree } from "./inspect-hooks.js";
34
+ export {
35
+ getFiberHooks,
36
+ inspectHooks,
37
+ type HookSource,
38
+ type HooksNode,
39
+ type HooksTree,
40
+ } from "./inspect-hooks.js";
27
41
  export { parseHookNames, type HookNames } from "./parse-hook-names.js";