const { spawn } = require("node:child_process"); const fs = require("node:fs/promises"); const path = require("node:path"); const repoRoot = path.resolve(__dirname, "../../.."); const docsDir = path.resolve(repoRoot, "apps/docs"); const stateFile = path.resolve(repoRoot, ".artifacts/test-results/storybook-server.json"); const baseURL = "http://127.0.0.1:6006"; const startupTimeoutMs = 120_000; const shutdownTimeoutMs = 10_000; const pollIntervalMs = 250; const storybookCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; const storybookArgs = ["run", "storybook"]; const reuseExistingServer = !process.env.CI; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function isServerResponsive() { try { const response = await fetch(baseURL, { redirect: "manual", signal: AbortSignal.timeout(1_000) }); return response.status < 404 || [400, 401, 402, 403].includes(response.status); } catch { return false; } } async function waitFor(condition, timeoutMs, failureMessage) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await condition()) { return; } await sleep(pollIntervalMs); } throw new Error(failureMessage); } async function ensureStateDir() { await fs.mkdir(path.dirname(stateFile), { recursive: true }); } async function readServerState() { try { const raw = await fs.readFile(stateFile, "utf8"); return JSON.parse(raw); } catch { return null; } } async function clearServerState() { await fs.rm(stateFile, { force: true }); } async function stopProcess(pid) { try { if (process.platform === "win32") { process.kill(pid, "SIGTERM"); } else { process.kill(-pid, "SIGTERM"); } } catch (error) { if (error && error.code === "ESRCH") { return; } throw error; } try { await waitFor( async () => !(await isServerResponsive()), shutdownTimeoutMs, `Timed out waiting ${shutdownTimeoutMs}ms for Storybook to stop.` ); } catch { try { if (process.platform === "win32") { process.kill(pid, "SIGKILL"); } else { process.kill(-pid, "SIGKILL"); } } catch (error) { if (!error || error.code !== "ESRCH") { throw error; } } } } async function startStorybookServer() { if (await isServerResponsive()) { if (reuseExistingServer) { return; } throw new Error(`${baseURL} is already in use, and CI runs must start their own Storybook server.`); } const child = spawn(storybookCommand, storybookArgs, { cwd: docsDir, detached: process.platform !== "win32", env: process.env, shell: false, stdio: "ignore" }); const exitPromise = new Promise((_, reject) => { child.once("error", (error) => { reject(error); }); child.once("exit", (code, signal) => { reject( new Error( `Storybook exited before becoming ready (code: ${code ?? "null"}, signal: ${signal ?? "null"}).` ) ); }); }); try { await Promise.race([ waitFor( async () => isServerResponsive(), startupTimeoutMs, `Timed out waiting ${startupTimeoutMs}ms for Storybook on ${baseURL}.` ), exitPromise ]); } catch (error) { await stopProcess(child.pid); throw error; } child.unref(); await ensureStateDir(); await fs.writeFile(stateFile, JSON.stringify({ pid: child.pid }, null, 2)); } async function stopStorybookServer() { const state = await readServerState(); if (!state) { return; } try { await stopProcess(state.pid); } finally { await clearServerState(); } } module.exports = { clearServerState, readServerState, startStorybookServer, stopStorybookServer };