bippy 0.5.37 → 0.5.39

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.
@@ -0,0 +1,224 @@
1
+ import type { HooksNode, HooksTree, HookSource } from "./inspect-hooks.js";
2
+ import { getSourceMap, getSourceFromSourceMap, type SourceMap } from "./symbolication.js";
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
5
+ export interface HookNames extends Map<string, string> {}
6
+
7
+ const UNNAMED_HOOKS = new Set([
8
+ "Effect",
9
+ "LayoutEffect",
10
+ "InsertionEffect",
11
+ "ImperativeHandle",
12
+ "DebugValue",
13
+ ]);
14
+
15
+ // HACK: matches `const/let/var [name, ...] = use...(...` or `const/let/var name = use...(...`
16
+ // across up to 10 lines; handles TypeScript generics like `useState<T>(`
17
+ const HOOK_DECLARATION_REGEX =
18
+ /(?:const|let|var)\s+((?:\[[\s\S]*?\]|\w+))\s*=\s*(?:[\w$.]+\.)*use[A-Z]\w*\s*(?:<[\s\S]*?>)?\s*\(/g;
19
+
20
+ export const getHookSourceLocationKey = (hookSource: HookSource): string =>
21
+ `${hookSource.fileName ?? ""}:${hookSource.lineNumber ?? 0}:${hookSource.columnNumber ?? 0}`;
22
+
23
+ const flattenHooksTree = (hooksTree: HooksTree): HooksNode[] => {
24
+ const hooksList: HooksNode[] = [];
25
+ const collectNamedHooks = (tree: HooksTree): void => {
26
+ for (const hook of tree) {
27
+ if (UNNAMED_HOOKS.has(hook.name)) continue;
28
+ hooksList.push(hook);
29
+ if (hook.subHooks.length > 0) collectNamedHooks(hook.subHooks);
30
+ }
31
+ };
32
+ collectNamedHooks(hooksTree);
33
+ return hooksList;
34
+ };
35
+
36
+ const findSourceContentByFileName = (
37
+ sources: string[],
38
+ sourcesContent: string[] | undefined,
39
+ fileName: string,
40
+ ): string | null => {
41
+ if (!sourcesContent) return null;
42
+ const sourceIndex = sources.indexOf(fileName);
43
+ return sourceIndex !== -1 ? (sourcesContent[sourceIndex] ?? null) : null;
44
+ };
45
+
46
+ const getSourceContentFromSourceMap = (
47
+ sourceMap: SourceMap,
48
+ originalFileName: string,
49
+ ): string | null => {
50
+ const directResult = findSourceContentByFileName(
51
+ sourceMap.sources,
52
+ sourceMap.sourcesContent,
53
+ originalFileName,
54
+ );
55
+ if (directResult) return directResult;
56
+
57
+ if (sourceMap.sections) {
58
+ for (const section of sourceMap.sections) {
59
+ const sectionResult = findSourceContentByFileName(
60
+ section.map.sources,
61
+ section.map.sourcesContent,
62
+ originalFileName,
63
+ );
64
+ if (sectionResult) return sectionResult;
65
+ }
66
+ }
67
+ return null;
68
+ };
69
+
70
+ const extractVariableNameFromBinding = (binding: string): string | null => {
71
+ const trimmed = binding.trim();
72
+ if (trimmed.startsWith("[")) {
73
+ const match = trimmed.match(/\[\s*(\w+)/);
74
+ return match ? match[1] : null;
75
+ }
76
+ return /^\w+$/.test(trimmed) ? trimmed : null;
77
+ };
78
+
79
+ export const extractHookVariableName = (
80
+ sourceCode: string,
81
+ lineNumber: number,
82
+ columnNumber: number,
83
+ ): string | null => {
84
+ const lines = sourceCode.split("\n");
85
+ const hookLineIndex = lineNumber - 1;
86
+
87
+ if (hookLineIndex < 0 || hookLineIndex >= lines.length) return null;
88
+
89
+ const searchStartLine = Math.max(0, hookLineIndex - 10);
90
+ const chunkLines = lines.slice(searchStartLine, hookLineIndex + 1);
91
+ const sourceChunk = chunkLines.join("\n");
92
+
93
+ const allMatches = [...sourceChunk.matchAll(HOOK_DECLARATION_REGEX)];
94
+
95
+ const hookPositionInChunk = sourceChunk.lastIndexOf("\n") + 1 + columnNumber;
96
+ const closestMatch = allMatches.filter((match) => match.index! <= hookPositionInChunk).at(-1);
97
+
98
+ if (closestMatch) {
99
+ return extractVariableNameFromBinding(closestMatch[1]);
100
+ }
101
+
102
+ return null;
103
+ };
104
+
105
+ interface ResolvedSource {
106
+ sourceCode: string;
107
+ lineNumber: number;
108
+ columnNumber: number;
109
+ }
110
+
111
+ interface SourceResolutionContext {
112
+ sourceMapsByFile: Map<string, SourceMap | null>;
113
+ sourceContentCache: Map<string, string | null>;
114
+ fetchFn?: (url: string) => Promise<Response>;
115
+ }
116
+
117
+ const resolveOriginalSource = async (
118
+ runtimeFileName: string,
119
+ runtimeLine: number,
120
+ runtimeColumn: number,
121
+ context: SourceResolutionContext,
122
+ ): Promise<ResolvedSource | null> => {
123
+ const { sourceMapsByFile, sourceContentCache, fetchFn } = context;
124
+
125
+ if (!sourceMapsByFile.has(runtimeFileName)) {
126
+ sourceMapsByFile.set(runtimeFileName, await getSourceMap(runtimeFileName, true, fetchFn));
127
+ }
128
+
129
+ const sourceMap = sourceMapsByFile.get(runtimeFileName) ?? null;
130
+
131
+ if (sourceMap) {
132
+ const originalLocation = getSourceFromSourceMap(sourceMap, runtimeLine, runtimeColumn);
133
+ if (originalLocation?.fileName && originalLocation.lineNumber !== undefined) {
134
+ const cacheKey = `sourcemap:${runtimeFileName}:${originalLocation.fileName}`;
135
+ if (!sourceContentCache.has(cacheKey)) {
136
+ sourceContentCache.set(
137
+ cacheKey,
138
+ getSourceContentFromSourceMap(sourceMap, originalLocation.fileName),
139
+ );
140
+ }
141
+ const originalSourceCode = sourceContentCache.get(cacheKey) ?? null;
142
+ if (originalSourceCode) {
143
+ return {
144
+ sourceCode: originalSourceCode,
145
+ lineNumber: originalLocation.lineNumber,
146
+ columnNumber: originalLocation.columnNumber ?? 0,
147
+ };
148
+ }
149
+ }
150
+ }
151
+
152
+ if (!sourceContentCache.has(runtimeFileName)) {
153
+ try {
154
+ const fetchImpl = fetchFn ?? fetch;
155
+ const response = await fetchImpl(runtimeFileName);
156
+ sourceContentCache.set(runtimeFileName, response.ok ? await response.text() : null);
157
+ } catch {
158
+ sourceContentCache.set(runtimeFileName, null);
159
+ }
160
+ }
161
+
162
+ const runtimeSourceCode = sourceContentCache.get(runtimeFileName) ?? null;
163
+ if (runtimeSourceCode) {
164
+ return {
165
+ sourceCode: runtimeSourceCode,
166
+ lineNumber: runtimeLine,
167
+ columnNumber: runtimeColumn,
168
+ };
169
+ }
170
+
171
+ return null;
172
+ };
173
+
174
+ export const parseHookNames = async (
175
+ hooksTree: HooksTree,
176
+ fetchFn?: (url: string) => Promise<Response>,
177
+ ): Promise<HookNames> => {
178
+ const hookNames: HookNames = new Map();
179
+ const hooksList = flattenHooksTree(hooksTree);
180
+
181
+ if (hooksList.length === 0) return hookNames;
182
+
183
+ const resolutionContext: SourceResolutionContext = {
184
+ sourceMapsByFile: new Map(),
185
+ sourceContentCache: new Map(),
186
+ fetchFn,
187
+ };
188
+
189
+ await Promise.all(
190
+ hooksList.map(async (hook) => {
191
+ const hookSource = hook.hookSource;
192
+ if (
193
+ !hookSource ||
194
+ !hookSource.fileName ||
195
+ hookSource.lineNumber === null ||
196
+ hookSource.columnNumber === null
197
+ ) {
198
+ return;
199
+ }
200
+
201
+ const resolved = await resolveOriginalSource(
202
+ hookSource.fileName,
203
+ hookSource.lineNumber,
204
+ hookSource.columnNumber,
205
+ resolutionContext,
206
+ );
207
+
208
+ if (!resolved) return;
209
+
210
+ const variableName = extractHookVariableName(
211
+ resolved.sourceCode,
212
+ resolved.lineNumber,
213
+ resolved.columnNumber,
214
+ );
215
+
216
+ if (variableName) {
217
+ const locationKey = getHookSourceLocationKey(hookSource);
218
+ hookNames.set(locationKey, variableName);
219
+ }
220
+ }),
221
+ );
222
+
223
+ return hookNames;
224
+ };
package/src/types.ts CHANGED
@@ -10,9 +10,67 @@ export type Flags = number;
10
10
  export type Lanes = number;
11
11
  export type TypeOfMode = number;
12
12
  export type RootTag = 0 | 1 | 2;
13
- export type LanePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17;
14
- export type WorkTag = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24;
15
- export type HookType = "useState" | "useReducer" | "useContext" | "useRef" | "useEffect" | "useLayoutEffect" | "useCallback" | "useMemo" | "useImperativeHandle" | "useDebugValue" | "useDeferredValue" | "useTransition" | "useMutableSource" | "useOpaqueIdentifier" | "useCacheRefresh";
13
+ export type LanePriority =
14
+ | 0
15
+ | 1
16
+ | 2
17
+ | 3
18
+ | 4
19
+ | 5
20
+ | 6
21
+ | 7
22
+ | 8
23
+ | 9
24
+ | 10
25
+ | 11
26
+ | 12
27
+ | 13
28
+ | 14
29
+ | 15
30
+ | 16
31
+ | 17;
32
+ export type WorkTag =
33
+ | 0
34
+ | 1
35
+ | 2
36
+ | 3
37
+ | 4
38
+ | 5
39
+ | 6
40
+ | 7
41
+ | 8
42
+ | 9
43
+ | 10
44
+ | 11
45
+ | 12
46
+ | 13
47
+ | 14
48
+ | 15
49
+ | 16
50
+ | 17
51
+ | 18
52
+ | 19
53
+ | 20
54
+ | 21
55
+ | 22
56
+ | 23
57
+ | 24;
58
+ export type HookType =
59
+ | "useState"
60
+ | "useReducer"
61
+ | "useContext"
62
+ | "useRef"
63
+ | "useEffect"
64
+ | "useLayoutEffect"
65
+ | "useCallback"
66
+ | "useMemo"
67
+ | "useImperativeHandle"
68
+ | "useDebugValue"
69
+ | "useDeferredValue"
70
+ | "useTransition"
71
+ | "useMutableSource"
72
+ | "useOpaqueIdentifier"
73
+ | "useCacheRefresh";
16
74
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
75
  export type FiberRoot = any;
18
76
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -27,10 +85,17 @@ export type React$AbstractComponent<_Config, _Instance = unknown> = any;
27
85
  export type HostConfig = Record<string, any>;
28
86
 
29
87
  // Structural interfaces
30
- export interface Source { fileName: string; lineNumber: number }
88
+ export interface Source {
89
+ fileName: string;
90
+ lineNumber: number;
91
+ }
31
92
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- export interface RefObject { current: any }
33
- export interface Thenable<T> { then(resolve: () => T, reject?: () => T): T }
93
+ export interface RefObject {
94
+ current: any;
95
+ }
96
+ export interface Thenable<T> {
97
+ then(resolve: () => T, reject?: () => T): T;
98
+ }
34
99
 
35
100
  export interface ReactContext<T> {
36
101
  $$typeof: symbol | number;
@@ -47,21 +112,66 @@ export interface ReactContext<T> {
47
112
  displayName?: string;
48
113
  }
49
114
 
50
- export interface ReactProviderType<T> { $$typeof: symbol | number; _context: ReactContext<T> }
51
- export interface ReactProvider<T> { $$typeof: symbol | number; type: ReactProviderType<T>; key: null | string; ref: null; props: { value: T; children?: ReactNode } }
52
- export interface ReactConsumer<T> { $$typeof: symbol | number; type: ReactContext<T>; key: null | string; ref: null; props: { children: (value: T) => ReactNode; unstable_observedBits?: number } }
115
+ export interface ReactProviderType<T> {
116
+ $$typeof: symbol | number;
117
+ _context: ReactContext<T>;
118
+ }
119
+ export interface ReactProvider<T> {
120
+ $$typeof: symbol | number;
121
+ type: ReactProviderType<T>;
122
+ key: null | string;
123
+ ref: null;
124
+ props: { value: T; children?: ReactNode };
125
+ }
126
+ export interface ReactConsumer<T> {
127
+ $$typeof: symbol | number;
128
+ type: ReactContext<T>;
129
+ key: null | string;
130
+ ref: null;
131
+ props: { children: (value: T) => ReactNode; unstable_observedBits?: number };
132
+ }
53
133
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
- export interface ReactPortal { $$typeof: symbol | number; key: null | string; containerInfo: any; children: ReactNode; implementation: any }
134
+ export interface ReactPortal {
135
+ $$typeof: symbol | number;
136
+ key: null | string;
137
+ containerInfo: any;
138
+ children: ReactNode;
139
+ implementation: any;
140
+ }
55
141
 
56
- export interface ComponentSelector { $$typeof: symbol | number; value: React$AbstractComponent<never, unknown> }
57
- export interface HasPseudoClassSelector { $$typeof: symbol | number; value: Selector[] }
58
- export interface RoleSelector { $$typeof: symbol | number; value: string }
59
- export interface TextSelector { $$typeof: symbol | number; value: string }
60
- export interface TestNameSelector { $$typeof: symbol | number; value: string }
61
- export type Selector = ComponentSelector | HasPseudoClassSelector | RoleSelector | TextSelector | TestNameSelector;
142
+ export interface ComponentSelector {
143
+ $$typeof: symbol | number;
144
+ value: React$AbstractComponent<never, unknown>;
145
+ }
146
+ export interface HasPseudoClassSelector {
147
+ $$typeof: symbol | number;
148
+ value: Selector[];
149
+ }
150
+ export interface RoleSelector {
151
+ $$typeof: symbol | number;
152
+ value: string;
153
+ }
154
+ export interface TextSelector {
155
+ $$typeof: symbol | number;
156
+ value: string;
157
+ }
158
+ export interface TestNameSelector {
159
+ $$typeof: symbol | number;
160
+ value: string;
161
+ }
162
+ export type Selector =
163
+ | ComponentSelector
164
+ | HasPseudoClassSelector
165
+ | RoleSelector
166
+ | TextSelector
167
+ | TestNameSelector;
62
168
 
63
169
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
- export interface DevToolsConfig<Instance = any, TextInstance = any, RendererInspectionConfig = any> {
170
+ export interface DevToolsConfig<
171
+ Instance = any,
172
+ TextInstance = any,
173
+ RendererInspectionConfig = any,
174
+ > {
65
175
  bundleType: BundleType;
66
176
  version: string;
67
177
  rendererPackageName: string;
@@ -76,12 +186,37 @@ export interface SuspenseHydrationCallbacks<SuspenseInstance = unknown> {
76
186
 
77
187
  export interface TransitionTracingCallbacks {
78
188
  onTransitionStart?: (transitionName: string, startTime: number) => void;
79
- onTransitionProgress?: (transitionName: string, startTime: number, currentTime: number, pending: Array<{ name: null | string }>) => void;
80
- onTransitionIncomplete?: (transitionName: string, startTime: number, deletions: Array<{ type: string; name?: string; newName?: string; endTime: number }>) => void;
189
+ onTransitionProgress?: (
190
+ transitionName: string,
191
+ startTime: number,
192
+ currentTime: number,
193
+ pending: Array<{ name: null | string }>,
194
+ ) => void;
195
+ onTransitionIncomplete?: (
196
+ transitionName: string,
197
+ startTime: number,
198
+ deletions: Array<{ type: string; name?: string; newName?: string; endTime: number }>,
199
+ ) => void;
81
200
  onTransitionComplete?: (transitionName: string, startTime: number, endTime: number) => void;
82
- onMarkerProgress?: (transitionName: string, marker: string, startTime: number, currentTime: number, pending: Array<{ name: null | string }>) => void;
83
- onMarkerIncomplete?: (transitionName: string, marker: string, startTime: number, deletions: Array<{ type: string; name?: string; newName?: string; endTime: number }>) => void;
84
- onMarkerComplete?: (transitionName: string, marker: string, startTime: number, endTime: number) => void;
201
+ onMarkerProgress?: (
202
+ transitionName: string,
203
+ marker: string,
204
+ startTime: number,
205
+ currentTime: number,
206
+ pending: Array<{ name: null | string }>,
207
+ ) => void;
208
+ onMarkerIncomplete?: (
209
+ transitionName: string,
210
+ marker: string,
211
+ startTime: number,
212
+ deletions: Array<{ type: string; name?: string; newName?: string; endTime: number }>,
213
+ ) => void;
214
+ onMarkerComplete?: (
215
+ transitionName: string,
216
+ marker: string,
217
+ startTime: number,
218
+ endTime: number,
219
+ ) => void;
85
220
  }
86
221
 
87
222
  // The base Fiber interface from react-reconciler, used to derive bippy's Fiber below