@fluixi/devtools 0.2.0-alpha.1 → 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 +10 -3
@@ -0,0 +1,217 @@
1
+ // One commit, with room to read it.
2
+ //
3
+ // A column beside the graph is the wrong shape for six before-and-after objects, so the full
4
+ // view floats over the page, moved and resized by whoever is reading it.
5
+ //
6
+ // It does not block: the graph keeps updating behind it and stays clickable.
7
+ import { changeRows, handleCopyClick, CHANGE_CSS } from './change-view.js';
8
+ const escape = (text) => text.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
9
+ const clock = (t) => {
10
+ const d = new Date(t);
11
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
12
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
13
+ };
14
+ const VERBS = {
15
+ write: 'written',
16
+ ran: 'ran',
17
+ created: 'made',
18
+ disposed: 'gone',
19
+ };
20
+ /** So it cannot be resized into a sliver. */
21
+ const MIN_W = 320;
22
+ const MIN_H = 200;
23
+ /**
24
+ * Pointer capture, for the reason the splitters need it: without it the events stop arriving
25
+ * when the pointer crosses the graph or an iframe, the release is never seen, and the panel
26
+ * keeps following the mouse.
27
+ */
28
+ function draggable(handle, onMove) {
29
+ handle.addEventListener('pointerdown', (event) => {
30
+ if (event.button !== 0)
31
+ return;
32
+ // Capturing here retargets the following click onto the bar, so the button never hears
33
+ // it. That is why the close button did nothing.
34
+ if (event.target.closest('button'))
35
+ return;
36
+ event.preventDefault();
37
+ event.stopPropagation();
38
+ handle.setPointerCapture(event.pointerId);
39
+ let lastX = event.clientX;
40
+ let lastY = event.clientY;
41
+ const move = (e) => {
42
+ onMove(e.clientX - lastX, e.clientY - lastY);
43
+ lastX = e.clientX;
44
+ lastY = e.clientY;
45
+ };
46
+ const stop = () => {
47
+ handle.removeEventListener('pointermove', move);
48
+ handle.removeEventListener('pointerup', stop);
49
+ handle.removeEventListener('pointercancel', stop);
50
+ handle.removeEventListener('lostpointercapture', stop);
51
+ if (handle.hasPointerCapture(event.pointerId))
52
+ handle.releasePointerCapture(event.pointerId);
53
+ };
54
+ handle.addEventListener('pointermove', move);
55
+ handle.addEventListener('pointerup', stop);
56
+ handle.addEventListener('pointercancel', stop);
57
+ handle.addEventListener('lostpointercapture', stop);
58
+ });
59
+ }
60
+ export function createDetailPanel(host, options = {}) {
61
+ const panel = host.ownerDocument.createElement('div');
62
+ panel.className = 'dp-panel';
63
+ panel.hidden = true;
64
+ panel.innerHTML =
65
+ '<div class="dp-bar"><span class="dp-title"></span>' +
66
+ '<button type="button" class="dp-close" title="Close" aria-label="Close">✕</button></div>' +
67
+ '<div class="dp-body"></div>' +
68
+ '<div class="dp-grip" title="Drag to resize"></div>';
69
+ host.appendChild(panel);
70
+ const title = panel.querySelector('.dp-title');
71
+ const body = panel.querySelector('.dp-body');
72
+ const bar = panel.querySelector('.dp-bar');
73
+ const grip = panel.querySelector('.dp-grip');
74
+ let x = 0;
75
+ let y = 0;
76
+ let width = 520;
77
+ let height = 380;
78
+ let open = false;
79
+ const place = () => {
80
+ panel.style.left = `${Math.round(x)}px`;
81
+ panel.style.top = `${Math.round(y)}px`;
82
+ panel.style.width = `${Math.round(width)}px`;
83
+ panel.style.height = `${Math.round(height)}px`;
84
+ };
85
+ draggable(bar, (dx, dy) => {
86
+ // Clamped, or a panel dragged off the top has no bar left to drag back by.
87
+ const view = panel.ownerDocument.defaultView;
88
+ x = Math.max(0, Math.min((view?.innerWidth ?? width) - 60, x + dx));
89
+ y = Math.max(0, Math.min((view?.innerHeight ?? height) - 30, y + dy));
90
+ place();
91
+ });
92
+ draggable(grip, (dx, dy) => {
93
+ width = Math.max(MIN_W, width + dx);
94
+ height = Math.max(MIN_H, height + dy);
95
+ place();
96
+ });
97
+ panel.querySelector('.dp-close')?.addEventListener('click', () => close());
98
+ body.addEventListener('click', (e) => {
99
+ // Copy first: the button sits inside a row.
100
+ if (handleCopyClick(e))
101
+ return;
102
+ const row = e.target.closest('.dp-node');
103
+ if (row?.dataset.id)
104
+ options.onPick?.(Number(row.dataset.id));
105
+ });
106
+ function close() {
107
+ open = false;
108
+ panel.hidden = true;
109
+ }
110
+ return {
111
+ open(commit, graph) {
112
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
113
+ const cause = commit.cause;
114
+ title.textContent = cause
115
+ ? `${nameOf(cause)} · ${clock(commit.t)}`
116
+ : `${commit.commit === 0 ? 'first render' : `commit ${commit.commit}`} · ${clock(commit.t)}`;
117
+ const headline = cause && cause.from !== undefined && cause.to !== undefined
118
+ ? `<div class="dp-change"><span class="ch-from">${escape(cause.from)}</span>` +
119
+ `<span class="ch-arrow">→</span><span class="ch-to">${escape(cause.to)}</span></div>`
120
+ : '';
121
+ // Every event: this view exists because the row could not hold them all.
122
+ const groups = ['write', 'ran', 'created', 'disposed']
123
+ .map((kind) => {
124
+ const rows = commit.events.filter((e) => e.kind === kind && e !== cause);
125
+ if (!rows.length)
126
+ return '';
127
+ const seen = new Map();
128
+ for (const event of rows) {
129
+ const found = seen.get(event.id);
130
+ if (found)
131
+ found.count++;
132
+ else
133
+ seen.set(event.id, { name: nameOf(event), count: 1 });
134
+ }
135
+ const items = [...seen]
136
+ .map(([id, { name, count }]) => `<button type="button" class="dp-node" data-id="${id}">${escape(name)}${count > 1 ? ` ×${count}` : ''}</button>`)
137
+ .join('');
138
+ return `<div class="dp-group"><div class="dp-verb">${VERBS[kind]}</div><div class="dp-nodes">${items}</div></div>`;
139
+ })
140
+ .join('');
141
+ // Where the value reaches the page — the last step of the chain.
142
+ const node = cause ? graph.get(cause.id) : undefined;
143
+ // Both: a template's holes can share one original position, and then only the compiled
144
+ // line tells them apart.
145
+ const where = (t) => {
146
+ if (!t.source)
147
+ return '';
148
+ const file = (t.source.file ?? '').split('/').pop() ?? '';
149
+ const out = t.source.compiled;
150
+ const tail = out && out.line !== t.source.line ? ` · out ${out.line}` : '';
151
+ return ` — ${file}:${t.source.line ?? ''}${tail}`;
152
+ };
153
+ const targets = node?.targets?.length
154
+ ? `<div class="dp-group"><div class="dp-verb">updates</div><div class="dp-targets">${node.targets
155
+ .map((t) => `<div>${escape(t.name ? `${t.name} of ${t.element}` : t.element)}` +
156
+ `<span class="dp-where">${escape(where(t))}</span></div>`)
157
+ .join('')}</div></div>`
158
+ : '';
159
+ body.innerHTML = headline + changeRows(cause?.change) + groups + targets;
160
+ if (!open) {
161
+ // Once per opening: jumping back to the middle on every entry would be unusable.
162
+ const view = panel.ownerDocument.defaultView;
163
+ x = Math.max(12, ((view?.innerWidth ?? 900) - width) / 2);
164
+ y = Math.max(12, ((view?.innerHeight ?? 600) - height) / 3);
165
+ place();
166
+ }
167
+ open = true;
168
+ panel.hidden = false;
169
+ },
170
+ close,
171
+ get isOpen() {
172
+ return open;
173
+ },
174
+ destroy() {
175
+ panel.remove();
176
+ },
177
+ };
178
+ }
179
+ export const DETAIL_PANEL_CSS = `
180
+ .dp-panel {
181
+ position: fixed; z-index: 40; display: flex; flex-direction: column;
182
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 8px;
183
+ background: var(--fx-panel, #14161c); color: var(--fx-label, #e7e9ee);
184
+ box-shadow: 0 12px 40px rgba(0, 0, 0, .45); overflow: hidden;
185
+ }
186
+ .dp-panel[hidden] { display: none; }
187
+ .dp-bar {
188
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;
189
+ cursor: move; user-select: none;
190
+ border-bottom: 1px solid var(--fx-line, #ffffff1f);
191
+ }
192
+ .dp-title { font-family: ui-monospace, monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
193
+ .dp-close {
194
+ margin-left: auto; width: 22px; height: 20px; padding: 0; cursor: pointer; font: inherit; font-size: 11px;
195
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
196
+ background: transparent; color: var(--fx-muted, #8a90a2);
197
+ }
198
+ .dp-close:hover { color: var(--fx-label, #e7e9ee); }
199
+ .dp-body { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; }
200
+ .dp-change { font-family: ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }
201
+ .dp-group { margin-top: 10px; }
202
+ .dp-verb { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--fx-muted, #8a90a2); }
203
+ .dp-nodes { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
204
+ .dp-node {
205
+ padding: 2px 7px; cursor: pointer; font: inherit; font-family: ui-monospace, monospace; font-size: 11px;
206
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 999px;
207
+ background: transparent; color: var(--fx-label, #e7e9ee);
208
+ }
209
+ .dp-node:hover { border-color: var(--fx-accent, #5eead4); }
210
+ .dp-targets { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: var(--fx-label, #e7e9ee); }
211
+ .dp-where { color: var(--fx-muted, #8a90a2); }
212
+ /* Bottom-right, where every desktop puts it. */
213
+ .dp-grip {
214
+ position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize;
215
+ background: linear-gradient(135deg, transparent 50%, var(--fx-line, #ffffff40) 50%);
216
+ }
217
+ ${CHANGE_CSS}`;
package/dist/diff.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export type ChangeOp = 'added' | 'removed' | 'changed';
2
+ export interface ChangeEntry {
3
+ /** An array index as a string, or an object key. */
4
+ readonly key: string;
5
+ readonly op: ChangeOp;
6
+ /** Formatted, and short: a diff is a list, not a document. */
7
+ readonly from?: string;
8
+ readonly to?: string;
9
+ /**
10
+ * The two sides as JSON, for showing an object as an object and for copying it. Per entry,
11
+ * so the cost is one row rather than the whole value. Absent for a cycle, a function, a dom
12
+ * node, or anything over the cap.
13
+ */
14
+ readonly fromJson?: string;
15
+ readonly toJson?: string;
16
+ }
17
+ export interface ValueChange {
18
+ /** How to read `entries`: positions for an array, keys for an object, neither for a value. */
19
+ readonly shape: 'value' | 'array' | 'object';
20
+ readonly entries?: ChangeEntry[];
21
+ /** Set when the diff had to stop early, so a reader knows the list is not the whole of it. */
22
+ readonly more?: number;
23
+ /** Array only, and worth saying on its own: a list that grew or shrank. */
24
+ readonly length?: {
25
+ readonly from: number;
26
+ readonly to: number;
27
+ };
28
+ }
29
+ /** A plain value is its own diff: the entry already carries both sides. */
30
+ export declare function diffValues(before: unknown, after: unknown): ValueChange | undefined;
31
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACjC,8FAA8F;IAC9F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAyHD,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAUnF"}
package/dist/diff.js ADDED
@@ -0,0 +1,132 @@
1
+ // What changed, rather than what the value is now.
2
+ //
3
+ // `todos [{…}] → [{…}, {…}]` says nothing the graph did not. "One row added at index 2, here
4
+ // it is" does. Computed at the write, while both sides are still in hand — afterwards only
5
+ // the new value exists.
6
+ import { formatValue } from './format.js';
7
+ /** Enough to see what happened, few enough that a write stays cheap. */
8
+ const MAX_ENTRIES = 24;
9
+ /** Per side of one entry. A row of a list, not the whole row's contents. */
10
+ const ENTRY_VALUE = 120;
11
+ /** Past this an entry is a document rather than a row, and not worth carrying per write. */
12
+ const ENTRY_JSON = 4000;
13
+ const show = (value) => formatValue(value, ENTRY_VALUE);
14
+ /** Structure only: `show` already gives the whole of a number, and a second copy of `3` helps nobody. */
15
+ function json(value) {
16
+ if (value === null || typeof value !== 'object')
17
+ return undefined;
18
+ try {
19
+ const text = JSON.stringify(value);
20
+ return text && text.length <= ENTRY_JSON ? text : undefined;
21
+ }
22
+ catch {
23
+ // Cyclic, or a getter that threw. The formatted string still says what it is.
24
+ return undefined;
25
+ }
26
+ }
27
+ /** One side of an entry: what to read, and what to copy. */
28
+ const side = (value, key) => {
29
+ const text = json(value);
30
+ return { [key]: show(value), ...(text ? { [`${key}Json`]: text } : {}) };
31
+ };
32
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ /**
34
+ * Reference first: a store hands back the same proxy for an unchanged row, so an append costs
35
+ * one comparison each. The shallow fallback catches rows rebuilt with the same contents.
36
+ */
37
+ function same(a, b) {
38
+ if (Object.is(a, b))
39
+ return true;
40
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
41
+ return false;
42
+ if (Array.isArray(a) !== Array.isArray(b))
43
+ return false;
44
+ const ka = Object.keys(a);
45
+ const kb = Object.keys(b);
46
+ if (ka.length !== kb.length)
47
+ return false;
48
+ return ka.every((k) => Object.is(a[k], b[k]));
49
+ }
50
+ /**
51
+ * Ends trimmed first. Index-against-index reports every row after an insertion as changed,
52
+ * which is true of the positions and useless to whoever added one row.
53
+ */
54
+ function diffArray(before, after) {
55
+ let head = 0;
56
+ while (head < before.length && head < after.length && same(before[head], after[head]))
57
+ head++;
58
+ let tail = 0;
59
+ while (tail < before.length - head &&
60
+ tail < after.length - head &&
61
+ same(before[before.length - 1 - tail], after[after.length - 1 - tail])) {
62
+ tail++;
63
+ }
64
+ const entries = [];
65
+ let skipped = 0;
66
+ const push = (entry) => {
67
+ if (entries.length < MAX_ENTRIES)
68
+ entries.push(entry);
69
+ else
70
+ skipped++;
71
+ };
72
+ const removedCount = before.length - head - tail;
73
+ const addedCount = after.length - head - tail;
74
+ const overlap = Math.min(removedCount, addedCount);
75
+ // The overlap is a replacement in place; the remainder is the insertion or deletion.
76
+ for (let i = 0; i < overlap; i++) {
77
+ push({
78
+ key: String(head + i),
79
+ op: 'changed',
80
+ ...side(before[head + i], 'from'),
81
+ ...side(after[head + i], 'to'),
82
+ });
83
+ }
84
+ for (let i = overlap; i < addedCount; i++) {
85
+ push({ key: String(head + i), op: 'added', ...side(after[head + i], 'to') });
86
+ }
87
+ for (let i = overlap; i < removedCount; i++) {
88
+ push({ key: String(head + i), op: 'removed', ...side(before[head + i], 'from') });
89
+ }
90
+ return {
91
+ shape: 'array',
92
+ entries,
93
+ ...(skipped ? { more: skipped } : {}),
94
+ ...(before.length !== after.length ? { length: { from: before.length, to: after.length } } : {}),
95
+ };
96
+ }
97
+ function diffObject(before, after) {
98
+ const entries = [];
99
+ let skipped = 0;
100
+ const push = (entry) => {
101
+ if (entries.length < MAX_ENTRIES)
102
+ entries.push(entry);
103
+ else
104
+ skipped++;
105
+ };
106
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
107
+ const had = key in before;
108
+ const has = key in after;
109
+ if (had && has) {
110
+ if (!same(before[key], after[key])) {
111
+ push({ key, op: 'changed', ...side(before[key], 'from'), ...side(after[key], 'to') });
112
+ }
113
+ }
114
+ else if (has)
115
+ push({ key, op: 'added', ...side(after[key], 'to') });
116
+ else
117
+ push({ key, op: 'removed', ...side(before[key], 'from') });
118
+ }
119
+ return { shape: 'object', entries, ...(skipped ? { more: skipped } : {}) };
120
+ }
121
+ /** A plain value is its own diff: the entry already carries both sides. */
122
+ export function diffValues(before, after) {
123
+ if (Array.isArray(before) && Array.isArray(after)) {
124
+ const change = diffArray(before, after);
125
+ return change.entries?.length || change.length ? change : undefined;
126
+ }
127
+ if (isPlainObject(before) && isPlainObject(after)) {
128
+ const change = diffObject(before, after);
129
+ return change.entries?.length ? change : undefined;
130
+ }
131
+ return undefined;
132
+ }
package/dist/dom.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A serialisable description of the DOM a tag rendered.
3
+ *
4
+ * The chrome panel reads the graph through `eval` and the editor panel over http, so neither
5
+ * can hold Elements. This is the same tree with the elements reduced to labels.
6
+ *
7
+ * The walk stops at any element that is another tag's root and records that tag's id in
8
+ * `owner`. That keeps the result a tree of the application rather than the whole document,
9
+ * and lets the consumer nest the child tag's own DOM under its own row.
10
+ */
11
+ import type { RNode } from './instrument.js';
12
+ export interface DomNode {
13
+ /** The element as a selector: `div#app.card`. */
14
+ label: string;
15
+ children: DomNode[];
16
+ /** The node whose root this element is, when it is one. */
17
+ owner?: number;
18
+ /**
19
+ * Ids of the values that write into this element. A consumer outside the page has no
20
+ * elements to compare, so it cannot work this out for itself.
21
+ */
22
+ writes?: number[];
23
+ /**
24
+ * Tags that render inside this element without producing one of their own, such as a boundary
25
+ * like Suspense, or a component whose body is another component. They sit here rather than
26
+ * beside the element, which is where the nesting actually puts them.
27
+ */
28
+ inside?: number[];
29
+ /**
30
+ * The element the application was mounted into. It is nobody's output, having been on the
31
+ * page first, so it belongs above the root tag rather than under it.
32
+ */
33
+ container?: boolean;
34
+ /**
35
+ * Where the element was written. Stamped on it by the runtime when the compiler listed
36
+ * the positions of a template's elements; absent in a build without source locations.
37
+ */
38
+ at?: {
39
+ file: string;
40
+ line: number;
41
+ column: number;
42
+ };
43
+ /** What plugins attached to this element, by plugin name. */
44
+ extra?: Record<string, unknown>;
45
+ /** Set only in-page. A description that crossed a wire has no element. */
46
+ element?: Element;
47
+ }
48
+ /** Elements described per tag. Caps how large a snapshot can get. */
49
+ export declare const DOM_BUDGET = 200;
50
+ /**
51
+ * Static attributes only: a class computed at runtime is not in the source.
52
+ *
53
+ * Written out rather than spread-and-join. This runs once per element of the page on every
54
+ * sample, and building three throwaway arrays to read at most two class names cost more
55
+ * than everything else the walk does.
56
+ */
57
+ export declare function describeElement(element: Element): string;
58
+ /**
59
+ * Describe what every tag rendered, in one pass.
60
+ *
61
+ * Per-tag would rebuild the element maps each time, which is enough work to stall a render
62
+ * loop on a page of any size.
63
+ */
64
+ export declare function describeAll(graph: Map<number, RNode>,
65
+ /** What a tag returned. Must not fall back to anything, or two tags claim one element. */
66
+ roots: (of: number) => readonly Element[],
67
+ /** Where a value writes. */
68
+ writtenBy: (of: number) => readonly Element[], kinds: ReadonlySet<string>,
69
+ /** What plugins attached to an element, when any did. */
70
+ extraFor?: (element: Element) => Record<string, unknown> | undefined): Map<number, DomNode[]>;
71
+ /** How deep a tag sits in the component nesting, following what created it. */
72
+ export declare function tagDepths(graph: Map<number, RNode>, kinds: ReadonlySet<string>): Map<number, number>;
73
+ /**
74
+ * Drop the Elements so the result can be serialised.
75
+ *
76
+ * Everything else is copied as-is rather than field by field. The copy that describes and the
77
+ * copy that strips are built separately, since an extension bundles its own, so a list of fields
78
+ * here goes stale against a description made elsewhere, and the field just disappears.
79
+ */
80
+ export declare function withoutElements(nodes: readonly DomNode[]): DomNode[];
81
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,OAAO;IACtB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,EAAE,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAUD,qEAAqE;AACrE,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CASxD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC;AACzB,0FAA0F;AAC1F,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,SAAS,OAAO,EAAE;AACzC,4BAA4B;AAC5B,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,SAAS,OAAO,EAAE,EAC7C,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC;AAC1B,yDAAyD;AACzD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACnE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAwExB;AAwDD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAcpG;AAuED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAKpE"}