Files
youle_cocos/cocoscreator_projects/scripts/lib/skin.mjs
T
joywayerandClaude Opus 5 44264db8ba fix(tools): skin.mjs 显式报错尺寸不可读,补全测试覆盖
Finding 1: 尺寸校验不能无声跳过(null 被当作通过)。
新增 size-unreadable 错误码,当图片无法读取尺寸时显式报错。

Finding 2: 测试 10 是伪测试(.wav 太短,被长度守卫拦下)。
改用真实 PNG 字节但 .wav 扩展名,验证 isImage() 门控在。

新增两条用例:
- size-unreadable 错误场景(override 侧非 PNG)
- PNG 签名检查(长度足够但签名不对)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 21:32:01 +08:00

113 lines
4.0 KiB
JavaScript
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.
import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
import { join, posix, sep, extname, relative } from 'node:path';
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
const SPINE_ROOT = 'spine'; // framework/ui/spine/<资源名>/ 为一套 Spine
const SKIPPED = new Set(['.gitkeep']);
/** 列出目录下所有资源的相对路径(正斜杠),排除 .meta 与 .gitkeep。 */
export function listAssets(dir) {
if (!existsSync(dir)) return [];
const out = [];
(function walk(cur) {
for (const name of readdirSync(cur)) {
const p = join(cur, name);
if (statSync(p).isDirectory()) { walk(p); continue; }
if (name.endsWith('.meta') || SKIPPED.has(name)) continue;
out.push(relative(dir, p).split(sep).join(posix.sep));
}
})(dir);
return out;
}
/** 读 PNG 宽高;非 PNG 或读取失败返回 null。 */
export function readPngSize(file) {
try {
const b = readFileSync(file);
if (b.length < 24 || b.toString('ascii', 1, 4) !== 'PNG') return null;
return { width: b.readUInt32BE(16), height: b.readUInt32BE(20) };
} catch { return null; }
}
const isImage = (rel) => IMAGE_EXTS.has(extname(rel).toLowerCase());
const isSpine = (rel) => rel.split(posix.sep)[0] === SPINE_ROOT;
/** Spine 资源的分组键:spine/<资源名> */
const spineGroup = (rel) => rel.split(posix.sep).slice(0, 2).join(posix.sep);
/**
* 校验 override 是否合法。规则见 spec §3.2。
* 返回 { matched: 相对路径[], errors: {path, code, message}[] }。
*/
export function validateOverrides(frameworkUiDir, overrideDir) {
const fwList = listAssets(frameworkUiDir);
const fwSet = new Set(fwList);
const ovList = listAssets(overrideDir);
const ovSet = new Set(ovList);
const matched = [];
const errors = [];
// 1) 逐个 override 校验:孤儿 / 尺寸
for (const rel of ovList) {
if (!fwSet.has(rel)) {
const base = rel.slice(0, rel.length - extname(rel).length);
const alt = fwList.find((f) => f.slice(0, f.length - extname(f).length) === base);
errors.push({
path: rel,
code: 'orphan',
message: alt
? `框架里没有 ${rel},但存在同名的 ${alt}——扩展名对不上`
: `框架里没有对应资源:${rel}`,
});
continue;
}
// Spine 贴图豁免尺寸校验(区域坐标在 .atlas 内,整套替换时自洽)
const metaPath = join(overrideDir, ...`${rel}.meta`.split(posix.sep));
const needSizeCheck =
isImage(rel) && !isSpine(rel) && !existsSync(metaPath);
if (needSizeCheck) {
const a = readPngSize(join(frameworkUiDir, ...rel.split(posix.sep)));
const b = readPngSize(join(overrideDir, ...rel.split(posix.sep)));
if (!a || !b) {
const side = !a ? '框架' : 'override';
errors.push({
path: rel,
code: 'size-unreadable',
message:
`框架 UI 图片必须是 PNG(无法读取尺寸:${side})。` +
`请改用 PNG,或连 .meta 一起提供(例外通道)`,
});
continue;
}
if (a.width !== b.width || a.height !== b.height) {
errors.push({
path: rel,
code: 'size-mismatch',
message:
`尺寸不一致:框架 ${a.width}x${a.height},override ${b.width}x${b.height}。` +
`请改为同尺寸,或连 .meta 一起提供(例外通道)`,
});
continue;
}
}
matched.push(rel);
}
// 2) Spine 整套完整性:override 动了某套,就必须整套都在
const touchedGroups = new Set(ovList.filter(isSpine).map(spineGroup));
for (const g of touchedGroups) {
const required = fwList.filter((f) => isSpine(f) && spineGroup(f) === g);
const missing = required.filter((f) => !ovSet.has(f));
if (missing.length) {
errors.push({
path: g,
code: 'spine-incomplete',
message: `Spine 必须整套覆盖,缺少:${missing.join('、')}`,
});
}
}
return { matched, errors };
}