3import { execSync } from 'node:child_process' 6import { fileURLToPath } from 'node:url' 14const killPort = (port: number | undefined) => { 15 try { execSync(`lsof -ti:${port} -sTCP:LISTEN | xargs kill -9 2>/dev/null`, { stdio: 'ignore' }) } catch {} 18const checkPortFree = async (port: number | undefined) => { 19 try { await fetch(`http://127.0.0.1:${port}`, { signal: AbortSignal.timeout(300) }); return false } 23const getPortOwner = (port: number | undefined) => { 24 try { return execSync(`lsof -i:${port} 2>/dev/null || true`).toString().trim() } 27export const startDevServer = async ({envH = {}, isQuiet = true, captureOutput = false, onStop, testInstance = true}: {envH?: Record<string, string>, isQuiet?: boolean, captureOutput?: boolean, onStop?: () => void, testInstance?: boolean} = {}) => { 28 const {appPath, importMetaUrl, svcPortNo, testSvcPortNo} = getAppCfg() 30 const port = testInstance ? (testSvcPortNo ?? svcPortNo) : svcPortNo 31 if (testInstance) envH = {...envH, NEXT_SVC_PORT_NO: String(port)} 32 const appDir = `${ptDir}/${appPath}` 33 const syncScript = fileURLToPath(importMetaUrl) 37 if (await checkPortFree(port)) return true 40 }, {timeoutMs: 10000}) 42 const owner = getPortOwner(port) 43 throw new Error(`Port ${port} in use by another process (couldn't kill). Stop it manually:\n${owner}`) 46 const abortController = new AbortController() 47 const envStr = Object.entries(envH).map(([k, v]) => `${k}=${v}`).join(' ') 48 const cmd = `cd ${appDir} && ${envStr ? envStr + ' ' : ''}PT_BG_TASK=1 node ${syncScript} devserver` 50 // Always capture output for debugging; also used by tests when captureOutput=true 51 const capturedOutput = { stdout: '', stderr: '' } 52 const onDataFnc = (data: string) => { 53 capturedOutput.stdout += data 57 const serverPromise = liveSpawn({ cmd, signal: abortController.signal, isQuiet, noOutCmd: true, onDataFnc }) 59 const exitHandler = () => { 60 if (!abortController.signal.aborted) { 61 try { execSync(`lsof -ti:${port} -sTCP:LISTEN | xargs kill -9 2>/dev/null`, { stdio: 'ignore' }) } catch {} // catch:userapproved 64 process.on('exit', exitHandler) 66 const devHost = testInstance ? `127.0.0.1:${port}` : ((getPubEnv().gpSiteHost as string | undefined)?.includes('.localhost:') ? getPubEnv().gpSiteHost as string : `127.0.0.1:${port}`) 69 const r = await fetch(`http://${devHost}`, {signal: AbortSignal.timeout(2000)}) 70 return r.ok || r.status === 404 71 } catch { return false } 72 }, {timeoutMs: 90000, intervalMs: 1000}) 75 abortController.abort() 76 await sleep(300) // let subprocess flush output 77 const output = capturedOutput.stdout.trim() 79 console.log('\n--- devserver failed to start ---') 80 console.log(output.slice(-2000)) 83 const logPath = process.env.PTNODE_LOG_PATH 88 const stop = async () => { 89 process.removeListener('exit', exitHandler) 91 abortController.abort() 92 await serverPromise.catch(() => {}) 97 return { stop, getOutput: () => capturedOutput.stdout + capturedOutput.stderr }