Opt-in DOM inspector rendering the session, module-registry and signal-graph views.
Module devtoolsUI | Source packages/front/fw/src/dom/rendering/devtools-ui.js | Deps devtools | Worker-safe no
devtools exposes data only. devtoolsUI is the fw-side, no-bundler renderer of
that data: it turns devtools.inspect(), runtime.snapshot() and
signal.inspectGraph() into DOM, and does nothing else.
Resolve
const devtoolsUI = runtime.resolve('devtoolsUI');
// Returns: { render, renderSession, renderRegistry, renderGraph }
Opt-in contract
- Importing the file mounts nothing. No side effect at import, no ambient
read, no auto-mount. Measured in a fresh realm: after importing the module,
document.bodyhas zero children and zero text. - Instantiating mounts nothing.
factory()builds closures only. - A view exists only after an explicit
render*call with a target element the caller supplies. Nothing is discovered, scanned or hooked for you. - Each call appends a fresh subtree and returns it; the target is never
cleared. The caller owns the lifetime (
root.remove()).
Read-only guarantee
The inspector never writes back into what it inspects. Asserted against the sources, not the view:
| Source | Read through | What is guaranteed |
|---|---|---|
uiSession |
devtools.inspect() / summarize() |
Session snapshot identical before/after; the session's own container DOM untouched |
| Module registry | runtime.snapshot() |
Frozen metadata rows; runtime.instances unchanged — rendering instantiates nothing |
| Signal graph | signal.inspectGraph() |
Graph identical before/after; no effect re-runs and no dependency is registered |
runtime.list() is deliberately rejected by renderRegistry: it hands back
live ModuleDefinitions (splicable dependencies, callable factory), which
would make the view a write channel. Only snapshot() is accepted.
Every value reaches the DOM through textContent, so no inspected string can
ever be interpreted as markup.
API
| Method | Signature | Returns |
|---|---|---|
render |
(target: Element, sources?: { session?, runtime?, signal?, filter? }) => Element |
Composite root <section class="fw-devtools-ui">, one view per supplied source, in the fixed order session → registry → graph |
renderSession |
(target: Element, session: object) => Element |
<section data-view="session"> — devtools.inspect() view |
renderRegistry |
(target: Element, runtime: object, filter?: object) => Element |
<section data-view="registry"> — runtime.snapshot(filter) view |
renderGraph |
(target: Element, signal: object) => Element |
<section data-view="graph"> — signal.inspectGraph() view |
All four throw when target is not a DOM element. renderRegistry throws
unless its source exposes snapshot(); renderGraph throws unless its source
exposes inspectGraph().
The three views
- Session — container, one-line
summarize(), then tables for blocks (block/logical ids/loop), listeners, lists, and a counts table for the remaining collections. - Module registry — one row per registered version:
module/version/type/dependencies/latest/instantiated. - Signal graph — the live effects (
effect/deps), the signals they observe (signal/kind) and the edges between them.
Examples
const devtoolsUI = runtime.resolve('devtoolsUI');
const panel = document.querySelector('#inspector');
// Everything at once.
const root = devtoolsUI.render(panel, {
session: myUiSession,
runtime, // read through runtime.snapshot()
signal: signalInstance, // the signal.factory() INSTANCE, not the descriptor
});
// …later
root.remove();
// One view at a time, with a registry filter.
devtoolsUI.renderRegistry(panel, runtime, { type: 'fw.dom.' }); // prefix match
devtoolsUI.renderGraph(panel, signalInstance);
Notes
- The signal-graph view is partial by design, and says so on screen. It is
the effect dependency graph: a signal no live effect observes is not a node,
and the own sources of a
computedare not edges (acomputedread by an effect does appear, as a node of kindcomputed). The rendered note states this — absence in the view is not absence in the app. - The graph is per
signal.factory()instance. Pass the instance your app actually uses; a module descriptor is rejected, and a different instance would report a different (correct but unrelated) graph. - Lockdown-compatible, measured with zero carve-outs. The single dependency
devtoolsis itself measured lockdown-clean, and the DOM surface used here (createElement,appendChild,textContent,setAttribute,className) works post-lockdown()— seepackages/front/fw/tests/devtools-ui-lockdown.integration.test.js, which drives a pre-freeze instance, a fresh post-freeze factory and the canonicalruntime.resolve()path, with a control assertion proving the freeze applied. - No live updates. Views are snapshots at call time; there is no "session mutated" hook to subscribe to. Re-render to refresh.
- The module registers no listener, installs no timer and holds no state between calls, so a rendered view can be dropped by removing its root element.