import { Fiber } from "../types.js"; import { FiberSource } from "./types.js"; import { SCHEME_REGEX, INTERNAL_SCHEME_PREFIXES, ABOUT_REACT_PREFIX, ANONYMOUS_FILE_PATTERNS, SOURCE_FILE_EXTENSION_REGEX, BUNDLED_FILE_PATTERN_REGEX, QUERY_PARAMETER_PATTERN_REGEX, } from "./constants.js"; import { getOwnerStack } from "./owner-stack.js"; export const hasDebugSource = ( fiber: Fiber, ): fiber is Fiber & { _debugSource: NonNullable; } => { const debugSource = fiber._debugSource; if (!debugSource) { return false; } return ( typeof debugSource === "object" && debugSource !== null && "fileName" in debugSource && typeof debugSource.fileName === "string" && "lineNumber" in debugSource && typeof debugSource.lineNumber === "number" ); }; /** * Returns the source of where the component is used. Available only in dev, for composite {@link Fiber}s. * * @example * ```ts * function Parent() { * const data = useData(); * return ; // <-- captures THIS line * } * * function Child({ name }) { * return
{name}
; * } * * const source = await getSource(fiber); * console.log(source.fileName, source.lineNumber); * ``` */ export const getSource = async ( fiber: Fiber, cache = true, fetchFn?: (url: string) => Promise, ): Promise => { if (hasDebugSource(fiber)) { const debugSource = fiber._debugSource; return debugSource || null; } const componentStack = await getOwnerStack(fiber, cache, fetchFn); for (const stackFrame of componentStack) { if (stackFrame.fileName) { return { fileName: stackFrame.fileName, lineNumber: stackFrame.lineNumber, columnNumber: stackFrame.columnNumber, functionName: stackFrame.functionName, }; } } return null; }; const getPathSegmentCount = (path: string): number => path.split("/").filter(Boolean).length; const getFirstPathSegment = (path: string): string | null => { const segments = path.split("/").filter(Boolean); return segments[0] ?? null; }; const stripSingleBasePathPrefix = (path: string): string => { const firstSlashIndex = path.indexOf("/", 1); if (firstSlashIndex === -1) { return path; } const basePath = path.slice(0, firstSlashIndex); if (getPathSegmentCount(basePath) !== 1) { return path; } const remainderPath = path.slice(firstSlashIndex); if (!SOURCE_FILE_EXTENSION_REGEX.test(remainderPath)) { return path; } if (getPathSegmentCount(remainderPath) < 2) { return path; } const firstRemainderSegment = getFirstPathSegment(remainderPath); if (!firstRemainderSegment) { return path; } if (firstRemainderSegment.startsWith("@")) { return path; } if (firstRemainderSegment.length > 4) { return path; } return remainderPath; }; export const normalizeFileName = (fileName: string): string => { if (!fileName) { return ""; } if (ANONYMOUS_FILE_PATTERNS.some((pattern) => pattern === fileName)) { return ""; } let normalizedFileName = fileName; const isHttpUrl = normalizedFileName.startsWith("http://") || normalizedFileName.startsWith("https://"); if (isHttpUrl) { try { const parsedUrl = new URL(normalizedFileName); normalizedFileName = parsedUrl.pathname; } catch {} } if (isHttpUrl) { normalizedFileName = stripSingleBasePathPrefix(normalizedFileName); } if (normalizedFileName.startsWith(ABOUT_REACT_PREFIX)) { const remainder = normalizedFileName.slice(ABOUT_REACT_PREFIX.length); const slashIndex = remainder.indexOf("/"); const colonIndex = remainder.indexOf(":"); if (slashIndex !== -1 && (colonIndex === -1 || slashIndex < colonIndex)) { normalizedFileName = remainder.slice(slashIndex + 1); } else { normalizedFileName = remainder; } } let didStripPrefix = true; while (didStripPrefix) { didStripPrefix = false; for (const prefix of INTERNAL_SCHEME_PREFIXES) { if (normalizedFileName.startsWith(prefix)) { normalizedFileName = normalizedFileName.slice(prefix.length); if (prefix === "file:///") { normalizedFileName = `/${normalizedFileName.replace(/^\/+/, "")}`; } didStripPrefix = true; break; } } } if (SCHEME_REGEX.test(normalizedFileName)) { const schemeMatch = normalizedFileName.match(SCHEME_REGEX); if (schemeMatch) { normalizedFileName = normalizedFileName.slice(schemeMatch[0].length); } } if (normalizedFileName.startsWith("//")) { const firstPathSlashIndex = normalizedFileName.indexOf("/", 2); normalizedFileName = firstPathSlashIndex === -1 ? "" : normalizedFileName.slice(firstPathSlashIndex); } const queryParameterIndex = normalizedFileName.indexOf("?"); if (queryParameterIndex !== -1) { const potentialQueryParameters = normalizedFileName.slice(queryParameterIndex); if (QUERY_PARAMETER_PATTERN_REGEX.test(potentialQueryParameters)) { normalizedFileName = normalizedFileName.slice(0, queryParameterIndex); } } return normalizedFileName; }; export const isSourceFile = (fileName: string): boolean => { const normalizedFileName = normalizeFileName(fileName); if (!normalizedFileName) { return false; } if (!SOURCE_FILE_EXTENSION_REGEX.test(normalizedFileName)) { return false; } if (BUNDLED_FILE_PATTERN_REGEX.test(normalizedFileName)) { return false; } return true; };