# 构建管线与分发拓扑:id 化目录、无链接、合集层级 ## 背景 `dist/` 原先手工维护:`assets/{images,effects,audios}/` 平铺、一个 `dist/` 就是"全部壁纸",`project.json`、`preset.js` 也都是手写的。要加第三档壁纸、要按"单档/合集"两种形态发布、还要有个本地调试服,这套手工结构撑不住了。本次把它换成 `pnpm build` 驱动的构建管线。 ## 决策 ### 1. 一份代码,四种分发:`dist/releases/<类型>-/` | 分发 | 目录名 | 含壁纸 | 结构 | | --- | --- | --- | --- | > 类型只有两种:**单档**与**合集**(`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 条。