import { command } from "flux:subprocess" import { dir, file } from "flux:fs" import type { ServerWebSocket } from "flux:http" import { state } from "./state" import { armWatcher } from "./watcher" import type { BundleOutput } from "../types/bundle" // The single rebuild-and-push path: the initial bundle at start, every // MCP reload and every reload-on-save go through here. The bundle itself // runs in a bun subprocess (`sol bundle --json`, the one thing only bun can // do); this side latches the result for late-joining clients, broadcasts // it, and re-arms the watcher from the bundle's inputs (watcher.ts). // Build the reload message for the client protocol. `manifest` is the // bundle's version manifest JSON string; when present, clients install the // push into their version store before applying it (absent for the // build-failure trigger, which must never be installed). proxyHttp is a // message flag, not a build input. function buildReload(code: string, manifest?: string) { let config = state.config return { type: "reload", proxyHttp: config.proxyHttp, args: config.args, ...(manifest ? { manifest } : {}), code, } } // Reload code that fails to start the engine on purpose. The runtime treats a // startup error like any app that never called render() and falls back to its // baked-in BSOD screen, so a build that doesn't compile shows the BSOD instead // of leaving the previous app frozen on screen. const BSOD_TRIGGER = `throw new Error("SolidRT: build failed")` /** A push is going out to `ws`: until the client reports its first frame, * queries to it wait instead of timing out against an engine that is still * starting (control.ts awaitFirstFrame). Only for a runtime that sends * `ready` (ClientEntry `loading` non-null); an older one is queried right * away as before. */ export function pushed(ws: ServerWebSocket) { let info = state.clients.get(ws) if (!info || info.loading === null) return info.loading = true let entry = state.loading.get(ws) if (entry) entry.since = Date.now() else state.loading.set(ws, { since: Date.now(), waiters: [] }) } function latchAndSend(text: string) { state.currentReload = text for (let [ws, info] of state.clients) { // The runtime restarts the app's clock with the app. info.timeScale = 1 pushed(ws) ws.send(text) } } /** Latch the build-failure trigger and push it, so every client (and any * that connects later) shows the BSOD instead of a stale app. */ export function showBuildFailure() { state.currentMaps = null latchAndSend(JSON.stringify(buildReload(BSOD_TRIGGER))) } // Rebuild from config.entry via `sol bundle --json` (a bun subprocess run // in config.cwd with config.entryArgs, so it resolves the mode this server // did), then latch (for late-joining clients) and broadcast the reload to // every connected client. Resolves with an error message on failure (a build // error), or null on success. export async function rebuildAndBroadcast(): Promise { let config = state.config let args = [...config.sol.slice(1), "bundle", "--json", "--dev", ...config.entryArgs] if (config.minify) args.push("--minify") let result = await command(config.sol[0]!, args, { cwd: config.cwd }).output() if (!result.success) { let stderr = typeof result.stderr === "string" ? result.stderr : "" armWatcher(null) return `Rebuild failed:\n${stderr.trim()}` } // The bundle is one JSON object on stdout (types/bundle.d.ts). let bundle: BundleOutput try { bundle = JSON.parse(typeof result.stdout === "string" ? result.stdout : "") } catch { armWatcher(null) return "Rebuild failed: unreadable bundler output" } armWatcher(bundle.inputs ?? null) // Isolate bundles are manifest assets clients fetch from our /isolates/ // route (served from cacheDir), so they must be on disk before the push. let maps: Record = {} if (bundle.map) maps.main = bundle.map for (let isolate of bundle.isolates ?? []) { let path = `${config.cacheDir}/isolates/${isolate.id}.js` await dir(path.slice(0, path.lastIndexOf("/"))).create() await file(path).write(isolate.code) if (isolate.map) maps[isolate.id] = isolate.map } state.currentMaps = Object.keys(maps).length ? maps : null state.projectModules = bundle.projectModules ?? [] // The local client was resolved against an earlier bundle (main.ts): a // bundle that now imports a module of the project's runtime cannot run // on the stock one, which is what the first import will report. if (state.stockClientFor && state.projectModules.length > 0) { console.error( `[cli] The app imports ${state.projectModules[0]}, which only the project's runtime has, and the local client is the stock one (no solidrt-go at ${state.stockClientFor}). Build the runtime and restart the client with sol client.`, ) } latchAndSend(JSON.stringify(buildReload(bundle.code ?? "", bundle.manifest))) return null }