bippy
Version:
hack into react internals
214 lines (179 loc) • 5.56 kB
text/typescript
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<Fiber["_debugSource"]>;
} => {
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 <Child name={data.name} />; // <-- captures THIS line
* }
*
* function Child({ name }) {
* return <div>{name}</div>;
* }
*
* const source = await getSource(fiber);
* console.log(source.fileName, source.lineNumber);
* ```
*/
export const getSource = async (
fiber: Fiber,
cache = true,
fetchFn?: (url: string) => Promise<Response>,
): Promise<FiberSource | null> => {
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;
};