Files
youle_cocos/cocoscreator_projects/scripts/lib/materialize.mjs
T
joywayerandClaude Opus 5 5708926711 fix(skin): 修复最终评审发现的 6 处缺陷(junction 排除失效/meta 静默丢失等)
跨任务视角发现的问题:Critical1 materialize.mjs 的 junction 排除比较在
Windows 上因长路径前缀恒假、从未生效,且测试夹具的 junction 指向真源自身,
掩盖了此问题;Critical2 composeSkin 例外通道在 uuid 缺失/框架侧无 meta 时
静默失败,违反第二准则;Important3 可覆盖范围误把 theme/ 下的框架 TS 代码
也纳入;Important5 cli-entry 测试漏注册 check-skin/build-game;Important6
composeSkin 与 check-skin 各自拼路径,未共用 paths.mjs 权威推导;Important7
--platform 缺值时静默落到默认平台。逐条修复并补测试,Critical1 用诱饵框架
目录验证过能真正杀掉该 bug(变异推演见 fix report)。

spec §5 step5 的 dist/<name>/ 拷贝按要求标注为本期未实现,不实现。

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

102 lines
4.8 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 { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
import { join, posix, dirname, relative, sep, resolve, basename } from 'node:path';
import { listCoverableAssets, validateOverrides } from './skin.mjs';
import { gameOverrideDir, frameworkUiDirOf } from './paths.mjs';
/**
* 工程缓存目录:实体化时跳过。
* **只匹配工程根的直接子目录**——绝不可按目录名在任意层级匹配,否则会连
* `assets/game/native/` 这类业务目录一起吞掉(与 .gitignore 无锚点模式同一类错误,见 Task 1)。
*/
const CACHE_DIRS = new Set(['library', 'temp', 'local', 'build', 'native', 'profiles', 'node_modules']);
export class SkinValidationError extends Error {
constructor(errors) {
super(`皮肤覆盖校验未通过(${errors.length} 处问题)`);
this.name = 'SkinValidationError';
this.errors = errors;
}
}
/**
* 把子游戏工程实体化到 destDir:
* `assets/framework` 由 junction 变成对 frameworkSrc 的真实目录拷贝;跳过缓存目录。
* **frameworkSrc 全程只读**(spec §5.1 ①)。
*/
export function materialize(gameDir, frameworkSrc, destDir) {
if (existsSync(destDir)) throw new Error(`目标已存在:${destDir}`);
mkdirSync(destDir, { recursive: true });
// 1) 拷工程本体,跳过缓存目录与 assets/framework(后者单独从真源拷)
const frameworkRel = join('assets', 'framework');
const gameDirNorm = resolve(gameDir);
cpSync(gameDir, destDir, {
recursive: true,
dereference: true,
filter: (src) => {
// 处理 Windows UNC 长路径前缀 (\\?\):cpSync 传入的 src 在 Windows 上
// 每一层都带该前缀,若不归一化,下面任何基于 rel 的比较都会恒假
// (包括曾经直接 `src === frameworkLink` 的写法——那是 Critical 1 的成因)。
const srcNorm = resolve(src.replace(/^\\\\\?\\/, ''));
const rel = relative(gameDirNorm, srcNorm);
if (!rel) return true; // gameDir 自身
if (rel === frameworkRel) return false; // junction:单独从真源拷贝,不经此路径解引用
return !CACHE_DIRS.has(rel.split(sep)[0]); // 只看第一段:锚定到工程根
},
});
// 2) 从真源实体化 framework(不依赖 junction 存在,spec §5.1 ②)
cpSync(frameworkSrc, join(destDir, 'assets', 'framework'), { recursive: true, dereference: true });
}
/**
* 把临时工程内的 `assets/game/override/**` 合成到 `assets/framework/ui/**`。
* 先整体校验,不通过则抛错且**不写任何文件**(避免半成品)。
*/
export function composeSkin(destDir) {
// 路径推导唯一权威在 paths.mjs(check-skin 用同一套),composeSkin 不再自己拼一份地图
const frameworkUiDir = frameworkUiDirOf(destDir);
const overrideDir = gameOverrideDir(destDir);
const gameName = basename(destDir);
const { matched, errors } = validateOverrides(frameworkUiDir, overrideDir);
if (errors.length) throw new SkinValidationError(errors);
for (const rel of matched) {
const parts = rel.split(posix.sep);
const from = join(overrideDir, ...parts);
const to = join(frameworkUiDir, ...parts);
mkdirSync(dirname(to), { recursive: true });
copyFileSync(from, to);
// 例外通道:override 自带 meta 时,把 uuid 改回框架原值以保持引用
const ovMeta = `${from}.meta`;
const fwMeta = `${to}.meta`;
if (existsSync(ovMeta) && existsSync(fwMeta)) {
const fwUuid = JSON.parse(readFileSync(fwMeta, 'utf8')).uuid;
if (!fwUuid) {
// 静默通过会写出没有 uuid 的 .meta——Cocos 重新生成 UUID,框架 Prefab 对该
// 资源的引用在发布包里断裂,这正是"保留 .meta"整个设计要防的唯一失效模式。
throw new SkinValidationError([{
path: rel,
code: 'meta-missing-uuid',
message: `${gameName}: 框架侧 .meta 没有 uuid(${fwMeta}),无法为例外通道保留引用,请先修复框架该文件的 .meta`,
}]);
}
const merged = JSON.parse(readFileSync(ovMeta, 'utf8'));
merged.uuid = fwUuid;
writeFileSync(fwMeta, `${JSON.stringify(merged, null, 2)}\n`);
} else if (existsSync(ovMeta) && !existsSync(fwMeta)) {
// 框架侧没有 .meta 时整段合并被跳过,override 自带的 meta 会被无声丢弃——
// 同样违反"下游不兜底":必须显式报错,而不是悄悄忽略 override 的意图。
throw new SkinValidationError([{
path: rel,
code: 'meta-orphan',
message: `${gameName}: override 提供了 ${rel}.meta,但框架侧没有对应的 .meta(${fwMeta}),无法合并 uuid(例外通道要求框架侧资源本身已有 .meta)`,
}]);
}
}
return { matched, frameworkTotal: listCoverableAssets(frameworkUiDir).length };
}