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,95 @@
|
||||
// 热更新验收:真的改一个源文件,看浏览器是不是按预期更新。
|
||||
//
|
||||
// 断言两件不同的事,不能混为一谈:
|
||||
// · 改 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);
|
||||
Reference in new issue
Block a user