feat(tools): materialize 实体化与皮肤合成
junction 变真实拷贝(发布链路不依赖 junction), 框架真源全程只读。 合成先整体校验再写, 不产生半成品; 例外通道把 override meta 的 uuid 改回框架原值以保持 Prefab 引用不断。 修复:Windows UNC 长路径前缀导致 cpSync filter 中 relative() 失败, 规范化 src 路径后正确计算相对位置进行缓存目录过滤。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
||||
import { join, posix, dirname, relative, sep, resolve } from 'node:path';
|
||||
import { listAssets, validateOverrides } from './skin.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 frameworkLink = join(gameDir, 'assets', 'framework');
|
||||
const gameDirNorm = resolve(gameDir);
|
||||
cpSync(gameDir, destDir, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: (src) => {
|
||||
if (src === frameworkLink) return false;
|
||||
// 处理 Windows UNC 长路径前缀 (\\?\) 导致 relative() 失败的问题
|
||||
const srcNorm = resolve(src.replace(/^\\\\\?\\/, ''));
|
||||
const rel = relative(gameDirNorm, srcNorm);
|
||||
if (!rel) return true; // gameDir 自身
|
||||
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) {
|
||||
const frameworkUiDir = join(destDir, 'assets', 'framework', 'ui');
|
||||
const overrideDir = join(destDir, 'assets', 'game', 'override');
|
||||
|
||||
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;
|
||||
const merged = JSON.parse(readFileSync(ovMeta, 'utf8'));
|
||||
merged.uuid = fwUuid;
|
||||
writeFileSync(fwMeta, `${JSON.stringify(merged, null, 2)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return { matched, frameworkTotal: listAssets(frameworkUiDir).length };
|
||||
}
|
||||
Reference in New Issue
Block a user