/** * Starts the dev instances of `dev/instances.ts`, for `deno task dev`: for * each, an API server that restarts on change and a Vite server, wired to * each other and kept apart from the other instances by the rule that module * states. Every line they print is prefixed with the instance's name. Ctrl+C * stops them all, and so does any of them stopping, such as a Vite server * whose port is taken. * * It runs with the base `.env` loaded, which every server inherits, and starts * the instances it is given by name, or all of them. In the devcontainer, each * instance's service in `.devcontainer/compose.yaml` runs it for that * instance; outside, `deno task dev` runs it. * * @module */ import { type DevInstance, INSTANCES } from "./instances.ts"; /** The colors of the instances' prefixes, in the order of `INSTANCES`. */ const COLORS = [36, 35, 33, 32, 34, 31]; /** * Returns the environment of `instance`'s servers, over the inherited one: * the rule of `dev/instances.ts`, then the instance's own `env`. */ function envOf(instance: DevInstance): Record { const { name, apiPort, webPort } = instance; return { GERBEUR_INSTANCE: name, GERBEUR_PORT: String(apiPort), GERBEUR_PUBLIC_URL: `http://localhost:${apiPort}`, GERBEUR_FRONTEND_URL: `http://localhost:${webPort}`, GERBEUR_DB_PATH: `api/sql/${name}.db`, GERBEUR_UPLOADS_DIR: `api/uploads/${name}`, // Every instance is on localhost, over http. GERBEUR_FEDERATION_ALLOW_PRIVATE: "1", VITE_API_PROTOCOL: "http", VITE_API_HOSTNAME: "localhost", VITE_API_PORT: String(apiPort), ...instance.env, }; } /** Writes each line of `stream` to `out`, after `prefix`. */ async function pipeLines( stream: ReadableStream, out: typeof Deno.stdout, prefix: string, ): Promise { const encoder = new TextEncoder(); const decoder = new TextDecoder(); let rest = ""; for await (const bytes of stream) { const lines = (rest + decoder.decode(bytes, { stream: true })).split("\n"); rest = lines.pop() ?? ""; for (const line of lines) { await out.write(encoder.encode(`${prefix} ${line}\n`)); } } if (rest) await out.write(encoder.encode(`${prefix} ${rest}\n`)); } /** Returns the instances `names` asks for, all of them when it is empty. */ function pick(names: string[]): readonly DevInstance[] { const unknown = names.filter((n) => !INSTANCES.some((i) => i.name === n)); if (unknown.length > 0) { console.error( `Unknown instance ${unknown.join(", ")}; dev/instances.ts has ${ INSTANCES.map((i) => i.name).join(", ") }.`, ); Deno.exit(1); } return names.length === 0 ? INSTANCES : INSTANCES.filter((i) => names.includes(i.name)); } const chosen = pick(Deno.args); const width = Math.max(...chosen.map((i) => i.name.length)) + 4; const children: { label: string; process: Deno.ChildProcess }[] = []; for (const instance of chosen) { const color = COLORS[INSTANCES.indexOf(instance) % COLORS.length]; const env = envOf(instance); for ( const [role, args] of [ ["api", ["run", "-A", "--watch", "api/main.ts"]], [ "web", [ "run", "-A", "npm:vite", "--port", String(instance.webPort), "--strictPort", ], ], ] as const ) { const label = `${instance.name}:${role}`; const prefix = `\x1b[${color}m${label.padEnd(width)}\x1b[0m│`; const process = new Deno.Command(Deno.execPath(), { args: [...args], env, stdin: "null", stdout: "piped", stderr: "piped", }).spawn(); pipeLines(process.stdout, Deno.stdout, prefix); pipeLines(process.stderr, Deno.stderr, prefix); children.push({ label, process }); } } console.log( chosen.map((i) => `${ i.name.padEnd(width) } http://localhost:${i.webPort} (API :${i.apiPort}, api/sql/${i.name}.db)` ).join("\n"), ); let stopping = false; /** Stops every server still running, then exits with `code`. */ async function stopAll(code: number): Promise { if (!stopping) { stopping = true; for (const { process } of children) { try { process.kill("SIGTERM"); } catch { // Already gone. } } await Promise.allSettled(children.map((c) => c.process.status)); } Deno.exit(code); } for (const signal of ["SIGINT", "SIGTERM"] as const) { Deno.addSignalListener(signal, () => stopAll(0)); } const first = await Promise.race( children.map(async ({ label, process }) => ({ label, status: await process.status, })), ); if (!stopping) { console.error( `${first.label} stopped (exit ${first.status.code}); stopping the others.`, ); await stopAll(first.status.code || 1); }