import { execFile, spawn } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const devPort = Number(process.env.PLAYWRIGHT_DEV_PORT || 5173); const baseURL = `http://localhost:${devPort}`; const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `port-${devPort}`) .trim() .replace(/[^a-zA-Z0-9._-]+/g, "-"); const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`); async function getListeningProcessOnWindows(port) { const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], { cwd: process.cwd(), }); const match = stdout.match(new RegExp(`^\\s*TCP\\s+[^\\s]+:${port}\\s+[^\\s]+\\s+LISTENING\\s+(\\d+)\\s*$`, "mi")); if (!match) { return null; } const pid = Number(match[1]); const command = `Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}' | Select-Object -ExpandProperty CommandLine`; const processResult = await execFileAsync("powershell", ["-NoProfile", "-Command", command], { cwd: process.cwd(), }).catch(() => ({ stdout: "" })); return { Id: pid, CommandLine: processResult.stdout.trim(), }; } async function getListeningProcessOnUnix(port) { try { const { stdout } = await execFileAsync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp", "-Fc"], { cwd: process.cwd(), }); const pidMatch = stdout.match(/^p(\d+)$/m); const commandMatch = stdout.match(/^c(.+)$/m); if (!pidMatch) { return null; } return { Id: Number(pidMatch[1]), CommandLine: commandMatch ? commandMatch[1] : "", }; } catch { return null; } } async function getListeningProcess(port) { if (process.platform === "win32") { return getListeningProcessOnWindows(port); } return getListeningProcessOnUnix(port); } async function killProcessTree(pid) { if (!pid) { return; } if (process.platform === "win32") { await execFileAsync("taskkill", ["/PID", String(pid), "/T", "/F"], { cwd: process.cwd() }).catch(() => {}); return; } try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { // ignore } } } async function waitForServerReady(url, timeoutMs = 120_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const response = await fetch(url, { redirect: "manual" }); if (response.status < 500) { return; } } catch { // keep polling until ready } await new Promise((resolve) => setTimeout(resolve, 1000)); } throw new Error(`Timed out waiting for the local Playwright dev server at ${url}.`); } export default async function globalSetup() { if (process.env.PLAYWRIGHT_BASE_URL) { return; } await fs.mkdir(path.dirname(pidFile), { recursive: true }); const existing = await getListeningProcess(devPort); if (existing) { if (!/vite(?:\.js)?/i.test(existing.CommandLine || "")) { throw new Error(`Port ${devPort} is already in use by a non-Vite process: ${existing.CommandLine}`); } await killProcessTree(existing.Id); await new Promise((resolve) => setTimeout(resolve, 1000)); } const serverProcess = process.platform === "win32" ? spawn( "cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host localhost --port ${devPort} --strictPort`], { cwd: process.cwd(), detached: true, env: { ...process.env, PLAYWRIGHT: "1", }, stdio: "ignore", windowsHide: true, } ) : spawn("npm", ["run", "dev", "--", "--host", "localhost", "--port", String(devPort), "--strictPort"], { cwd: process.cwd(), detached: true, env: { ...process.env, PLAYWRIGHT: "1", }, stdio: "ignore", }); serverProcess.unref(); await fs.writeFile(pidFile, JSON.stringify({ pid: serverProcess.pid, port: devPort }), "utf8"); try { await waitForServerReady(baseURL); } catch (error) { await killProcessTree(serverProcess.pid); throw error; } }