Skip to content

Commit e5d70e5

Browse files
committed
chore: spike throwaway data-viewer prototype (jora live-query + discovery viewer)
Validates the live-query data-viewer architecture end to end: server-side jora over live registered objects (incl. the ViteDevServer serving the page), result normalizer, devframe RPC round-trip, remote stat-mode autocomplete, and a themed discovery struct view in devframe-style chrome. Stage-1 node spike passes 13/13 checks; findings and hazards documented in the prototype README. Throwaway code — lives on this spike branch as a primary source, not for merging into main.
1 parent 7d31965 commit e5d70e5

16 files changed

Lines changed: 1486 additions & 3 deletions
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# prototype-data-viewer
2+
3+
> **PROTOTYPE — throwaway code.** This is not an example and not a plugin.
4+
> It exists to answer one question and is kept on its spike branch as a
5+
> primary source. Do not merge, extend, or depend on it.
6+
7+
## The question
8+
9+
Can a **live-query data-viewer plugin** work on devframe? Architecture under
10+
test (decided 2026-07-16): queries always execute **server-side** against
11+
**live registered objects** (jora), results are normalized and returned over
12+
devframe RPC, and the browser renders them with a **themed discovery `struct`
13+
view** inside devframe-style chrome — with remote autocomplete from jora's
14+
stat mode. No stock discovery explorer page, no client-side dataset.
15+
16+
## Verdict: YES — architecture validated end to end
17+
18+
Stage 1 (`pnpm --filter prototype-data-viewer spike`): **13/13 checks pass**
19+
against a real programmatic `ViteDevServer`.
20+
21+
Stage 2 (`pnpm --filter prototype-data-viewer dev`, then open the page):
22+
full loop works in-browser against the Vite server serving the page itself —
23+
e.g. this query over the live Vite 8 environment module graph:
24+
25+
```jora
26+
environments.client.moduleGraph.idToModuleMap.mapEntries().value.({ url, type, importers: importers.fromSet().url })
27+
```
28+
29+
renders 20 modules in discovery's struct view at `rpc 9ms · jora 0.63ms ·
30+
normalize 0.25ms · 101 nodes`, with server-side suggestions and synced
31+
light/dark theming.
32+
33+
## Findings (things the real plugin must know)
34+
35+
1. **Registry pattern**: `WeakMap<DevframeNodeContext, Map<id, entry>>`
36+
(`src/registry.ts`) works exactly like `plugins/git/src/rpc/context.ts`.
37+
The Vite host registers the live server in `configureServer` where both
38+
the context and the `ViteDevServer` are in scope (`vite.config.ts`).
39+
2. **jora on live objects needs bridging methods** (`src/query-engine.ts`):
40+
Map/Set are opaque to raw jora, so ship `fromMap()` / `mapEntries()` /
41+
`fromSet()` / `ownKeys()` / `typeOf()`. **Duck-type Map-likes**: Vite 8's
42+
backward-compat `server.moduleGraph.idToModuleMap` is a Map-shaped plain
43+
object, not a `Map` instance.
44+
3. **Vite 8 compat `ModuleNode`s are getter facades** — own keys are only
45+
`_moduleGraph`/`_clientModule`/`_ssrModule`; the public props live on the
46+
prototype and are invisible to jora. Query
47+
`environments.<name>.moduleGraph` instead (real own fields).
48+
4. **The normalizer is the core asset** (`src/normalize.ts`): circular→`$ref`,
49+
Map/Set/class/function/BigInt/Error tagging, depth/entry/prop/string caps.
50+
Whole live `ResolvedConfig` → 873 nodes / 24 KB JSON in ~0.5 ms. With it,
51+
results are strict-JSON (`jsonSerializable: true` RPC).
52+
5. **Remote autocomplete works**: jora stat mode (`{ stat: true, tolerant:
53+
true }`) runs server-side in ~0.2–0.5 ms; completions arrive nested in a
54+
`suggestions` array per stat entry — flatten before shipping. Client
55+
debounces and re-requests (`src/client/main.ts`). jora does NOT
56+
prefix-filter; that's the client's job (this prototype doesn't).
57+
6. **discovery embeds fine in Vite** with two caveats: it needs
58+
`define: { global: 'globalThis' }` (throws `global is not defined`
59+
otherwise — webpack shims this, Vite doesn't), and its CSS ships via
60+
`?inline` import into the ViewModel's shadow root. Theming =
61+
`host.colorScheme.set()` + `--discovery-*` custom props set on the host
62+
element (`themeBridge` in `src/client/main.ts`).
63+
7. **Render path**: `new ViewModel({ container, styles })`
64+
`host.view.render(host.dom.pageContent, { view: 'struct', expanded: 2 },
65+
data, {})`. No `setData` needed for per-query rendering.
66+
8. **HAZARD (by design of live-query mode)**: a jora query can invoke any
67+
function reachable as an own property of the live object
68+
(`$f: danger.selfDestruct; $f()` mutated state in the spike — on the real
69+
server that could be `close()`), and own getters fire during traversal.
70+
jora also has no CPU/timeout limits. The real plugin must present this as
71+
eval-grade access to the dev server (localhost-only posture, no third-party
72+
exposure), and pin `jora >= 1.0.0-beta.16` (JS-injection fix landed in
73+
beta.14).
74+
9. **Versions pinned deliberately**: `jora@1.0.0-beta.16`,
75+
`@discoveryjs/discovery@1.0.0-beta.99` — both beta with breaking renames
76+
between betas; the real plugin should pin exact versions via a catalog.
77+
78+
## Deviations from repo conventions (prototype-only)
79+
80+
- `jora` + `@discoveryjs/discovery` pinned in the *default* catalog (pnpm
81+
auto-promoted them); a real plugin would place them in a named catalog
82+
- hand-rolled CSS chrome instead of UnoCSS + `@antfu/design` ports
83+
- plain textarea editor; production should embed discovery's `QueryEditor`
84+
(CodeMirror 5 + jora mode) with the async-suggestion re-trigger pattern
85+
86+
## Run it
87+
88+
```sh
89+
pnpm --filter prototype-data-viewer spike # stage 1: node-only proof, prints 13 checks
90+
pnpm --filter prototype-data-viewer dev # stage 2: http://localhost:5173/
91+
```
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Data Viewer — PROTOTYPE</title>
7+
<style>
8+
/*
9+
PROTOTYPE chrome — hand-rolled approximation of the devframe design
10+
tokens (a real plugin ports @antfu/design via UnoCSS; this spike only
11+
proves the discovery panel can sit inside devframe-style chrome and
12+
follow its light/dark scheme).
13+
*/
14+
:root {
15+
--bg-base: #ffffff;
16+
--bg-secondary: #f6f6f5;
17+
--bg-active: #ececea;
18+
--color-base: #383836;
19+
--color-muted: #71716e;
20+
--color-faint: #9c9c98;
21+
--border-base: #e4e4e2;
22+
--primary: #3a6a45;
23+
--primary-soft: #3a6a4518;
24+
--danger: #ab3f2e;
25+
}
26+
html.dark {
27+
--bg-base: #121212;
28+
--bg-secondary: #1b1b1b;
29+
--bg-active: #262626;
30+
--color-base: #dbdbd8;
31+
--color-muted: #8f8f8b;
32+
--color-faint: #6b6b67;
33+
--border-base: #2a2a2a;
34+
--primary: #7fb08a;
35+
--primary-soft: #7fb08a1c;
36+
--danger: #e08373;
37+
}
38+
* { box-sizing: border-box; }
39+
body {
40+
margin: 0;
41+
font: 14px/1.5 -apple-system, 'DM Sans', 'Segoe UI', sans-serif;
42+
background: var(--bg-base);
43+
color: var(--color-base);
44+
height: 100vh;
45+
display: flex;
46+
flex-direction: column;
47+
}
48+
header {
49+
display: flex; align-items: center; gap: 12px;
50+
padding: 8px 14px;
51+
border-bottom: 1px solid var(--border-base);
52+
flex: none;
53+
}
54+
header .brand { display: flex; align-items: center; gap: 8px; font-weight: 600; }
55+
header .brand .logo {
56+
width: 22px; height: 22px; border-radius: 5px;
57+
background: var(--primary-soft); color: var(--primary);
58+
display: grid; place-items: center; font-size: 13px;
59+
}
60+
header .brand .proto {
61+
font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
62+
color: var(--danger); border: 1px solid currentColor;
63+
padding: 1px 6px; border-radius: 99px;
64+
}
65+
select, button {
66+
font: inherit; color: var(--color-base);
67+
background: var(--bg-secondary);
68+
border: 1px solid var(--border-base);
69+
border-radius: 6px; padding: 4px 10px;
70+
}
71+
button { cursor: pointer; }
72+
button:hover { background: var(--bg-active); }
73+
button.primary { background: var(--primary); border-color: var(--primary); color: #fff; }
74+
.spacer { flex: 1; }
75+
#status { font-size: 12px; color: var(--color-muted); }
76+
77+
#workbench { display: flex; flex-direction: column; flex: 1; min-height: 0; }
78+
#editor-row { position: relative; flex: none; padding: 10px 14px 4px; }
79+
#query {
80+
width: 100%; min-height: 64px; resize: vertical;
81+
font: 13px/1.6 ui-monospace, 'DM Mono', 'Fira Code', monospace;
82+
color: var(--color-base); background: var(--bg-secondary);
83+
border: 1px solid var(--border-base); border-radius: 8px;
84+
padding: 10px 12px; outline: none;
85+
}
86+
#query:focus { border-color: var(--primary); }
87+
#examples { display: flex; gap: 6px; flex-wrap: wrap; padding: 6px 14px; }
88+
#examples button {
89+
font-size: 12px; font-family: ui-monospace, monospace;
90+
padding: 2px 8px; border-radius: 99px; color: var(--color-muted);
91+
}
92+
#suggest {
93+
position: absolute; z-index: 10; left: 26px; top: 100%;
94+
min-width: 260px; max-height: 240px; overflow: auto;
95+
background: var(--bg-base); border: 1px solid var(--border-base);
96+
border-radius: 8px; box-shadow: 0 8px 24px #0003; display: none;
97+
}
98+
#suggest .item {
99+
display: flex; justify-content: space-between; gap: 16px;
100+
padding: 4px 10px; cursor: pointer;
101+
font: 12px/1.6 ui-monospace, monospace;
102+
}
103+
#suggest .item.active, #suggest .item:hover { background: var(--primary-soft); }
104+
#suggest .item .type { color: var(--color-faint); font-size: 11px; }
105+
#result-panel {
106+
flex: 1; min-height: 0; margin: 8px 14px 14px;
107+
border: 1px solid var(--border-base); border-radius: 8px;
108+
overflow: hidden; background: var(--bg-base);
109+
}
110+
#result { height: 100%; overflow: auto; }
111+
#error {
112+
display: none; margin: 0 14px 10px; padding: 8px 12px;
113+
border: 1px solid var(--danger); border-radius: 8px;
114+
color: var(--danger); font-family: ui-monospace, monospace; font-size: 12px;
115+
}
116+
</style>
117+
</head>
118+
<body>
119+
<header>
120+
<div class="brand">
121+
<span class="logo"></span>
122+
<span>Data Viewer</span>
123+
<span class="proto">PROTOTYPE</span>
124+
</div>
125+
<select id="source"></select>
126+
<button id="run" class="primary" title="Ctrl/Cmd+Enter">Run ▸</button>
127+
<div class="spacer"></div>
128+
<span id="status">connecting…</span>
129+
<button id="theme" title="toggle color scheme"></button>
130+
</header>
131+
<div id="workbench">
132+
<div id="editor-row">
133+
<textarea id="query" spellcheck="false" placeholder="jora query, e.g. config.plugins.name — Ctrl+Space for suggestions"></textarea>
134+
<div id="suggest"></div>
135+
</div>
136+
<div id="examples"></div>
137+
<div id="error"></div>
138+
<div id="result-panel"><div id="result"></div></div>
139+
</div>
140+
<script type="module" src="/src/client/main.ts"></script>
141+
</body>
142+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "prototype-data-viewer",
3+
"type": "module",
4+
"version": "0.0.0",
5+
"private": true,
6+
"description": "PROTOTYPE (throwaway) — validates the live-query data-viewer architecture: server-side jora over live objects + RPC + themed discovery struct view. Not a real example; see README.",
7+
"scripts": {
8+
"dev": "vite --host",
9+
"spike": "tsx src/spike.ts",
10+
"typecheck": "tsc --noEmit"
11+
},
12+
"dependencies": {
13+
"@discoveryjs/discovery": "catalog:",
14+
"devframe": "workspace:*",
15+
"jora": "catalog:"
16+
},
17+
"devDependencies": {
18+
"get-port-please": "catalog:deps",
19+
"tsx": "catalog:build",
20+
"vite": "catalog:build"
21+
}
22+
}

0 commit comments

Comments
 (0)