'use strict'; const fs = require('fs'); const path = require('path'); const { listAssets, queryAssetData, queryAssetInfo, queryAssetMeta } = require('./assets'); const { resolveProjectPath } = require('./path-safety'); function requestEditorMessage(channel, method, ...args) { if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') { throw new Error('Editor.Message.request is unavailable in the Cocos extension host.'); } return Editor.Message.request(channel, method, ...args); } function assetUrlToPath(projectPath, url) { if (!url || !String(url).startsWith('db://assets/')) { return ''; } return path.join(projectPath, String(url).slice('db://'.length)); } function assetFilePath(projectPath, info) { const candidates = [ info && info.file, info && info.path, info && info.source, info && info.url ? assetUrlToPath(projectPath, info.url) : '', ].filter(Boolean); for (const candidate of candidates) { const fullPath = path.isAbsolute(candidate) ? resolveProjectPath(projectPath, candidate) : resolveProjectPath(projectPath, candidate); if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { return fullPath; } } return ''; } function collectUuidReferences(value, refs = [], pointer = '') { if (!value || typeof value !== 'object') { return refs; } if (Array.isArray(value)) { value.forEach((item, index) => collectUuidReferences(item, refs, `${pointer}/${index}`)); return refs; } for (const [key, child] of Object.entries(value)) { const childPointer = `${pointer}/${key}`; if ( typeof child === 'string' && (key.toLowerCase().includes('uuid') || key === '__uuid__' || key === 'assetUuid' || key === 'prefabUuid') ) { refs.push({ uuid: child, path: childPointer, key }); } else { collectUuidReferences(child, refs, childPointer); } } return refs; } function getByJsonPath(target, jsonPath) { const segments = String(jsonPath || '') .replace(/^\//, '') .split(/[/.]/) .map((segment) => segment.trim()) .filter(Boolean); let current = target; for (const segment of segments) { if (current == null) { return undefined; } current = current[segment]; } return current; } function setByJsonPath(target, jsonPath, value) { const segments = String(jsonPath || '') .replace(/^\//, '') .split(/[/.]/) .map((segment) => segment.trim()) .filter(Boolean); if (!segments.length) { throw new Error('jsonPath is required.'); } let current = target; for (let index = 0; index < segments.length - 1; index += 1) { const segment = segments[index]; if (current[segment] == null || typeof current[segment] !== 'object') { current[segment] = {}; } current = current[segment]; } current[segments[segments.length - 1]] = value; } async function inspectPrefab(projectPath, target) { const info = await queryAssetInfo(target); const meta = await queryAssetMeta(target).catch(() => null); const data = await queryAssetData(target).catch(() => null); const filePath = assetFilePath(projectPath, info); const content = filePath ? fs.readFileSync(filePath, 'utf8') : ''; const parsed = content ? JSON.parse(content) : data; const references = collectUuidReferences(parsed).slice(0, 500); return { info, meta, filePath: filePath ? path.relative(projectPath, filePath).replace(/\\/g, '/') : '', referenceCount: references.length, references, }; } async function validatePrefabReferences(projectPath, options = {}) { const targets = options.target ? [options.target] : (await listAssets({ pattern: options.pattern || 'db://assets/**', ccType: 'cc.Prefab' })) .slice(0, Number.isFinite(options.limit) ? Math.max(1, Math.min(200, options.limit)) : 50) .map((asset) => asset.uuid || asset.url) .filter(Boolean); const prefabs = []; for (const target of targets) { const prefab = await inspectPrefab(projectPath, target); const checked = []; const missing = []; for (const ref of prefab.references) { try { const info = await queryAssetInfo(ref.uuid); checked.push({ ...ref, exists: true, asset: { uuid: info.uuid, url: info.url, type: info.type } }); } catch (error) { missing.push({ ...ref, exists: false, error: error.message }); } } prefabs.push({ target, filePath: prefab.filePath, referenceCount: prefab.referenceCount, checkedCount: checked.length + missing.length, missingCount: missing.length, missing, }); } const missingCount = prefabs.reduce((sum, prefab) => sum + prefab.missingCount, 0); return { ok: missingCount === 0, prefabCount: prefabs.length, missingCount, prefabs, }; } async function duplicatePrefab(projectPath, options = {}) { const source = String(options.source || '').trim(); const target = String(options.target || '').trim(); if (!source || !target) { throw new Error('source and target are required.'); } const info = await queryAssetInfo(source); const sourcePath = assetFilePath(projectPath, info); if (!sourcePath) { throw new Error(`Prefab source file was not found: ${source}`); } const targetPath = resolveProjectPath(projectPath, target.endsWith('.prefab') ? target : `${target}.prefab`); const assetsRoot = path.join(projectPath, 'assets'); const relativeToAssets = path.relative(assetsRoot, targetPath); if (relativeToAssets.startsWith('..') || path.isAbsolute(relativeToAssets)) { throw new Error('target must be inside the Cocos assets directory.'); } if (fs.existsSync(targetPath) && options.overwrite !== true) { throw new Error(`Target prefab already exists: ${target}`); } fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.copyFileSync(sourcePath, targetPath); return { duplicated: true, source: path.relative(projectPath, sourcePath).replace(/\\/g, '/'), target: path.relative(projectPath, targetPath).replace(/\\/g, '/'), }; } async function editPrefabJson(projectPath, options = {}) { const target = String(options.target || '').trim(); if (!target) { throw new Error('target is required.'); } const info = await queryAssetInfo(target); const filePath = assetFilePath(projectPath, info); if (!filePath) { throw new Error(`Prefab file was not found: ${target}`); } const original = fs.readFileSync(filePath, 'utf8'); let updated = original; if (options.search !== undefined) { const search = String(options.search); if (!search) { throw new Error('search must not be empty.'); } if (!original.includes(search)) { throw new Error('search text was not found in prefab file.'); } updated = options.replaceAll ? original.split(search).join(String(options.replace || '')) : original.replace(search, String(options.replace || '')); } else { const json = JSON.parse(original); const value = JSON.parse(String(options.valueJson || 'null')); setByJsonPath(json, options.jsonPath, value); updated = JSON.stringify(json, null, 2) + '\n'; } JSON.parse(updated); if (options.createBackup) { fs.writeFileSync(`${filePath}.bak`, original, 'utf8'); } fs.writeFileSync(filePath, updated, 'utf8'); return { edited: true, path: path.relative(projectPath, filePath).replace(/\\/g, '/'), oldValue: options.jsonPath ? getByJsonPath(JSON.parse(original), options.jsonPath) : undefined, validation: await validatePrefabReferences(projectPath, { target }), }; } async function applyPrefabInstance(nodeUuid) { const uuid = String(nodeUuid || '').trim(); if (!uuid) { throw new Error('node uuid is required.'); } const result = await requestEditorMessage('scene', 'apply-prefab', uuid); return { applied: true, uuid, result }; } async function revertPrefabInstance(nodeUuid) { const uuid = String(nodeUuid || '').trim(); if (!uuid) { throw new Error('node uuid is required.'); } const candidates = ['revert-prefab', 'restore-prefab']; let lastError = null; for (const method of candidates) { try { const result = await requestEditorMessage('scene', method, uuid); return { reverted: true, uuid, method, result }; } catch (error) { lastError = error; } } throw lastError || new Error('No prefab revert editor message was available.'); } module.exports = { applyPrefabInstance, duplicatePrefab, editPrefabJson, inspectPrefab, revertPrefabInstance, validatePrefabReferences, };