import { readFile } from "node:fs/promises"; import { abs, log, walkFiles } from "./lib/fs.ts"; import { VaultError } from "./lib/vault.ts"; // pnpm check:syntax —— 把"源码必须 strip-safe"从约定升级成门禁。 // // Node 26 能直接跑 .ts,但它只做**类型擦除**:enum、构造函数参数属性、namespace 会以 // ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 崩掉(已实测)。这类语法在 tsc 下完全合法,所以只有真实加载 // 才发现得了——而那通常是 `pnpm dev` 已经跑起来之后。这里做一次静态扫描,提前挡在门口。 // // 刻意**不做**动态 import 验证:那些入口脚本一被执行就有副作用(起服务、写 dist)。 const BANNED: { pattern: RegExp; what: string; why: string }[] = [ { pattern: /^[ \t]*(?:export[ \t]+)?(?:const[ \t]+)?enum[ \t]/m, what: "enum", why: "类型擦除模式下没有运行时实现,Node 直接报错", }, { pattern: /^[ \t]*(?:export[ \t]+)?(?:declare[ \t]+)?namespace[ \t]/m, what: "namespace", why: "同上,且它是过时的模块写法", }, { pattern: /constructor[ \t]*\([^)]*\b(?:public|private|protected|readonly)[ \t]+\w+/s, what: "构造函数参数属性(constructor(public x: T))", why: "需要编译器注入赋值语句,擦除模式做不到", }, { pattern: /\bimport[ \t]+\w+[ \t]*=[ \t]*require[ \t]*\(/, what: "import x = require()", why: "TS 特有语法,非 ESM" }, ]; /** 扫描范围:所有会被 Node 直接执行的 .ts(运行时脚本经 tsc 编译,不受此限制)。 */ async function sourceFiles(): Promise { const files: string[] = []; for (const dir of ["tools", "src/simulator"]) { for (const file of await walkFiles(abs(dir))) { if (file.endsWith(".ts")) files.push(`${dir}/${file}`); } } return files.sort(); } /** * 先剥掉注释与字符串/模板字面量再匹配。 * * 这不是洁癖:本文件自己就用正则写着这些被禁语法的样子,若直接扫全文会自我举报。 * 同理,任何文档或消息文本里出现 "enum " 也不该被当成代码。 */ function stripCommentsAndLiterals(text: string): string { return text .replace(/\/\*[\s\S]*?\*\//g, " ") .replace(/\/\/[^\n]*/g, "") .replace(/`(?:\\[\s\S]|[^`\\])*`/g, '""') .replace(/'(?:\\[\s\S]|[^'\\\n])*'/g, '""') .replace(/"(?:\\[\s\S]|[^"\\\n])*"/g, '""'); } const problems: string[] = []; for (const relPath of await sourceFiles()) { const text = stripCommentsAndLiterals(await readFile(abs(relPath), "utf8")); for (const { pattern, what, why } of BANNED) { const match = pattern.exec(text); if (!match) continue; problems.push(`${relPath} 用了 ${what} —— ${why}`); } } if (problems.length > 0) { log("check:syntax ✗"); for (const problem of problems) log(` ✗ ${problem}`); throw new VaultError(`源码里有 ${problems.length} 处 Node 类型擦除不支持的语法`); } log("check:syntax ✓ 所有会被 Node 直接执行的 .ts 都是 strip-safe");