bippy 0.5.43 → 0.6.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.
- package/dist/core.cjs +1 -1
- package/dist/core.d.cts +28 -313
- package/dist/core.d.ts +28 -313
- package/dist/core.js +1 -1
- package/dist/core2.d.cts +3 -2
- package/dist/core2.d.ts +3 -2
- package/dist/get-source.cjs +19 -0
- package/dist/get-source.js +19 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.iife.js +1 -1
- package/dist/index.js +1 -1
- package/dist/install-hook-only.cjs +1 -1
- package/dist/install-hook-only.iife.js +1 -1
- package/dist/install-hook-only.js +1 -1
- package/dist/rdt-hook.cjs +1 -1
- package/dist/rdt-hook.js +1 -1
- package/dist/react-refresh.cjs +9 -0
- package/dist/react-refresh.d.cts +66 -0
- package/dist/react-refresh.d.ts +66 -0
- package/dist/react-refresh.js +9 -0
- package/dist/source.cjs +4 -19
- package/dist/source.d.cts +35 -27
- package/dist/source.d.ts +35 -27
- package/dist/source.js +4 -19
- package/dist/unsubscribe.d.cts +298 -0
- package/dist/unsubscribe.d.ts +298 -0
- package/package.json +14 -3
- package/src/core.ts +312 -334
- package/src/rdt-hook.ts +57 -16
- package/src/react-refresh/constants.ts +9 -0
- package/src/react-refresh/detect-hmr-transport.ts +33 -0
- package/src/react-refresh/index.ts +173 -0
- package/src/react-refresh/metro-hmr-transport.ts +188 -0
- package/src/react-refresh/next-webpack-hmr-transport.ts +72 -0
- package/src/react-refresh/normalize-hmr-file-path.ts +24 -0
- package/src/react-refresh/types.ts +7 -0
- package/src/react-refresh/vite-hmr-transport.ts +116 -0
- package/src/source/constants.ts +11 -0
- package/src/source/get-display-name-from-source.ts +3 -3
- package/src/source/get-source.ts +53 -9
- package/src/source/index.ts +17 -8
- package/src/source/inspect-hooks.ts +7 -4
- package/src/source/owner-stack.ts +193 -29
- package/src/source/parse-debug-stack.ts +133 -0
- package/src/source/parse-stack.ts +6 -105
- package/src/source/symbolication.ts +48 -13
- package/src/types.ts +20 -7
- package/src/unsubscribe.ts +17 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { HMR_RECONNECT_DELAY_MS, VITE_WS_TOKEN_REGEX } from "./constants.js";
|
|
2
|
+
import { HmrTransport, HmrUpdateHandler } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the accepted file paths from a raw Vite HMR WebSocket message.
|
|
6
|
+
* Only `js-update` entries are kept (a `css-update` swaps a stylesheet link
|
|
7
|
+
* without re-running modules). Returns an empty array for any other message
|
|
8
|
+
* shape.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* parseViteUpdatePaths(rawMessageData);
|
|
13
|
+
* // ["/src/app.tsx"]
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const parseViteUpdatePaths = (rawMessageData: string): string[] => {
|
|
17
|
+
let message: unknown;
|
|
18
|
+
try {
|
|
19
|
+
message = JSON.parse(rawMessageData);
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
if (typeof message !== "object" || message === null) return [];
|
|
24
|
+
if (!("type" in message) || message.type !== "update") return [];
|
|
25
|
+
if (!("updates" in message) || !Array.isArray(message.updates)) return [];
|
|
26
|
+
const filePaths: string[] = [];
|
|
27
|
+
for (const update of message.updates) {
|
|
28
|
+
if (typeof update !== "object" || update === null) continue;
|
|
29
|
+
if (!("type" in update) || update.type !== "js-update") continue;
|
|
30
|
+
if (!("acceptedPath" in update) || typeof update.acceptedPath !== "string") continue;
|
|
31
|
+
filePaths.push(update.acceptedPath);
|
|
32
|
+
}
|
|
33
|
+
return filePaths;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// HACK: a standalone script is not a Vite module, so import.meta.hot is
|
|
37
|
+
// unavailable; open a second HMR WebSocket using the wsToken scraped from
|
|
38
|
+
// the dev server's own /@vite/client source.
|
|
39
|
+
const fetchViteWsToken = async (): Promise<string | null> => {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch("/@vite/client");
|
|
42
|
+
if (!response.ok) return null;
|
|
43
|
+
const clientSource = await response.text();
|
|
44
|
+
return VITE_WS_TOKEN_REGEX.exec(clientSource)?.[1] ?? null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Subscribes to the current page's Vite dev server HMR WebSocket and invokes
|
|
52
|
+
* `onHmrUpdate` with the updated file paths on every hot update. Reconnects
|
|
53
|
+
* automatically when the dev server restarts. Resolves `null` when the page
|
|
54
|
+
* is not served by Vite.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const transport = await createViteHmrTransport((filePaths) => {
|
|
59
|
+
* console.log("hot updated:", filePaths);
|
|
60
|
+
* });
|
|
61
|
+
* transport?.dispose();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const createViteHmrTransport = async (
|
|
65
|
+
onHmrUpdate: HmrUpdateHandler,
|
|
66
|
+
): Promise<HmrTransport | null> => {
|
|
67
|
+
if (typeof window === "undefined" || typeof WebSocket === "undefined") return null;
|
|
68
|
+
const initialWsToken = await fetchViteWsToken();
|
|
69
|
+
if (!initialWsToken) return null;
|
|
70
|
+
|
|
71
|
+
let isDisposed = false;
|
|
72
|
+
let socket: WebSocket | null = null;
|
|
73
|
+
let reconnectTimerId: number | undefined;
|
|
74
|
+
|
|
75
|
+
const scheduleReconnect = () => {
|
|
76
|
+
if (isDisposed) return;
|
|
77
|
+
reconnectTimerId = window.setTimeout(() => {
|
|
78
|
+
void fetchViteWsToken().then((freshWsToken) => {
|
|
79
|
+
if (isDisposed) return;
|
|
80
|
+
if (freshWsToken) {
|
|
81
|
+
connect(freshWsToken);
|
|
82
|
+
} else {
|
|
83
|
+
scheduleReconnect();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}, HMR_RECONNECT_DELAY_MS);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const connect = (wsToken: string) => {
|
|
90
|
+
if (isDisposed) return;
|
|
91
|
+
const socketProtocol = location.protocol === "https:" ? "wss" : "ws";
|
|
92
|
+
const connectedSocket = new WebSocket(
|
|
93
|
+
`${socketProtocol}://${location.host}/?token=${wsToken}`,
|
|
94
|
+
"vite-hmr",
|
|
95
|
+
);
|
|
96
|
+
socket = connectedSocket;
|
|
97
|
+
connectedSocket.onmessage = (event) => {
|
|
98
|
+
const filePaths = parseViteUpdatePaths(String(event.data));
|
|
99
|
+
if (filePaths.length > 0) onHmrUpdate(filePaths);
|
|
100
|
+
};
|
|
101
|
+
connectedSocket.onclose = scheduleReconnect;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
connect(initialWsToken);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
dispose: () => {
|
|
108
|
+
isDisposed = true;
|
|
109
|
+
window.clearTimeout(reconnectTimerId);
|
|
110
|
+
if (socket) {
|
|
111
|
+
socket.onclose = null;
|
|
112
|
+
socket.close();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
package/src/source/constants.ts
CHANGED
|
@@ -14,6 +14,8 @@ export const INTERNAL_SCHEME_PREFIXES = [
|
|
|
14
14
|
|
|
15
15
|
export const ABOUT_REACT_PREFIX = "about://React/";
|
|
16
16
|
|
|
17
|
+
export const SERVER_COMPONENT_URL_PREFIXES = ["rsc://", ABOUT_REACT_PREFIX] as const;
|
|
18
|
+
|
|
17
19
|
export const ANONYMOUS_FILE_PATTERNS = ["<anonymous>", "eval", ""] as const;
|
|
18
20
|
|
|
19
21
|
export const SOURCE_FILE_EXTENSION_REGEX = /\.(jsx|tsx|ts|js)$/;
|
|
@@ -26,3 +28,12 @@ export const QUERY_PARAMETER_PATTERN_REGEX = /^\?[\w~.-]+(?:=[^&#]*)?(?:&[\w~.-]
|
|
|
26
28
|
export const SERVER_FRAME_MARKER = "(at Server)";
|
|
27
29
|
|
|
28
30
|
export const SERVER_ENV_PATTERN = /\(at [^)]+\)$/;
|
|
31
|
+
|
|
32
|
+
export const REACT_STACK_BOTTOM_FRAME_PATTERNS = [
|
|
33
|
+
"react_stack_bottom_frame",
|
|
34
|
+
"react-stack-bottom-frame",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
// the first frame of a _debugStack is the JSX factory itself (jsxDEV), never
|
|
38
|
+
// user code, matching what react's own captureOwnerStack pops
|
|
39
|
+
export const JSX_FACTORY_FRAME_COUNT = 1;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fiber } from "../types.js";
|
|
2
2
|
import { getDisplayName } from "../core.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getParentStack } from "./owner-stack.js";
|
|
4
4
|
import { getSourceFromSourceMap, getSourceMap } from "./symbolication.js";
|
|
5
5
|
import { StackFrame } from "./parse-stack.js";
|
|
6
6
|
|
|
@@ -46,8 +46,8 @@ export const getDisplayNameFromSource = async (
|
|
|
46
46
|
cache = true,
|
|
47
47
|
fetchFn?: (url: string) => Promise<Response>,
|
|
48
48
|
): Promise<string | null> => {
|
|
49
|
-
const
|
|
50
|
-
const stackFrame =
|
|
49
|
+
const parentStackFrames = await getParentStack(fiber, cache, fetchFn);
|
|
50
|
+
const stackFrame = parentStackFrames.filter((innerFrame) => innerFrame.fileName)[0];
|
|
51
51
|
|
|
52
52
|
if (!stackFrame?.fileName) {
|
|
53
53
|
return getDisplayName(fiber.type);
|
package/src/source/get-source.ts
CHANGED
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
BUNDLED_FILE_PATTERN_REGEX,
|
|
11
11
|
QUERY_PARAMETER_PATTERN_REGEX,
|
|
12
12
|
} from "./constants.js";
|
|
13
|
-
import {
|
|
13
|
+
import { getDefinitionFrameFromOwnedChild, getParentStack, hasDebugStack } from "./owner-stack.js";
|
|
14
|
+
import { parseDebugStack } from "./parse-debug-stack.js";
|
|
15
|
+
import { StackFrame } from "./parse-stack.js";
|
|
16
|
+
import { symbolicateStack } from "./symbolication.js";
|
|
14
17
|
|
|
15
18
|
export const hasDebugSource = (
|
|
16
19
|
fiber: Fiber,
|
|
@@ -31,9 +34,46 @@ export const hasDebugSource = (
|
|
|
31
34
|
);
|
|
32
35
|
};
|
|
33
36
|
|
|
37
|
+
const toFiberSource = (stackFrame: StackFrame): FiberSource | null =>
|
|
38
|
+
stackFrame.fileName
|
|
39
|
+
? {
|
|
40
|
+
fileName: stackFrame.fileName,
|
|
41
|
+
lineNumber: stackFrame.lineNumber,
|
|
42
|
+
columnNumber: stackFrame.columnNumber,
|
|
43
|
+
functionName: stackFrame.functionName,
|
|
44
|
+
}
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
// the fiber's own _debugStack (react 19) is captured at its JSX creation
|
|
48
|
+
// site, so its first user-space frame IS the usage site - no need to
|
|
49
|
+
// re-invoke the component like the throwing trick does
|
|
50
|
+
const getUsageFrameFromDebugStack = (fiber: Fiber): StackFrame | null => {
|
|
51
|
+
if (!hasDebugStack(fiber)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const { frames, isTrusted } = parseDebugStack(fiber._debugStack);
|
|
55
|
+
if (!isTrusted) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
for (const stackFrame of frames) {
|
|
59
|
+
if (stackFrame.fileName) {
|
|
60
|
+
return stackFrame;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
};
|
|
65
|
+
|
|
34
66
|
/**
|
|
35
67
|
* Returns the source of where the component is used. Available only in dev, for composite {@link Fiber}s.
|
|
36
68
|
*
|
|
69
|
+
* Resolution order:
|
|
70
|
+
* 1. `_debugSource` (react <19, requires the JSX source babel transform)
|
|
71
|
+
* 2. the fiber's own `_debugStack` (react 19) - the exact JSX creation site
|
|
72
|
+
* 3. an owned child's `_debugStack` bottom frame (react 19) - a location
|
|
73
|
+
* inside the component's own body; works for components that the throwing
|
|
74
|
+
* trick cannot locate (no hooks, no props access)
|
|
75
|
+
* 4. the legacy owner-stack path (throwing trick re-invocation)
|
|
76
|
+
*
|
|
37
77
|
* @example
|
|
38
78
|
* ```ts
|
|
39
79
|
* function Parent() {
|
|
@@ -59,16 +99,20 @@ export const getSource = async (
|
|
|
59
99
|
return debugSource || null;
|
|
60
100
|
}
|
|
61
101
|
|
|
62
|
-
const
|
|
102
|
+
const debugStackFrame =
|
|
103
|
+
getUsageFrameFromDebugStack(fiber) ?? getDefinitionFrameFromOwnedChild(fiber);
|
|
104
|
+
if (debugStackFrame) {
|
|
105
|
+
const [symbolicatedFrame] = await symbolicateStack([debugStackFrame], cache, fetchFn);
|
|
106
|
+
const debugStackSource = toFiberSource(symbolicatedFrame);
|
|
107
|
+
if (debugStackSource) {
|
|
108
|
+
return debugStackSource;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
63
111
|
|
|
64
|
-
|
|
112
|
+
const parentStackFrames = await getParentStack(fiber, cache, fetchFn);
|
|
113
|
+
for (const stackFrame of parentStackFrames) {
|
|
65
114
|
if (stackFrame.fileName) {
|
|
66
|
-
return
|
|
67
|
-
fileName: stackFrame.fileName,
|
|
68
|
-
lineNumber: stackFrame.lineNumber,
|
|
69
|
-
columnNumber: stackFrame.columnNumber,
|
|
70
|
-
functionName: stackFrame.functionName,
|
|
71
|
-
};
|
|
115
|
+
return toFiberSource(stackFrame);
|
|
72
116
|
}
|
|
73
117
|
}
|
|
74
118
|
return null;
|
package/src/source/index.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
export { formatOwnerStack, getOwnerStack, getParentStack, hasDebugStack } from "./owner-stack.js";
|
|
2
|
+
export { getSource, isSourceFile, normalizeFileName } from "./get-source.js";
|
|
3
|
+
export {
|
|
4
|
+
getSourceFromSourceMap,
|
|
5
|
+
getSourceMap,
|
|
6
|
+
symbolicateStack,
|
|
7
|
+
type DecodedSourceMapSection,
|
|
8
|
+
type IndexSourceMap,
|
|
9
|
+
type RawSourceMap,
|
|
10
|
+
type SourceMap,
|
|
11
|
+
type StandardSourceMap,
|
|
12
|
+
} from "./symbolication.js";
|
|
13
|
+
export type { FiberSource } from "./types.js";
|
|
14
|
+
export { parseStack, type ParseOptions, type StackFrame } from "./parse-stack.js";
|
|
15
|
+
export { getDisplayNameFromSource } from "./get-display-name-from-source.js";
|
|
16
|
+
export { getFiberHooks, type HookSource, type HooksNode, type HooksTree } from "./inspect-hooks.js";
|
|
17
|
+
export { parseHookNames, type HookNames } from "./parse-hook-names.js";
|
|
@@ -282,16 +282,16 @@ const dispatcherUseMemoCache = (size: number): unknown[] => {
|
|
|
282
282
|
)?.memoCache;
|
|
283
283
|
if (memoCache === null || memoCache === undefined) return [];
|
|
284
284
|
|
|
285
|
-
let
|
|
286
|
-
if (
|
|
287
|
-
|
|
285
|
+
let memoCacheSlots = memoCache.data[memoCache.index];
|
|
286
|
+
if (memoCacheSlots === undefined) {
|
|
287
|
+
memoCacheSlots = memoCache.data[memoCache.index] = Array.from(
|
|
288
288
|
{ length: size },
|
|
289
289
|
() => REACT_MEMO_CACHE_SENTINEL,
|
|
290
290
|
);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
memoCache.index++;
|
|
294
|
-
return
|
|
294
|
+
return memoCacheSlots;
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
const dispatcherUseOptimistic = (passthrough: unknown): [unknown, () => void] => {
|
|
@@ -473,6 +473,9 @@ const findSharedIndex = (
|
|
|
473
473
|
rootStack: StackFrame[],
|
|
474
474
|
rootIndex: number,
|
|
475
475
|
): number => {
|
|
476
|
+
// mostLikelyAncestorIndex is cached across inspections, so it can exceed the
|
|
477
|
+
// bounds of a later, shorter root stack (e.g. truncated Error.stackTraceLimit)
|
|
478
|
+
if (rootIndex >= rootStack.length) return -1;
|
|
476
479
|
const source = rootStack[rootIndex].source;
|
|
477
480
|
hookSearch: for (let hookIndex = 0; hookIndex < hookStack.length; hookIndex++) {
|
|
478
481
|
if (hookStack[hookIndex].source === source) {
|
|
@@ -17,8 +17,14 @@ import {
|
|
|
17
17
|
getDisplayName,
|
|
18
18
|
traverseFiber,
|
|
19
19
|
} from "../core.js";
|
|
20
|
-
import {
|
|
20
|
+
import { ServerComponentInfo } from "../types.js";
|
|
21
|
+
import {
|
|
22
|
+
SERVER_FRAME_MARKER,
|
|
23
|
+
SERVER_ENV_PATTERN,
|
|
24
|
+
SERVER_COMPONENT_URL_PREFIXES,
|
|
25
|
+
} from "./constants.js";
|
|
21
26
|
|
|
27
|
+
import { parseDebugStack } from "./parse-debug-stack.js";
|
|
22
28
|
import { parseStack, StackFrame } from "./parse-stack.js";
|
|
23
29
|
import { symbolicateStack } from "./symbolication.js";
|
|
24
30
|
|
|
@@ -30,6 +36,60 @@ export const hasDebugStack = (
|
|
|
30
36
|
return fiber._debugStack instanceof Error && typeof fiber._debugStack?.stack === "string";
|
|
31
37
|
};
|
|
32
38
|
|
|
39
|
+
const isFiberOwner = (owner: Fiber | ServerComponentInfo): owner is Fiber =>
|
|
40
|
+
typeof (owner as Fiber).tag === "number";
|
|
41
|
+
|
|
42
|
+
// react's typings (and bippy's Fiber, which mirrors them) declare _debugOwner
|
|
43
|
+
// as a Fiber, but react 19 flight sets a ReactComponentInfo object for server
|
|
44
|
+
// component owners
|
|
45
|
+
const getDebugOwner = (fiber: Fiber): Fiber | ServerComponentInfo | undefined =>
|
|
46
|
+
fiber._debugOwner as Fiber | ServerComponentInfo | undefined;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Locates a frame inside the fiber's own function body without invoking it:
|
|
50
|
+
* any child fiber owned by this fiber was created by JSX inside its body, so
|
|
51
|
+
* the bottom user-space frame of that child's _debugStack sits in this
|
|
52
|
+
* component. The enclosing line/column (V8 CallSite API) points at the
|
|
53
|
+
* function definition start. Requires React 19 (_debugStack).
|
|
54
|
+
*/
|
|
55
|
+
export const getDefinitionFrameFromOwnedChild = (fiber: Fiber): StackFrame | null => {
|
|
56
|
+
let ownedChildDebugStack: Error | null = null;
|
|
57
|
+
traverseFiber(fiber, (childFiber) => {
|
|
58
|
+
if (childFiber === fiber) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const childOwner = childFiber._debugOwner;
|
|
62
|
+
if (
|
|
63
|
+
(childOwner === fiber || (fiber.alternate !== null && childOwner === fiber.alternate)) &&
|
|
64
|
+
childFiber._debugStack instanceof Error
|
|
65
|
+
) {
|
|
66
|
+
ownedChildDebugStack = childFiber._debugStack;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
});
|
|
71
|
+
if (!ownedChildDebugStack) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { frames, isTrusted } = parseDebugStack(ownedChildDebugStack);
|
|
76
|
+
if (!isTrusted) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
for (let frameIndex = frames.length - 1; frameIndex >= 0; frameIndex--) {
|
|
80
|
+
const stackFrame = frames[frameIndex];
|
|
81
|
+
if (!stackFrame.fileName) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...stackFrame,
|
|
86
|
+
lineNumber: stackFrame.enclosingLineNumber || stackFrame.lineNumber,
|
|
87
|
+
columnNumber: stackFrame.enclosingColumnNumber || stackFrame.columnNumber,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
33
93
|
const getCurrentDispatcher = (): null | React.RefObject<unknown> => {
|
|
34
94
|
const rdtHook = getRDTHook();
|
|
35
95
|
for (const renderer of [...Array.from(_renderers), ...Array.from(rdtHook.renderers.values())]) {
|
|
@@ -68,6 +128,10 @@ export const describeDebugInfoFrame = (name: string, env?: string): string => {
|
|
|
68
128
|
|
|
69
129
|
let reEntry = false;
|
|
70
130
|
|
|
131
|
+
// Computing a frame throws and parses two full error stacks, so cache per
|
|
132
|
+
// component type like React DevTools does.
|
|
133
|
+
const componentFrameCache = new WeakMap<React.ComponentType<unknown>, string>();
|
|
134
|
+
|
|
71
135
|
// https://github.com/facebook/react/blob/f739642745577a8e4dcb9753836ac3589b9c590a/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js#L22
|
|
72
136
|
const describeNativeComponentFrame = (
|
|
73
137
|
component: React.ComponentType<unknown>,
|
|
@@ -77,6 +141,11 @@ const describeNativeComponentFrame = (
|
|
|
77
141
|
return "";
|
|
78
142
|
}
|
|
79
143
|
|
|
144
|
+
const cachedFrame = componentFrameCache.get(component);
|
|
145
|
+
if (cachedFrame !== undefined) {
|
|
146
|
+
return cachedFrame;
|
|
147
|
+
}
|
|
148
|
+
|
|
80
149
|
const previousPrepareStackTrace = Error.prepareStackTrace;
|
|
81
150
|
// HACK: V8 API allows undefined but bun-types declares it as non-optional
|
|
82
151
|
(Error as { prepareStackTrace?: typeof Error.prepareStackTrace }).prepareStackTrace = undefined;
|
|
@@ -257,6 +326,7 @@ const describeNativeComponentFrame = (
|
|
|
257
326
|
stackFrame = stackFrame.replace("<anonymous>", displayName);
|
|
258
327
|
}
|
|
259
328
|
// Return the line we found.
|
|
329
|
+
componentFrameCache.set(component, stackFrame);
|
|
260
330
|
return stackFrame;
|
|
261
331
|
}
|
|
262
332
|
} while (sampleIndex >= 1 && controlIndex >= 0);
|
|
@@ -277,6 +347,7 @@ const describeNativeComponentFrame = (
|
|
|
277
347
|
|
|
278
348
|
const componentName = component ? getDisplayName(component) : "";
|
|
279
349
|
const syntheticFrame = componentName ? describeBuiltInComponentFrame(componentName) : "";
|
|
350
|
+
componentFrameCache.set(component, syntheticFrame);
|
|
280
351
|
return syntheticFrame;
|
|
281
352
|
};
|
|
282
353
|
|
|
@@ -334,11 +405,11 @@ export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string =>
|
|
|
334
405
|
};
|
|
335
406
|
|
|
336
407
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
408
|
+
* Builds a component-stack string by walking the fiber `return` chain, the
|
|
409
|
+
* pre-react-19 substitute for `_debugStack` (which is why the frame locations
|
|
410
|
+
* come from re-invocation in {@link describeFiber} rather than a real stack).
|
|
340
411
|
*/
|
|
341
|
-
export const
|
|
412
|
+
export const getFallbackParentStack = (thisFiber: Fiber): string => {
|
|
342
413
|
try {
|
|
343
414
|
let componentStack = "";
|
|
344
415
|
let currentFiber: Fiber | null = thisFiber;
|
|
@@ -403,21 +474,21 @@ export const formatOwnerStack = (stack: string): string => {
|
|
|
403
474
|
// don't want/need
|
|
404
475
|
formattedStack = formattedStack.slice(29);
|
|
405
476
|
}
|
|
406
|
-
|
|
407
|
-
if (
|
|
477
|
+
const firstNewlineIndex = formattedStack.indexOf("\n");
|
|
478
|
+
if (firstNewlineIndex !== -1) {
|
|
408
479
|
// pop the JSX frame
|
|
409
|
-
formattedStack = formattedStack.slice(
|
|
480
|
+
formattedStack = formattedStack.slice(firstNewlineIndex + 1);
|
|
410
481
|
}
|
|
411
|
-
|
|
482
|
+
let bottomFrameIndex = Math.max(
|
|
412
483
|
formattedStack.indexOf("react_stack_bottom_frame"),
|
|
413
484
|
formattedStack.indexOf("react-stack-bottom-frame"),
|
|
414
485
|
);
|
|
415
|
-
if (
|
|
416
|
-
|
|
486
|
+
if (bottomFrameIndex !== -1) {
|
|
487
|
+
bottomFrameIndex = formattedStack.lastIndexOf("\n", bottomFrameIndex);
|
|
417
488
|
}
|
|
418
|
-
if (
|
|
489
|
+
if (bottomFrameIndex !== -1) {
|
|
419
490
|
// cut off everything after the bottom frame since it'll be internals.
|
|
420
|
-
formattedStack = formattedStack.slice(0,
|
|
491
|
+
formattedStack = formattedStack.slice(0, bottomFrameIndex);
|
|
421
492
|
} else {
|
|
422
493
|
// we didn't find any internal callsite out to user space.
|
|
423
494
|
// This means that this was called outside an owner or the owner is fully internal.
|
|
@@ -427,17 +498,14 @@ export const formatOwnerStack = (stack: string): string => {
|
|
|
427
498
|
return formattedStack;
|
|
428
499
|
};
|
|
429
500
|
|
|
430
|
-
interface
|
|
501
|
+
interface DebugStackEntry {
|
|
431
502
|
componentName: string;
|
|
432
503
|
stackFrames: StackFrame[];
|
|
433
504
|
}
|
|
434
505
|
|
|
435
506
|
const isReactServerComponentFrame = (stackFrame: StackFrame): boolean =>
|
|
436
507
|
Boolean(
|
|
437
|
-
stackFrame.functionName &&
|
|
438
|
-
stackFrame.fileName &&
|
|
439
|
-
(stackFrame.fileName.startsWith("rsc://") ||
|
|
440
|
-
stackFrame.fileName.startsWith(ABOUT_REACT_PREFIX)),
|
|
508
|
+
stackFrame.functionName && stackFrame.fileName && isServerComponentUrl(stackFrame.fileName),
|
|
441
509
|
);
|
|
442
510
|
|
|
443
511
|
const areStackFramesEqual = (firstFrame: StackFrame, secondFrame: StackFrame): boolean =>
|
|
@@ -446,12 +514,12 @@ const areStackFramesEqual = (firstFrame: StackFrame, secondFrame: StackFrame): b
|
|
|
446
514
|
firstFrame.columnNumber === secondFrame.columnNumber;
|
|
447
515
|
|
|
448
516
|
const buildFunctionNameToRscFramesMap = (
|
|
449
|
-
|
|
517
|
+
debugStackEntries: DebugStackEntry[],
|
|
450
518
|
): Map<string, StackFrame[]> => {
|
|
451
519
|
const functionNameToRscFrames = new Map<string, StackFrame[]>();
|
|
452
520
|
|
|
453
|
-
for (const
|
|
454
|
-
for (const stackFrame of
|
|
521
|
+
for (const debugStackEntry of debugStackEntries) {
|
|
522
|
+
for (const stackFrame of debugStackEntry.stackFrames) {
|
|
455
523
|
if (!isReactServerComponentFrame(stackFrame)) continue;
|
|
456
524
|
|
|
457
525
|
const functionName = stackFrame.functionName!;
|
|
@@ -501,8 +569,55 @@ const getEnrichedServerStackFrame = (
|
|
|
501
569
|
};
|
|
502
570
|
};
|
|
503
571
|
|
|
504
|
-
const
|
|
505
|
-
|
|
572
|
+
const isServerComponentUrl = (url: string): boolean =>
|
|
573
|
+
SERVER_COMPONENT_URL_PREFIXES.some((prefix) => url.startsWith(prefix));
|
|
574
|
+
|
|
575
|
+
// flight installs fake server frames (rsc:// or about://React/ urls) into
|
|
576
|
+
// client-side debug stacks, so server detection must be per-frame
|
|
577
|
+
const markFlightServerFrame = (stackFrame: StackFrame): StackFrame =>
|
|
578
|
+
!stackFrame.isServer && stackFrame.fileName && isServerComponentUrl(stackFrame.fileName)
|
|
579
|
+
? { ...stackFrame, isServer: true }
|
|
580
|
+
: stackFrame;
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Builds owner-chain stack frames from the _debugStack errors React 19
|
|
584
|
+
* attaches at JSX creation, walking `_debugOwner` across both client fibers
|
|
585
|
+
* and server components (ReactComponentInfo, which chains via `.owner` and
|
|
586
|
+
* carries `.debugStack`). This is exact - no re-invoking components, no
|
|
587
|
+
* name-matching heuristics - but requires React 19.
|
|
588
|
+
*/
|
|
589
|
+
const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
|
|
590
|
+
const ownerStackFrames: StackFrame[] = [];
|
|
591
|
+
let owner: Fiber | ServerComponentInfo | null | undefined = fiber;
|
|
592
|
+
while (owner) {
|
|
593
|
+
if (isFiberOwner(owner)) {
|
|
594
|
+
const ownerFiber: Fiber = owner;
|
|
595
|
+
owner = getDebugOwner(ownerFiber);
|
|
596
|
+
if (owner && hasDebugStack(ownerFiber)) {
|
|
597
|
+
const { frames, isTrusted } = parseDebugStack(ownerFiber._debugStack);
|
|
598
|
+
if (isTrusted) {
|
|
599
|
+
for (const stackFrame of frames) {
|
|
600
|
+
ownerStackFrames.push(markFlightServerFrame(stackFrame));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
const serverOwner: ServerComponentInfo = owner;
|
|
606
|
+
owner = serverOwner.owner;
|
|
607
|
+
// server stacks are captured and pre-trimmed by flight, so they carry
|
|
608
|
+
// no bottom-frame sentinel and are trusted as-is
|
|
609
|
+
if (owner && serverOwner.debugStack instanceof Error) {
|
|
610
|
+
for (const serverFrame of parseDebugStack(serverOwner.debugStack).frames) {
|
|
611
|
+
ownerStackFrames.push({ ...serverFrame, isServer: true });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return ownerStackFrames;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const getDebugStackEntries = (rootFiber: Fiber): DebugStackEntry[] => {
|
|
620
|
+
const debugStackEntries: DebugStackEntry[] = [];
|
|
506
621
|
|
|
507
622
|
traverseFiber(
|
|
508
623
|
rootFiber,
|
|
@@ -514,7 +629,7 @@ const getOwnerStackEntries = (rootFiber: Fiber): OwnerStackEntry[] => {
|
|
|
514
629
|
? getDisplayName(currentFiber.type) || "<anonymous>"
|
|
515
630
|
: currentFiber.type;
|
|
516
631
|
|
|
517
|
-
|
|
632
|
+
debugStackEntries.push({
|
|
518
633
|
componentName,
|
|
519
634
|
stackFrames: parseStack(formatOwnerStack(currentFiber._debugStack?.stack)),
|
|
520
635
|
});
|
|
@@ -522,17 +637,24 @@ const getOwnerStackEntries = (rootFiber: Fiber): OwnerStackEntry[] => {
|
|
|
522
637
|
true,
|
|
523
638
|
);
|
|
524
639
|
|
|
525
|
-
return
|
|
640
|
+
return debugStackEntries;
|
|
526
641
|
};
|
|
527
642
|
|
|
528
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Returns a stack of ALL ancestor components in the render tree (the fiber's
|
|
645
|
+
* `return` chain), including wrappers that render `{children}` without having
|
|
646
|
+
* created this fiber's JSX. Locations come from re-invoking each component
|
|
647
|
+
* with a throwing dispatcher; server frames are enriched from debug stacks by
|
|
648
|
+
* name matching. Works on every React version.
|
|
649
|
+
*/
|
|
650
|
+
export const getParentStack = async (
|
|
529
651
|
fiber: Fiber,
|
|
530
652
|
shouldCache = true,
|
|
531
653
|
fetchFunction?: (url: string) => Promise<Response>,
|
|
532
654
|
): Promise<StackFrame[]> => {
|
|
533
|
-
const
|
|
534
|
-
const fallbackStackFrames = parseStack(
|
|
535
|
-
const functionNameToRscFrames = buildFunctionNameToRscFramesMap(
|
|
655
|
+
const debugStackEntries = getDebugStackEntries(fiber);
|
|
656
|
+
const fallbackStackFrames = parseStack(getFallbackParentStack(fiber));
|
|
657
|
+
const functionNameToRscFrames = buildFunctionNameToRscFramesMap(debugStackEntries);
|
|
536
658
|
const functionNameToUsageIndex = new Map<string, number>();
|
|
537
659
|
|
|
538
660
|
const enrichedStackFrames = fallbackStackFrames.map((stackFrame): StackFrame => {
|
|
@@ -559,3 +681,45 @@ export const getOwnerStack = async (
|
|
|
559
681
|
|
|
560
682
|
return symbolicateStack(deduplicatedStackFrames, shouldCache, fetchFunction);
|
|
561
683
|
};
|
|
684
|
+
|
|
685
|
+
// an owner frame is only actionable if it can point an editor somewhere:
|
|
686
|
+
// it needs a file location and must not be ignore-listed bundler/framework code
|
|
687
|
+
const isLocatableFrame = (stackFrame: StackFrame): boolean =>
|
|
688
|
+
Boolean(stackFrame.fileName) && !stackFrame.isIgnoreListed;
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Returns the stack of components that CREATED this fiber's JSX (the
|
|
692
|
+
* `_debugOwner` chain), with exact creation-site locations from React 19's
|
|
693
|
+
* `_debugStack` errors - including server component owners. Wrappers that
|
|
694
|
+
* merely render `{children}` do not appear; use {@link getParentStack} for
|
|
695
|
+
* the full render-tree ancestry. Falls back to {@link getParentStack} on
|
|
696
|
+
* React <19, when no trusted debug stacks exist, or when the owner chain
|
|
697
|
+
* yields no locatable frames (so callers always get the most useful stack
|
|
698
|
+
* available).
|
|
699
|
+
*/
|
|
700
|
+
export const getOwnerStack = async (
|
|
701
|
+
fiber: Fiber,
|
|
702
|
+
shouldCache = true,
|
|
703
|
+
fetchFunction?: (url: string) => Promise<Response>,
|
|
704
|
+
): Promise<StackFrame[]> => {
|
|
705
|
+
const debugStackFrames = getOwnerStackFromDebugStacks(fiber);
|
|
706
|
+
if (debugStackFrames.length > 0) {
|
|
707
|
+
// the owner chain does not include the fiber itself, but bippy's stacks
|
|
708
|
+
// always start with the fiber's own frame
|
|
709
|
+
const selfFrame: StackFrame = getDefinitionFrameFromOwnedChild(fiber) ?? {};
|
|
710
|
+
selfFrame.functionName = getDisplayName(fiber.type) ?? selfFrame.functionName;
|
|
711
|
+
const symbolicatedFrames = await symbolicateStack(
|
|
712
|
+
[selfFrame, ...debugStackFrames],
|
|
713
|
+
shouldCache,
|
|
714
|
+
fetchFunction,
|
|
715
|
+
);
|
|
716
|
+
const hasLocatableOwnerFrame = symbolicatedFrames.some(
|
|
717
|
+
(stackFrame, frameIndex) => frameIndex > 0 && isLocatableFrame(stackFrame),
|
|
718
|
+
);
|
|
719
|
+
if (hasLocatableOwnerFrame) {
|
|
720
|
+
return symbolicatedFrames;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return getParentStack(fiber, shouldCache, fetchFunction);
|
|
725
|
+
};
|