mirror of
https://github.com/HappyLifeOk/cc-3-8-x-mcp.git
synced 2026-08-20 13:37:13 +00:00
[cli] 新增: nested 路径查找 + classid-resolver 多目录扫描
This commit is contained in:
@@ -50,11 +50,14 @@ function _extractCcClassNames(src) {
|
||||
return names;
|
||||
}
|
||||
|
||||
/** 在 assetsDir 下找所有 .ts(跳过 .d.ts),返回绝对路径数组。 */
|
||||
function _listTsFiles(assetsDir) {
|
||||
/** 在 scanDirs 下找所有 .ts(跳过 .d.ts),返回绝对路径数组。 */
|
||||
function _listTsFiles(scanDirs) {
|
||||
const existingDirs = scanDirs.filter((dir) => fs.existsSync(dir));
|
||||
if (existingDirs.length === 0) return [];
|
||||
const roots = existingDirs.map((dir) => `"${dir.replace(/"/g, '\\"')}"`).join(' ');
|
||||
try {
|
||||
const raw = execSync(
|
||||
`find "${assetsDir}" -name "*.ts" -not -name "*.d.ts" -type f`,
|
||||
`find ${roots} -name "*.ts" -not -name "*.d.ts" -type f`,
|
||||
{ encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 }
|
||||
);
|
||||
return raw.trim().split('\n').filter(Boolean);
|
||||
@@ -66,9 +69,11 @@ function _listTsFiles(assetsDir) {
|
||||
/** 建 projectRoot 下的 className -> entry 索引。 */
|
||||
function _buildIndex(projectRoot) {
|
||||
const scriptsDir = path.join(projectRoot, 'assets', 'scripts');
|
||||
const scanDir = fs.existsSync(scriptsDir) ? scriptsDir : path.join(projectRoot, 'assets');
|
||||
const scanDirs = fs.existsSync(scriptsDir)
|
||||
? [scriptsDir, path.join(projectRoot, 'extensions')]
|
||||
: [path.join(projectRoot, 'assets'), path.join(projectRoot, 'extensions')];
|
||||
|
||||
const tsFiles = _listTsFiles(scanDir);
|
||||
const tsFiles = _listTsFiles(scanDirs);
|
||||
const index = new Map();
|
||||
|
||||
for (const tsPath of tsFiles) {
|
||||
|
||||
@@ -82,6 +82,89 @@ function getNestedCompFileId(hostPrefabPath, elements, stubNodeId, compType, nod
|
||||
);
|
||||
}
|
||||
|
||||
function _findNestedNodeByPath(nEls, pathParts) {
|
||||
if (!Array.isArray(pathParts) || pathParts.length === 0) return null;
|
||||
let currentId = null;
|
||||
for (let i = 0; i < nEls.length; i++) {
|
||||
const el = nEls[i];
|
||||
if (!el || el.__type__ !== 'cc.Node') continue;
|
||||
if (el._parent !== null && el._parent !== undefined) continue;
|
||||
if (el._name === pathParts[0]) {
|
||||
currentId = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (currentId === null) {
|
||||
for (let i = 0; i < nEls.length; i++) {
|
||||
const el = nEls[i];
|
||||
if (el && el.__type__ === 'cc.Node' && el._name === pathParts[0]) {
|
||||
currentId = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentId === null) return null;
|
||||
|
||||
for (let partIdx = 1; partIdx < pathParts.length; partIdx++) {
|
||||
const current = nEls[currentId];
|
||||
const children = Array.isArray(current._children) ? current._children : [];
|
||||
let nextId = null;
|
||||
for (const childRef of children) {
|
||||
if (!childRef || typeof childRef.__id__ !== 'number') continue;
|
||||
const child = nEls[childRef.__id__];
|
||||
if (child && child.__type__ === 'cc.Node' && child._name === pathParts[partIdx]) {
|
||||
nextId = childRef.__id__;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nextId === null) return null;
|
||||
currentId = nextId;
|
||||
}
|
||||
return { node: nEls[currentId], nodeId: currentId };
|
||||
}
|
||||
|
||||
function _getNestedNodeFileIdByPath(hostPrefabPath, elements, stubNodeId, pathParts) {
|
||||
const { nestedPath, nestedData } = _loadNestedPrefab(hostPrefabPath, elements, stubNodeId);
|
||||
const nEls = nestedData.elements;
|
||||
const found = _findNestedNodeByPath(nEls, pathParts);
|
||||
if (!found) {
|
||||
throw new Error(`getNestedNodeFileIdByPath: 在嵌套 prefab "${nestedPath}" 中找不到路径 "${pathParts.join('/')}"`);
|
||||
}
|
||||
const node = found.node;
|
||||
if (!node._prefab || typeof node._prefab.__id__ !== 'number') {
|
||||
throw new Error(`getNestedNodeFileIdByPath: 路径 "${pathParts.join('/')}" 的节点没有 PrefabInfo`);
|
||||
}
|
||||
const pi = nEls[node._prefab.__id__];
|
||||
if (!pi || pi.__type__ !== 'cc.PrefabInfo' || typeof pi.fileId !== 'string' || pi.fileId.length === 0) {
|
||||
throw new Error(`getNestedNodeFileIdByPath: 路径 "${pathParts.join('/')}" 的节点没有有效 fileId`);
|
||||
}
|
||||
return pi.fileId;
|
||||
}
|
||||
|
||||
function _getNestedCompFileIdByPath(hostPrefabPath, elements, stubNodeId, compType, pathParts) {
|
||||
const { nestedPath, nestedData } = _loadNestedPrefab(hostPrefabPath, elements, stubNodeId);
|
||||
const nEls = nestedData.elements;
|
||||
const found = _findNestedNodeByPath(nEls, pathParts);
|
||||
if (!found) {
|
||||
throw new Error(`getNestedCompFileIdByPath: 在嵌套 prefab "${nestedPath}" 中找不到路径 "${pathParts.join('/')}"`);
|
||||
}
|
||||
const comps = Array.isArray(found.node._components) ? found.node._components : [];
|
||||
for (const compRef of comps) {
|
||||
if (!compRef || typeof compRef.__id__ !== 'number') continue;
|
||||
const comp = nEls[compRef.__id__];
|
||||
if (!comp || comp.__type__ !== compType) continue;
|
||||
if (!comp.__prefab || typeof comp.__prefab.__id__ !== 'number') continue;
|
||||
const cpi = nEls[comp.__prefab.__id__];
|
||||
if (!cpi || cpi.__type__ !== 'cc.CompPrefabInfo') continue;
|
||||
if (typeof cpi.fileId !== 'string' || cpi.fileId.length === 0) continue;
|
||||
return cpi.fileId;
|
||||
}
|
||||
throw new Error(
|
||||
`getNestedCompFileIdByPath: 嵌套 prefab "${nestedPath}" 的路径 "${pathParts.join('/')}" ` +
|
||||
`找不到 ${compType} 组件,或该组件没有 cc.CompPrefabInfo.fileId。`
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 嵌套 prefab:找目标节点的 PrefabInfo.fileId ──────────────
|
||||
|
||||
/**
|
||||
@@ -267,6 +350,15 @@ function resolveLocalIdChain(hostPrefabPath, elements, stubNodeId, compType, sub
|
||||
return resolveLocalIdChain(hostPrefabPath, elements, stubNodeId, compType, subNode[0]);
|
||||
}
|
||||
|
||||
try {
|
||||
if (compType === 'cc.Node') {
|
||||
return [_getNestedNodeFileIdByPath(hostPrefabPath, elements, stubNodeId, subNode)];
|
||||
}
|
||||
return [_getNestedCompFileIdByPath(hostPrefabPath, elements, stubNodeId, compType, subNode)];
|
||||
} catch (_) {
|
||||
// 不是普通子节点路径时,继续沿用旧语义:数组表示多层 nested stub 链。
|
||||
}
|
||||
|
||||
// 多层:从当前 stub 进入第一层嵌套,找名字 = subNode[0] 的内嵌 stub,
|
||||
// 拿到它在嵌套 prefab 内的 fileId,递归走剩下的路径
|
||||
const [firstSeg, ...restPath] = subNode;
|
||||
@@ -382,6 +474,8 @@ function addRootTargetOverride(prefabData, rootId, sourceCompId, propertyPath, t
|
||||
if (!ti || !Array.isArray(ti.localID)) continue;
|
||||
if (ti.localID.length !== localIdChain.length) continue;
|
||||
if (ti.localID.every((v, i) => v === localIdChain[i])) return;
|
||||
ti.localID = localIdChain.slice();
|
||||
return;
|
||||
}
|
||||
|
||||
const targetInfoId = elements.length;
|
||||
|
||||
@@ -112,7 +112,7 @@ const SCHEMAS = {
|
||||
'reorder-children': { required: ['node', 'order'], optional: [] },
|
||||
// add-node 的 node 是「新节点描述对象」而非 selector
|
||||
'add-node': { required: ['parent', 'node'], optional: [], typeOverrides: { node: 'object' } },
|
||||
'remove-node': { required: ['target'], optional: [] },
|
||||
'remove-node': { required: [], optional: ['target', 'node'] },
|
||||
'clone-node': { required: ['source', 'parent', 'name'], optional: [] },
|
||||
'add-component': { required: ['node', 'componentType'], optional: ['props'] },
|
||||
'remove-component': { required: ['node', 'componentType'], optional: [] },
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 并递归断开整棵子树所有节点/组件的 _parent 引用。
|
||||
// 节点元素本身保留在数组(保持其他 __id__ 稳定)。
|
||||
// op: { op: 'remove-node', target: string|{id:N} }
|
||||
// 兼容旧文档写法:{ op: 'remove-node', node: string|{id:N} }
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -15,7 +16,10 @@ const {
|
||||
|
||||
function execRemoveNode(prefabData, op) {
|
||||
const { elements, rootId } = prefabData;
|
||||
const { target: targetSelector } = op;
|
||||
const targetSelector = op.target !== undefined ? op.target : op.node;
|
||||
if (targetSelector === undefined) {
|
||||
throw new Error(`editPrefab [remove-node]: 缺少 target(兼容旧写法 node)`);
|
||||
}
|
||||
|
||||
const { node: targetNode, nodeId: targetId } = resolveNode(prefabData, targetSelector, 'remove-node');
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const crypto = require('crypto');
|
||||
const { editPrefab } = require('../src/editor/index.js');
|
||||
const { parsePrefab } = require('../src/parse.js');
|
||||
const { listOverrides } = require('../src/overrides.js');
|
||||
const { addRootTargetOverride, resolveLocalIdChain } = require('../src/editor/nested.js');
|
||||
|
||||
const FIXTURE_PATH = path.resolve(__dirname, 'fixtures/HomeUI.prefab');
|
||||
|
||||
@@ -431,6 +432,27 @@ test('remove-node: 普通节点 → 父 _children 不再含引用', () => {
|
||||
assert.equal(orphan._parent, null, '孤儿节点 _parent 应为 null');
|
||||
});
|
||||
|
||||
test('remove-node: 兼容旧文档 node 字段', () => {
|
||||
const tmp = path.join(os.tmpdir(), `remove-node-alias-${crypto.randomBytes(4).toString('hex')}.prefab`);
|
||||
tmpFiles.push(tmp);
|
||||
const data = [
|
||||
{ __type__: 'cc.Prefab', data: { __id__: 1 } },
|
||||
{ __type__: 'cc.Node', _name: 'Root', _parent: null, _children: [{ __id__: 2 }], _components: [], _prefab: { __id__: 3 } },
|
||||
{ __type__: 'cc.Node', _name: 'toDeleteByNodeAlias', _parent: { __id__: 1 }, _children: [], _components: [], _prefab: { __id__: 4 } },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 1 }, asset: { __id__: 0 }, fileId: 'rootFileId', instance: null, targetOverrides: null, nestedPrefabInstanceRoots: null },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 2 }, asset: { __id__: 0 }, fileId: 'childFileId', instance: null, targetOverrides: null, nestedPrefabInstanceRoots: null },
|
||||
];
|
||||
fs.writeFileSync(tmp, JSON.stringify(data));
|
||||
|
||||
editPrefab(tmp, [
|
||||
{ op: 'remove-node', node: 'toDeleteByNodeAlias' },
|
||||
]);
|
||||
|
||||
const reparsed = parsePrefab(tmp);
|
||||
const orphan = reparsed.elements[2];
|
||||
assert.equal(orphan._parent, null, '旧 node 字段写法也应正确删除节点');
|
||||
});
|
||||
|
||||
test('remove-node: stub 子节点(mountedChildren 内) → mountedChildren 移除', () => {
|
||||
const tmp = cloneFixture('remove-stub-child');
|
||||
tmpFiles.push(tmp);
|
||||
@@ -517,6 +539,67 @@ test('remove-node: 删嵌套 stub → 清根 targetOverrides 中指向它的悬
|
||||
);
|
||||
});
|
||||
|
||||
test('set-component-ref: refSubNode 数组可定位嵌套 prefab 内普通子节点路径', () => {
|
||||
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'nested-path-project-'));
|
||||
tmpFiles.push(path.join(project, 'assets', 'host.prefab'));
|
||||
fs.writeFileSync(path.join(project, 'package.json'), '{}');
|
||||
fs.mkdirSync(path.join(project, 'assets', 'nested'), { recursive: true });
|
||||
|
||||
const nestedUuid = '11111111-2222-4333-8444-555555555555';
|
||||
const nestedPath = path.join(project, 'assets', 'nested', 'Reddot.prefab');
|
||||
const nestedData = [
|
||||
{ __type__: 'cc.Prefab', data: { __id__: 1 } },
|
||||
{ __type__: 'cc.Node', _name: 'Reddot', _parent: null, _children: [{ __id__: 2 }], _components: [], _prefab: { __id__: 6 } },
|
||||
{ __type__: 'cc.Node', _name: 'content', _parent: { __id__: 1 }, _children: [{ __id__: 3 }], _components: [], _prefab: { __id__: 7 } },
|
||||
{ __type__: 'cc.Node', _name: 'title', _parent: { __id__: 2 }, _children: [], _components: [{ __id__: 4 }], _prefab: { __id__: 8 } },
|
||||
{ __type__: 'cc.Label', node: { __id__: 3 }, __prefab: { __id__: 5 } },
|
||||
{ __type__: 'cc.CompPrefabInfo', fileId: 'labelTitleFileId' },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 1 }, asset: { __id__: 0 }, fileId: 'rootFileId', instance: null },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 2 }, asset: { __id__: 0 }, fileId: 'contentFileId', instance: null },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 3 }, asset: { __id__: 0 }, fileId: 'titleFileId', instance: null },
|
||||
];
|
||||
fs.writeFileSync(nestedPath, JSON.stringify(nestedData));
|
||||
fs.writeFileSync(nestedPath + '.meta', JSON.stringify({ uuid: nestedUuid }));
|
||||
|
||||
const hostPath = path.join(project, 'assets', 'host.prefab');
|
||||
const hostData = [
|
||||
{ __type__: 'cc.Prefab', data: { __id__: 1 } },
|
||||
{ __type__: 'cc.Node', _name: 'Host', _parent: null, _children: [{ __id__: 2 }], _components: [], _prefab: { __id__: 5 } },
|
||||
{ __type__: 'cc.Node', _parent: { __id__: 1 }, _prefab: { __id__: 3 } },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 2 }, asset: { __uuid__: nestedUuid, __expectedType__: 'cc.Prefab' }, fileId: 'stubFileId', instance: { __id__: 4 } },
|
||||
{ __type__: 'cc.PrefabInstance', fileId: 'instFileId', prefabRootNode: { __id__: 1 }, mountedChildren: [], mountedComponents: [], propertyOverrides: [], removedComponents: [] },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 1 }, asset: { __id__: 0 }, fileId: 'hostRootFileId', instance: null, targetOverrides: null, nestedPrefabInstanceRoots: [{ __id__: 2 }] },
|
||||
];
|
||||
fs.writeFileSync(hostPath, JSON.stringify(hostData));
|
||||
|
||||
const chain = resolveLocalIdChain(hostPath, hostData, 2, 'cc.Label', ['content', 'title']);
|
||||
assert.deepEqual(chain, ['labelTitleFileId'], '普通子节点路径应解析到 title 的 Label fileId');
|
||||
});
|
||||
|
||||
test('set-component-ref: 同 source/property/target 不同 localID 时覆盖旧 targetInfo', () => {
|
||||
const data = [
|
||||
{ __type__: 'cc.Prefab', data: { __id__: 1 } },
|
||||
{ __type__: 'cc.Node', _name: 'Root', _parent: null, _children: [{ __id__: 2 }], _components: [{ __id__: 6 }], _prefab: { __id__: 3 } },
|
||||
{ __type__: 'cc.Node', _name: null, _parent: { __id__: 1 }, _prefab: { __id__: 4 } },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 1 }, asset: { __id__: 0 }, fileId: 'rootFileId', instance: null, targetOverrides: [{ __id__: 8 }], nestedPrefabInstanceRoots: [{ __id__: 2 }] },
|
||||
{ __type__: 'cc.PrefabInfo', root: { __id__: 2 }, asset: { __uuid__: 'nested' }, fileId: 'stubFileId', instance: { __id__: 5 } },
|
||||
{ __type__: 'cc.PrefabInstance', fileId: 'instFileId', prefabRootNode: { __id__: 1 }, mountedChildren: [], mountedComponents: [], propertyOverrides: [], removedComponents: [] },
|
||||
{ __type__: 'SomeComp', node: { __id__: 1 }, __prefab: { __id__: 7 } },
|
||||
{ __type__: 'cc.CompPrefabInfo', fileId: 'sourceCompFileId' },
|
||||
{ __type__: 'cc.TargetOverrideInfo', source: { __id__: 6 }, sourceInfo: null, propertyPath: ['_reddot'], target: { __id__: 2 }, targetInfo: { __id__: 9 } },
|
||||
{ __type__: 'cc.TargetInfo', localID: ['oldNodeFileId'] },
|
||||
];
|
||||
const prefabData = { elements: data };
|
||||
|
||||
addRootTargetOverride(prefabData, 1, 6, ['_reddot'], 2, ['newComponentFileId']);
|
||||
|
||||
const rootPi = data[3];
|
||||
assert.equal(rootPi.targetOverrides.length, 1, '同字段覆盖不应新增重复 override');
|
||||
const override = data[rootPi.targetOverrides[0].__id__];
|
||||
const targetInfo = data[override.targetInfo.__id__];
|
||||
assert.deepEqual(targetInfo.localID, ['newComponentFileId'], 'localID 应被覆盖为新的组件 fileId');
|
||||
});
|
||||
|
||||
// ─── clone-node ───────────────────────────────────────────────
|
||||
|
||||
test('clone-node: 整棵子树复制、所有 _parent 正确、新 fileId 不与原冲突', () => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const { test, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { compressUuid } = require('../src/id.js');
|
||||
const { clearCache, resolveClassIdByName } = require('../src/classid-resolver.js');
|
||||
|
||||
const tmpRoots = [];
|
||||
|
||||
after(() => {
|
||||
clearCache();
|
||||
for (const root of tmpRoots) {
|
||||
try { fs.rmSync(root, { recursive: true, force: true }); } catch (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
test('ClassIdResolver: 扫描 extensions 下的 ccclass 脚本', () => {
|
||||
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'classid-ext-project-'));
|
||||
tmpRoots.push(project);
|
||||
|
||||
fs.writeFileSync(path.join(project, 'package.json'), '{}');
|
||||
fs.mkdirSync(path.join(project, 'assets', 'scripts'), { recursive: true });
|
||||
fs.mkdirSync(path.join(project, 'extensions', 'cc-state-controller', 'lib'), { recursive: true });
|
||||
|
||||
const uuid = 'fca1c7d0-b8e9-4a6f-9c3d-2e5f18a04b7c';
|
||||
const tsPath = path.join(project, 'extensions', 'cc-state-controller', 'lib', 'StateController.ts');
|
||||
fs.writeFileSync(tsPath, "import { _decorator } from 'cc';\nconst { ccclass } = _decorator;\n@ccclass('StateController')\nexport class StateController {}\n");
|
||||
fs.writeFileSync(tsPath + '.meta', JSON.stringify({ uuid }));
|
||||
|
||||
assert.equal(resolveClassIdByName('StateController', project), compressUuid(uuid));
|
||||
});
|
||||
Reference in New Issue
Block a user