@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.
- package/README.md +80 -2
- package/dist/callsite.cjs +72 -0
- package/dist/callsite.d.ts +22 -0
- package/dist/callsite.d.ts.map +1 -0
- package/dist/callsite.js +82 -0
- package/dist/callsite.mjs +51 -0
- package/dist/change-view.cjs +134 -0
- package/dist/change-view.d.ts +10 -0
- package/dist/change-view.d.ts.map +1 -0
- package/dist/change-view.js +102 -0
- package/dist/change-view.mjs +111 -0
- package/dist/copy.cjs +63 -0
- package/dist/copy.d.ts +9 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +52 -0
- package/dist/copy.mjs +42 -0
- package/dist/describe.cjs +55 -0
- package/dist/describe.d.ts +3 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +47 -0
- package/dist/describe.mjs +34 -0
- package/dist/detail-panel.cjs +307 -0
- package/dist/detail-panel.d.ts +17 -0
- package/dist/detail-panel.d.ts.map +1 -0
- package/dist/detail-panel.js +217 -0
- package/dist/detail-panel.mjs +284 -0
- package/dist/diff.cjs +225 -0
- package/dist/diff.d.ts +31 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +132 -0
- package/dist/diff.mjs +202 -0
- package/dist/dom.cjs +204 -0
- package/dist/dom.d.ts +81 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +269 -0
- package/dist/dom.mjs +183 -0
- package/dist/export.cjs +221 -0
- package/dist/export.d.ts +55 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +90 -0
- package/dist/export.mjs +209 -0
- package/dist/float.cjs +232 -0
- package/dist/float.d.ts +54 -0
- package/dist/float.d.ts.map +1 -0
- package/dist/float.js +256 -0
- package/dist/float.mjs +211 -0
- package/dist/format.cjs +175 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +188 -0
- package/dist/format.mjs +154 -0
- package/dist/graph-view.cjs +893 -0
- package/dist/graph-view.d.ts +79 -2
- package/dist/graph-view.d.ts.map +1 -1
- package/dist/graph-view.js +866 -50
- package/dist/graph-view.mjs +870 -0
- package/dist/highlight.cjs +140 -0
- package/dist/highlight.d.ts +56 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +154 -0
- package/dist/highlight.mjs +119 -0
- package/dist/history.cjs +95 -0
- package/dist/history.d.ts +59 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +85 -0
- package/dist/history.mjs +74 -0
- package/dist/hook.cjs +1562 -0
- package/dist/hook.d.ts +60 -0
- package/dist/hook.d.ts.map +1 -0
- package/dist/hook.js +142 -0
- package/dist/hook.mjs +1553 -0
- package/dist/index.cjs +4552 -0
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.mjs +4544 -0
- package/dist/inspector.cjs +688 -0
- package/dist/inspector.d.ts +60 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/inspector.js +415 -0
- package/dist/inspector.mjs +676 -0
- package/dist/instrument.cjs +497 -0
- package/dist/instrument.d.ts +147 -5
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +164 -44
- package/dist/instrument.mjs +487 -0
- package/dist/kind-filter.cjs +172 -0
- package/dist/kind-filter.d.ts +39 -0
- package/dist/kind-filter.d.ts.map +1 -0
- package/dist/kind-filter.js +151 -0
- package/dist/kind-filter.mjs +151 -0
- package/dist/menu.cjs +141 -0
- package/dist/menu.d.ts +37 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +125 -0
- package/dist/menu.mjs +120 -0
- package/dist/observe.cjs +1352 -0
- package/dist/observe.d.ts +80 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +893 -0
- package/dist/observe.mjs +1345 -0
- package/dist/palette.cjs +52 -0
- package/dist/palette.d.ts +11 -0
- package/dist/palette.d.ts.map +1 -0
- package/dist/palette.js +27 -0
- package/dist/palette.mjs +31 -0
- package/dist/path.cjs +45 -0
- package/dist/path.d.ts +19 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +49 -0
- package/dist/path.mjs +24 -0
- package/dist/plugin.cjs +87 -0
- package/dist/plugin.d.ts +121 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +74 -0
- package/dist/plugin.mjs +66 -0
- package/dist/plugins/context.cjs +98 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +80 -0
- package/dist/plugins/context.mjs +75 -0
- package/dist/plugins/directives.cjs +98 -0
- package/dist/plugins/directives.d.ts +25 -0
- package/dist/plugins/directives.d.ts.map +1 -0
- package/dist/plugins/directives.js +78 -0
- package/dist/plugins/directives.mjs +75 -0
- package/dist/plugins/index.cjs +162 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +9 -0
- package/dist/plugins/index.mjs +140 -0
- package/dist/source.cjs +111 -0
- package/dist/source.d.ts +72 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +153 -0
- package/dist/source.mjs +90 -0
- package/dist/timeline.cjs +350 -0
- package/dist/timeline.d.ts +30 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +271 -0
- package/dist/timeline.mjs +327 -0
- package/dist/tree.cjs +574 -0
- package/dist/tree.d.ts +128 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +645 -0
- package/dist/tree.mjs +551 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/value-pane.cjs +223 -0
- package/dist/value-pane.d.ts +10 -0
- package/dist/value-pane.d.ts.map +1 -0
- package/dist/value-pane.js +61 -0
- package/dist/value-pane.mjs +200 -0
- package/dist/wire.cjs +109 -0
- package/dist/wire.d.ts +100 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +93 -0
- package/dist/wire.mjs +89 -0
- package/package.json +17 -5
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/copy.ts
|
|
2
|
+
function confirmOn(button, said = "copied") {
|
|
3
|
+
if (!button) return;
|
|
4
|
+
const was = button.textContent;
|
|
5
|
+
button.textContent = said;
|
|
6
|
+
setTimeout(() => {
|
|
7
|
+
button.textContent = was;
|
|
8
|
+
}, 1200);
|
|
9
|
+
}
|
|
10
|
+
function fallback(text, button) {
|
|
11
|
+
try {
|
|
12
|
+
const doc = button?.ownerDocument ?? document;
|
|
13
|
+
const area = doc.createElement("textarea");
|
|
14
|
+
area.value = text;
|
|
15
|
+
area.setAttribute("readonly", "");
|
|
16
|
+
area.style.position = "fixed";
|
|
17
|
+
area.style.opacity = "0";
|
|
18
|
+
doc.body.appendChild(area);
|
|
19
|
+
area.select();
|
|
20
|
+
doc.execCommand("copy");
|
|
21
|
+
area.remove();
|
|
22
|
+
confirmOn(button);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function copyText(text, button) {
|
|
29
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
30
|
+
if (clipboard?.writeText) {
|
|
31
|
+
try {
|
|
32
|
+
await clipboard.writeText(text);
|
|
33
|
+
confirmOn(button);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return fallback(text, button);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/change-view.ts
|
|
42
|
+
var escape = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
43
|
+
var SIGN = { added: "+", removed: "−", changed: "~" };
|
|
44
|
+
function pretty(json) {
|
|
45
|
+
if (!json) return void 0;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.stringify(JSON.parse(json), null, 2);
|
|
48
|
+
} catch {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function sideOf(text, json, cls) {
|
|
53
|
+
const block = pretty(json);
|
|
54
|
+
if (!block) return `<span class="ch-${cls}">${escape(text ?? "")}</span>`;
|
|
55
|
+
return `<div class="ch-block ch-${cls}"><button type="button" class="ch-copy" title="Copy" data-copy="${escape(block)}">copy</button><pre class="ch-json">${escape(block)}</pre></div>`;
|
|
56
|
+
}
|
|
57
|
+
function changeRows(change, limit = Infinity) {
|
|
58
|
+
if (!change?.entries?.length) return "";
|
|
59
|
+
const shown = change.entries.slice(0, limit);
|
|
60
|
+
const hidden = change.entries.length - shown.length + (change.more ?? 0);
|
|
61
|
+
const rows = shown.map((entry) => {
|
|
62
|
+
const from = sideOf(entry.from, entry.fromJson, "from");
|
|
63
|
+
const to = sideOf(entry.to, entry.toJson, "to");
|
|
64
|
+
const sides = entry.op === "changed" ? `${from}<span class="ch-arrow">→</span>${to}` : entry.op === "added" ? to : from;
|
|
65
|
+
const stacked = entry.fromJson || entry.toJson ? " ch-stacked" : "";
|
|
66
|
+
return `<div class="ch-row ch-${entry.op}${stacked}"><span class="ch-sign">${SIGN[entry.op] ?? "·"}</span><span class="ch-key">${escape(entry.key)}</span><span class="ch-sides">${sides}</span></div>`;
|
|
67
|
+
}).join("");
|
|
68
|
+
const size = change.length ? `<div class="ch-size">${change.length.from} → ${change.length.to} items</div>` : "";
|
|
69
|
+
return `<div class="ch-list">${size}${rows}${hidden > 0 ? `<div class="ch-more">and ${hidden} more</div>` : ""}</div>`;
|
|
70
|
+
}
|
|
71
|
+
function handleCopyClick(event) {
|
|
72
|
+
const button = event.target?.closest?.(".ch-copy");
|
|
73
|
+
if (!button) return false;
|
|
74
|
+
void copyText(button.dataset.copy ?? "", button);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
var CHANGE_CSS = `
|
|
78
|
+
.ch-list { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; }
|
|
79
|
+
.ch-size { color: var(--fx-muted, #8a90a2); margin-bottom: 2px; }
|
|
80
|
+
.ch-row { display: flex; gap: 6px; align-items: baseline; padding: 1px 0; }
|
|
81
|
+
.ch-row.ch-stacked { align-items: flex-start; padding: 3px 0; }
|
|
82
|
+
.ch-sign { flex: none; width: 9px; text-align: center; }
|
|
83
|
+
.ch-added .ch-sign { color: var(--state, #34d399); }
|
|
84
|
+
.ch-removed .ch-sign { color: var(--fx-write, #fb7185); }
|
|
85
|
+
.ch-changed .ch-sign { color: var(--effect, #fbbf24); }
|
|
86
|
+
.ch-key { flex: none; color: var(--fx-muted, #8a90a2); }
|
|
87
|
+
.ch-sides { min-width: 0; flex: 1; overflow-wrap: anywhere; }
|
|
88
|
+
.ch-stacked .ch-sides { display: flex; flex-direction: column; gap: 2px; }
|
|
89
|
+
.ch-from { color: var(--fx-muted, #8a90a2); }
|
|
90
|
+
.ch-row:not(.ch-stacked) .ch-from { text-decoration: line-through; }
|
|
91
|
+
.ch-arrow { color: var(--fx-muted, #8a90a2); margin: 0 4px; }
|
|
92
|
+
.ch-to { color: var(--fx-label, #e7e9ee); }
|
|
93
|
+
.ch-more { color: var(--fx-muted, #8a90a2); margin-top: 2px; }
|
|
94
|
+
/* In the corner: an object can be tall, and a button under it is a scroll away. */
|
|
95
|
+
.ch-block { position: relative; border-radius: 4px; border: 1px solid var(--fx-line, #ffffff14); }
|
|
96
|
+
.ch-block.ch-from { opacity: .72; }
|
|
97
|
+
.ch-json { margin: 0; padding: 5px 7px; overflow-x: auto; white-space: pre; font: inherit; }
|
|
98
|
+
.ch-copy {
|
|
99
|
+
position: absolute; top: 3px; right: 3px; padding: 1px 6px; cursor: pointer;
|
|
100
|
+
font: inherit; font-size: 9.5px; opacity: 0; transition: opacity .12s;
|
|
101
|
+
border: 1px solid var(--fx-line, #ffffff26); border-radius: 3px;
|
|
102
|
+
background: var(--fx-panel, #14161c); color: var(--fx-muted, #8a90a2);
|
|
103
|
+
}
|
|
104
|
+
.ch-block:hover .ch-copy, .ch-copy:focus { opacity: 1; }
|
|
105
|
+
.ch-copy:hover { color: var(--fx-label, #e7e9ee); }
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
// src/detail-panel.ts
|
|
109
|
+
var escape2 = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
110
|
+
var clock = (t) => {
|
|
111
|
+
const d = new Date(t);
|
|
112
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
113
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
114
|
+
};
|
|
115
|
+
var VERBS = {
|
|
116
|
+
write: "written",
|
|
117
|
+
ran: "ran",
|
|
118
|
+
created: "made",
|
|
119
|
+
disposed: "gone"
|
|
120
|
+
};
|
|
121
|
+
var MIN_W = 320;
|
|
122
|
+
var MIN_H = 200;
|
|
123
|
+
function draggable(handle, onMove) {
|
|
124
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
125
|
+
if (event.button !== 0) return;
|
|
126
|
+
if (event.target.closest("button")) return;
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
event.stopPropagation();
|
|
129
|
+
handle.setPointerCapture(event.pointerId);
|
|
130
|
+
let lastX = event.clientX;
|
|
131
|
+
let lastY = event.clientY;
|
|
132
|
+
const move = (e) => {
|
|
133
|
+
onMove(e.clientX - lastX, e.clientY - lastY);
|
|
134
|
+
lastX = e.clientX;
|
|
135
|
+
lastY = e.clientY;
|
|
136
|
+
};
|
|
137
|
+
const stop = () => {
|
|
138
|
+
handle.removeEventListener("pointermove", move);
|
|
139
|
+
handle.removeEventListener("pointerup", stop);
|
|
140
|
+
handle.removeEventListener("pointercancel", stop);
|
|
141
|
+
handle.removeEventListener("lostpointercapture", stop);
|
|
142
|
+
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
|
|
143
|
+
};
|
|
144
|
+
handle.addEventListener("pointermove", move);
|
|
145
|
+
handle.addEventListener("pointerup", stop);
|
|
146
|
+
handle.addEventListener("pointercancel", stop);
|
|
147
|
+
handle.addEventListener("lostpointercapture", stop);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function createDetailPanel(host, options = {}) {
|
|
151
|
+
const panel = host.ownerDocument.createElement("div");
|
|
152
|
+
panel.className = "dp-panel";
|
|
153
|
+
panel.hidden = true;
|
|
154
|
+
panel.innerHTML = '<div class="dp-bar"><span class="dp-title"></span><button type="button" class="dp-close" title="Close" aria-label="Close">✕</button></div><div class="dp-body"></div><div class="dp-grip" title="Drag to resize"></div>';
|
|
155
|
+
host.appendChild(panel);
|
|
156
|
+
const title = panel.querySelector(".dp-title");
|
|
157
|
+
const body = panel.querySelector(".dp-body");
|
|
158
|
+
const bar = panel.querySelector(".dp-bar");
|
|
159
|
+
const grip = panel.querySelector(".dp-grip");
|
|
160
|
+
let x = 0;
|
|
161
|
+
let y = 0;
|
|
162
|
+
let width = 520;
|
|
163
|
+
let height = 380;
|
|
164
|
+
let open = false;
|
|
165
|
+
const place = () => {
|
|
166
|
+
panel.style.left = `${Math.round(x)}px`;
|
|
167
|
+
panel.style.top = `${Math.round(y)}px`;
|
|
168
|
+
panel.style.width = `${Math.round(width)}px`;
|
|
169
|
+
panel.style.height = `${Math.round(height)}px`;
|
|
170
|
+
};
|
|
171
|
+
draggable(bar, (dx, dy) => {
|
|
172
|
+
const view = panel.ownerDocument.defaultView;
|
|
173
|
+
x = Math.max(0, Math.min((view?.innerWidth ?? width) - 60, x + dx));
|
|
174
|
+
y = Math.max(0, Math.min((view?.innerHeight ?? height) - 30, y + dy));
|
|
175
|
+
place();
|
|
176
|
+
});
|
|
177
|
+
draggable(grip, (dx, dy) => {
|
|
178
|
+
width = Math.max(MIN_W, width + dx);
|
|
179
|
+
height = Math.max(MIN_H, height + dy);
|
|
180
|
+
place();
|
|
181
|
+
});
|
|
182
|
+
panel.querySelector(".dp-close")?.addEventListener("click", () => close());
|
|
183
|
+
body.addEventListener("click", (e) => {
|
|
184
|
+
if (handleCopyClick(e)) return;
|
|
185
|
+
const row = e.target.closest(".dp-node");
|
|
186
|
+
if (row?.dataset.id) options.onPick?.(Number(row.dataset.id));
|
|
187
|
+
});
|
|
188
|
+
function close() {
|
|
189
|
+
open = false;
|
|
190
|
+
panel.hidden = true;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
open(commit, graph) {
|
|
194
|
+
const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
|
|
195
|
+
const cause = commit.cause;
|
|
196
|
+
title.textContent = cause ? `${nameOf(cause)} · ${clock(commit.t)}` : `${commit.commit === 0 ? "first render" : `commit ${commit.commit}`} · ${clock(commit.t)}`;
|
|
197
|
+
const headline = cause && cause.from !== void 0 && cause.to !== void 0 ? `<div class="dp-change"><span class="ch-from">${escape2(cause.from)}</span><span class="ch-arrow">→</span><span class="ch-to">${escape2(cause.to)}</span></div>` : "";
|
|
198
|
+
const groups = ["write", "ran", "created", "disposed"].map((kind) => {
|
|
199
|
+
const rows = commit.events.filter((e) => e.kind === kind && e !== cause);
|
|
200
|
+
if (!rows.length) return "";
|
|
201
|
+
const seen = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const event of rows) {
|
|
203
|
+
const found = seen.get(event.id);
|
|
204
|
+
if (found) found.count++;
|
|
205
|
+
else seen.set(event.id, { name: nameOf(event), count: 1 });
|
|
206
|
+
}
|
|
207
|
+
const items = [...seen].map(
|
|
208
|
+
([id, { name, count }]) => `<button type="button" class="dp-node" data-id="${id}">${escape2(name)}${count > 1 ? ` ×${count}` : ""}</button>`
|
|
209
|
+
).join("");
|
|
210
|
+
return `<div class="dp-group"><div class="dp-verb">${VERBS[kind]}</div><div class="dp-nodes">${items}</div></div>`;
|
|
211
|
+
}).join("");
|
|
212
|
+
const node = cause ? graph.get(cause.id) : void 0;
|
|
213
|
+
const where = (t) => {
|
|
214
|
+
if (!t.source) return "";
|
|
215
|
+
const file = (t.source.file ?? "").split("/").pop() ?? "";
|
|
216
|
+
const out = t.source.compiled;
|
|
217
|
+
const tail = out && out.line !== t.source.line ? ` · out ${out.line}` : "";
|
|
218
|
+
return ` — ${file}:${t.source.line ?? ""}${tail}`;
|
|
219
|
+
};
|
|
220
|
+
const targets = node?.targets?.length ? `<div class="dp-group"><div class="dp-verb">updates</div><div class="dp-targets">${node.targets.map(
|
|
221
|
+
(t) => `<div>${escape2(t.name ? `${t.name} of ${t.element}` : t.element)}<span class="dp-where">${escape2(where(t))}</span></div>`
|
|
222
|
+
).join("")}</div></div>` : "";
|
|
223
|
+
body.innerHTML = headline + changeRows(cause?.change) + groups + targets;
|
|
224
|
+
if (!open) {
|
|
225
|
+
const view = panel.ownerDocument.defaultView;
|
|
226
|
+
x = Math.max(12, ((view?.innerWidth ?? 900) - width) / 2);
|
|
227
|
+
y = Math.max(12, ((view?.innerHeight ?? 600) - height) / 3);
|
|
228
|
+
place();
|
|
229
|
+
}
|
|
230
|
+
open = true;
|
|
231
|
+
panel.hidden = false;
|
|
232
|
+
},
|
|
233
|
+
close,
|
|
234
|
+
get isOpen() {
|
|
235
|
+
return open;
|
|
236
|
+
},
|
|
237
|
+
destroy() {
|
|
238
|
+
panel.remove();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
var DETAIL_PANEL_CSS = `
|
|
243
|
+
.dp-panel {
|
|
244
|
+
position: fixed; z-index: 40; display: flex; flex-direction: column;
|
|
245
|
+
border: 1px solid var(--fx-line, #ffffff26); border-radius: 8px;
|
|
246
|
+
background: var(--fx-panel, #14161c); color: var(--fx-label, #e7e9ee);
|
|
247
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, .45); overflow: hidden;
|
|
248
|
+
}
|
|
249
|
+
.dp-panel[hidden] { display: none; }
|
|
250
|
+
.dp-bar {
|
|
251
|
+
display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;
|
|
252
|
+
cursor: move; user-select: none;
|
|
253
|
+
border-bottom: 1px solid var(--fx-line, #ffffff1f);
|
|
254
|
+
}
|
|
255
|
+
.dp-title { font-family: ui-monospace, monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
256
|
+
.dp-close {
|
|
257
|
+
margin-left: auto; width: 22px; height: 20px; padding: 0; cursor: pointer; font: inherit; font-size: 11px;
|
|
258
|
+
border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
|
|
259
|
+
background: transparent; color: var(--fx-muted, #8a90a2);
|
|
260
|
+
}
|
|
261
|
+
.dp-close:hover { color: var(--fx-label, #e7e9ee); }
|
|
262
|
+
.dp-body { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; }
|
|
263
|
+
.dp-change { font-family: ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }
|
|
264
|
+
.dp-group { margin-top: 10px; }
|
|
265
|
+
.dp-verb { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--fx-muted, #8a90a2); }
|
|
266
|
+
.dp-nodes { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
|
267
|
+
.dp-node {
|
|
268
|
+
padding: 2px 7px; cursor: pointer; font: inherit; font-family: ui-monospace, monospace; font-size: 11px;
|
|
269
|
+
border: 1px solid var(--fx-line, #ffffff1f); border-radius: 999px;
|
|
270
|
+
background: transparent; color: var(--fx-label, #e7e9ee);
|
|
271
|
+
}
|
|
272
|
+
.dp-node:hover { border-color: var(--fx-accent, #5eead4); }
|
|
273
|
+
.dp-targets { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: var(--fx-label, #e7e9ee); }
|
|
274
|
+
.dp-where { color: var(--fx-muted, #8a90a2); }
|
|
275
|
+
/* Bottom-right, where every desktop puts it. */
|
|
276
|
+
.dp-grip {
|
|
277
|
+
position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize;
|
|
278
|
+
background: linear-gradient(135deg, transparent 50%, var(--fx-line, #ffffff40) 50%);
|
|
279
|
+
}
|
|
280
|
+
${CHANGE_CSS}`;
|
|
281
|
+
export {
|
|
282
|
+
DETAIL_PANEL_CSS,
|
|
283
|
+
createDetailPanel
|
|
284
|
+
};
|
package/dist/diff.cjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/diff.ts
|
|
21
|
+
var diff_exports = {};
|
|
22
|
+
__export(diff_exports, {
|
|
23
|
+
diffValues: () => diffValues
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(diff_exports);
|
|
26
|
+
|
|
27
|
+
// src/format.ts
|
|
28
|
+
var MAX_DEPTH = 3;
|
|
29
|
+
var MAX_ENTRIES = 8;
|
|
30
|
+
function domName(v) {
|
|
31
|
+
if (typeof v?.nodeType !== "number" || typeof v.nodeName !== "string") return null;
|
|
32
|
+
return v.tagName ? `<${v.tagName.toLowerCase()}>` : `#${v.nodeName.toLowerCase()}`;
|
|
33
|
+
}
|
|
34
|
+
function fnName(v) {
|
|
35
|
+
return v.name ? `ƒ ${v.name}()` : "ƒ";
|
|
36
|
+
}
|
|
37
|
+
function className(v) {
|
|
38
|
+
const name = v.constructor?.name;
|
|
39
|
+
return name && name !== "Object" ? `${name} ` : "";
|
|
40
|
+
}
|
|
41
|
+
function primitive(v) {
|
|
42
|
+
if (v === null) return "null";
|
|
43
|
+
switch (typeof v) {
|
|
44
|
+
case "undefined":
|
|
45
|
+
return "undefined";
|
|
46
|
+
case "string":
|
|
47
|
+
return JSON.stringify(v);
|
|
48
|
+
case "number":
|
|
49
|
+
case "boolean":
|
|
50
|
+
return String(v);
|
|
51
|
+
case "bigint":
|
|
52
|
+
return `${v}n`;
|
|
53
|
+
case "symbol":
|
|
54
|
+
return v.toString();
|
|
55
|
+
case "function":
|
|
56
|
+
return fnName(v);
|
|
57
|
+
default:
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function walk(value, depth, seen) {
|
|
62
|
+
const simple = primitive(value);
|
|
63
|
+
if (simple !== null) return simple;
|
|
64
|
+
const object = value;
|
|
65
|
+
if (seen.has(object)) return "[circular]";
|
|
66
|
+
const dom = domName(object);
|
|
67
|
+
if (dom) return dom;
|
|
68
|
+
if (object instanceof Date) return Number.isNaN(object.getTime()) ? "Invalid Date" : object.toISOString();
|
|
69
|
+
if (object instanceof RegExp) return String(object);
|
|
70
|
+
if (object instanceof Error) return `${object.name}: ${object.message}`;
|
|
71
|
+
if (object instanceof Promise) return "Promise";
|
|
72
|
+
if (ArrayBuffer.isView(object)) {
|
|
73
|
+
return `${className(object).trim()}(${object.length ?? ""})`;
|
|
74
|
+
}
|
|
75
|
+
if (depth >= MAX_DEPTH) return Array.isArray(object) ? "[…]" : "{…}";
|
|
76
|
+
seen.add(object);
|
|
77
|
+
try {
|
|
78
|
+
if (Array.isArray(object)) {
|
|
79
|
+
const shown2 = object.slice(0, MAX_ENTRIES).map((item) => walk(item, depth + 1, seen));
|
|
80
|
+
if (object.length > MAX_ENTRIES) shown2.push("…");
|
|
81
|
+
return `[${shown2.join(", ")}]`;
|
|
82
|
+
}
|
|
83
|
+
if (object instanceof Map) {
|
|
84
|
+
const shown2 = [];
|
|
85
|
+
for (const [k, v] of object) {
|
|
86
|
+
if (shown2.length === MAX_ENTRIES) {
|
|
87
|
+
shown2.push("…");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
shown2.push(`${walk(k, depth + 1, seen)} => ${walk(v, depth + 1, seen)}`);
|
|
91
|
+
}
|
|
92
|
+
return `Map(${object.size}) {${shown2.join(", ")}}`;
|
|
93
|
+
}
|
|
94
|
+
if (object instanceof Set) {
|
|
95
|
+
const shown2 = [];
|
|
96
|
+
for (const item of object) {
|
|
97
|
+
if (shown2.length === MAX_ENTRIES) {
|
|
98
|
+
shown2.push("…");
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
shown2.push(walk(item, depth + 1, seen));
|
|
102
|
+
}
|
|
103
|
+
return `Set(${object.size}) {${shown2.join(", ")}}`;
|
|
104
|
+
}
|
|
105
|
+
const keys = Object.keys(object);
|
|
106
|
+
const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
|
|
107
|
+
let read;
|
|
108
|
+
try {
|
|
109
|
+
read = walk(object[key], depth + 1, seen);
|
|
110
|
+
} catch {
|
|
111
|
+
read = "[unreadable]";
|
|
112
|
+
}
|
|
113
|
+
return `${key}: ${read}`;
|
|
114
|
+
});
|
|
115
|
+
if (keys.length > MAX_ENTRIES) shown.push("…");
|
|
116
|
+
return `${className(object)}{${shown.join(", ")}}`;
|
|
117
|
+
} finally {
|
|
118
|
+
seen.delete(object);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function formatValue(value, limit = 240) {
|
|
122
|
+
let text;
|
|
123
|
+
try {
|
|
124
|
+
text = walk(value, 0, /* @__PURE__ */ new Set());
|
|
125
|
+
} catch {
|
|
126
|
+
text = "[unreadable]";
|
|
127
|
+
}
|
|
128
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/diff.ts
|
|
132
|
+
var MAX_ENTRIES2 = 24;
|
|
133
|
+
var ENTRY_VALUE = 120;
|
|
134
|
+
var ENTRY_JSON = 4e3;
|
|
135
|
+
var show = (value) => formatValue(value, ENTRY_VALUE);
|
|
136
|
+
function json(value) {
|
|
137
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
138
|
+
try {
|
|
139
|
+
const text = JSON.stringify(value);
|
|
140
|
+
return text && text.length <= ENTRY_JSON ? text : void 0;
|
|
141
|
+
} catch {
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
var side = (value, key) => {
|
|
146
|
+
const text = json(value);
|
|
147
|
+
return { [key]: show(value), ...text ? { [`${key}Json`]: text } : {} };
|
|
148
|
+
};
|
|
149
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
150
|
+
function same(a, b) {
|
|
151
|
+
if (Object.is(a, b)) return true;
|
|
152
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
153
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
154
|
+
const ka = Object.keys(a);
|
|
155
|
+
const kb = Object.keys(b);
|
|
156
|
+
if (ka.length !== kb.length) return false;
|
|
157
|
+
return ka.every((k) => Object.is(a[k], b[k]));
|
|
158
|
+
}
|
|
159
|
+
function diffArray(before, after) {
|
|
160
|
+
let head = 0;
|
|
161
|
+
while (head < before.length && head < after.length && same(before[head], after[head])) head++;
|
|
162
|
+
let tail = 0;
|
|
163
|
+
while (tail < before.length - head && tail < after.length - head && same(before[before.length - 1 - tail], after[after.length - 1 - tail])) {
|
|
164
|
+
tail++;
|
|
165
|
+
}
|
|
166
|
+
const entries = [];
|
|
167
|
+
let skipped = 0;
|
|
168
|
+
const push = (entry) => {
|
|
169
|
+
if (entries.length < MAX_ENTRIES2) entries.push(entry);
|
|
170
|
+
else skipped++;
|
|
171
|
+
};
|
|
172
|
+
const removedCount = before.length - head - tail;
|
|
173
|
+
const addedCount = after.length - head - tail;
|
|
174
|
+
const overlap = Math.min(removedCount, addedCount);
|
|
175
|
+
for (let i = 0; i < overlap; i++) {
|
|
176
|
+
push({
|
|
177
|
+
key: String(head + i),
|
|
178
|
+
op: "changed",
|
|
179
|
+
...side(before[head + i], "from"),
|
|
180
|
+
...side(after[head + i], "to")
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
for (let i = overlap; i < addedCount; i++) {
|
|
184
|
+
push({ key: String(head + i), op: "added", ...side(after[head + i], "to") });
|
|
185
|
+
}
|
|
186
|
+
for (let i = overlap; i < removedCount; i++) {
|
|
187
|
+
push({ key: String(head + i), op: "removed", ...side(before[head + i], "from") });
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
shape: "array",
|
|
191
|
+
entries,
|
|
192
|
+
...skipped ? { more: skipped } : {},
|
|
193
|
+
...before.length !== after.length ? { length: { from: before.length, to: after.length } } : {}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function diffObject(before, after) {
|
|
197
|
+
const entries = [];
|
|
198
|
+
let skipped = 0;
|
|
199
|
+
const push = (entry) => {
|
|
200
|
+
if (entries.length < MAX_ENTRIES2) entries.push(entry);
|
|
201
|
+
else skipped++;
|
|
202
|
+
};
|
|
203
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
204
|
+
const had = key in before;
|
|
205
|
+
const has = key in after;
|
|
206
|
+
if (had && has) {
|
|
207
|
+
if (!same(before[key], after[key])) {
|
|
208
|
+
push({ key, op: "changed", ...side(before[key], "from"), ...side(after[key], "to") });
|
|
209
|
+
}
|
|
210
|
+
} else if (has) push({ key, op: "added", ...side(after[key], "to") });
|
|
211
|
+
else push({ key, op: "removed", ...side(before[key], "from") });
|
|
212
|
+
}
|
|
213
|
+
return { shape: "object", entries, ...skipped ? { more: skipped } : {} };
|
|
214
|
+
}
|
|
215
|
+
function diffValues(before, after) {
|
|
216
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
217
|
+
const change = diffArray(before, after);
|
|
218
|
+
return change.entries?.length || change.length ? change : void 0;
|
|
219
|
+
}
|
|
220
|
+
if (isPlainObject(before) && isPlainObject(after)) {
|
|
221
|
+
const change = diffObject(before, after);
|
|
222
|
+
return change.entries?.length ? change : void 0;
|
|
223
|
+
}
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
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
|
+
}
|