bippy 0.5.43 → 0.6.0

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.
Files changed (50) hide show
  1. package/dist/core.cjs +1 -1
  2. package/dist/core.d.cts +28 -313
  3. package/dist/core.d.ts +28 -313
  4. package/dist/core.js +1 -1
  5. package/dist/core2.d.cts +3 -2
  6. package/dist/core2.d.ts +3 -2
  7. package/dist/get-source.cjs +19 -0
  8. package/dist/get-source.js +19 -0
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.d.cts +3 -2
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.iife.js +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/install-hook-only.cjs +1 -1
  15. package/dist/install-hook-only.iife.js +1 -1
  16. package/dist/install-hook-only.js +1 -1
  17. package/dist/rdt-hook.cjs +1 -1
  18. package/dist/rdt-hook.js +1 -1
  19. package/dist/react-refresh.cjs +9 -0
  20. package/dist/react-refresh.d.cts +66 -0
  21. package/dist/react-refresh.d.ts +66 -0
  22. package/dist/react-refresh.js +9 -0
  23. package/dist/source.cjs +4 -19
  24. package/dist/source.d.cts +35 -27
  25. package/dist/source.d.ts +35 -27
  26. package/dist/source.js +4 -19
  27. package/dist/unsubscribe.d.cts +298 -0
  28. package/dist/unsubscribe.d.ts +298 -0
  29. package/package.json +14 -3
  30. package/src/core.ts +312 -334
  31. package/src/rdt-hook.ts +57 -16
  32. package/src/react-refresh/constants.ts +9 -0
  33. package/src/react-refresh/detect-hmr-transport.ts +33 -0
  34. package/src/react-refresh/index.ts +173 -0
  35. package/src/react-refresh/metro-hmr-transport.ts +188 -0
  36. package/src/react-refresh/next-webpack-hmr-transport.ts +72 -0
  37. package/src/react-refresh/normalize-hmr-file-path.ts +24 -0
  38. package/src/react-refresh/types.ts +7 -0
  39. package/src/react-refresh/vite-hmr-transport.ts +116 -0
  40. package/src/source/constants.ts +11 -0
  41. package/src/source/get-display-name-from-source.ts +3 -3
  42. package/src/source/get-source.ts +53 -9
  43. package/src/source/index.ts +17 -8
  44. package/src/source/inspect-hooks.ts +7 -4
  45. package/src/source/owner-stack.ts +193 -29
  46. package/src/source/parse-debug-stack.ts +133 -0
  47. package/src/source/parse-stack.ts +6 -105
  48. package/src/source/symbolication.ts +48 -13
  49. package/src/types.ts +20 -7
  50. package/src/unsubscribe.ts +17 -0
@@ -0,0 +1,133 @@
1
+ import { JSX_FACTORY_FRAME_COUNT, REACT_STACK_BOTTOM_FRAME_PATTERNS } from "./constants.js";
2
+ import { parseStack, StackFrame } from "./parse-stack.js";
3
+
4
+ interface V8CallSite {
5
+ getFunctionName?: () => string | null;
6
+ getScriptNameOrSourceURL?: () => string | null;
7
+ getLineNumber?: () => number | null;
8
+ getColumnNumber?: () => number | null;
9
+ getEnclosingLineNumber?: () => number | null;
10
+ getEnclosingColumnNumber?: () => number | null;
11
+ getTypeName?: () => string | null;
12
+ getMethodName?: () => string | null;
13
+ getEvalOrigin?: () => string | null;
14
+ isNative?: () => boolean;
15
+ isEval?: () => boolean;
16
+ toString: () => string;
17
+ }
18
+
19
+ export interface ParsedDebugStack {
20
+ frames: StackFrame[];
21
+ // React appends a react-stack-bottom-frame sentinel to stacks captured
22
+ // during render; without it the JSX was created outside a render and the
23
+ // lower frames are arbitrary bootstrapping code
24
+ isTrusted: boolean;
25
+ }
26
+
27
+ const parsedDebugStackCache = new WeakMap<Error, ParsedDebugStack>();
28
+
29
+ const isReactBottomFrameName = (functionName: string): boolean =>
30
+ REACT_STACK_BOTTOM_FRAME_PATTERNS.some((pattern) => functionName.includes(pattern));
31
+
32
+ const getCallSiteFunctionName = (callSite: V8CallSite): string => {
33
+ const functionName = callSite.getFunctionName?.() ?? "";
34
+ if (functionName) {
35
+ return functionName;
36
+ }
37
+ const typeName = callSite.getTypeName?.() ?? "";
38
+ const methodName = callSite.getMethodName?.() ?? "";
39
+ if (typeName && methodName) {
40
+ return `${typeName}.${methodName}`;
41
+ }
42
+ return methodName;
43
+ };
44
+
45
+ const collectStructuredFrames = (callSites: V8CallSite[]): ParsedDebugStack => {
46
+ const frames: StackFrame[] = [];
47
+ for (
48
+ let callSiteIndex = JSX_FACTORY_FRAME_COUNT;
49
+ callSiteIndex < callSites.length;
50
+ callSiteIndex++
51
+ ) {
52
+ const callSite = callSites[callSiteIndex];
53
+ const functionName = getCallSiteFunctionName(callSite);
54
+ if (isReactBottomFrameName(functionName)) {
55
+ return { frames, isTrusted: true };
56
+ }
57
+ if (callSite.isNative?.()) {
58
+ frames.push({ functionName: functionName || undefined });
59
+ continue;
60
+ }
61
+ let fileName = callSite.getScriptNameOrSourceURL?.() ?? "";
62
+ if (!fileName && callSite.isEval?.()) {
63
+ fileName = callSite.getEvalOrigin?.() ?? "";
64
+ }
65
+ frames.push({
66
+ functionName: functionName && functionName !== "<anonymous>" ? functionName : undefined,
67
+ fileName: fileName && fileName !== "<anonymous>" ? fileName : undefined,
68
+ lineNumber: callSite.getLineNumber?.() ?? undefined,
69
+ columnNumber: callSite.getColumnNumber?.() ?? undefined,
70
+ enclosingLineNumber: callSite.getEnclosingLineNumber?.() ?? undefined,
71
+ enclosingColumnNumber: callSite.getEnclosingColumnNumber?.() ?? undefined,
72
+ source: ` at ${callSite.toString()}`,
73
+ });
74
+ }
75
+ return { frames, isTrusted: false };
76
+ };
77
+
78
+ const parseMaterializedStack = (stackString: string): ParsedDebugStack => {
79
+ let bottomFrameIndex = -1;
80
+ for (const pattern of REACT_STACK_BOTTOM_FRAME_PATTERNS) {
81
+ bottomFrameIndex = stackString.indexOf(pattern);
82
+ if (bottomFrameIndex !== -1) break;
83
+ }
84
+ const trimmedStack =
85
+ bottomFrameIndex === -1
86
+ ? stackString
87
+ : stackString.slice(0, stackString.lastIndexOf("\n", bottomFrameIndex));
88
+ return {
89
+ frames: parseStack(trimmedStack).slice(JSX_FACTORY_FRAME_COUNT),
90
+ isTrusted: bottomFrameIndex !== -1,
91
+ };
92
+ };
93
+
94
+ /**
95
+ * Parses a React `_debugStack` Error, preferring V8's structured CallSite API
96
+ * (via Error.prepareStackTrace) over string parsing. Structured frames carry
97
+ * enclosing line/column - the function definition start, not the call site -
98
+ * and are immune to source-mapped `.stack` strings. Falls back to string
99
+ * parsing on engines without CallSites (JSC, SpiderMonkey) or when the stack
100
+ * was already materialized. The leading JSX factory frame (jsxDEV) and
101
+ * everything at or below React's bottom-frame sentinel are dropped.
102
+ */
103
+ export const parseDebugStack = (debugStack: Error): ParsedDebugStack => {
104
+ const cachedResult = parsedDebugStackCache.get(debugStack);
105
+ if (cachedResult) {
106
+ return cachedResult;
107
+ }
108
+
109
+ let structuredResult: ParsedDebugStack | null = null;
110
+ const collectFramesAndFormatStack = (error: Error, callSites: V8CallSite[]): string => {
111
+ structuredResult = collectStructuredFrames(callSites);
112
+ // this return value becomes error.stack permanently, so emit the default
113
+ // V8 format for any later reader of the same error
114
+ let stackString = `${error.name || "Error"}: ${error.message || ""}`;
115
+ for (const callSite of callSites) {
116
+ stackString += `\n at ${callSite.toString()}`;
117
+ }
118
+ return stackString;
119
+ };
120
+ const previousPrepareStackTrace = Error.prepareStackTrace;
121
+ // node's CallSite typings disagree with browser-safe optional methods
122
+ Error.prepareStackTrace = collectFramesAndFormatStack as typeof Error.prepareStackTrace;
123
+ let stackString: string;
124
+ try {
125
+ stackString = String(debugStack.stack);
126
+ } finally {
127
+ Error.prepareStackTrace = previousPrepareStackTrace;
128
+ }
129
+
130
+ const result = structuredResult ?? parseMaterializedStack(stackString);
131
+ parsedDebugStackCache.set(debugStack, result);
132
+ return result;
133
+ };
@@ -2,11 +2,17 @@ export interface StackFrame {
2
2
  args?: unknown[];
3
3
  columnNumber?: number;
4
4
  lineNumber?: number;
5
+ // start of the enclosing function (the definition, not the call site);
6
+ // only available from V8's structured CallSite API
7
+ enclosingLineNumber?: number;
8
+ enclosingColumnNumber?: number;
5
9
  fileName?: string;
6
10
  functionName?: string;
7
11
  source?: string;
8
12
  isServer?: boolean;
9
13
  isSymbolicated?: boolean;
14
+ // the source map ignore-listed this frame's original source (x_google_ignoreList)
15
+ isIgnoreListed?: boolean;
10
16
  }
11
17
 
12
18
  export interface ParseOptions {
@@ -19,14 +25,6 @@ const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/;
19
25
  const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
20
26
  const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code\])?$/;
21
27
 
22
- const getNonStandardStacktrace = (error: unknown): string | null => {
23
- if (error && typeof error === "object") {
24
- const stacktrace = (error as Record<string, unknown>)["stacktrace"];
25
- return typeof stacktrace === "string" ? stacktrace : null;
26
- }
27
- return null;
28
- };
29
-
30
28
  export const parseStack = (stackString: string, options?: ParseOptions): StackFrame[] => {
31
29
  if (options?.includeInElement !== false) {
32
30
  const lines = stackString.split("\n");
@@ -76,10 +74,6 @@ const applySlice = <T>(lines: T[], options?: ParseOptions): T[] => {
76
74
  return lines;
77
75
  };
78
76
 
79
- export const parseV8OrIE = (error: Error, options?: ParseOptions): StackFrame[] => {
80
- return parseV8OrIeString(error.stack!, options);
81
- };
82
-
83
77
  export const parseV8OrIeString = (stack: string, options?: ParseOptions): StackFrame[] => {
84
78
  const filteredLines = applySlice(
85
79
  stack.split("\n").filter((line) => {
@@ -120,10 +114,6 @@ export const parseV8OrIeString = (stack: string, options?: ParseOptions): StackF
120
114
  });
121
115
  };
122
116
 
123
- export const parseFFOrSafari = (error: Error, options?: ParseOptions): StackFrame[] => {
124
- return parseFFOrSafariString(error.stack!, options);
125
- };
126
-
127
117
  export const parseFFOrSafariString = (stack: string, options?: ParseOptions): StackFrame[] => {
128
118
  const filteredLines = applySlice(
129
119
  stack.split("\n").filter((line) => {
@@ -158,92 +148,3 @@ export const parseFFOrSafariString = (stack: string, options?: ParseOptions): St
158
148
  }
159
149
  });
160
150
  };
161
-
162
- export const parseOpera = (error: Error, options?: ParseOptions): StackFrame[] => {
163
- const nonStandardStacktrace = getNonStandardStacktrace(error);
164
- if (
165
- !nonStandardStacktrace ||
166
- (error.message.includes("\n") &&
167
- error.message.split("\n").length > nonStandardStacktrace.split("\n").length)
168
- ) {
169
- return parseOpera9(error, options);
170
- }
171
- if (!error.stack) return parseOpera10(error, options);
172
- return parseOpera11(error, options);
173
- };
174
-
175
- export const parseOpera9 = (error: Error, options?: ParseOptions): StackFrame[] => {
176
- const lineRegex = /Line (\d+).*script (?:in )?(\S+)/i;
177
- const messageLines = error.message.split("\n");
178
- const parsedFrames: StackFrame[] = [];
179
-
180
- for (let i = 2, len = messageLines.length; i < len; i += 2) {
181
- const match = lineRegex.exec(messageLines[i]);
182
- if (match) {
183
- parsedFrames.push({
184
- fileName: match[2],
185
- lineNumber: +match[1],
186
- source: messageLines[i],
187
- });
188
- }
189
- }
190
-
191
- return applySlice(parsedFrames, options);
192
- };
193
-
194
- export const parseOpera10 = (error: Error, options?: ParseOptions): StackFrame[] => {
195
- const lineRegex = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;
196
- const nonStandardStacktrace = getNonStandardStacktrace(error);
197
- const stacktraceLines = (nonStandardStacktrace || "").split("\n");
198
- const parsedFrames: StackFrame[] = [];
199
-
200
- for (let i = 0, len = stacktraceLines.length; i < len; i += 2) {
201
- const match = lineRegex.exec(stacktraceLines[i]);
202
- if (match) {
203
- parsedFrames.push({
204
- functionName: match[3] || undefined,
205
- fileName: match[2],
206
- lineNumber: match[1] ? +match[1] : undefined,
207
- source: stacktraceLines[i],
208
- });
209
- }
210
- }
211
-
212
- return applySlice(parsedFrames, options);
213
- };
214
-
215
- export const parseOpera11 = (error: Error, options?: ParseOptions): StackFrame[] => {
216
- const filteredLines = applySlice(
217
- // @ts-expect-error missing stack property
218
- error.stack.split("\n").filter((line) => {
219
- return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/);
220
- }),
221
- options,
222
- );
223
-
224
- return filteredLines.map<StackFrame>((line) => {
225
- const tokens = line.split("@");
226
- const locationParts = extractLocation(tokens.pop()!);
227
- const functionCall = tokens.shift() || "";
228
- const functionName =
229
- functionCall.replace(/<anonymous function(: (\w+))?>/, "$2").replace(/\([^)]*\)/g, "") ||
230
- undefined;
231
- let argsRaw: string | undefined;
232
- if (functionCall.match(/\(([^)]*)\)/))
233
- argsRaw = functionCall.replace(/^[^(]+\(([^)]*)\)$/, "$1");
234
-
235
- const args =
236
- argsRaw === undefined || argsRaw === "[arguments not available]"
237
- ? undefined
238
- : argsRaw.split(",");
239
-
240
- return {
241
- functionName,
242
- args,
243
- fileName: locationParts[0],
244
- lineNumber: locationParts[1] ? +locationParts[1] : undefined,
245
- columnNumber: locationParts[2] ? +locationParts[2] : undefined,
246
- source: line,
247
- };
248
- });
249
- };
@@ -5,6 +5,7 @@ import { StackFrame } from "./parse-stack.js";
5
5
  export interface DecodedSourceMapSection {
6
6
  map: {
7
7
  file?: string;
8
+ ignoredSourceIndices?: Set<number>;
8
9
  mappings: SourceMapSegment[][];
9
10
  names?: string[];
10
11
  sourceRoot?: string;
@@ -35,6 +36,7 @@ export type RawSourceMap = IndexSourceMap | StandardSourceMap;
35
36
 
36
37
  export interface SourceMap {
37
38
  file?: string;
39
+ ignoredSourceIndices?: Set<number>;
38
40
  mappings: SourceMapSegment[][];
39
41
  names?: string[];
40
42
  sections?: DecodedSourceMapSection[];
@@ -47,12 +49,14 @@ export interface SourceMap {
47
49
  // https://developer.chrome.com/blog/sourcemaps#the_anatomy_of_a_source_map
48
50
  export interface StandardSourceMap {
49
51
  file?: string;
52
+ ignoreList?: number[];
50
53
  mappings: string;
51
54
  names?: string[];
52
55
  sourceRoot?: string;
53
56
  sources: string[];
54
57
  sourcesContent?: string[];
55
58
  version: 3;
59
+ x_google_ignoreList?: number[];
56
60
  }
57
61
 
58
62
  // has a scheme, e.g. http://, https://, file://, data:, etc.
@@ -77,6 +81,7 @@ const getSourceFromMappings = (
77
81
  sources: string[],
78
82
  lineIndexInMappings: number,
79
83
  column: number,
84
+ ignoredSourceIndices?: Set<number>,
80
85
  ): StackFrame | null => {
81
86
  if (lineIndexInMappings < 0 || lineIndexInMappings >= mappings.length) {
82
87
  return null;
@@ -87,12 +92,18 @@ const getSourceFromMappings = (
87
92
  return null;
88
93
  }
89
94
 
95
+ // Segments within a line are sorted by generated column, so binary search for
96
+ // the last segment at or before the column.
90
97
  let closestLineSegment: null | SourceMapSegment = null;
91
- for (const lineSegment of lineMapping) {
92
- if (lineSegment[0] <= column) {
93
- closestLineSegment = lineSegment;
98
+ let lowIndex = 0;
99
+ let highIndex = lineMapping.length - 1;
100
+ while (lowIndex <= highIndex) {
101
+ const middleIndex = (lowIndex + highIndex) >> 1;
102
+ if (lineMapping[middleIndex][0] <= column) {
103
+ closestLineSegment = lineMapping[middleIndex];
104
+ lowIndex = middleIndex + 1;
94
105
  } else {
95
- break;
106
+ highIndex = middleIndex - 1;
96
107
  }
97
108
  }
98
109
 
@@ -116,6 +127,7 @@ const getSourceFromMappings = (
116
127
  columnNumber: sourceColumn,
117
128
  fileName,
118
129
  lineNumber: sourceLine + 1,
130
+ isIgnoreListed: ignoredSourceIndices?.has(sourceIndex) ?? false,
119
131
  };
120
132
  };
121
133
 
@@ -125,12 +137,14 @@ export const getSourceFromSourceMap = (
125
137
  column: number,
126
138
  ): StackFrame | null => {
127
139
  if (sourceMap.sections) {
140
+ // Section offsets are 0-based while stack trace lines are 1-based.
141
+ const lineIndex = line - 1;
128
142
  let targetSection: DecodedSourceMapSection | null = null;
129
143
 
130
144
  for (const section of sourceMap.sections) {
131
145
  if (
132
- line > section.offset.line ||
133
- (line === section.offset.line && column >= section.offset.column)
146
+ lineIndex > section.offset.line ||
147
+ (lineIndex === section.offset.line && column >= section.offset.column)
134
148
  ) {
135
149
  targetSection = section;
136
150
  } else {
@@ -142,29 +156,40 @@ export const getSourceFromSourceMap = (
142
156
  return null;
143
157
  }
144
158
 
145
- const relativeLine = line - targetSection.offset.line;
159
+ const relativeLine = lineIndex - targetSection.offset.line;
146
160
  const relativeColumn =
147
- line === targetSection.offset.line ? column - targetSection.offset.column : column;
161
+ lineIndex === targetSection.offset.line ? column - targetSection.offset.column : column;
148
162
 
149
163
  return getSourceFromMappings(
150
164
  targetSection.map.mappings,
151
165
  targetSection.map.sources,
152
166
  relativeLine,
153
167
  relativeColumn,
168
+ targetSection.map.ignoredSourceIndices,
154
169
  );
155
170
  }
156
171
 
157
- return getSourceFromMappings(sourceMap.mappings, sourceMap.sources, line - 1, column);
172
+ return getSourceFromMappings(
173
+ sourceMap.mappings,
174
+ sourceMap.sources,
175
+ line - 1,
176
+ column,
177
+ sourceMap.ignoredSourceIndices,
178
+ );
158
179
  };
159
180
 
160
181
  const getSourceMapUrl = (url: string, content: string): null | string => {
161
- const lines = content.split("\n");
182
+ // Walk lines backwards without content.split("\n"), which would allocate a
183
+ // string per line of the entire bundle.
162
184
  let sourceMapUrl: string | undefined;
163
- for (let i = lines.length - 1; i >= 0 && !sourceMapUrl; i--) {
164
- const regexMatch = lines[i].match(SOURCEMAP_REGEX);
185
+ let searchEnd = content.length;
186
+ while (searchEnd > 0 && !sourceMapUrl) {
187
+ const lineStart = content.lastIndexOf("\n", searchEnd - 1) + 1;
188
+ const regexMatch = content.slice(lineStart, searchEnd).match(SOURCEMAP_REGEX);
165
189
  if (regexMatch) {
166
190
  sourceMapUrl = regexMatch[1] || regexMatch[2];
167
191
  }
192
+ searchEnd = lineStart - 1;
168
193
  }
169
194
 
170
195
  if (!sourceMapUrl) {
@@ -181,8 +206,14 @@ const getSourceMapUrl = (url: string, content: string): null | string => {
181
206
  return sourceMapUrl;
182
207
  };
183
208
 
209
+ const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> | undefined => {
210
+ const ignoreList = rawSourceMap.ignoreList ?? rawSourceMap.x_google_ignoreList;
211
+ return Array.isArray(ignoreList) && ignoreList.length > 0 ? new Set(ignoreList) : undefined;
212
+ };
213
+
184
214
  const decodeStandardSourceMap = (rawSourceMap: StandardSourceMap): SourceMap => ({
185
215
  file: rawSourceMap.file,
216
+ ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
186
217
  mappings: decode(rawSourceMap.mappings),
187
218
  names: rawSourceMap.names,
188
219
  sourceRoot: rawSourceMap.sourceRoot,
@@ -196,6 +227,7 @@ const decodeIndexSourceMap = (rawSourceMap: IndexSourceMap): SourceMap => {
196
227
  ({ map, offset }) => ({
197
228
  map: {
198
229
  ...map,
230
+ ignoredSourceIndices: getIgnoredSourceIndices(map),
199
231
  mappings: decode(map.mappings),
200
232
  },
201
233
  offset,
@@ -268,7 +300,9 @@ export const getSourceMapImpl = async (
268
300
  const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
269
301
 
270
302
  if (!sourceMapUrl) return null;
271
- if (!isFetchableUrl(sourceMapUrl)) {
303
+ // inline data: maps (vite dev, babel inline sourcemaps) are decoded by
304
+ // fetch itself, so they bypass the network-url check
305
+ if (!isFetchableUrl(sourceMapUrl) && !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
272
306
  return null;
273
307
  }
274
308
 
@@ -356,6 +390,7 @@ export const symbolicateStack = async (
356
390
  fileName: symbolicatedSource.fileName,
357
391
  lineNumber: symbolicatedSource.lineNumber,
358
392
  columnNumber: symbolicatedSource.columnNumber,
393
+ isIgnoreListed: symbolicatedSource.isIgnoreListed,
359
394
  isSymbolicated: true,
360
395
  };
361
396
  }),
package/src/types.ts CHANGED
@@ -298,6 +298,22 @@ export interface Family {
298
298
  current: unknown;
299
299
  }
300
300
 
301
+ /**
302
+ * React 19 flight metadata for a server component owner (ReactComponentInfo).
303
+ * Unlike client owners it has no `tag`; the owner chain continues via `owner`.
304
+ */
305
+ export interface ServerComponentInfo {
306
+ name?: string;
307
+ env?: string;
308
+ owner?: Fiber | ServerComponentInfo | null;
309
+ debugStack?: Error | null;
310
+ }
311
+
312
+ export interface RendererRefreshUpdate {
313
+ staleFamilies: Set<Family>;
314
+ updatedFamilies: Set<Family>;
315
+ }
316
+
301
317
  /**
302
318
  * Represents a react-internal Fiber node.
303
319
  */
@@ -367,6 +383,9 @@ export interface ReactDevToolsGlobalHook {
367
383
  onCommitFiberRoot: (rendererID: number, root: FiberRoot, priority: number | void) => void;
368
384
  onCommitFiberUnmount: (rendererID: number, fiber: Fiber) => void;
369
385
  onPostCommitFiberRoot: (rendererID: number, root: FiberRoot) => void;
386
+ // called by dev builds of react-reconciler on root schedule; absent from
387
+ // the hook react-refresh installs, so it stays optional
388
+ onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: ReactNode) => void;
370
389
  renderers: Map<number, ReactRenderer>;
371
390
  supportsFiber: boolean;
372
391
 
@@ -402,13 +421,7 @@ export interface ReactRenderer {
402
421
  reconcilerVersion: string;
403
422
  rendererPackageName: string;
404
423
  // react refresh
405
- scheduleRefresh?: (
406
- root: FiberRoot,
407
- update: {
408
- staleFamilies: Set<Family>;
409
- updatedFamilies: Set<Family>;
410
- },
411
- ) => void;
424
+ scheduleRefresh?: (root: FiberRoot, update: RendererRefreshUpdate) => void;
412
425
  scheduleRoot?: (root: FiberRoot, element: React.ReactNode) => void;
413
426
  scheduleUpdate?: (fiber: Fiber) => void;
414
427
 
@@ -0,0 +1,17 @@
1
+ export interface Unsubscribe extends Disposable {
2
+ (): void;
3
+ }
4
+
5
+ /**
6
+ * Wraps a teardown callback so it is both callable and a `Disposable`,
7
+ * letting subscriptions compose through explicit resource management:
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * using instrumentation = instrument({ onCommitFiberRoot });
12
+ * using refresh = instrumentReactRefresh({ onRefresh: handleRefresh });
13
+ * // both torn down automatically at scope exit
14
+ * ```
15
+ */
16
+ export const toUnsubscribe = (dispose: () => void): Unsubscribe =>
17
+ Object.assign(dispose, { [Symbol.dispose]: dispose });