diff --git a/apps/web/package.json b/apps/web/package.json index 8b77e6ebb..6dc3c59e7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -52,7 +52,8 @@ "storybook": "cross-env VITE_API_URL=/api/v1 VITE_USE_MSW=true VITE_STORYBOOK=true storybook dev -p 6006", "build-storybook": "cross-env VITE_API_URL=/api/v1 VITE_USE_MSW=true VITE_STORYBOOK=true storybook build", "test:storybook": "node scripts/audit-storybook.js", - "test:storybook:playwright": "playwright test --config=playwright.config.storybook.ts" + "test:storybook:playwright": "playwright test --config=playwright.config.storybook.ts", + "validate:storybook": "node scripts/validate-storybook.cjs" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/apps/web/scripts/validate-storybook.cjs b/apps/web/scripts/validate-storybook.cjs new file mode 100644 index 000000000..e7c9208ef --- /dev/null +++ b/apps/web/scripts/validate-storybook.cjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/** + * validate-storybook: build Storybook, serve on 6007, run audit, then stop server. + * Single command for CI/local: npm run validate:storybook + */ +const { spawn, execSync } = require('child_process'); +const http = require('http'); +const path = require('path'); + +const PORT = 6007; +const INDEX_URL = `http://localhost:${PORT}/index.json`; +const POLL_MS = 500; +const POLL_TIMEOUT_MS = 60000; + +function waitForUrl(url, timeoutMs) { + const start = Date.now(); + return new Promise((resolve, reject) => { + const tryOnce = () => { + const req = http.get(url, (res) => { + if (res.statusCode === 200) { + resolve(); + return; + } + res.resume(); + schedule(); + }); + req.on('error', () => schedule()); + req.setTimeout(5000, () => { + req.destroy(); + schedule(); + }); + }; + const schedule = () => { + if (Date.now() - start > timeoutMs) { + reject(new Error(`Timeout waiting for ${url}`)); + return; + } + setTimeout(tryOnce, POLL_MS); + }; + tryOnce(); + }); +} + +function runBuild() { + console.log('[validate-storybook] Running build-storybook...'); + execSync('npm run build-storybook', { + stdio: 'inherit', + cwd: path.resolve(__dirname, '..'), + }); +} + +function main() { + const cwd = path.resolve(__dirname, '..'); + + runBuild(); + + const serve = spawn('npx', ['serve', 'storybook-static', '-l', String(PORT)], { + stdio: 'pipe', + shell: true, + cwd, + }); + + let auditExitCode = 1; + serve.on('error', (err) => { + console.error('[validate-storybook] Failed to start serve:', err); + process.exit(1); + }); + + waitForUrl(INDEX_URL, POLL_TIMEOUT_MS) + .then(() => { + console.log('[validate-storybook] Server ready, running audit...'); + const audit = spawn('node', ['scripts/audit-storybook.js'], { + stdio: 'inherit', + shell: true, + cwd, + }); + return new Promise((res) => { + audit.on('exit', (code) => { + auditExitCode = code ?? 1; + res(); + }); + }); + }) + .catch((err) => { + console.error('[validate-storybook]', err.message); + auditExitCode = 1; + }) + .finally(() => { + serve.kill('SIGTERM'); + serve.on('exit', () => { + process.exit(auditExitCode); + }); + setTimeout(() => { + serve.kill('SIGKILL'); + process.exit(auditExitCode); + }, 5000); + }); +} + +main();