Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,7 @@ These definitions follow React’s private implementation and may change between

### `getSource`

Returns the source location for a Fiber from these renderers:

- DOM
- Native
- Terminal
- Canvas
- PDF
- Custom
Returns the source location for a Fiber.

```typescript
import { getSource } from "bippy/source";
Expand Down
8 changes: 5 additions & 3 deletions packages/bippy/src/source/inspect-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
BippyUnsupportedHookError,
} from "../errors.js";
import { getReactWorkTagsForFiber } from "../react-internals/index.js";
import { parseStack, type StackFrame } from "./parse-stack.js";
import { createStackParser, parseStack, type StackFrame } from "./parse-stack.js";
import {
getRendererDispatcherRefs,
readDispatcher,
Expand Down Expand Up @@ -604,8 +604,9 @@ const findPrimitiveIndex = (hookStack: StackFrame[], hook: HookLogEntry): number
const parseTrimmedStack = (
rootStack: StackFrame[],
hook: HookLogEntry,
parseHookStack: (stack: string) => StackFrame[],
): [StackFrame | null, StackFrame[] | null] => {
const hookStack = parseErrorStack(hook.stackError);
const hookStack = parseHookStack(hook.stackError.stack || "");
const rootIndex = findCommonAncestorIndex(rootStack, hookStack);
const primitiveIndex = findPrimitiveIndex(hookStack, hook);
if (rootIndex === -1 || primitiveIndex === -1 || rootIndex - primitiveIndex < 2) {
Expand All @@ -626,13 +627,14 @@ const NON_ID_HOOK_PRIMITIVES = new Set([

const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): HooksTree => {
const rootChildren: HooksNode[] = [];
const parseHookStack = createStackParser();
let previousStack: StackFrame[] | null = null;
let levelChildren = rootChildren;
let nativeHookID = 0;
const childrenStack: HooksNode[][] = [];

for (const hook of capturedHookLog) {
const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook, parseHookStack);
let displayName = hook.displayName;
if (displayName === null && primitiveFrame !== null) {
const primitiveName = parseHookName(primitiveFrame.functionName);
Expand Down
174 changes: 111 additions & 63 deletions packages/bippy/src/source/parse-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ export const parseStack = (stackString: string, options?: ParseOptions): StackFr
const frames: StackFrame[] = [];
for (const rawLine of lines) {
if (/^\s*at\s+/.test(rawLine)) {
const parsed = parseV8OrIeString(rawLine)[0];
if (parsed) frames.push(parsed);
if (CHROME_IE_STACK_REGEXP.test(rawLine)) frames.push(parseV8Line(rawLine));
} else if (/^\s*in\s+/.test(rawLine)) {
const elementName = rawLine
.replace(/^\s*in\s+/, "")
.replace(/\s*(?:\(at .*\)|\[[^\]]+\])$/, "");
frames.push({ functionName: elementName, source: rawLine });
} else if (rawLine.match(FIREFOX_SAFARI_STACK_REGEXP)) {
const parsed = parseFFOrSafariString(rawLine)[0];
if (parsed) frames.push(parsed);
if (!SAFARI_NATIVE_CODE_REGEXP.test(rawLine)) frames.push(parseSafariLine(rawLine));
}
}
return frames;
Expand All @@ -48,6 +46,18 @@ export const parseStack = (stackString: string, options?: ParseOptions): StackFr
return parseFFOrSafariString(stackString);
};

const getPositionIndex = (location: string, endIndex: number): number => {
let positionIndex = endIndex - 1;
while (positionIndex >= 0) {
const character = location.charCodeAt(positionIndex);
if (character < 48 || character > 57) break;
positionIndex--;
}
return positionIndex < endIndex - 1 && location.charCodeAt(positionIndex) === 58
? positionIndex
: -1;
};

export const extractLocation = (
urlLike: string,
): [string, string | undefined, string | undefined] => {
Expand All @@ -59,77 +69,115 @@ export const extractLocation = (
const isWrappedLocation = urlLike.startsWith("(") && /:\d+\)$/.test(urlLike);
const sanitizedResult = isWrappedLocation ? urlLike.slice(1, -1) : urlLike;

const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
const parts = regExp.exec(sanitizedResult);
if (!parts) return [sanitizedResult, undefined, undefined];
return [parts[1], parts[2] || undefined, parts[3] || undefined] as const;
};
if (/[\n\r\u2028\u2029]/.test(sanitizedResult)) {
const parts = /(.+?)(?::(\d+))?(?::(\d+))?$/.exec(sanitizedResult);
return parts
? [parts[1], parts[2] || undefined, parts[3] || undefined]
: [sanitizedResult, undefined, undefined];
}

export const parseV8OrIeString = (stack: string): StackFrame[] => {
const filteredLines = stack.split("\n").filter((line) => {
return !!line.match(CHROME_IE_STACK_REGEXP);
});

return filteredLines.map((line): StackFrame => {
let currentLine = line;
if (currentLine.includes("(eval ")) {
currentLine = currentLine
.replace(/eval code/g, "eval")
.replace(/(\(eval at [^()]*)|(,.*$)/g, "");
}
let sanitizedLine = currentLine
.replace(/^\s+/, "")
.replace(/\(eval code/g, "(")
.replace(/^.*?\s+/, "");
const lastPositionIndex = getPositionIndex(sanitizedResult, sanitizedResult.length);
if (lastPositionIndex <= 0) return [sanitizedResult, undefined, undefined];
const previousPositionIndex = getPositionIndex(sanitizedResult, lastPositionIndex);
if (previousPositionIndex <= 0) {
return [
sanitizedResult.slice(0, lastPositionIndex),
sanitizedResult.slice(lastPositionIndex + 1),
undefined,
];
}
return [
sanitizedResult.slice(0, previousPositionIndex),
sanitizedResult.slice(previousPositionIndex + 1, lastPositionIndex),
sanitizedResult.slice(lastPositionIndex + 1),
];
};

const locationMatch = sanitizedLine.match(/ (\(.+\)$)/);
const parseV8Line = (line: string): StackFrame => {
let currentLine = line;
if (currentLine.includes("(eval ")) {
currentLine = currentLine
.replace(/eval code/g, "eval")
.replace(/(\(eval at [^()]*)|(,.*$)/g, "");
}
let sanitizedLine = currentLine
.replace(/^\s+/, "")
.replace(/\(eval code/g, "(")
.replace(/^.*?\s+/, "");

const locationMatch = sanitizedLine.match(/ (\(.+\)$)/);

sanitizedLine = locationMatch ? sanitizedLine.replace(locationMatch[0], "") : sanitizedLine;

const locationParts = extractLocation(locationMatch ? locationMatch[1] : sanitizedLine);
const functionName = (locationMatch && sanitizedLine) || undefined;
const fileName = ["eval", "<anonymous>", "(native)"].includes(locationParts[0])
? undefined
: locationParts[0];

return {
functionName,
fileName,
lineNumber: locationParts[1] ? +locationParts[1] : undefined,
columnNumber: locationParts[2] ? +locationParts[2] : undefined,
source: currentLine,
};
};

sanitizedLine = locationMatch ? sanitizedLine.replace(locationMatch[0], "") : sanitizedLine;
const parseSafariLine = (line: string): StackFrame => {
let currentLine = line;
if (currentLine.includes(" > eval"))
currentLine = currentLine.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");

const locationParts = extractLocation(locationMatch ? locationMatch[1] : sanitizedLine);
const functionName = (locationMatch && sanitizedLine) || undefined;
const fileName = ["eval", "<anonymous>", "(native)"].includes(locationParts[0])
? undefined
: locationParts[0];
if (!currentLine.includes("@") && !currentLine.includes(":")) {
return {
functionName: currentLine,
};
} else {
const functionNameRegex =
/(([^\n\r"\u2028\u2029]*".[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*(?:@[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*)*(?:[\n\r\u2028\u2029][^@]*)?)?[^@]*)@/;
const matches = currentLine.match(functionNameRegex);
const functionName = matches && matches[1] ? matches[1] : undefined;
const locationParts = extractLocation(currentLine.replace(functionNameRegex, ""));

return {
functionName,
fileName,
fileName: locationParts[0],
lineNumber: locationParts[1] ? +locationParts[1] : undefined,
columnNumber: locationParts[2] ? +locationParts[2] : undefined,
source: currentLine,
};
});
}
};

export const parseFFOrSafariString = (stack: string): StackFrame[] => {
const filteredLines = stack.split("\n").filter((line) => {
return !line.match(SAFARI_NATIVE_CODE_REGEXP);
});

return filteredLines.map((line): StackFrame => {
let currentLine = line;
if (currentLine.includes(" > eval"))
currentLine = currentLine.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");

if (!currentLine.includes("@") && !currentLine.includes(":")) {
return {
functionName: currentLine,
};
} else {
const functionNameRegex =
/(([^\n\r"\u2028\u2029]*".[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*(?:@[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*)*(?:[\n\r\u2028\u2029][^@]*)?)?[^@]*)@/;
const matches = currentLine.match(functionNameRegex);
const functionName = matches && matches[1] ? matches[1] : undefined;
const locationParts = extractLocation(currentLine.replace(functionNameRegex, ""));

return {
functionName,
fileName: locationParts[0],
lineNumber: locationParts[1] ? +locationParts[1] : undefined,
columnNumber: locationParts[2] ? +locationParts[2] : undefined,
source: currentLine,
};
const parseLines = (
stack: string,
isV8: boolean,
cache?: Map<string, StackFrame>,
): StackFrame[] => {
const frames: StackFrame[] = [];
for (const line of stack.split("\n")) {
let frame = cache?.get(line);
if (!frame) {
if (isV8 ? !CHROME_IE_STACK_REGEXP.test(line) : SAFARI_NATIVE_CODE_REGEXP.test(line))
continue;
frame = isV8 ? parseV8Line(line) : parseSafariLine(line);
cache?.set(line, frame);
}
});
frames.push(frame);
}
return frames;
};

export const parseV8OrIeString = (stack: string): StackFrame[] => parseLines(stack, true);

export const parseFFOrSafariString = (stack: string): StackFrame[] => parseLines(stack, false);

export const createStackParser = () => {
const v8Frames = new Map<string, StackFrame>();
const safariFrames = new Map<string, StackFrame>();
return (stack: string): StackFrame[] => {
const isV8 = CHROME_IE_STACK_REGEXP.test(stack);
return parseLines(stack, isV8, isV8 ? v8Frames : safariFrames);
};
};
19 changes: 15 additions & 4 deletions packages/bippy/src/source/symbolication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ export const getSourceFromSourceMap = (
);
};

const getStringIndex = (values: string[], target: string): number => {
for (let valueIndex = 0; valueIndex < values.length; valueIndex++) {
if (values[valueIndex] === target) return valueIndex;
}
return -1;
};

const getSourceFromMappingsByFunctionName = (
mappings: SourceMapMappings,
sources: string[],
Expand All @@ -215,13 +222,17 @@ const getSourceFromMappingsByFunctionName = (
ignoredSourceIndices?: Set<number>,
): StackFrame | null => {
if (!names) return null;
const functionNameIndex = names.indexOf(functionName);
const functionNameIndex = getStringIndex(names, functionName);
if (functionNameIndex === -1) return null;

let ignoredSource: StackFrame | null = null;
for (const lineMapping of mappings) {
for (const segment of lineMapping) {
for (let lineIndex = 0; lineIndex < mappings.length; lineIndex++) {
const lineMapping = mappings[lineIndex];
for (let segmentIndex = 0; segmentIndex < lineMapping.length; segmentIndex++) {
const segment = lineMapping[segmentIndex];
if (segment[4] !== functionNameIndex) continue;
if (ignoredSource && segment[1] !== undefined && ignoredSourceIndices?.has(segment[1]))
continue;
const source = getSourceFromSegment(segment, sources, ignoredSourceIndices, names);
if (!source) continue;
if (!source.isIgnoreListed) return source;
Expand Down Expand Up @@ -267,7 +278,7 @@ const findSourceContentByFileName = (
fileName: string,
): string | null => {
if (!sourcesContent) return null;
const sourceIndex = sources.indexOf(fileName);
const sourceIndex = getStringIndex(sources, fileName);
return sourceIndex === -1 ? null : (sourcesContent[sourceIndex] ?? null);
};

Expand Down
Loading
Loading