|
| 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 | +``` |
0 commit comments