bippy 0.7.3-dev.049a43d → 0.7.3-dev.0ac832d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,24 @@ export type {
18
18
  } from "./generated/react-work-tags.js";
19
19
  export * from "./types.js";
20
20
 
21
+ interface InheritedWorkTags {
22
+ workTags: Readonly<ReactWorkTagMap>;
23
+ generation: number;
24
+ }
25
+
21
26
  const defaultReactWorkTags = getReactWorkTags();
22
27
  const fiberReactWorkTags = new WeakMap<Fiber, Readonly<ReactWorkTagMap>>();
28
+ const inheritedFiberWorkTags = new WeakMap<Fiber, InheritedWorkTags>();
29
+ let workTagGeneration = 0;
30
+
31
+ const getCachedWorkTags = (fiber: Fiber): Readonly<ReactWorkTagMap> | undefined => {
32
+ const assignedWorkTags = fiberReactWorkTags.get(fiber);
33
+ if (assignedWorkTags) return assignedWorkTags;
34
+ const inheritedWorkTags = inheritedFiberWorkTags.get(fiber);
35
+ return inheritedWorkTags?.generation === workTagGeneration
36
+ ? inheritedWorkTags.workTags
37
+ : undefined;
38
+ };
23
39
 
24
40
  // React's experimental channel historically reported "0.0.0-experimental-<sha>"
25
41
  // as the runtime version; those builds use modern work tags, not the 16.x rows
@@ -39,25 +55,36 @@ export const getReactWorkTagsForRenderer = (
39
55
 
40
56
  export const setReactWorkTagsForFiber = (fiber: Fiber, renderer?: ReactRenderer): void => {
41
57
  const workTags = getReactWorkTagsForRenderer(renderer);
58
+ if (
59
+ getCachedWorkTags(fiber) !== workTags ||
60
+ (fiber.alternate && getCachedWorkTags(fiber.alternate) !== workTags)
61
+ ) {
62
+ workTagGeneration++;
63
+ }
42
64
  fiberReactWorkTags.set(fiber, workTags);
43
65
  if (fiber.alternate) fiberReactWorkTags.set(fiber.alternate, workTags);
44
66
  };
45
67
 
46
68
  export const getReactWorkTagsForFiber = (fiber: Fiber): Readonly<ReactWorkTagMap> => {
47
- const cachedWorkTags = fiberReactWorkTags.get(fiber);
48
- if (cachedWorkTags) return cachedWorkTags;
49
-
50
- const traversedFibers: Fiber[] = [fiber];
51
- let rootFiber = fiber;
52
- while (rootFiber.return) {
53
- rootFiber = rootFiber.return;
54
- traversedFibers.push(rootFiber);
69
+ let workTags = getCachedWorkTags(fiber);
70
+ if (workTags) return workTags;
71
+ const traversedFibers: Fiber[] = [];
72
+ let ancestor = fiber;
73
+ while (!workTags) {
74
+ traversedFibers.push(ancestor);
75
+ const parent = ancestor.return;
76
+ if (!parent) {
77
+ workTags = inheritedFiberWorkTags.get(ancestor)?.workTags ?? defaultReactWorkTags;
78
+ break;
79
+ }
80
+ ancestor = parent;
81
+ workTags = getCachedWorkTags(ancestor);
55
82
  }
56
- const workTags = fiberReactWorkTags.get(rootFiber) ?? defaultReactWorkTags;
83
+ const inheritedWorkTags = { workTags, generation: workTagGeneration };
57
84
  for (const traversedFiber of traversedFibers) {
58
- fiberReactWorkTags.set(traversedFiber, workTags);
85
+ inheritedFiberWorkTags.set(traversedFiber, inheritedWorkTags);
59
86
  if (traversedFiber.alternate) {
60
- fiberReactWorkTags.set(traversedFiber.alternate, workTags);
87
+ inheritedFiberWorkTags.set(traversedFiber.alternate, inheritedWorkTags);
61
88
  }
62
89
  }
63
90
  return workTags;
package/src/react.ts CHANGED
@@ -1,77 +1,162 @@
1
1
  import "./install-hook-only.js";
2
- import * as React from "react";
3
- import { isFiber } from "./core.js";
2
+ import React from "react";
3
+ import { isFiber, traverseFiber } from "./core.js";
4
+ import { _renderers } from "./rdt-hook.js";
5
+ import { getCurrentFiberFromRoot } from "./react-internals/current-fiber.js";
4
6
  import type { Fiber } from "./react-internals/index.js";
5
7
 
6
8
  export type { Fiber } from "./react-internals/index.js";
7
9
 
8
- const preserveState = (state: undefined): undefined => state;
9
- const readEmptySnapshot = (): undefined => undefined;
10
- const unsubscribeFromEmptyStore = (): void => {};
11
- const subscribeToEmptyStore = (): (() => void) => unsubscribeFromEmptyStore;
12
- const useSyncExternalStore: unknown = Reflect.get(React, "useSyncExternalStore");
10
+ interface RenderMarker {
11
+ (state: undefined): undefined;
12
+ }
13
13
 
14
- const captureFiberFromHook = (useCaptureHook: () => void): Fiber | null => {
15
- const originalBind = Function.prototype.bind;
16
- let capturedFiber: Fiber | null = null;
17
- // HACK: React binds hook callbacks to the rendering Fiber in production but exposes no public API for it.
14
+ interface ReducerQueue {
15
+ lastRenderedReducer: unknown;
16
+ }
17
+
18
+ interface FiberCapture {
19
+ fiber: Fiber;
20
+ queue: ReducerQueue | null;
21
+ }
22
+
23
+ const isReducerQueue = (value: unknown): value is ReducerQueue =>
24
+ typeof value === "object" && value !== null && "lastRenderedReducer" in value;
25
+
26
+ const hasRenderMarker = (fiber: Fiber, renderMarker: RenderMarker): boolean => {
27
+ for (let hook = fiber.memoizedState; hook; hook = hook.next) {
28
+ if (Array.isArray(hook.memoizedState) && hook.memoizedState[0] === renderMarker) return true;
29
+ }
30
+ return false;
31
+ };
32
+
33
+ const getReducerQueue = (fiber: Fiber, renderMarker: RenderMarker): ReducerQueue | null => {
34
+ for (let hook = fiber.memoizedState; hook; hook = hook.next) {
35
+ if (isReducerQueue(hook.queue) && hook.queue.lastRenderedReducer === renderMarker)
36
+ return hook.queue;
37
+ }
38
+ return null;
39
+ };
40
+
41
+ const getDevToolsFiber = (renderMarker: RenderMarker): Fiber | null => {
42
+ for (const renderer of _renderers) {
43
+ try {
44
+ const fiber = renderer.getCurrentFiber?.();
45
+ if (fiber && isFiber(fiber) && hasRenderMarker(fiber, renderMarker)) return fiber;
46
+ } catch {}
47
+ }
48
+ return null;
49
+ };
50
+
51
+ const captureReducerFiber = (
52
+ renderMarker: RenderMarker,
53
+ shouldCapture: boolean,
54
+ ): FiberCapture | null => {
55
+ let originalBind: typeof Function.prototype.bind | undefined;
56
+ if (shouldCapture) {
57
+ try {
58
+ originalBind = Function.prototype.bind;
59
+ } catch {}
60
+ }
61
+ if (!originalBind) {
62
+ React.useReducer(renderMarker, undefined);
63
+ return null;
64
+ }
65
+
66
+ let capture: FiberCapture | null = null;
67
+ let isCapturing = true;
68
+ // HACK: Production React binds a new reducer's Fiber and queue; validate the private reducer marker, not argument positions.
18
69
  const bindProxy = new Proxy(originalBind, {
19
- apply: (bind, functionToBind, boundArguments) => {
20
- const fiber = boundArguments[1];
21
- if (!capturedFiber && isFiber(fiber)) {
22
- capturedFiber = fiber;
70
+ apply: (bind, callback, boundArguments) => {
71
+ if (isCapturing && !capture) {
72
+ try {
73
+ const queue = boundArguments.find(
74
+ (argument): argument is ReducerQueue =>
75
+ isReducerQueue(argument) && argument.lastRenderedReducer === renderMarker,
76
+ );
77
+ if (queue) {
78
+ const fiber = boundArguments.find(
79
+ (argument): argument is Fiber => isFiber(argument) && isFiber(argument.return),
80
+ );
81
+ if (fiber) capture = { fiber, queue };
82
+ }
83
+ } catch {}
23
84
  }
24
- return Reflect.apply(bind, functionToBind, boundArguments);
85
+ return Reflect.apply(bind, callback, boundArguments);
25
86
  },
26
87
  });
27
- Reflect.set(Function.prototype, "bind", bindProxy);
28
88
 
29
89
  try {
30
- useCaptureHook();
90
+ try {
91
+ Reflect.set(Function.prototype, "bind", bindProxy);
92
+ } catch {}
93
+ React.useReducer(renderMarker, undefined);
94
+ return capture;
31
95
  } finally {
32
- if (Function.prototype.bind === bindProxy) {
33
- Reflect.set(Function.prototype, "bind", originalBind);
34
- }
96
+ isCapturing = false;
97
+ capture = null;
98
+ try {
99
+ if (Function.prototype.bind === bindProxy) {
100
+ Reflect.set(Function.prototype, "bind", originalBind);
101
+ }
102
+ } catch {}
35
103
  }
36
-
37
- return capturedFiber;
38
- };
39
-
40
- const useExternalStoreCapture = (): void => {
41
- if (typeof useSyncExternalStore !== "function") return;
42
- Reflect.apply(useSyncExternalStore, React, [
43
- subscribeToEmptyStore,
44
- readEmptySnapshot,
45
- readEmptySnapshot,
46
- ]);
47
104
  };
48
105
 
49
- const useReducerCapture = (): void => {
50
- React.useReducer(preserveState, undefined);
106
+ // HACK: React <16.13 attaches hooks after render; the shared reducer queue validates the root-walk fallback.
107
+ const getRenderingFiberFromRoot = (
108
+ { fiber, queue }: FiberCapture,
109
+ renderMarker: RenderMarker,
110
+ ): Fiber | null => {
111
+ if (queue?.lastRenderedReducer !== renderMarker) return null;
112
+ let rootFiber = fiber;
113
+ while (rootFiber.return) rootFiber = rootFiber.return;
114
+ const root = rootFiber.stateNode;
115
+ if (typeof root !== "object" || root === null || !("current" in root) || !isFiber(root.current)) {
116
+ return null;
117
+ }
118
+ const renderingRoot = root.current.alternate;
119
+ const renderingFiber = getCurrentFiberFromRoot(fiber)?.alternate;
120
+ if (renderingFiber) {
121
+ let ancestor = renderingFiber;
122
+ while (ancestor.return) ancestor = ancestor.return;
123
+ if (ancestor === renderingRoot) return renderingFiber;
124
+ }
125
+ return traverseFiber(renderingRoot, (candidate) => {
126
+ if (candidate !== fiber && candidate !== fiber.alternate) return false;
127
+ let parent = candidate;
128
+ while (parent.return) parent = parent.return;
129
+ return parent === renderingRoot;
130
+ });
51
131
  };
52
132
 
53
- const useFiberWithExternalStore = (): Fiber | undefined =>
54
- captureFiberFromHook(useExternalStoreCapture) ?? undefined;
55
-
56
- const useFiberWithReducer = (): Fiber | undefined => {
57
- const committedFiberRef = React.useRef<Fiber | null>(null);
58
- const renderedFiberRef = React.useRef<Fiber | null>(null);
59
- const hookFiber = captureFiberFromHook(useReducerCapture);
133
+ export const useFiber = (): Fiber | undefined => {
134
+ "use no memo";
135
+ const fiberRef = React.useRef<FiberCapture | null>(null);
136
+ const renderMarker: RenderMarker = (state) => state;
137
+ React.useMemo(() => renderMarker, [renderMarker]);
138
+ const devToolsFiber = fiberRef.current ? null : getDevToolsFiber(renderMarker);
139
+ const capture = captureReducerFiber(renderMarker, !fiberRef.current && !devToolsFiber);
140
+ if (devToolsFiber) {
141
+ fiberRef.current = {
142
+ fiber: devToolsFiber,
143
+ queue: getReducerQueue(devToolsFiber, renderMarker),
144
+ };
145
+ return devToolsFiber;
146
+ }
147
+ const knownCapture = capture ?? fiberRef.current;
148
+ if (!knownCapture) return undefined;
149
+ const knownFiber = knownCapture.fiber;
60
150
  const fiber =
61
- hookFiber ??
62
- (renderedFiberRef.current !== committedFiberRef.current
63
- ? renderedFiberRef.current
64
- : committedFiberRef.current?.alternate) ??
65
- committedFiberRef.current;
66
-
67
- renderedFiberRef.current = fiber;
68
-
69
- React.useEffect(() => {
70
- committedFiberRef.current = fiber;
71
- }, [fiber]);
72
-
151
+ capture?.fiber ??
152
+ (hasRenderMarker(knownFiber, renderMarker)
153
+ ? knownFiber
154
+ : knownFiber.alternate && hasRenderMarker(knownFiber.alternate, renderMarker)
155
+ ? knownFiber.alternate
156
+ : getRenderingFiberFromRoot(knownCapture, renderMarker));
157
+ if (fiber) {
158
+ knownCapture.fiber = fiber;
159
+ fiberRef.current = knownCapture;
160
+ }
73
161
  return fiber ?? undefined;
74
162
  };
75
-
76
- export const useFiber =
77
- typeof useSyncExternalStore === "function" ? useFiberWithExternalStore : useFiberWithReducer;
@@ -14,7 +14,7 @@ import {
14
14
  BippyUnsupportedHookError,
15
15
  } from "../errors.js";
16
16
  import { getReactWorkTagsForFiber } from "../react-internals/index.js";
17
- import { parseStack, type StackFrame } from "./parse-stack.js";
17
+ import { createStackParser, parseStack, type StackFrame } from "./parse-stack.js";
18
18
  import {
19
19
  getRendererDispatcherRefs,
20
20
  readDispatcher,
@@ -87,6 +87,16 @@ let currentHook: MemoizedState | null = null;
87
87
  let currentContextDependency: ContextDependency<unknown> | null = null;
88
88
  let currentThenableIndex = 0;
89
89
  let currentThenableState: unknown[] | null = null;
90
+ let currentMemoCacheIndex = 0;
91
+ let isInspectingHooks = false;
92
+
93
+ const assertNotInspectingHooks = (): void => {
94
+ if (isInspectingHooks) {
95
+ throw new BippyHookInspectionError(
96
+ "Hook inspection cannot be called during another inspection.",
97
+ );
98
+ }
99
+ };
90
100
 
91
101
  const SuspenseException: unknown = new Error(
92
102
  "Suspense interrupted this render. This error is an internal implementation detail of `use`.",
@@ -323,16 +333,8 @@ const dispatcherUseMemoCache = (size: number): unknown[] => {
323
333
  const memoCache = fiber.updateQueue?.memoCache;
324
334
  if (memoCache === null || memoCache === undefined) return [];
325
335
 
326
- let memoCacheSlots = memoCache.data[memoCache.index];
327
- if (memoCacheSlots === undefined) {
328
- memoCacheSlots = memoCache.data[memoCache.index] = Array.from(
329
- { length: size },
330
- () => REACT_MEMO_CACHE_SENTINEL,
331
- );
332
- }
333
-
334
- memoCache.index++;
335
- return memoCacheSlots;
336
+ const memoCacheSlots = memoCache.data[currentMemoCacheIndex++];
337
+ return memoCacheSlots?.slice() ?? Array.from({ length: size }, () => REACT_MEMO_CACHE_SENTINEL);
336
338
  };
337
339
 
338
340
  const dispatcherUseOptimistic = (passthrough: unknown): [unknown, () => void] => {
@@ -602,8 +604,9 @@ const findPrimitiveIndex = (hookStack: StackFrame[], hook: HookLogEntry): number
602
604
  const parseTrimmedStack = (
603
605
  rootStack: StackFrame[],
604
606
  hook: HookLogEntry,
607
+ parseHookStack: (stack: string) => StackFrame[],
605
608
  ): [StackFrame | null, StackFrame[] | null] => {
606
- const hookStack = parseErrorStack(hook.stackError);
609
+ const hookStack = parseHookStack(hook.stackError.stack || "");
607
610
  const rootIndex = findCommonAncestorIndex(rootStack, hookStack);
608
611
  const primitiveIndex = findPrimitiveIndex(hookStack, hook);
609
612
  if (rootIndex === -1 || primitiveIndex === -1 || rootIndex - primitiveIndex < 2) {
@@ -624,13 +627,14 @@ const NON_ID_HOOK_PRIMITIVES = new Set([
624
627
 
625
628
  const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): HooksTree => {
626
629
  const rootChildren: HooksNode[] = [];
630
+ const parseHookStack = createStackParser();
627
631
  let previousStack: StackFrame[] | null = null;
628
632
  let levelChildren = rootChildren;
629
633
  let nativeHookID = 0;
630
634
  const childrenStack: HooksNode[][] = [];
631
635
 
632
636
  for (const hook of capturedHookLog) {
633
- const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
637
+ const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook, parseHookStack);
634
638
  let displayName = hook.displayName;
635
639
  if (displayName === null && primitiveFrame !== null) {
636
640
  const primitiveName = parseHookName(primitiveFrame.functionName);
@@ -834,6 +838,7 @@ const performDispatcherInspection = <TArgs extends unknown[]>(
834
838
  ): HooksTree => {
835
839
  const previousDispatcher = readDispatcher(dispatcherRef);
836
840
  writeDispatcher(dispatcherRef, dispatcherProxy);
841
+ isInspectingHooks = true;
837
842
 
838
843
  let capturedHookLog: HookLogEntry[] = [];
839
844
  let ancestorStackError: Error | undefined;
@@ -846,6 +851,7 @@ const performDispatcherInspection = <TArgs extends unknown[]>(
846
851
  } finally {
847
852
  capturedHookLog = hookLog;
848
853
  hookLog = [];
854
+ isInspectingHooks = false;
849
855
  writeDispatcher(dispatcherRef, previousDispatcher);
850
856
  }
851
857
 
@@ -938,6 +944,7 @@ export const inspectHooks = (
938
944
  props: Record<string, unknown>,
939
945
  target: ReactDevToolsTarget = globalThis,
940
946
  ): HooksTree => {
947
+ assertNotInspectingHooks();
941
948
  const dispatcherRef = requireDispatcherRef(undefined, target);
942
949
  getPrimitiveStackCache();
943
950
  currentHook = null;
@@ -961,6 +968,7 @@ export const inspectHooks = (
961
968
  };
962
969
 
963
970
  export const getFiberHooks = (fiber: Fiber): HooksTree => {
971
+ assertNotInspectingHooks();
964
972
  const dispatcherRef = requireDispatcherRef(fiber);
965
973
  const workTags = getReactWorkTagsForFiber(fiber);
966
974
 
@@ -976,6 +984,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
976
984
 
977
985
  currentHook = fiber.memoizedState;
978
986
  currentFiber = fiber;
987
+ currentMemoCacheIndex = 0;
979
988
 
980
989
  const debugThenableState = fiber.dependencies?._debugThenableState;
981
990
  const usedThenables = Array.isArray(debugThenableState)
@@ -984,18 +993,16 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
984
993
  currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
985
994
  currentThenableIndex = 0;
986
995
 
987
- resolveContextDependency(fiber);
988
-
989
- const type = fiber.type;
990
- let props = fiber.memoizedProps;
991
- if (type !== fiber.elementType) {
992
- props = resolveDefaultProps(type, props);
993
- }
994
-
995
996
  const originalConsoleMethods = suppressConsole();
996
997
  const contextMap = new Map<ReactContext<unknown>, unknown>();
997
998
 
998
999
  try {
1000
+ resolveContextDependency(fiber);
1001
+ const type = fiber.type;
1002
+ const props =
1003
+ type !== fiber.elementType
1004
+ ? resolveDefaultProps(type, fiber.memoizedProps)
1005
+ : fiber.memoizedProps;
999
1006
  if (
1000
1007
  currentContextDependency !== null &&
1001
1008
  !Object.hasOwn(currentContextDependency, "memoizedValue")
@@ -1030,6 +1037,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
1030
1037
  currentContextDependency = null;
1031
1038
  currentThenableState = null;
1032
1039
  currentThenableIndex = 0;
1040
+ currentMemoCacheIndex = 0;
1033
1041
  restoreContexts(contextMap);
1034
1042
  restoreConsole(originalConsoleMethods);
1035
1043
  }
@@ -28,16 +28,14 @@ export const parseStack = (stackString: string, options?: ParseOptions): StackFr
28
28
  const frames: StackFrame[] = [];
29
29
  for (const rawLine of lines) {
30
30
  if (/^\s*at\s+/.test(rawLine)) {
31
- const parsed = parseV8OrIeString(rawLine)[0];
32
- if (parsed) frames.push(parsed);
31
+ if (CHROME_IE_STACK_REGEXP.test(rawLine)) frames.push(parseV8Line(rawLine));
33
32
  } else if (/^\s*in\s+/.test(rawLine)) {
34
33
  const elementName = rawLine
35
34
  .replace(/^\s*in\s+/, "")
36
35
  .replace(/\s*(?:\(at .*\)|\[[^\]]+\])$/, "");
37
36
  frames.push({ functionName: elementName, source: rawLine });
38
37
  } else if (rawLine.match(FIREFOX_SAFARI_STACK_REGEXP)) {
39
- const parsed = parseFFOrSafariString(rawLine)[0];
40
- if (parsed) frames.push(parsed);
38
+ if (!SAFARI_NATIVE_CODE_REGEXP.test(rawLine)) frames.push(parseSafariLine(rawLine));
41
39
  }
42
40
  }
43
41
  return frames;
@@ -48,6 +46,18 @@ export const parseStack = (stackString: string, options?: ParseOptions): StackFr
48
46
  return parseFFOrSafariString(stackString);
49
47
  };
50
48
 
49
+ const getPositionIndex = (location: string, endIndex: number): number => {
50
+ let positionIndex = endIndex - 1;
51
+ while (positionIndex >= 0) {
52
+ const character = location.charCodeAt(positionIndex);
53
+ if (character < 48 || character > 57) break;
54
+ positionIndex--;
55
+ }
56
+ return positionIndex < endIndex - 1 && location.charCodeAt(positionIndex) === 58
57
+ ? positionIndex
58
+ : -1;
59
+ };
60
+
51
61
  export const extractLocation = (
52
62
  urlLike: string,
53
63
  ): [string, string | undefined, string | undefined] => {
@@ -59,77 +69,115 @@ export const extractLocation = (
59
69
  const isWrappedLocation = urlLike.startsWith("(") && /:\d+\)$/.test(urlLike);
60
70
  const sanitizedResult = isWrappedLocation ? urlLike.slice(1, -1) : urlLike;
61
71
 
62
- const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
63
- const parts = regExp.exec(sanitizedResult);
64
- if (!parts) return [sanitizedResult, undefined, undefined];
65
- return [parts[1], parts[2] || undefined, parts[3] || undefined] as const;
66
- };
72
+ if (/[\n\r\u2028\u2029]/.test(sanitizedResult)) {
73
+ const parts = /(.+?)(?::(\d+))?(?::(\d+))?$/.exec(sanitizedResult);
74
+ return parts
75
+ ? [parts[1], parts[2] || undefined, parts[3] || undefined]
76
+ : [sanitizedResult, undefined, undefined];
77
+ }
67
78
 
68
- export const parseV8OrIeString = (stack: string): StackFrame[] => {
69
- const filteredLines = stack.split("\n").filter((line) => {
70
- return !!line.match(CHROME_IE_STACK_REGEXP);
71
- });
72
-
73
- return filteredLines.map((line): StackFrame => {
74
- let currentLine = line;
75
- if (currentLine.includes("(eval ")) {
76
- currentLine = currentLine
77
- .replace(/eval code/g, "eval")
78
- .replace(/(\(eval at [^()]*)|(,.*$)/g, "");
79
- }
80
- let sanitizedLine = currentLine
81
- .replace(/^\s+/, "")
82
- .replace(/\(eval code/g, "(")
83
- .replace(/^.*?\s+/, "");
79
+ const lastPositionIndex = getPositionIndex(sanitizedResult, sanitizedResult.length);
80
+ if (lastPositionIndex <= 0) return [sanitizedResult, undefined, undefined];
81
+ const previousPositionIndex = getPositionIndex(sanitizedResult, lastPositionIndex);
82
+ if (previousPositionIndex <= 0) {
83
+ return [
84
+ sanitizedResult.slice(0, lastPositionIndex),
85
+ sanitizedResult.slice(lastPositionIndex + 1),
86
+ undefined,
87
+ ];
88
+ }
89
+ return [
90
+ sanitizedResult.slice(0, previousPositionIndex),
91
+ sanitizedResult.slice(previousPositionIndex + 1, lastPositionIndex),
92
+ sanitizedResult.slice(lastPositionIndex + 1),
93
+ ];
94
+ };
84
95
 
85
- const locationMatch = sanitizedLine.match(/ (\(.+\)$)/);
96
+ const parseV8Line = (line: string): StackFrame => {
97
+ let currentLine = line;
98
+ if (currentLine.includes("(eval ")) {
99
+ currentLine = currentLine
100
+ .replace(/eval code/g, "eval")
101
+ .replace(/(\(eval at [^()]*)|(,.*$)/g, "");
102
+ }
103
+ let sanitizedLine = currentLine
104
+ .replace(/^\s+/, "")
105
+ .replace(/\(eval code/g, "(")
106
+ .replace(/^.*?\s+/, "");
107
+
108
+ const locationMatch = sanitizedLine.match(/ (\(.+\)$)/);
109
+
110
+ sanitizedLine = locationMatch ? sanitizedLine.replace(locationMatch[0], "") : sanitizedLine;
111
+
112
+ const locationParts = extractLocation(locationMatch ? locationMatch[1] : sanitizedLine);
113
+ const functionName = (locationMatch && sanitizedLine) || undefined;
114
+ const fileName = ["eval", "<anonymous>", "(native)"].includes(locationParts[0])
115
+ ? undefined
116
+ : locationParts[0];
117
+
118
+ return {
119
+ functionName,
120
+ fileName,
121
+ lineNumber: locationParts[1] ? +locationParts[1] : undefined,
122
+ columnNumber: locationParts[2] ? +locationParts[2] : undefined,
123
+ source: currentLine,
124
+ };
125
+ };
86
126
 
87
- sanitizedLine = locationMatch ? sanitizedLine.replace(locationMatch[0], "") : sanitizedLine;
127
+ const parseSafariLine = (line: string): StackFrame => {
128
+ let currentLine = line;
129
+ if (currentLine.includes(" > eval"))
130
+ currentLine = currentLine.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");
88
131
 
89
- const locationParts = extractLocation(locationMatch ? locationMatch[1] : sanitizedLine);
90
- const functionName = (locationMatch && sanitizedLine) || undefined;
91
- const fileName = ["eval", "<anonymous>", "(native)"].includes(locationParts[0])
92
- ? undefined
93
- : locationParts[0];
132
+ if (!currentLine.includes("@") && !currentLine.includes(":")) {
133
+ return {
134
+ functionName: currentLine,
135
+ };
136
+ } else {
137
+ const functionNameRegex =
138
+ /(([^\n\r"\u2028\u2029]*".[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*(?:@[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*)*(?:[\n\r\u2028\u2029][^@]*)?)?[^@]*)@/;
139
+ const matches = currentLine.match(functionNameRegex);
140
+ const functionName = matches && matches[1] ? matches[1] : undefined;
141
+ const locationParts = extractLocation(currentLine.replace(functionNameRegex, ""));
94
142
 
95
143
  return {
96
144
  functionName,
97
- fileName,
145
+ fileName: locationParts[0],
98
146
  lineNumber: locationParts[1] ? +locationParts[1] : undefined,
99
147
  columnNumber: locationParts[2] ? +locationParts[2] : undefined,
100
148
  source: currentLine,
101
149
  };
102
- });
150
+ }
103
151
  };
104
152
 
105
- export const parseFFOrSafariString = (stack: string): StackFrame[] => {
106
- const filteredLines = stack.split("\n").filter((line) => {
107
- return !line.match(SAFARI_NATIVE_CODE_REGEXP);
108
- });
109
-
110
- return filteredLines.map((line): StackFrame => {
111
- let currentLine = line;
112
- if (currentLine.includes(" > eval"))
113
- currentLine = currentLine.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");
114
-
115
- if (!currentLine.includes("@") && !currentLine.includes(":")) {
116
- return {
117
- functionName: currentLine,
118
- };
119
- } else {
120
- const functionNameRegex =
121
- /(([^\n\r"\u2028\u2029]*".[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*(?:@[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*)*(?:[\n\r\u2028\u2029][^@]*)?)?[^@]*)@/;
122
- const matches = currentLine.match(functionNameRegex);
123
- const functionName = matches && matches[1] ? matches[1] : undefined;
124
- const locationParts = extractLocation(currentLine.replace(functionNameRegex, ""));
125
-
126
- return {
127
- functionName,
128
- fileName: locationParts[0],
129
- lineNumber: locationParts[1] ? +locationParts[1] : undefined,
130
- columnNumber: locationParts[2] ? +locationParts[2] : undefined,
131
- source: currentLine,
132
- };
153
+ const parseLines = (
154
+ stack: string,
155
+ isV8: boolean,
156
+ cache?: Map<string, StackFrame>,
157
+ ): StackFrame[] => {
158
+ const frames: StackFrame[] = [];
159
+ for (const line of stack.split("\n")) {
160
+ let frame = cache?.get(line);
161
+ if (!frame) {
162
+ if (isV8 ? !CHROME_IE_STACK_REGEXP.test(line) : SAFARI_NATIVE_CODE_REGEXP.test(line))
163
+ continue;
164
+ frame = isV8 ? parseV8Line(line) : parseSafariLine(line);
165
+ cache?.set(line, frame);
133
166
  }
134
- });
167
+ frames.push(frame);
168
+ }
169
+ return frames;
170
+ };
171
+
172
+ export const parseV8OrIeString = (stack: string): StackFrame[] => parseLines(stack, true);
173
+
174
+ export const parseFFOrSafariString = (stack: string): StackFrame[] => parseLines(stack, false);
175
+
176
+ export const createStackParser = () => {
177
+ const v8Frames = new Map<string, StackFrame>();
178
+ const safariFrames = new Map<string, StackFrame>();
179
+ return (stack: string): StackFrame[] => {
180
+ const isV8 = CHROME_IE_STACK_REGEXP.test(stack);
181
+ return parseLines(stack, isV8, isV8 ? v8Frames : safariFrames);
182
+ };
135
183
  };