feat(storybook): add one-click Windows launcher
This commit is contained in:
1 parent
d537926417
commit
c86d12bebc
3 files changed
+116
No files matched your search
@@ -0,0 +1,84 @@
|
||||
// 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; }
|
||||
}
|
||||
Reference in new issue
Block a user