把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。
- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
(.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)
同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)
pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
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<string[]> {
|
||
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");
|