Files
SpineWallpaper/tools/checks/verify-hot-reload.mts
T
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

96 lines
4.8 KiB
TypeScript
Raw 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.
// 热更新验收:真的改一个源文件,看浏览器是不是按预期更新。
//
// 断言两件不同的事,不能混为一谈:
// · 改 CSS → **只换样式表**,页面不重载(Spine 播放器与面板状态都保住)
// · 改 runtime → **整页刷新**
//
// 用"页面上的标记还在不在"来区分这两者:整页刷新会把 window 上的标记抹掉,
// 只换样式表则不会。只看"页面变了没有"是分不出这两种情况的。
//
// 用法:node tools/checks/verify-hot-reload.mts [base]
// 前提:调试服在跑,且是**新版**(带热更新)的 tools/dev.ts。
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
import { withPage, sleep } from "../cdp.mjs";
const BASE = process.argv[2] ?? "http://127.0.0.1:5173";
const RELEASE = "collection-all";
const CSS_FILE = "src/styles/index.css";
const TS_FILE = "src/runtime/viewport-fitter.ts";
const cssOriginal = readFileSync(CSS_FILE, "utf8");
const tsOriginal = readFileSync(TS_FILE, "utf8");
let failures = 0;
const check = (ok: unknown, label: string, detail?: unknown) => {
if (!ok) failures++;
console.log(` ${ok ? "✓" : "✗"} ${label}${detail !== undefined ? ` → ${String(detail)}` : ""}`);
};
/** 等到条件成立,超时返回 false。 */
async function waitFor(evaluate, expression, ms, tag) {
const deadline = Date.now() + ms;
while (Date.now() < deadline) {
if (await evaluate(expression).catch(() => false)) return true;
await sleep(150);
}
console.log(` (${tag} 等待超时)`);
return false;
}
try {
await withPage({ width: 1280, height: 900, profile: "tools/.cache/edge-hotreload" }, async ({ send, evaluate }) => {
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/` });
await sleep(3500);
console.log("=== ① 注入与连接 ===");
const injected = await evaluate(`!!document.querySelector('script[src*="/__dev/events"]') || document.documentElement.innerHTML.includes("/__dev/events")`);
check(injected === true, "页面里注入了热更新客户端");
const esState = await evaluate(`(function(){
// EventSource 没有暴露实例,用一条同地址的连接探一下服务端在不在
return fetch("/__dev/events", { method: "GET" }).then(function (r) {
return r.headers.get("content-type") || "";
}).catch(function () { return "ERR"; });
})()`);
check(/text\/event-stream/.test(esState ?? ""), "SSE 端点返回 text/event-stream", esState);
console.log("\n=== ② 改 CSS:只换样式表,不整页重载 ===");
await evaluate(`window.__hotReloadMarker = "css";`);
// 页面有两条样式表(spine-player.css / index.css),要盯的是**被改的那一条**,
// 盯第一条会把"换了别的样式表"当成成功。
const indexHref = `(function(){
var links = document.querySelectorAll('link[rel="stylesheet"]');
for (var i = 0; i < links.length; i++) if (links[i].href.indexOf("index.css") >= 0) return links[i].href;
return null;
})()`;
const beforeHref = await evaluate(indexHref);
appendFileSync(CSS_FILE, `\n/* hot-reload probe ${Date.now()} */\n`);
const swapped = await waitFor(evaluate, `${indexHref} !== ${JSON.stringify(beforeHref)}`, 25000, "样式表换新");
check(swapped, "被改的样式表 href 换成了带 ?t= 的新地址", await evaluate(indexHref));
const markerAfterCss = await evaluate(`window.__hotReloadMarker`);
check(markerAfterCss === "css", "页面**没有**整页重载(window 上的标记还在)", String(markerAfterCss));
const cssText = await evaluate(`fetch(${indexHref}).then(function(r){ return r.text(); })`);
check(/hot-reload probe/.test(cssText ?? ""), "换上的样式表确实是新内容");
console.log("\n=== ③ 改 runtime:整页刷新 ===");
writeFileSync(CSS_FILE, cssOriginal); // 先把 CSS 还原,免得下面这一轮被判成 css-only
await sleep(2500);
await evaluate(`window.__hotReloadMarker = "ts";`);
const markerBefore = await evaluate(`window.__hotReloadMarker`);
check(markerBefore === "ts", "刷新前标记已就位", String(markerBefore));
appendFileSync(TS_FILE, `\n// hot-reload probe ${Date.now()}\n`);
const reloaded = await waitFor(evaluate, `window.__hotReloadMarker === undefined`, 30000, "整页刷新");
check(reloaded, "页面整页刷新了(标记被抹掉)");
check((await evaluate(`!!document.getElementById("wesim")`)) === true, "刷新后模拟器面板重新挂上");
});
} finally {
// 无论成败都要还原源码,否则测试会把探针注释留在仓库里
writeFileSync(CSS_FILE, cssOriginal);
writeFileSync(TS_FILE, tsOriginal);
await sleep(2500);
console.log("\n源文件已还原");
}
console.log(failures === 0 ? "热更新:通过" : `热更新:失败 ${failures} 处`);
process.exit(failures === 0 ? 0 : 1);