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:
1 parent
b8eee05d78
commit
3f11426964
297 files changed
+216627
-1926
No files matched your search
@@ -0,0 +1,189 @@
|
||||
// 调试服注入到 index.html 里的驱动脚本。
|
||||
//
|
||||
// 为什么单独一个文件:调试服(pnpm dev)与对拍工具(tools/serve.mjs)都往页面里注入脚本,
|
||||
// 两边的注入点与语义必须一致,否则"调试服里好的、对拍里坏"这类分歧会持续存在。
|
||||
//
|
||||
// 这里生成的都是**经典脚本**(非 module):它们在 index.html 的 <script type="module"> 之前执行,
|
||||
// 因此能在壁纸模块注册 window.wallpaperPropertyListener 之前就把钩子准备好。
|
||||
|
||||
/**
|
||||
* 调试服提供模拟器的固定路径(`simulatorMode: "url"` 的默认值)。
|
||||
*
|
||||
* 放在调试服自己的命名空间下,而不是某个分发目录里:模拟器默认不进发布产物(ADR 0006 第 1 条),
|
||||
* 但调试服总是需要它。两边共用一个常量,避免"驱动里写死一个、服务器里写死另一个"再次错位。
|
||||
*
|
||||
* **静态托管(GitHub Pages)不能用它**:那是根绝对路径,Pages 会把它解析到域名根,
|
||||
* 而不是仓库子路径。静态构建必须传 `simulatorUrl: "./scripts/wallpaper-engine.js"`。
|
||||
*/
|
||||
export const SIMULATOR_URL = "/simulator/wallpaper-engine.js";
|
||||
|
||||
/**
|
||||
* 静态构建(`pnpm build --with-sim`)里注入的模拟器地址。
|
||||
*
|
||||
* 相对路径是**必须**的:GitHub Pages 把站点放在 `/<repo>/` 子路径下,
|
||||
* 根绝对路径 `/scripts/…` 会 404。相对路径也让同一个分发目录放到任何位置都能用。
|
||||
*/
|
||||
export const SIMULATOR_URL_STATIC = "./scripts/wallpaper-engine.js";
|
||||
|
||||
export interface SimulatorDriverOptions {
|
||||
/** 原始查询串(用于把 __props / fps / __paused 等参数交给模拟器)。 */
|
||||
search: string;
|
||||
/** 分发目录名。 */
|
||||
dir: string;
|
||||
/** project.json 的 title。 */
|
||||
title: string;
|
||||
/** project.json 的 version。 */
|
||||
version: string;
|
||||
/** 本分发的默认预设 id。 */
|
||||
defaultPresetId: string;
|
||||
/** project.json 的 preview 字段(分发根下的文件名)。缺省 = 该分发没有预览图。 */
|
||||
preview?: string;
|
||||
/**
|
||||
* 装载方式:
|
||||
* "url"(默认) 经典脚本里的动态 import,从调试服自己的 `/simulator/wallpaper-engine.js` 取。
|
||||
* "global" 直接调 `window.mountWallpaperEngineSimulator`,模拟器已由前面一个
|
||||
* <script> 内联在同一页里。
|
||||
*
|
||||
* 自包含包(`pnpm build --sim`)必须用 "global":file:// 下动态 import 会被 CORS 拦掉。
|
||||
*
|
||||
* 为什么 "url" 不指向 `/release/<dir>/scripts/…`:模拟器是**调试期专有**资源,
|
||||
* 按 ADR 0006 §1 默认不进发布产物,只有 `pnpm build --with-sim` 才会往分发里放一份。
|
||||
* 而调试服**总是**需要它。指向分发内部会让"没加 --with-sim 的默认构建"下
|
||||
* 动态 import 404、面板静默消失——正是这里踩过的坑。改由调试服提供,
|
||||
* 分发目录则永远保持"就是发布产物"的样子。
|
||||
*/
|
||||
simulatorMode?: "url" | "global";
|
||||
/**
|
||||
* `simulatorMode: "url"` 时从哪儿取模拟器本体。默认 `SIMULATOR_URL`(调试服的绝对路由)。
|
||||
* 静态构建传 `SIMULATOR_URL_STATIC`。
|
||||
*/
|
||||
simulatorUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟器驱动:解析 URL 参数 → 装载模拟器 → 调 mountWallpaperEngineSimulator。
|
||||
*
|
||||
* 模拟器本身是 ES module(`pnpm build:sim` 产出 scripts/wallpaper-engine.js)。这里刻意**不**用
|
||||
* `<script type="module">`:module 脚本会被延迟到解析完成之后执行,那样它就不可能早于壁纸模块
|
||||
* 就位,而"属性在 load 之前到达"这一时序要求 API 必须在 index.js 注册 listener 之前就绪。
|
||||
* 用经典脚本里的动态 import 装载,既满足时序,又能让模拟器源码正常使用 import/export。
|
||||
*/
|
||||
export function SIMULATOR_DRIVER(options: SimulatorDriverOptions): string {
|
||||
const config = JSON.stringify({
|
||||
search: options.search,
|
||||
dir: options.dir,
|
||||
title: options.title,
|
||||
version: options.version,
|
||||
defaultPresetId: options.defaultPresetId,
|
||||
preview: options.preview,
|
||||
});
|
||||
const load =
|
||||
options.simulatorMode === "global"
|
||||
? `var mount = window.mountWallpaperEngineSimulator;
|
||||
if (typeof mount !== "function") { console.error("[WE 模拟器] 内联的模拟器没有挂上 window.mountWallpaperEngineSimulator"); return; }
|
||||
mount(createOptions());`
|
||||
: `var moduleUrl = ${JSON.stringify(options.simulatorUrl ?? SIMULATOR_URL)};
|
||||
import(moduleUrl).then(function (mod) {
|
||||
mod.mountWallpaperEngineSimulator(createOptions());
|
||||
}, function (error) {
|
||||
console.error("[WE 模拟器] 加载失败:", error);
|
||||
});`;
|
||||
|
||||
return `<script>
|
||||
(function () {
|
||||
// 幂等:--with-sim 构建的分发**自带**驱动,调试服又会再注入一次。
|
||||
// 没有这道闸,模拟器会被 mount 两遍,页面上出现两个面板、属性也下发两次。
|
||||
if (window.__weSimDriver) return;
|
||||
window.__weSimDriver = true;
|
||||
var cfg = ${config};
|
||||
var q = new URLSearchParams(cfg.search || location.search);
|
||||
var props = {};
|
||||
try { props = JSON.parse(q.get("__props") || "{}"); } catch (e) { props = {}; }
|
||||
// project.json 的默认值作为底,URL 参数覆盖它
|
||||
var defaults = {};
|
||||
var defs = window.__weProperties || null;
|
||||
if (defs) for (var k in defs) if (defs[k] && "value" in defs[k]) defaults[k] = defs[k].value;
|
||||
var fpsRaw = q.get("fps");
|
||||
function createOptions() {
|
||||
var opts = {
|
||||
releaseName: cfg.title + " · " + cfg.dir,
|
||||
preview: cfg.preview,
|
||||
properties: defs || {},
|
||||
initialProps: Object.assign({}, defaults, props),
|
||||
initialFps: fpsRaw === null ? 0 : Number(fpsRaw) || 0,
|
||||
initialPaused: q.get("__paused") === "1",
|
||||
propsAt: q.get("__propsAt") === "dom" ? "dom" : "load"
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
function afterMount() {
|
||||
if (!window.__weSim) return;
|
||||
window.__weSim.release.version = cfg.version;
|
||||
window.__weSim.release.dir = cfg.dir;
|
||||
}
|
||||
${load}
|
||||
afterMount();
|
||||
})();
|
||||
</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ?nojs=1 的收尾脚本:剥掉所有 <script> 之后,页面上什么都不剩,连"是否加载成功"都无从判断。
|
||||
* 这里补一个小小的状态标记,让 cdp/shot 的断言仍然能读到 readyState。
|
||||
*/
|
||||
export const NOJS_DRIVER = `<script>
|
||||
document.title = "TESTSTATE " + JSON.stringify({ mode: "nojs", readyState: document.readyState, scripts: 0 });
|
||||
</script>`;
|
||||
|
||||
/**
|
||||
* 热更新客户端(只有调试服注入,发布产物里没有)。
|
||||
*
|
||||
* 连调试服的 SSE,按消息决定怎么更新:
|
||||
* building → 左下角出现一个小药丸,说明正在重建(不然保存后有 1 秒左右毫无反馈)
|
||||
* css → **只换样式表**,不整页重载:保住已经加载好的 Spine 播放器与面板状态
|
||||
* reload → 整页刷新
|
||||
* error → 显示错误并且**不刷新**:构建失败时刷新只会把一个半成品页面端上来
|
||||
*
|
||||
* 药丸挂在 documentElement 下,理由与模拟器面板相同:body 上会被打 transform/filter
|
||||
* (翻转、颜色选项),挂 body 里会跟着壁纸一起被镜像、被调色。
|
||||
*/
|
||||
export const LIVE_RELOAD_CLIENT = `<script>
|
||||
(function () {
|
||||
if (!window.EventSource) return;
|
||||
var pill = null;
|
||||
function show(text, tone) {
|
||||
if (!pill) {
|
||||
pill = document.createElement("div");
|
||||
pill.id = "dev-reload-pill";
|
||||
pill.style.cssText = "position:fixed;left:12px;bottom:12px;z-index:2147483647;" +
|
||||
"font:12px/1.5 'Segoe UI',system-ui,sans-serif;padding:6px 12px;border-radius:999px;" +
|
||||
"border:1px solid #3a3a40;box-shadow:0 4px 14px rgba(0,0,0,.5);max-width:62vw;white-space:pre-wrap;";
|
||||
document.documentElement.append(pill);
|
||||
}
|
||||
pill.style.display = "block";
|
||||
pill.style.background = tone === "error" ? "#5a1f1f" : "#2a2a2e";
|
||||
pill.style.color = tone === "error" ? "#ffb4b4" : "#e6e6e8";
|
||||
pill.textContent = text;
|
||||
}
|
||||
function hide() { if (pill) pill.style.display = "none"; }
|
||||
|
||||
var es = new EventSource("/__dev/events");
|
||||
es.onmessage = function (event) {
|
||||
var msg;
|
||||
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
||||
if (msg.type === "building") { show("⟳ 重新构建中… " + (msg.steps || []).join(" → ")); return; }
|
||||
if (msg.type === "error") { show("✗ 构建失败(页面没有刷新)\\n" + msg.message, "error"); return; }
|
||||
if (msg.type === "css") {
|
||||
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
links[i].href = links[i].href.split("?")[0] + "?t=" + Date.now();
|
||||
}
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
if (msg.type === "reload") location.reload();
|
||||
};
|
||||
// 调试服重启时连接会断,EventSource 自己会重连,这里不需要做任何事。
|
||||
es.onerror = function () {};
|
||||
})();
|
||||
</script>`;
|
||||
Reference in new issue
Block a user