gesso-devtools 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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/index.d.ts +1076 -0
- package/dist/index.js +3014 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import { ActionCause, ActionEntry, ChannelErrorEntry, ChannelPort, CommandEntry, DevtoolsEvent, DevtoolsRequest, FrameEntry, FrameMetrics, PatchEntry, RuntimeErrorSource, ShellToRuntimeMessage, UiFramePhase, UiNodeReport, UiTreeNode, WorkerHandle } from "gesso-framework";
|
|
2
|
+
//#region src/sourceMap.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Enough of Source Map v3 to turn a compiled stack frame back into the
|
|
5
|
+
* line somebody wrote.
|
|
6
|
+
*
|
|
7
|
+
* Written here rather than taken from npm for the reason the rest of
|
|
8
|
+
* the repository's tooling gives (`scripts/gen-layout-fixtures.ts`,
|
|
9
|
+
* `scripts/check-webgpu-parity.ts`): the whole of what this package
|
|
10
|
+
* needs is a VLQ decoder and a binary search, and a dependency that
|
|
11
|
+
* ships a `SourceMapConsumer` with a WASM payload is a heavier thing to
|
|
12
|
+
* put in front of a developer trying to read an error than the error
|
|
13
|
+
* was.
|
|
14
|
+
*
|
|
15
|
+
* Two deliberate omissions. Index maps (`sections`) are not read — no
|
|
16
|
+
* bundler this project builds with emits one. Names are not read
|
|
17
|
+
* either: a stack frame already carries the function name the engine
|
|
18
|
+
* knew, and the mapped name is only occasionally better.
|
|
19
|
+
*/
|
|
20
|
+
/** A source map as a bundler writes it. */
|
|
21
|
+
interface SourceMapV3 {
|
|
22
|
+
version: number;
|
|
23
|
+
file?: string;
|
|
24
|
+
sourceRoot?: string;
|
|
25
|
+
sources: (string | null)[];
|
|
26
|
+
sourcesContent?: (string | null)[];
|
|
27
|
+
names?: string[];
|
|
28
|
+
mappings: string;
|
|
29
|
+
}
|
|
30
|
+
/** Where a generated position came from, with 1-based line and column. */
|
|
31
|
+
interface OriginalPosition {
|
|
32
|
+
/** The source as the map names it, with `sourceRoot` already applied. */
|
|
33
|
+
source: string;
|
|
34
|
+
line: number;
|
|
35
|
+
column: number;
|
|
36
|
+
/**
|
|
37
|
+
* The original text of the source, when the map inlined it.
|
|
38
|
+
*
|
|
39
|
+
* Carried on the position rather than fetched from the consumer
|
|
40
|
+
* afterwards because a chained lookup ends in a consumer the caller
|
|
41
|
+
* never sees: the map that knows the text is the last one in the
|
|
42
|
+
* chain, not the one the caller started from. See
|
|
43
|
+
* `SourceMapStore.originalFor`.
|
|
44
|
+
*/
|
|
45
|
+
content?: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One decoded mapping segment.
|
|
49
|
+
*
|
|
50
|
+
* Held as a flat tuple rather than an object because a map for a
|
|
51
|
+
* medium application has hundreds of thousands of them, and they exist
|
|
52
|
+
* only to be searched.
|
|
53
|
+
*/
|
|
54
|
+
type Segment = [generatedColumn: number, sourceIndex: number, sourceLine: number, sourceColumn: number];
|
|
55
|
+
/**
|
|
56
|
+
* Decodes the `mappings` string into one array of segments per
|
|
57
|
+
* generated line.
|
|
58
|
+
*
|
|
59
|
+
* Segments carrying only a generated column — a run of output with no
|
|
60
|
+
* original position, which is what a bundler emits for code it
|
|
61
|
+
* synthesised — are dropped rather than kept with nulls. A lookup that
|
|
62
|
+
* lands in one should fall back to the nearest earlier real mapping,
|
|
63
|
+
* and dropping them is how that happens without a second case.
|
|
64
|
+
*/
|
|
65
|
+
declare function decodeMappings(mappings: string): Segment[][];
|
|
66
|
+
/**
|
|
67
|
+
* A decoded map, answering "which line of which source is this?".
|
|
68
|
+
*
|
|
69
|
+
* Decoding is done once in the constructor because a page that shows
|
|
70
|
+
* one error usually shows several from the same file, and each of them
|
|
71
|
+
* asks about a handful of positions.
|
|
72
|
+
*/
|
|
73
|
+
declare class SourceMapConsumer {
|
|
74
|
+
private readonly lines;
|
|
75
|
+
private readonly sources;
|
|
76
|
+
private readonly contents;
|
|
77
|
+
constructor(map: SourceMapV3);
|
|
78
|
+
/**
|
|
79
|
+
* Maps a generated position — 1-based line and column, as every
|
|
80
|
+
* engine writes them in a stack — back to an original one.
|
|
81
|
+
*
|
|
82
|
+
* Returns the last mapping at or before the column, which is the
|
|
83
|
+
* definition of a source map's coverage: a mapping holds until the
|
|
84
|
+
* next one starts. Null when the line has no mappings at all.
|
|
85
|
+
*/
|
|
86
|
+
lookup(line: number, column: number): OriginalPosition | null;
|
|
87
|
+
/** The original text of a source, when the map inlined it. */
|
|
88
|
+
contentFor(source: string): string | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The `sourceMappingURL` a script declares, or null.
|
|
92
|
+
*
|
|
93
|
+
* The last one wins, and the search is a `lastIndexOf` over the whole
|
|
94
|
+
* text rather than a regex over the tail: an inlined map is a single
|
|
95
|
+
* comment megabytes long, so "near the end" is not where its opening
|
|
96
|
+
* is.
|
|
97
|
+
*/
|
|
98
|
+
declare function parseSourceMappingUrl(script: string): string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Fetches and caches the map for each script a stack mentions.
|
|
101
|
+
*
|
|
102
|
+
* Every method resolves rather than rejects: this runs while
|
|
103
|
+
* something has already gone wrong, and an overlay that throws while
|
|
104
|
+
* explaining a throw is worse than an overlay showing a raw stack.
|
|
105
|
+
* A script with no map, a map that 404s and a map that is not JSON all
|
|
106
|
+
* come back as null, and the caller shows what the engine gave it.
|
|
107
|
+
*/
|
|
108
|
+
declare class SourceMapStore {
|
|
109
|
+
private readonly cache;
|
|
110
|
+
private readonly load;
|
|
111
|
+
constructor(load?: (url: string) => Promise<string>);
|
|
112
|
+
/** The consumer for a script URL, fetched at most once per store. */
|
|
113
|
+
consumerFor(scriptUrl: string): Promise<SourceMapConsumer | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Where a position in a script was written, following the chain of
|
|
116
|
+
* maps as far as it goes.
|
|
117
|
+
*
|
|
118
|
+
* One lookup is not enough, and the reason took a browser to find. A
|
|
119
|
+
* frame inside `gesso-framework` names Vite's optimised dependency
|
|
120
|
+
* bundle; that bundle's map points at the package's own
|
|
121
|
+
* `dist/index.js`, because the optimiser does not chain to the map
|
|
122
|
+
* the package ships; and it is the package's map that knows about
|
|
123
|
+
* `src/app/worker/RenderWorkerApp.ts`. Stopping after one step gave
|
|
124
|
+
* a frame in a bundled file with a five-figure line number, which is
|
|
125
|
+
* exactly the thing shipping the maps was meant to prevent.
|
|
126
|
+
*
|
|
127
|
+
* So each answer is resolved against the map that gave it and asked
|
|
128
|
+
* again, until a source has no map of its own — which is the source
|
|
129
|
+
* somebody wrote. `depth` is a guard against a map that names
|
|
130
|
+
* itself; four is more levels than any real toolchain stacks.
|
|
131
|
+
*/
|
|
132
|
+
originalFor(scriptUrl: string, line: number, column: number, depth?: number): Promise<OriginalPosition | null>;
|
|
133
|
+
private resolve;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/ErrorOverlay.d.ts
|
|
137
|
+
/**
|
|
138
|
+
* Where an error came from.
|
|
139
|
+
*
|
|
140
|
+
* The render worker's four sources, plus `window` for the thread the
|
|
141
|
+
* overlay itself runs on — the single-thread configuration, and
|
|
142
|
+
* anything the shell does around the app.
|
|
143
|
+
*/
|
|
144
|
+
type ErrorOrigin = RuntimeErrorSource | 'window';
|
|
145
|
+
interface ErrorOverlayOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Also write every error to the console (default true).
|
|
148
|
+
*
|
|
149
|
+
* On by default because the overlay is a second place to see an
|
|
150
|
+
* error, not a replacement for the first: the console keeps the live
|
|
151
|
+
* object, its `cause`, and the "expand to see the real frames" that
|
|
152
|
+
* no snapshot of a stack can offer.
|
|
153
|
+
*/
|
|
154
|
+
echoToConsole?: boolean;
|
|
155
|
+
/** Where source maps are fetched from. Injected by the specs. */
|
|
156
|
+
sourceMaps?: SourceMapStore;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The error overlay: what a worker threw, drawn over the app that was
|
|
160
|
+
* running when it threw.
|
|
161
|
+
*
|
|
162
|
+
* A canvas UI has no equivalent of a page that stops rendering. When a
|
|
163
|
+
* render worker throws, the last good frame stays on screen — pixels
|
|
164
|
+
* that look exactly like a working application — and the only witness
|
|
165
|
+
* is a console message on a thread the developer has to know to
|
|
166
|
+
* select. `WorkerApp` already forwards those errors to `onError`;
|
|
167
|
+
* this is that callback, with the stack put back through the source
|
|
168
|
+
* maps and the offending line quoted.
|
|
169
|
+
*
|
|
170
|
+
* It is a development tool and it makes a development tool's trade:
|
|
171
|
+
* it fetches source maps, keeps every distinct error of the session,
|
|
172
|
+
* and covers the application it is reporting on.
|
|
173
|
+
*/
|
|
174
|
+
declare class ErrorOverlay {
|
|
175
|
+
private readonly host;
|
|
176
|
+
/**
|
|
177
|
+
* The document the host belongs to, rather than the global one.
|
|
178
|
+
*
|
|
179
|
+
* The same rule `SemanticsMirror` follows: everything this class
|
|
180
|
+
* builds hangs off the element it was handed, so it works in a
|
|
181
|
+
* second window and can be driven by a fake document in a spec —
|
|
182
|
+
* which is the only way to test it in a suite that runs in Node.
|
|
183
|
+
*/
|
|
184
|
+
private readonly doc;
|
|
185
|
+
private readonly view;
|
|
186
|
+
private readonly container;
|
|
187
|
+
private readonly root;
|
|
188
|
+
private readonly panel;
|
|
189
|
+
private readonly maps;
|
|
190
|
+
private readonly echo;
|
|
191
|
+
private readonly entries;
|
|
192
|
+
private readonly restoreHostPosition;
|
|
193
|
+
private shown;
|
|
194
|
+
private disposed;
|
|
195
|
+
private detachWindow;
|
|
196
|
+
constructor(host: HTMLElement, options?: ErrorOverlayOptions);
|
|
197
|
+
/** How many distinct errors have been reported. */
|
|
198
|
+
get count(): number;
|
|
199
|
+
/** True while the overlay is covering the app. */
|
|
200
|
+
get visible(): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Reports an error, in the shape `WorkerApp`'s `onError` hands it
|
|
203
|
+
* over, so the whole wiring is `onError: overlay.report`.
|
|
204
|
+
*
|
|
205
|
+
* Bound as a field rather than a method for exactly that: it is
|
|
206
|
+
* passed as a callback far more often than it is called.
|
|
207
|
+
*/
|
|
208
|
+
report: (message: string, stack?: string, origin?: ErrorOrigin) => void;
|
|
209
|
+
/** Reports a thrown value, which is usually but not always an Error. */
|
|
210
|
+
reportError: (error: unknown, origin?: ErrorOrigin) => void;
|
|
211
|
+
/**
|
|
212
|
+
* Catches what this thread throws, too.
|
|
213
|
+
*
|
|
214
|
+
* The single-thread configuration runs components here, and even in
|
|
215
|
+
* the worker configuration the shell around the app can throw. Returns
|
|
216
|
+
* a function that stops listening; `dispose` calls it as well.
|
|
217
|
+
*/
|
|
218
|
+
captureWindowErrors(target?: Window | null): () => void;
|
|
219
|
+
/** Hides the overlay. The errors are kept and can be shown again. */
|
|
220
|
+
hide(): void;
|
|
221
|
+
/** Hides the overlay and forgets every error it was holding. */
|
|
222
|
+
clear(): void;
|
|
223
|
+
dispose(): void;
|
|
224
|
+
private show;
|
|
225
|
+
private handleKeyDown;
|
|
226
|
+
/**
|
|
227
|
+
* Maps the entry's stack and quotes the line it points at.
|
|
228
|
+
*
|
|
229
|
+
* Deliberately after the first paint: the raw stack is on screen
|
|
230
|
+
* within a frame of the error, and the mapped one replaces it when
|
|
231
|
+
* the network answers. A developer looking at an error should never
|
|
232
|
+
* be waiting on a fetch to see it.
|
|
233
|
+
*/
|
|
234
|
+
private resolveSources;
|
|
235
|
+
/** `element`, bound to this overlay's document. */
|
|
236
|
+
private el;
|
|
237
|
+
/** A header button, bound to this overlay's document. */
|
|
238
|
+
private button;
|
|
239
|
+
private render;
|
|
240
|
+
private step;
|
|
241
|
+
private copy;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Mounts an error overlay over an application's host element.
|
|
245
|
+
*
|
|
246
|
+
* const overlay = mountErrorOverlay(host);
|
|
247
|
+
* createApp({ renderWorker, onError: overlay.report });
|
|
248
|
+
* overlay.captureWindowErrors();
|
|
249
|
+
*/
|
|
250
|
+
declare function mountErrorOverlay(host: HTMLElement, options?: ErrorOverlayOptions): ErrorOverlay;
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/ActionLog.d.ts
|
|
253
|
+
/**
|
|
254
|
+
* The store action log: every command a view sent
|
|
255
|
+
* across the barrier, every patch that came back, on one timeline,
|
|
256
|
+
* with the view rewindable to any point on it.
|
|
257
|
+
*
|
|
258
|
+
* The seam is the port. A channel is two message types over a
|
|
259
|
+
* `ChannelPort`, so a recorder that sits in the middle of one sees
|
|
260
|
+
* both directions at their only crossing, needs nothing from the
|
|
261
|
+
* framework beyond the two type guards the protocol already exports,
|
|
262
|
+
* and can put a patch back on the wire, which is what makes time
|
|
263
|
+
* travel a replay rather than a second write path into the replica.
|
|
264
|
+
*
|
|
265
|
+
* Nothing here touches the DOM. That is deliberate: the tap belongs
|
|
266
|
+
* wherever the ports already are, which in the worker configuration is
|
|
267
|
+
* the render worker, and a recorder that imported a document could not
|
|
268
|
+
* go there. `ActionLogPanel` is the half that draws.
|
|
269
|
+
*
|
|
270
|
+
* **What time travel does.** It rewrites what the *view* holds. The
|
|
271
|
+
* authoritative state lives on the other thread and is not rewound,
|
|
272
|
+
* cannot be rewound from here, and does not know this happened. While
|
|
273
|
+
* the log is pinned to a step, patches still arriving are recorded and
|
|
274
|
+
* held rather than delivered, so the view stays where it was put;
|
|
275
|
+
* going live delivers the state the application actually reached. A
|
|
276
|
+
* command sent from the view while pinned is forwarded like any other,
|
|
277
|
+
* because the application is still running and pretending otherwise
|
|
278
|
+
* would be a lie about a button that visibly did something.
|
|
279
|
+
*/
|
|
280
|
+
interface ActionLog {
|
|
281
|
+
/**
|
|
282
|
+
* Wraps a worker handle so every channel opened on it is recorded.
|
|
283
|
+
*
|
|
284
|
+
* `tokens` is read for one thing: the value both ends of a channel
|
|
285
|
+
* start from. The first patch a provider sends is a diff against the
|
|
286
|
+
* token's initial, so a recorder that started from nothing would
|
|
287
|
+
* reconstruct an early step out of a patch whose base it never had.
|
|
288
|
+
*/
|
|
289
|
+
tap(handle: WorkerHandle, tokens: readonly ActionLogToken[]): WorkerHandle;
|
|
290
|
+
/**
|
|
291
|
+
* Wraps one port, for a channel fed from this thread.
|
|
292
|
+
*
|
|
293
|
+
* `createChannelRegistry` makes the pair for a `source` registration
|
|
294
|
+
* itself and hands out neither end, so a local channel is tapped by
|
|
295
|
+
* providing it by hand instead: call `provide(token, source, port)`
|
|
296
|
+
* on one end of a `MessageChannel` and register the other through
|
|
297
|
+
* this.
|
|
298
|
+
*/
|
|
299
|
+
tapPort(port: ChannelPort, token: ActionLogToken): ChannelPort;
|
|
300
|
+
/**
|
|
301
|
+
* Names what is being answered for as long as the returned function
|
|
302
|
+
* has not been called, so everything recorded meanwhile carries the
|
|
303
|
+
* same `ActionCause`.
|
|
304
|
+
*
|
|
305
|
+
* Called around the dispatch of one input, which is the only moment
|
|
306
|
+
* where a cause is known rather than guessed: the command a click's
|
|
307
|
+
* listener sends is sent synchronously inside it. Nothing is
|
|
308
|
+
* allocated for an input that records nothing, so wrapping every
|
|
309
|
+
* pointer move costs a function call and a null check.
|
|
310
|
+
*
|
|
311
|
+
* The patches that answer such a command are given the same cause,
|
|
312
|
+
* which is an inference and the record says so: the barrier carries
|
|
313
|
+
* no request id, so the recorder ties the next patch batch on that
|
|
314
|
+
* channel to the command that preceded it, until a frame has drawn
|
|
315
|
+
* one or another command replaces it.
|
|
316
|
+
*/
|
|
317
|
+
cause(label: string): () => void;
|
|
318
|
+
/**
|
|
319
|
+
* Records the frame that drew whatever has been recorded since the
|
|
320
|
+
* last one, closing the chain from click to command to patches to
|
|
321
|
+
* pixels.
|
|
322
|
+
*
|
|
323
|
+
* Nothing is recorded for a frame with nothing to close, so an
|
|
324
|
+
* application drawing sixty frames a second while its channels are
|
|
325
|
+
* quiet adds nothing to the timeline.
|
|
326
|
+
*/
|
|
327
|
+
frame(id: number): void;
|
|
328
|
+
/** The timeline, oldest first. */
|
|
329
|
+
readonly entries: readonly ActionEntry[];
|
|
330
|
+
/** The channels this log is tapping, by name, in the order tapped. */
|
|
331
|
+
readonly channels: readonly string[];
|
|
332
|
+
/**
|
|
333
|
+
* The entry the view is pinned to, or null when the view is live.
|
|
334
|
+
*
|
|
335
|
+
* A sequence number rather than an index, because entries fall off
|
|
336
|
+
* the front of a bounded log and an index would then mean a
|
|
337
|
+
* different entry than it did a moment ago.
|
|
338
|
+
*/
|
|
339
|
+
readonly pinnedTo: number | null;
|
|
340
|
+
/**
|
|
341
|
+
* Rewinds every tapped channel's view to the state it held once
|
|
342
|
+
* `seq` had been applied, or catches it up to the application when
|
|
343
|
+
* `seq` is null.
|
|
344
|
+
*
|
|
345
|
+
* Each key is posted to the replica as one `{ op: 'set', path: [] }`,
|
|
346
|
+
* which is the shape a reattaching client is already answered with,
|
|
347
|
+
* so nothing downstream can tell a replay from a resync.
|
|
348
|
+
*/
|
|
349
|
+
jumpTo(seq: number | null): void;
|
|
350
|
+
/** Empties the timeline and goes live. Tapped channels stay tapped. */
|
|
351
|
+
clear(): void;
|
|
352
|
+
/** Called whenever the timeline or the pinned step changed. */
|
|
353
|
+
subscribe(listener: () => void): () => void;
|
|
354
|
+
/**
|
|
355
|
+
* Stops recording, leaving every tapped channel working.
|
|
356
|
+
*
|
|
357
|
+
* The relay is not removed. A port handed to a replica cannot be
|
|
358
|
+
* taken back out of the middle of it, so what this does is stop
|
|
359
|
+
* listening: messages pass through, nothing is written down, and a
|
|
360
|
+
* held patch could not be stranded by it.
|
|
361
|
+
*/
|
|
362
|
+
dispose(): void;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* A channel's identity, structurally.
|
|
366
|
+
*
|
|
367
|
+
* The same erasure `ChannelRegistration` uses, and for the same
|
|
368
|
+
* reason: a list of channels has no single generic instantiation, and
|
|
369
|
+
* the only two things a recorder wants from a token are its name and
|
|
370
|
+
* the value both ends start from.
|
|
371
|
+
*/
|
|
372
|
+
interface ActionLogToken {
|
|
373
|
+
readonly name: string;
|
|
374
|
+
readonly initial: object;
|
|
375
|
+
}
|
|
376
|
+
interface ActionLogOptions {
|
|
377
|
+
/**
|
|
378
|
+
* Most entries kept. Default 200.
|
|
379
|
+
*
|
|
380
|
+
* A dropped entry is not forgotten, only unaddressable: its patches
|
|
381
|
+
* are folded into the base state each channel is reconstructed from,
|
|
382
|
+
* so a jump to a step that survived is still exact.
|
|
383
|
+
*/
|
|
384
|
+
readonly limit?: number;
|
|
385
|
+
}
|
|
386
|
+
declare function createActionLog(options?: ActionLogOptions): ActionLog;
|
|
387
|
+
/**
|
|
388
|
+
* Sends each new entry as it is recorded, and returns a function that
|
|
389
|
+
* stops.
|
|
390
|
+
*
|
|
391
|
+
* `subscribe` says only that something changed, so the sequence number
|
|
392
|
+
* is what tells a new entry from the ones already sent; a `clear`
|
|
393
|
+
* resets it, and a bounded log retiring old entries does not. Both
|
|
394
|
+
* routes to a panel need exactly this: the hook, for a log in the
|
|
395
|
+
* page, and the render worker tap, for one on the other side of a
|
|
396
|
+
* thread.
|
|
397
|
+
*/
|
|
398
|
+
declare function forwardNewEntries(log: ActionLog, send: (entry: ActionEntry) => void): () => void;
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/ActionLogPanel.d.ts
|
|
401
|
+
/**
|
|
402
|
+
* The action log's panel: the timeline, and a click on any step to put
|
|
403
|
+
* the view back where it was at that step.
|
|
404
|
+
*
|
|
405
|
+
* DOM in a shadow root over the canvas, for the reasons the error
|
|
406
|
+
* overlay and the node inspector both give: the application owns the
|
|
407
|
+
* canvas, and a panel drawn into the scene would be part of the scene
|
|
408
|
+
* it is describing.
|
|
409
|
+
*
|
|
410
|
+
* It differs from the node inspector in one way that matters. The
|
|
411
|
+
* inspector takes no pointer events because the person is hovering the
|
|
412
|
+
* thing it describes; this panel is operated, so it takes them over
|
|
413
|
+
* itself and nowhere else. That is also why it is behind a toggle
|
|
414
|
+
* rather than always up: while it is showing, the canvas underneath it
|
|
415
|
+
* is not reachable.
|
|
416
|
+
*/
|
|
417
|
+
interface ActionLogPanel {
|
|
418
|
+
/** Shows or hides the panel. A hidden panel stops rendering. */
|
|
419
|
+
setVisible(visible: boolean): void;
|
|
420
|
+
readonly visible: boolean;
|
|
421
|
+
dispose(): void;
|
|
422
|
+
}
|
|
423
|
+
interface ActionLogPanelOptions {
|
|
424
|
+
/** Which corner it floats in. Default `'bottom-right'`. */
|
|
425
|
+
readonly corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
426
|
+
}
|
|
427
|
+
declare function mountActionLogPanel(host: HTMLElement, log: ActionLog, options?: ActionLogPanelOptions): ActionLogPanel;
|
|
428
|
+
/**
|
|
429
|
+
* The direction, as one glyph: up for what the view sent, down for
|
|
430
|
+
* what came back, a square for the frame that drew it.
|
|
431
|
+
*/
|
|
432
|
+
declare function actionGlyph(entry: ActionEntry): string;
|
|
433
|
+
/** One entry as a line: the command and its payload, the patched keys, the frame, or the error. */
|
|
434
|
+
declare function describeActionEntry(entry: ActionEntry): string;
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/RenderWorkerTap.d.ts
|
|
437
|
+
/**
|
|
438
|
+
* The action log in the worker configuration, and the click at the
|
|
439
|
+
* head of every chain it records.
|
|
440
|
+
*
|
|
441
|
+
* The action log recorded the gap and the reason for it. A channel's
|
|
442
|
+
* ports are made where the replicas are, which in the worker
|
|
443
|
+
* configuration is the render worker; the shell holds neither end and
|
|
444
|
+
* never sees a patch, deliberately. So the recorder was written to run
|
|
445
|
+
* in a worker — it imports no DOM and its entries are plain data — and
|
|
446
|
+
* then nothing ran it there, because the two ways to wire it up were
|
|
447
|
+
* "one line in the render worker's entry" and "route every patch
|
|
448
|
+
* through the main thread", and the second would falsify the very
|
|
449
|
+
* thing the route exists to show.
|
|
450
|
+
*
|
|
451
|
+
* This is that one line, made general. It stands on the wire the way
|
|
452
|
+
* the recorder itself does, which is the rule `0047` set: **the tap
|
|
453
|
+
* goes where the messages already are, never somewhere new.** In a
|
|
454
|
+
* render worker three kinds of message go past one object, the
|
|
455
|
+
* worker's global:
|
|
456
|
+
*
|
|
457
|
+
* - the shell's port to the application worker, which arrives with
|
|
458
|
+
* `init` and is what a tapped channel has to be opened over;
|
|
459
|
+
* - every input the shell forwards, which is where a cause begins;
|
|
460
|
+
* - every frame the runtime reports, which is where one ends.
|
|
461
|
+
*
|
|
462
|
+
* Nothing here reaches into the runtime, and nothing in the framework
|
|
463
|
+
* knows it exists. Used after `renderRoot`, whose constructor installs
|
|
464
|
+
* the handler this wraps:
|
|
465
|
+
*
|
|
466
|
+
* const app = renderRoot(AppRoot).useService(Counter);
|
|
467
|
+
* const actions = createActionLog();
|
|
468
|
+
* const tap = tapRenderWorker(actions);
|
|
469
|
+
* const data = tap.applicationWorker([Catalog, Cart]);
|
|
470
|
+
* app.useChannel(Catalog, { worker: data }).useChannel(Cart, { worker: data });
|
|
471
|
+
*
|
|
472
|
+
* Guard it with `import.meta.env.DEV` or the equivalent: a tap in a
|
|
473
|
+
* production bundle is a recorder holding patch batches for a session
|
|
474
|
+
* nobody is watching.
|
|
475
|
+
*/
|
|
476
|
+
/** The worker global, as far as the tap is concerned. */
|
|
477
|
+
interface RenderWorkerHost {
|
|
478
|
+
onmessage: ((event: {
|
|
479
|
+
data: unknown;
|
|
480
|
+
}) => void) | null;
|
|
481
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
482
|
+
}
|
|
483
|
+
interface RenderWorkerTapOptions {
|
|
484
|
+
/** Where to stand. Default: the worker's own global. */
|
|
485
|
+
readonly host?: RenderWorkerHost;
|
|
486
|
+
/**
|
|
487
|
+
* Whether entries are posted to the shell as devtools events, so a
|
|
488
|
+
* panel outside the page shows them. Default true.
|
|
489
|
+
*
|
|
490
|
+
* The route a log in the page does not need: there the devtools hook
|
|
491
|
+
* has the log itself to read.
|
|
492
|
+
*/
|
|
493
|
+
readonly forward?: boolean;
|
|
494
|
+
}
|
|
495
|
+
interface RenderWorkerTap {
|
|
496
|
+
/**
|
|
497
|
+
* A handle on the application worker the shell supplied, with every
|
|
498
|
+
* channel opened over it recorded.
|
|
499
|
+
*
|
|
500
|
+
* Registrations run before `init` and the shell's port arrives with
|
|
501
|
+
* it, which is why this is a handle rather than a port: it is opened
|
|
502
|
+
* when the runtime starts, by which time the port is here. Passing
|
|
503
|
+
* it to `useChannel` is what replaces the `APPLICATION_WORKER`
|
|
504
|
+
* placeholder the render worker would otherwise swap in.
|
|
505
|
+
*/
|
|
506
|
+
applicationWorker(tokens: readonly ActionLogToken[]): WorkerHandle;
|
|
507
|
+
dispose(): void;
|
|
508
|
+
}
|
|
509
|
+
declare function tapRenderWorker(log: ActionLog, options?: RenderWorkerTapOptions): RenderWorkerTap;
|
|
510
|
+
/**
|
|
511
|
+
* What to call the cause an input starts, or null for a message that
|
|
512
|
+
* is not an input.
|
|
513
|
+
*
|
|
514
|
+
* Position is in the label because two presses in different places are
|
|
515
|
+
* two different causes to the person who made them, and the ids alone
|
|
516
|
+
* do not say which was which.
|
|
517
|
+
*/
|
|
518
|
+
declare function inputLabel(message: ShellToRuntimeMessage | undefined): string | null;
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/NodePicker.d.ts
|
|
521
|
+
/**
|
|
522
|
+
* Click a node on the canvas to pin it in the panel
|
|
523
|
+
* (the first of the two things the node inspector deferred).
|
|
524
|
+
*
|
|
525
|
+
* Picking was left out because the panel could not name a node back
|
|
526
|
+
* to the runtime. The devtools panel built that half: `select` and
|
|
527
|
+
* `highlight` address a node by id, and the inspector already reports
|
|
528
|
+
* the node under the pointer while it is on. What was still missing is
|
|
529
|
+
* the click, and a click is the one part of this that cannot happen in
|
|
530
|
+
* the render thread. By the time the runtime has an event, the event
|
|
531
|
+
* has been dispatched; taking it back would mean asking the
|
|
532
|
+
* application to forget a press it may already have acted on.
|
|
533
|
+
*
|
|
534
|
+
* So the pick happens where the click arrives, in the capture phase,
|
|
535
|
+
* over the element the canvas is in. That is the shell doing what the
|
|
536
|
+
* shell does — forwarding input, or in this case declining to
|
|
537
|
+
* — and it costs nothing while picking is off,
|
|
538
|
+
* because the listeners are attached only then.
|
|
539
|
+
*
|
|
540
|
+
* What it pins is whatever the inspector last reported as hovered, so
|
|
541
|
+
* a picker needs the inspector on; the panel turns it on with the same
|
|
542
|
+
* toggle.
|
|
543
|
+
*/
|
|
544
|
+
interface NodePickerOptions {
|
|
545
|
+
/**
|
|
546
|
+
* The element to take clicks over, in the capture phase. The one the
|
|
547
|
+
* application is mounted in, so the canvas is inside it.
|
|
548
|
+
*/
|
|
549
|
+
readonly host: EventTarget;
|
|
550
|
+
/** The node under the pointer, from the runtime's `hover` reports. */
|
|
551
|
+
hovered(): string | null;
|
|
552
|
+
}
|
|
553
|
+
/** What a panel drives: arm it, and hear what was picked. */
|
|
554
|
+
interface DevtoolsPicker {
|
|
555
|
+
/** Whether clicking the canvas pins a node instead of reaching the application. */
|
|
556
|
+
setEnabled(enabled: boolean): void;
|
|
557
|
+
readonly enabled: boolean;
|
|
558
|
+
/** Called with the id of the node picked. Pass null to stop listening. */
|
|
559
|
+
onPick(listener: ((id: string) => void) | null): void;
|
|
560
|
+
dispose(): void;
|
|
561
|
+
}
|
|
562
|
+
declare function createNodePicker(options: NodePickerOptions): DevtoolsPicker;
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/NodeReportView.d.ts
|
|
565
|
+
interface NodeReportViewOptions {
|
|
566
|
+
/**
|
|
567
|
+
* Makes the props editable, calling this with the new value when one
|
|
568
|
+
* is committed; `null` means "remove it", which puts an inherited
|
|
569
|
+
* value back.
|
|
570
|
+
*
|
|
571
|
+
* Absent for a read-only view. The corner inspector passes nothing,
|
|
572
|
+
* because it sets `pointer-events: none` and could not be typed into
|
|
573
|
+
* anyway.
|
|
574
|
+
*/
|
|
575
|
+
onEditProp?(name: string, value: unknown): void;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* A `UiNodeReport` as DOM: the node inspector's body, shared with the
|
|
579
|
+
* devtools panel so a node read in a corner of the canvas and a node
|
|
580
|
+
* picked from a tree are described in the same words.
|
|
581
|
+
*/
|
|
582
|
+
declare function renderNodeReport(doc: Document, report: UiNodeReport, options?: NodeReportViewOptions): HTMLElement[];
|
|
583
|
+
/**
|
|
584
|
+
* The rules the report's elements use, for any stylesheet that shows one.
|
|
585
|
+
*
|
|
586
|
+
* Colours are the devtools panel's custom properties with the dark
|
|
587
|
+
* palette as fallback, so the corner inspector, which defines none of
|
|
588
|
+
* them, is unchanged, and the panel recolours the same report by
|
|
589
|
+
* defining them.
|
|
590
|
+
*/
|
|
591
|
+
declare const NODE_REPORT_STYLES = "\nh1 { margin: 0 0 6px; font-size: 12px; color: var(--gd-accent, #79c0ff); overflow-wrap: anywhere; }\nh2 {\n margin: 10px 0 4px;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--gd-muted, #8b949e);\n}\np { margin: 0 0 2px; }\n.label { color: var(--gd-muted, #8b949e); }\n.rows { display: grid; grid-template-columns: auto 1fr; gap: 0 8px; margin: 0; }\ndt { color: var(--gd-muted, #8b949e); overflow-wrap: anywhere; }\ndt.modifier { color: var(--gd-purple, #d2a8ff); }\ndt.binding { color: var(--gd-green, #7ee787); }\ndt.provided { color: var(--gd-orange, #ffa657); }\ndd { margin: 0; overflow-wrap: anywhere; }\ndd.editable { cursor: pointer; border-radius: 3px; }\ndd.editable:hover { background: var(--gd-bg-hover, #21262d); }\n.edit {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--gd-accent, #79c0ff);\n border-radius: 3px;\n padding: 0 3px;\n background: var(--gd-bg, #0d1117);\n color: var(--gd-text, #e6edf3);\n font: inherit;\n}\n.note { color: var(--gd-faint, #6e7681); }\n.stream { display: block; color: var(--gd-green, #7ee787); }\n.explanation { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--gd-text-strong, #c9d1d9); }\n";
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/PanelProtocol.d.ts
|
|
594
|
+
/**
|
|
595
|
+
* What a devtools panel and the page it inspects say to each other
|
|
596
|
+
*.
|
|
597
|
+
*
|
|
598
|
+
* The framework's `DevtoolsRequest` and `DevtoolsEvent` are one
|
|
599
|
+
* application's vocabulary. A page may run several (a documentation
|
|
600
|
+
* site's examples), and a panel arrives after they started, so this
|
|
601
|
+
* layer adds the two things the framework's protocol does not have: an
|
|
602
|
+
* application id on every message, and a greeting that answers with
|
|
603
|
+
* the list.
|
|
604
|
+
*
|
|
605
|
+
* Both sides speak through a `DevtoolsPort`, which is only `post` and
|
|
606
|
+
* `onMessage`. Two ports are provided here: a pair joined in memory,
|
|
607
|
+
* for a panel mounted in the same page and for tests, and one over
|
|
608
|
+
* `window.postMessage`, which is how a browser extension's content
|
|
609
|
+
* script reaches a page. The extension's own hop, from its content
|
|
610
|
+
* script to its devtools page, is one more port of the same shape and
|
|
611
|
+
* lives with the extension.
|
|
612
|
+
*/
|
|
613
|
+
interface DevtoolsAppInfo {
|
|
614
|
+
readonly id: string;
|
|
615
|
+
readonly name: string;
|
|
616
|
+
}
|
|
617
|
+
/** Page to panel. */
|
|
618
|
+
type PageMessage =
|
|
619
|
+
/** The applications the page has connected; sent on `hello` and whenever the list changes. */
|
|
620
|
+
{
|
|
621
|
+
type: 'apps';
|
|
622
|
+
apps: readonly DevtoolsAppInfo[];
|
|
623
|
+
} | {
|
|
624
|
+
type: 'event';
|
|
625
|
+
app: string;
|
|
626
|
+
event: DevtoolsEvent;
|
|
627
|
+
} |
|
|
628
|
+
/** A store action log entry, from an `ActionLog` the application connected alongside itself. */
|
|
629
|
+
{
|
|
630
|
+
type: 'action';
|
|
631
|
+
app: string;
|
|
632
|
+
entry: ActionEntry;
|
|
633
|
+
} |
|
|
634
|
+
/**
|
|
635
|
+
* A node the person clicked on the canvas while the panel was
|
|
636
|
+
* picking. The panel selects it; the click never reaches the
|
|
637
|
+
* application.
|
|
638
|
+
*/
|
|
639
|
+
{
|
|
640
|
+
type: 'picked';
|
|
641
|
+
app: string;
|
|
642
|
+
id: string;
|
|
643
|
+
};
|
|
644
|
+
/** Panel to page. */
|
|
645
|
+
type PanelMessage =
|
|
646
|
+
/** "Is anyone there": answered with `apps`. */
|
|
647
|
+
{
|
|
648
|
+
type: 'hello';
|
|
649
|
+
} | {
|
|
650
|
+
type: 'request';
|
|
651
|
+
app: string;
|
|
652
|
+
request: DevtoolsRequest;
|
|
653
|
+
} |
|
|
654
|
+
/**
|
|
655
|
+
* Turns click-to-pick on or off.
|
|
656
|
+
*
|
|
657
|
+
* Not a `DevtoolsRequest`, because it is not a question for the
|
|
658
|
+
* runtime. Picking is a click that must be taken before the
|
|
659
|
+
* application sees it, and the only place a click can be taken is
|
|
660
|
+
* the page, where it arrives; the runtime is a thread away and would
|
|
661
|
+
* have to be asked to un-dispatch something. So the page answers
|
|
662
|
+
* this one, and the runtime is asked to `select` the node the page
|
|
663
|
+
* names, which is a question it already answers.
|
|
664
|
+
*/
|
|
665
|
+
{
|
|
666
|
+
type: 'pick';
|
|
667
|
+
app: string;
|
|
668
|
+
enabled: boolean;
|
|
669
|
+
};
|
|
670
|
+
/** One end of a conversation: what it hears and what it says. */
|
|
671
|
+
interface DevtoolsPort<In, Out> {
|
|
672
|
+
post(message: Out): void;
|
|
673
|
+
/** Returns a function that stops listening. */
|
|
674
|
+
onMessage(listener: (message: In) => void): () => void;
|
|
675
|
+
/** Drops every listener and stops posting. */
|
|
676
|
+
close(): void;
|
|
677
|
+
}
|
|
678
|
+
/** The page's end. */
|
|
679
|
+
type PagePort = DevtoolsPort<PanelMessage, PageMessage>;
|
|
680
|
+
/** The panel's end. */
|
|
681
|
+
type PanelPort = DevtoolsPort<PageMessage, PanelMessage>;
|
|
682
|
+
/**
|
|
683
|
+
* Two ports joined in memory, delivering synchronously.
|
|
684
|
+
*
|
|
685
|
+
* For a panel mounted in the page it inspects, and for specs: the
|
|
686
|
+
* hook and the panel are exercised end to end with nothing between
|
|
687
|
+
* them but a function call.
|
|
688
|
+
*/
|
|
689
|
+
declare function createDirectPorts(): {
|
|
690
|
+
page: PagePort;
|
|
691
|
+
panel: PanelPort;
|
|
692
|
+
};
|
|
693
|
+
/** What a message carries over `window.postMessage`, so both sides can ignore everything else on the window. */
|
|
694
|
+
interface Envelope<T> {
|
|
695
|
+
readonly source: typeof ENVELOPE_SOURCE;
|
|
696
|
+
/** Who it is for. A page ignores what it sent, and so does a panel. */
|
|
697
|
+
readonly to: 'page' | 'panel';
|
|
698
|
+
readonly message: T;
|
|
699
|
+
}
|
|
700
|
+
declare const ENVELOPE_SOURCE = "gesso-devtools";
|
|
701
|
+
declare function isEnvelope(value: unknown, to: 'page' | 'panel'): value is Envelope<unknown>;
|
|
702
|
+
/** The part of `Window` the transport uses, so a spec can hand it a plain object. */
|
|
703
|
+
interface WindowLike {
|
|
704
|
+
addEventListener(type: 'message', listener: (event: {
|
|
705
|
+
data: unknown;
|
|
706
|
+
source?: unknown;
|
|
707
|
+
}) => void): void;
|
|
708
|
+
removeEventListener(type: 'message', listener: (event: {
|
|
709
|
+
data: unknown;
|
|
710
|
+
source?: unknown;
|
|
711
|
+
}) => void): void;
|
|
712
|
+
postMessage(message: unknown, targetOrigin: string): void;
|
|
713
|
+
readonly location?: {
|
|
714
|
+
readonly origin: string;
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* The page's end of a conversation over its own window.
|
|
719
|
+
*
|
|
720
|
+
* A content script shares the page's window but not its JavaScript, so
|
|
721
|
+
* `window.postMessage` to the page's own origin is the one channel
|
|
722
|
+
* they have. Messages are posted to that origin rather than to `*`,
|
|
723
|
+
* and the page hears only envelopes addressed to it, so its own posts
|
|
724
|
+
* come straight back through the same listener and are dropped.
|
|
725
|
+
*/
|
|
726
|
+
declare function windowPagePort(win: WindowLike): PagePort;
|
|
727
|
+
/** The other end, for whatever sits in the page beside it (an extension's content script). */
|
|
728
|
+
declare function windowPanelPort(win: WindowLike): PanelPort;
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/DevtoolsHook.d.ts
|
|
731
|
+
/**
|
|
732
|
+
* The page's side of the devtools panel.
|
|
733
|
+
*
|
|
734
|
+
* One object per page, kept on the window under a well-known name the
|
|
735
|
+
* way React's devtools hook is, so that several bundles (a docs site's
|
|
736
|
+
* examples, an application and its dependency) connect to the same
|
|
737
|
+
* hook and a panel sees one list. Applications register with it;
|
|
738
|
+
* panels attach ports to it; the hook routes requests to the right
|
|
739
|
+
* application and fans every application's events out to every port.
|
|
740
|
+
*
|
|
741
|
+
* `connectDevtools(app)` is the one line an application adds. It also
|
|
742
|
+
* installs the `window.postMessage` transport the first time, which is
|
|
743
|
+
* how a browser extension's content script finds the page: nothing is
|
|
744
|
+
* running in a page that has not connected, and a page that has
|
|
745
|
+
* connected answers `hello`.
|
|
746
|
+
*/
|
|
747
|
+
/** What an application must offer: both `WorkerApp` and `GessoApp` do. */
|
|
748
|
+
interface DevtoolsApp {
|
|
749
|
+
devtools(request: DevtoolsRequest): void;
|
|
750
|
+
/**
|
|
751
|
+
* Taken over by the hook while connected. An application that was
|
|
752
|
+
* listening to its own devtools events has to choose: the panel or
|
|
753
|
+
* itself. In practice the events only exist for panels.
|
|
754
|
+
*/
|
|
755
|
+
onDevtools(listener: ((event: DevtoolsEvent) => void) | null): void;
|
|
756
|
+
}
|
|
757
|
+
interface ConnectDevtoolsOptions {
|
|
758
|
+
/** What the panel calls this application. Default: the document title, else `app`. */
|
|
759
|
+
readonly name?: string;
|
|
760
|
+
/**
|
|
761
|
+
* A store action log the panel should show alongside. The log stays
|
|
762
|
+
* where it is (it taps ports in the page); the panel is sent each
|
|
763
|
+
* entry as it is recorded.
|
|
764
|
+
*/
|
|
765
|
+
readonly actions?: ActionLog;
|
|
766
|
+
/**
|
|
767
|
+
* Lets the panel pick a node by clicking the canvas.
|
|
768
|
+
*
|
|
769
|
+
* The page's half of picking, because a click has to be taken before
|
|
770
|
+
* the application sees it and only the page has it in time. See
|
|
771
|
+
* `createNodePicker`.
|
|
772
|
+
*/
|
|
773
|
+
readonly picker?: DevtoolsPicker;
|
|
774
|
+
/**
|
|
775
|
+
* Where the hook lives and where the `postMessage` transport
|
|
776
|
+
* listens. Default: the global window. `null` keeps the hook off any
|
|
777
|
+
* window and installs no transport, for a panel mounted directly.
|
|
778
|
+
*/
|
|
779
|
+
readonly window?: (WindowLike & HookHost) | null;
|
|
780
|
+
}
|
|
781
|
+
interface DevtoolsHook {
|
|
782
|
+
/** The connected applications, in the order they connected. */
|
|
783
|
+
readonly apps: readonly DevtoolsAppInfo[];
|
|
784
|
+
/** Connects an application. Returns a function that disconnects it. */
|
|
785
|
+
register(app: DevtoolsApp, options?: Omit<ConnectDevtoolsOptions, 'window'>): () => void;
|
|
786
|
+
/** Attaches a panel's port. Returns a function that detaches it. */
|
|
787
|
+
attach(port: PagePort): () => void;
|
|
788
|
+
}
|
|
789
|
+
/** The property the hook is kept under. */
|
|
790
|
+
declare const HOOK_PROPERTY = "__GESSO_DEVTOOLS__";
|
|
791
|
+
/** A window, as far as the hook is concerned: somewhere to keep itself. */
|
|
792
|
+
interface HookHost {
|
|
793
|
+
[HOOK_PROPERTY]?: DevtoolsHook;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* The page's hook, created on first use.
|
|
797
|
+
*
|
|
798
|
+
* With a window, the hook is stored on it and the `postMessage`
|
|
799
|
+
* transport is attached once; without one (`null`), a fresh hook with
|
|
800
|
+
* no transport, for specs and for a panel in the same page.
|
|
801
|
+
*/
|
|
802
|
+
declare function getDevtoolsHook(win?: (WindowLike & HookHost) | null): DevtoolsHook;
|
|
803
|
+
/**
|
|
804
|
+
* Connects an application to the page's devtools hook, so a panel can
|
|
805
|
+
* find it. Returns a function that disconnects it; call it when the
|
|
806
|
+
* application is disposed.
|
|
807
|
+
*/
|
|
808
|
+
declare function connectDevtools(app: DevtoolsApp, options?: ConnectDevtoolsOptions): () => void;
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/DevtoolsPanel.d.ts
|
|
811
|
+
/**
|
|
812
|
+
* The devtools panel: the tree, a node's
|
|
813
|
+
* report, the workers' consoles, the frame profiler and the action log,
|
|
814
|
+
* in one place, docked outside the canvas.
|
|
815
|
+
*
|
|
816
|
+
* It is plain DOM against a `PanelPort`, and knows nothing about where
|
|
817
|
+
* it is mounted: a Chrome extension's devtools page, or a pane in the
|
|
818
|
+
* page itself. That is what lets the same panel be both, and what lets
|
|
819
|
+
* a spec drive it through a port joined in memory.
|
|
820
|
+
*
|
|
821
|
+
* Unlike the in-page inspector, this panel does not float over the
|
|
822
|
+
* application, so it takes pointer events and can be as tall as its
|
|
823
|
+
* host. The trade is that it cannot point at the canvas with the
|
|
824
|
+
* pointer; it points with `highlight`, and the runtime draws the box.
|
|
825
|
+
*/
|
|
826
|
+
interface DevtoolsPanel {
|
|
827
|
+
/** The application the panel is showing, or null with none connected. */
|
|
828
|
+
readonly app: DevtoolsAppInfo | null;
|
|
829
|
+
/** Shows a different connected application. */
|
|
830
|
+
show(appId: string): void;
|
|
831
|
+
/** Changes the theme; see `DevtoolsPanelOptions.theme`. */
|
|
832
|
+
setTheme(theme: DevtoolsPanelTheme): void;
|
|
833
|
+
dispose(): void;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* `light` and `dark` are the panel's two palettes; `auto` follows the
|
|
837
|
+
* viewer's `prefers-color-scheme`. A host that knows better than the
|
|
838
|
+
* media query says so: Chrome's devtools has its own theme setting,
|
|
839
|
+
* which the extension reads and passes here, and the playground is
|
|
840
|
+
* dark whatever the system prefers.
|
|
841
|
+
*/
|
|
842
|
+
type DevtoolsPanelTheme = 'light' | 'dark' | 'auto';
|
|
843
|
+
interface DevtoolsPanelOptions {
|
|
844
|
+
/** Which palette. Default `auto`. */
|
|
845
|
+
readonly theme?: DevtoolsPanelTheme;
|
|
846
|
+
/** How many levels of the tree open on the first snapshot. Default 4. */
|
|
847
|
+
readonly openDepth?: number;
|
|
848
|
+
/** How many console entries are kept. Default 500. */
|
|
849
|
+
readonly consoleLimit?: number;
|
|
850
|
+
/** How many action entries are kept. Default 300. */
|
|
851
|
+
readonly actionLimit?: number;
|
|
852
|
+
}
|
|
853
|
+
declare function mountDevtoolsPanel(host: HTMLElement, port: PanelPort, options?: DevtoolsPanelOptions): DevtoolsPanel;
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/TreeRows.d.ts
|
|
856
|
+
/**
|
|
857
|
+
* A tree snapshot as the rows a panel draws.
|
|
858
|
+
*
|
|
859
|
+
* The panel keeps a set of expanded ids and asks for the rows; the
|
|
860
|
+
* tree can be replaced by a new snapshot every frame while the set
|
|
861
|
+
* stays, so a person's unfolding survives the application changing
|
|
862
|
+
* under it. Ids are positional, which is what makes that work and also
|
|
863
|
+
* what makes it approximate: a row that was a button can become a text
|
|
864
|
+
* when a list reorders. The report says what it is now.
|
|
865
|
+
*/
|
|
866
|
+
interface TreeRow {
|
|
867
|
+
readonly node: UiTreeNode;
|
|
868
|
+
readonly depth: number;
|
|
869
|
+
readonly expandable: boolean;
|
|
870
|
+
readonly expanded: boolean;
|
|
871
|
+
/** The nearest component anchor at or above this node, if any. */
|
|
872
|
+
readonly owner?: string;
|
|
873
|
+
}
|
|
874
|
+
/** The rows of the tree with `expanded` nodes unfolded, in document order. */
|
|
875
|
+
declare function treeRows(root: UiTreeNode, expanded: ReadonlySet<string>): TreeRow[];
|
|
876
|
+
/** The ids of every node with children down to `depth` levels, for a first unfolding. */
|
|
877
|
+
declare function idsToDepth(root: UiTreeNode, depth: number): string[];
|
|
878
|
+
/** The ids of the ancestors of `id`, root first, or null when the tree has no such node. */
|
|
879
|
+
declare function pathTo(root: UiTreeNode, id: string): string[] | null;
|
|
880
|
+
/** One row's label, as the tree prints it. */
|
|
881
|
+
declare function rowLabel(row: TreeRow): string;
|
|
882
|
+
//#endregion
|
|
883
|
+
//#region src/NodeInspector.d.ts
|
|
884
|
+
/**
|
|
885
|
+
* The node inspector: what the thing under the
|
|
886
|
+
* pointer is, and where every part of it came from.
|
|
887
|
+
*
|
|
888
|
+
* L8 already answered "why is this box that size" and printed it as
|
|
889
|
+
* text. This is the rest of the question a person actually has, which
|
|
890
|
+
* turned out to be four more: what did the element declare, what is a
|
|
891
|
+
* modifier writing over it, what is it inheriting, and which component
|
|
892
|
+
* rendered it. The layout explanation is still here, at the bottom,
|
|
893
|
+
* because it is still the answer to the first one.
|
|
894
|
+
*
|
|
895
|
+
* It is DOM, in a shadow root, over the canvas, for the same reasons
|
|
896
|
+
* the error overlay is: the application it is inspecting owns the
|
|
897
|
+
* canvas, and a panel drawn inside the scene would be part of the
|
|
898
|
+
* scene it is describing. It takes no pointer events at all, because
|
|
899
|
+
* the person is hovering the canvas underneath it and a panel that
|
|
900
|
+
* swallowed the pointer would erase the very thing it is showing.
|
|
901
|
+
*/
|
|
902
|
+
interface NodeInspector {
|
|
903
|
+
/** Shows the report, or hides the panel when null. */
|
|
904
|
+
set(report: UiNodeReport | null): void;
|
|
905
|
+
dispose(): void;
|
|
906
|
+
}
|
|
907
|
+
interface NodeInspectorOptions {
|
|
908
|
+
/**
|
|
909
|
+
* Which corner it floats in. Default `'bottom-left'`.
|
|
910
|
+
*
|
|
911
|
+
* A corner rather than a docked side, because the panel must not
|
|
912
|
+
* change the size of the element the application is mounted in: a
|
|
913
|
+
* readout that resized the canvas would relayout the scene it is
|
|
914
|
+
* describing, and the boxes it is pointing at would move.
|
|
915
|
+
*/
|
|
916
|
+
readonly corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
917
|
+
}
|
|
918
|
+
declare function mountNodeInspector(host: HTMLElement, options?: NodeInspectorOptions): NodeInspector;
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/FrameProfiler.d.ts
|
|
921
|
+
/**
|
|
922
|
+
* The frame profiler: where a frame's time went, over
|
|
923
|
+
* the last few seconds, as a picture.
|
|
924
|
+
*
|
|
925
|
+
* The timings have existed since L7 and the playground has been showing
|
|
926
|
+
* them as one clipped line of text, which is enough to read a number
|
|
927
|
+
* off and not enough to see a shape. A stall, a phase that only wakes
|
|
928
|
+
* on some frames, a render that grew when a route changed: all three
|
|
929
|
+
* are obvious in a strip of bars and invisible in a running average.
|
|
930
|
+
*
|
|
931
|
+
* It draws into a small canvas rather than a div per bar, because at 60
|
|
932
|
+
* frames a second a DOM per frame is more main-thread work than the
|
|
933
|
+
* thing being profiled. For the same reason it redraws on a timer
|
|
934
|
+
* rather than on every frame: the history is kept per frame, the
|
|
935
|
+
* picture is repainted a few times a second.
|
|
936
|
+
*/
|
|
937
|
+
interface FrameProfiler {
|
|
938
|
+
/** Feed it every frame. Cheap: it appends and returns. */
|
|
939
|
+
report(metrics: FrameMetrics): void;
|
|
940
|
+
/** Shows or hides the panel. Hidden panels stop redrawing. */
|
|
941
|
+
setVisible(visible: boolean): void;
|
|
942
|
+
readonly visible: boolean;
|
|
943
|
+
dispose(): void;
|
|
944
|
+
}
|
|
945
|
+
interface FrameProfilerOptions {
|
|
946
|
+
/** How many frames the strip holds. Default 180, about three seconds. */
|
|
947
|
+
readonly history?: number;
|
|
948
|
+
/** How often the picture is repainted, in milliseconds. Default 250. */
|
|
949
|
+
readonly redrawMs?: number;
|
|
950
|
+
readonly corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
951
|
+
/**
|
|
952
|
+
* `floating` (the default) sits in a corner over the application;
|
|
953
|
+
* `docked` fills its host, for a panel that is not over anything.
|
|
954
|
+
*/
|
|
955
|
+
readonly layout?: 'floating' | 'docked';
|
|
956
|
+
}
|
|
957
|
+
/** One bar's worth of history. */
|
|
958
|
+
interface FrameSample {
|
|
959
|
+
readonly phases: Readonly<Record<UiFramePhase, number>>;
|
|
960
|
+
readonly total: number;
|
|
961
|
+
/** Gap since the previous frame, on the rendering thread's clock. */
|
|
962
|
+
readonly gap: number;
|
|
963
|
+
readonly input: number | null;
|
|
964
|
+
}
|
|
965
|
+
declare function mountFrameProfiler(host: HTMLElement, options?: FrameProfilerOptions): FrameProfiler;
|
|
966
|
+
interface FrameSummary {
|
|
967
|
+
readonly fps: number;
|
|
968
|
+
readonly meanTotal: number;
|
|
969
|
+
readonly worstTotal: number;
|
|
970
|
+
readonly worstGap: number;
|
|
971
|
+
readonly worstInput: number | null;
|
|
972
|
+
readonly worstPhase: Record<UiFramePhase, number>;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* The window's numbers.
|
|
976
|
+
*
|
|
977
|
+
* Peak rather than mean for the phases, for the reason the playground's
|
|
978
|
+
* status line already gives: patches and environment run on a small
|
|
979
|
+
* minority of frames, so a mean would report them as idle on exactly
|
|
980
|
+
* the frames where they were the cost.
|
|
981
|
+
*/
|
|
982
|
+
declare function summarize(samples: readonly FrameSample[]): FrameSummary;
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/codeFrame.d.ts
|
|
985
|
+
/** One line of a code frame, with the offending one marked. */
|
|
986
|
+
interface CodeFrameLine {
|
|
987
|
+
number: number;
|
|
988
|
+
text: string;
|
|
989
|
+
/** True for the line the error was reported on. */
|
|
990
|
+
target: boolean;
|
|
991
|
+
}
|
|
992
|
+
/** A few lines of original source around a mapped position. */
|
|
993
|
+
interface CodeFrame {
|
|
994
|
+
lines: CodeFrameLine[];
|
|
995
|
+
/** 1-based column on the target line, for the caret under it. */
|
|
996
|
+
column: number;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Cuts `context` lines either side of a position out of a source file.
|
|
1000
|
+
*
|
|
1001
|
+
* This is the part of an overlay that a stack trace cannot replace: a
|
|
1002
|
+
* file and a line number send a person to their editor, while the line
|
|
1003
|
+
* itself is often the whole answer — a `.length` on something that is
|
|
1004
|
+
* undefined reads as the bug the moment it is on screen.
|
|
1005
|
+
*
|
|
1006
|
+
* Tabs are expanded to two spaces so the caret column below the line
|
|
1007
|
+
* lands where the character does. Nothing else is transformed; the text
|
|
1008
|
+
* reaches the DOM as text, never as markup.
|
|
1009
|
+
*/
|
|
1010
|
+
declare function codeFrame(source: string, line: number, column: number, context?: number): CodeFrame | null;
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region src/stackTrace.d.ts
|
|
1013
|
+
/** A position in a compiled file, 1-based as every engine reports it. */
|
|
1014
|
+
interface StackLocation {
|
|
1015
|
+
url: string;
|
|
1016
|
+
line: number;
|
|
1017
|
+
column: number;
|
|
1018
|
+
}
|
|
1019
|
+
/** One line of a stack, parsed as far as it could be. */
|
|
1020
|
+
interface StackFrame {
|
|
1021
|
+
/** The line exactly as the engine wrote it. */
|
|
1022
|
+
raw: string;
|
|
1023
|
+
/** The function name the engine knew, or null for an anonymous frame. */
|
|
1024
|
+
fn: string | null;
|
|
1025
|
+
/** Where it ran, or null when the line named no file. */
|
|
1026
|
+
location: StackLocation | null;
|
|
1027
|
+
/** Where it was written, once a source map has been consulted. */
|
|
1028
|
+
original: OriginalPosition | null;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Splits a stack string into frames, dropping the message lines an
|
|
1032
|
+
* engine puts above them.
|
|
1033
|
+
*
|
|
1034
|
+
* The message is dropped rather than parsed because the caller already
|
|
1035
|
+
* has it: `onError` reports the message and the stack separately, and
|
|
1036
|
+
* a V8 stack repeats the message in its first line.
|
|
1037
|
+
*/
|
|
1038
|
+
declare function parseStack(stack: string): StackFrame[];
|
|
1039
|
+
/**
|
|
1040
|
+
* Fills in `original` for every frame whose script has a source map.
|
|
1041
|
+
*
|
|
1042
|
+
* Maps are fetched once per script and the frames of one stack usually
|
|
1043
|
+
* name two or three scripts, so this is a couple of requests. It never
|
|
1044
|
+
* rejects: a frame that cannot be mapped keeps its compiled location,
|
|
1045
|
+
* which is what the console would have shown anyway.
|
|
1046
|
+
*
|
|
1047
|
+
* `originalFor` rather than one `lookup`, because a frame inside a
|
|
1048
|
+
* published package is two maps deep: the bundler's map of the
|
|
1049
|
+
* dependency bundle, then the package's own. See its comment.
|
|
1050
|
+
*/
|
|
1051
|
+
declare function mapStack(frames: StackFrame[], store: SourceMapStore): Promise<StackFrame[]>;
|
|
1052
|
+
/**
|
|
1053
|
+
* The first frame worth putting a code frame under.
|
|
1054
|
+
*
|
|
1055
|
+
* Frames inside the framework are skipped while any application frame
|
|
1056
|
+
* remains, because an error thrown from a component surfaces through
|
|
1057
|
+
* several layers of runtime and the runtime is almost never where the
|
|
1058
|
+
* bug is. If every frame is a framework frame, the first one wins —
|
|
1059
|
+
* the framework is then genuinely the answer.
|
|
1060
|
+
*/
|
|
1061
|
+
declare function primaryFrame(frames: StackFrame[]): StackFrame | null;
|
|
1062
|
+
/**
|
|
1063
|
+
* A path short enough to read in a header: same-origin prefix and
|
|
1064
|
+
* query string removed, `node_modules` collapsed to the package.
|
|
1065
|
+
*
|
|
1066
|
+
* The query matters more than it looks. A dev server rewrites imports
|
|
1067
|
+
* with cache-busting parameters — `/src/App.tsx?t=1724965201` — and a
|
|
1068
|
+
* stack full of those is unreadable for a reason that has nothing to
|
|
1069
|
+
* do with the error.
|
|
1070
|
+
*/
|
|
1071
|
+
declare function shortenPath(url: string, origin?: string): string;
|
|
1072
|
+
/** `path:line:column` for a frame, mapped when it could be. */
|
|
1073
|
+
declare function formatFrame(frame: StackFrame, origin?: string): string;
|
|
1074
|
+
//#endregion
|
|
1075
|
+
export { type ActionCause, type ActionEntry, type ActionLog, type ActionLogOptions, type ActionLogPanel, type ActionLogPanelOptions, type ActionLogToken, type ChannelErrorEntry, type CodeFrame, type CodeFrameLine, type CommandEntry, type ConnectDevtoolsOptions, type DevtoolsApp, type DevtoolsAppInfo, type DevtoolsHook, type DevtoolsPanel, type DevtoolsPanelOptions, type DevtoolsPanelTheme, type DevtoolsPicker, type DevtoolsPort, ENVELOPE_SOURCE, type Envelope, type ErrorOrigin, ErrorOverlay, type ErrorOverlayOptions, type FrameEntry, type FrameProfiler, type FrameProfilerOptions, type FrameSample, type FrameSummary, HOOK_PROPERTY, type HookHost, NODE_REPORT_STYLES, type NodeInspector, type NodeInspectorOptions, type NodePickerOptions, type NodeReportViewOptions, type OriginalPosition, type PageMessage, type PagePort, type PanelMessage, type PanelPort, type PatchEntry, type RenderWorkerHost, type RenderWorkerTap, type RenderWorkerTapOptions, SourceMapConsumer, SourceMapStore, type SourceMapV3, type StackFrame, type StackLocation, type TreeRow, type WindowLike, actionGlyph, codeFrame, connectDevtools, createActionLog, createDirectPorts, createNodePicker, decodeMappings, describeActionEntry, formatFrame, forwardNewEntries, getDevtoolsHook, idsToDepth, inputLabel, isEnvelope, mapStack, mountActionLogPanel, mountDevtoolsPanel, mountErrorOverlay, mountFrameProfiler, mountNodeInspector, parseSourceMappingUrl, parseStack, pathTo, primaryFrame, renderNodeReport, rowLabel, shortenPath, summarize, tapRenderWorker, treeRows, windowPagePort, windowPanelPort };
|
|
1076
|
+
//# sourceMappingURL=index.d.ts.map
|