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

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 (96) hide show
  1. package/README.md +80 -2
  2. package/dist/callsite.d.ts +22 -0
  3. package/dist/callsite.d.ts.map +1 -0
  4. package/dist/callsite.js +82 -0
  5. package/dist/change-view.d.ts +10 -0
  6. package/dist/change-view.d.ts.map +1 -0
  7. package/dist/change-view.js +102 -0
  8. package/dist/copy.d.ts +9 -0
  9. package/dist/copy.d.ts.map +1 -0
  10. package/dist/copy.js +52 -0
  11. package/dist/describe.d.ts +3 -0
  12. package/dist/describe.d.ts.map +1 -0
  13. package/dist/describe.js +47 -0
  14. package/dist/detail-panel.d.ts +17 -0
  15. package/dist/detail-panel.d.ts.map +1 -0
  16. package/dist/detail-panel.js +217 -0
  17. package/dist/diff.d.ts +31 -0
  18. package/dist/diff.d.ts.map +1 -0
  19. package/dist/diff.js +132 -0
  20. package/dist/dom.d.ts +81 -0
  21. package/dist/dom.d.ts.map +1 -0
  22. package/dist/dom.js +269 -0
  23. package/dist/export.d.ts +55 -0
  24. package/dist/export.d.ts.map +1 -0
  25. package/dist/export.js +90 -0
  26. package/dist/float.d.ts +54 -0
  27. package/dist/float.d.ts.map +1 -0
  28. package/dist/float.js +256 -0
  29. package/dist/format.d.ts +16 -0
  30. package/dist/format.d.ts.map +1 -0
  31. package/dist/format.js +188 -0
  32. package/dist/graph-view.d.ts +79 -2
  33. package/dist/graph-view.d.ts.map +1 -1
  34. package/dist/graph-view.js +866 -50
  35. package/dist/highlight.d.ts +56 -0
  36. package/dist/highlight.d.ts.map +1 -0
  37. package/dist/highlight.js +154 -0
  38. package/dist/history.d.ts +59 -0
  39. package/dist/history.d.ts.map +1 -0
  40. package/dist/history.js +85 -0
  41. package/dist/hook.d.ts +60 -0
  42. package/dist/hook.d.ts.map +1 -0
  43. package/dist/hook.js +142 -0
  44. package/dist/index.d.ts +44 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +25 -1
  47. package/dist/inspector.d.ts +60 -0
  48. package/dist/inspector.d.ts.map +1 -0
  49. package/dist/inspector.js +415 -0
  50. package/dist/instrument.d.ts +147 -5
  51. package/dist/instrument.d.ts.map +1 -1
  52. package/dist/instrument.js +164 -44
  53. package/dist/kind-filter.d.ts +39 -0
  54. package/dist/kind-filter.d.ts.map +1 -0
  55. package/dist/kind-filter.js +151 -0
  56. package/dist/menu.d.ts +37 -0
  57. package/dist/menu.d.ts.map +1 -0
  58. package/dist/menu.js +125 -0
  59. package/dist/observe.d.ts +80 -0
  60. package/dist/observe.d.ts.map +1 -0
  61. package/dist/observe.js +893 -0
  62. package/dist/palette.d.ts +11 -0
  63. package/dist/palette.d.ts.map +1 -0
  64. package/dist/palette.js +27 -0
  65. package/dist/path.d.ts +19 -0
  66. package/dist/path.d.ts.map +1 -0
  67. package/dist/path.js +49 -0
  68. package/dist/plugin.d.ts +121 -0
  69. package/dist/plugin.d.ts.map +1 -0
  70. package/dist/plugin.js +74 -0
  71. package/dist/plugins/context.d.ts +10 -0
  72. package/dist/plugins/context.d.ts.map +1 -0
  73. package/dist/plugins/context.js +80 -0
  74. package/dist/plugins/directives.d.ts +25 -0
  75. package/dist/plugins/directives.d.ts.map +1 -0
  76. package/dist/plugins/directives.js +78 -0
  77. package/dist/plugins/index.d.ts +7 -0
  78. package/dist/plugins/index.d.ts.map +1 -0
  79. package/dist/plugins/index.js +9 -0
  80. package/dist/source.d.ts +72 -0
  81. package/dist/source.d.ts.map +1 -0
  82. package/dist/source.js +153 -0
  83. package/dist/timeline.d.ts +30 -0
  84. package/dist/timeline.d.ts.map +1 -0
  85. package/dist/timeline.js +271 -0
  86. package/dist/tree.d.ts +128 -0
  87. package/dist/tree.d.ts.map +1 -0
  88. package/dist/tree.js +645 -0
  89. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  90. package/dist/value-pane.d.ts +10 -0
  91. package/dist/value-pane.d.ts.map +1 -0
  92. package/dist/value-pane.js +61 -0
  93. package/dist/wire.d.ts +100 -0
  94. package/dist/wire.d.ts.map +1 -0
  95. package/dist/wire.js +93 -0
  96. package/package.json +7 -2
package/dist/source.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Where a reactive node was created.
3
+ *
4
+ * The compiler marks each creation site in development (`sourceLocations`) by calling the
5
+ * global installed here, just before the statement that makes the node. The instrumented
6
+ * primitive takes the mark and keeps it on the graph node.
7
+ *
8
+ * A single slot, not a stack: in `signal(compute(signal(1)))` the inner call takes the mark
9
+ * and the outer gets none, rather than claiming a location that is not its own.
10
+ */
11
+ /** Kept in step with SOURCE_MARKER and COMPONENT_MARKER in the compiler. */
12
+ const MARKER = '__fx_source';
13
+ const COMPONENT_MARKER = '__fx_component';
14
+ const HOLE_MARKER = '__fx_hole';
15
+ const PROPS_MARKER = '__fx_props';
16
+ const MARKS = '__FLUIXI_DEVTOOLS_MARKS__';
17
+ const host = globalThis;
18
+ const shared = host[MARKS] ?? {
19
+ pending: null,
20
+ expiring: false,
21
+ components: new Map(),
22
+ };
23
+ host[MARKS] = shared;
24
+ /**
25
+ * Where each component was declared, by name.
26
+ *
27
+ * The graph sees the memo the compiler wraps a component in, and all that memo carries back
28
+ * is the name stamped on what the component returned — so name is the only key available.
29
+ * Two components sharing a name are recorded as ambiguous and reported as unknown, since a
30
+ * location pointing at the wrong file is worse than none.
31
+ */
32
+ /**
33
+ * Record where the next node is about to be created. The compiler calls this; call it
34
+ * yourself only for a node the compiler cannot see, immediately before the primitive.
35
+ */
36
+ export function markReactiveSource(file, line, column, name) {
37
+ const location = locate(file, line, column);
38
+ // With dependencies marked, `signal()` in the framework marks its own `createSignal` line
39
+ // in between the application's mark and the node it asked for. The call site is the useful
40
+ // location, so a dependency never takes a mark the project has already left unclaimed.
41
+ if (shared.pending && location.external && !shared.pending.location?.external)
42
+ return;
43
+ shared.pending = { name, location };
44
+ shared.expiring = false;
45
+ }
46
+ /**
47
+ * A project file is named relative to the project root, so anything absolute or under
48
+ * node_modules came from a dependency. Derived here rather than sent, so the mark the
49
+ * compiler emits stays the four arguments it has always been.
50
+ */
51
+ function locate(file, line, column) {
52
+ const external = !!file && (/^([a-zA-Z]:)?[\\/]/.test(file) || file.includes('node_modules/'));
53
+ return external ? { file, line, column, external } : { file, line, column };
54
+ }
55
+ /** Record where a component was declared. The compiler calls this. */
56
+ export function markComponentSource(name, file, line, column) {
57
+ const seen = shared.components.get(name);
58
+ const next = locate(file, line, column);
59
+ if (seen === undefined) {
60
+ shared.components.set(name, next);
61
+ return;
62
+ }
63
+ if (!seen || seen.file !== file || seen.line !== line)
64
+ shared.components.set(name, null);
65
+ }
66
+ /** Where a component of this name was declared, when exactly one was. */
67
+ export function componentSource(name) {
68
+ return shared.components.get(name) ?? undefined;
69
+ }
70
+ /**
71
+ * Reading keeps it for the rest of the turn.
72
+ *
73
+ * One statement can make several nodes: `createStore({ a, b })` builds a signal per key
74
+ * inside the framework, and all of them belong to the line that asked for the store. So the
75
+ * mark survives the read and is dropped on the next microtask, which is the end of the
76
+ * statement that left it as far as anything synchronous is concerned.
77
+ */
78
+ export function takeReactiveSource() {
79
+ const mark = shared.pending;
80
+ if (mark) {
81
+ // The variable name belongs to the node the author was declaring, not to the ones the
82
+ // framework built underneath it — `cart`, then two unnamed signals at the same line.
83
+ if (mark.name !== undefined)
84
+ shared.pending = { location: mark.location };
85
+ if (!shared.expiring) {
86
+ shared.expiring = true;
87
+ queueMicrotask(() => {
88
+ shared.expiring = false;
89
+ shared.pending = null;
90
+ });
91
+ }
92
+ }
93
+ return mark;
94
+ }
95
+ /** Never replaces an existing marker: two copies of devtools would each hold half the marks. */
96
+ /**
97
+ * Where each hole was written, by the accessor the compiler stamped. Keyed by the function,
98
+ * not a slot: it is built and read at different moments.
99
+ */
100
+ const HOLES = new WeakMap();
101
+ /**
102
+ * Also written onto the accessor itself, because the runtime does not always hand the same
103
+ * object on to `insert`: hydration wraps an accessor so the cursor can be replaced when it
104
+ * is finally read. The wrapper copies this across, and the WeakMap alone would miss it.
105
+ */
106
+ const HOLE_TAG = '__fx_at';
107
+ /** Returns the accessor, not a wrapper: it is the binding's value. */
108
+ function markHole(accessor, line, column, name) {
109
+ if (accessor && (typeof accessor === 'object' || typeof accessor === 'function')) {
110
+ const at = name === undefined ? { line, column } : { line, column, name };
111
+ HOLES.set(accessor, at);
112
+ try {
113
+ Object.defineProperty(accessor, HOLE_TAG, { value: at, configurable: true });
114
+ }
115
+ catch {
116
+ // Frozen or a proxy that refuses writes; the WeakMap still covers the unwrapped case.
117
+ }
118
+ }
119
+ // The template is being built, so whatever declaration left a mark is finished with. A
120
+ // mark outlives the statement that set it on purpose — a store's key signals are made
121
+ // inside the call and belong to its line — but a component body is one long synchronous
122
+ // run, and without this every node the template makes claimed the last signal's position.
123
+ shared.pending = null;
124
+ return accessor;
125
+ }
126
+ /** Where each prop of a component tag was written, by the props object it was passed in. */
127
+ const PROPS = new WeakMap();
128
+ /** Returns the props object untouched: it is what the component is about to be handed. */
129
+ function markProps(props, at) {
130
+ if (props && typeof props === 'object')
131
+ PROPS.set(props, at);
132
+ return props;
133
+ }
134
+ /** Where the props of this object were written, when the compiler recorded them. */
135
+ export function propsLocations(props) {
136
+ return props && typeof props === 'object' ? PROPS.get(props) : undefined;
137
+ }
138
+ /** The hole an accessor came from, when the compiler marked it. */
139
+ export function holeLocation(accessor) {
140
+ if (!accessor || (typeof accessor !== 'object' && typeof accessor !== 'function'))
141
+ return undefined;
142
+ return HOLES.get(accessor) ?? accessor[HOLE_TAG];
143
+ }
144
+ export function installSourceMarker() {
145
+ if (typeof host[MARKER] !== 'function')
146
+ host[MARKER] = markReactiveSource;
147
+ if (typeof host[COMPONENT_MARKER] !== 'function')
148
+ host[COMPONENT_MARKER] = markComponentSource;
149
+ if (typeof host[HOLE_MARKER] !== 'function')
150
+ host[HOLE_MARKER] = markHole;
151
+ if (typeof host[PROPS_MARKER] !== 'function')
152
+ host[PROPS_MARKER] = markProps;
153
+ }
@@ -0,0 +1,30 @@
1
+ import { type Commit, type GraphEvent } from './history.js';
2
+ import type { RNode } from './instrument.js';
3
+ export interface Timeline {
4
+ /** Fold in what a snapshot carried. Events already seen are ignored, so polling is safe. */
5
+ push(events: readonly GraphEvent[]): void;
6
+ /** Stop taking new events, or start again. Paused, what is on screen holds still. */
7
+ setPaused(paused: boolean): void;
8
+ readonly paused: boolean;
9
+ /** Draw. `graph` supplies the labels, which the events only carry ids for. */
10
+ render(graph: Map<number, RNode>): void;
11
+ /** Node ids in the selected commit, for a view to highlight. Empty when none is selected. */
12
+ selected(): number[];
13
+ clear(): void;
14
+ }
15
+ export interface TimelineOptions {
16
+ /** What to say before anything has happened. */
17
+ empty?: string;
18
+ /** How many commits to keep. */
19
+ limit?: number;
20
+ /** A commit was selected, or deselected with null. */
21
+ onSelect?(ids: number[], commit: Commit | null): void;
22
+ /** A side panel can say six rows changed; it cannot show six before-and-after objects. */
23
+ onExpand?(commit: Commit): void;
24
+ }
25
+ /** The graph first, since a node can be renamed; then the name the event carried. */
26
+ export type NameOf = (event: GraphEvent) => string;
27
+ export declare function createTimeline(host: HTMLElement, options?: TimelineOptions): Timeline;
28
+ /** Default styling for what createTimeline writes, in the shape DEVTOOLS_CSS is used. */
29
+ export declare const TIMELINE_CSS = "\n.tl-head { position: relative; display: flex; gap: 4px; padding: 0 .25rem; }\n.tl-pause {\n position: absolute; right: .4rem; top: 40%; transform: translateY(-50%);\n flex: none; width: 26px; cursor: pointer; font: inherit; font-size: 10px;\n border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;\n background: transparent; color: var(--fx-muted, #8a90a2);\n}\n.tl-pause:hover { color: var(--fx-label, #e7e9ee); }\n.tl-pause.on { color: var(--fx-accent, #5eead4); border-color: var(--fx-accent, #5eead4); }\n.tl-filter {\n flex: 1; min-width: 0; box-sizing: border-box; padding: 3px 6px;\n font: inherit; font-size: 11.5px; border-radius: 4px;\n border: 1px solid var(--fx-line, #ffffff1f);\n background: var(--fx-input, transparent); color: var(--fx-label, #e7e9ee);\n padding-inline-end: 2rem;\n}\n.tl-empty { color: var(--fx-muted, #8a90a2); font-size: 12px; }\n.tl-commit { padding: 4px 6px; border-radius: 4px; cursor: pointer; font-size: 11.5px; }\n.tl-commit:hover { background: var(--fx-hover, #ffffff10); }\n.tl-commit.selected { background: var(--fx-selected, #ffffff1a); }\n.tl-line { display: flex; align-items: baseline; gap: 6px; }\n.tl-caret { color: var(--fx-muted, #8a90a2); font-size: 9px; flex: none; }\n.tl-time { font-family: ui-monospace, monospace; font-size: 10px; color: var(--fx-muted, #8a90a2); flex: none; }\n.tl-name { color: var(--fx-label, #e7e9ee); font-family: ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.tl-did { margin-left: auto; color: var(--fx-muted, #8a90a2); flex: none; }\n.tl-body { margin: 3px 0 2px 15px; }\n/* Wraps: an open entry exists to show the whole value. */\n.tl-change {\n font-family: ui-monospace, monospace; font-size: 11px;\n color: var(--fx-accent, #5eead4); overflow-wrap: anywhere; white-space: pre-wrap;\n}\n.tl-part { margin-top: 2px; color: var(--fx-label, #e7e9ee); font-size: 11px; overflow-wrap: anywhere; }\n.tl-verb { color: var(--fx-muted, #8a90a2); }\n.tl-more {\n margin-top: 5px; padding: 2px 8px; font: inherit; font-size: 10.5px; cursor: pointer;\n border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;\n background: transparent; color: var(--fx-muted, #8a90a2);\n}\n.tl-more:hover { color: var(--fx-label, #e7e9ee); }\n\n.ch-list { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; }\n.ch-size { color: var(--fx-muted, #8a90a2); margin-bottom: 2px; }\n.ch-row { display: flex; gap: 6px; align-items: baseline; padding: 1px 0; }\n.ch-row.ch-stacked { align-items: flex-start; padding: 3px 0; }\n.ch-sign { flex: none; width: 9px; text-align: center; }\n.ch-added .ch-sign { color: var(--state, #34d399); }\n.ch-removed .ch-sign { color: var(--fx-write, #fb7185); }\n.ch-changed .ch-sign { color: var(--effect, #fbbf24); }\n.ch-key { flex: none; color: var(--fx-muted, #8a90a2); }\n.ch-sides { min-width: 0; flex: 1; overflow-wrap: anywhere; }\n.ch-stacked .ch-sides { display: flex; flex-direction: column; gap: 2px; }\n.ch-from { color: var(--fx-muted, #8a90a2); }\n.ch-row:not(.ch-stacked) .ch-from { text-decoration: line-through; }\n.ch-arrow { color: var(--fx-muted, #8a90a2); margin: 0 4px; }\n.ch-to { color: var(--fx-label, #e7e9ee); }\n.ch-more { color: var(--fx-muted, #8a90a2); margin-top: 2px; }\n/* In the corner: an object can be tall, and a button under it is a scroll away. */\n.ch-block { position: relative; border-radius: 4px; border: 1px solid var(--fx-line, #ffffff14); }\n.ch-block.ch-from { opacity: .72; }\n.ch-json { margin: 0; padding: 5px 7px; overflow-x: auto; white-space: pre; font: inherit; }\n.ch-copy {\n position: absolute; top: 3px; right: 3px; padding: 1px 6px; cursor: pointer;\n font: inherit; font-size: 9.5px; opacity: 0; transition: opacity .12s;\n border: 1px solid var(--fx-line, #ffffff26); border-radius: 3px;\n background: var(--fx-panel, #14161c); color: var(--fx-muted, #8a90a2);\n}\n.ch-block:hover .ch-copy, .ch-copy:focus { opacity: 1; }\n.ch-copy:hover { color: var(--fx-label, #e7e9ee); }\n";
30
+ //# sourceMappingURL=timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAOA,OAAO,EAAa,KAAK,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,QAAQ;IACvB,4FAA4F;IAC5F,IAAI,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAAC;IAC1C,qFAAqF;IACrF,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,8EAA8E;IAC9E,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC;IACxC,6FAA6F;IAC7F,QAAQ,IAAI,MAAM,EAAE,CAAC;IACrB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,QAAQ,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACtD,0FAA0F;IAC1F,QAAQ,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAWD,qFAAqF;AACrF,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;AA6CnD,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,eAAoB,GAAG,QAAQ,CAwLzF;AAED,yFAAyF;AACzF,eAAO,MAAM,YAAY,o8HAwCX,CAAC"}
@@ -0,0 +1,271 @@
1
+ // The history, as something to read.
2
+ //
3
+ // Commits, not a flat log: one click makes a write, four runs and a disposal, interleaved with
4
+ // whatever the router was doing. Grouped, they read as "this is what that click did".
5
+ //
6
+ // Newest first, and re-rendered only when it would say something different — a panel that
7
+ // repaints on every poll cannot be scrolled or selected.
8
+ import { toCommits } from './history.js';
9
+ import { changeRows, handleCopyClick, CHANGE_CSS } from './change-view.js';
10
+ const escape = (text) => text.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
11
+ const clock = (t) => {
12
+ const d = new Date(t);
13
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
14
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
15
+ };
16
+ /** The name of what the commit started from: `count`, or what to say when there is none. */
17
+ function subject(commit, nameOf) {
18
+ if (!commit.cause)
19
+ return commit.commit === 0 ? 'first render' : 'continued';
20
+ return nameOf(commit.cause);
21
+ }
22
+ /** `0 → 1`, when the write carried both sides of it. */
23
+ function change(commit) {
24
+ const cause = commit.cause;
25
+ if (!cause || cause.from === undefined || cause.to === undefined)
26
+ return '';
27
+ return `${cause.from} → ${cause.to}`;
28
+ }
29
+ /** The whole of it, for the tooltip: the row elides, this does not. */
30
+ function detail(commit, nameOf) {
31
+ const lines = [`${subject(commit, nameOf)} ${change(commit)}`.trim()];
32
+ const named = (kind, verb) => {
33
+ const ids = commit.events.filter((e) => e.kind === kind && e !== commit.cause).map(nameOf);
34
+ if (ids.length)
35
+ lines.push(`${verb}: ${[...new Set(ids)].join(', ')}`);
36
+ };
37
+ named('ran', 'ran');
38
+ named('write', 'written');
39
+ named('created', 'made');
40
+ named('disposed', 'gone');
41
+ return lines.join('\n');
42
+ }
43
+ /** "2 ran · 1 created" — what the commit caused, without listing every line of it. */
44
+ function summary(commit) {
45
+ const counts = { ran: 0, created: 0, disposed: 0, write: 0 };
46
+ for (const event of commit.events)
47
+ counts[event.kind]++;
48
+ // The opening write is the title, so it is not also a thing the commit did.
49
+ const writes = counts.write - (commit.cause ? 1 : 0);
50
+ return [
51
+ counts.ran && `${counts.ran} ran`,
52
+ writes > 0 && `${writes} written`,
53
+ counts.created && `${counts.created} made`,
54
+ counts.disposed && `${counts.disposed} gone`,
55
+ ]
56
+ .filter(Boolean)
57
+ .join(' · ');
58
+ }
59
+ export function createTimeline(host, options = {}) {
60
+ const empty = options.empty ?? 'nothing has happened yet';
61
+ const limit = options.limit ?? 100;
62
+ let events = [];
63
+ /** The highest seq folded in. Every producer counts up, so this is all dedupe needs. */
64
+ let seen = -1;
65
+ /** Selected, expanded and lit on the graph are one state: all three say "this one". */
66
+ let selected = null;
67
+ let filter = '';
68
+ let signature = '';
69
+ /**
70
+ * Held still on request.
71
+ *
72
+ * A page that writes on every frame fills this faster than anyone can read it, and the
73
+ * entry being looked at scrolls away. Dropped rather than buffered: catching up on a
74
+ * thousand events nobody watched is not what the button is for.
75
+ */
76
+ let paused = false;
77
+ // Built once: re-rendering the box being typed in would take the caret with it.
78
+ host.innerHTML =
79
+ '<div class="tl-head">' +
80
+ '<input class="tl-filter" type="search" placeholder="filter by name or value" aria-label="Filter the timeline" />' +
81
+ '<button type="button" class="tl-pause" title="Stop taking new updates, so the list holds still while you read it">❚❚</button>' +
82
+ '</div>' +
83
+ '<div class="tl-list"></div>';
84
+ const input = host.querySelector('.tl-filter');
85
+ const list = host.querySelector('.tl-list');
86
+ const pause = host.querySelector('.tl-pause');
87
+ const all = () => toCommits(events).slice(-limit).reverse();
88
+ const idsOf = (commit) => {
89
+ const ids = new Set();
90
+ for (const event of commit.events) {
91
+ ids.add(event.id);
92
+ if (event.by !== undefined)
93
+ ids.add(event.by);
94
+ }
95
+ return [...ids];
96
+ };
97
+ /** Everything the row says, which is what a reader is searching against. */
98
+ const haystack = (commit, nameOf) => `${detail(commit, nameOf)} ${summary(commit)}`.toLowerCase();
99
+ const shown = (nameOf) => filter ? all().filter((c) => haystack(c, nameOf).includes(filter)) : all();
100
+ input.addEventListener('input', () => {
101
+ filter = input.value.trim().toLowerCase();
102
+ signature = '';
103
+ });
104
+ const showPaused = () => {
105
+ pause.textContent = paused ? '▶' : '❚❚';
106
+ pause.classList.toggle('on', paused);
107
+ pause.title = paused
108
+ ? 'Taking updates again'
109
+ : 'Stop taking new updates, so the list holds still while you read it';
110
+ };
111
+ pause.addEventListener('click', () => {
112
+ paused = !paused;
113
+ showPaused();
114
+ signature = '';
115
+ });
116
+ list.addEventListener('click', (e) => {
117
+ // Copying from an open row must not also shut it.
118
+ if (handleCopyClick(e))
119
+ return;
120
+ const row = e.target?.closest?.('.tl-commit');
121
+ if (!row)
122
+ return;
123
+ const number = Number(row.dataset.commit);
124
+ // Inside the row, so it would otherwise shut what it was opening.
125
+ if (e.target.classList.contains('tl-more')) {
126
+ const full = all().find((c) => c.commit === number);
127
+ if (full)
128
+ options.onExpand?.(full);
129
+ return;
130
+ }
131
+ // Clicking the open one shuts it — the only way back to watching everything.
132
+ selected = selected === number ? null : number;
133
+ const commit = all().find((c) => c.commit === selected) ?? null;
134
+ options.onSelect?.(commit ? idsOf(commit) : [], commit);
135
+ signature = '';
136
+ });
137
+ return {
138
+ setPaused(next) {
139
+ paused = next;
140
+ showPaused();
141
+ signature = '';
142
+ },
143
+ get paused() {
144
+ return paused;
145
+ },
146
+ push(incoming) {
147
+ if (paused)
148
+ return;
149
+ let added = false;
150
+ for (const event of incoming) {
151
+ if (event.seq <= seen)
152
+ continue;
153
+ events.push(event);
154
+ seen = event.seq;
155
+ added = true;
156
+ }
157
+ if (!added)
158
+ return;
159
+ // Bounded by commits: cutting mid-commit shows a click with half of what it caused.
160
+ if (events.length > limit * 40)
161
+ events = events.slice(-limit * 40);
162
+ },
163
+ render(graph) {
164
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
165
+ const rows = shown(nameOf);
166
+ const next = `${seen}|${selected ?? ''}|${filter}|${rows.length}|${graph.size}`;
167
+ if (next === signature)
168
+ return;
169
+ signature = next;
170
+ if (!rows.length) {
171
+ // With a filter typed, "nothing has happened" would read as recording being broken.
172
+ list.innerHTML = `<span class="tl-empty">${escape(filter ? 'nothing matches' : empty)}</span>`;
173
+ return;
174
+ }
175
+ list.innerHTML = rows
176
+ .map((commit) => {
177
+ const open = commit.commit === selected;
178
+ const did = summary(commit);
179
+ const moved = change(commit);
180
+ const parts = [
181
+ ['ran', 'ran'],
182
+ ['write', 'written'],
183
+ ['created', 'made'],
184
+ ['disposed', 'gone'],
185
+ ];
186
+ const body = parts
187
+ .map(([kind, verb]) => {
188
+ const names = [
189
+ ...new Set(commit.events.filter((e) => e.kind === kind && e !== commit.cause).map(nameOf)),
190
+ ];
191
+ return names.length
192
+ ? `<div class="tl-part"><span class="tl-verb">${verb}</span> ${escape(names.join(', '))}</div>`
193
+ : '';
194
+ })
195
+ .join('');
196
+ return (`<div class="tl-commit${open ? ' selected' : ''}"` +
197
+ ` data-commit="${commit.commit}" title="${escape(detail(commit, nameOf))}">` +
198
+ '<div class="tl-line">' +
199
+ `<span class="tl-caret">${open ? '▾' : '▸'}</span>` +
200
+ `<span class="tl-time">${clock(commit.t)}</span>` +
201
+ `<span class="tl-name">${escape(subject(commit, nameOf))}</span>` +
202
+ (did ? `<span class="tl-did">${escape(did)}</span>` : '') +
203
+ '</div>' +
204
+ // Open, the value gets the width and wraps.
205
+ (open
206
+ ? `<div class="tl-body">${moved ? `<div class="tl-change">${escape(moved)}</div>` : ''}${changeRows(commit.cause?.change, 6)}${body}${
207
+ // Only worth offering when there is more than the row can hold.
208
+ options.onExpand && (commit.cause?.change || commit.events.length > 6)
209
+ ? '<button type="button" class="tl-more">open in full</button>'
210
+ : ''}</div>`
211
+ : '') +
212
+ '</div>');
213
+ })
214
+ .join('');
215
+ },
216
+ selected() {
217
+ const commit = all().find((c) => c.commit === selected);
218
+ return commit ? idsOf(commit) : [];
219
+ },
220
+ clear() {
221
+ events = [];
222
+ seen = -1;
223
+ selected = null;
224
+ signature = '';
225
+ // The filter box stays: a new run is usually a rerun of what they were narrowing to.
226
+ list.innerHTML = `<span class="tl-empty">${escape(empty)}</span>`;
227
+ },
228
+ };
229
+ }
230
+ /** Default styling for what createTimeline writes, in the shape DEVTOOLS_CSS is used. */
231
+ export const TIMELINE_CSS = `
232
+ .tl-head { position: relative; display: flex; gap: 4px; padding: 0 .25rem; }
233
+ .tl-pause {
234
+ position: absolute; right: .4rem; top: 40%; transform: translateY(-50%);
235
+ flex: none; width: 26px; cursor: pointer; font: inherit; font-size: 10px;
236
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
237
+ background: transparent; color: var(--fx-muted, #8a90a2);
238
+ }
239
+ .tl-pause:hover { color: var(--fx-label, #e7e9ee); }
240
+ .tl-pause.on { color: var(--fx-accent, #5eead4); border-color: var(--fx-accent, #5eead4); }
241
+ .tl-filter {
242
+ flex: 1; min-width: 0; box-sizing: border-box; padding: 3px 6px;
243
+ font: inherit; font-size: 11.5px; border-radius: 4px;
244
+ border: 1px solid var(--fx-line, #ffffff1f);
245
+ background: var(--fx-input, transparent); color: var(--fx-label, #e7e9ee);
246
+ padding-inline-end: 2rem;
247
+ }
248
+ .tl-empty { color: var(--fx-muted, #8a90a2); font-size: 12px; }
249
+ .tl-commit { padding: 4px 6px; border-radius: 4px; cursor: pointer; font-size: 11.5px; }
250
+ .tl-commit:hover { background: var(--fx-hover, #ffffff10); }
251
+ .tl-commit.selected { background: var(--fx-selected, #ffffff1a); }
252
+ .tl-line { display: flex; align-items: baseline; gap: 6px; }
253
+ .tl-caret { color: var(--fx-muted, #8a90a2); font-size: 9px; flex: none; }
254
+ .tl-time { font-family: ui-monospace, monospace; font-size: 10px; color: var(--fx-muted, #8a90a2); flex: none; }
255
+ .tl-name { color: var(--fx-label, #e7e9ee); font-family: ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
256
+ .tl-did { margin-left: auto; color: var(--fx-muted, #8a90a2); flex: none; }
257
+ .tl-body { margin: 3px 0 2px 15px; }
258
+ /* Wraps: an open entry exists to show the whole value. */
259
+ .tl-change {
260
+ font-family: ui-monospace, monospace; font-size: 11px;
261
+ color: var(--fx-accent, #5eead4); overflow-wrap: anywhere; white-space: pre-wrap;
262
+ }
263
+ .tl-part { margin-top: 2px; color: var(--fx-label, #e7e9ee); font-size: 11px; overflow-wrap: anywhere; }
264
+ .tl-verb { color: var(--fx-muted, #8a90a2); }
265
+ .tl-more {
266
+ margin-top: 5px; padding: 2px 8px; font: inherit; font-size: 10.5px; cursor: pointer;
267
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
268
+ background: transparent; color: var(--fx-muted, #8a90a2);
269
+ }
270
+ .tl-more:hover { color: var(--fx-label, #e7e9ee); }
271
+ ${CHANGE_CSS}`;
package/dist/tree.d.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * What the page is made of, as a tree.
3
+ *
4
+ * The graph answers which value depends on which. It does not answer what the page is built
5
+ * from, and that is the question a tree is for: `<Router>` holding a `<Counter>` and an
6
+ * `<Another>`, each openable to see what it holds.
7
+ *
8
+ * Both are drawn from the same nodes. A component's place in the tree is `createdIn`, the
9
+ * node whose run made it, lifted through everything that is not itself a tag, since the
10
+ * compiler wraps each component in a memo and that memo is not something anyone wrote.
11
+ */
12
+ import type { NodeKind, RNode } from './instrument.js';
13
+ import type { DomNode } from './dom.js';
14
+ /** Kinds that are tags in the source, and so have a place in a tree of them. */
15
+ export declare const TREE_KINDS: ReadonlySet<NodeKind>;
16
+ export interface TreeNode {
17
+ id: number;
18
+ label: string;
19
+ kind: NodeKind;
20
+ children: TreeNode[];
21
+ /** A value the author declared rather than a tag they wrote. */
22
+ value?: boolean;
23
+ /** Came down as its own chunk. */
24
+ lazy?: boolean;
25
+ /** What a plugin wants said beside the label. */
26
+ badge?: string;
27
+ }
28
+ /**
29
+ * An element row, for a host that wants to say something about one.
30
+ *
31
+ * An element is not a node, so it has no id to select. What it does have is the tag it
32
+ * belongs to and the values writing into it, which between them are what there is to say.
33
+ */
34
+ export interface ElementPick {
35
+ /** The row key, which doubles as a path to the element. */
36
+ key: string;
37
+ /** The element as a selector: `div#app.card`. */
38
+ label: string;
39
+ owner?: number;
40
+ writes?: number[];
41
+ /** Where the element itself was written, when the build recorded it. */
42
+ at?: {
43
+ file: string;
44
+ line: number;
45
+ column: number;
46
+ };
47
+ /** What plugins attached to this element. */
48
+ extra?: Record<string, unknown>;
49
+ /** Only in a host that is in the page. */
50
+ element?: Element;
51
+ }
52
+ /** Whatever the host can say about the dom under a tag. Elements only when it has them. */
53
+ export type { DomNode } from './dom.js';
54
+ /**
55
+ * The tags in `graph`, each under the tag that made it.
56
+ *
57
+ * Ordered by id, which is creation order, so siblings read down the page the way they were
58
+ * rendered. A node whose ancestors are all untagged is a root.
59
+ */
60
+ export interface TreeOptions {
61
+ /**
62
+ * Also list the reactive values each tag made: the signals and memos the author wrote
63
+ * inside it. Machinery is left out: a node the framework named itself does not count, so
64
+ * a component shows `count` and not the memo it was wrapped in.
65
+ *
66
+ * Only an explicit `authored: false` is machinery. A graph read from the source says
67
+ * nothing either way, and everything in it was written by hand.
68
+ */
69
+ values?: boolean;
70
+ /**
71
+ * Keep only what this names, and whatever holds it. An ancestor is kept even when its own
72
+ * name does not match, since dropping it would leave the match with nothing above it.
73
+ */
74
+ filter?: string;
75
+ }
76
+ /**
77
+ * Put the tags that rendered back into page order, leaving everything else untouched.
78
+ *
79
+ * Sorting by id alone reads as creation order, which is wrong for a keyed list: rebuilding a
80
+ * row hands the replacement the highest id, so the row you edited jumped to the bottom.
81
+ *
82
+ * Only the slots those tags already occupy are reshuffled. Sorting the whole list by `order`
83
+ * instead sent every value to the end of its band, so editing a todo moved the `left` memo
84
+ * as well, which had nothing to do with the change.
85
+ */
86
+ export declare function inPageOrder<T extends {
87
+ order?: number;
88
+ }>(nodes: T[]): T[];
89
+ export declare function componentTree(graph: Map<number, RNode>, options?: TreeOptions): TreeNode[];
90
+ /** Every id in the tree, for a caller that wants to open or shut all of it. */
91
+ export declare function idsIn(nodes: readonly TreeNode[]): number[];
92
+ export interface ComponentTreeOptions {
93
+ /** Clicking a row. The host decides what selecting means, usually the inspector. */
94
+ onSelect?: (id: number) => void;
95
+ /**
96
+ * Clicking an element row. Elements have no id, so a host that leaves this out gets no
97
+ * selection for them at all, which is what made half the tree look inert.
98
+ */
99
+ onPick?: (pick: ElementPick) => void;
100
+ /** Double-clicking a row, for a host that can open the file it came from. */
101
+ onReveal?: (id: number) => void;
102
+ /** Double-clicking an element row. */
103
+ onRevealPick?: (pick: ElementPick) => void;
104
+ /**
105
+ * The row under the pointer, and null on the way out. For a host that can reach the page
106
+ * and show where the node writes. An element row gives the element itself, which needs no
107
+ * looking up.
108
+ */
109
+ onHover?: (id: number | null, element?: Element, path?: string) => void;
110
+ /**
111
+ * What each tag rendered. A host in the page walks its own elements; one reading a
112
+ * snapshot passes what the producer described. Omitted draws tags only.
113
+ */
114
+ domFor?: (id: number) => readonly DomNode[];
115
+ /** What to say when nothing has rendered yet. */
116
+ empty?: string;
117
+ /** Start with the values shown. @default false */
118
+ values?: boolean;
119
+ }
120
+ export interface ComponentTree {
121
+ /** Draw `graph`, with `selected` marked. Cheap to call every frame. */
122
+ render(graph: Map<number, RNode>, selected?: number | null): void;
123
+ clear(): void;
124
+ destroy(): void;
125
+ }
126
+ export declare function createComponentTree(host: HTMLElement, options?: ComponentTreeOptions): ComponentTree;
127
+ export declare const TREE_CSS: string;
128
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"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"}