#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import crypto from 'node:crypto'; import { execFileSync } from 'node:child_process'; import { build } from 'esbuild'; const args = new Map(); for (let i = 2; i < process.argv.length; i += 1) { const value = process.argv[i]; if (value.startsWith('--')) args.set(value.slice(2), process.argv[i + 1]?.startsWith('--') ? true : process.argv[++i]); } const input = args.get('input'); const output = args.get('output'); const screens = Number(args.get('screens') || 1); const root = process.cwd(); const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(4).toString('hex')}`; const reportDir = output ? path.resolve(output) : path.resolve(root, '_local', 'h2-preview', runId); const failurePath = path.join(reportDir, 'verification-failure.json'); const rel = (p) => path.relative(root, p) || '.'; const fail = (code, message, details = {}) => { fs.mkdirSync(reportDir, { recursive: true }); const report = { h2: 'H-2', status: 'FAIL', code, message, details, createdAt: new Date().toISOString(), input: input ? path.resolve(input) : null }; fs.writeFileSync(failurePath, JSON.stringify(report, null, 2) + '\n'); console.error(JSON.stringify(report, null, 2)); process.exitCode = 1; }; if (!input) fail('INPUT_REQUIRED', 'Use --input with a .tsx/.jsx entry file or a directory containing one.'); else { const inputPath = path.resolve(input); const stat = fs.existsSync(inputPath) && fs.statSync(inputPath); if (!stat) fail('INPUT_NOT_FOUND', `Input does not exist: ${inputPath}`); else { const entry = stat.isDirectory() ? ['App.tsx', 'index.tsx', 'main.tsx', 'App.jsx', 'index.jsx', 'main.jsx'].map((x) => path.join(inputPath, x)).find(fs.existsSync) : inputPath; if (!entry || !/\.(tsx|jsx)$/.test(entry)) fail('INVALID_INPUT', 'Input must resolve to an independent React TSX/JSX entry.'); else if (path.resolve(reportDir) === path.resolve(root, 'dist') || path.resolve(reportDir) === path.resolve(root, 'storybook-static')) fail('PROTECTED_OUTPUT', 'Shared dist and Storybook output are not valid H-2 output paths.'); else if (fs.existsSync(path.join(reportDir, 'preview.html')) || fs.existsSync(path.join(reportDir, 'verification.json'))) fail('OUTPUT_EXISTS', 'Refusing to overwrite an existing H-2 output. Choose a new --output directory.'); else { const before = requireGitStatus(); const outputBefore = fs.existsSync(reportDir) ? fs.readdirSync(reportDir) : []; try { fs.mkdirSync(reportDir, { recursive: true }); const temp = path.join(reportDir, '.bundle.js'); await build({ entryPoints: [entry], outfile: temp, bundle: true, format: 'iife', platform: 'browser', target: 'es2020', sourcemap: false, logLevel: 'silent', absWorkingDir: path.dirname(entry) }); const js = fs.readFileSync(temp, 'utf8'); fs.rmSync(temp, { force: true }); const source = fs.readFileSync(entry, 'utf8'); const urls = [...source.matchAll(/(?:src|href)\s*=\s*["']([^"']+)["']/gi)].map((m) => m[1]); const external = urls.filter((u) => /^(https?:|\/\/)/i.test(u)); const unresolved = urls.filter((u) => !/^(data:|#|https?:|\/\/|\/)/i.test(u) && !fs.existsSync(path.resolve(path.dirname(entry), u.split('#')[0]))); if (external.length || unresolved.length) throw new Error(`External or unresolved asset references: ${JSON.stringify({ external, unresolved })}`); const html = `H-2 Preview
`; fs.writeFileSync(path.join(reportDir, 'preview.html'), html); const verification = { h2: 'H-2', status: 'PASS', input: path.resolve(entry), output: path.join(reportDir, 'preview.html'), runtime: 'REACT_TSX', screens, checks: { isolatedBuild: true, singlePreviewHtml: true, fileUrlReady: true, externalUrls: external, unresolvedRelativePaths: unresolved, console: 'NOT_RUN', runtime: 'NOT_RUN', network: 'NOT_RUN', captures: 'NOT_RUN', protectedOutputsUnchanged: true, preexistingChanges: before, postRunChanges: requireGitStatus(), outputBefore, outputAfter: fs.readdirSync(reportDir) }, note: 'Browser/runtime/capture checks remain NOT_RUN until a browser runner executes them; NOT_RUN is not PASS.' }; fs.writeFileSync(path.join(reportDir, 'verification.json'), JSON.stringify(verification, null, 2) + '\n'); console.log(JSON.stringify({ status: 'PASS', preview: path.join(reportDir, 'preview.html'), verification: path.join(reportDir, 'verification.json') }, null, 2)); } catch (error) { fail('BUILD_OR_ASSET_CHECK_FAILED', error.message, { entry: rel(entry), output: reportDir }); } } } } function requireGitStatus() { try { return execFileSync('git', ['status', '--short'], { encoding: 'utf8' }).split(/\r?\n/).filter(Boolean); } catch { return ['UNAVAILABLE']; } }