Files
bookorbit/scripts/client-typecheck-baseline.mjs
T
2026-03-06 09:08:15 -06:00

121 lines
3.9 KiB
JavaScript

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)