@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.
- package/README.md +80 -2
- package/dist/callsite.d.ts +22 -0
- package/dist/callsite.d.ts.map +1 -0
- package/dist/callsite.js +82 -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/copy.d.ts +9 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +52 -0
- package/dist/describe.d.ts +3 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +47 -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/diff.d.ts +31 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +132 -0
- package/dist/dom.d.ts +81 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +269 -0
- package/dist/export.d.ts +55 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +90 -0
- package/dist/float.d.ts +54 -0
- package/dist/float.d.ts.map +1 -0
- package/dist/float.js +256 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +188 -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/highlight.d.ts +56 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +154 -0
- package/dist/history.d.ts +59 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +85 -0
- package/dist/hook.d.ts +60 -0
- package/dist/hook.d.ts.map +1 -0
- package/dist/hook.js +142 -0
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/inspector.d.ts +60 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/inspector.js +415 -0
- package/dist/instrument.d.ts +147 -5
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +164 -44
- 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/menu.d.ts +37 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +125 -0
- package/dist/observe.d.ts +80 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +893 -0
- package/dist/palette.d.ts +11 -0
- package/dist/palette.d.ts.map +1 -0
- package/dist/palette.js +27 -0
- package/dist/path.d.ts +19 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +49 -0
- package/dist/plugin.d.ts +121 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +74 -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/directives.d.ts +25 -0
- package/dist/plugins/directives.d.ts.map +1 -0
- package/dist/plugins/directives.js +78 -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/source.d.ts +72 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +153 -0
- package/dist/timeline.d.ts +30 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +271 -0
- package/dist/tree.d.ts +128 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +645 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- 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/wire.d.ts +100 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +93 -0
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @fluixi/devtools
|
|
1
|
+
// @fluixi/devtools: live inspector for the Fluixi reactive graph.
|
|
2
2
|
// Instrumented signal/memo/effect register into a graph; createGraphView renders + animates the
|
|
3
3
|
// State → Memo → Effect propagation.
|
|
4
4
|
//
|
|
@@ -16,4 +16,28 @@
|
|
|
16
16
|
// const tick = () => { reactor.sample(); view.update(reactor.graph); requestAnimationFrame(tick); };
|
|
17
17
|
// tick();
|
|
18
18
|
export { createReactor, nodeVersion, nodeState } from './instrument.js';
|
|
19
|
+
export { observeGraph } from './observe.js';
|
|
20
|
+
export { installDevtoolsHook } from './hook.js';
|
|
21
|
+
export { formatValue, shorten } from './format.js';
|
|
22
|
+
export { callSite, currentCallSite } from './callsite.js';
|
|
23
|
+
export { serializeGraph, snapshotToRenderMap, applySnapshot } from './wire.js';
|
|
24
|
+
export { toCommits } from './history.js';
|
|
25
|
+
export { markReactiveSource } from './source.js';
|
|
19
26
|
export { createGraphView, DEVTOOLS_CSS } from './graph-view.js';
|
|
27
|
+
export { createInspector, INSPECTOR_CSS } from './inspector.js';
|
|
28
|
+
export { createValuePane, VALUE_PANE_CSS } from './value-pane.js';
|
|
29
|
+
export { indentValue } from './format.js';
|
|
30
|
+
export { createTimeline, TIMELINE_CSS } from './timeline.js';
|
|
31
|
+
export { shortenPath } from './path.js';
|
|
32
|
+
export { registerDevtoolsPlugin, devtoolsPlugins, annotationOf, pluginRows, pluginBadge } from './plugin.js';
|
|
33
|
+
export { createComponentTree, componentTree, TREE_KINDS, TREE_CSS } from './tree.js';
|
|
34
|
+
export { showHighlight, hideHighlight, removeHighlight, startPicking } from './highlight.js';
|
|
35
|
+
export { makeFloatable, FLOAT_CSS } from './float.js';
|
|
36
|
+
export { createKindFilter, FILTERABLE_KINDS, DROPDOWN_FILTER_CSS, KIND_FILTER_CSS } from './kind-filter.js';
|
|
37
|
+
export { createMenu, MENU_CSS } from './menu.js';
|
|
38
|
+
export { createDetailPanel, DETAIL_PANEL_CSS } from './detail-panel.js';
|
|
39
|
+
export { changeRows, handleCopyClick, CHANGE_CSS } from './change-view.js';
|
|
40
|
+
export { copyText } from './copy.js';
|
|
41
|
+
export { exportGraph, graphToJson } from './export.js';
|
|
42
|
+
export { diffValues } from './diff.js';
|
|
43
|
+
export { describeNode } from './describe.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RNode } from './instrument.js';
|
|
2
|
+
import type { ReactiveSourceLocation } from './source.js';
|
|
3
|
+
export interface Inspector {
|
|
4
|
+
/** Draw the node with this id, or the empty state when it is undefined. */
|
|
5
|
+
show(graph: Map<number, RNode>, id: number | null | undefined): void;
|
|
6
|
+
/**
|
|
7
|
+
* Draw an element instead of a node.
|
|
8
|
+
*
|
|
9
|
+
* An element is not in the graph — nothing computes it — but it is the part of the page a
|
|
10
|
+
* reader recognises, and the graph knows which tag rendered it and which values write into
|
|
11
|
+
* it. Those, and the line each binding was written on, are what there is to say.
|
|
12
|
+
*/
|
|
13
|
+
showElement(graph: Map<number, RNode>, pick: ElementInspection | null | undefined): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
}
|
|
16
|
+
/** An element to inspect. The shape `createComponentTree` hands to its `onPick`. */
|
|
17
|
+
export interface ElementInspection {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
owner?: number;
|
|
21
|
+
writes?: number[];
|
|
22
|
+
/** What plugins attached to this element. */
|
|
23
|
+
extra?: Record<string, unknown>;
|
|
24
|
+
/** Where the element itself was written, when the build recorded it. */
|
|
25
|
+
at?: ReactiveSourceLocation;
|
|
26
|
+
}
|
|
27
|
+
export interface InspectorOptions {
|
|
28
|
+
/** What to say when nothing is selected. */
|
|
29
|
+
empty?: string;
|
|
30
|
+
/** How much of a value to show. */
|
|
31
|
+
limit?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Clicking a location. Omitted leaves them as plain text, which is what a host with
|
|
34
|
+
* nowhere to open a file wants.
|
|
35
|
+
*/
|
|
36
|
+
onReveal?: (location: ReactiveSourceLocation) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The project a path is shown relative to. Without it, a path is shortened to the package
|
|
39
|
+
* it names, which is as much as a graph can work out on its own.
|
|
40
|
+
*/
|
|
41
|
+
root?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Write a value from the panel. Given the text as typed: the host parses it, since what
|
|
44
|
+
* counts as a value is the application's business, and returns whether it took.
|
|
45
|
+
*
|
|
46
|
+
* Omitted leaves values read-only, which is what a panel reading a snapshot over a wire
|
|
47
|
+
* wants — there is nothing on the other end to write to.
|
|
48
|
+
*/
|
|
49
|
+
onEdit?: (id: number, raw: string) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Start with framework locations shown. Off by default: for someone writing an
|
|
52
|
+
* application, where the framework instantiated a component is noise. For someone working
|
|
53
|
+
* on the framework it is the whole point, so it is a toggle rather than a rule.
|
|
54
|
+
*/
|
|
55
|
+
internals?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function createInspector(host: HTMLElement, options?: InspectorOptions): Inspector;
|
|
58
|
+
/** Default styling for what createInspector writes, in the shape DEVTOOLS_CSS is used. */
|
|
59
|
+
export declare const INSPECTOR_CSS: string;
|
|
60
|
+
//# sourceMappingURL=inspector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspector.d.ts","sourceRoot":"","sources":["../src/inspector.ts"],"names":[],"mappings":"AASA,OAAO,EAA0B,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAErE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAO1D,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;IACrE;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;IACzF,KAAK,IAAI,IAAI,CAAC;CACf;AAED,oFAAoF;AACpF,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,wEAAwE;IACxE,EAAE,CAAC,EAAE,sBAAsB,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACtD;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAuFD,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS,CA2S5F;AAED,0FAA0F;AAC1F,eAAO,MAAM,aAAa,QAuCzB,CAAC"}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// What one node is, in words.
|
|
2
|
+
//
|
|
3
|
+
// The graph draws a node in the space of a card: a name, a kind, a clipped value, a file and
|
|
4
|
+
// a line. Everything else about it — the version, the run count, what it reads, what reads
|
|
5
|
+
// it, where it was declared as against where it was used — needs room, and every host that
|
|
6
|
+
// draws the graph wants the same answer. So it lives here rather than in each of them.
|
|
7
|
+
//
|
|
8
|
+
// Rendered as html into an element the host owns, and re-rendered only when what it says
|
|
9
|
+
// would change: a panel repainting on every frame makes text impossible to select.
|
|
10
|
+
import { nodeState, nodeVersion } from './instrument.js';
|
|
11
|
+
import { formatValue } from './format.js';
|
|
12
|
+
import { shortenPath } from './path.js';
|
|
13
|
+
import { pluginRows, pluginElementRows } from './plugin.js';
|
|
14
|
+
import { kindRules } from './palette.js';
|
|
15
|
+
import { createValuePane, VALUE_PANE_CSS } from './value-pane.js';
|
|
16
|
+
const escape = (text) => text.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
17
|
+
const clip = (text, n = 140) => (text.length > n ? `${text.slice(0, n - 1)}…` : text);
|
|
18
|
+
/**
|
|
19
|
+
* A location the host can open, carrying enough to find it again on the click.
|
|
20
|
+
*
|
|
21
|
+
* The position travels in the markup rather than in a closure because the panel is written
|
|
22
|
+
* as a string of html and replaced wholesale on each change.
|
|
23
|
+
*/
|
|
24
|
+
const place = (text, source, live) => {
|
|
25
|
+
if (!source || !live || !source.file)
|
|
26
|
+
return escape(text);
|
|
27
|
+
return (`<button type="button" class="ins-at" data-file="${escape(source.file)}"` +
|
|
28
|
+
` data-line="${source.line ?? ''}" data-column="${source.column ?? ''}"` +
|
|
29
|
+
` title="Open ${escape(source.file)}">${escape(text)}</button>`);
|
|
30
|
+
};
|
|
31
|
+
const at = (source, root) => source ? `${source.file ? shortenPath(source.file, root) : ''}:${source.line ?? ''}:${source.column ?? ''}` : '';
|
|
32
|
+
/**
|
|
33
|
+
* The location, and where it is in the compiled file when the two differ.
|
|
34
|
+
*
|
|
35
|
+
* A template's holes all map back to the line the template opens at, so the original position
|
|
36
|
+
* names the component and nothing finer. The compiled one is what separates this binding from
|
|
37
|
+
* the three beside it.
|
|
38
|
+
*/
|
|
39
|
+
const bothAt = (source, root) => {
|
|
40
|
+
if (!source)
|
|
41
|
+
return '';
|
|
42
|
+
const out = source.compiled;
|
|
43
|
+
const same = out?.line === source.line && out?.column === source.column;
|
|
44
|
+
return out && !same
|
|
45
|
+
? `${at(source, root)} · out ${out.line ?? ''}:${out.column ?? ''}`
|
|
46
|
+
: at(source, root);
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The line worth opening for a tag.
|
|
50
|
+
*
|
|
51
|
+
* A component the framework instantiated — a lazy route, a layout the router mounted — was
|
|
52
|
+
* created inside the framework, so `source` is a line in a package. `declaredAt` is where it
|
|
53
|
+
* was actually written. Only one line is shown for an element, so it had better be that one.
|
|
54
|
+
*/
|
|
55
|
+
function authoredPlace(node) {
|
|
56
|
+
for (const place of [node.source, node.declaredAt, node.usedAt]) {
|
|
57
|
+
if (place && !place.external)
|
|
58
|
+
return place;
|
|
59
|
+
}
|
|
60
|
+
return node.declaredAt ?? node.source ?? node.usedAt;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The tags this value writes inside, and how many times each.
|
|
64
|
+
*
|
|
65
|
+
* A signal handed to a component through a prop is read by the render effect that binds it,
|
|
66
|
+
* which is machinery the graph does not draw — so there is no edge from the signal to the
|
|
67
|
+
* component, and a card for it looks connected only to whatever declared it. The
|
|
68
|
+
* descriptions already say it exactly: each element records the values writing into it, and
|
|
69
|
+
* it sits in the tree of the tag that rendered it.
|
|
70
|
+
*/
|
|
71
|
+
function reaches(graph, id) {
|
|
72
|
+
const count = new Map();
|
|
73
|
+
const writesHere = (nodes) => nodes.some((n) => n.writes?.includes(id) || writesHere(n.children));
|
|
74
|
+
for (const node of graph.values()) {
|
|
75
|
+
if (node.id === id || !node.dom?.length)
|
|
76
|
+
continue;
|
|
77
|
+
if (writesHere(node.dom))
|
|
78
|
+
count.set(node.label, (count.get(node.label) ?? 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
return [...count].map(([label, n]) => (n > 1 ? `${label} ×${n}` : label));
|
|
81
|
+
}
|
|
82
|
+
/** The live value when the node can be read, the last one it reported otherwise. */
|
|
83
|
+
function valueOf(node, limit) {
|
|
84
|
+
if (node.read) {
|
|
85
|
+
try {
|
|
86
|
+
return formatValue(node.read(), limit);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A reader that throws is still a node worth inspecting.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return node.detail ?? node.value ?? '';
|
|
93
|
+
}
|
|
94
|
+
export function createInspector(host, options = {}) {
|
|
95
|
+
const empty = options.empty ?? 'click a node to inspect';
|
|
96
|
+
const limit = options.limit ?? 600;
|
|
97
|
+
const reveals = typeof options.onReveal === 'function';
|
|
98
|
+
const root = options.root;
|
|
99
|
+
const edits = typeof options.onEdit === 'function';
|
|
100
|
+
/** While the reader is typing, nothing redraws: the input would go with it. */
|
|
101
|
+
let editing = null;
|
|
102
|
+
let field = null;
|
|
103
|
+
let internals = options.internals === true;
|
|
104
|
+
/** Built on first use: most sessions never open one. */
|
|
105
|
+
let pane = null;
|
|
106
|
+
const doc = host.ownerDocument;
|
|
107
|
+
// Anything cut to fit carries the whole of it, and opens a pane rather than a tooltip.
|
|
108
|
+
host.addEventListener('click', (event) => {
|
|
109
|
+
const cell = event.target?.closest?.('[data-full]');
|
|
110
|
+
if (!cell)
|
|
111
|
+
return;
|
|
112
|
+
pane ??= createValuePane(host);
|
|
113
|
+
pane.open(cell.dataset.label || 'value', cell.dataset.full || '');
|
|
114
|
+
});
|
|
115
|
+
// Kept out of the redrawn markup: the panel replaces its content wholesale, and a control
|
|
116
|
+
// inside it would be rebuilt under the pointer every time something changed.
|
|
117
|
+
const header = doc.createElement('div');
|
|
118
|
+
header.className = 'ins-header';
|
|
119
|
+
const bar = doc.createElement('label');
|
|
120
|
+
bar.className = 'ins-bar';
|
|
121
|
+
const box = doc.createElement('input');
|
|
122
|
+
box.type = 'checkbox';
|
|
123
|
+
box.checked = internals;
|
|
124
|
+
bar.append(box, doc.createTextNode('framework'));
|
|
125
|
+
bar.title = 'Also show where the framework created this, not only where it was written';
|
|
126
|
+
const body = doc.createElement('div');
|
|
127
|
+
body.className = 'ins-body';
|
|
128
|
+
header.appendChild(bar);
|
|
129
|
+
host.replaceChildren(header, body);
|
|
130
|
+
box.addEventListener('change', () => {
|
|
131
|
+
internals = box.checked;
|
|
132
|
+
signature = '';
|
|
133
|
+
});
|
|
134
|
+
if (edits) {
|
|
135
|
+
host.addEventListener('click', (event) => {
|
|
136
|
+
const cell = event.target?.closest?.('.ins-edit');
|
|
137
|
+
if (!cell || editing !== null)
|
|
138
|
+
return;
|
|
139
|
+
const id = Number(cell.dataset.id);
|
|
140
|
+
editing = id;
|
|
141
|
+
const input = host.ownerDocument.createElement('input');
|
|
142
|
+
field = input;
|
|
143
|
+
input.className = 'ins-input';
|
|
144
|
+
input.value = cell.dataset.raw ?? '';
|
|
145
|
+
cell.replaceWith(input);
|
|
146
|
+
input.focus();
|
|
147
|
+
input.select();
|
|
148
|
+
const done = (commit) => {
|
|
149
|
+
if (editing === null)
|
|
150
|
+
return;
|
|
151
|
+
editing = null;
|
|
152
|
+
field = null;
|
|
153
|
+
// Whatever it was is redrawn from the graph on the next frame.
|
|
154
|
+
signature = '';
|
|
155
|
+
if (commit)
|
|
156
|
+
options.onEdit(id, input.value);
|
|
157
|
+
};
|
|
158
|
+
input.addEventListener('keydown', (e) => {
|
|
159
|
+
if (e.key === 'Enter')
|
|
160
|
+
done(true);
|
|
161
|
+
else if (e.key === 'Escape')
|
|
162
|
+
done(false);
|
|
163
|
+
});
|
|
164
|
+
input.addEventListener('blur', () => done(true));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (reveals) {
|
|
168
|
+
host.addEventListener('click', (event) => {
|
|
169
|
+
const button = event.target?.closest?.('.ins-at');
|
|
170
|
+
if (!button)
|
|
171
|
+
return;
|
|
172
|
+
const line = Number(button.dataset.line);
|
|
173
|
+
const column = Number(button.dataset.column);
|
|
174
|
+
options.onReveal({
|
|
175
|
+
file: button.dataset.file,
|
|
176
|
+
...(Number.isFinite(line) && button.dataset.line ? { line } : {}),
|
|
177
|
+
...(Number.isFinite(column) && button.dataset.column ? { column } : {}),
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
let signature = '';
|
|
182
|
+
return {
|
|
183
|
+
show(graph, id) {
|
|
184
|
+
const node = id == null ? undefined : graph.get(id);
|
|
185
|
+
if (!node) {
|
|
186
|
+
if (signature === 'empty')
|
|
187
|
+
return;
|
|
188
|
+
signature = 'empty';
|
|
189
|
+
body.innerHTML = `<span class="ins-empty">${escape(empty)}</span>`;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const labelOf = (other) => graph.get(other)?.label ?? '?';
|
|
193
|
+
const reads = [...node.deps].map(labelOf);
|
|
194
|
+
// Binding cards are left out: `bound` below lists the same places, and says where
|
|
195
|
+
// each one is. With both, every binding was named twice on the one line.
|
|
196
|
+
const feeds = [...graph.values()]
|
|
197
|
+
.filter((n) => n.deps.has(node.id) && n.kind !== 'binding')
|
|
198
|
+
.map((n) => n.label);
|
|
199
|
+
const writes = [...node.writes].map(labelOf);
|
|
200
|
+
const value = valueOf(node, limit);
|
|
201
|
+
const state = nodeState(node);
|
|
202
|
+
// A card the view invented, not a node: version and run count are the signal's, and
|
|
203
|
+
// repeating them here would read as facts about a thing that does not compute.
|
|
204
|
+
const synthetic = node.kind === 'binding' || node.kind === 'setter' || node.kind === 'group';
|
|
205
|
+
const owner = node.createdIn !== undefined ? graph.get(node.createdIn)?.label : undefined;
|
|
206
|
+
const reaching = reaches(graph, node.id);
|
|
207
|
+
const made = [...graph.values()].filter((n) => n.createdIn === node.id).map((n) => n.label);
|
|
208
|
+
// Bindings are not nodes, so a signal read only by the dom shows no readers at all.
|
|
209
|
+
// Named when the dom layer said what they write, counted when it did not.
|
|
210
|
+
const bound = node.targets?.length
|
|
211
|
+
? node.targets
|
|
212
|
+
.map((t) => {
|
|
213
|
+
const where = bothAt(t.source, root);
|
|
214
|
+
const what = escape(t.name ? `${t.name} of ${t.element}` : t.element);
|
|
215
|
+
return where ? `${what} (${place(where, t.source, reveals)})` : what;
|
|
216
|
+
})
|
|
217
|
+
.join(', ')
|
|
218
|
+
: node.bindings
|
|
219
|
+
? `${node.bindings} dom binding${node.bindings > 1 ? 's' : ''}`
|
|
220
|
+
: '';
|
|
221
|
+
// A node on screen is part of the graph, so every line says something: "nothing yet" is
|
|
222
|
+
// an answer, and leaving the row out reads as the inspector having failed to look.
|
|
223
|
+
const relations = [
|
|
224
|
+
`<span>↤ reads ${reads.length ? `<b>${escape(reads.join(', '))}</b>` : '<i>nothing</i>'}</span>`,
|
|
225
|
+
// `bound` already carries markup for the places a value reaches, so only the
|
|
226
|
+
// labels are escaped here.
|
|
227
|
+
`<span>↦ feeds ${feeds.length || bound
|
|
228
|
+
? `<b>${[...feeds.map(escape), bound].filter(Boolean).join(', ')}</b>`
|
|
229
|
+
: '<i>nothing yet</i>'}</span>`,
|
|
230
|
+
writes.length ? `<span>✎ writes <b>${escape(writes.join(', '))}</b></span>` : '',
|
|
231
|
+
// Where the value is set from code the graph has no node for.
|
|
232
|
+
node.setAt?.length
|
|
233
|
+
? `<span>⌁ set at <b>${node.setAt.map((w) => place(bothAt(w, root), w, reveals)).join(', ')}</b></span>`
|
|
234
|
+
: '',
|
|
235
|
+
owner ? `<span>◧ made in <b>${escape(owner)}</b></span>` : '',
|
|
236
|
+
reaching.length ? `<span>◨ reaches <b>${escape(reaching.join(', '))}</b></span>` : '',
|
|
237
|
+
...pluginRows(node, graph).map((row) => {
|
|
238
|
+
const where = row.at && { ...row.at, file: row.at.file ?? node.source?.file };
|
|
239
|
+
const text = where?.file ? bothAt(where, root) : '';
|
|
240
|
+
const more = row.value !== undefined && row.value.length > 140;
|
|
241
|
+
return `<span${more
|
|
242
|
+
? ` class="ins-more" data-full="${escape(row.value)}" data-label="${escape(row.label)}"`
|
|
243
|
+
: ''} title="${escape(row.value ?? row.label)}">⌘ ${escape(row.label)}${row.value ? `=<b>${escape(clip(row.value))}</b>` : ''}${text ? ` ${place(text, where, reveals)}` : ''}</span>`;
|
|
244
|
+
}),
|
|
245
|
+
// What the tag was written with. A prop's position has no file of its own: it was
|
|
246
|
+
// written on the tag, so it belongs to whichever file that is.
|
|
247
|
+
...(node.props ?? []).map((p) => {
|
|
248
|
+
const where = p.at && { ...p.at, file: p.at.file ?? node.source?.file };
|
|
249
|
+
const at = where?.file ? bothAt(where, root) : '';
|
|
250
|
+
return `<span>⇥ ${escape(p.name)}=<b>${escape(clip(p.value ?? '', 60))}</b>${at ? ` ${place(at, where, reveals)}` : ''}</span>`;
|
|
251
|
+
}),
|
|
252
|
+
made.length ? `<span>▸ made <b>${escape(made.join(', '))}</b></span>` : '',
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join('');
|
|
256
|
+
const next = `${node.id}|${node.kind}|${value}|${node.runs}|${state ?? ''}|${relations}|${at(node.source, root)}|${at(node.usedAt, root)}|${bound}`;
|
|
257
|
+
// An input that has left the page is not one anybody is typing into. Without this a
|
|
258
|
+
// commit that never arrived — the panel redrew, the tab changed — would hold the
|
|
259
|
+
// inspector on whatever it last wrote, for good.
|
|
260
|
+
if (editing !== null && field?.isConnected !== true) {
|
|
261
|
+
editing = null;
|
|
262
|
+
field = null;
|
|
263
|
+
}
|
|
264
|
+
if (editing !== null)
|
|
265
|
+
return;
|
|
266
|
+
if (next === signature)
|
|
267
|
+
return;
|
|
268
|
+
signature = next;
|
|
269
|
+
body.innerHTML =
|
|
270
|
+
`<div class="ins-head"><span class="ins-kind n-${node.kind}">${node.kind}</span>` +
|
|
271
|
+
`<b>${escape(node.label)}</b>` +
|
|
272
|
+
(synthetic
|
|
273
|
+
? '<span class="ins-meta">not a node</span></div>'
|
|
274
|
+
: `<span class="ins-meta">v${nodeVersion(node)} · ${node.runs} runs${state ? ` · ${state}` : ''}</span></div>`) +
|
|
275
|
+
(edits && node.kind === 'state'
|
|
276
|
+
? `<div class="ins-val ins-edit" data-id="${node.id}" data-raw="${escape(value)}"` +
|
|
277
|
+
` title="Click to set ${escape(node.label)}">${escape(clip(value))}</div>`
|
|
278
|
+
: `<div class="ins-val${value.length > 140 ? ' ins-more' : ''}"${value.length > 140 ? ` data-full="${escape(value)}" data-label="${escape(node.label)}"` : ''} title="${escape(value)}">${escape(clip(value))}</div>`) +
|
|
279
|
+
`<div class="ins-rel">${relations}</div>` +
|
|
280
|
+
// Only when the graph carries one. A snapshot from a build that was not marked has
|
|
281
|
+
// no locations at all, and an empty line there reads as missing data.
|
|
282
|
+
(node.source
|
|
283
|
+
? `<div class="ins-source">${node.source.external ? '📦' : '📍'} ${place(bothAt(node.source, root), node.source, reveals)}</div>`
|
|
284
|
+
: '') +
|
|
285
|
+
// A component is created where its tag is written; where it is declared is somewhere
|
|
286
|
+
// else, and only worth a line when it is.
|
|
287
|
+
(node.declaredAt && at(node.declaredAt, root) !== at(node.source, root)
|
|
288
|
+
? `<div class="ins-source">◧ declared ${place(at(node.declaredAt, root), node.declaredAt, reveals)}</div>`
|
|
289
|
+
: '') +
|
|
290
|
+
// A node a package made on the application's behalf: the package's line is where it
|
|
291
|
+
// was created, and this is the line that asked for it.
|
|
292
|
+
(node.usedAt
|
|
293
|
+
? `<div class="ins-source">↗ used at ${place(bothAt(node.usedAt, root), node.usedAt, reveals)}</div>`
|
|
294
|
+
: '') +
|
|
295
|
+
// Nothing said where it came from at all: the compiler did not mark this project and
|
|
296
|
+
// the stack had nothing to offer either. Saying so beats a card that just sits there.
|
|
297
|
+
(!node.source && !node.usedAt && !node.declaredAt
|
|
298
|
+
? '<div class="ins-source">— no source: build without sourceLocations</div>'
|
|
299
|
+
: '');
|
|
300
|
+
},
|
|
301
|
+
showElement(graph, pick) {
|
|
302
|
+
if (!pick) {
|
|
303
|
+
if (signature === 'empty')
|
|
304
|
+
return;
|
|
305
|
+
signature = 'empty';
|
|
306
|
+
body.innerHTML = `<span class="ins-empty">${escape(empty)}</span>`;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const owner = pick.owner === undefined ? undefined : graph.get(pick.owner);
|
|
310
|
+
const writers = (pick.writes ?? [])
|
|
311
|
+
.map((id) => graph.get(id))
|
|
312
|
+
.filter((n) => n !== undefined);
|
|
313
|
+
// Where the binding into this element was written. A value can write to several
|
|
314
|
+
// places, so it is the one naming this element that matters, not the first.
|
|
315
|
+
const bindingAt = (node) => node.targets?.find((t) => t.element === pick.label || t.element.endsWith(` in ${pick.label}`))
|
|
316
|
+
?.source;
|
|
317
|
+
const rows = writers
|
|
318
|
+
.map((node) => {
|
|
319
|
+
const where = bindingAt(node);
|
|
320
|
+
const text = where ? bothAt(where, root) : '';
|
|
321
|
+
return `<span>⌁ <b>${escape(node.label)}</b>${text ? ` ${place(text, where, reveals)}` : ''}</span>`;
|
|
322
|
+
})
|
|
323
|
+
.join('');
|
|
324
|
+
const relations = [
|
|
325
|
+
owner
|
|
326
|
+
? `<span>◧ rendered by <b>${escape(owner.label)}</b></span>`
|
|
327
|
+
: '<span>◧ rendered by <i>nothing in the graph</i></span>',
|
|
328
|
+
writers.length ? rows : '<span>⌁ <i>nothing writes here</i></span>',
|
|
329
|
+
...pluginElementRows(pick.extra).map((row) => {
|
|
330
|
+
const where = row.at && { ...row.at, file: row.at.file ?? owner?.source?.file };
|
|
331
|
+
const text = where?.file ? bothAt(where, root) : '';
|
|
332
|
+
const more = row.value !== undefined && row.value.length > 140;
|
|
333
|
+
return `<span${more
|
|
334
|
+
? ` class="ins-more" data-full="${escape(row.value)}" data-label="${escape(row.label)}"`
|
|
335
|
+
: ''} title="${escape(row.value ?? row.label)}">⌘ ${escape(row.label)}${row.value ? `=<b>${escape(clip(row.value))}</b>` : ''}${text ? ` ${place(text, where, reveals)}` : ''}</span>`;
|
|
336
|
+
}),
|
|
337
|
+
].join('');
|
|
338
|
+
// The element's own line when the build recorded one. Failing that, where the tag was
|
|
339
|
+
// declared rather than where it was used: an element sits in the component's body, so
|
|
340
|
+
// the line `<App/>` was written on says nothing about it.
|
|
341
|
+
const source = pick.at ?? (owner && (owner.declaredAt ?? authoredPlace(owner)));
|
|
342
|
+
// Where the framework built it, when that is somewhere else. Only on request: for an
|
|
343
|
+
// application it is a line in a package nobody asked about.
|
|
344
|
+
const madeIn = internals && owner?.source && owner.source !== source ? owner.source : undefined;
|
|
345
|
+
const next = `el|${pick.key}|${pick.label}|${relations}|${at(source, root)}|${at(madeIn, root)}`;
|
|
346
|
+
if (editing !== null && field?.isConnected !== true) {
|
|
347
|
+
editing = null;
|
|
348
|
+
field = null;
|
|
349
|
+
}
|
|
350
|
+
if (editing !== null)
|
|
351
|
+
return;
|
|
352
|
+
if (next === signature)
|
|
353
|
+
return;
|
|
354
|
+
signature = next;
|
|
355
|
+
body.innerHTML =
|
|
356
|
+
`<div class="ins-head"><span class="ins-kind n-element">element</span>` +
|
|
357
|
+
`<b>${escape(pick.label)}</b>` +
|
|
358
|
+
`<span class="ins-meta">not a node</span></div>` +
|
|
359
|
+
`<div class="ins-rel">${relations}</div>` +
|
|
360
|
+
// The tag's own line. An element has no position of its own in the graph — only the
|
|
361
|
+
// component that rendered it does — so this is as close as the source gets.
|
|
362
|
+
(source
|
|
363
|
+
? `<div class="ins-source">${source.external ? '📦' : '📍'} ${place(bothAt(source, root), source, reveals)}</div>`
|
|
364
|
+
: '') +
|
|
365
|
+
(madeIn
|
|
366
|
+
? `<div class="ins-source">📦 made in ${place(bothAt(madeIn, root), madeIn, reveals)}</div>`
|
|
367
|
+
: '');
|
|
368
|
+
},
|
|
369
|
+
clear() {
|
|
370
|
+
signature = '';
|
|
371
|
+
body.innerHTML = `<span class="ins-empty">${escape(empty)}</span>`;
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/** Default styling for what createInspector writes, in the shape DEVTOOLS_CSS is used. */
|
|
376
|
+
export const INSPECTOR_CSS = `
|
|
377
|
+
${VALUE_PANE_CSS}
|
|
378
|
+
/* Anything cut to fit says so, and opens the whole of it. */
|
|
379
|
+
.ins-more { cursor: zoom-in; }
|
|
380
|
+
.ins-header{ display: flex; align-items: center; gap: 8px; padding: .3rem .5rem; }
|
|
381
|
+
.ins-body { padding: .5rem; }
|
|
382
|
+
.ins-more:hover { text-decoration: underline dotted; }
|
|
383
|
+
.ins-empty { color: var(--fx-muted, #8a90a2); font-size: 12px; }
|
|
384
|
+
.ins-bar {
|
|
385
|
+
display: flex; align-items: center; gap: 5px; margin-bottom: 6px;
|
|
386
|
+
font-size: 10.5px; color: var(--fx-muted, #8a90a2); cursor: pointer; user-select: none;
|
|
387
|
+
}
|
|
388
|
+
.ins-bar:hover { color: var(--fx-label, #e7e9ee); }
|
|
389
|
+
.ins-bar input { width: 11px; height: 11px; margin: 0; accent-color: var(--fx-accent, #5eead4); }
|
|
390
|
+
.ins-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
391
|
+
.ins-head b { font-size: 13px; color: var(--fx-label, #e7e9ee); }
|
|
392
|
+
.ins-kind { font-family: ui-monospace, monospace; font-size: 9.5px; text-transform: uppercase; letter-spacing: 1px; }
|
|
393
|
+
${kindRules((k) => `.ins-kind.n-${k}`, 'color')}
|
|
394
|
+
.ins-meta { margin-left: auto; color: var(--fx-muted, #8a90a2); font-family: ui-monospace, monospace; font-size: 10.5px; }
|
|
395
|
+
.ins-val { font-family: ui-monospace, monospace; font-size: 12px; color: var(--fx-label, #e7e9ee); word-break: break-word; }
|
|
396
|
+
.ins-rel { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 6px; color: var(--fx-muted, #8a90a2); font-size: 11.5px; }
|
|
397
|
+
.ins-rel b { color: var(--fx-label, #e7e9ee); font-weight: 600; }
|
|
398
|
+
.ins-rel i { font-style: normal; opacity: .6; }
|
|
399
|
+
.ins-edit { cursor: text; border-bottom: 1px dashed var(--fx-line, #262a35); }
|
|
400
|
+
.ins-edit:hover { border-bottom-color: var(--fx-accent, #5eead4); }
|
|
401
|
+
.ins-input {
|
|
402
|
+
width: 100%; box-sizing: border-box; padding: 1px 4px;
|
|
403
|
+
font: inherit; font-family: ui-monospace, monospace;
|
|
404
|
+
color: var(--fx-label, #e7e9ee); background: var(--fx-bg, #0f1115);
|
|
405
|
+
border: 1px solid var(--fx-accent, #5eead4); border-radius: 3px;
|
|
406
|
+
}
|
|
407
|
+
.ins-at {
|
|
408
|
+
font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
409
|
+
cursor: pointer; text-decoration: underline; text-decoration-style: dotted;
|
|
410
|
+
text-underline-offset: 2px;
|
|
411
|
+
}
|
|
412
|
+
.ins-at:hover { color: var(--fx-accent, #5eead4); }
|
|
413
|
+
.ins-at:focus-visible { outline: 1px solid var(--fx-accent, #5eead4); outline-offset: 1px; }
|
|
414
|
+
.ins-source { margin-top: 6px; font-family: ui-monospace, monospace; font-size: 10.5px; color: var(--fx-muted, #8a90a2); }
|
|
415
|
+
`;
|