#!/usr/bin/env bun // bin/sol lands here. This file routes the command word and nothing else: // the bun commands live one folder each (src//main.ts) and load on // demand, and `run`/`server` launch the flux dev server (src/server/), a // process complete on its own that this launcher only resolves the binaries // for (okf/done/sol-command-folders.md). import { fileURLToPath } from "node:url" import { existsSync, unlinkSync } from "node:fs" import { tmpdir } from "node:os" import { resolve } from "node:path" import { values, command, source, port, appArgs, validateArgs } from "./lib/args" import { printUsage, printVersion, hint } from "./lib/usage" import { requireBinary } from "./lib/util" import { buildServerBundle } from "./lib/server-bundle" import { fluxScriptEnv } from "./lib/flux-script" import { UsageError } from "./lib/fail" // -- Help and version -- // Answered before anything else: neither takes a command, so they must not // fall through to the usage error. if (values.help) { printUsage() process.exit(0) } if (values.version) { printVersion() process.exit(0) } // -- Validate args -- validateArgs() // Force the production export condition for prod bundles. Bun auto-activates the // "development" condition whenever NODE_ENV != "production" (read once at startup), // and an auto-active condition cannot be turned off via Bun.build({ conditions }). // Our deps (@solidjs/signals, solid-js) only expose a "development" branch + a // default fallback - no "production" key - so adding conditions does nothing; the // only way to reach the default (smaller, no extra invariants) build is to stop // the auto-activation by setting NODE_ENV=production. Since that is read at startup, // we re-exec rather than mutate process.env. Assumes sol runs via bun (argv is // [bun, script, ...]); would need rework if ever shipped as a compiled binary. // `android --apk` packs too. let packs = command === "pack" || (command === "android" && values.apk) let isProdBuild = (command === "bundle" || command === "render" || packs) && !values.dev if (isProdBuild && process.env.NODE_ENV !== "production") { let proc = Bun.spawnSync({ cmd: [process.execPath, ...process.argv.slice(1)], env: { ...process.env, NODE_ENV: "production" }, stdin: "inherit", stdout: "inherit", stderr: "inherit", }) process.exit(proc.exitCode ?? 0) } // -- run / server: launch the dev server -- // The server script the flux binary runs: the prebuilt bundle a published // CLI ships, or (in a checkout) one built now into a temp file, removed on // exit. async function serverScript(): Promise<{ path: string; temp: boolean }> { let prebuilt = fileURLToPath(new URL("../dist/server.js", import.meta.url)) if (existsSync(prebuilt)) return { path: prebuilt, temp: false } let outfile = resolve(tmpdir(), `sol-dev-server-${process.pid}.js`) await buildServerBundle(outfile) return { path: outfile, temp: true } } // `demo` serves a demos project the same way `run` serves any other one: // the entry is resolved for the caller (src/demo/main.ts) and the server is // started IN that project, so mode.ts resolves it as a project and its // assets/ folder is served like any app's. async function launchServer(withClient: boolean, demo?: { cwd: string; entry: string }) { let flux = requireBinary("flux") let script = await serverScript() let args: string[] = [] if (demo) { args.push(demo.entry, "--project") } else { if (source !== undefined) args.push(source) if (values.project) args.push("--project") if (values.file) args.push("--file") } if (port !== undefined) args.push("--port", String(port)) for (let flag of ["lan", "proxy-http", "tunnel", "stats", "minify"] as const) { if (values[flag]) args.push(`--${flag}`) } if (values.capture) args.push("--capture", values.capture) if (withClient) { args.push("--client", values.client ?? "0") if (values["data-root"]) args.push("--data-root", values["data-root"]) if (values.size) args.push("--size", values.size) } if (appArgs.length) args.push("--", ...appArgs) let proc = Bun.spawn([flux, script.path, ...args], { cwd: demo?.cwd, // stdin is passed through for the server's repl (flux:tty): with no // terminal there the server runs without one. stdio: ["inherit", "inherit", "inherit"], env: fluxScriptEnv(flux), }) // The server ends itself on these (drops its record, stops the client); // this process just relays them and waits. let relay = () => proc.kill("SIGTERM") process.on("SIGINT", relay) process.on("SIGTERM", relay) let code = await proc.exited if (script.temp) { try { unlinkSync(script.path) } catch {} } process.exit(code) } // -- Dispatch -- const COMMANDS = ["init", "bundle", "check", "test", "pack", "render", "client", "console", "android", "mcp", "tool"] as const type Command = (typeof COMMANDS)[number] // A usage or configuration error anywhere below (lib/fail.ts) ends here: its // message, exit code 1. try { if (command === "server") { await launchServer(false) } else if (command === "run") { await launchServer(true) } else if (command === "demo") { let { main } = await import("./demo/main") let target = await main() if (target) await launchServer(true, target) } else if (command !== undefined && (COMMANDS as readonly string[]).includes(command)) { let { main } = await import(`./${command as Command}/main`) await main() } else { hint(command === undefined ? undefined : `Unknown command "${command}"`) } } catch (e) { if (!(e instanceof UsageError)) throw e console.error(e.message) process.exit(1) }