diff --git a/.gitignore b/.gitignore index 3136ae77..64222599 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ dist coverage react-grab-extension.zip tsup.config.bundled_*.mjs -packages/website/public/react-grab.global.js \ No newline at end of file +packages/website/public/react-grab.global.js +tmp \ No newline at end of file diff --git a/packages/react-grab-claude-code/package.json b/packages/react-grab-claude-code/package.json index 74d27637..da728f53 100644 --- a/packages/react-grab-claude-code/package.json +++ b/packages/react-grab-claude-code/package.json @@ -28,6 +28,7 @@ "build": "rm -rf dist && NODE_ENV=production tsup" }, "devDependencies": { + "@types/node": "^20.0.0", "tsup": "^8.4.0" }, "dependencies": { diff --git a/packages/react-grab-claude-code/tsconfig.json b/packages/react-grab-claude-code/tsconfig.json index 47264605..cad2400d 100644 --- a/packages/react-grab-claude-code/tsconfig.json +++ b/packages/react-grab-claude-code/tsconfig.json @@ -8,7 +8,8 @@ "skipLibCheck": true, "declaration": true, "declarationMap": true, - "noEmit": true + "noEmit": true, + "types": ["node"] }, "include": ["src"] } diff --git a/packages/react-grab-cursor/package.json b/packages/react-grab-cursor/package.json index da087dfe..7c46cbf3 100644 --- a/packages/react-grab-cursor/package.json +++ b/packages/react-grab-cursor/package.json @@ -28,6 +28,7 @@ "build": "rm -rf dist && NODE_ENV=production tsup" }, "devDependencies": { + "@types/node": "^20.0.0", "tsup": "^8.4.0" }, "dependencies": { diff --git a/packages/react-grab-cursor/tsconfig.json b/packages/react-grab-cursor/tsconfig.json index eb51098d..f73b95c7 100644 --- a/packages/react-grab-cursor/tsconfig.json +++ b/packages/react-grab-cursor/tsconfig.json @@ -8,7 +8,8 @@ "skipLibCheck": true, "declaration": true, "declarationMap": true, - "noEmit": true + "noEmit": true, + "types": ["node"] }, "include": ["src"] } diff --git a/packages/react-grab/src/context.ts b/packages/react-grab/src/context.ts index e152bebd..b28f637e 100644 --- a/packages/react-grab/src/context.ts +++ b/packages/react-grab/src/context.ts @@ -1,11 +1,20 @@ import { isSourceFile, normalizeFileName, - getOwnerStack, - StackFrame, + getSource, + FiberSource, } from "bippy/source"; import { isCapitalized } from "./utils/is-capitalized.js"; -import { getFiberFromHostInstance, isInstrumentationActive } from "bippy"; +import { + getFiberFromHostInstance, + isInstrumentationActive, + getLatestFiber, + isFiber, + isHostFiber, + isCompositeFiber, + getDisplayName, + traverseFiber, +} from "bippy"; const NEXT_INTERNAL_COMPONENT_NAMES = new Set([ "InnerLayoutRouter", @@ -57,13 +66,58 @@ export const checkIsSourceComponentName = (name: string): boolean => { return true; }; +interface StackFrame { + name: string; + source: FiberSource | null; +} + +interface UnresolvedStackFrame { + name: string; + sourcePromise: Promise; +} + export const getStack = async ( element: Element, ): Promise => { if (!isInstrumentationActive()) return []; - const fiber = getFiberFromHostInstance(element); - if (!fiber) return null; - return await getOwnerStack(fiber); + + try { + const maybeFiber = getFiberFromHostInstance(element); + if (!maybeFiber || !isFiber(maybeFiber)) return []; + const fiber = getLatestFiber(maybeFiber); + + const unresolvedStack: Array = []; + + traverseFiber( + fiber, + (currentFiber) => { + const displayName = isHostFiber(currentFiber) + ? typeof currentFiber.type === "string" + ? currentFiber.type + : null + : getDisplayName(currentFiber); + + if (displayName && !checkIsInternalComponentName(displayName)) { + unresolvedStack.push({ + name: displayName, + sourcePromise: getSource(currentFiber), + }); + } + }, + true, + ); + + const resolvedStack = await Promise.all( + unresolvedStack.map(async (frame) => ({ + name: frame.name, + source: await frame.sourcePromise, + })), + ); + + return resolvedStack.filter((frame) => frame.source !== null); + } catch { + return []; + } }; export const getNearestComponentName = async ( @@ -74,8 +128,8 @@ export const getNearestComponentName = async ( if (!stack) return null; for (const frame of stack) { - if (frame.functionName && checkIsSourceComponentName(frame.functionName)) { - return frame.functionName; + if (frame.name && checkIsSourceComponentName(frame.name)) { + return frame.name; } } @@ -86,11 +140,26 @@ interface GetElementContextOptions { maxLines?: number; } +const formatFileName = (fileName: string): string => { + const normalized = normalizeFileName(fileName); + + // For Vite projects, try to create a dev server URL format + if (typeof window !== 'undefined' && window.location.port) { + // Extract src path if it exists + const srcMatch = normalized.match(/\/src\/.+$/); + if (srcMatch) { + return `//${window.location.host}${srcMatch[0]}`; + } + } + + return normalized; +}; + export const getElementContext = async ( element: Element, options: GetElementContextOptions = {}, ): Promise => { - const { maxLines = 3 } = options; + const { maxLines = 10 } = options; const html = getHTMLPreview(element); const stack = await getStack(element); const isNextProject = checkIsNextProject(); @@ -100,36 +169,31 @@ export const getElementContext = async ( for (const frame of stack) { if (stackContext.length >= maxLines) break; - if ( - frame.isServer && - (!frame.functionName || checkIsSourceComponentName(frame.functionName)) - ) { - stackContext.push( - `\n in ${frame.functionName || ""} (at Server)`, - ); + if (!frame.source) { + stackContext.push(`\n at ${frame.name}`); continue; } - if (frame.fileName && isSourceFile(frame.fileName)) { - let line = "\n in "; - const hasComponentName = - frame.functionName && checkIsSourceComponentName(frame.functionName); - - if (hasComponentName) { - line += `${frame.functionName} (at `; - } - line += normalizeFileName(frame.fileName); + if (frame.source.fileName.startsWith("about://React/Server")) { + stackContext.push(`\n at ${frame.name} (Server)`); + continue; + } - // HACK: bundlers like vite mess up the line number and column number - if (isNextProject && frame.lineNumber && frame.columnNumber) { - line += `:${frame.lineNumber}:${frame.columnNumber}`; - } + if (!isSourceFile(frame.source.fileName)) { + stackContext.push(`\n at ${frame.name}`); + continue; + } - if (hasComponentName) { - line += `)`; - } + const formattedFileName = formatFileName(frame.source.fileName); + const framePart = `\n at ${frame.name} in ${formattedFileName}`; - stackContext.push(line); + if (isNextProject) { + stackContext.push( + `${framePart}:${frame.source.lineNumber}:${frame.source.columnNumber}`, + ); + } else { + // bundlers like vite mess up the line number and column number + stackContext.push(framePart); } } } @@ -139,8 +203,8 @@ export const getElementContext = async ( export const getFileName = (stack: Array): string | null => { for (const frame of stack) { - if (frame.fileName && isSourceFile(frame.fileName)) { - return normalizeFileName(frame.fileName); + if (frame.source && isSourceFile(frame.source.fileName)) { + return normalizeFileName(frame.source.fileName); } } return null; diff --git a/packages/react-grab/src/core.tsx b/packages/react-grab/src/core.tsx index cdbbafcb..d2104550 100644 --- a/packages/react-grab/src/core.tsx +++ b/packages/react-grab/src/core.tsx @@ -812,9 +812,9 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { .then((stack) => { if (!stack) return; for (const frame of stack) { - if (frame.fileName && isSourceFile(frame.fileName)) { - setSelectionFilePath(normalizeFileName(frame.fileName)); - setSelectionLineNumber(frame.lineNumber); + if (frame.source && isSourceFile(frame.source.fileName)) { + setSelectionFilePath(normalizeFileName(frame.source.fileName)); + setSelectionLineNumber(frame.source.lineNumber); return; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61b999ae..25aa3cf7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: specifier: workspace:* version: link:../react-grab devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.23 tsup: specifier: ^8.4.0 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -216,6 +219,9 @@ importers: specifier: workspace:* version: link:../react-grab devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.23 tsup: specifier: ^8.4.0 version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)