feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源

把项目从「手写 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 处引用自包含。
This commit is contained in:
Shuery committed 2026-10-02 01:27:02 +08:00
1 parent b8eee05d78
commit 3f11426964
297 files changed
+216627 -1926

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
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");