// Git source launcher. Uses the existing npm scripts; no portable runtime. import { spawn } from 'node:child_process'; import { readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import net from 'node:net'; process.chdir(fileURLToPath(new URL('../', import.meta.url))); const pkg = JSON.parse(readFileSync('package.json', 'utf8')); const portMatch = pkg.scripts?.storybook?.match(/(?:-p|--port)\s+(\d+)/); if (!portMatch) throw new Error('Cannot determine the port from package.json scripts.storybook.'); const port = Number(portMatch[1]); const url = `http://localhost:${port}/`; const [major, minor] = process.versions.node.split('.').map(Number); if (!((major === 20 && minor >= 19) || (major === 22 && minor >= 12) || major > 22)) { throw new Error('Node.js 20.19+ (20.x) or 22.12+ is required by the lockfile.'); } function run(args, options = {}) { return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', args], { stdio: 'inherit', windowsHide: true, ...options, }); } function completed(child) { return new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`Command failed with exit code ${code}. See output above.`))); }); } async function ready() { try { const response = await fetch(`${url}index.json`, { signal: AbortSignal.timeout(1500) }); if (!response.ok) return false; const index = await response.json(); if (!index.entries?.['components-form-button--all'] || !index.entries?.['theme-designer-playground--editor']) return false; return (await fetch(`${url}iframe.html`, { signal: AbortSignal.timeout(1500) })).ok; } catch { return false; } } function occupied() { return new Promise(resolve => { const socket = net.connect({ host: '127.0.0.1', port }); socket.setTimeout(1500); socket.once('connect', () => { socket.destroy(); resolve(true); }); socket.once('error', () => { socket.destroy(); resolve(false); }); socket.once('timeout', () => { socket.destroy(); resolve(true); }); }); } async function openBrowser() { await completed(spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `Start-Process '${url}'`], { stdio: 'inherit', windowsHide: true, })); } if (await ready()) { await openBrowser(); } else { if (await occupied()) throw new Error(`Port ${port} is occupied by a server that is not a ready HMDesign Storybook. Stop it and retry.`); await completed(run('npm.cmd --version', { stdio: 'ignore' })); if (!existsSync('node_modules/.bin/storybook.cmd')) await completed(run('npm.cmd ci')); const missingDist = ['packages/ui', 'packages/center-graphic'].some(dir => { const workspace = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8')); return Object.values(workspace.exports).some(value => { const entry = typeof value === 'string' ? value : value.import; return entry && !existsSync(`${dir}/${entry}`); }); }); if (missingDist) await completed(run('npm.cmd run build:packages')); // Exact port prevents interactive port selection, including a startup race. const server = run('npm.cmd run storybook -- --exact-port --ci'); let stopped = false; const exit = completed(server).finally(() => { stopped = true; }); // Attach a handler immediately while readiness polling is in progress. exit.catch(() => {}); const stop = () => completed(run(`taskkill /pid ${server.pid} /t /f`, { stdio: 'ignore' })).catch(() => {}); process.once('SIGINT', async () => { await stop(); process.exit(0); }); process.once('SIGTERM', async () => { await stop(); process.exit(0); }); try { const deadline = Date.now() + 180000; while (!stopped && Date.now() < deadline) { if (await ready()) { await openBrowser(); await exit; break; } await new Promise(resolve => setTimeout(resolve, 500)); } if (stopped) await exit; else throw new Error('Storybook readiness timed out after 3 minutes.'); } catch (error) { await stop(); throw error; } }