import { spawnSync } from "node:child_process"; import { readdirSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const sourceFiles = collectSourceFiles(path.join(rootDir, "src")); let explicitAnyCount = 0; for (const file of sourceFiles) { const text = readFileSync(file, "utf8"); const stripped = stripComments(text); const matches = [ ...stripped.matchAll(/:\s*any\b/gu), ...stripped.matchAll(/<\s*any\s*>/gu), ...stripped.matchAll(/as\s+any\b/gu), ...stripped.matchAll(/Array\s*<\s*any\s*>/gu) ]; if (matches.length > 0) { explicitAnyCount += matches.length; console.error(` ${path.relative(rootDir, file)}: ${matches.length} explicit any usage(s)`); } } if (explicitAnyCount > 0) { console.error(`hwlab-cloud-web tsc-check: ${explicitAnyCount} explicit any usage(s) in Vue source`); process.exit(2); } const vueTscBin = resolveVueTscBin(rootDir); if (!vueTscBin) { console.error("hwlab-cloud-web tsc-check: vue-tsc not found in ancestor node_modules/.bin or PATH"); process.exit(2); } const result = spawnSync(vueTscBin, ["--noEmit", "--project", path.join(rootDir, "tsconfig.json")], { cwd: rootDir, encoding: "utf8" }); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); if (result.error) console.error(`hwlab-cloud-web tsc-check: failed to start ${vueTscBin}: ${result.error.message}`); if (result.status !== 0) { console.error(`hwlab-cloud-web tsc-check: vue-tsc failed with exit ${result.status ?? "unknown"}`); process.exit(result.status ?? 2); } console.log("hwlab-cloud-web tsc-check: passed (strict Vue TS, 0 explicit any)"); function resolveVueTscBin(startDir: string): string | null { let current = startDir; while (true) { const candidate = path.join(current, "node_modules/.bin/vue-tsc"); if (isRunnableFile(candidate)) return candidate; const parent = path.dirname(current); if (parent === current) break; current = parent; } for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) { const candidate = path.join(dir, process.platform === "win32" ? "vue-tsc.cmd" : "vue-tsc"); if (isRunnableFile(candidate)) return candidate; } return null; } function isRunnableFile(candidate: string): boolean { try { return statSync(candidate).isFile(); } catch { return false; } } function collectSourceFiles(dir: string): string[] { const out: string[] = []; for (const entry of readdirSync(dir)) { if (entry === "node_modules" || entry === "dist" || entry.startsWith(".")) continue; const full = path.join(dir, entry); const stat = statSync(full); if (stat.isDirectory()) out.push(...collectSourceFiles(full)); else if (/\.(ts|vue)$/u.test(full)) out.push(full); } return out; } function stripComments(text: string): string { return text.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/.*$/gmu, "$1"); }