@fluixi/devtools 0.2.0-alpha.2 → 0.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +80 -2
  2. package/dist/callsite.cjs +72 -0
  3. package/dist/callsite.d.ts +22 -0
  4. package/dist/callsite.d.ts.map +1 -0
  5. package/dist/callsite.js +82 -0
  6. package/dist/callsite.mjs +51 -0
  7. package/dist/change-view.cjs +134 -0
  8. package/dist/change-view.d.ts +10 -0
  9. package/dist/change-view.d.ts.map +1 -0
  10. package/dist/change-view.js +102 -0
  11. package/dist/change-view.mjs +111 -0
  12. package/dist/copy.cjs +63 -0
  13. package/dist/copy.d.ts +9 -0
  14. package/dist/copy.d.ts.map +1 -0
  15. package/dist/copy.js +52 -0
  16. package/dist/copy.mjs +42 -0
  17. package/dist/describe.cjs +55 -0
  18. package/dist/describe.d.ts +3 -0
  19. package/dist/describe.d.ts.map +1 -0
  20. package/dist/describe.js +47 -0
  21. package/dist/describe.mjs +34 -0
  22. package/dist/detail-panel.cjs +307 -0
  23. package/dist/detail-panel.d.ts +17 -0
  24. package/dist/detail-panel.d.ts.map +1 -0
  25. package/dist/detail-panel.js +217 -0
  26. package/dist/detail-panel.mjs +284 -0
  27. package/dist/diff.cjs +225 -0
  28. package/dist/diff.d.ts +31 -0
  29. package/dist/diff.d.ts.map +1 -0
  30. package/dist/diff.js +132 -0
  31. package/dist/diff.mjs +202 -0
  32. package/dist/dom.cjs +204 -0
  33. package/dist/dom.d.ts +81 -0
  34. package/dist/dom.d.ts.map +1 -0
  35. package/dist/dom.js +269 -0
  36. package/dist/dom.mjs +183 -0
  37. package/dist/export.cjs +221 -0
  38. package/dist/export.d.ts +55 -0
  39. package/dist/export.d.ts.map +1 -0
  40. package/dist/export.js +90 -0
  41. package/dist/export.mjs +209 -0
  42. package/dist/float.cjs +232 -0
  43. package/dist/float.d.ts +54 -0
  44. package/dist/float.d.ts.map +1 -0
  45. package/dist/float.js +256 -0
  46. package/dist/float.mjs +211 -0
  47. package/dist/format.cjs +175 -0
  48. package/dist/format.d.ts +16 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +188 -0
  51. package/dist/format.mjs +154 -0
  52. package/dist/graph-view.cjs +893 -0
  53. package/dist/graph-view.d.ts +79 -2
  54. package/dist/graph-view.d.ts.map +1 -1
  55. package/dist/graph-view.js +866 -50
  56. package/dist/graph-view.mjs +870 -0
  57. package/dist/highlight.cjs +140 -0
  58. package/dist/highlight.d.ts +56 -0
  59. package/dist/highlight.d.ts.map +1 -0
  60. package/dist/highlight.js +154 -0
  61. package/dist/highlight.mjs +119 -0
  62. package/dist/history.cjs +95 -0
  63. package/dist/history.d.ts +59 -0
  64. package/dist/history.d.ts.map +1 -0
  65. package/dist/history.js +85 -0
  66. package/dist/history.mjs +74 -0
  67. package/dist/hook.cjs +1562 -0
  68. package/dist/hook.d.ts +60 -0
  69. package/dist/hook.d.ts.map +1 -0
  70. package/dist/hook.js +142 -0
  71. package/dist/hook.mjs +1553 -0
  72. package/dist/index.cjs +4552 -0
  73. package/dist/index.d.ts +44 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +25 -1
  76. package/dist/index.mjs +4544 -0
  77. package/dist/inspector.cjs +688 -0
  78. package/dist/inspector.d.ts +60 -0
  79. package/dist/inspector.d.ts.map +1 -0
  80. package/dist/inspector.js +415 -0
  81. package/dist/inspector.mjs +676 -0
  82. package/dist/instrument.cjs +497 -0
  83. package/dist/instrument.d.ts +147 -5
  84. package/dist/instrument.d.ts.map +1 -1
  85. package/dist/instrument.js +164 -44
  86. package/dist/instrument.mjs +487 -0
  87. package/dist/kind-filter.cjs +172 -0
  88. package/dist/kind-filter.d.ts +39 -0
  89. package/dist/kind-filter.d.ts.map +1 -0
  90. package/dist/kind-filter.js +151 -0
  91. package/dist/kind-filter.mjs +151 -0
  92. package/dist/menu.cjs +141 -0
  93. package/dist/menu.d.ts +37 -0
  94. package/dist/menu.d.ts.map +1 -0
  95. package/dist/menu.js +125 -0
  96. package/dist/menu.mjs +120 -0
  97. package/dist/observe.cjs +1352 -0
  98. package/dist/observe.d.ts +80 -0
  99. package/dist/observe.d.ts.map +1 -0
  100. package/dist/observe.js +893 -0
  101. package/dist/observe.mjs +1345 -0
  102. package/dist/palette.cjs +52 -0
  103. package/dist/palette.d.ts +11 -0
  104. package/dist/palette.d.ts.map +1 -0
  105. package/dist/palette.js +27 -0
  106. package/dist/palette.mjs +31 -0
  107. package/dist/path.cjs +45 -0
  108. package/dist/path.d.ts +19 -0
  109. package/dist/path.d.ts.map +1 -0
  110. package/dist/path.js +49 -0
  111. package/dist/path.mjs +24 -0
  112. package/dist/plugin.cjs +87 -0
  113. package/dist/plugin.d.ts +121 -0
  114. package/dist/plugin.d.ts.map +1 -0
  115. package/dist/plugin.js +74 -0
  116. package/dist/plugin.mjs +66 -0
  117. package/dist/plugins/context.cjs +98 -0
  118. package/dist/plugins/context.d.ts +10 -0
  119. package/dist/plugins/context.d.ts.map +1 -0
  120. package/dist/plugins/context.js +80 -0
  121. package/dist/plugins/context.mjs +75 -0
  122. package/dist/plugins/directives.cjs +98 -0
  123. package/dist/plugins/directives.d.ts +25 -0
  124. package/dist/plugins/directives.d.ts.map +1 -0
  125. package/dist/plugins/directives.js +78 -0
  126. package/dist/plugins/directives.mjs +75 -0
  127. package/dist/plugins/index.cjs +162 -0
  128. package/dist/plugins/index.d.ts +7 -0
  129. package/dist/plugins/index.d.ts.map +1 -0
  130. package/dist/plugins/index.js +9 -0
  131. package/dist/plugins/index.mjs +140 -0
  132. package/dist/source.cjs +111 -0
  133. package/dist/source.d.ts +72 -0
  134. package/dist/source.d.ts.map +1 -0
  135. package/dist/source.js +153 -0
  136. package/dist/source.mjs +90 -0
  137. package/dist/timeline.cjs +350 -0
  138. package/dist/timeline.d.ts +30 -0
  139. package/dist/timeline.d.ts.map +1 -0
  140. package/dist/timeline.js +271 -0
  141. package/dist/timeline.mjs +327 -0
  142. package/dist/tree.cjs +574 -0
  143. package/dist/tree.d.ts +128 -0
  144. package/dist/tree.d.ts.map +1 -0
  145. package/dist/tree.js +645 -0
  146. package/dist/tree.mjs +551 -0
  147. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  148. package/dist/value-pane.cjs +223 -0
  149. package/dist/value-pane.d.ts +10 -0
  150. package/dist/value-pane.d.ts.map +1 -0
  151. package/dist/value-pane.js +61 -0
  152. package/dist/value-pane.mjs +200 -0
  153. package/dist/wire.cjs +109 -0
  154. package/dist/wire.d.ts +100 -0
  155. package/dist/wire.d.ts.map +1 -0
  156. package/dist/wire.js +93 -0
  157. package/dist/wire.mjs +89 -0
  158. package/package.json +17 -5
@@ -0,0 +1,893 @@
1
+ // Watching an ordinary app.
2
+ //
3
+ // createReactor hands out its own primitives, so it only ever sees code written against it.
4
+ // This sees code written against @fluixi/core: the runtime reports each node it makes, and
5
+ // the node it reports carries its own edges. A source keeps `observers` and a consumer keeps
6
+ // `sources`, so dependencies are read off the real graph instead of being rebuilt from
7
+ // wrapped reads.
8
+ //
9
+ // The graph it fills is the same Map<number, RNode> createGraphView already draws, so a
10
+ // renderer does not care which of the two produced it.
11
+ import { observeReactiveNodes, untrack, } from '@fluixi/reactive/signal';
12
+ import { componentSource, holeLocation, installSourceMarker, propsLocations, takeReactiveSource } from './source.js';
13
+ import { currentCallSite } from './callsite.js';
14
+ import { KIND_OF_BUILTIN } from './instrument.js';
15
+ import { formatValue, shorten } from './format.js';
16
+ import { createHistory } from './history.js';
17
+ import { diffValues } from './diff.js';
18
+ import { describeNode } from './describe.js';
19
+ import { describeAll, tagDepths } from './dom.js';
20
+ import { devtoolsPlugins, } from './plugin.js';
21
+ import { TREE_KINDS } from './tree.js';
22
+ /**
23
+ * Number each tag by where its element sits in the document.
24
+ *
25
+ * A keyed list rebuilds a row instead of mutating it, so the replacement carries the highest
26
+ * id. Sorting by id then put the new card last whichever row changed, and the flash always
27
+ * appeared at the bottom of the list. Only something holding the elements can say otherwise.
28
+ *
29
+ * Done in `sample`, not when a description is asked for: the graph view never asks for one,
30
+ * so stamping this lazily left every node unnumbered there and the fix showed only in the tree.
31
+ */
32
+ function stampDocumentOrder(graph, rendered) {
33
+ const roots = [];
34
+ for (const node of graph.values()) {
35
+ if (!TREE_KINDS.has(node.kind))
36
+ continue;
37
+ const element = rendered(node.id)[0];
38
+ if (element?.isConnected)
39
+ roots.push({ id: node.id, element });
40
+ }
41
+ // DOCUMENT_POSITION_FOLLOWING: b comes after a, so a sorts first.
42
+ roots.sort((a, b) => (a.element.compareDocumentPosition(b.element) & 4 ? -1 : 1));
43
+ for (const node of graph.values())
44
+ node.order = undefined;
45
+ roots.forEach(({ id }, at) => {
46
+ const node = graph.get(id);
47
+ if (node)
48
+ node.order = at;
49
+ });
50
+ }
51
+ /** How far back a consumer may be and still be told what was dropped. */
52
+ const REMOVED_LOG = 500;
53
+ const KINDS = {
54
+ signal: 'state',
55
+ memo: 'memo',
56
+ effect: 'effect',
57
+ store: 'store',
58
+ resource: 'resource',
59
+ };
60
+ /** Long enough for an object's first fields, short enough that a few hundred stays small. */
61
+ const HISTORY_VALUE = 80;
62
+ /** How many distinct places may be remembered as writing one signal. */
63
+ const SET_SITES = 8;
64
+ const PREFIX = {
65
+ state: 's',
66
+ memo: 'm',
67
+ effect: 'e',
68
+ store: 'st',
69
+ resource: 'r',
70
+ component: 'c',
71
+ // Only the static graph makes these: at runtime control flow is a named memo, and a route
72
+ // handler never reaches a browser.
73
+ control: 'x',
74
+ router: 'rt',
75
+ handler: 'h',
76
+ // Never produced here: these are cards a view draws, not nodes anything creates.
77
+ group: 'g',
78
+ binding: 'b',
79
+ setter: 'w',
80
+ // A plugin's, and it names them itself.
81
+ directive: 'd',
82
+ context: 'ctx',
83
+ provider: 'p',
84
+ };
85
+ /**
86
+ * Start watching. Nodes made before this returns are not in the graph, so attach before the
87
+ * app's modules load, which in practice means before the entry import.
88
+ */
89
+ /**
90
+ * What a component was handed, read for display.
91
+ *
92
+ * Every prop is a getter over whatever the caller wrote, so reading one runs that
93
+ * expression. Untracked, or the read would subscribe whatever is sampling to it and the
94
+ * panel would drive the graph by looking at it. `children` is skipped: reading it builds
95
+ * the subtree, which is a side effect nobody asked for by opening an inspector.
96
+ */
97
+ function readProps(props) {
98
+ const at = propsLocations(props);
99
+ const out = [];
100
+ for (const name of Object.keys(props)) {
101
+ if (name === 'children')
102
+ continue;
103
+ const where = at?.[name];
104
+ const report = {
105
+ name,
106
+ ...(where ? { at: { line: where[0], column: where[1] } } : {}),
107
+ };
108
+ try {
109
+ report.value = formatValue(untrack(() => props[name]), 200);
110
+ }
111
+ catch {
112
+ // A getter that throws is still a prop worth naming.
113
+ }
114
+ out.push(report);
115
+ }
116
+ return out.length ? out : undefined;
117
+ }
118
+ export function observeGraph(options = {}) {
119
+ installSourceMarker();
120
+ const origin = options.origin ?? (typeof location === 'object' ? location.origin : undefined);
121
+ const graph = new Map();
122
+ /**
123
+ * Whether this node is the wrapper that made the stamped value rather than one further
124
+ * down that only passes it along.
125
+ *
126
+ * `$name` is set on what a component returns, so it travels with the value and every memo
127
+ * holding it looks like the component. The one that made it is the one none of its
128
+ * dependencies already has. Otherwise a component under `Suspense` draws twice.
129
+ */
130
+ const claims = (node, value) => {
131
+ for (const dep of node.deps) {
132
+ const other = graph.get(dep);
133
+ if (other?.read && other.read() === value)
134
+ return false;
135
+ }
136
+ return true;
137
+ };
138
+ /** Core node → id, so a report about a node already seen finds it. */
139
+ const ids = new WeakMap();
140
+ /**
141
+ * Plumbing the graph does not draw, mapped to the drawn node it was created inside. A row
142
+ * added to a list is created inside a render effect; without this it would arrive with no
143
+ * ancestor at all, and the component that caused it would sit on its own.
144
+ */
145
+ const through = new WeakMap();
146
+ /**
147
+ * The same plumbing, iterable.
148
+ *
149
+ * A signal whose only readers are dom bindings looks like a node nothing uses, because the
150
+ * bindings are not drawn. They are still what makes it matter, so they are counted.
151
+ * Dropped when the effect that holds them is disposed.
152
+ */
153
+ const internals = new Set();
154
+ /**
155
+ * What each binding effect writes into, keyed by the effect: one effect can read several
156
+ * signals and all of them drive the same place. A description, never the element.
157
+ */
158
+ const targets = new WeakMap();
159
+ /** The props object each component was handed, so its values can be read on request. */
160
+ const handed = new Map();
161
+ /**
162
+ * Where each binding was wired up. Taken at creation: the first run is scheduled, so by the
163
+ * time it reports, the frame that asked for it has left the stack.
164
+ */
165
+ const bindingSites = new WeakMap();
166
+ /**
167
+ * The element each binding writes into, weakly.
168
+ *
169
+ * `targets` describes elements rather than holding them, because holding one keeps a
170
+ * removed subtree alive. A WeakRef does not, so this can answer "show me that on the
171
+ * page" without pinning anything.
172
+ */
173
+ const boundTo = new WeakMap();
174
+ /** The reverse, for a pointer landing somewhere: element → the node driving it. */
175
+ const drivenBy = new WeakMap();
176
+ /** Per node, the elements it drives. Rebuilt each sample, like the descriptions are. */
177
+ let reaches = new Map();
178
+ /** Cleared on each sample: the page has moved on. */
179
+ let described = null;
180
+ /**
181
+ * What each tag rendered, weakly.
182
+ *
183
+ * A component's value is the dom it returned, so this is exact where the bindings are
184
+ * only nearby: a binding is recorded against the element containing it, which for a small
185
+ * component is whatever the page happens to have put it in.
186
+ */
187
+ const rendered = new Map();
188
+ /** The elements in a component's return value, however it was shaped. */
189
+ const elementsIn = (value) => {
190
+ if (value instanceof Element)
191
+ return [value];
192
+ if (value instanceof DocumentFragment)
193
+ return [...value.children];
194
+ // A component returning several roots hands back an array of nodes.
195
+ if (Array.isArray(value))
196
+ return value.flatMap(elementsIn);
197
+ return [];
198
+ };
199
+ const cores = new Map();
200
+ /**
201
+ * How to write each signal, when the runtime said.
202
+ *
203
+ * The core node can be set directly, and doing that marks its readers dirty without
204
+ * flushing them: the graph moves and the page does not. This is the application's own
205
+ * setter, so a write from a panel lands the same way a click would.
206
+ */
207
+ const setters = new Map();
208
+ const runsAtLastSample = new Map();
209
+ const unnamed = new Set();
210
+ const recording = (options.history ?? 200) > 0;
211
+ const wantSites = options.bindingSources ?? true;
212
+ /** A stack location in the author's terms, when the host can say. */
213
+ const mapped = (location) => {
214
+ if (!options.mapSource)
215
+ return location;
216
+ try {
217
+ // A mapper that cannot place a position must not turn a usable location into none.
218
+ const found = options.mapSource(location);
219
+ // The compiled position is kept: it distinguishes bindings the map cannot.
220
+ return found
221
+ ? { ...location, ...found, compiled: { line: location.line, column: location.column } }
222
+ : location;
223
+ }
224
+ catch {
225
+ return location;
226
+ }
227
+ };
228
+ const history = createHistory(options.history ?? 200);
229
+ let seq = 0;
230
+ /**
231
+ * The raw value each node last reported, so a write can be diffed against it. A formatted
232
+ * string cannot be. One reference per node, so this pins no more than the graph does.
233
+ */
234
+ const raws = new Map();
235
+ /** Untracked: this runs inside a write, and a tracked read would change what it watches. */
236
+ const readRaw = (node) => {
237
+ if (!node.read)
238
+ return undefined;
239
+ try {
240
+ return untrack(() => node.read());
241
+ }
242
+ catch {
243
+ // A reader that throws is not worth losing the event over.
244
+ return undefined;
245
+ }
246
+ };
247
+ /**
248
+ * Removals, kept rather than drained, each stamped with the revision it happened at.
249
+ *
250
+ * Draining meant whichever consumer polled first took them, and a page with both a
251
+ * browser panel and an editor panel watching has two. Bounded, because nothing here is
252
+ * ever consumed. A consumer older than the oldest entry is told to start again.
253
+ */
254
+ const removed = [];
255
+ /** Monotonic, bumped for every node the sample finds moved. */
256
+ let revision = 0;
257
+ /** The revision each node last moved at. */
258
+ const revs = new Map();
259
+ /** How far `takeRemoved` has drained, for a caller too old to keep a cursor of its own. */
260
+ let drained = 0;
261
+ /** What plugins asked to hear about. Empty unless one registered. */
262
+ const onDirective = [];
263
+ const onContext = [];
264
+ /** What plugins decided a node really is, applied once it has been named. */
265
+ const retyped = new Map();
266
+ /** Where each context was written, taken from the stack at the moment it was made. */
267
+ const contextSites = new Map();
268
+ /** What plugins attached to elements, by element then plugin name. */
269
+ const elementExtra = new WeakMap();
270
+ /** The nearest node the graph knows, looking outward through anything it does not. */
271
+ const ancestor = (parents) => {
272
+ if (!parents)
273
+ return undefined;
274
+ for (let i = parents.length - 1; i >= 0; i--) {
275
+ const key = parents[i];
276
+ const found = ids.get(key) ?? through.get(key);
277
+ if (found !== undefined)
278
+ return found;
279
+ }
280
+ return undefined;
281
+ };
282
+ const stop = (options.observe ?? observeReactiveNodes)({
283
+ created(report) {
284
+ const core = report.node;
285
+ if (!core || typeof core !== 'object')
286
+ return;
287
+ if (report.internal) {
288
+ // Not drawn, but remembered: whatever it creates belongs to what created it, and
289
+ // what it reads is the reason a signal with no visible readers is not idle.
290
+ const owner = ancestor(report.parents);
291
+ if (owner !== undefined)
292
+ through.set(core, owner);
293
+ internals.add(core);
294
+ if (wantSites) {
295
+ // The package's frame when there is no application one: a binding inside a library
296
+ // component is still somewhere. `external` marks the difference.
297
+ const site = currentCallSite(origin);
298
+ if (site)
299
+ bindingSites.set(core, mapped(site.location));
300
+ }
301
+ return;
302
+ }
303
+ const id = seq++;
304
+ const handle = report.handle;
305
+ // One mark, one node, read here and nowhere else, matching the reactor.
306
+ const mark = takeReactiveSource();
307
+ // What a builtin tag is, decided from its name before anything reads its value.
308
+ //
309
+ // `Switch` names its own memo and returns a dom node, so nothing downstream carries a
310
+ // `$name` to recognise it by later. `Router`, `Suspense` and `Outlet` are not named by
311
+ // the runtime at all. What names them is the compiler's mark on the tag, and without
312
+ // reading that they stay plain memos and the tree lists them as values.
313
+ //
314
+ // A mark on a memo is a tag the compiler saw. On anything else it is whatever the
315
+ // author called their own value, so a signal someone named `Show` is left alone.
316
+ const base = KINDS[report.kind];
317
+ const marked = base === 'memo' && mark?.name ? KIND_OF_BUILTIN[mark.name] : undefined;
318
+ const kind = (report.name ? KIND_OF_BUILTIN[report.name] : undefined) ?? marked ?? base;
319
+ const named = report.name ?? mark?.name;
320
+ if (!named)
321
+ unnamed.add(id);
322
+ // Only for a node the compiler did not mark. This is called from inside the creating
323
+ // call, so the frames that led to it are still on the stack.
324
+ const site = mark?.location ? undefined : currentCallSite(origin);
325
+ const node = {
326
+ id,
327
+ label: named ?? `${PREFIX[kind]}${id}`,
328
+ kind,
329
+ // Only a compiler mark means the author wrote this and gave it that name. A name
330
+ // the runtime supplies, such as `Show.when`, `mapArray` or `row index`, describes framework
331
+ // machinery, and so does no name at all.
332
+ authored: mark?.name != null,
333
+ deps: new Set(),
334
+ // A write edge needs to know which consumer was running when a setter fired, and
335
+ // nothing observed records that. Reads carry the graph; writes do not.
336
+ writes: new Set(),
337
+ runs: 0,
338
+ lastPulse: 0,
339
+ core,
340
+ // The application's line is where it was asked for; a package's is where it was made,
341
+ // and for a node made while that package loaded it is the only thing there is.
342
+ ...(mark?.location ? { source: mark.location } : {}),
343
+ // Mapped, unlike `mark`: these two came off a stack, so they are positions in the
344
+ // compiled file until a host translates them.
345
+ ...(site && !site.external ? { usedAt: mapped(site.location) } : {}),
346
+ ...(site && site.external && !mark?.location ? { source: mapped(site.location) } : {}),
347
+ };
348
+ const owner = ancestor(report.parents);
349
+ if (owner !== undefined)
350
+ node.createdIn = owner;
351
+ // A node made on another's behalf, such as a store's key signal built the first time
352
+ // something reads it, belongs where that one was declared rather than where it was read.
353
+ const originId = report.origin ? ids.get(report.origin) : undefined;
354
+ const from = originId !== undefined ? graph.get(originId) : undefined;
355
+ if (from) {
356
+ node.createdIn = originId;
357
+ if (from.source)
358
+ node.source = from.source;
359
+ }
360
+ // An effect returns nothing worth showing, so it has no reader and pulses on its runs.
361
+ if (kind !== 'effect' && typeof handle === 'function') {
362
+ // Not read here: a memo is lazy, and forcing it from inside its own creation would
363
+ // run the author's fn earlier than the app asked for. The first sample reads it.
364
+ const read = handle;
365
+ node.read = () => untrack(read);
366
+ }
367
+ ids.set(core, id);
368
+ // The accessor too: a node built on another's behalf names its owner by the handle it
369
+ // was given, which is the reader, not the core node behind it.
370
+ if (handle && (typeof handle === 'object' || typeof handle === 'function')) {
371
+ ids.set(handle, id);
372
+ }
373
+ cores.set(id, core);
374
+ if (typeof report.set === 'function')
375
+ setters.set(id, report.set);
376
+ graph.set(id, node);
377
+ history.created(id, node.createdIn, node.label);
378
+ },
379
+ ran(target) {
380
+ const id = ids.get(target);
381
+ if (id !== undefined) {
382
+ const node = graph.get(id);
383
+ if (node)
384
+ node.runs++;
385
+ history.ran(id);
386
+ }
387
+ },
388
+ wrote(target, by) {
389
+ const writer = by ? ids.get(by) : undefined;
390
+ const written = ids.get(target);
391
+ if (written === undefined)
392
+ return;
393
+ // Nothing running: a handler, a timer, a socket, none of which the graph has a node
394
+ // for. Where it was called is all there is to say, and the stack still has it.
395
+ if (wantSites && writer === undefined) {
396
+ const node = graph.get(written);
397
+ const site = currentCallSite(origin);
398
+ if (node && site) {
399
+ const seen = (node.setAt ??= []);
400
+ const at = mapped(site.location);
401
+ // One handler called two hundred times is one place.
402
+ if (!seen.some((s) => s.file === at.file && s.line === at.line && s.column === at.column)) {
403
+ if (seen.length >= SET_SITES)
404
+ seen.shift();
405
+ seen.push(at);
406
+ }
407
+ }
408
+ }
409
+ // Before the edge check and whether or not anything was running: the write with no
410
+ // writer is the one the timeline is built around.
411
+ if (recording) {
412
+ const node = graph.get(written);
413
+ // The write has landed, so this is the new value; the old one was kept last time.
414
+ const after = node ? readRaw(node) : undefined;
415
+ const before = raws.get(written);
416
+ if (node)
417
+ raws.set(written, after);
418
+ // Both sides cut to the same budget, or the arrow reads as two kinds of thing.
419
+ const shown = node?.detail ?? node?.value;
420
+ history.wrote(written, writer, {
421
+ from: shown === undefined ? undefined : shorten(shown, HISTORY_VALUE),
422
+ to: node ? shorten(formatValue(after, HISTORY_VALUE * 2), HISTORY_VALUE) : undefined,
423
+ // Which row was added is the fact a reader is after.
424
+ change: diffValues(before, after),
425
+ label: node?.label,
426
+ });
427
+ }
428
+ // Only a write from inside a computation is an edge: a handler is not a node.
429
+ if (writer === undefined || writer === written)
430
+ return;
431
+ graph.get(writer)?.writes.add(written);
432
+ },
433
+ directive(node, target, kind, name, accessor) {
434
+ if (!onDirective.length)
435
+ return;
436
+ const core = node;
437
+ const owner = core && typeof core === 'object' ? ids.get(core) : undefined;
438
+ const at = holeLocation(accessor);
439
+ const report = {
440
+ owner: owner !== undefined ? (through.get(core) ?? owner) : undefined,
441
+ kind,
442
+ name,
443
+ element: describeNode(target),
444
+ target,
445
+ ...(at ? { at } : {}),
446
+ };
447
+ for (const fn of onDirective) {
448
+ try {
449
+ fn(report);
450
+ }
451
+ catch {
452
+ // A plugin with a bug does not get to stop the graph being built.
453
+ }
454
+ }
455
+ },
456
+ contextCreated(id) {
457
+ if (!onContext.length || contextSites.has(id))
458
+ return;
459
+ const site = currentCallSite(origin);
460
+ if (site)
461
+ contextSites.set(id, mapped(site.location));
462
+ },
463
+ context(node, id, kind, value) {
464
+ if (!onContext.length)
465
+ return;
466
+ const core = node;
467
+ const owner = core && typeof core === 'object' ? ids.get(core) : undefined;
468
+ const report = {
469
+ owner: owner !== undefined ? (through.get(core) ?? owner) : undefined,
470
+ id,
471
+ name: id.description || 'context',
472
+ ...(owner !== undefined ? { ownerLabel: graph.get(owner)?.label } : {}),
473
+ // Where the context itself was written. The provider's line is a fallback, and it
474
+ // reads as a lie when two providers share one context.
475
+ ...(contextSites.get(id) ?? graph.get(owner ?? -1)?.source
476
+ ? { at: contextSites.get(id) ?? graph.get(owner ?? -1)?.source }
477
+ : {}),
478
+ kind,
479
+ ...(value === undefined ? {} : { value: formatValue(value) }),
480
+ };
481
+ for (const fn of onContext) {
482
+ try {
483
+ fn(report);
484
+ }
485
+ catch {
486
+ /* ignored */
487
+ }
488
+ }
489
+ },
490
+ props(node, props) {
491
+ const core = node;
492
+ if (!core || typeof core !== 'object' || !props)
493
+ return;
494
+ const id = ids.get(core);
495
+ // Held, not read. Every prop is a getter over what the caller passed, so reading one
496
+ // here would subscribe whatever is running to it.
497
+ if (id !== undefined)
498
+ handed.set(id, props);
499
+ },
500
+ bound(node, target, kind, name, accessor) {
501
+ const core = node;
502
+ if (!core || typeof core !== 'object')
503
+ return;
504
+ // The compiler's mark first. It names the hole itself; the stack only names the call
505
+ // that built the template, which every binding in that template shares.
506
+ const hole = holeLocation(accessor);
507
+ const site = hole
508
+ ? { ...hole, file: bindingSites.get(core)?.file }
509
+ : bindingSites.get(core);
510
+ // A content hole is reported against its marker comment, which has no box to draw.
511
+ const box = target instanceof Element
512
+ ? target
513
+ : (target?.parentElement ?? undefined);
514
+ if (box)
515
+ boundTo.set(core, new WeakRef(box));
516
+ targets.set(core, {
517
+ kind,
518
+ element: describeNode(target),
519
+ ...(name ? { name } : {}),
520
+ ...(site ? { source: site } : {}),
521
+ });
522
+ },
523
+ moved(target, origin, name) {
524
+ const node = graph.get(ids.get(target) ?? -1);
525
+ if (!node)
526
+ return;
527
+ const owner = ids.get(origin);
528
+ if (owner !== undefined && graph.has(owner))
529
+ node.createdIn = owner;
530
+ if (name) {
531
+ node.label = name;
532
+ unnamed.delete(node.id);
533
+ }
534
+ },
535
+ disposed(target) {
536
+ internals.delete(target);
537
+ const id = ids.get(target);
538
+ if (id === undefined)
539
+ return;
540
+ removed.push({ id, rev: ++revision });
541
+ revs.delete(id);
542
+ if (removed.length > REMOVED_LOG)
543
+ removed.splice(0, removed.length - REMOVED_LOG);
544
+ // The label goes with it: nothing is left to look the name up in.
545
+ history.disposed(id, graph.get(id)?.label);
546
+ graph.delete(id);
547
+ handed.delete(id);
548
+ raws.delete(id);
549
+ cores.delete(id);
550
+ rendered.delete(id);
551
+ setters.delete(id);
552
+ runsAtLastSample.delete(id);
553
+ unnamed.delete(id);
554
+ },
555
+ });
556
+ /** What this node read on its last run, as ids: the edges the core already holds. */
557
+ const edges = (id, node) => {
558
+ const sources = cores.get(id)?.sources;
559
+ node.deps.clear();
560
+ if (!sources)
561
+ return;
562
+ for (const source of sources) {
563
+ const dep = ids.get(source);
564
+ if (dep !== undefined && dep !== id)
565
+ node.deps.add(dep);
566
+ }
567
+ };
568
+ const api = {
569
+ graph,
570
+ sample() {
571
+ described = null;
572
+ const changed = [];
573
+ /** Recorded once here, so every consumer can be told about it at its own pace. */
574
+ const moved = (id) => {
575
+ changed.push(id);
576
+ revs.set(id, ++revision);
577
+ };
578
+ const now = Date.now();
579
+ // What the dom reads, per node. Recounted rather than kept: a binding's sources change
580
+ // with every run, and a stale count is worse than none.
581
+ const bindings = new Map();
582
+ const drives = new Map();
583
+ const seen = new Map();
584
+ for (const internal of internals) {
585
+ const target = targets.get(internal);
586
+ const element = boundTo.get(internal);
587
+ for (const source of internal.sources ?? []) {
588
+ const id = ids.get(source);
589
+ if (id === undefined)
590
+ continue;
591
+ bindings.set(id, (bindings.get(id) ?? 0) + 1);
592
+ if (element) {
593
+ const found = element.deref();
594
+ // First writer wins: an element driven by two values answers with the one that
595
+ // reached it first, and the rest are still reachable from the node itself.
596
+ if (found && !drivenBy.has(found))
597
+ drivenBy.set(found, id);
598
+ const held = seen.get(id);
599
+ if (held)
600
+ held.push(element);
601
+ else
602
+ seen.set(id, [element]);
603
+ }
604
+ if (!target)
605
+ continue;
606
+ const list = drives.get(id);
607
+ if (list)
608
+ list.push(target);
609
+ else
610
+ drives.set(id, [target]);
611
+ }
612
+ }
613
+ reaches = seen;
614
+ for (const node of graph.values()) {
615
+ const count = bindings.get(node.id) ?? 0;
616
+ if (node.bindings !== count)
617
+ node.bindings = count || undefined;
618
+ const list = drives.get(node.id);
619
+ node.targets = list?.length ? list : undefined;
620
+ }
621
+ for (const node of graph.values()) {
622
+ if (!node.read) {
623
+ edges(node.id, node);
624
+ // A node absent from the table is one this call is seeing for the first time. It
625
+ // counts as changed, or an effect would be created into a delta nobody sends.
626
+ const seen = runsAtLastSample.get(node.id);
627
+ if (seen === undefined || node.runs > seen) {
628
+ node.lastPulse = now;
629
+ moved(node.id);
630
+ }
631
+ runsAtLastSample.set(node.id, node.runs);
632
+ continue;
633
+ }
634
+ const raw = node.read();
635
+ // For the next write to diff against; without it the first change has no entries.
636
+ if (recording)
637
+ raws.set(node.id, raw);
638
+ // After the read: a memo that has never been pulled has no sources yet, and reading
639
+ // it is what makes it collect them.
640
+ edges(node.id, node);
641
+ // A memo holding something the runtime stamped a component name on is the wrapper
642
+ // the compiler put round that component, so it can say what the author wrote.
643
+ const stamped = raw?.$name;
644
+ if (node.kind === 'memo' && typeof stamped === 'string' && stamped && claims(node, raw)) {
645
+ // A control-flow tag stamps its name the same way, so the name decides what this
646
+ // is. Without it `Show` and `Match` are drawn as components the author wrote.
647
+ node.kind = KIND_OF_BUILTIN[stamped] ?? 'component';
648
+ node.declaredAt = componentSource(stamped);
649
+ node.source ??= node.declaredAt;
650
+ // Loaded as its own chunk. Worth saying: it explains a tag that only turns up after
651
+ // a navigation, and a suspense boundary that had something to wait for.
652
+ if (raw.$lazy)
653
+ node.lazy = true;
654
+ if (unnamed.has(node.id)) {
655
+ node.label = stamped;
656
+ unnamed.delete(node.id);
657
+ }
658
+ }
659
+ // Whatever it returned, for a panel asking where this is on the page. Read every
660
+ // sample because a tag can re-render into different elements.
661
+ if (TREE_KINDS.has(node.kind)) {
662
+ const found = elementsIn(raw);
663
+ if (found.length)
664
+ rendered.set(node.id, found.map((el) => new WeakRef(el)));
665
+ const given = handed.get(node.id);
666
+ if (given)
667
+ node.props = readProps(given);
668
+ }
669
+ // Formatted once: the card shows the head of it, the tooltip and the inspector the
670
+ // rest. Reading the value twice would be reading a proxy twice.
671
+ const detail = formatValue(raw);
672
+ const next = shorten(detail);
673
+ if (next !== node.value || detail !== node.detail) {
674
+ node.detail = detail;
675
+ node.value = next;
676
+ // A signal never recomputes, so a write is the only thing it can count.
677
+ if (node.kind === 'state')
678
+ node.runs++;
679
+ node.lastPulse = now;
680
+ moved(node.id);
681
+ }
682
+ runsAtLastSample.set(node.id, node.runs);
683
+ }
684
+ // After the promotion above, so a plugin's answer survives being named.
685
+ //
686
+ // Two limits, both because a kind decides more than a colour. Only a plain component
687
+ // is sharpened, so a Router that provides a context is still a Router. And only into
688
+ // another tag: every other kind is outside TREE_KINDS, which is what puts a node in
689
+ // the tree and lets it own the elements it rendered, so a component turned into a
690
+ // state would quietly take its whole subtree with it.
691
+ for (const [id, kind] of retyped) {
692
+ const node = graph.get(id);
693
+ if (node?.kind === 'component' && TREE_KINDS.has(kind))
694
+ node.kind = kind;
695
+ }
696
+ // When something moved, and when a tag is still unnumbered. The first samples run
697
+ // before the elements are on the page, so without the second test the numbering only
698
+ // appeared after the first edit and the layout shifted under the user at that point.
699
+ // A still page with everything numbered does none of this.
700
+ const unnumbered = () => {
701
+ for (const node of graph.values()) {
702
+ if (TREE_KINDS.has(node.kind) && node.order === undefined)
703
+ return true;
704
+ }
705
+ return false;
706
+ };
707
+ if (changed.length || unnumbered())
708
+ stampDocumentOrder(graph, (of) => api.renderedFor(of));
709
+ return changed;
710
+ },
711
+ get revision() {
712
+ return revision;
713
+ },
714
+ takeRemoved() {
715
+ const out = removed.filter((entry) => entry.rev > drained).map((entry) => entry.id);
716
+ drained = revision;
717
+ return out;
718
+ },
719
+ changedSince(rev) {
720
+ const out = [];
721
+ for (const [id, at] of revs)
722
+ if (at > rev)
723
+ out.push(id);
724
+ return out;
725
+ },
726
+ removedSince(rev) {
727
+ return removed.filter((entry) => entry.rev > rev).map((entry) => entry.id);
728
+ },
729
+ outrun(rev) {
730
+ return removed.length >= REMOVED_LOG && rev < removed[0].rev;
731
+ },
732
+ elementsFor(id) {
733
+ const live = [];
734
+ const collect = (from, where = reaches) => {
735
+ for (const ref of where.get(from) ?? []) {
736
+ const found = ref.deref();
737
+ // Still in the document: an element detached since the last sample is not
738
+ // somewhere anything can be shown.
739
+ if (found?.isConnected && !live.includes(found))
740
+ live.push(found);
741
+ }
742
+ };
743
+ // What the tag itself rendered, which is exact.
744
+ collect(id, rendered);
745
+ if (live.length)
746
+ return live;
747
+ collect(id);
748
+ // A value drives the element its binding was recorded against. A tag that returned
749
+ // nothing usable falls back to where the values inside it reach.
750
+ if (!live.length) {
751
+ const inside = (node) => {
752
+ const seen = new Set();
753
+ let at = node.createdIn;
754
+ while (at !== undefined && !seen.has(at)) {
755
+ if (at === id)
756
+ return true;
757
+ seen.add(at);
758
+ at = graph.get(at)?.createdIn;
759
+ }
760
+ return false;
761
+ };
762
+ for (const node of graph.values())
763
+ if (inside(node))
764
+ collect(node.id);
765
+ }
766
+ return live;
767
+ },
768
+ setValue(id, next) {
769
+ const node = graph.get(id);
770
+ const write = setters.get(id);
771
+ if (!node || node.kind !== 'state' || !write)
772
+ return false;
773
+ write(next);
774
+ return true;
775
+ },
776
+ renderedFor(id) {
777
+ const live = [];
778
+ for (const ref of rendered.get(id) ?? []) {
779
+ const found = ref.deref();
780
+ if (found?.isConnected && !live.includes(found))
781
+ live.push(found);
782
+ }
783
+ return live;
784
+ },
785
+ domFor(id) {
786
+ // Built once per sample. Every tag needs the same element maps, and a page redraws
787
+ // this many times a second.
788
+ described ??= describeAll(graph, (of) => api.renderedFor(of), (of) => api.elementsFor(of), TREE_KINDS, (element) => elementExtra.get(element));
789
+ return described.get(id) ?? [];
790
+ },
791
+ nodeAt(element) {
792
+ // What rendered it, before what writes into it.
793
+ //
794
+ // `drivenBy` answers with the value that reached an element first, which for a row in
795
+ // a keyed list is the `For` that put it there rather than the component the row is.
796
+ // Picking a TodoItem, or anything inside one, therefore selected `For` every time.
797
+ //
798
+ // Several tags can claim one element, since a component returning another component's
799
+ // output shares it. The innermost is the one a pointer is over, so depth in the
800
+ // component nesting decides, the same way the dom description does.
801
+ const depth = tagDepths(graph, TREE_KINDS);
802
+ const rendering = new Map();
803
+ for (const node of graph.values()) {
804
+ if (!TREE_KINDS.has(node.kind))
805
+ continue;
806
+ for (const found of api.renderedFor(node.id)) {
807
+ const held = rendering.get(found);
808
+ if (held === undefined || (depth.get(node.id) ?? 0) > (depth.get(held) ?? 0)) {
809
+ rendering.set(found, node.id);
810
+ }
811
+ }
812
+ }
813
+ for (let at = element; at; at = at.parentElement) {
814
+ const owner = rendering.get(at);
815
+ if (owner !== undefined && graph.has(owner))
816
+ return owner;
817
+ }
818
+ // Nothing rendered it, so fall back to whatever writes into it.
819
+ for (let at = element; at; at = at.parentElement) {
820
+ const id = drivenBy.get(at);
821
+ if (id !== undefined && graph.has(id))
822
+ return id;
823
+ }
824
+ return undefined;
825
+ },
826
+ takeEvents() {
827
+ return history.take();
828
+ },
829
+ detach() {
830
+ stop();
831
+ internals.clear();
832
+ graph.clear();
833
+ cores.clear();
834
+ rendered.clear();
835
+ setters.clear();
836
+ runsAtLastSample.clear();
837
+ unnamed.clear();
838
+ raws.clear();
839
+ history.clear();
840
+ },
841
+ };
842
+ // Started last, so a plugin adding a node finds the graph ready.
843
+ for (const plugin of devtoolsPlugins()) {
844
+ if (!plugin.collect)
845
+ continue;
846
+ const context = {
847
+ onDirective: (fn) => onDirective.push(fn),
848
+ onContext: (fn) => onContext.push(fn),
849
+ annotate: (id, data) => {
850
+ const node = graph.get(id);
851
+ if (node)
852
+ (node.extra ??= {})[plugin.name] = data;
853
+ },
854
+ annotation: (id) => graph.get(id)?.extra?.[plugin.name],
855
+ // Remembered, not applied here. A component is still a memo when its context is
856
+ // provided, and the promotion that gives it its name only runs while it is one.
857
+ retype: (id, kind) => retyped.set(id, kind),
858
+ annotateElement: (target, data) => {
859
+ if (!target || typeof target !== 'object')
860
+ return;
861
+ const held = elementExtra.get(target) ?? {};
862
+ held[plugin.name] = data;
863
+ elementExtra.set(target, held);
864
+ },
865
+ elementAnnotation: (target) => target && typeof target === 'object' ? elementExtra.get(target)?.[plugin.name] : undefined,
866
+ add: (spec) => {
867
+ const id = seq++;
868
+ graph.set(id, {
869
+ id,
870
+ label: spec.label,
871
+ kind: spec.kind,
872
+ deps: new Set(spec.deps ?? []),
873
+ writes: new Set(),
874
+ runs: 0,
875
+ lastPulse: 0,
876
+ authored: spec.authored ?? false,
877
+ ...(spec.createdIn !== undefined ? { createdIn: spec.createdIn } : {}),
878
+ ...(spec.value !== undefined ? { value: spec.value } : {}),
879
+ ...(spec.detail !== undefined ? { detail: spec.detail } : {}),
880
+ ...(spec.source ? { source: spec.source } : {}),
881
+ });
882
+ return id;
883
+ },
884
+ };
885
+ try {
886
+ plugin.collect(context);
887
+ }
888
+ catch {
889
+ /* ignored */
890
+ }
891
+ }
892
+ return api;
893
+ }