import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { spawnSync } from 'node:child_process' const mode = process.argv[2] ?? 'check' const rootDir = process.cwd() const baselinePath = resolve(rootDir, 'client/typecheck-baseline.txt') function runClientTypecheck() { const result = spawnSync('pnpm', ['--filter', 'client', 'exec', 'vue-tsc', '--build', '--pretty', 'false'], { cwd: rootDir, encoding: 'utf8', }) if (result.error) { throw result.error } const combinedOutput = `${result.stdout ?? ''}\n${result.stderr ?? ''}` const diagnostics = parseDiagnostics(combinedOutput) return { result, combinedOutput, diagnostics } } function parseDiagnostics(output) { const regex = /^(.*\.(?:ts|tsx|vue)\(\d+,\d+\): error TS\d+: .*)$/gm const rows = [] for (const match of output.matchAll(regex)) { const raw = String(match[1] ?? '').trim() rows.push(normalizeDiagnostic(raw)) } return [...new Set(rows)].sort((a, b) => a.localeCompare(b)) } function normalizeDiagnostic(row) { const rootPrefix = `${rootDir.replaceAll('\\', '/')}/` let normalized = row.replaceAll('\\', '/') normalized = normalized.split(`${rootPrefix}client/`).join('') normalized = normalized.split(rootPrefix).join('') if (normalized.startsWith('client/')) normalized = normalized.slice('client/'.length) return normalized } function readBaseline() { if (!existsSync(baselinePath)) return [] return readFileSync(baselinePath, 'utf8') .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith('#')) } function writeBaseline(diagnostics) { mkdirSync(dirname(baselinePath), { recursive: true }) const header = [ '# Client typecheck baseline', '# Auto-generated by: pnpm run typecheck:client:baseline:update', '# One diagnostic per line. Do not edit manually unless necessary.', '', ] writeFileSync(baselinePath, `${header.join('\n')}${diagnostics.join('\n')}\n`, 'utf8') } if (mode === 'update') { const { result, combinedOutput, diagnostics } = runClientTypecheck() if (result.status !== 0 && diagnostics.length === 0) { console.error('Client typecheck failed without parsable diagnostics output.') console.error(combinedOutput) process.exit(1) } writeBaseline(diagnostics) console.log(`Updated ${baselinePath} with ${diagnostics.length} diagnostics.`) process.exit(0) } if (mode !== 'check') { console.error(`Unknown mode "${mode}". Use "check" or "update".`) process.exit(2) } const { result, combinedOutput, diagnostics } = runClientTypecheck() if (result.status === 0) { console.log('Client typecheck is clean.') process.exit(0) } if (diagnostics.length === 0) { console.error('Client typecheck failed without parsable diagnostics output.') console.error(combinedOutput) process.exit(1) } const baseline = readBaseline() if (baseline.length === 0) { console.error(`No baseline found at ${baselinePath}.`) console.error('Run: pnpm run typecheck:client:baseline:update') process.exit(1) } const baselineSet = new Set(baseline) const newDiagnostics = diagnostics.filter((line) => !baselineSet.has(line)) if (newDiagnostics.length > 0) { console.error(`Found ${newDiagnostics.length} new client type errors not present in baseline:`) for (const line of newDiagnostics) { console.error(`- ${line}`) } console.error('If these are intentional and accepted, refresh baseline:') console.error('pnpm run typecheck:client:baseline:update') process.exit(1) } const resolvedCount = baseline.filter((line) => !diagnostics.includes(line)).length console.log(`No new client type errors. Baseline size: ${baseline.length}. Current: ${diagnostics.length}. Resolved: ${resolvedCount}.`) if (resolvedCount > 0) { console.log('You can refresh baseline to capture improvements:') console.log('pnpm run typecheck:client:baseline:update') } process.exit(0)