Web Bootstrap Emission — Migration Plan (2026)
Web Bootstrap Emission — Migration Plan (2026)
Section titled “Web Bootstrap Emission — Migration Plan (2026)”Produced by a 5-agent scouting workflow (
web-bootstrap-migration-scout) + Stage-0 verification. Goal: fully migrate the vox-mental-tracker web bootstrap to Vox-emitted output, eliminating the hand-written app-level split-brain.
Problem (verified)
Section titled “Problem (verified)”The web target emits only artifacts — {Component}.tsx, routes.manifest.ts,
routes.ts, vox-client.ts, types.ts — and pushes all bootstrap to
hand-written files (emitter.rs:171 generate_with_options). RN, by contrast,
emits a complete runnable bootstrap (rn/mod.rs:57 generate_rn → App.tsx /
Expo Router tree + full Expo scaffold). The mental-tracker hand-writes
src/main.tsx (entry + a client router over the emitted routes.manifest),
src/runtime.ts (runtime-global install), src/sync.ts (SW), and
src/ErrorBoundary.tsx; src/pages/SettingsPage.tsx is an orphan duplicate
of the emitted SettingsPage.
Stage-0 finding (verified)
Section titled “Stage-0 finding (verified)”str()→String()andlen()rewrites work — no barestr(/len(in emitted web. Those globals are not load-bearing.- Emitted components do reference
mobile.*(andSpeech.*/std.*) as bare globals. The emittedruntime-install.tsmust reproduce those browser-fallback globals and the__VOX_TEST_TRANSCRIPT__e2e hook (runtime.ts).
What WEB codegen must newly emit (new web_entry.rs)
Section titled “What WEB codegen must newly emit (new web_entry.rs)”Three artifacts into dist/ (overwritten every build — NOT the never-overwrite
scaffold.rs path, which is retired as the bootstrap mechanism):
entry.tsx—ReactDOM.createRoot(#root).render(<VoxApp/>)+ side-effect import ofruntime-install. Mirror of RNemit_app_tsx(rn/scaffold.rs:200).vox-app.tsx— genericBrowserRouter+RouteswalkingvoxRoutes(VoxRoute[],route_manifest.rs:168-192), generalizing the hand-writtenrenderRoutes(). Must branch to a flat single-component mount when no routes are declared (voxRoutesis absent —route_manifest.rs:118), usingscreen_root_component_names(hir).runtime-install.ts— installs the browser-fallbackmobile/Speech/stdglobals +__VOX_TEST_TRANSCRIPT__. (Follow-up C2: publish@vox/runtime-web-shims.)
Wire-in: push the three pairs into files from emitter.rs after the component
loop, gated on mode != Library and !hir.components.is_empty().
Delete vs keep (app src/)
Section titled “Delete vs keep (app src/)”| File | Verdict | Why |
|---|---|---|
src/main.tsx | DELETE | Superseded by emitted entry.tsx + vox-app.tsx. |
src/runtime.ts | DELETE (after Stage 2) | Becomes emitted runtime-install.ts. |
src/pages/SettingsPage.tsx | DELETE | Confirmed orphan — emitted SettingsPage is canonical. |
src/ErrorBoundary.tsx | KEEP | Real app glue (React error boundary + crash log); no Vox equivalent. |
src/sync.ts | KEEP | Real app glue (PWA service-worker registration). |
index.html | KEEP (edit) | Provides #root; repoint script src/main.tsx → dist/entry.tsx. |
Glue hook: emitted entry.tsx looks for an optional src/app-hooks.ts
exporting { wrapApp?, onBoot? } — ErrorBoundary wraps via wrapApp,
registerServiceWorker runs via onBoot. Keeps the genuine glue app-owned
without a hand-written bootstrap.
Tests to keep green
Section titled “Tests to keep green”tests/runtime_shim.test.tsimports../src/runtime→ repoint to emitted shim.tests/e2e/voice_flow.spec.tsneeds__VOX_TEST_TRANSCRIPT__→ emitted shim must keep it.package.jsonbuild chain (build:vox→vite build) is unchanged; emitted files land indist/.
Default & opt-out
Section titled “Default & opt-out”- Default: emit full bootstrap every web build (parity with RN, single source).
- Opt-out:
--no-emit-entry/entry = falseviaCodegenOptionsfor external-React consumers (interop Phase 5). Inverts today’s opt-in--scaffold/VOX_WEB_EMIT_SCAFFOLD, which is retired (build.rs:294-300); keep--scaffoldas a deprecated alias one release.
Staged build order (each its own PR; gates: vox check, web build, app e2e, arch-check)
Section titled “Staged build order (each its own PR; gates: vox check, web build, app e2e, arch-check)”- ✅ Resolve the
str-global unknown — not load-bearing;mobile/Speech/stdare. - ✅ Emit
vox-app.tsxrouter —web_entry.rs; dependency-free history router; tsc-clean vsroutes.manifest. - ✅ Emit
runtime-install.ts— generic browser shims +__VOX_TEST_TRANSCRIPT__. - ✅ Emit
entry.tsx+ defaultapp-hooks.tsx; repointindex.html— app glue via app-ownedsrc/app-hooks.tsx(overrides the emitted default viapostbuild-fixup.mjs). - ✅ Delete orphans —
main.tsx,runtime.ts,pages/SettingsPage.tsx; repointruntime_shim.test.ts. - ✅ Opt-out + glue + scaffold retirement —
--no-emit-entry(VOX_WEB_NO_EMIT_ENTRY) wired; the hand-written glue (ErrorBoundary/sync/app-hooks) folded into emitted defaults (vox-error-boundary.tsx+vox-sw-register.tswired into the defaultapp-hooks.tsx) so the mental-tracker’ssrc/is nowmain.voxonly — zero hand-written TS.scaffold.rsbootstrap retired:react_interop_scaffold_files→web_config_files(config-only; nomain.tsx/App.tsx),--scaffold→--emit-config(deprecated alias kept).
Desktop (Tauri): no change needed — the emitted bootstrap (entry.tsx mounting #root over the dependency-free router) runs unmodified inside a Tauri 2 webview; the same web-dist bundle is the desktop frontend.
Done. All five stages landed and verified. A fresh Vox web app is fully generated from .vox with zero hand-written TypeScript.
Verification: web_entry unit tests + cli-tests fixtures (20/0) + the app’s Vite bundle of the emitted bootstrap + Playwright e2e (build:web must out-run Playwright’s 180s webServer timeout — pre-build the release vox binary so build:vox is fast).
#rootassumption implicit inentry.tsx—index.htmlmust keepid="root".- Router lock-in: emitted
vox-app.tsxhardcodesreact-routerv7; external/TanStack consumers use--no-emit-entry. - No-routes path must branch to flat mount or route-less apps break.
__VOX_TEST_TRANSCRIPT__must survive the move into emitted code.
Key files: new crates/vox-codegen-ts/src/web_entry.rs;
emitter.rs:171-225 (wire-in + CodegenOptions flag); build.rs:294-300
(retire scaffold trigger); scaffold.rs (retire); app: index.html (repoint),
new src/app-hooks.ts, delete main.tsx/runtime.ts/pages/SettingsPage.tsx.