From 550e6da9d1969ccc0b627579bfbd061291d47249 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 27 Aug 2026 21:36:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20check-skin=20=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 脱离构建即可自检 override, 秒级反馈; 有错退出码 1 并逐条指出 子游戏名+文件路径+原因(第二准则: 错误早暴露、可定位)。 Co-Authored-By: Claude Opus 5 (1M context) --- cocoscreator_projects/package.json | 1 + cocoscreator_projects/scripts/check-skin.mjs | 40 +++++++++ .../scripts/test/check-skin.test.mjs | 85 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 cocoscreator_projects/scripts/check-skin.mjs create mode 100644 cocoscreator_projects/scripts/test/check-skin.test.mjs diff --git a/cocoscreator_projects/package.json b/cocoscreator_projects/package.json index 1ccf3c2..d72f704 100644 --- a/cocoscreator_projects/package.json +++ b/cocoscreator_projects/package.json @@ -9,6 +9,7 @@ "new-game": "node scripts/new-game.mjs", "check-cocos": "node scripts/check-cocos-version.mjs", "bump-cocos": "node scripts/bump-cocos.mjs", + "check-skin": "node scripts/check-skin.mjs", "test:framework": "node scripts/run-framework-tests.mjs", "typecheck:framework": "tsc -p tsconfig.framework.json --noEmit" }, diff --git a/cocoscreator_projects/scripts/check-skin.mjs b/cocoscreator_projects/scripts/check-skin.mjs new file mode 100644 index 0000000..5656b36 --- /dev/null +++ b/cocoscreator_projects/scripts/check-skin.mjs @@ -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 '); + 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); +} diff --git a/cocoscreator_projects/scripts/test/check-skin.test.mjs b/cocoscreator_projects/scripts/test/check-skin.test.mjs new file mode 100644 index 0000000..8fbef04 --- /dev/null +++ b/cocoscreator_projects/scripts/test/check-skin.test.mjs @@ -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 }); } +});