bippy 0.7.3-dev.775b69e → 0.7.3-dev.7d8af8d

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.
package/src/rdt-hook.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // This module must load before React so renderers can inject into the hook.
2
2
 
3
+ import { callListener } from "./call-listener.js";
3
4
  import type { FiberRoot, ReactDevToolsGlobalHook, ReactRenderer } from "./react-internals/index.js";
4
5
 
5
6
  export interface Unsubscribe extends Disposable {
@@ -23,6 +24,10 @@ interface RDTHookReplaceListener {
23
24
  (rdtHook: ReactDevToolsGlobalHook, target: ReactDevToolsTarget): void;
24
25
  }
25
26
 
27
+ interface RDTHookReplaceSubscription {
28
+ listener: RDTHookReplaceListener;
29
+ }
30
+
26
31
  export interface ReactDevToolsTarget {
27
32
  __REACT_DEVTOOLS_GLOBAL_HOOK__?: ReactDevToolsGlobalHook;
28
33
  }
@@ -93,7 +98,7 @@ export const _renderers = new Set<ReactRenderer>();
93
98
 
94
99
  const activeListenerTargets = new WeakMap<ActiveListener, Set<ReactDevToolsTarget>>();
95
100
  const rendererInjectSubscriptions = new Set<RendererInjectSubscription>();
96
- const rdtHookReplaceListeners = new Set<RDTHookReplaceListener>();
101
+ const rdtHookReplaceSubscriptions = new Set<RDTHookReplaceSubscription>();
97
102
  const notifiedRenderersByTarget = new WeakMap<ReactDevToolsTarget, WeakSet<ReactRenderer>>();
98
103
 
99
104
  const addActiveListener = (listener: ActiveListener, target: ReactDevToolsTarget): void => {
@@ -115,9 +120,12 @@ export const removeActiveListener = (
115
120
  }
116
121
  };
117
122
 
118
- const notifyActiveListeners = (target: ReactDevToolsTarget): void => {
119
- for (const listener of _onActiveListeners) {
120
- if (activeListenerTargets.get(listener)?.has(target)) listener();
123
+ const notifyActiveListeners = (
124
+ target: ReactDevToolsTarget,
125
+ pendingListeners = [..._onActiveListeners],
126
+ ): void => {
127
+ for (const listener of pendingListeners) {
128
+ if (activeListenerTargets.get(listener)?.has(target)) callListener(listener, undefined);
121
129
  }
122
130
  };
123
131
 
@@ -125,8 +133,11 @@ const notifyRendererInjectListeners = (
125
133
  target: ReactDevToolsTarget,
126
134
  renderer: ReactRenderer,
127
135
  ): void => {
128
- for (const subscription of rendererInjectSubscriptions) {
129
- if (subscription.target === target) subscription.listener(renderer);
136
+ const subscriptionSnapshot = [...rendererInjectSubscriptions];
137
+ for (const subscription of subscriptionSnapshot) {
138
+ if (rendererInjectSubscriptions.has(subscription) && subscription.target === target) {
139
+ callListener(subscription.listener, subscription, renderer);
140
+ }
130
141
  }
131
142
  };
132
143
 
@@ -134,8 +145,11 @@ const notifyRDTHookReplaceListeners = (
134
145
  rdtHook: ReactDevToolsGlobalHook,
135
146
  target: ReactDevToolsTarget,
136
147
  ): void => {
137
- for (const listener of rdtHookReplaceListeners) {
138
- listener(rdtHook, target);
148
+ const subscriptionSnapshot = [...rdtHookReplaceSubscriptions];
149
+ for (const subscription of subscriptionSnapshot) {
150
+ if (rdtHookReplaceSubscriptions.has(subscription)) {
151
+ callListener(subscription.listener, undefined, rdtHook, target);
152
+ }
139
153
  }
140
154
  };
141
155
 
@@ -152,9 +166,10 @@ export const onRendererInject = (
152
166
  };
153
167
 
154
168
  export const onRDTHookReplace = (listener: RDTHookReplaceListener): Unsubscribe => {
155
- rdtHookReplaceListeners.add(listener);
169
+ const subscription = { listener };
170
+ rdtHookReplaceSubscriptions.add(subscription);
156
171
  return createUnsubscribe(() => {
157
- rdtHookReplaceListeners.delete(listener);
172
+ rdtHookReplaceSubscriptions.delete(subscription);
158
173
  });
159
174
  };
160
175
 
@@ -242,10 +257,11 @@ export const installRDTHook = (
242
257
  _renderers.add(renderer);
243
258
  nextRenderers.set(rendererId, renderer);
244
259
  });
245
- if (ourRenderers.size > 0 || rdtHookReplaceListeners.size > 0) {
246
- patchRDTHook(onActive, target);
260
+ if (ourRenderers.size > 0 || rdtHookReplaceSubscriptions.size > 0) {
261
+ patchRDTHook(undefined, target, () => notifyRDTHookReplaceListeners(newHook, target));
262
+ } else {
263
+ notifyRDTHookReplaceListeners(newHook, target);
247
264
  }
248
- notifyRDTHookReplaceListeners(rdtHook, target);
249
265
  }
250
266
  },
251
267
  });
@@ -287,9 +303,10 @@ export const installRDTHook = (
287
303
  export const patchRDTHook = (
288
304
  onActive?: ActiveListener,
289
305
  target: ReactDevToolsTarget = globalThis,
306
+ onReady?: () => void,
290
307
  ): void => {
291
308
  if (onActive) addActiveListener(onActive, target);
292
- let didNotifyActiveListeners = false;
309
+ let shouldNotifyActiveListeners = false;
293
310
  const rdtHook = getTargetHook(target);
294
311
  if (!rdtHook) return;
295
312
  const renderers = getRendererMap(rdtHook);
@@ -305,28 +322,25 @@ export const patchRDTHook = (
305
322
  if (!isReactDevtools) {
306
323
  rdtHook.on = noOp;
307
324
  }
308
- if (renderers.size) {
309
- renderers.forEach((renderer) => _renderers.add(renderer));
310
- rdtHook._instrumentationIsActive = true;
311
- notifyActiveListeners(target);
312
- didNotifyActiveListeners = true;
313
- } else if (!isReactDevtools && isReactRefresh(rdtHook)) {
314
- // HACK: react-refresh's stub inject never records renderers, so a React app
315
- // that injected before bippy loaded is undetectable through the renderers map.
316
- // A react-refresh hook implies a dev renderer, so activate immediately.
317
- rdtHook._instrumentationIsActive = true;
318
- notifyActiveListeners(target);
319
- didNotifyActiveListeners = true;
320
- }
325
+ const shouldActivate = renderers.size > 0 || (!isReactDevtools && isReactRefresh(rdtHook));
321
326
  const previousInject = rdtHook.inject;
322
327
  rdtHook.inject = (renderer) => {
323
328
  const rendererId = previousInject.call(rdtHook, renderer);
324
329
  trackInjectedRenderer(rdtHook, target, renderers, rendererId, renderer);
325
330
  return rendererId;
326
331
  };
332
+ if (shouldActivate) {
333
+ renderers.forEach((renderer) => _renderers.add(renderer));
334
+ rdtHook._instrumentationIsActive = true;
335
+ shouldNotifyActiveListeners = true;
336
+ }
327
337
  }
328
- if (!didNotifyActiveListeners && (renderers.size || rdtHook._instrumentationIsActive)) {
329
- onActive?.();
338
+ const pendingListeners = shouldNotifyActiveListeners ? [..._onActiveListeners] : [];
339
+ onReady?.();
340
+ if (shouldNotifyActiveListeners) {
341
+ notifyActiveListeners(target, pendingListeners);
342
+ } else if (onActive && (renderers.size || rdtHook._instrumentationIsActive)) {
343
+ callListener(onActive, undefined);
330
344
  }
331
345
  };
332
346
 
@@ -25,6 +25,9 @@ export const ReactSymbols = {
25
25
  DEPRECATED_ASYNC_MODE_SYMBOL_STRING: "Symbol(react.async_mode)",
26
26
  ELEMENT_SYMBOL_STRING: "Symbol(react.transitional.element)",
27
27
  LEGACY_ELEMENT_SYMBOL_STRING: "Symbol(react.element)",
28
+ STRICT_MODE_NUMBER: 60108,
29
+ STRICT_MODE_SYMBOL_DESCRIPTION: "react.strict_mode",
30
+ STRICT_MODE_SYMBOL_STRING: "Symbol(react.strict_mode)",
28
31
  } as const;
29
32
 
30
33
  export interface ReactWorkTagMap {
@@ -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,30 +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
- let workTags = defaultReactWorkTags;
53
- while (rootFiber.return) {
54
- rootFiber = rootFiber.return;
55
- const ancestorWorkTags = fiberReactWorkTags.get(rootFiber);
56
- if (ancestorWorkTags) {
57
- workTags = ancestorWorkTags;
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;
58
78
  break;
59
79
  }
60
- traversedFibers.push(rootFiber);
80
+ ancestor = parent;
81
+ workTags = getCachedWorkTags(ancestor);
61
82
  }
83
+ const inheritedWorkTags = { workTags, generation: workTagGeneration };
62
84
  for (const traversedFiber of traversedFibers) {
63
- fiberReactWorkTags.set(traversedFiber, workTags);
85
+ inheritedFiberWorkTags.set(traversedFiber, inheritedWorkTags);
64
86
  if (traversedFiber.alternate) {
65
- fiberReactWorkTags.set(traversedFiber.alternate, workTags);
87
+ inheritedFiberWorkTags.set(traversedFiber.alternate, inheritedWorkTags);
66
88
  }
67
89
  }
68
90
  return workTags;
package/src/react.ts CHANGED
@@ -2,6 +2,7 @@ import "./install-hook-only.js";
2
2
  import React from "react";
3
3
  import { isFiber, traverseFiber } from "./core.js";
4
4
  import { _renderers } from "./rdt-hook.js";
5
+ import { getCurrentFiberFromRoot } from "./react-internals/current-fiber.js";
5
6
  import type { Fiber } from "./react-internals/index.js";
6
7
 
7
8
  export type { Fiber } from "./react-internals/index.js";
@@ -115,6 +116,12 @@ const getRenderingFiberFromRoot = (
115
116
  return null;
116
117
  }
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
+ }
118
125
  return traverseFiber(renderingRoot, (candidate) => {
119
126
  if (candidate !== fiber && candidate !== fiber.alternate) return false;
120
127
  let parent = candidate;
@@ -147,6 +154,9 @@ export const useFiber = (): Fiber | undefined => {
147
154
  : knownFiber.alternate && hasRenderMarker(knownFiber.alternate, renderMarker)
148
155
  ? knownFiber.alternate
149
156
  : getRenderingFiberFromRoot(knownCapture, renderMarker));
150
- if (fiber) fiberRef.current = { fiber, queue: knownCapture.queue };
157
+ if (fiber) {
158
+ knownCapture.fiber = fiber;
159
+ fiberRef.current = knownCapture;
160
+ }
151
161
  return fiber ?? undefined;
152
162
  };
@@ -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,
@@ -604,8 +604,9 @@ const findPrimitiveIndex = (hookStack: StackFrame[], hook: HookLogEntry): number
604
604
  const parseTrimmedStack = (
605
605
  rootStack: StackFrame[],
606
606
  hook: HookLogEntry,
607
+ parseHookStack: (stack: string) => StackFrame[],
607
608
  ): [StackFrame | null, StackFrame[] | null] => {
608
- const hookStack = parseErrorStack(hook.stackError);
609
+ const hookStack = parseHookStack(hook.stackError.stack || "");
609
610
  const rootIndex = findCommonAncestorIndex(rootStack, hookStack);
610
611
  const primitiveIndex = findPrimitiveIndex(hookStack, hook);
611
612
  if (rootIndex === -1 || primitiveIndex === -1 || rootIndex - primitiveIndex < 2) {
@@ -626,13 +627,14 @@ const NON_ID_HOOK_PRIMITIVES = new Set([
626
627
 
627
628
  const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): HooksTree => {
628
629
  const rootChildren: HooksNode[] = [];
630
+ const parseHookStack = createStackParser();
629
631
  let previousStack: StackFrame[] | null = null;
630
632
  let levelChildren = rootChildren;
631
633
  let nativeHookID = 0;
632
634
  const childrenStack: HooksNode[][] = [];
633
635
 
634
636
  for (const hook of capturedHookLog) {
635
- const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
637
+ const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook, parseHookStack);
636
638
  let displayName = hook.displayName;
637
639
  if (displayName === null && primitiveFrame !== null) {
638
640
  const primitiveName = parseHookName(primitiveFrame.functionName);
@@ -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
  };
@@ -207,6 +207,13 @@ export const getSourceFromSourceMap = (
207
207
  );
208
208
  };
209
209
 
210
+ const getStringIndex = (values: string[], target: string): number => {
211
+ for (let valueIndex = 0; valueIndex < values.length; valueIndex++) {
212
+ if (values[valueIndex] === target) return valueIndex;
213
+ }
214
+ return -1;
215
+ };
216
+
210
217
  const getSourceFromMappingsByFunctionName = (
211
218
  mappings: SourceMapMappings,
212
219
  sources: string[],
@@ -215,13 +222,17 @@ const getSourceFromMappingsByFunctionName = (
215
222
  ignoredSourceIndices?: Set<number>,
216
223
  ): StackFrame | null => {
217
224
  if (!names) return null;
218
- const functionNameIndex = names.indexOf(functionName);
225
+ const functionNameIndex = getStringIndex(names, functionName);
219
226
  if (functionNameIndex === -1) return null;
220
227
 
221
228
  let ignoredSource: StackFrame | null = null;
222
- for (const lineMapping of mappings) {
223
- for (const segment of lineMapping) {
229
+ for (let lineIndex = 0; lineIndex < mappings.length; lineIndex++) {
230
+ const lineMapping = mappings[lineIndex];
231
+ for (let segmentIndex = 0; segmentIndex < lineMapping.length; segmentIndex++) {
232
+ const segment = lineMapping[segmentIndex];
224
233
  if (segment[4] !== functionNameIndex) continue;
234
+ if (ignoredSource && segment[1] !== undefined && ignoredSourceIndices?.has(segment[1]))
235
+ continue;
225
236
  const source = getSourceFromSegment(segment, sources, ignoredSourceIndices, names);
226
237
  if (!source) continue;
227
238
  if (!source.isIgnoreListed) return source;
@@ -267,7 +278,7 @@ const findSourceContentByFileName = (
267
278
  fileName: string,
268
279
  ): string | null => {
269
280
  if (!sourcesContent) return null;
270
- const sourceIndex = sources.indexOf(fileName);
281
+ const sourceIndex = getStringIndex(sources, fileName);
271
282
  return sourceIndex === -1 ? null : (sourcesContent[sourceIndex] ?? null);
272
283
  };
273
284