Files
youle_cocos/.pnpm-store/v11/projects/f47659282495706053836086ad375b3e/scripts/lib/materialize.mjs
T
2026-09-05 00:12:11 +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 };
}