diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab2a24..03004d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## next + +- Improved hook path capturing: + - Adjusted the stack trace limit to capture up to 25 entries, guaranteeing a minimum of 20 path entries, an improvement from the previous 6 (fixes #30) + - Implemented an alternative method for path extraction to cater to scenarios where a function is either unnamed or its name doesn't start with `use` + ## 0.7.3 (June 9, 2023) - Added `StrictMode` capturing and displaying in components tree diff --git a/playground/cases/hooks.tsx b/playground/cases/hooks.tsx index 56867bd..54c0a3b 100644 --- a/playground/cases/hooks.tsx +++ b/playground/cases/hooks.tsx @@ -92,13 +92,34 @@ function Root() { } function useFoo() { + badNameHook(); + evalHook(); + anonymousHook(); + anonymousHookFactory(); return useBar(); } function useBar() { + const [, setState] = React.useState(0); + + React.useEffect(() => { + setState(1); + }, []); + return React.useContext(CtxA); } +function badNameHook() { + return useBar(); +} + +const evalHook = new Function("useBar", "return () => useBar()")(useBar); +const anonymousHook = () => useBar(); +const anonymousHookFactory = ( + () => () => + useBar() +)(); + const Child = React.forwardRef(function Child( { prop = 123 }: { prop: number }, ref diff --git a/src/publisher/react-integration/dispatcher-trap.ts b/src/publisher/react-integration/dispatcher-trap.ts index 28c8bb1..b71a431 100644 --- a/src/publisher/react-integration/dispatcher-trap.ts +++ b/src/publisher/react-integration/dispatcher-trap.ts @@ -12,7 +12,7 @@ import { HookCompute, } from "../types"; import { CoreApi } from "./core"; -import { extractCallLoc, parseStackTraceLine } from "./utils/stackTrace"; +import { extractCallLoc, getParsedStackTrace } from "./utils/stackTrace"; type StateHookName = "useState" | "useReducer" | "useTransition"; type MemoHookName = "useMemo" | "useCallback"; @@ -43,10 +43,8 @@ type FiberDispatcherInfo = { hooks: HookInfo[]; }; -function extractHookPath(depth = 0) { - const stack = String(new Error().stack) - .split("\n") - .slice(4 + depth); +function extractHookPath(depth = 0, stopHookPathLocation: string | null) { + const stack = getParsedStackTrace(depth + 4); const path = []; const result: TransferCallTrace = { path: undefined, @@ -54,19 +52,18 @@ function extractHookPath(depth = 0) { }; let prev: TransferCallTrace | TransferCallTracePoint = result; - for (const line of stack) { - const parsed = parseStackTraceLine(line); - + for (const parsed of stack) { if (!parsed) { break; } - prev.loc = parsed.loc; - - if (!parsed.name.startsWith("use")) { + if (parsed.loc === stopHookPathLocation) { + path.shift(); + path.shift(); break; } + prev.loc = parsed.loc; path.unshift( (prev = { name: parsed.name, @@ -96,6 +93,7 @@ export function createDispatcherTrap( let currentEffectName: "effect" | "layout-effect" | null = null; let currentFiberHookIndex = 0; let dispatchCalls: FiberDispatchCall[] = []; + let stopHookPathLocation: string | null = null; // let currentFiberRerenderState: RerenderState | null = null; const knownDispatcher = new Set(); const ignoreDispatcherTransition = new Set(); @@ -123,7 +121,7 @@ export function createDispatcherTrap( name, deps, context, - trace: extractHookPath(1), + trace: extractHookPath(1, stopHookPathLocation), }); } @@ -386,9 +384,7 @@ export function createDispatcherTrap( if (typeof renderer.getCurrentFiber === "function") { Object.defineProperty(renderer.currentDispatcherRef, "current", { - get() { - return currentDispatcher; - }, + get: () => currentDispatcher, set(nextDispatcher: Dispatcher | null) { const nextCurrentFiber = renderer.getCurrentFiber(); const prevDispatcher = currentDispatcher; @@ -400,6 +396,7 @@ export function createDispatcherTrap( currentFiber = nextCurrentFiber; currentFiberCollectInfo = null; currentFiberHookIndex = 0; + currentRoot = null; if (currentFiber !== null) { const alternate = currentFiber.alternate; @@ -412,6 +409,7 @@ export function createDispatcherTrap( ); if (!fiberTypeInfo.has(fiberTypeId)) { + stopHookPathLocation = extractCallLoc(1); fiberTypeInfo.set( fiberTypeId, (currentFiberCollectInfo = { diff --git a/src/publisher/react-integration/utils/stackTrace.ts b/src/publisher/react-integration/utils/stackTrace.ts index e779f62..48833e3 100644 --- a/src/publisher/react-integration/utils/stackTrace.ts +++ b/src/publisher/react-integration/utils/stackTrace.ts @@ -2,21 +2,43 @@ // https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js const UNKNOWN_FUNCTION = ""; +const hasOwn = + Object.hasOwn || + ((target, prop) => Object.prototype.hasOwnProperty.call(target, prop)); type LineParseResult = null | { name: string; loc: string | null; }; -function getCallStackLine(depth: number) { - const stack = String(new Error().stack).split("\n"); - - return stack[stack[0] === "Error" ? depth + 1 : depth]; +function lazyParseArray( + array: T[], + parse: (val: T) => LineParseResult +) { + const cache: LineParseResult[] = []; + + return new Proxy(array, { + get(target, prop) { + if (typeof prop === "string") { + const index = Number(prop); + + if (isFinite(index) && hasOwn(array, prop)) { + // Check if the value at the index is already parsed + if (typeof cache[index] === "undefined") { + cache[index] = parse(array[index]); + } + + return cache[index]; + } + } + + return (target as any)[prop]; + }, + }); } export function extractCallLoc(depth: number) { - const line = getCallStackLine(depth + 3); - const parsed = line ? parseStackTraceLine(line) : null; + const parsed = getParsedStackTrace()[depth + 2]; if (parsed && parsed.loc) { return parsed.loc; @@ -25,22 +47,51 @@ export function extractCallLoc(depth: number) { return null; } -/** - * This parses the different stack traces and puts them into one format - * This borrows heavily from TraceKit (https://github.com/csnover/TraceKit) - */ -export function parseStackTrace(stackString: string) { - const lines = stackString.split("\n"); +export function getParsedStackTrace(skip = 1, limit = 25) { + const prevPrepareStackTrace = Error.prepareStackTrace; + const prevStackTraceLimit = Error.stackTraceLimit; + + try { + Error.stackTraceLimit = limit; + Error.prepareStackTrace = (_, callSites) => { + result = lazyParseArray(callSites.slice(skip), parseCallSite); + + return ""; + }; - return lines.reduce((stack, line) => { - const parseResult = parseStackTraceLine(line); + let result: NodeJS.CallSite[] | string[] | null = null; + const stack = new Error().stack; - if (parseResult) { - stack.push(parseResult); + if (result === null && stack) { + const lines = stack.trim().split("\n"); + + result = lazyParseArray( + lines.slice(lines[0] === "Error" ? skip + 1 : skip), + parseStackTraceLine + ); } - return stack; - }, [] as LineParseResult[]); + return (result || []) as unknown as LineParseResult[]; // TS doesn't handle Proxy right + } finally { + Error.stackTraceLimit = prevStackTraceLimit; + Error.prepareStackTrace = prevPrepareStackTrace; + } +} + +function parseCallSite( + callSite: NodeJS.CallSite & { getScriptNameOrSourceURL?: () => string | null } +) { + const filename = + typeof callSite.getScriptNameOrSourceURL === "function" + ? callSite.getScriptNameOrSourceURL() + : callSite.getFileName(); + + return { + loc: filename + ? `${filename}:${callSite.getLineNumber()}:${callSite.getColumnNumber()}` + : null, + name: callSite.getFunctionName() || UNKNOWN_FUNCTION, + }; } export function parseStackTraceLine(line: string): LineParseResult {