From f48ebb65ba088abc592044bddf430540c810d4da Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 18 Jun 2025 20:22:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8F=92=E4=BB=B6=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BB=A3=E7=A0=81=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../source/CodeGenerator.ts | 2 +- .../source/handlers/BehaviorTreeHandler.ts | 40 ++-- .../cocos-ecs-extension/source/main.ts | 18 +- .../composables/useBehaviorTreeEditor.ts | 106 +++++++++++ .../composables/useInstallation.ts | 20 +- .../behavior-tree/utils/installUtils.ts | 34 ++-- .../static/style/behavior-tree/base.css | 136 ++++++++++++++ .../static/style/behavior-tree/toolbar.css | 173 +++++++++++++++++- .../behavior-tree/BehaviorTreeEditor.html | 60 +++++- extensions/cocos/cocos-ecs/package-lock.json | 1 + 10 files changed, 521 insertions(+), 69 deletions(-) diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/CodeGenerator.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/CodeGenerator.ts index 68dec643..a5bf258f 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/CodeGenerator.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/CodeGenerator.ts @@ -94,7 +94,7 @@ ${comments} export class ${className} extends ${options.systemType} { constructor() { - super(${matcherSetup}${options.systemType === 'IntervalSystem' ? ', 1000 / 60 // 60fps' : ''}); + super(${matcherSetup}${options.systemType === 'IntervalSystem' ? ', 1000 / 60' : ''})${options.systemType === 'IntervalSystem' ? '; // 60fps' : ';'} } ${processMethod} diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/handlers/BehaviorTreeHandler.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/handlers/BehaviorTreeHandler.ts index aeb40d8e..9399be61 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/handlers/BehaviorTreeHandler.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/handlers/BehaviorTreeHandler.ts @@ -14,30 +14,21 @@ export class BehaviorTreeHandler { const projectPath = Editor.Project.path; const command = 'npm install @esengine/ai'; - console.log(`Installing Behavior Tree AI to project: ${projectPath}`); - return new Promise((resolve) => { exec(command, { cwd: projectPath }, (error, stdout, stderr) => { - console.log('Install stdout:', stdout); - if (stderr) console.log('Install stderr:', stderr); - if (error) { - console.error('Behavior Tree AI installation failed:', error); + console.error('AI系统安装失败:', error.message); resolve(false); } else { - console.log('Behavior Tree AI installation completed successfully'); - // 验证安装是否成功 const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai'); const installSuccess = fs.existsSync(nodeModulesPath); - if (installSuccess) { - console.log('Behavior Tree AI installed successfully'); - resolve(true); - } else { - console.warn('Behavior Tree AI directory not found after install'); - resolve(false); + if (!installSuccess) { + console.warn('安装完成但未找到AI系统目录,请检查网络连接'); } + + resolve(installSuccess); } }); }); @@ -50,30 +41,21 @@ export class BehaviorTreeHandler { const projectPath = Editor.Project.path; const command = 'npm update @esengine/ai'; - console.log(`Updating Behavior Tree AI in project: ${projectPath}`); - return new Promise((resolve) => { exec(command, { cwd: projectPath }, (error, stdout, stderr) => { - console.log('Update stdout:', stdout); - if (stderr) console.log('Update stderr:', stderr); - if (error) { - console.error('Behavior Tree AI update failed:', error); + console.error('AI系统更新失败:', error.message); resolve(false); } else { - console.log('Behavior Tree AI update completed successfully'); - // 验证更新是否成功 const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai'); const updateSuccess = fs.existsSync(nodeModulesPath); - if (updateSuccess) { - console.log('Behavior Tree AI updated successfully'); - resolve(true); - } else { - console.warn('Behavior Tree AI directory not found after update'); - resolve(false); + if (!updateSuccess) { + console.warn('更新完成但未找到AI系统目录'); } + + resolve(updateSuccess); } }); }); @@ -96,7 +78,7 @@ export class BehaviorTreeHandler { return '@esengine/ai' in dependencies; } catch (error) { - console.error('Error checking Behavior Tree AI installation:', error); + console.error('检查AI系统安装状态失败:', error); return false; } } diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts index 1a0fae07..4256b411 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/main.ts @@ -93,15 +93,25 @@ export const methods: { [key: string]: (...any: any) => any } = { /** * 安装行为树AI系统 */ - 'install-behavior-tree'() { - BehaviorTreeHandler.install(); + async 'install-behavior-tree'() { + try { + return await BehaviorTreeHandler.install(); + } catch (error) { + console.error('安装行为树AI系统失败:', error); + return false; + } }, /** * 更新行为树AI系统 */ - 'update-behavior-tree'() { - BehaviorTreeHandler.update(); + async 'update-behavior-tree'() { + try { + return await BehaviorTreeHandler.update(); + } catch (error) { + console.error('更新行为树AI系统失败:', error); + return false; + } }, /** diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts index a8ff4dec..36e010bb 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts @@ -347,9 +347,113 @@ export function useBehaviorTreeEditor() { } }; + // 复制到剪贴板 + const copyToClipboard = async () => { + try { + const code = computedProps.exportedCode(); + await navigator.clipboard.writeText(code); + + // 显示成功消息 + const toast = document.createElement('div'); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + background: #4caf50; + color: white; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + z-index: 10001; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + `; + toast.textContent = '已复制到剪贴板!'; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }, 10); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (document.body.contains(toast)) { + document.body.removeChild(toast); + } + }, 300); + }, 2000); + } catch (error) { + alert('复制到剪贴板失败: ' + error); + } + }; + + // 保存到文件 + const saveToFile = () => { + const code = computedProps.exportedCode(); + const format = appState.exportFormat.value; + const extension = format === 'json' ? '.json' : '.ts'; + const mimeType = format === 'json' ? 'application/json' : 'text/typescript'; + + // 创建文件并下载 + const blob = new Blob([code], { type: mimeType }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `behavior_tree_config${extension}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + URL.revokeObjectURL(url); + + // 显示成功消息 + const toast = document.createElement('div'); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + background: #4caf50; + color: white; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + z-index: 10001; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + `; + toast.textContent = `文件已保存: behavior_tree_config${extension}`; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }, 10); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (document.body.contains(toast)) { + document.body.removeChild(toast); + } + }, 300); + }, 3000); + }; + onMounted(() => { + // 自动检查安装状态 + installation.checkInstallStatus(); + const appContainer = document.querySelector('#behavior-tree-app'); if (appContainer) { (appContainer as any).loadFileContent = fileOps.loadFileContent; @@ -457,6 +561,8 @@ export function useBehaviorTreeEditor() { autoLayout, validateTree, clearAllConnections, + copyToClipboard, + saveToFile, // 节点选择相关 selectNode: (nodeId: string) => { // 选中普通节点时,取消条件节点的选中 diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useInstallation.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useInstallation.ts index c409b759..66c89089 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useInstallation.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useInstallation.ts @@ -19,6 +19,7 @@ export function useInstallation( isInstalled.value = result.installed; version.value = result.version; } catch (error) { + console.error('检查AI系统安装状态失败:', error); isInstalled.value = false; version.value = null; } finally { @@ -30,10 +31,23 @@ export function useInstallation( const handleInstall = async () => { isInstalling.value = true; try { - await installBehaviorTreeAI(Editor.Project.path); - await checkInstallStatus(); + const result = await installBehaviorTreeAI(Editor.Project.path); + + if (result) { + // 等待文件系统更新 + await new Promise(resolve => setTimeout(resolve, 2000)); + await checkInstallStatus(); + + // 如果第一次检查失败,再次尝试 + if (!isInstalled.value) { + await new Promise(resolve => setTimeout(resolve, 1000)); + await checkInstallStatus(); + } + } else { + console.error('AI系统安装失败'); + } } catch (error) { - // 安装失败时静默处理 + console.error('安装AI系统时发生错误:', error); } finally { isInstalling.value = false; } diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/installUtils.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/installUtils.ts index 0cbbb553..4cea1245 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/installUtils.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/installUtils.ts @@ -50,9 +50,9 @@ export function getInstallStatusText( isInstalled: boolean, version: string | null ): string { - if (isChecking) return '检查中...'; - if (isInstalling) return '安装中...'; - return isInstalled ? `✅ AI系统已安装 (v${version})` : '❌ AI系统未安装'; + if (isChecking) return '检查状态中...'; + if (isInstalling) return '正在安装AI系统...'; + return isInstalled ? 'AI系统已安装' : 'AI系统未安装'; } /** @@ -70,19 +70,13 @@ export function getInstallStatusClass( * 安装行为树AI系统 * 通过发送消息到主进程来执行真实的npm安装命令 */ -export async function installBehaviorTreeAI(projectPath: string): Promise { +export async function installBehaviorTreeAI(projectPath: string): Promise { try { - // 通过Editor.Message发送安装消息到主进程 - // 主进程会执行实际的npm install @esengine/ai命令 const result = await Editor.Message.request('cocos-ecs-extension', 'install-behavior-tree'); - - if (!result) { - throw new Error('安装请求失败,未收到主进程响应'); - } - - // 安装完成 + return Boolean(result); } catch (error) { - throw error; + console.error('请求安装AI系统失败:', error); + return false; } } @@ -90,18 +84,12 @@ export async function installBehaviorTreeAI(projectPath: string): Promise * 更新行为树AI系统 * 通过发送消息到主进程来执行真实的npm更新命令 */ -export async function updateBehaviorTreeAI(projectPath: string): Promise { +export async function updateBehaviorTreeAI(projectPath: string): Promise { try { - // 通过Editor.Message发送更新消息到主进程 - // 主进程会执行实际的npm update @esengine/ai命令 const result = await Editor.Message.request('cocos-ecs-extension', 'update-behavior-tree'); - - if (!result) { - throw new Error('更新请求失败,未收到主进程响应'); - } - - // 更新完成 + return Boolean(result); } catch (error) { - throw error; + console.error('请求更新AI系统失败:', error); + return false; } } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css index bca194be..c54affc4 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css @@ -72,4 +72,140 @@ flex: 1; min-height: 400px; } +} + +/* 编辑器容器禁用状态 */ +.editor-container.disabled { + position: relative; + pointer-events: none; + opacity: 0.3; + filter: blur(1px); +} + +/* 安装遮罩层 */ +.install-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(26, 32, 44, 0.95); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + pointer-events: all; +} + +.overlay-content { + text-align: center; + padding: 40px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + backdrop-filter: blur(20px); + max-width: 400px; + width: 90%; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.overlay-icon { + font-size: 60px; + margin-bottom: 20px; + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +.overlay-content h3 { + font-size: 24px; + font-weight: 600; + margin-bottom: 12px; + color: #ffffff; +} + +.overlay-content p { + font-size: 16px; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 30px; + line-height: 1.5; +} + +.overlay-actions { + margin-bottom: 20px; +} + +.overlay-install-btn { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 14px 28px; + background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%); + border: none; + border-radius: 12px; + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + min-width: 200px; +} + +.overlay-install-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4); +} + +.overlay-install-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); +} + +.overlay-install-btn.installing { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + animation: installing-glow 2s infinite; +} + +@keyframes installing-glow { + 0%, 100% { + box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); + } + 50% { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.8); + } +} + +.loading-spinner { + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top: 2px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-left: 8px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.overlay-note { + padding: 12px; + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 8px; + color: rgba(255, 255, 255, 0.7); +} + +.overlay-note small { + font-size: 14px; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css index 9c0fae24..94ced083 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css @@ -59,11 +59,13 @@ font-size: 12px; } -.tool-btn:hover { +.tool-btn:hover:not(:disabled) { background: rgba(255,255,255,0.2); transform: translateY(-1px); } + + .tool-btn.has-changes { background: rgba(255, 107, 107, 0.2); border-color: #ff6b6b; @@ -79,11 +81,176 @@ } } -.toolbar-right .install-status { +/* 安装状态容器 */ +.install-status-container { + position: relative; +} + +.install-status { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 12px; + min-width: 280px; + backdrop-filter: blur(10px); + transition: all 0.3s ease; +} + +.install-status:hover { + background: rgba(255,255,255,0.15); + border-color: rgba(255,255,255,0.3); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +/* 状态信息 */ +.status-info { display: flex; align-items: center; gap: 10px; + flex: 1; +} + +.status-icon { + font-size: 18px; + line-height: 1; +} + +.status-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.main-text { + font-size: 13px; + font-weight: 500; + color: white; +} + +.version-text { + font-size: 11px; + color: rgba(255,255,255,0.7); +} + +/* 安装按钮 */ +.install-btn { + display: flex; + align-items: center; + gap: 6px; padding: 8px 16px; - background: rgba(255,255,255,0.1); + background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%); + border: none; border-radius: 8px; + color: white; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.install-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4); +} + +.install-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); +} + +.install-btn.installing { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); +} + +.btn-icon { + font-size: 14px; + line-height: 1; +} + +.btn-text { + font-size: 12px; +} + +/* 加载点动画 */ +.loading-dots { + display: flex; + gap: 2px; + margin-left: 4px; +} + +.loading-dots span { + width: 3px; + height: 3px; + background: white; + border-radius: 50%; + animation: loading-dots 1.4s infinite ease-in-out; +} + +.loading-dots span:nth-child(1) { animation-delay: -0.32s; } +.loading-dots span:nth-child(2) { animation-delay: -0.16s; } + +@keyframes loading-dots { + 0%, 80%, 100% { + transform: scale(0); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +/* 刷新按钮 */ +.refresh-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 6px; + color: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 12px; +} + +.refresh-btn:hover { + background: rgba(255,255,255,0.2); + transform: rotate(180deg); +} + +/* 状态颜色 */ +.install-status.installed { + border-color: rgba(34, 197, 94, 0.5); + background: rgba(34, 197, 94, 0.1); +} + +.install-status.not-installed { + border-color: rgba(239, 68, 68, 0.5); + background: rgba(239, 68, 68, 0.1); +} + +.install-status.installing { + border-color: rgba(59, 130, 246, 0.5); + background: rgba(59, 130, 246, 0.1); + animation: installing-pulse 2s infinite; +} + +@keyframes installing-pulse { + 0%, 100% { + box-shadow: 0 0 5px rgba(59, 130, 246, 0.3); + } + 50% { + box-shadow: 0 0 15px rgba(59, 130, 246, 0.6); + } } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html index d0598d63..3cadfc90 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html @@ -24,16 +24,64 @@
-
- {{ installStatusText() }} - +
+
+
+ {{ isInstalled ? '✅' : (isInstalling ? '⏳' : '❌') }} +
+ {{ installStatusText() }} + 版本 {{ version }} +
+
+ + +
-
+
+ +
+
+
🤖
+

需要安装AI系统

+

行为树编辑器需要安装AI系统才能正常使用

+
+ +
+
+ 📝 安装完成后编辑器将自动可用 +
+
+
diff --git a/extensions/cocos/cocos-ecs/package-lock.json b/extensions/cocos/cocos-ecs/package-lock.json index a7fbdcaa..d5ce273a 100644 --- a/extensions/cocos/cocos-ecs/package-lock.json +++ b/extensions/cocos/cocos-ecs/package-lock.json @@ -14,6 +14,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@esengine/ai/-/ai-2.0.1.tgz", "integrity": "sha512-qGGYc4kYlSJzCkBDJa+p5OruOnDvnL2oJ/ciKSHsPJVdn1tIefPEkUofJyMVGo4my5ubGr2ky6igTLtLYmhzRg==", + "license": "MIT", "engines": { "node": ">=16.0.0" }