Files
SpineWallpaper/docs/adr/0005-build-pipeline-and-dist-topology.md
Shuery 3f11426964 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 处引用自包含。
2026-10-02 01:27:02 +08:00

101 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 构建管线与分发拓扑:id 化目录、无链接、合集层级
## 背景
`dist/` 原先手工维护:`assets/{images,effects,audios}/` 平铺、一个 `dist/` 就是"全部壁纸",`project.json`、`preset.js` 也都是手写的。要加第三档壁纸、要按"单档/合集"两种形态发布、还要有个本地调试服,这套手工结构撑不住了。本次把它换成 `pnpm build` 驱动的构建管线。
## 决策
### 1. 一份代码,四种分发:`dist/releases/<类型>-<id>/`
| 分发 | 目录名 | 含壁纸 | 结构 |
| --- | --- | --- | --- |
> 类型只有两种:**单档**与**合集**(`type: "single" | "collection"`)。下表后两行都是合集,
> 区别在收档范围 `scope: "game" | "all"`——「全部合集」不是第三种类型。
| 单档 | `single-<游戏id>-<壁纸id>` | 1 | `<根>/preset.js`、`<根>/audios/` |
| 游戏合集 | `collection-<游戏id>` | 该游戏全部 | `<根>/<壁纸id>/preset.js`、`<根>/audios/<壁纸id>/` |
| 全部合集 | `collection-all` | 全部 | `<根>/<游戏id>/<壁纸id>/preset.js`、`<根>/audios/<壁纸id>/` |
`pnpm build name`、`pnpm build single`、`pnpm build collect` 是**选择构建哪些分发**的三个入口,不是三种不同的构建逻辑——同一份 `wallpapers/` 源数据,按需生成不同的子集。
具体范围(实现见 `tools/build.ts` 的 `parseArgs`):
| 输入 | 范围 |
| --- | --- |
| `pnpm build` | 全编:全部单档 + 每个游戏合集 + 全部合集 |
| `pnpm build <壁纸id> […]` | 指定壁纸(可多个) |
| `pnpm build single [壁纸id …]` | 只编单档;不给 id = 全部单档 |
| `pnpm build collect [游戏id …]` | 只编合集;不给参数 = 全部合集 |
子命令与壁纸 id 共用同一段位置,靠**词表**区分:`single`/`collect`/`sim` 是保留字,其余裸词都是壁纸 id。注意 `pnpm build collect`(无参,全部合集)与 `--collect all`(只编「全部壁纸合集」)语义不同,两者都保留。
### 2. 目录名用 id,不用显示名(**修订 ADR 0001**)
ADR 0001 决定"目录名用显示名、键用 id"。本次把**目录名也改成 id**:
- 分发目录要能直接上传、要避免中文路径在上传与跨平台解压时的编码问题;
- `dist/releases/` 下要能一眼看出哪档是"合集"、哪档是"单档",所以加 `<类型>-` 前缀;
- 显示名进 `meta.json`,改标题不再需要 `git mv` 一堆目录,也不再让"改标题"变成一次全站路径风险。
代价:`wallpapers/` 与产物目录的可读性变差。用 `meta.json` 里的 `name` 与 `dist-map.json` 的 `displayName` 补回来(调试服索引页就是按它渲染的)。
ADR 0001 里"保留 `effects/`、`images/`、`audios/` 三层同名目录"这一条**不变**:Spine 骨架 JSON 硬编码了 `"images": "../images/"`,改了就得动骨架文件。(**已被 ADR 0008 取代**——贴图页并到 atlas 同目录后,骨架 JSON 里那个前缀归零了。)
### 3. 不产生任何链接,只做拷贝
早期的方案考虑过 NTFS 硬链接来省磁盘(全量构建约 290 MB)。**否决**:链接会把"产物自包含"这条基本性质废掉——拷贝到别的机器、上传、或者只拷一个分发目录时,链接目标不在,产物就残了。
所以构建只做普通拷贝,每个分发目录都是完整的字节副本。磁盘代价用"只构建需要的那几个分发"(`pnpm build name` / `collect`)来换,而不是用链接。
### 4. 分发内不出现绝对链接与跨目录相对链接
一个分发目录必须能被整体拷到别的机器上传,因此产物里:
- 不出现 `/xxx` 这类根绝对路径(脱离分发根就没有意义);
- 不出现 `../` 越出分发根的引用;
- 不出现 `file:///` 与 `//`。
例外:合集分发里壁纸的 `preset.js` 会用 `../audios/<壁纸id>/` 指到**同一分发内**的合集根,这不越界。`pnpm check:dist` 把这几条做成硬门禁。
### 5. 路径用 `import.meta.url` 现算,按分发深度生成
`preset.js` 里的每个路径都是 `asset("./images/x.webp")` 这种相对**自身**的形式(见 ADR 0002)。同一档壁纸在不同分发里深度不同,所以每个分发目录的 `preset.js` 都是**现算现写**的,不能一份源码拷到多处:
```
单档 ./audios/<壁纸id>/<文件>
游戏合集 ../audios/<壁纸id>/<文件>
全部合集 ../../audios/<壁纸id>/<文件>
```
这条路径必须与"音频实际搬到哪儿"严格一致。为此构建**先算落点、再生成 `preset.js`**,并由 `check:dist` 在产物上反查每个 `source:` 指向的文件是否真的存在——"语法正确、路径指向空气"的产物会一路通过类型检查与引用扫描,然后在 WE 里静音(本次重构真的写出过一次)。
### 6. 音频集中到合集根,按壁纸分目录
合集分发的 `bgm` 下拉要能选到分发内任一壁纸的音源,所以音频统一集中到合集根的 `audios/`,不再逐壁纸重复一份:
```
audios/<壁纸id>/<文件> 各壁纸自己的音源(互相隔离)
audios/<文件> 游戏级共享音频(wallpapers/<游戏id>/audios/)
```
按壁纸 id 分一层,是为了让"同一首歌被两档壁纸各自引用"和"两档壁纸各有一首同名但不同的曲子"都变成可表达的状态——扁平布局下后者只能报错。共享层保持扁平,重名即歧义,直接构建失败。
单档分发没有合集根,音频留在壁纸目录内的 `audios/`(**非合集分发不出现共享音频目录**)。
### 7. `VERSION` 是版本号的唯一写者
`VERSION` 文件 → `project.json.version`(**数字**,WE 的格式)。构建时会断言"全部合集"这一档的 `project.json` 与线上已发布版本**逐字段一致**——重构不该悄悄改变已发布产物的语义。已知的唯一偏差是格式化:WE 自己的美化器输出 `"key" : value`(冒号前有空格)并展开嵌套,我们用 `JSON.stringify(…, null, "\t")`。字段名、键序、取值全部对齐。
### 8. 产物 JS 的语法目标由编译器兜底
运行时脚本经 `tsc` 编译,跑在 WE 自带的 CEF 里——那不是一个能随 Node 升级的引擎。因此 `tsconfig` 里显式关掉 `useDefineForClassFields`,让类字段降级成构造函数赋值;否则 `x = 1` / `x;` 这类声明式字段会原样输出,实测会让整个 `index.js` 直接 SyntaxError 白屏。
反过来,`presets.js` 与 `preset.js` 是**生成器拼出来的裸 JS 文本**,编译器完全不看它们。所以 `check:dist` 会让 V8 真的解析一遍每个产物模块(`vm.SourceTextModule`,只解析不执行)——这条门禁抓到过一次真事故(数组字面量里写了对象字面量的计算属性名)。
## 未采纳的方案
- **硬链接/符号链接省磁盘**:见第 3 条。
- **一个 `dist/` 装所有分发**:无法整体上传,且本地调试时"越界引用"会被别的分发意外命中,反而掩盖问题。
- **目录名继续用显示名**:见第 2 条。