* DEV: spawn Vite as a child process (NOT via createServer in-process — Vite's * config loader fights with the tsx loader powering this CLI), wait for the dev * server, then open the browser. Narrows the bridge's allowlist to the bound * file's directory via FH_BUILDER_ALLOW_ROOT.
(resolvedFile?: string, allowRoot?: string)
| 75 | * file's directory via FH_BUILDER_ALLOW_ROOT. |
| 76 | */ |
| 77 | async function openDev(resolvedFile?: string, allowRoot?: string): Promise<void> { |
| 78 | const here = path.dirname(fileURLToPath(import.meta.url)); |
| 79 | const appRoot = path.resolve(here, ".."); |
| 80 | |
| 81 | const env: NodeJS.ProcessEnv = { ...process.env }; |
| 82 | if (allowRoot) env.FH_BUILDER_ALLOW_ROOT = allowRoot; |
| 83 | |
| 84 | const port = 5173; |
| 85 | const url = resolvedFile |
| 86 | ? `http://localhost:${port}/?file=${encodeURIComponent(resolvedFile)}` |
| 87 | : `http://localhost:${port}/`; |
| 88 | |
| 89 | const viteBin = locateViteBin(appRoot); |
| 90 | // --no-open: this CLI is the sole opener — it builds the `?file=` URL and |
| 91 | // opens it below. Without this, vite.config's `server.open: true` would also |
| 92 | // open a tab, at the bare URL (no ?file=), so you'd get two tabs and the |
| 93 | // wrong one focused. (CLI flag overrides config; `dev` still auto-opens.) |
| 94 | const vite = spawn(viteBin, ["--port", String(port), "--host", "127.0.0.1", "--strictPort", "--no-open"], { |
| 95 | cwd: appRoot, |
| 96 | env, |
| 97 | stdio: "inherit", |
| 98 | shell: process.platform === "win32", // .cmd shims need a shell on Windows |
| 99 | }); |
| 100 | |
| 101 | vite.on("error", (err) => { |
| 102 | process.stderr.write(`Failed to start vite: ${err.message}\n`); |
| 103 | process.exit(1); |
| 104 | }); |
| 105 | |
| 106 | // Poll until the port answers, then open the browser. Bounded so we don't |
| 107 | // hang forever if vite never starts. |
| 108 | void (async () => { |
| 109 | const ok = await waitForPort(port, 15000); |
| 110 | if (!ok) { |
| 111 | process.stderr.write("Vite didn't come up within 15s.\n"); |
| 112 | return; |
| 113 | } |
| 114 | process.stdout.write(`\nfh-workflow running at ${url}\n`); |
| 115 | if (resolvedFile) process.stdout.write(`Bound to ${resolvedFile}\n`); |
| 116 | process.stdout.write("Press Ctrl+C to stop.\n\n"); |
| 117 | openInBrowser(url); |
| 118 | })(); |
| 119 | |
| 120 | // Keep our process alive until vite exits (stdio is inherited, so Ctrl+C |
| 121 | // goes straight to vite which then exits, then we exit). |
| 122 | await new Promise<void>((resolve) => { |
| 123 | vite.on("exit", (code) => { |
| 124 | process.exit(code ?? 0); |
| 125 | resolve(); |
| 126 | }); |
| 127 | }); |
| 128 | } |
| 129 | |
| 130 | function locateViteBin(appRoot: string): string { |
| 131 | const cmd = process.platform === "win32" ? "vite.cmd" : "vite"; |
no test coverage detected