bippy 0.7.3-dev.a6ed02c → 0.7.3-dev.c0e872e

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.
@@ -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
  };
@@ -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