85 lines
4.0 KiB
JavaScript
85 lines
4.0 KiB
JavaScript
// 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; }
|
|
}
|