react-perf-recorder 0.1.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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/claude/README.md +10 -0
- package/claude/agents/perf-recorder.md +44 -0
- package/claude/mcp.json +8 -0
- package/claude/skills/react-perf-recorder/SKILL.md +52 -0
- package/claude/skills/react-perf-recorder/references/causes-and-actions.md +47 -0
- package/claude/skills/react-perf-recorder/references/from-scripts.md +36 -0
- package/claude/skills/react-perf-recorder/references/getting-a-recording.md +41 -0
- package/claude/skills/react-perf-recorder/references/measuring-a-fix.md +42 -0
- package/claude/skills/react-perf-recorder/references/panel.md +21 -0
- package/claude/skills/react-perf-recorder/references/reading-a-recording.md +55 -0
- package/dist/browser/chunk-7DJUCCWG.js +447 -0
- package/dist/browser/chunk-NTY2W4HE.js +182 -0
- package/dist/browser/client.d.ts +589 -0
- package/dist/browser/client.js +7161 -0
- package/dist/browser/index-BlkKhwHe.d.ts +585 -0
- package/dist/browser/plugins/proxy-memoize.d.ts +7 -0
- package/dist/browser/plugins/proxy-memoize.js +41 -0
- package/dist/browser/plugins/react-query.d.ts +5 -0
- package/dist/browser/plugins/react-query.js +74 -0
- package/dist/browser/plugins/zustand.d.ts +11 -0
- package/dist/browser/plugins/zustand.js +171 -0
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +12 -0
- package/dist/cli.js +23119 -0
- package/dist/engine.iife.js +3661 -0
- package/dist/node/chunk-HS2BJBJX.js +170 -0
- package/dist/node/plugin-api-zXFxjYba.d.cts +61 -0
- package/dist/node/plugin-api-zXFxjYba.d.ts +61 -0
- package/dist/node/plugins/proxy-memoize.cjs +214 -0
- package/dist/node/plugins/proxy-memoize.d.cts +16 -0
- package/dist/node/plugins/proxy-memoize.d.ts +16 -0
- package/dist/node/plugins/proxy-memoize.js +51 -0
- package/dist/node/plugins/react-query.cjs +32 -0
- package/dist/node/plugins/react-query.d.cts +6 -0
- package/dist/node/plugins/react-query.d.ts +6 -0
- package/dist/node/plugins/react-query.js +7 -0
- package/dist/node/plugins/zustand.cjs +247 -0
- package/dist/node/plugins/zustand.d.cts +17 -0
- package/dist/node/plugins/zustand.d.ts +17 -0
- package/dist/node/plugins/zustand.js +61 -0
- package/dist/node/vite.cjs +1262 -0
- package/dist/node/vite.d.cts +103 -0
- package/dist/node/vite.d.ts +103 -0
- package/dist/node/vite.js +1072 -0
- package/docs/contributing.md +24 -0
- package/docs/how-it-works.md +34 -0
- package/docs/mcp.md +62 -0
- package/docs/measuring-a-fix.md +65 -0
- package/docs/options.md +22 -0
- package/docs/panel.md +59 -0
- package/docs/plugins.md +57 -0
- package/docs/recording.md +44 -0
- package/package.json +139 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { R as RuntimePlugin, F as Fiber, a as RuntimePluginFactory, P as PluginInfo, b as PluginContext, C as CauseInput, c as Primitive, S as SessionContext, d as PluginSection, e as Conditions, f as RecordingV2, g as ReasonInfo, h as RootStat, H as HookInfo, A as ActionRecord, G as GLOBAL_KEY } from './index-BlkKhwHe.js';
|
|
2
|
+
|
|
3
|
+
interface Describer {
|
|
4
|
+
selector(fn: Function): string;
|
|
5
|
+
store(getSnapshot: Function): string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CauseEvent {
|
|
9
|
+
plugin: string;
|
|
10
|
+
type: string;
|
|
11
|
+
atMs: number;
|
|
12
|
+
changes?: Array<{
|
|
13
|
+
key: string;
|
|
14
|
+
prev: unknown;
|
|
15
|
+
next: unknown;
|
|
16
|
+
}>;
|
|
17
|
+
data?: Record<string, Primitive>;
|
|
18
|
+
/** Components the event scheduled updates on, when the core knows them: the cause goes to those roots only. */
|
|
19
|
+
fibers?: Set<Fiber>;
|
|
20
|
+
/** The event knows which components it woke — even if that turned out to be none of them. */
|
|
21
|
+
aimed?: true;
|
|
22
|
+
}
|
|
23
|
+
type PluginEntry = [RuntimePluginFactory | RuntimePlugin, unknown];
|
|
24
|
+
interface Loaded {
|
|
25
|
+
plugin: RuntimePlugin;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Runtime halves of the plugins: labels, cause events and their sections of the recording. */
|
|
29
|
+
declare class PluginHost implements Describer {
|
|
30
|
+
readonly loaded: Loaded[];
|
|
31
|
+
recording: boolean;
|
|
32
|
+
/** Set by the recorder: components that got updates since the last commit, to aim a cause at their roots. */
|
|
33
|
+
targets: (() => Set<Fiber>) | null;
|
|
34
|
+
private buffer;
|
|
35
|
+
/** `waitForTimer` events, until a timer of their plugin's packages runs. */
|
|
36
|
+
private waiting;
|
|
37
|
+
private readonly owners;
|
|
38
|
+
/** Plugins whose events a timer of theirs has delivered in this recording. */
|
|
39
|
+
private readonly delivering;
|
|
40
|
+
private t0;
|
|
41
|
+
readonly warnings: string[];
|
|
42
|
+
constructor(entries: PluginEntry[]);
|
|
43
|
+
setupAll(): void;
|
|
44
|
+
info(): PluginInfo[];
|
|
45
|
+
context(name: string): PluginContext;
|
|
46
|
+
/** Aimed only when the caller says it can be: an event that runs before React cannot say whom it woke. */
|
|
47
|
+
emit(plugin: string, event: CauseInput, fibers?: Set<Fiber>): CauseEvent | null;
|
|
48
|
+
get hasWaiting(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* A timer of `library` updated components: the events its plugin had waiting since before it started go to them,
|
|
51
|
+
* or to the commit as it is when `fibers` is null (the commit ran inside the timer). Returns whether it was a
|
|
52
|
+
* plugin's delivery, which then needs no cause of its own. A timer that updated no one is not called: a library
|
|
53
|
+
* runs other timers too (garbage collection, the next poll), and one of those must not take the events.
|
|
54
|
+
*/
|
|
55
|
+
deliver(library: string | null, fibers: () => Set<Fiber> | null, startedAt?: number): boolean;
|
|
56
|
+
drain(): CauseEvent[];
|
|
57
|
+
now(): number;
|
|
58
|
+
selector(fn: Function, depth?: number): string;
|
|
59
|
+
store(getSnapshot: Function): string | null;
|
|
60
|
+
private describe;
|
|
61
|
+
start(session: Omit<SessionContext, keyof PluginContext>, t0: number): void;
|
|
62
|
+
commit(session: Omit<SessionContext, keyof PluginContext>): void;
|
|
63
|
+
stop(session: Omit<SessionContext, keyof PluginContext>): Record<string, PluginSection>;
|
|
64
|
+
conditions(): Conditions;
|
|
65
|
+
private guard;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ScopeHandle {
|
|
69
|
+
kind: 'scope';
|
|
70
|
+
/** Every fiber from the HostRoot down to the scope component; either object of an alternate pair. */
|
|
71
|
+
chain: Fiber[];
|
|
72
|
+
name: string;
|
|
73
|
+
source: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface EngineConfig {
|
|
77
|
+
version: string;
|
|
78
|
+
projectRoot: string;
|
|
79
|
+
wrapperPattern: string;
|
|
80
|
+
actions: {
|
|
81
|
+
values: boolean;
|
|
82
|
+
secretSelector: string;
|
|
83
|
+
};
|
|
84
|
+
maxDurationMs: number;
|
|
85
|
+
bigCommit: number;
|
|
86
|
+
timelineLimit: number;
|
|
87
|
+
/** Timers are wrapped at boot; off leaves the page's timers alone and timer causes out. */
|
|
88
|
+
timers?: boolean;
|
|
89
|
+
}
|
|
90
|
+
interface RecordOptions {
|
|
91
|
+
source?: string;
|
|
92
|
+
label?: string;
|
|
93
|
+
scope?: ScopeHandle | null;
|
|
94
|
+
watch?: string[];
|
|
95
|
+
zones?: Record<string, string | {
|
|
96
|
+
selector: string;
|
|
97
|
+
viaAriaControls?: boolean;
|
|
98
|
+
}>;
|
|
99
|
+
highlight?: boolean;
|
|
100
|
+
actions?: boolean;
|
|
101
|
+
hookNames?: boolean;
|
|
102
|
+
prune?: boolean;
|
|
103
|
+
/** Count animation frames for a rough fps; the commit scan itself costs frame time, so trust long frames more. */
|
|
104
|
+
frames?: boolean;
|
|
105
|
+
bigCommit?: number;
|
|
106
|
+
timeline?: number;
|
|
107
|
+
meta?: Record<string, Primitive>;
|
|
108
|
+
/** Work out parent-caused reasons for the first instances of a component per commit only; render counts stay exact. */
|
|
109
|
+
sampleReasons?: boolean;
|
|
110
|
+
}
|
|
111
|
+
interface HighlightSink {
|
|
112
|
+
/** The panel's toggle; a recording draws and counts as highlighted only while it is on. */
|
|
113
|
+
enabled?: boolean;
|
|
114
|
+
/** `mounted`: the tops of subtrees mounted into the tree, outlined apart from renders. */
|
|
115
|
+
flash(pairs: Array<[Element, string, Fiber]>, withoutDom: Set<Fiber>, mounted?: Set<Fiber>): void;
|
|
116
|
+
/** Time spent measuring and drawing outside commits since the last call. */
|
|
117
|
+
takeCostMs?(): number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface BootConfig extends EngineConfig {
|
|
121
|
+
/** `{base}__react-perf-recorder`; null when no dev server stores sessions. */
|
|
122
|
+
endpoint: string | null;
|
|
123
|
+
}
|
|
124
|
+
type ScopeSpec = ScopeHandle | {
|
|
125
|
+
selector: string;
|
|
126
|
+
component?: string;
|
|
127
|
+
level?: number;
|
|
128
|
+
} | {
|
|
129
|
+
names: string[];
|
|
130
|
+
} | null;
|
|
131
|
+
interface StartOptions extends Omit<RecordOptions, 'scope'> {
|
|
132
|
+
scope?: ScopeSpec;
|
|
133
|
+
/** Stream the session to the dev server; on by default when there is one. */
|
|
134
|
+
save?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** What the tree may hide: components of packages, providers, and the app's own unnamed wrappers. */
|
|
137
|
+
interface Shown {
|
|
138
|
+
library: boolean;
|
|
139
|
+
providers: boolean;
|
|
140
|
+
}
|
|
141
|
+
interface Owner {
|
|
142
|
+
name: string;
|
|
143
|
+
source: string;
|
|
144
|
+
/** An unnamed component the app left for the pattern to name: `Anonymous`, `Memo`, `ForwardRef`. */
|
|
145
|
+
wrapper: boolean;
|
|
146
|
+
/** Hands a context down and nothing else. */
|
|
147
|
+
provider: boolean;
|
|
148
|
+
/** A component of a package, not of the app: no site of its own, or a file under node_modules. */
|
|
149
|
+
library: boolean;
|
|
150
|
+
fiber: Fiber;
|
|
151
|
+
}
|
|
152
|
+
interface Saved extends RecordingV2 {
|
|
153
|
+
id?: string;
|
|
154
|
+
dir?: string;
|
|
155
|
+
saveError?: string;
|
|
156
|
+
}
|
|
157
|
+
/** The page-side API: `window.__REACT_PERF_RECORDER__.engine`. */
|
|
158
|
+
declare class Engine {
|
|
159
|
+
readonly config: BootConfig;
|
|
160
|
+
readonly plugins: PluginHost;
|
|
161
|
+
private highlight;
|
|
162
|
+
private ownHost;
|
|
163
|
+
last: Saved | null;
|
|
164
|
+
private recorder;
|
|
165
|
+
/** The area the running recording was started with, as it was found: a load recording finds it before the panel does. */
|
|
166
|
+
private recordingScope;
|
|
167
|
+
private writer;
|
|
168
|
+
private timer;
|
|
169
|
+
/** `saved`: a recording that stopped on its own at the length limit, once it is saved — no one else asked for it. */
|
|
170
|
+
private listeners;
|
|
171
|
+
/** The stop the length limit made, for whoever was waiting to stop it themselves. */
|
|
172
|
+
private autoStop;
|
|
173
|
+
private readonly wrapperRe;
|
|
174
|
+
private idle;
|
|
175
|
+
private idleHighlight;
|
|
176
|
+
private idleRetry;
|
|
177
|
+
constructor(config: BootConfig, plugins: PluginHost, highlight?: HighlightSink | null, ownHost?: Element | null);
|
|
178
|
+
/** The panel and its highlight canvas: events inside the panel are not user actions. */
|
|
179
|
+
attachUi(highlight: HighlightSink | null, ownHost: Element | null): void;
|
|
180
|
+
get version(): string;
|
|
181
|
+
get recording(): boolean;
|
|
182
|
+
get scopeOfRecording(): ScopeHandle | null;
|
|
183
|
+
onChange(listener: (state: 'started' | 'stopped' | 'saved', saved?: Saved) => void): () => boolean;
|
|
184
|
+
status(): {
|
|
185
|
+
version: string;
|
|
186
|
+
recording: boolean;
|
|
187
|
+
busyOwner: string | null;
|
|
188
|
+
react: {
|
|
189
|
+
found: boolean;
|
|
190
|
+
version: string | null;
|
|
191
|
+
roots: number;
|
|
192
|
+
};
|
|
193
|
+
scope: {
|
|
194
|
+
name: string;
|
|
195
|
+
source: string;
|
|
196
|
+
} | null;
|
|
197
|
+
sessionId: string | null;
|
|
198
|
+
plugins: PluginInfo[];
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Outlines renders in the area while nothing is recorded. A recording takes the commit hook over and, with
|
|
202
|
+
* `highlight` on, keeps drawing; this resumes after it stops.
|
|
203
|
+
*/
|
|
204
|
+
highlightWhenIdle(on: boolean, scope?: ScopeSpec): void;
|
|
205
|
+
get idleHighlighting(): boolean;
|
|
206
|
+
private syncIdleHighlight;
|
|
207
|
+
start(options?: StartOptions): {
|
|
208
|
+
scope: {
|
|
209
|
+
name: string;
|
|
210
|
+
source: string;
|
|
211
|
+
} | null;
|
|
212
|
+
};
|
|
213
|
+
/** Stops, builds the recording and, with a dev server, saves it; resolves with the id of the saved session. */
|
|
214
|
+
stop(): Promise<Saved>;
|
|
215
|
+
record(durationMs: number, options?: StartOptions): Promise<Saved>;
|
|
216
|
+
live(): {
|
|
217
|
+
commits: number;
|
|
218
|
+
commitsInScope: number;
|
|
219
|
+
renders: number;
|
|
220
|
+
rendersPerSec: number;
|
|
221
|
+
elapsedMs: number;
|
|
222
|
+
scopeState: "attached" | "lost" | null;
|
|
223
|
+
topRoots: ({
|
|
224
|
+
reason: string;
|
|
225
|
+
info: ReasonInfo;
|
|
226
|
+
name: string;
|
|
227
|
+
hits: number;
|
|
228
|
+
perHit: number;
|
|
229
|
+
} | {
|
|
230
|
+
reason: string;
|
|
231
|
+
info?: undefined;
|
|
232
|
+
name: string;
|
|
233
|
+
hits: number;
|
|
234
|
+
perHit: number;
|
|
235
|
+
})[];
|
|
236
|
+
} | null;
|
|
237
|
+
noteHmr(type: string, paths: string[]): void;
|
|
238
|
+
/** The page unloads mid-recording: flush what is left; the server marks the session interrupted. */
|
|
239
|
+
interrupt(): void;
|
|
240
|
+
/**
|
|
241
|
+
* The app's own components on the page right now, by name. A script that was told to record inside one and did
|
|
242
|
+
* not find it can say what there is instead of failing into nothing.
|
|
243
|
+
*/
|
|
244
|
+
componentNames(limit?: number): string[];
|
|
245
|
+
/**
|
|
246
|
+
* Every instance of a component on the page now, found by its name and file. A line that moved since the
|
|
247
|
+
* recording (the code was edited) still finds it by the file; failing that, by the name alone.
|
|
248
|
+
*/
|
|
249
|
+
findComponents(name: string, source?: string, limit?: number): Fiber[];
|
|
250
|
+
/**
|
|
251
|
+
* The topmost of the app's own components, as the tree shows them with these filters: where a tree of the whole
|
|
252
|
+
* app opens when no area has been picked yet.
|
|
253
|
+
*/
|
|
254
|
+
topComponent(shown: Shown): Fiber | null;
|
|
255
|
+
/** Composite ancestors of an element, nearest first. */
|
|
256
|
+
owners(el: Element): Owner[];
|
|
257
|
+
/** The fiber itself when it is a component, then its composite ancestors, nearest first. */
|
|
258
|
+
ownersOfFiber(fiber: Fiber): Owner[];
|
|
259
|
+
ownerOf(fiber: Fiber): Owner;
|
|
260
|
+
/** Nearest components below one; what is hidden is walked through, not stopped at. */
|
|
261
|
+
childOwners(fiber: Fiber, shown: Shown): Owner[];
|
|
262
|
+
/** A component the tree leaves out: the checkboxes decide, and an unnamed wrapper follows the library one. */
|
|
263
|
+
hidden(owner: Owner, shown: Shown): boolean;
|
|
264
|
+
scopeFromFiber(fiber: Fiber): ScopeHandle;
|
|
265
|
+
scopeFromElement(el: Element, level?: number): ScopeHandle;
|
|
266
|
+
/** Finds the component again after a reload by its composite path, e.g. ['OrdersPanel', 'PositionTable']. */
|
|
267
|
+
scopeFromNames(names: string[]): ScopeHandle;
|
|
268
|
+
private resolveScope;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface RootLine {
|
|
272
|
+
root: string;
|
|
273
|
+
source: string;
|
|
274
|
+
path: string;
|
|
275
|
+
hits: number;
|
|
276
|
+
hitsPerSec: number;
|
|
277
|
+
instances: number;
|
|
278
|
+
perHit: number;
|
|
279
|
+
noDomChange: number;
|
|
280
|
+
/** Components mounted under the root: remounts on every hit point at a component declared in render or a new key. */
|
|
281
|
+
mounts?: number;
|
|
282
|
+
renderMsPerHit?: number;
|
|
283
|
+
reasons: string[];
|
|
284
|
+
causes: string[];
|
|
285
|
+
lanes?: string;
|
|
286
|
+
}
|
|
287
|
+
interface ActionLine {
|
|
288
|
+
id: number;
|
|
289
|
+
what: string;
|
|
290
|
+
atSec: number;
|
|
291
|
+
commits: number;
|
|
292
|
+
renders: number;
|
|
293
|
+
reaction: number;
|
|
294
|
+
perChar?: string;
|
|
295
|
+
latencyMs?: number;
|
|
296
|
+
longFrames?: number;
|
|
297
|
+
topRoot?: string;
|
|
298
|
+
}
|
|
299
|
+
interface Summary {
|
|
300
|
+
id?: string;
|
|
301
|
+
status?: string;
|
|
302
|
+
partial?: boolean;
|
|
303
|
+
createdAt: string;
|
|
304
|
+
label?: string;
|
|
305
|
+
source: string;
|
|
306
|
+
url: string;
|
|
307
|
+
viewport: string;
|
|
308
|
+
durationSec: number;
|
|
309
|
+
scope: {
|
|
310
|
+
name: string;
|
|
311
|
+
source: string;
|
|
312
|
+
state: string;
|
|
313
|
+
remounts: number;
|
|
314
|
+
} | null;
|
|
315
|
+
totals: {
|
|
316
|
+
commits: number;
|
|
317
|
+
commitsPerSec: number;
|
|
318
|
+
commitsInScope: number;
|
|
319
|
+
renders: number;
|
|
320
|
+
rendersPerScopeCommit: number;
|
|
321
|
+
rendersFromOutside: number;
|
|
322
|
+
rendersWithoutDom: number;
|
|
323
|
+
domTextChanges: number;
|
|
324
|
+
rendersPerTextChange: number | null;
|
|
325
|
+
};
|
|
326
|
+
topRoots: RootLine[];
|
|
327
|
+
outsideRoots: RootLine[];
|
|
328
|
+
topCauses: Array<{
|
|
329
|
+
key: string;
|
|
330
|
+
events: number;
|
|
331
|
+
commits: number;
|
|
332
|
+
keys?: string;
|
|
333
|
+
}>;
|
|
334
|
+
actions: ActionLine[];
|
|
335
|
+
plugins: Record<string, {
|
|
336
|
+
version: number;
|
|
337
|
+
highlights: string[];
|
|
338
|
+
}>;
|
|
339
|
+
/** useMemo and useCallback that recompute on most renders, worst first. */
|
|
340
|
+
memos?: string[];
|
|
341
|
+
frames: {
|
|
342
|
+
longTasks: number;
|
|
343
|
+
maxLongTaskMs: number;
|
|
344
|
+
longFrames: number;
|
|
345
|
+
worstFrameMs: number;
|
|
346
|
+
};
|
|
347
|
+
overhead: RecordingV2['overhead'];
|
|
348
|
+
warnings: string[];
|
|
349
|
+
}
|
|
350
|
+
/** `full` keeps the whole chain; `short` stops at the package API the app called: `useSelector › zustand.useStore`. */
|
|
351
|
+
type HookMode = 'full' | 'short';
|
|
352
|
+
declare function hookText(hook: HookInfo | undefined, mode?: HookMode): string;
|
|
353
|
+
/** `12× state #2 SAME-CONTENT · useController › useFormState › State @ src/Field.tsx:48 const { fieldState } = …` */
|
|
354
|
+
declare function reasonLine(root: RootStat, reason: ReasonInfo | undefined, n: number, mode?: HookMode): string;
|
|
355
|
+
declare function actionText(action: ActionRecord): string;
|
|
356
|
+
declare function summarize(rec: RecordingV2 & {
|
|
357
|
+
id?: string;
|
|
358
|
+
status?: string;
|
|
359
|
+
}, top?: number, hooks?: HookMode): Summary;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Outlines of rendered components on one canvas, `Name ×N` for the render count; grey when the DOM did not change.
|
|
363
|
+
* Rects come through IntersectionObserver, so drawing forces no layout.
|
|
364
|
+
*/
|
|
365
|
+
declare class Highlighter implements HighlightSink {
|
|
366
|
+
enabled: boolean;
|
|
367
|
+
private readonly canvas;
|
|
368
|
+
private readonly ctx;
|
|
369
|
+
private pending;
|
|
370
|
+
/** Renders in a row with less than a fade between them: a steady ticker keeps counting up and turns red. */
|
|
371
|
+
private counts;
|
|
372
|
+
/** One box per element, so a component rendering again refreshes its outline instead of stacking another. */
|
|
373
|
+
private flashes;
|
|
374
|
+
private frameRequested;
|
|
375
|
+
private drawing;
|
|
376
|
+
private costMs;
|
|
377
|
+
private colours;
|
|
378
|
+
/** Components picked on the report's timeline: outlined until the pick changes, whatever the highlight toggle says. */
|
|
379
|
+
private pinned;
|
|
380
|
+
constructor(parent: ShadowRoot | Element);
|
|
381
|
+
/** Outlines these components until the next call; an empty list takes them away. */
|
|
382
|
+
pin(items: Array<{
|
|
383
|
+
fiber: Fiber;
|
|
384
|
+
label: string;
|
|
385
|
+
}>): void;
|
|
386
|
+
private redraw;
|
|
387
|
+
takeCostMs(): number;
|
|
388
|
+
reset(): void;
|
|
389
|
+
flash(pairs: Array<[Element, string, Fiber]>, withoutDom: Set<Fiber>, mounted?: Set<Fiber>): void;
|
|
390
|
+
private measure;
|
|
391
|
+
private draw;
|
|
392
|
+
/** Labels repeat from frame to frame while a box fades: measured once each. */
|
|
393
|
+
private readonly labelWidths;
|
|
394
|
+
private widthOf;
|
|
395
|
+
/** A box around everything a picked component draws, in the colour of picking, and its label above. */
|
|
396
|
+
private drawPinned;
|
|
397
|
+
/** Grey when the render changed nothing; otherwise green, amber and red by how often it came. */
|
|
398
|
+
private colourOf;
|
|
399
|
+
private clear;
|
|
400
|
+
private resize;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** One thing to do again, with enough of the element to find it on a freshly loaded page. */
|
|
404
|
+
interface ReplayStep {
|
|
405
|
+
kind: 'click' | 'typing' | 'key' | 'change' | 'submit' | 'scroll';
|
|
406
|
+
/** Milliseconds after the previous step began (the first: after the replay began). */
|
|
407
|
+
afterMs: number;
|
|
408
|
+
/** How long it took; typing spreads its characters over it. */
|
|
409
|
+
durationMs: number;
|
|
410
|
+
what: string;
|
|
411
|
+
selector?: string;
|
|
412
|
+
nth?: number;
|
|
413
|
+
key?: string;
|
|
414
|
+
chars?: number;
|
|
415
|
+
/** The typed or chosen value, only when the recording kept values. */
|
|
416
|
+
value?: string;
|
|
417
|
+
scrollTo?: number;
|
|
418
|
+
}
|
|
419
|
+
interface ReplayPlan {
|
|
420
|
+
from?: string;
|
|
421
|
+
steps: ReplayStep[];
|
|
422
|
+
/** How long to go on recording after the last step: the tail of the original, for the same background. */
|
|
423
|
+
tailMs: number;
|
|
424
|
+
/** Actions that cannot be done again, and why. */
|
|
425
|
+
skipped: string[];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
type Corner = 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
|
429
|
+
|
|
430
|
+
interface PanelOptions {
|
|
431
|
+
corner: Corner;
|
|
432
|
+
highlight: boolean;
|
|
433
|
+
shortcuts: {
|
|
434
|
+
record: string;
|
|
435
|
+
pick: string;
|
|
436
|
+
};
|
|
437
|
+
interrupted: {
|
|
438
|
+
id: string;
|
|
439
|
+
} | null;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The panel's state; the markup is a preact view of it (`components/PanelView.tsx`), redrawn from `sync()`.
|
|
443
|
+
* The view lives in the panel's own shadow root, so the app's React never sees it.
|
|
444
|
+
*/
|
|
445
|
+
declare class Panel {
|
|
446
|
+
private engine;
|
|
447
|
+
private options;
|
|
448
|
+
readonly host: HTMLDivElement;
|
|
449
|
+
private shadow;
|
|
450
|
+
private container;
|
|
451
|
+
private state;
|
|
452
|
+
private scope;
|
|
453
|
+
private before;
|
|
454
|
+
private liveTimer;
|
|
455
|
+
private picker;
|
|
456
|
+
private busy;
|
|
457
|
+
private visible;
|
|
458
|
+
/** Set by the page for as long as it has nothing to record (a front page, docs); never remembered. */
|
|
459
|
+
private suppressed;
|
|
460
|
+
private tree;
|
|
461
|
+
private result;
|
|
462
|
+
private message;
|
|
463
|
+
private highlighter;
|
|
464
|
+
/** A commit or action picked on the timeline: its outlines alone are on the page until the pick is undone. */
|
|
465
|
+
private outlining;
|
|
466
|
+
/** The pointer that just finished a drag of the dot; its click opens nothing. */
|
|
467
|
+
private dragged;
|
|
468
|
+
private readonly handlers;
|
|
469
|
+
constructor(engine: Engine, options: PanelOptions);
|
|
470
|
+
setHighlighter(highlighter: Highlighter): void;
|
|
471
|
+
/** Panel host goes on <html>, outside body: the app's root lookups and DOM observers never see it. */
|
|
472
|
+
mount(): void;
|
|
473
|
+
get shadowRoot(): ShadowRoot;
|
|
474
|
+
/** Something the view shows has changed outside the panel — a component's file came back from the dev server. */
|
|
475
|
+
redraw(): void;
|
|
476
|
+
/** Hides the panel, its outlines and its shortcuts while `on`, without touching what the person chose to see. */
|
|
477
|
+
suppress(on: boolean): void;
|
|
478
|
+
show(): void;
|
|
479
|
+
/** Renders in the area are outlined all the time the panel is shown, not only while recording. */
|
|
480
|
+
private syncIdleHighlight;
|
|
481
|
+
private initialVisibility;
|
|
482
|
+
/** What the view can ask for, in one place; each line is a method below. */
|
|
483
|
+
private buildHandlers;
|
|
484
|
+
/** The area, the note and the watched components are put aside, so the reloaded page can pick the recording up. */
|
|
485
|
+
private reloadIntoRecording;
|
|
486
|
+
private copyScope;
|
|
487
|
+
/**
|
|
488
|
+
* The clipboard is written in the same task as the click, never after an await: browsers may refuse a copy
|
|
489
|
+
* that leaves the user's gesture.
|
|
490
|
+
*/
|
|
491
|
+
private copyArea;
|
|
492
|
+
/**
|
|
493
|
+
* The area picked before the reload, found again by its component path once the app renders it.
|
|
494
|
+
* A page without that component keeps the whole app as the area, silently.
|
|
495
|
+
*/
|
|
496
|
+
private restoreScope;
|
|
497
|
+
/**
|
|
498
|
+
* A recording from the page load finds its area itself, before the panel's own search does and then stops; the
|
|
499
|
+
* panel shows that area, or the pill says Pick while the recording is of one component only.
|
|
500
|
+
*/
|
|
501
|
+
private adoptRecordingScope;
|
|
502
|
+
/** Hovering the area's name outlines it on the page, unless the picker is already drawing something. */
|
|
503
|
+
private outlineScope;
|
|
504
|
+
private setHighlight;
|
|
505
|
+
private setNote;
|
|
506
|
+
private sync;
|
|
507
|
+
private viewProps;
|
|
508
|
+
private toggleWatch;
|
|
509
|
+
private start;
|
|
510
|
+
/** The roots of a picked commit or action, on the page as it is now: every instance of each, labelled with its hits. */
|
|
511
|
+
private outlineRoots;
|
|
512
|
+
/** Reloads and does the actions of the report again, recording: the same scenario after a change of the code. */
|
|
513
|
+
private repeat;
|
|
514
|
+
/** Where a replay is: the header says it instead of the running numbers. */
|
|
515
|
+
private replaying;
|
|
516
|
+
replayProgress(at: number | null, of?: number): void;
|
|
517
|
+
/** A replay has done its steps: stop as Stop does, and say why if it could not finish them. */
|
|
518
|
+
finish(failure: string | null): Promise<void>;
|
|
519
|
+
private showResult;
|
|
520
|
+
/** The component path of the area the report was recorded in. */
|
|
521
|
+
private resultArea;
|
|
522
|
+
/** The last recording against the one before it in this tab, when they are of the same page and area. */
|
|
523
|
+
private compared;
|
|
524
|
+
private stop;
|
|
525
|
+
private togglePicker;
|
|
526
|
+
/** Reopens the tree on the current area; without one, picks from scratch. */
|
|
527
|
+
private editScope;
|
|
528
|
+
/** The area as it was before the tree opened: Esc puts it back, whatever was tried in between. */
|
|
529
|
+
private rememberScope;
|
|
530
|
+
/** The committed fiber of the area; after a remount the area is found again by its component path. */
|
|
531
|
+
private scopeTarget;
|
|
532
|
+
/** What was just copied, by the key of the button that copied it: that button shows a tick for a moment. */
|
|
533
|
+
private copied;
|
|
534
|
+
private copiedTimer;
|
|
535
|
+
private copy;
|
|
536
|
+
private onPicked;
|
|
537
|
+
private setScope;
|
|
538
|
+
private showTree;
|
|
539
|
+
private say;
|
|
540
|
+
private setCollapsed;
|
|
541
|
+
private onShortcut;
|
|
542
|
+
/**
|
|
543
|
+
* Dragging moves the panel by inline styles, which no render touches; on release the dock becomes state.
|
|
544
|
+
* The dot is a button, so the drag takes over only once the pointer has really moved.
|
|
545
|
+
*/
|
|
546
|
+
private onDragStart;
|
|
547
|
+
/** A drag that started on the dot ends in a click; that one click must not open the panel. */
|
|
548
|
+
private openFromDot;
|
|
549
|
+
private persist;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
declare const format: {
|
|
553
|
+
summarize: typeof summarize;
|
|
554
|
+
reasonLine: typeof reasonLine;
|
|
555
|
+
hookText: typeof hookText;
|
|
556
|
+
actionText: typeof actionText;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
interface ClientConfig extends BootConfig {
|
|
560
|
+
panel: false | {
|
|
561
|
+
corner: Corner;
|
|
562
|
+
highlight: boolean;
|
|
563
|
+
shortcuts: {
|
|
564
|
+
record: string;
|
|
565
|
+
pick: string;
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
interface HotContext {
|
|
570
|
+
on(event: string, cb: (payload: any) => void): void;
|
|
571
|
+
}
|
|
572
|
+
interface RecorderGlobal {
|
|
573
|
+
version: string;
|
|
574
|
+
engine: Engine;
|
|
575
|
+
panel: Panel | null;
|
|
576
|
+
/** The same formatting as the panel and the MCP server, for scripts that print their own answer. */
|
|
577
|
+
format: typeof format;
|
|
578
|
+
/** Does the steps of a recording again while one is being recorded; `record_page` calls it for a replay. */
|
|
579
|
+
replay(plan: ReplayPlan): Promise<void>;
|
|
580
|
+
}
|
|
581
|
+
declare global {
|
|
582
|
+
interface Window {
|
|
583
|
+
[GLOBAL_KEY]?: RecorderGlobal;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/** Called by the virtual entry the Vite plugin injects at the top of <head>: runs before the app's modules. */
|
|
587
|
+
declare function boot(config: ClientConfig, plugins: PluginEntry[], hot?: HotContext): RecorderGlobal;
|
|
588
|
+
|
|
589
|
+
export { type BootConfig, type ClientConfig, type RecorderGlobal, boot };
|