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 explicitAnyLabel = "explicit " + "any"; const sourceFiles = [ ...collectSourceFiles(path.join(rootDir, "src")), ...collectSourceFiles(path.join(rootDir, "scripts")) ]; let explicitAnyCount = 0; for (const file of sourceFiles) { const text = readFileSync(file, "utf8"); const stripped = stripCommentsAndStrings(text); const count = countExplicitAny(stripped); if (count > 0) { explicitAnyCount += count; console.error(` ${path.relative(rootDir, file)}: ${count} ${explicitAnyLabel} usage(s)`); } } if (explicitAnyCount > 0) { console.error(`hwlab-cloud-web tsc-check: ${explicitAnyCount} ${explicitAnyLabel} usage(s) in Web source`); } else { console.log(`hwlab-cloud-web tsc-check: ${explicitAnyLabel} scan passed (0 usages)`); } const sourceShapeStatus = runBunScript("source-shape", path.join(rootDir, "scripts/check.ts")); const vueTscBin = resolveVueTscBin(rootDir); let vueTscStatus = 0; if (!vueTscBin) { console.error("hwlab-cloud-web tsc-check: vue-tsc not found in ancestor node_modules/.bin or PATH"); vueTscStatus = 2; } else { 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}`); vueTscStatus = result.status ?? (result.error ? 2 : 0); if (vueTscStatus === 0) { console.log("hwlab-cloud-web tsc-check: vue-tsc passed"); } else { console.error(`hwlab-cloud-web tsc-check: vue-tsc failed with exit ${result.status ?? "unknown"}`); } } if (explicitAnyCount > 0 || sourceShapeStatus !== 0 || vueTscStatus !== 0) { console.error(`hwlab-cloud-web tsc-check: failed (source-shape=${sourceShapeStatus}, vue-tsc=${vueTscStatus}, ${explicitAnyLabel}=${explicitAnyCount})`); process.exit(firstFailureStatus(sourceShapeStatus, vueTscStatus, explicitAnyCount > 0 ? 2 : 0)); } console.log(`hwlab-cloud-web tsc-check: passed (source-shape, vue-tsc, 0 ${explicitAnyLabel})`); function runBunScript(label: string, scriptPath: string): number { const result = spawnSync(process.execPath, ["run", scriptPath], { 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 ${label}: ${result.error.message}`); const status = result.status ?? (result.error ? 2 : 0); if (status === 0) console.log(`hwlab-cloud-web tsc-check: ${label} passed`); else console.error(`hwlab-cloud-web tsc-check: ${label} failed with exit ${result.status ?? "unknown"}`); return status; } function firstFailureStatus(...statuses: number[]): number { return statuses.find((status) => status !== 0) ?? 2; } function explicitAnyPatterns(): RegExp[] { const token = "a" + "ny"; return [ new RegExp(`:\\s*${token}\\b`, "gu"), new RegExp(`\\bas\\s+${token}\\b`, "gu"), new RegExp(`<[^>\\n]*\\b${token}\\b[^>\\n]*>`, "gu"), new RegExp(`\\b${token}\\s*\\[\\s*\\]`, "gu"), new RegExp(`\\b${token}\\s*(?:[|&;,)=]|$)`, "gu") ]; } function countExplicitAny(strippedSource: string): number { const token = "a" + "ny"; const tokenPattern = new RegExp(`\\b${token}\\b`, "gu"); const positions = new Set(); for (const pattern of explicitAnyPatterns()) { for (const match of strippedSource.matchAll(pattern)) { const index = match.index; if (index === undefined) continue; for (const tokenMatch of match[0].matchAll(tokenPattern)) { if (tokenMatch.index !== undefined) positions.add(index + tokenMatch.index); } } } return positions.size; } 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 stripCommentsAndStrings(text: string): string { return text .replace(/\/\*[\s\S]*?\*\//gu, "") .replace(/(^|[^:])\/\/.*$/gmu, "$1") .replace(/`(?:\\[\s\S]|[^`\\])*`/gu, "``") .replace(/"(?:\\.|[^"\\])*"/gu, '""') .replace(/'(?:\\.|[^'\\])*'/gu, "''"); }