From 824da453f3029efe135f4bd49da550bd5c8b894a Mon Sep 17 00:00:00 2001 From: rh Date: Thu, 31 Oct 2024 10:37:08 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88=E6=9C=AC=E7=9A=84?= =?UTF-8?q?=E5=88=9D=E7=95=A5=E4=BF=AE=E6=94=B9=EF=BC=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- base.tsconfig.json | 22 ++ dist/main.js | 472 +++++++++++++++++++++++++++++++++++ dist/panels/default/index.js | 1 + dist/util/FileUtil.js | 80 ++++++ dist/util/ObjectUtil.js | 67 +++++ i18n/en.js | 1 + i18n/zh.js | 1 + package-lock.json | 277 ++++++++++++++++++++ package.json | 41 +++ scripts/preinstall.js | 1 + source/main.ts | 455 +++++++++++++++++++++++++++++++++ source/util/FileUtil.ts | 64 +++++ source/util/ObjectUtil.ts | 66 +++++ tsconfig.json | 29 +++ 14 files changed, 1577 insertions(+) create mode 100644 base.tsconfig.json create mode 100644 dist/main.js create mode 100644 dist/panels/default/index.js create mode 100644 dist/util/FileUtil.js create mode 100644 dist/util/ObjectUtil.js create mode 100644 i18n/en.js create mode 100644 i18n/zh.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/preinstall.js create mode 100644 source/main.ts create mode 100644 source/util/FileUtil.ts create mode 100644 source/util/ObjectUtil.ts create mode 100644 tsconfig.json diff --git a/base.tsconfig.json b/base.tsconfig.json new file mode 100644 index 0000000..6fd6302 --- /dev/null +++ b/base.tsconfig.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json", + "compilerOptions": { + "target": "ES2017", + "module": "CommonJS", + "moduleResolution": "node", + "inlineSourceMap": true, + "inlineSources": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./source", + "types": [ + "node", + "@cocos/creator-types/editor", + ] + } +} \ No newline at end of file diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000..a1318ba --- /dev/null +++ b/dist/main.js @@ -0,0 +1,472 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.methods = void 0; +exports.load = load; +exports.unload = unload; +const ObjectUtil_1 = __importDefault(require("./util/ObjectUtil")); +const FileUtil_1 = __importDefault(require("./util/FileUtil")); +const package_json_1 = __importDefault(require("../package.json")); +const Fs = require('fs'); +const Path = require('path'); +const EXTENSION_NAME = '🔎'; +/** 扩展名对应文件类型 */ +const typeMap = { + '.fire': 'scene', + '.prefab': 'prefab', + '.anim': 'animation', + '.mtl': 'material', + '.fnt.meta': 'font', +}; +/** 类型对应图标 */ +const iconMap = { + 'scene': '📺', + 'prefab': '🧸', + 'node': '💾', + 'component': '💿', + 'property': '🎲', + 'asset': '📦', +}; +const translate = (key) => `H.${key}`; +/** + * @en Registration method for the main process of Extension + * @zh 为扩展的主进程的注册方法 + */ +exports.methods = { + openDebug() { + console.log('openDebug'); + Editor.Panel.open(package_json_1.default.name); + }, + /** + * @en A method that can be triggered by message + * @zh 通过 message 触发的方法 + */ + findCurrentSelection() { + return __awaiter(this, void 0, void 0, function* () { + const uuids = Editor.Selection.getSelected('asset'); + if (uuids.length === 0) { + console.log(`[${EXTENSION_NAME}]`, '先选择资源'); + return; + } + // 根据 uuid 查找 + for (let i = 0; i < uuids.length; i++) { + yield this.module.methods.findViaUuid(uuids[i]); + } + }); + }, + /** + * 使用 uuid 进行查找 + * @param {string} uuid + */ + findViaUuid(uuid) { + return __awaiter(this, void 0, void 0, function* () { + // 是否为有效 uuid + if (!Editor.Utils.UUID.isUUID(uuid)) { + console.log(`[${EXTENSION_NAME}]`, 'invalidUuid' + uuid); + return; + } + // 获取资源信息 + const assetInfo = yield Editor.Message.request('asset-db', 'query-asset-info', uuid); + if (assetInfo) { + const url = assetInfo.url.replace('db://', ''); + // 暂不查找文件夹 + if (assetInfo.type === 'folder') { + console.log(`[${EXTENSION_NAME}]`, 'notSupportFolder' + url); + return; + } + // 处理文件路径 & 打印头部日志 + const urlItems = url.split('/'); + if (!urlItems[urlItems.length - 1].includes('.')) { + urlItems.splice(urlItems.length - 1); + } + console.log(`[${EXTENSION_NAME}]`, 'findAssetRefs' + urlItems.join('/')); + // 记录子资源 uuid + const subUuids = assetInfo ? [] : null; + // 资源类型检查 + if (assetInfo.type === "cc.ImageAsset") { + // 纹理子资源 + uuid = uuid + '@f9941'; + } + else if (assetInfo.type === 'cc.Script') { + // 脚本 + uuid = Editor.Utils.UUID.compressUUID(uuid, true); + } + // 查找 + const results = uuid ? yield this.findReferences(uuid) : []; + // Done + this.printResult(results); + } + else { + // 不存在的资源,直接查找 uuid + console.log(`[${EXTENSION_NAME}]`, 'findAssetRefs' + uuid); + const result = yield this.findReferences(uuid); + // Done + this.printResult(result); + } + }); + }, + /** + * 获取对象中是否包含指定值以及相应属性名 + * @param {object} object 对象 + * @param {any} value 值 + * @returns {{ contains: boolean, property: string }} + */ + getContainsInfo(object, value) { + const result = { + contains: false, + property: null + }; + // 搜索 + const search = (target, parentKey) => { + if (ObjectUtil_1.default.isObject(target)) { + for (const key in target) { + if (target[key] === value) { + result.contains = true; + result.property = parentKey; + return; + } + search(target[key], key); + } + } + else if (Array.isArray(target)) { + for (let i = 0, l = target.length; i < l; i++) { + search(target[i], parentKey); + } + } + }; + search(object, null); + // Done + return result; + }, + /** + * 查找节点中的引用 + * @param {object} node 目标节点 + * @param {string} uuid 查找的 uuid + * @param {object} nodeTree 节点所在的节点树 + * @param {object[]} container 结果容器 + */ + findRefsInNode(node, uuid, nodeTree, container) { + return __awaiter(this, void 0, void 0, function* () { + // 检查节点上的组件是否有引用 + const components = node.components; + if (components && components.length > 0) { + for (let i = 0, l = components.length; i < l; i++) { + const info = this.getContainsInfo(components[i], uuid); + if (!info.contains) + continue; + // 资源类型 + let type = components[i]['__type__']; + // 是否为脚本资源 + if (Editor.Utils.UUID.isUUID(type)) { + const scriptUuid = Editor.Utils.UUID.decompressUUID(type), assetInfo = yield Editor.Message.request('asset-db', 'query-asset-info', scriptUuid); + type = Path.basename(assetInfo.url); + } + // 处理属性名称 + let property = info.property; + if (property) { + // Label 组件需要特殊处理 + if (type === 'cc.Label' && property === '_N$file') { + property = 'font'; + } + else { + // 去除属性名的前缀 + if (property.startsWith('_N$')) { + property = property.replace('_N$', ''); + } + else if (property[0] === '_') { + property = property.substring(1); + } + } + } + // 保存结果 + container.push({ + node: node.path, + component: type, + property: property + }); + } + } + // 检查预制体是否有引用 + const prefab = node.prefab; + if (prefab) { + // 排除预制体自己 + if (uuid !== nodeTree['__uuid__']) { + const contains = ObjectUtil_1.default.containsValue(prefab, uuid); + if (contains) { + container.push({ + node: node.path + }); + } + } + } + // 遍历子节点 + const children = node.children; + if (children && children.length > 0) { + for (let i = 0, l = children.length; i < l; i++) { + yield this.findRefsInNode(children[i], uuid, nodeTree, container); + } + } + }); + }, + /** + * 查找引用 + * @param {string} uuid + * @returns {object[]} + */ + findReferences(uuid) { + return __awaiter(this, void 0, void 0, function* () { + const results = []; + /** + * 文件处理函数 + * @param {string} filePath 文件路径 + * @param {Fs.Stats} stats + */ + const searchHandler = (filePath, stats) => __awaiter(this, void 0, void 0, function* () { + const extname = Path.extname(filePath); + // 场景和预制体资源 + if (extname === '.fire' || extname === '.prefab') { + // 将资源数据转为节点树 + const nodeTree = this.getNodeTree(filePath), children = nodeTree.children, refs = []; + // 遍历节点 + for (let i = 0, l = children.length; i < l; i++) { + yield this.findRefsInNode(children[i], uuid, nodeTree, refs); + } + // 保存当前文件引用结果 + if (refs.length > 0) { + results.push({ + type: typeMap[extname], + fileUrl: yield Editor.Message.request('asset-db', 'query-url', filePath), //Editor.assetdb.fspathToUrl(), + refs: refs + }); + } + } + // 动画资源 + else if (extname === '.anim') { + const data = JSON.parse(Fs.readFileSync(filePath)), curveData = data['curveData'], contains = ObjectUtil_1.default.containsValue(curveData, uuid); + if (contains) { + results.push({ + type: typeMap[extname], + fileUrl: yield Editor.Message.request('asset-db', 'query-url', filePath) + }); + } + } + // 材质和字体资源 + else if (extname === '.mtl' || filePath.indexOf('.fnt.meta') !== -1) { + const data = JSON.parse(Fs.readFileSync(filePath)), contains = ObjectUtil_1.default.containsValue(data, uuid); + if (contains && !(data['uuid'] === uuid)) { + const _extname = (extname === '.mtl') ? '.mtl' : '.fnt.meta'; + results.push({ + type: typeMap[_extname], + fileUrl: yield Editor.Message.request('asset-db', 'query-url', filePath) + }); + } + } + }); + // 遍历资源目录下的文件 + const assetsPath = yield Editor.Message.request('asset-db', 'query-path', 'db://assets/'); + yield FileUtil_1.default.map(assetsPath, searchHandler); + // Done + return results; + }); + }, + /** + * 获取节点树 + * @param {string} filePath 文件路径 + * @returns {object} + */ + getNodeTree(filePath) { + // 获取缓存 + let cache = this.cache; + if (!cache) { + cache = this.cache = Object.create(null); + } + // 从缓存中读取 + if (!cache[filePath]) { + // 将资源数据转为节点树 + const data = JSON.parse(Fs.readFileSync(filePath)); + cache[filePath] = this.convertToNodeTree(data); + } + // Done + return cache[filePath]; + }, + /** + * 读取节点 + * @param {object} source 元数据 + * @param {number} id 节点 ID + * @param {object} parent 父容器 + */ + convertNode(source, id, parent) { + const node = Object.create(null), sourceData = source[id]; + // 基本信息 + node['__id__'] = id; + node['_name'] = sourceData['_name']; + node['__type__'] = sourceData['__type__']; + // 路径 + const parentPath = parent.path || (parent['_name'] || null); + node.path = (parentPath ? `${parentPath}/` : '') + node['_name']; + // 组件 + const components = sourceData['_components']; + if (components && components.length > 0) { + const _components = node.components = []; + for (let i = 0, l = components.length; i < l; i++) { + const sourceComponent = source[components[i]['__id__']]; + _components.push(this.extractValidInfo(sourceComponent)); + } + } + // 预制体引用 + const prefab = sourceData['_prefab']; + if (prefab) { + const realPrefab = source[prefab['__id__']]; + node.prefab = this.extractValidInfo(realPrefab); + } + // 子节点 + const children = sourceData['_children']; + if (children && children.length > 0) { + node.children = []; + for (let i = 0, l = children.length; i < l; i++) { + const childId = children[i]['__id__']; + this.convertNode(source, childId, node); + } + } + // 保存数据 + parent.children.push(node); + }, + /** +* 提取有效信息(含有 uuid) +* @param {object} data 元数据 +* @returns {{ __type__: string, _name: string, fileId?: string }} +*/ + extractValidInfo(data) { + const info = Object.create(null); + // 记录有用的属性 + const keys = ['__type__', '_name', 'fileId']; + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + if (data[key]) { + info[key] = data[key]; + } + } + // 记录包含 uuid 的属性 + for (const key in data) { + if (ObjectUtil_1.default.containsProperty(data[key], '__uuid__')) { + info[key] = data[key]; + } + } + // Done + return info; + }, + /** + * 将资源数据转为节点树 + * @param {object} source 元数据 + * @returns {object} + */ + convertToNodeTree(source) { + const nodeTree = Object.create(null), type = source[0]['__type__']; + // 场景资源 + if (type === 'cc.SceneAsset') { + const sceneId = source[0]['scene']['__id__'], children = source[sceneId]['_children']; + nodeTree['__type__'] = 'cc.Scene'; // 类型 + nodeTree['__id__'] = sceneId; // ID + // 遍历节点 + nodeTree.children = []; + for (let i = 0, l = children.length; i < l; i++) { + const nodeId = children[i]['__id__']; + this.convertNode(source, nodeId, nodeTree); + } + } + // 预制体资源 + else if (type === 'cc.Prefab') { + const uuid = source[source.length - 1]['asset']['__uuid__']; + nodeTree['__type__'] = 'cc.Prefab'; // 类型 + nodeTree['__uuid__'] = uuid; // uuid + // 从根节点开始读取 + nodeTree.children = []; + const rootId = source[0]['data']['__id__']; + this.convertNode(source, rootId, nodeTree); + } + // Done + return nodeTree; + }, + printResult(results) { + if (results.length === 0) { + console.log(`[${EXTENSION_NAME}]`, 'noRefs'); + console.log(`${'----'.repeat(36)}`); + return; + } + // 添加引用 + const nodeRefs = []; + let nodeRefsCount = 0; + const assetRefs = []; + let assetRefsCount = 0; + for (let i = 0, l = results.length; i < l; i++) { + const result = results[i], type = result.type, url = result.fileUrl.replace('db://', '').replace('.meta', ''); + if (type === 'scene' || type === 'prefab') { + nodeRefs.push(`   · ${iconMap[type]} [${translate(type)}] ${url}`); + const refs = result.refs; + for (let j = 0, n = refs.length; j < n; j++) { + nodeRefsCount++; + { + const ref = refs[j]; + let item = `       ${iconMap['node']} [${translate('node')}] ${ref.node}`; + if (ref.component) { + item += `  →  ${iconMap['component']} [${translate('component')}] ${ref.component}`; + } + if (ref.property) { + item += `  →  ${iconMap['property']} [${translate('property')}] ${ref.property}`; + } + nodeRefs.push(item); + } + } + } + else { + assetRefsCount++; + assetRefs.push(`   · ${iconMap['asset']} [${translate(type)}] ${url}`); + } + } + // 合并 + const texts = [`[${EXTENSION_NAME}] ${translate('searchResult')} >>>`]; + if (nodeRefs.length > 0) { + nodeRefs.unshift(`   📙 ${translate('引用数')} x ${nodeRefsCount}`); + texts.push(...nodeRefs); + } + if (assetRefs.length > 0) { + assetRefs.unshift(`   📘 ${translate('资源引用数')} x ${assetRefsCount}`); + texts.push(...assetRefs); + } + // 分隔 + texts.push(`${'----'.repeat(36)}`); + // 打印到控制台 + if (this.expand) { + // 逐行打印 + for (let i = 0, l = texts.length; i < l; i++) { + console.log(texts[i]); + } + } + else { + // 单行打印 + const content = texts.join('\n'); + console.log(content); + } + } +}; +/** + * @en Method Triggered on Extension Startup + * @zh 扩展启动时触发的方法 + */ +function load() { } +/** + * @en Method triggered when uninstalling the extension + * @zh 卸载扩展时触发的方法 + */ +function unload() { } diff --git a/dist/panels/default/index.js b/dist/panels/default/index.js new file mode 100644 index 0000000..51b80b1 --- /dev/null +++ b/dist/panels/default/index.js @@ -0,0 +1 @@ +"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const fs_extra_1=require("fs-extra"),path_1=require("path"),vue_1=require("vue"),panelDataMap=new WeakMap;module.exports=Editor.Panel.define({listeners:{show(){console.log("show")},hide(){console.log("hide")}},template:(0,fs_extra_1.readFileSync)((0,path_1.join)(__dirname,"../../../static/template/default/index.html"),"utf-8"),style:(0,fs_extra_1.readFileSync)((0,path_1.join)(__dirname,"../../../static/style/default/index.css"),"utf-8"),$:{app:"#app",text:"#text"},methods:{hello(){this.$.text&&(this.$.text.innerHTML="hello",console.log("[cocos-panel-html.default]: hello"))}},ready(){var e;this.$.text&&(this.$.text.innerHTML="Hello Cocos."),this.$.app&&((e=(0,vue_1.createApp)({})).config.compilerOptions.isCustomElement=e=>e.startsWith("ui-"),e.component("MyCounter",{template:(0,fs_extra_1.readFileSync)((0,path_1.join)(__dirname,"../../../static/template/vue/counter.html"),"utf-8"),data(){return{counter:0}},methods:{addition(){this.counter+=1},subtraction(){--this.counter}}}),e.mount(this.$.app),panelDataMap.set(this,e))},beforeClose(){},close(){var e=panelDataMap.get(this);e&&e.unmount()}}); \ No newline at end of file diff --git a/dist/util/FileUtil.js b/dist/util/FileUtil.js new file mode 100644 index 0000000..db8e476 --- /dev/null +++ b/dist/util/FileUtil.js @@ -0,0 +1,80 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const Fs = require('fs'); +const Path = require('path'); +/** + * 文件工具 + */ +class FileUtil { + /** + * 复制文件/文件夹 + * @param {Fs.PathLike} srcPath 源路径 + * @param {Fs.PathLike} destPath 目标路径 + */ + static copy(srcPath, destPath) { + if (!Fs.existsSync(srcPath)) + return; + const stats = Fs.statSync(srcPath); + if (stats.isDirectory()) { + if (!Fs.existsSync(destPath)) + Fs.mkdirSync(destPath); + const names = Fs.readdirSync(srcPath); + for (const name of names) { + this.copy(Path.join(srcPath, name), Path.join(destPath, name)); + } + } + else if (stats.isFile()) { + Fs.writeFileSync(destPath, Fs.readFileSync(srcPath)); + } + } + /** + * 删除文件/文件夹 + * @param {Fs.PathLike} path 路径 + */ + static delete(path) { + if (!Fs.existsSync(path)) + return; + const stats = Fs.statSync(path); + if (stats.isDirectory()) { + const names = Fs.readdirSync(path); + for (const name of names) { + this.delete(Path.join(path, name)); + } + Fs.rmdirSync(path); + } + else if (stats.isFile()) { + Fs.unlinkSync(path); + } + } + /** + * 遍历文件/文件夹并执行函数 + * @param {Fs.PathLike} path 路径 + * @param {(filePath: Fs.PathLike, stat: Fs.Stats) => void} handler 处理函数 + */ + static map(path, handler) { + return __awaiter(this, void 0, void 0, function* () { + if (!Fs.existsSync(path)) + return; + const stats = Fs.statSync(path); + if (stats.isDirectory()) { + const names = Fs.readdirSync(path); + for (const name of names) { + yield this.map(Path.join(path, name), handler); + } + } + else if (stats.isFile()) { + yield handler(path, stats); + } + }); + } +} +exports.default = FileUtil; diff --git a/dist/util/ObjectUtil.js b/dist/util/ObjectUtil.js new file mode 100644 index 0000000..ac2f925 --- /dev/null +++ b/dist/util/ObjectUtil.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * 对象工具 + */ +class ObjectUtil { + /** + * 判断指定值是否是一个对象 + * @param {any} arg 参数 + */ + static isObject(arg) { + return Object.prototype.toString.call(arg) === '[object Object]'; + } + /** + * 对象中是否包含指定的属性 + * @param {object} object 对象 + * @param {any} name 属性名 + */ + static containsProperty(object, name) { + let result = false; + const search = (_object) => { + if (this.isObject(_object)) { + for (const key in _object) { + if (key == name) { + result = true; + return; + } + search(_object[key]); + } + } + else if (Array.isArray(_object)) { + for (let i = 0; i < _object.length; i++) { + search(_object[i]); + } + } + }; + search(object); + return result; + } + /** + * 对象中是否包含指定的值 + * @param {object} object 对象 + * @param {any} value 值 + */ + static containsValue(object, value) { + let result = false; + const search = (_object) => { + if (this.isObject(_object)) { + for (const key in _object) { + if (_object[key] === value) { + result = true; + return; + } + search(_object[key]); + } + } + else if (Array.isArray(_object)) { + for (let i = 0; i < _object.length; i++) { + search(_object[i]); + } + } + }; + search(object); + return result; + } +} +exports.default = ObjectUtil; diff --git a/i18n/en.js b/i18n/en.js new file mode 100644 index 0000000..1b1ffa7 --- /dev/null +++ b/i18n/en.js @@ -0,0 +1 @@ +"use strict";module.exports={open_panel:"Default Panel",send_to_panel:"Send message to Default Panel",description:"Extension with a panel based on Vue3.x"}; \ No newline at end of file diff --git a/i18n/zh.js b/i18n/zh.js new file mode 100644 index 0000000..ead340a --- /dev/null +++ b/i18n/zh.js @@ -0,0 +1 @@ +"use strict";module.exports={open_panel:"默认面板",send_to_panel:"发送消息给面板",description:"含有一个基于Vue3.x开发的面板的扩展"}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..83b92b6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,277 @@ +{ + "name": "references-finder", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "references-finder", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "fs-extra": "^10.0.0", + "vue": "^3.1.4" + }, + "devDependencies": { + "@cocos/creator-types": "^3.8.4", + "@types/fs-extra": "^9.0.5", + "@types/node": "^18.17.1" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@cocos/creator-types": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@cocos/creator-types/-/creator-types-3.8.4.tgz", + "integrity": "sha512-z+8qx726Zl/8rUUpbLjVAiNPn4XweRACEaY1vHeR3EHIcSQ9wtFlIFd2SZX5rOE8lt1S95nxv8OeLYXfbN4/2Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.61", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.61.tgz", + "integrity": "sha512-z8fH66NcVkDzBItOao+Nyh0fiy7CYdxIyxnNCcZ60aY0I+EA/y4TSi/S/W9i8DIQvwVo7a0pgzAxmDeNnqrpkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + }, + "peerDependencies": { + "vue": "3.3.4" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.4", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.2", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/magic-string": { + "version": "0.30.3", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.30", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vue": { + "version": "3.3.4", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e54cab9 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "$schema": "./@types/schema/package/index.json", + "package_version": 2, + "name": "references-finder", + "version": "1.0.0", + "author": "abc126500", + "editor": ">=3.8.4", + "scripts": { + "preinstall": "node ./scripts/preinstall.js", + "build": "tsc" + }, + "description": "i18n:references-finder.description", + "main": "./dist/main.js", + "dependencies": { + "vue": "^3.1.4", + "fs-extra": "^10.0.0" + }, + "devDependencies": { + "@cocos/creator-types": "^3.8.4", + "@types/fs-extra": "^9.0.5", + "@types/node": "^18.17.1" + }, + "contributions": { + "messages": { + "findSelection": { + "public": true, + "description": "find the selected Referrend", + "methods": [ + "findCurrentSelection" + ] + } + }, + "shortcuts": [ + { + "message": "findSelection", + "win": "F6", + "mac": "F6" + } + ] + } +} diff --git a/scripts/preinstall.js b/scripts/preinstall.js new file mode 100644 index 0000000..3876bba --- /dev/null +++ b/scripts/preinstall.js @@ -0,0 +1 @@ +const readFileSync=require("fs")["readFileSync"],join=require("path")["join"],spawnSync=require("child_process")["spawnSync"],PATH={packageJSON:join(__dirname,"../package.json")};function checkCreatorTypesVersion(e){var o="win32"===process.platform?"npm.cmd":"npm";let n=spawnSync(o,["view","@cocos/creator-types","versions"]).stdout.toString();try{n=JSON.parse(listString)}catch(e){}return!!n.includes(e)}try{const e=readFileSync(PATH.packageJSON,"utf8"),f=JSON.parse(e),g=f.devDependencies["@cocos/creator-types"].replace(/^[^\d]+/,"");checkCreatorTypesVersion(g)||(console.log("Warning:"),console.log(" @en"),console.log(" Version check of @cocos/creator-types failed."),console.log(` The definition of ${g} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`),console.log(" The definition of the corresponding version will be released on npm after the editor is officially released."),console.log(" @zh"),console.log(" @cocos/creator-types 版本检查失败。"),console.log(` ${g} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`),console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。"))}catch(e){console.error(e)} \ No newline at end of file diff --git a/source/main.ts b/source/main.ts new file mode 100644 index 0000000..d401fb2 --- /dev/null +++ b/source/main.ts @@ -0,0 +1,455 @@ + +import ObjectUtil from './util/ObjectUtil'; +import FileUtil from './util/FileUtil'; +import PJSON from '../package.json'; + +const Fs = require('fs'); +const Path = require('path'); +const EXTENSION_NAME = '🔎'; +/** 扩展名对应文件类型 */ +const typeMap = { + '.fire': 'scene', + '.prefab': 'prefab', + '.anim': 'animation', + '.mtl': 'material', + '.fnt.meta': 'font', +} + +/** 类型对应图标 */ +const iconMap = { + 'scene': '📺', + 'prefab': '🧸', + 'node': '💾', + 'component': '💿', + 'property': '🎲', + 'asset': '📦', +} +const translate = (key) => `H.${key}`; +/** + * @en Registration method for the main process of Extension + * @zh 为扩展的主进程的注册方法 + */ +export const methods: { [key: string]: (...any: any) => any } = { + openDebug() { + console.log('openDebug') + Editor.Panel.open(PJSON.name); + }, + /** + * @en A method that can be triggered by message + * @zh 通过 message 触发的方法 + */ + async findCurrentSelection() { + const uuids = Editor.Selection.getSelected('asset'); + if (uuids.length === 0) { + console.log(`[${EXTENSION_NAME}]`, '先选择资源'); + return; + } + // 根据 uuid 查找 + for (let i = 0; i < uuids.length; i++) { + await this.module.methods.findViaUuid(uuids[i]); + } + }, + /** + * 使用 uuid 进行查找 + * @param {string} uuid + */ + async findViaUuid(uuid) { + // 是否为有效 uuid + if (!Editor.Utils.UUID.isUUID(uuid)) { + console.log(`[${EXTENSION_NAME}]`, 'invalidUuid' + uuid); + return; + } + // 获取资源信息 + const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', uuid); + if (assetInfo) { + const url = assetInfo.url.replace('db://', ''); + // 暂不查找文件夹 + if (assetInfo.type === 'folder') { + console.log(`[${EXTENSION_NAME}]`, 'notSupportFolder' + url); + return; + } + // 处理文件路径 & 打印头部日志 + const urlItems = url.split('/'); + if (!urlItems[urlItems.length - 1].includes('.')) { + urlItems.splice(urlItems.length - 1); + } + console.log(`[${EXTENSION_NAME}]`, 'findAssetRefs' + urlItems.join('/')); + // 记录子资源 uuid + const subUuids = assetInfo ? [] : null; + // 资源类型检查 + if (assetInfo.type === "cc.ImageAsset") { + // 纹理子资源 + uuid = uuid+'@f9941'; + } else if (assetInfo.type === 'cc.Script') { + // 脚本 + uuid = Editor.Utils.UUID.compressUUID(uuid, true); + } + // 查找 + const results = uuid ? await this.findReferences(uuid) : []; + // Done + this.printResult(results); + } else { + // 不存在的资源,直接查找 uuid + console.log(`[${EXTENSION_NAME}]`, 'findAssetRefs' + uuid); + const result = await this.findReferences(uuid); + // Done + this.printResult(result); + } + }, + /** + * 获取对象中是否包含指定值以及相应属性名 + * @param {object} object 对象 + * @param {any} value 值 + * @returns {{ contains: boolean, property: string }} + */ + getContainsInfo(object, value) { + const result = { + contains: false, + property: null + }; + // 搜索 + const search = (target, parentKey) => { + if (ObjectUtil.isObject(target)) { + for (const key in target) { + if (target[key] === value) { + result.contains = true; + result.property = parentKey; + return; + } + search(target[key], key); + } + } else if (Array.isArray(target)) { + for (let i = 0, l = target.length; i < l; i++) { + search(target[i], parentKey); + } + } + } + search(object, null); + // Done + return result; + }, + /** + * 查找节点中的引用 + * @param {object} node 目标节点 + * @param {string} uuid 查找的 uuid + * @param {object} nodeTree 节点所在的节点树 + * @param {object[]} container 结果容器 + */ + async findRefsInNode(node, uuid, nodeTree, container) { + // 检查节点上的组件是否有引用 + const components = node.components; + if (components && components.length > 0) { + for (let i = 0, l = components.length; i < l; i++) { + const info = this.getContainsInfo(components[i], uuid); + if (!info.contains) continue; + // 资源类型 + let type = components[i]['__type__']; + // 是否为脚本资源 + if (Editor.Utils.UUID.isUUID(type)) { + const scriptUuid = Editor.Utils.UUID.decompressUUID(type), + assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', scriptUuid); + type = Path.basename(assetInfo.url); + } + // 处理属性名称 + let property = info.property; + if (property) { + // Label 组件需要特殊处理 + if (type === 'cc.Label' && property === '_N$file') { + property = 'font'; + } else { + // 去除属性名的前缀 + if (property.startsWith('_N$')) { + property = property.replace('_N$', ''); + } else if (property[0] === '_') { + property = property.substring(1); + } + } + } + // 保存结果 + container.push({ + node: node.path, + component: type, + property: property + }); + } + } + // 检查预制体是否有引用 + const prefab = node.prefab; + if (prefab) { + // 排除预制体自己 + if (uuid !== nodeTree['__uuid__']) { + const contains = ObjectUtil.containsValue(prefab, uuid); + if (contains) { + container.push({ + node: node.path + }); + } + } + } + // 遍历子节点 + const children = node.children; + if (children && children.length > 0) { + for (let i = 0, l = children.length; i < l; i++) { + await this.findRefsInNode(children[i], uuid, nodeTree, container); + } + } + }, + /** + * 查找引用 + * @param {string} uuid + * @returns {object[]} + */ + async findReferences(uuid) { + const results = []; + /** + * 文件处理函数 + * @param {string} filePath 文件路径 + * @param {Fs.Stats} stats + */ + const searchHandler = async (filePath, stats) => { + const extname = Path.extname(filePath); + // 场景和预制体资源 + if (extname === '.fire' || extname === '.prefab') { + // 将资源数据转为节点树 + const nodeTree = this.getNodeTree(filePath), + children = nodeTree.children, + refs = []; + // 遍历节点 + for (let i = 0, l = children.length; i < l; i++) { + await this.findRefsInNode(children[i], uuid, nodeTree, refs); + } + // 保存当前文件引用结果 + if (refs.length > 0) { + results.push({ + type: typeMap[extname], + fileUrl: await Editor.Message.request('asset-db', 'query-url', filePath), //Editor.assetdb.fspathToUrl(), + refs: refs + }); + } + } + // 动画资源 + else if (extname === '.anim') { + const data = JSON.parse(Fs.readFileSync(filePath)), + curveData = data['curveData'], + contains = ObjectUtil.containsValue(curveData, uuid); + if (contains) { + results.push({ + type: typeMap[extname], + fileUrl: await Editor.Message.request('asset-db', 'query-url', filePath) + }); + } + } + // 材质和字体资源 + else if (extname === '.mtl' || filePath.indexOf('.fnt.meta') !== -1) { + const data = JSON.parse(Fs.readFileSync(filePath)), + contains = ObjectUtil.containsValue(data, uuid); + if (contains && !(data['uuid'] === uuid)) { + const _extname = (extname === '.mtl') ? '.mtl' : '.fnt.meta'; + results.push({ + type: typeMap[_extname], + fileUrl: await Editor.Message.request('asset-db', 'query-url', filePath) + }); + } + } + }; + // 遍历资源目录下的文件 + const assetsPath = await Editor.Message.request('asset-db', 'query-path', 'db://assets/'); + await FileUtil.map(assetsPath, searchHandler); + // Done + return results; + }, + /** + * 获取节点树 + * @param {string} filePath 文件路径 + * @returns {object} + */ + getNodeTree(filePath) { + // 获取缓存 + let cache = this.cache; + if (!cache) { + cache = this.cache = Object.create(null); + } + // 从缓存中读取 + if (!cache[filePath]) { + // 将资源数据转为节点树 + const data = JSON.parse(Fs.readFileSync(filePath)); + cache[filePath] = this.convertToNodeTree(data); + } + // Done + return cache[filePath]; + }, + /** + * 读取节点 + * @param {object} source 元数据 + * @param {number} id 节点 ID + * @param {object} parent 父容器 + */ + convertNode(source, id, parent) { + const node = Object.create(null), + sourceData = source[id]; + // 基本信息 + node['__id__'] = id; + node['_name'] = sourceData['_name']; + node['__type__'] = sourceData['__type__']; + // 路径 + const parentPath = parent.path || (parent['_name'] || null); + node.path = (parentPath ? `${parentPath}/` : '') + node['_name']; + // 组件 + const components = sourceData['_components']; + if (components && components.length > 0) { + const _components = node.components = []; + for (let i = 0, l = components.length; i < l; i++) { + const sourceComponent = source[components[i]['__id__']]; + _components.push(this.extractValidInfo(sourceComponent)); + } + } + // 预制体引用 + const prefab = sourceData['_prefab']; + if (prefab) { + const realPrefab = source[prefab['__id__']]; + node.prefab = this.extractValidInfo(realPrefab); + } + // 子节点 + const children = sourceData['_children']; + if (children && children.length > 0) { + node.children = []; + for (let i = 0, l = children.length; i < l; i++) { + const childId = children[i]['__id__']; + this.convertNode(source, childId, node); + } + } + // 保存数据 + parent.children.push(node); + }, + /** + * 提取有效信息(含有 uuid) + * @param {object} data 元数据 + * @returns {{ __type__: string, _name: string, fileId?: string }} + */ + extractValidInfo(data) { + const info = Object.create(null); + // 记录有用的属性 + const keys = ['__type__', '_name', 'fileId']; + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + if (data[key]) { + info[key] = data[key]; + } + } + // 记录包含 uuid 的属性 + for (const key in data) { + if (ObjectUtil.containsProperty(data[key], '__uuid__')) { + info[key] = data[key]; + } + } + // Done + return info; + }, + /** + * 将资源数据转为节点树 + * @param {object} source 元数据 + * @returns {object} + */ + convertToNodeTree(source) { + const nodeTree = Object.create(null), + type = source[0]['__type__']; + // 场景资源 + if (type === 'cc.SceneAsset') { + const sceneId = source[0]['scene']['__id__'], + children = source[sceneId]['_children']; + nodeTree['__type__'] = 'cc.Scene'; // 类型 + nodeTree['__id__'] = sceneId; // ID + // 遍历节点 + nodeTree.children = []; + for (let i = 0, l = children.length; i < l; i++) { + const nodeId = children[i]['__id__']; + this.convertNode(source, nodeId, nodeTree); + } + } + // 预制体资源 + else if (type === 'cc.Prefab') { + const uuid = source[source.length - 1]['asset']['__uuid__']; + nodeTree['__type__'] = 'cc.Prefab'; // 类型 + nodeTree['__uuid__'] = uuid; // uuid + // 从根节点开始读取 + nodeTree.children = []; + const rootId = source[0]['data']['__id__']; + this.convertNode(source, rootId, nodeTree); + } + // Done + return nodeTree; + }, + printResult(results) { + if (results.length === 0) { + console.log(`[${EXTENSION_NAME}]`, 'noRefs'); + console.log(`${'----'.repeat(36)}`); + return; + } + // 添加引用 + const nodeRefs = []; + let nodeRefsCount = 0; + const assetRefs = []; + let assetRefsCount = 0; + for (let i = 0, l = results.length; i < l; i++) { + const result = results[i], + type = result.type, + url = result.fileUrl.replace('db://', '').replace('.meta', ''); + if (type === 'scene' || type === 'prefab') { + nodeRefs.push(`   · ${iconMap[type]} [${translate(type)}] ${url}`); + const refs = result.refs; + for (let j = 0, n = refs.length; j < n; j++) { + nodeRefsCount++; + { + const ref = refs[j]; + let item = `       ${iconMap['node']} [${translate('node')}] ${ref.node}`; + if (ref.component) { + item += `  →  ${iconMap['component']} [${translate('component')}] ${ref.component}`; + } + if (ref.property) { + item += `  →  ${iconMap['property']} [${translate('property')}] ${ref.property}`; + } + nodeRefs.push(item); + } + } + } else { + assetRefsCount++; + assetRefs.push(`   · ${iconMap['asset']} [${translate(type)}] ${url}`); + } + } + // 合并 + const texts = [`[${EXTENSION_NAME}] ${translate('searchResult')} >>>`]; + if (nodeRefs.length > 0) { + nodeRefs.unshift(`   📙 ${translate('引用数')} x ${nodeRefsCount}`); + texts.push(...nodeRefs); + } + if (assetRefs.length > 0) { + assetRefs.unshift(`   📘 ${translate('资源引用数')} x ${assetRefsCount}`); + texts.push(...assetRefs); + } + // 分隔 + texts.push(`${'----'.repeat(36)}`); + // 打印到控制台 + if (this.expand) { + // 逐行打印 + for (let i = 0, l = texts.length; i < l; i++) { + console.log(texts[i]); + } + } else { + // 单行打印 + const content = texts.join('\n'); + console.log(content); + } + } +}; + +/** + * @en Method Triggered on Extension Startup + * @zh 扩展启动时触发的方法 + */ +export function load() { } + +/** + * @en Method triggered when uninstalling the extension + * @zh 卸载扩展时触发的方法 + */ +export function unload() { } diff --git a/source/util/FileUtil.ts b/source/util/FileUtil.ts new file mode 100644 index 0000000..6a44841 --- /dev/null +++ b/source/util/FileUtil.ts @@ -0,0 +1,64 @@ +const Fs = require('fs'); +const Path = require('path'); + +/** + * 文件工具 + */ +export default class FileUtil { + + /** + * 复制文件/文件夹 + * @param {Fs.PathLike} srcPath 源路径 + * @param {Fs.PathLike} destPath 目标路径 + */ + static copy(srcPath, destPath) { + if (!Fs.existsSync(srcPath)) return; + const stats = Fs.statSync(srcPath); + if (stats.isDirectory()) { + if (!Fs.existsSync(destPath)) Fs.mkdirSync(destPath); + const names = Fs.readdirSync(srcPath); + for (const name of names) { + this.copy(Path.join(srcPath, name), Path.join(destPath, name)); + } + } else if (stats.isFile()) { + Fs.writeFileSync(destPath, Fs.readFileSync(srcPath)); + } + } + + /** + * 删除文件/文件夹 + * @param {Fs.PathLike} path 路径 + */ + static delete(path) { + if (!Fs.existsSync(path)) return; + const stats = Fs.statSync(path); + if (stats.isDirectory()) { + const names = Fs.readdirSync(path); + for (const name of names) { + this.delete(Path.join(path, name)); + } + Fs.rmdirSync(path); + } else if (stats.isFile()) { + Fs.unlinkSync(path); + } + } + + /** + * 遍历文件/文件夹并执行函数 + * @param {Fs.PathLike} path 路径 + * @param {(filePath: Fs.PathLike, stat: Fs.Stats) => void} handler 处理函数 + */ + static async map(path:string, handler: (string,any)=>Promise) { + if (!Fs.existsSync(path)) return + const stats = Fs.statSync(path); + if (stats.isDirectory()) { + const names = Fs.readdirSync(path); + for (const name of names) { + await this.map(Path.join(path, name), handler); + } + } else if (stats.isFile()) { + await handler(path, stats); + } + } + +} diff --git a/source/util/ObjectUtil.ts b/source/util/ObjectUtil.ts new file mode 100644 index 0000000..d828aa6 --- /dev/null +++ b/source/util/ObjectUtil.ts @@ -0,0 +1,66 @@ +/** + * 对象工具 + */ +export default class ObjectUtil { + + /** + * 判断指定值是否是一个对象 + * @param {any} arg 参数 + */ + static isObject(arg) { + return Object.prototype.toString.call(arg) === '[object Object]'; + } + + /** + * 对象中是否包含指定的属性 + * @param {object} object 对象 + * @param {any} name 属性名 + */ + static containsProperty(object, name) { + let result = false; + const search = (_object) => { + if (this.isObject(_object)) { + for (const key in _object) { + if (key == name) { + result = true; + return; + } + search(_object[key]); + } + } else if (Array.isArray(_object)) { + for (let i = 0; i < _object.length; i++) { + search(_object[i]); + } + } + } + search(object); + return result; + } + + /** + * 对象中是否包含指定的值 + * @param {object} object 对象 + * @param {any} value 值 + */ + static containsValue(object, value) { + let result = false; + const search = (_object) => { + if (this.isObject(_object)) { + for (const key in _object) { + if (_object[key] === value) { + result = true; + return; + } + search(_object[key]); + } + } else if (Array.isArray(_object)) { + for (let i = 0; i < _object.length; i++) { + search(_object[i]); + } + } + } + search(object); + return result; + } + +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6898cc3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "CommonJS", + "inlineSourceMap": false, + "outDir": "./dist", + "rootDir": "./source", + "allowJs": true, + "lib": [ + "dom", + "es7", + "ES2017" + ], + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "types": [ + "@cocos/creator-types/editor/protected", + "@cocos/creator-types/editor/packages/builder/@types/public", + "@cocos/creator-types/editor/packages/builder/@types/public/global" + ] + }, + "exclude": [ + "./dist" + ], + "include": [ + "./source" + ] +} \ No newline at end of file