@agent-scope/runtime 1.0.1

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,425 @@
1
+ import { ComponentType, ComponentNode, PageReport, ConsoleEntry, CapturedError, HookState, SuspenseBoundaryInfo } from '@agent-scope/core';
2
+ export { CapturedError, ComponentNode, PageReport, SuspenseBoundaryInfo } from '@agent-scope/core';
3
+
4
+ /**
5
+ * devtools-hook.ts
6
+ *
7
+ * Installs and manages the React DevTools global hook
8
+ * (`window.__REACT_DEVTOOLS_GLOBAL_HOOK__`).
9
+ *
10
+ * React checks for this hook at module initialisation time, so it MUST be
11
+ * installed before any React bundle is evaluated. Call `installHook()` as
12
+ * early as possible (e.g. the very first script on the page).
13
+ */
14
+ /**
15
+ * Minimal shape of a React renderer as surfaced through the DevTools hook.
16
+ *
17
+ * React passes a much larger object; we only reference the fields we need.
18
+ */
19
+ interface ReactRenderer {
20
+ /** Opaque renderer ID assigned by the hook on injection. */
21
+ id: number;
22
+ /** The renderer object passed by React via `inject()`. */
23
+ renderer: any;
24
+ /** Set of fiber root containers managed by this renderer. */
25
+ fiberRoots: Set<any>;
26
+ }
27
+ /**
28
+ * The shape we install at `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`.
29
+ *
30
+ * React calls `inject(renderer)` when it boots, giving us access to its
31
+ * internal fiber roots. Everything else is just enough scaffolding to keep
32
+ * React happy.
33
+ */
34
+ interface ScopeDevToolsHook {
35
+ /** Required by React – signals that DevTools are present. */
36
+ isDisabled: false;
37
+ /** Required by React – signals DevTools support for profiling. */
38
+ supportsFiber: true;
39
+ /**
40
+ * Called by React when it initialises a new renderer instance.
41
+ * Returns the renderer's numeric ID.
42
+ */
43
+ inject(renderer: unknown): number;
44
+ /** Called by React before each commit (we no-op this). */
45
+ onCommitFiberRoot(rendererID: number, root: unknown, priorityLevel: unknown): void;
46
+ /** Called by React when an unmount occurs (we no-op this). */
47
+ onCommitFiberUnmount(rendererID: number, fiber: unknown): void;
48
+ /** Called by React before post-commit effects (we no-op this). */
49
+ onPostCommitFiberRoot(rendererID: number, root: unknown): void;
50
+ /** Extension point: registered renderers keyed by ID. */
51
+ _renderers: Map<number, ReactRenderer>;
52
+ /** Whether the hook was installed by Scope (vs. pre-existing DevTools). */
53
+ _isScopeHook: true;
54
+ }
55
+ /**
56
+ * Install the Scope DevTools hook at `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`.
57
+ *
58
+ * - If no hook exists yet, installs a new Scope hook.
59
+ * - If a Scope hook is already installed, returns the existing instance.
60
+ * - If a *third-party* hook is already present (e.g. real React DevTools),
61
+ * we patch it minimally so that renderer registrations are forwarded to us
62
+ * as well.
63
+ *
64
+ * Must be called **before React is loaded** to intercept `inject()`.
65
+ */
66
+ declare function installHook(): ScopeDevToolsHook;
67
+ /**
68
+ * Retrieve the currently installed hook.
69
+ *
70
+ * Returns `null` if `installHook()` has not yet been called.
71
+ */
72
+ declare function getHook(): ScopeDevToolsHook | null;
73
+ /**
74
+ * Return a snapshot of all registered React renderers.
75
+ *
76
+ * Empty until React calls `inject()`.
77
+ */
78
+ declare function getRenderers(): ReactRenderer[];
79
+ /**
80
+ * Wait until at least one React renderer has registered with the hook.
81
+ *
82
+ * @param timeout - Maximum milliseconds to wait (default: 10 000).
83
+ * @returns A promise that resolves with the first registered renderer.
84
+ * @throws If the timeout elapses with no renderer.
85
+ */
86
+ declare function waitForReact(timeout?: number): Promise<ReactRenderer>;
87
+
88
+ /**
89
+ * fiber-walker.ts
90
+ *
91
+ * Traverses a React fiber tree and converts it into the `ComponentNode` tree
92
+ * defined in `@agent-scope/core`.
93
+ *
94
+ * Fiber internals are undocumented React implementation details. We rely on:
95
+ * - `fiber.tag` — type classifier (see FIBER_TAGS below)
96
+ * - `fiber.type` — function/class reference or string for host elements
97
+ * - `fiber.memoizedProps` — current props snapshot
98
+ * - `fiber.child` — first child fiber
99
+ * - `fiber.sibling` — next sibling fiber
100
+ * - `fiber._debugSource` — source location injected by babel (dev only)
101
+ * - `fiber._debugID` — internal numeric ID (dev only, may be absent)
102
+ * - `fiber.index` — position among siblings
103
+ */
104
+
105
+ /**
106
+ * Minimal shape of a React fiber as accessed by this walker.
107
+ * Only the fields we actually read are declared here.
108
+ */
109
+ interface Fiber {
110
+ tag: number;
111
+ type: any;
112
+ memoizedProps: Record<string, any> | null;
113
+ child: Fiber | null;
114
+ sibling: Fiber | null;
115
+ return: Fiber | null;
116
+ /** Present in React dev builds (>=16). Numeric per-session fiber ID. */
117
+ _debugID?: number;
118
+ /** Position among siblings (0-based). Used as fallback ID. */
119
+ index: number;
120
+ /** Injected by `babel-plugin-transform-react-jsx-source` (dev only). */
121
+ _debugSource?: {
122
+ fileName: string;
123
+ lineNumber: number;
124
+ columnNumber: number;
125
+ } | null;
126
+ [key: string]: any;
127
+ }
128
+ interface WalkOptions {
129
+ /**
130
+ * When `true`, host elements (div, span, etc.) are included in the output.
131
+ * Default: `false`.
132
+ */
133
+ includeHostElements?: boolean;
134
+ }
135
+ /**
136
+ * Derive the display name from a fiber's `type` field.
137
+ *
138
+ * Resolution order:
139
+ * 1. `type.displayName`
140
+ * 2. `type.name` (function/class name — unless inferred from a property key)
141
+ * 3. For forwardRef wrappers: inner `render.displayName` / `render.name`
142
+ * 4. For memo wrappers: inner `type.displayName` / `type.name`
143
+ * 5. String tags for host elements ("div", "span", …)
144
+ * 6. Fallback: "Anonymous"
145
+ */
146
+ declare function extractName(fiber: Fiber): string;
147
+ /**
148
+ * Map a fiber tag (and optionally the `type` object's `$$typeof`) to one of
149
+ * our `ComponentType` discriminants.
150
+ */
151
+ declare function classifyType(fiber: Fiber): ComponentType;
152
+ /**
153
+ * Walk a fiber and all its descendants, collecting `ComponentNode`s.
154
+ *
155
+ * @param rootFiber - The starting fiber.
156
+ * @param options - Walk configuration.
157
+ * @returns The `ComponentNode` for `rootFiber`, or `null` if the root should
158
+ * be skipped. Children are populated recursively.
159
+ */
160
+ declare function walkFiber(fiber: Fiber | null, options?: WalkOptions): ComponentNode | null;
161
+ /**
162
+ * Walk a fiber root (the object stored in `ReactRenderer.fiberRoots`) and
163
+ * return the root `ComponentNode`, or `null` if the tree is empty.
164
+ *
165
+ * `fiberRoot.current` is the `HostRoot` fiber — we skip it and return the
166
+ * first meaningful child.
167
+ */
168
+ declare function walkFiberRoot(fiberRoot: any, options?: WalkOptions): ComponentNode | null;
169
+
170
+ /**
171
+ * capture.ts
172
+ *
173
+ * High-level `capture()` function — wires together the DevTools hook and fiber
174
+ * walker to produce a partial `PageReport`.
175
+ */
176
+
177
+ interface CaptureOptions extends WalkOptions {
178
+ /**
179
+ * URL to record in the report.
180
+ * Defaults to `window.location.href` in a browser context.
181
+ */
182
+ url?: string;
183
+ /**
184
+ * Timeout in milliseconds to wait for React to boot before giving up.
185
+ * Default: 10 000.
186
+ */
187
+ reactTimeout?: number;
188
+ }
189
+ /** The fields populated by `capture()`. The rest can be added by callers. */
190
+ type CaptureResult = Pick<PageReport, "url" | "timestamp" | "capturedIn" | "tree" | "consoleEntries" | "errors" | "suspenseBoundaries">;
191
+ /**
192
+ * Capture a snapshot of the current React component tree.
193
+ *
194
+ * Ensures the DevTools hook is installed, waits for React to register a
195
+ * renderer (if it hasn't already), then walks the first available fiber root.
196
+ *
197
+ * Console messages emitted during the capture window are recorded via the
198
+ * console interceptor and included in the returned `consoleEntries` field.
199
+ *
200
+ * Also detects error boundaries that have caught errors and Suspense
201
+ * boundaries with their current suspension status.
202
+ *
203
+ * @returns A partial `PageReport` containing `url`, `timestamp`, `capturedIn`,
204
+ * `tree`, `consoleEntries`, `errors`, and `suspenseBoundaries`.
205
+ * @throws If no React renderer registers within `reactTimeout` ms, or if the
206
+ * fiber tree is empty.
207
+ */
208
+ declare function capture(options?: CaptureOptions): Promise<CaptureResult>;
209
+
210
+ /**
211
+ * console-interceptor.ts
212
+ *
213
+ * Monkey-patches `console.log`, `console.warn`, `console.error`,
214
+ * `console.info`, `console.debug`, `console.group`, `console.groupCollapsed`,
215
+ * `console.table`, and `console.trace` to capture messages with React
216
+ * component attribution.
217
+ *
218
+ * Component attribution is resolved by reading the DevTools hook's
219
+ * currently-active fiber (set during React render). When no fiber is active
220
+ * (effect, event handler, async callback) `componentName` is `null`.
221
+ *
222
+ * The interceptor never suppresses output — every patched method calls the
223
+ * original implementation with all original arguments.
224
+ */
225
+
226
+ /**
227
+ * Install the console interceptor.
228
+ *
229
+ * Replaces `console.log`, `console.warn`, `console.error`, `console.info`,
230
+ * `console.debug`, `console.group`, `console.groupCollapsed`, `console.table`,
231
+ * and `console.trace` with wrappers that record entries.
232
+ *
233
+ * Idempotent — calling `install` twice without `uninstall` in between is safe.
234
+ */
235
+ declare function installConsoleInterceptor(): void;
236
+ /**
237
+ * Uninstall the console interceptor and restore all original methods.
238
+ *
239
+ * Idempotent — safe to call even if the interceptor is not installed.
240
+ */
241
+ declare function uninstallConsoleInterceptor(): void;
242
+ /**
243
+ * Return a snapshot of all captured `ConsoleEntry` objects since the last
244
+ * `clearConsoleEntries()` call (or since install).
245
+ *
246
+ * The returned array is a shallow copy — mutations do not affect the buffer.
247
+ */
248
+ declare function getConsoleEntries(): ConsoleEntry[];
249
+ /**
250
+ * Clear the captured entries buffer.
251
+ */
252
+ declare function clearConsoleEntries(): void;
253
+
254
+ /**
255
+ * error-detector.ts
256
+ *
257
+ * Detects React error boundaries in the fiber tree and extracts information
258
+ * about any errors they have caught.
259
+ *
260
+ * React error boundaries are class components that implement at least one of:
261
+ * - `static getDerivedStateFromError(error)` — static method
262
+ * - `componentDidCatch(error, info)` — instance method
263
+ *
264
+ * When a boundary has caught an error, React stores error info in the fiber's
265
+ * `memoizedState` (the current component state snapshot). Common patterns:
266
+ * - `{ hasError: true, error: Error }` (class-level state)
267
+ * - `{ error: Error }` (minimal pattern)
268
+ *
269
+ * We walk the fiber tree, identify boundary components, inspect their state,
270
+ * and build `CapturedError` objects when an error is present.
271
+ */
272
+
273
+ /**
274
+ * Walk the fiber tree depth-first and collect `CapturedError` entries for
275
+ * every error boundary that is currently holding a caught error.
276
+ *
277
+ * @param rootFiber - Top of the fiber tree (typically `HostRoot.child`).
278
+ * @returns Array of `CapturedError` objects; empty when nothing has been caught.
279
+ */
280
+ declare function detectErrors(rootFiber: Fiber | null): CapturedError[];
281
+
282
+ /**
283
+ * hooks-extractor.ts
284
+ *
285
+ * Extracts `HookState[]` from a React fiber's `memoizedState` linked list.
286
+ *
287
+ * React stores hooks as a singly-linked list where each node has:
288
+ * - `memoizedState` — the hook's stored value (shape varies by hook type)
289
+ * - `queue` — update queue present on useState / useReducer
290
+ * - `next` — pointer to the next hook node in call order
291
+ *
292
+ * Hook types are NOT tagged by React, so we identify them by inspecting the
293
+ * shape of each node (duck-typing). The heuristics below mirror the internal
294
+ * structure documented in React's source (ReactFiberHooks.js).
295
+ *
296
+ * @internal
297
+ */
298
+
299
+ /**
300
+ * Extract an ordered list of `HookState` snapshots from a fiber's
301
+ * `memoizedState` linked list.
302
+ *
303
+ * @param fiber - A React fiber object (typed as `any` because React doesn't
304
+ * export its internal fiber shape).
305
+ * @returns An ordered array of `HookState` entries (one per hook call),
306
+ * or an empty array if the fiber has no hooks or is null.
307
+ */
308
+ declare function extractHooks(fiber: any): HookState[];
309
+
310
+ /**
311
+ * profiler.ts
312
+ *
313
+ * Instruments React's commit cycle to capture per-component render timing data.
314
+ *
315
+ * React exposes profiling data on fiber objects in development/profiling builds:
316
+ * - `fiber.actualDuration` — wall-clock ms spent rendering this fiber + descendants
317
+ * - `fiber.selfBaseDuration` — ms spent rendering just this fiber (excl. children)
318
+ * - `fiber.actualStartTime` — performance.now() timestamp when render began
319
+ *
320
+ * We hook into `onCommitFiberRoot` on the DevTools hook to intercept every
321
+ * commit, walk the work-in-progress subtree, and accumulate counts + durations
322
+ * keyed by fiber identity.
323
+ */
324
+
325
+ /** Public snapshot returned by `getProfilingData`. */
326
+ interface ProfilingSnapshot {
327
+ renderCount: number;
328
+ renderDuration: number;
329
+ }
330
+ /**
331
+ * Install the profiler onto a DevTools hook.
332
+ *
333
+ * Wraps `hook.onCommitFiberRoot` so that every React commit triggers a walk
334
+ * of the committed fiber subtree to accumulate profiling data.
335
+ *
336
+ * Safe to call multiple times — subsequent calls on the same hook are no-ops.
337
+ */
338
+ declare function installProfiler(hook: ScopeDevToolsHook): void;
339
+ /**
340
+ * Retrieve the accumulated profiling snapshot for a fiber by its numeric ID.
341
+ *
342
+ * @param fiberId - The fiber's `_debugID` (or synthetic ID assigned by this module).
343
+ * @returns `{ renderCount, renderDuration }` if data exists, `null` otherwise.
344
+ */
345
+ declare function getProfilingData(fiberIdParam: number): ProfilingSnapshot | null;
346
+ /**
347
+ * Reset all accumulated profiling data and the installed state.
348
+ *
349
+ * Primarily used in tests and when re-initialising the capture session.
350
+ * Note: this does NOT restore the original `onCommitFiberRoot` handler —
351
+ * call `installProfiler` again on a fresh hook after resetting.
352
+ */
353
+ declare function resetProfilingData(): void;
354
+
355
+ /**
356
+ * suspense-detector.ts
357
+ *
358
+ * Detects `<Suspense>` boundaries in the fiber tree and determines their
359
+ * current status at capture time.
360
+ *
361
+ * React stores Suspense state in the fiber's `memoizedState` field using an
362
+ * internal linked-list structure. The first node in the list is the dehydrated
363
+ * / retrying state object when the boundary is suspended.
364
+ *
365
+ * Status heuristics (based on React internals):
366
+ *
367
+ * resolved — `memoizedState === null`
368
+ * The boundary rendered its normal children successfully.
369
+ *
370
+ * pending — `memoizedState.dehydrated` is set, OR the state object
371
+ * contains a thenable (`then` function) anywhere at the top
372
+ * level, indicating React is still waiting for data.
373
+ *
374
+ * fallback — `memoizedState` is non-null but does not look like a pending
375
+ * thenable; the fallback subtree is being rendered. This covers
376
+ * the case where the promise has been thrown but not yet
377
+ * resolved, and React is showing the fallback UI.
378
+ *
379
+ * Note: these are approximations over undocumented internals. React does not
380
+ * expose a public API for querying Suspense status from outside the component.
381
+ */
382
+
383
+ /**
384
+ * Walk the fiber tree and collect `SuspenseBoundaryInfo` for every
385
+ * `<Suspense>` boundary found.
386
+ *
387
+ * @param rootFiber - Starting fiber (typically the HostRoot's first child).
388
+ * @returns Flat list of Suspense boundary descriptors.
389
+ */
390
+ declare function detectSuspenseBoundaries(rootFiber: Fiber | null): SuspenseBoundaryInfo[];
391
+
392
+ /**
393
+ * @agent-scope/runtime
394
+ *
395
+ * Browser instrumentation — captures React component trees at runtime.
396
+ * Depends on @agent-scope/core for types.
397
+ *
398
+ * @packageDocumentation
399
+ */
400
+
401
+ /** Options for initializing the Scope runtime */
402
+ interface RuntimeOptions {
403
+ /** Called whenever a new PageReport is captured */
404
+ onCapture?: (report: PageReport) => void;
405
+ /** Maximum number of reports to buffer before evicting the oldest */
406
+ bufferSize?: number;
407
+ }
408
+ /** Manages an active capture session */
409
+ declare class ScopeRuntime {
410
+ private readonly buffer;
411
+ private readonly options;
412
+ constructor(options?: RuntimeOptions);
413
+ /** Record a captured PageReport */
414
+ record(report: PageReport): void;
415
+ /** Return all buffered reports and clear the buffer */
416
+ flush(): PageReport[];
417
+ /** Return a read-only snapshot of the buffer */
418
+ getBuffer(): readonly PageReport[];
419
+ /** Find a node by fiber ID within a component tree */
420
+ static findNode(root: ComponentNode, id: number): ComponentNode | null;
421
+ }
422
+ /** Create and return a new ScopeRuntime instance */
423
+ declare function createRuntime(options?: RuntimeOptions): ScopeRuntime;
424
+
425
+ export { type CaptureOptions, type CaptureResult, type Fiber, type ProfilingSnapshot, type ReactRenderer, type RuntimeOptions, type ScopeDevToolsHook, ScopeRuntime, type WalkOptions, capture, classifyType, clearConsoleEntries, createRuntime, detectErrors, detectSuspenseBoundaries, extractHooks, extractName, getConsoleEntries, getHook, getProfilingData, getRenderers, installConsoleInterceptor, installHook, installProfiler, resetProfilingData, uninstallConsoleInterceptor, waitForReact, walkFiber, walkFiberRoot };