Implements ADR 001 — Dual-build (docs/adr/001-dual-build-host-bundle.md, status: proposed). Refs docs/adr/001 §3.
Problem
The served bundle (/orion/static/solar/v0.2.0/, loaded by Pulsar CEF) ships bare ESM specifiers (react, react-dom/client, @preact/signals-react, @preact/signals-react/runtime, framer-motion, react/jsx-runtime, motion*) because vite.config.ts builds library mode with deps as rollupOptions.external. The CEF has no bundler and no import map → mount() never runs → black frame. This is B4, the last blocker to broadcast.
Scope (Solar repo)
- Add a second Vite build target — "host / standalone" (
docs/adr/001 §3.1):
- New config (e.g.
vite.config.host.ts, or --mode host switch). Non-library app build. No rollupOptions.external — Rollup inlines react / react-dom / @preact/signals-react / framer-motion / motion* into the chunks.
- Host entry that imports
mount from src and bootstraps from URL params (the current build-host-html.mjs bootstrap surface: orion/token/mode/scene/session, #scene target, onError console-only).
- Output to
dist/host/** (self-contained index.html + hashed JS/CSS).
- Keep the library build untouched (
docs/adr/001 §3.1, D2): dist/solar.js keeps externals; package.json exports map and main/module unchanged — Prism vendors this entry as-is.
npm run build runs both sequentially (library, then host, then host-html reconciliation). Reconcile scripts/build-host-html.mjs: if the host app build already emits a hashed index.html importing the hashed JS, reduce/remove the script; else point it at the host bundle filename. Minimal mechanic at Forge's discretion — the invariant is the test below.
- Anti-bare-specifier test (
docs/adr/001 §6.2): scan the built dist/host/** JS for any import/from of a non-relative / non-absolute specifier; fail on any survivor. Wire it into CI as a gate.
- Browser smoke test (
docs/adr/001 §6.3): Playwright loads dist/host/index.html + JS in a real browser with no import map against mock-orion; assert #scene mounts and there is no unresolved-module console error.
- Host-bundle size budget (
docs/adr/001 §4): add a host-bundle budget line to scripts/check-bundle-size.mjs (deps inlined → bigger than library; do NOT hold it to the 200/280 KiB library budgets).
- Bump to v0.2.1 (
docs/adr/001 §3.2): package.json version → 0.2.1; CHANGELOG [0.2.1] entry (patch — served-bundle bundling fix, mount()/LSDP unchanged). meta[name=generator] reflects 0.2.1. Served path becomes /orion/static/solar/v0.2.1/.
Branch
forge/solar-host-bundle-v0.2.1
Acceptance criteria (= ADR 001 §6)
Out of scope
- No change to
mount() / SolarError / LSDP wire dialect (would be a minor/major, not this patch).
- Orion serving config and Pulsar CEF URL flip: not touched here — same URL shape, new version path. Conduit validates the Orion↔Pulsar bundle contract; Keeper deploys v0.2.1.
Implements ADR 001 — Dual-build (
docs/adr/001-dual-build-host-bundle.md, status: proposed). Refsdocs/adr/001 §3.Problem
The served bundle (
/orion/static/solar/v0.2.0/, loaded by Pulsar CEF) ships bare ESM specifiers (react,react-dom/client,@preact/signals-react,@preact/signals-react/runtime,framer-motion,react/jsx-runtime,motion*) becausevite.config.tsbuilds library mode with deps asrollupOptions.external. The CEF has no bundler and no import map →mount()never runs → black frame. This is B4, the last blocker to broadcast.Scope (Solar repo)
docs/adr/001 §3.1):vite.config.host.ts, or--mode hostswitch). Non-library app build. NorollupOptions.external— Rollup inlines react / react-dom / @preact/signals-react / framer-motion / motion* into the chunks.mountfromsrcand bootstraps from URL params (the currentbuild-host-html.mjsbootstrap surface:orion/token/mode/scene/session,#scenetarget,onErrorconsole-only).dist/host/**(self-containedindex.html+ hashed JS/CSS).docs/adr/001 §3.1, D2):dist/solar.jskeeps externals;package.jsonexportsmap andmain/moduleunchanged — Prism vendors this entry as-is.npm run buildruns both sequentially (library, then host, then host-html reconciliation). Reconcilescripts/build-host-html.mjs: if the host app build already emits a hashedindex.htmlimporting the hashed JS, reduce/remove the script; else point it at the host bundle filename. Minimal mechanic at Forge's discretion — the invariant is the test below.docs/adr/001 §6.2): scan the builtdist/host/**JS for anyimport/fromof a non-relative / non-absolute specifier; fail on any survivor. Wire it into CI as a gate.docs/adr/001 §6.3): Playwright loadsdist/host/index.html+ JS in a real browser with no import map against mock-orion; assert#scenemounts and there is no unresolved-module console error.docs/adr/001 §4): add a host-bundle budget line toscripts/check-bundle-size.mjs(deps inlined → bigger than library; do NOT hold it to the 200/280 KiB library budgets).docs/adr/001 §3.2):package.jsonversion →0.2.1; CHANGELOG[0.2.1]entry (patch — served-bundle bundling fix,mount()/LSDP unchanged).meta[name=generator]reflects 0.2.1. Served path becomes/orion/static/solar/v0.2.1/.Branch
forge/solar-host-bundle-v0.2.1Acceptance criteria (= ADR 001 §6)
npm run buildemits two artefacts: library entry (dist/solar.js, externals preserved) and host bundle (dist/host/**).dist/host/index.html+ JS load andmount()in a real browser with no import map (Playwright/mock-orion):#scenemounts, no unresolved-module console error.import { mount } from "@zablab/solar"resolves to externals build;exportsunchanged; library gzip budgets still pass.package.json0.2.1+ CHANGELOG[0.2.1]+ generator meta0.2.1.Out of scope
mount()/SolarError/ LSDP wire dialect (would be a minor/major, not this patch).