import { Command } from "commander"; import { DistDirOptionDescription, DistDirOptionFlags, } from "../dist-dir-option"; import path from "path"; import writeErrorFile from "../../functions/write-error-file"; import grabDirNames from "../../utils/grab-dir-names"; import { existsSync, rmSync } from "fs"; const MAX_RETRIES = 5; const RETRY_RESET_AFTER_MS = 10000; export default function () { return new Command("start") .description("Start production server") .option(DistDirOptionFlags, DistDirOptionDescription) .action(async () => { const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames(); // A stale error file from a previous session makes the bundler // run in "silent" mode and pages fail with misleading messages. rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true }); await start(); }); } async function start() { // A loop, not recursion: each restart would otherwise keep the previous // call (and its subprocess) alive for the supervisor's whole lifetime. let retries = 0; while (true) { if (retries >= MAX_RETRIES) { console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`); process.exit(1); } const prod_spawn_file = path.resolve(__dirname, "prod-spawn.ts"); const prod_spawn_js_file = path.resolve(__dirname, "prod-spawn.js"); const final_spawn_file = existsSync(prod_spawn_js_file) ? prod_spawn_js_file : prod_spawn_file; const spawn_options = { cmd: ["bun", final_spawn_file], stdio: ["inherit", "inherit", "inherit"], onExit(subprocess, exitCode, signalCode, error) { writeErrorFile({ exitCode, error }); }, env: { ...process.env, NODE_ENV: "production", }, }; const started_at = Date.now(); let child_process; try { child_process = Bun.spawn(spawn_options); } catch (error) { console.error(`Failed to start production process:`, error); retries++; continue; } const exited = await child_process.exited; // A null exit means the child was killed by a signal (e.g. Ctrl+C // in the shared terminal), which we must not restart. if (exited == null) { return; } // Restart on any numeric exit (0 included: the server should never // exit cleanly). Only consecutive quick crashes count toward the // limit; a child that ran for a while starts the count over. retries = Date.now() - started_at >= RETRY_RESET_AFTER_MS ? 1 : retries + 1; } }