172 lines
3.8 KiB
JavaScript
172 lines
3.8 KiB
JavaScript
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
|
|
};
|