feat(tools): check-skin 独立校验 CLI
脱离构建即可自检 override, 秒级反馈; 有错退出码 1 并逐条指出 子游戏名+文件路径+原因(第二准则: 错误早暴露、可定位)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"new-game": "node scripts/new-game.mjs",
|
"new-game": "node scripts/new-game.mjs",
|
||||||
"check-cocos": "node scripts/check-cocos-version.mjs",
|
"check-cocos": "node scripts/check-cocos-version.mjs",
|
||||||
"bump-cocos": "node scripts/bump-cocos.mjs",
|
"bump-cocos": "node scripts/bump-cocos.mjs",
|
||||||
|
"check-skin": "node scripts/check-skin.mjs",
|
||||||
"test:framework": "node scripts/run-framework-tests.mjs",
|
"test:framework": "node scripts/run-framework-tests.mjs",
|
||||||
"typecheck:framework": "tsc -p tsconfig.framework.json --noEmit"
|
"typecheck:framework": "tsc -p tsconfig.framework.json --noEmit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { FRAMEWORK_UI_DIR, GAMES_DIR, gameOverrideDir } from './lib/paths.mjs';
|
||||||
|
import { listAssets, validateOverrides } from './lib/skin.mjs';
|
||||||
|
|
||||||
|
/** 校验某子游戏的皮肤覆盖。opts 可注入路径用于测试。 */
|
||||||
|
export function checkSkin(gameName, opts = {}) {
|
||||||
|
const gamesDir = opts.gamesDir ?? GAMES_DIR;
|
||||||
|
const frameworkUiDir = opts.frameworkUiDir ?? FRAMEWORK_UI_DIR;
|
||||||
|
|
||||||
|
const projectDir = join(gamesDir, gameName);
|
||||||
|
if (!existsSync(projectDir)) {
|
||||||
|
throw new Error(`找不到子游戏工程:${projectDir}`);
|
||||||
|
}
|
||||||
|
// override 目录不存在 = 该子游戏不换肤,合法
|
||||||
|
const { matched, errors } = validateOverrides(frameworkUiDir, gameOverrideDir(projectDir));
|
||||||
|
return { matched, errors, frameworkTotal: listAssets(frameworkUiDir).length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI 入口:有错误则退出码 1
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||||
|
const name = process.argv[2];
|
||||||
|
if (!name) {
|
||||||
|
console.error('用法: npm run check-skin <game>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const { matched, errors, frameworkTotal } = checkSkin(name);
|
||||||
|
console.log(`皮肤命中 ${matched.length} 处,框架可覆盖资源共 ${frameworkTotal} 个`);
|
||||||
|
if (errors.length === 0) {
|
||||||
|
console.log('OK: override 全部合法。');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
console.error(`\n发现 ${errors.length} 处问题(子游戏 ${name}):`);
|
||||||
|
for (const e of errors) {
|
||||||
|
console.error(` [${e.code}] ${e.path}\n ${e.message}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { mkdirSync, writeFileSync, rmSync, mkdtempSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { deflateSync } from 'node:zlib';
|
||||||
|
import { checkSkin } from '../check-skin.mjs';
|
||||||
|
|
||||||
|
function crc32(buf) {
|
||||||
|
let c = ~0;
|
||||||
|
for (const x of buf) { c ^= x; for (let i = 0; i < 8; i++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1)); }
|
||||||
|
return ~c >>> 0;
|
||||||
|
}
|
||||||
|
function chunk(type, data) {
|
||||||
|
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
|
||||||
|
const td = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
||||||
|
const c = Buffer.alloc(4); c.writeUInt32BE(crc32(td));
|
||||||
|
return Buffer.concat([len, td, c]);
|
||||||
|
}
|
||||||
|
function makePng(w, h) {
|
||||||
|
const ihdr = Buffer.alloc(13);
|
||||||
|
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2;
|
||||||
|
const raw = Buffer.alloc(h * (1 + w * 3));
|
||||||
|
return Buffer.concat([
|
||||||
|
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
||||||
|
chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function put(dir, rel, content) {
|
||||||
|
const p = join(dir, ...rel.split('/'));
|
||||||
|
mkdirSync(join(p, '..'), { recursive: true });
|
||||||
|
writeFileSync(p, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaffold() {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'youle-checkskin-'));
|
||||||
|
const frameworkUiDir = join(root, 'ui');
|
||||||
|
const gamesDir = join(root, 'games');
|
||||||
|
mkdirSync(frameworkUiDir, { recursive: true });
|
||||||
|
mkdirSync(join(gamesDir, 'demo', 'assets', 'game', 'override'), { recursive: true });
|
||||||
|
put(frameworkUiDir, 'atlas-hall/btn.png', makePng(64, 64));
|
||||||
|
put(frameworkUiDir, 'atlas-hall/bg.png', makePng(64, 64));
|
||||||
|
return { root, frameworkUiDir, gamesDir, overrideDir: join(gamesDir, 'demo', 'assets', 'game', 'override') };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('checkSkin 统计命中数与框架资源总数', () => {
|
||||||
|
const s = scaffold();
|
||||||
|
try {
|
||||||
|
put(s.overrideDir, 'atlas-hall/btn.png', makePng(64, 64));
|
||||||
|
const r = checkSkin('demo', { gamesDir: s.gamesDir, frameworkUiDir: s.frameworkUiDir });
|
||||||
|
assert.deepEqual(r.errors, []);
|
||||||
|
assert.deepEqual(r.matched, ['atlas-hall/btn.png']);
|
||||||
|
assert.equal(r.frameworkTotal, 2);
|
||||||
|
} finally { rmSync(s.root, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checkSkin 报出孤儿 override', () => {
|
||||||
|
const s = scaffold();
|
||||||
|
try {
|
||||||
|
put(s.overrideDir, 'atlas-hall/nope.png', makePng(64, 64));
|
||||||
|
const r = checkSkin('demo', { gamesDir: s.gamesDir, frameworkUiDir: s.frameworkUiDir });
|
||||||
|
assert.equal(r.errors.length, 1);
|
||||||
|
assert.equal(r.errors[0].code, 'orphan');
|
||||||
|
} finally { rmSync(s.root, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checkSkin 对不存在的子游戏抛错', () => {
|
||||||
|
const s = scaffold();
|
||||||
|
try {
|
||||||
|
assert.throws(
|
||||||
|
() => checkSkin('nosuch', { gamesDir: s.gamesDir, frameworkUiDir: s.frameworkUiDir }),
|
||||||
|
/找不到子游戏工程/,
|
||||||
|
);
|
||||||
|
} finally { rmSync(s.root, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checkSkin 对没有 override 目录的子游戏返回零命中而非报错', () => {
|
||||||
|
const s = scaffold();
|
||||||
|
try {
|
||||||
|
rmSync(s.overrideDir, { recursive: true, force: true });
|
||||||
|
const r = checkSkin('demo', { gamesDir: s.gamesDir, frameworkUiDir: s.frameworkUiDir });
|
||||||
|
assert.deepEqual(r.matched, []);
|
||||||
|
assert.deepEqual(r.errors, []);
|
||||||
|
} finally { rmSync(s.root, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user