feat(cli): add CLI tool for adding ECS framework to existing projects (#339)
* feat(cli): add CLI tool for adding ECS framework to existing projects - Support Cocos Creator 2.x/3.x, LayaAir 3.x, and Node.js platforms - Auto-detect project type based on directory structure - Generate ECSManager with full configuration (debug, remote debug, WebSocket URL) - Auto-install dependencies with npm/yarn/pnpm detection - Platform-specific decorators and lifecycle methods * chore: add changeset for @esengine/cli * fix(ci): fix YAML syntax error in ai-issue-helper workflow * fix(cli): resolve file system race conditions (CodeQL) * chore(ci): remove unused and broken workflows * fix(ci): fix YAML encoding in release.yml
This commit is contained in:
11
.changeset/clever-dragons-smile.md
Normal file
11
.changeset/clever-dragons-smile.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@esengine/cli": minor
|
||||
---
|
||||
|
||||
feat(cli): 添加 CLI 工具用于将 ECS 框架集成到现有项目
|
||||
|
||||
- 支持 Cocos Creator 2.x/3.x、LayaAir 3.x、Node.js 平台
|
||||
- 自动检测项目类型
|
||||
- 生成完整配置的 ECSManager(调试模式、远程调试、WebSocket URL)
|
||||
- 自动安装依赖(支持 npm/yarn/pnpm)
|
||||
- 针对不同平台生成正确的装饰器和生命周期方法
|
||||
32
.github/labeler.yml
vendored
32
.github/labeler.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# 自动标签配置
|
||||
# 根据 issue/PR 内容自动打标签
|
||||
|
||||
'bug':
|
||||
- '/(bug|错误|崩溃|crash|error|exception|问题)/i'
|
||||
|
||||
'enhancement':
|
||||
- '/(feature|功能|enhancement|improve|优化|建议)/i'
|
||||
|
||||
'documentation':
|
||||
- '/(doc|文档|readme|guide|tutorial|教程)/i'
|
||||
|
||||
'question':
|
||||
- '/(question|疑问|how to|如何|怎么)/i'
|
||||
|
||||
'performance':
|
||||
- '/(performance|性能|slow|慢|lag|卡顿|optimize)/i'
|
||||
|
||||
'core':
|
||||
- '/(@esengine\/ecs-framework|packages\/core|core package)/i'
|
||||
|
||||
'editor':
|
||||
- '/(editor|编辑器|tauri)/i'
|
||||
|
||||
'network':
|
||||
- '/(network|网络|multiplayer|多人)/i'
|
||||
|
||||
'help wanted':
|
||||
- '/(help wanted|需要帮助|求助)/i'
|
||||
|
||||
'good first issue':
|
||||
- '/(good first issue|新手友好|beginner)/i'
|
||||
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
73
.github/workflows/ai-batch-analyze-issues.yml
vendored
@@ -1,73 +0,0 @@
|
||||
name: AI Batch Analyze Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: '分析模式'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 'recent' # 最近 10 个 issue
|
||||
- 'open' # 所有打开的 issue
|
||||
- 'all' # 所有 issue(慎用)
|
||||
default: 'recent'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Install GitHub CLI
|
||||
run: |
|
||||
gh --version || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
sudo apt update
|
||||
sudo apt install gh)
|
||||
|
||||
- name: Batch Analyze Issues
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
MODE="${{ github.event.inputs.mode }}"
|
||||
|
||||
# 获取 issue 列表
|
||||
if [ "$MODE" = "recent" ]; then
|
||||
echo "📊 分析最近 10 个 issue..."
|
||||
ISSUES=$(gh issue list --limit 10 --json number --jq '.[].number')
|
||||
elif [ "$MODE" = "open" ]; then
|
||||
echo "📊 分析所有打开的 issue..."
|
||||
ISSUES=$(gh issue list --state open --json number --jq '.[].number')
|
||||
else
|
||||
echo "📊 分析所有 issue(这可能需要很长时间)..."
|
||||
ISSUES=$(gh issue list --state all --limit 100 --json number --jq '.[].number')
|
||||
fi
|
||||
|
||||
# 为每个 issue 添加 AI 分析评论
|
||||
for issue_num in $ISSUES; do
|
||||
echo "🤖 分析 Issue #$issue_num..."
|
||||
|
||||
# 获取 issue 内容
|
||||
ISSUE_BODY=$(gh issue view $issue_num --json body --jq '.body')
|
||||
ISSUE_TITLE=$(gh issue view $issue_num --json title --jq '.title')
|
||||
|
||||
# 添加触发评论
|
||||
gh issue comment $issue_num --body "@ai-helper 请帮我分析这个 issue" || true
|
||||
|
||||
# 避免 API 限制
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "✅ 批量分析完成!"
|
||||
echo "查看结果:https://github.com/${{ github.repository }}/issues"
|
||||
61
.github/workflows/ai-helper-tip.yml
vendored
61
.github/workflows/ai-helper-tip.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: AI Helper Tip
|
||||
|
||||
# 对所有新创建的 issue 自动回复 AI 助手使用说明(新老用户都适用)
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
tip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post AI Helper Usage Tip
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const message = [
|
||||
"## 🤖 AI 助手可用 | AI Helper Available",
|
||||
"",
|
||||
"**中文说明:**",
|
||||
"",
|
||||
"本项目配备了 AI 智能助手,可以帮助你快速获得解答!",
|
||||
"",
|
||||
"**使用方法:** 在评论中提及 `@ai-helper`,AI 会自动搜索项目代码并提供解决方案。",
|
||||
"",
|
||||
"**示例:**",
|
||||
"```",
|
||||
"@ai-helper 如何创建一个新的 System?",
|
||||
"@ai-helper 这个报错是什么原因?",
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"**English:**",
|
||||
"",
|
||||
"This project has an AI assistant to help you get answers quickly!",
|
||||
"",
|
||||
"**How to use:** Mention `@ai-helper` in a comment, and AI will automatically search the codebase and provide solutions.",
|
||||
"",
|
||||
"**Examples:**",
|
||||
"```",
|
||||
"@ai-helper How do I create a new System?",
|
||||
"@ai-helper What causes this error?",
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"💡 *AI 助手基于代码库提供建议,复杂问题建议等待维护者回复*",
|
||||
"💡 *AI suggestions are based on the codebase. For complex issues, please wait for maintainer responses*"
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: message
|
||||
});
|
||||
|
||||
console.log('✅ AI helper tip posted successfully');
|
||||
80
.github/workflows/ai-issue-helper.yml
vendored
80
.github/workflows/ai-issue-helper.yml
vendored
@@ -1,80 +0,0 @@
|
||||
name: AI Issue Helper
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
ai-helper:
|
||||
runs-on: ubuntu-latest
|
||||
# å<>ªåœ¨çœŸå®žç”¨æˆ·æ<C2B7><C3A6>到 @ai-helper 时触å<C2A6>‘,忽略机器人评è®? if: |
|
||||
contains(github.event.comment.body, '@ai-helper') &&
|
||||
github.event.comment.user.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get Issue Details
|
||||
id: issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
// é™<C3A9>制长度,é<C592>¿å…<C3A5>è¶…è¿?token é™<C3A9>制
|
||||
const maxLength = 1000;
|
||||
const truncate = (str, max) => {
|
||||
if (!str) return '';
|
||||
return str.length > max ? str.substring(0, max) + '...[内容过长已截æ–]' : str;
|
||||
};
|
||||
|
||||
core.exportVariable('ISSUE_TITLE', truncate(issue.data.title || '', 200));
|
||||
core.exportVariable('ISSUE_BODY', truncate(issue.data.body || '', maxLength));
|
||||
core.exportVariable('COMMENT_BODY', truncate(context.payload.comment.body || '', 500));
|
||||
core.exportVariable('ISSUE_NUMBER', context.issue.number);
|
||||
|
||||
- name: Create Prompt
|
||||
id: prompt
|
||||
run: |
|
||||
cat > prompt.txt << 'PROMPT_EOF'
|
||||
Issue #${{ env.ISSUE_NUMBER }}
|
||||
|
||||
æ ‡é¢˜: ${{ env.ISSUE_TITLE }}
|
||||
|
||||
内容: ${{ env.ISSUE_BODY }}
|
||||
|
||||
评论: ${{ env.COMMENT_BODY }}
|
||||
|
||||
请æ<C2B7>œç´¢é¡¹ç›®ä»£ç <C3A7>å¹¶æ<C2B6><C3A6>供解决方案ã€? PROMPT_EOF
|
||||
|
||||
- name: AI Analysis
|
||||
uses: actions/ai-inference@v1
|
||||
id: ai
|
||||
with:
|
||||
model: 'gpt-4o'
|
||||
enable-github-mcp: true
|
||||
max-tokens: 1500
|
||||
system-prompt: |
|
||||
ä½ æ˜¯ ECS Framework (TypeScript ECS 框架) çš?AI 助手ã€? 主è¦<C3A8>代ç <C3A7>åœ?packages/framework/core/srcã€? æ<>œç´¢ç›¸å…³ä»£ç <C3A7>å<EFBFBD>Žï¼Œç”¨ä¸æ–‡ç®€æ´<C3A6>回ç”问题,包å<E280A6>«é—®é¢˜åˆ†æž<C3A6>ã€<C3A3>解决方案和代ç <C3A7>引用ã€? prompt-file: prompt.txt
|
||||
|
||||
- name: Post AI Response
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai.outputs.response }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: process.env.AI_RESPONSE
|
||||
});
|
||||
56
.github/workflows/ai-issue-moderator.yml
vendored
56
.github/workflows/ai-issue-moderator.yml
vendored
@@ -1,56 +0,0 @@
|
||||
name: AI Issue Moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Content
|
||||
uses: actions/ai-inference@v1
|
||||
id: check
|
||||
with:
|
||||
model: 'gpt-4o-mini'
|
||||
system-prompt: |
|
||||
你是一个内容审查助手。
|
||||
检查内容是否包含:
|
||||
1. 垃圾信息或广告
|
||||
2. 恶意或攻击性内容
|
||||
3. 与项目完全无关的内容
|
||||
|
||||
只返回 "SPAM" 或 "OK",不要其他内容。
|
||||
prompt: |
|
||||
标题:${{ github.event.issue.title || github.event.comment.body }}
|
||||
|
||||
内容:
|
||||
${{ github.event.issue.body || github.event.comment.body }}
|
||||
|
||||
- name: Mark as Spam
|
||||
if: contains(steps.check.outputs.response, 'SPAM')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// 添加 spam 标签
|
||||
github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['spam']
|
||||
});
|
||||
|
||||
// 添加评论
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: '🤖 这个内容被 AI 检测为可能的垃圾内容。如果这是误判,请联系维护者。\n\n🤖 This content was detected as potential spam by AI. If this is a false positive, please contact the maintainers.'
|
||||
});
|
||||
160
.github/workflows/batch-label-issues.yml
vendored
160
.github/workflows/batch-label-issues.yml
vendored
@@ -1,160 +0,0 @@
|
||||
name: Batch Label Issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: 'æ ‡ç¾æ¨¡å¼<C3A5>'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 'recent' # 最�20 �issue
|
||||
- 'open' # 所有打开�issue
|
||||
- 'unlabeled' # å<>ªå¤„ç<E2809E>†æ²¡æœ‰æ ‡ç¾çš„ issue
|
||||
- 'all' # 所�issue(慎用)
|
||||
default: 'recent'
|
||||
|
||||
skip_labeled:
|
||||
description: 'è·³è¿‡å·²æœ‰æ ‡ç¾çš?issue'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
batch-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Batch Label Issues
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
MODE="${{ github.event.inputs.mode }}"
|
||||
SKIP_LABELED="${{ github.event.inputs.skip_labeled }}"
|
||||
|
||||
echo "📊 开始批é‡<C3A9>æ‰“æ ‡ç¾..."
|
||||
echo "模å¼<C3A5>: $MODE"
|
||||
echo "è·³è¿‡å·²æ ‡ç? $SKIP_LABELED"
|
||||
|
||||
# 获å<C2B7>– issue 列表
|
||||
if [ "$MODE" = "recent" ]; then
|
||||
echo "📋 获å<C2B7>–最è¿?20 ä¸?issue..."
|
||||
ISSUES=$(gh issue list --limit 20 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
|
||||
elif [ "$MODE" = "open" ]; then
|
||||
echo "📋 获å<C2B7>–所有打开çš?issue..."
|
||||
ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
|
||||
elif [ "$MODE" = "unlabeled" ]; then
|
||||
echo "📋 获å<C2B7>–æ²¡æœ‰æ ‡ç¾çš?issue..."
|
||||
ISSUES=$(gh issue list --state all --json number,labels,title,body --jq '.[] | select(.labels | length == 0) | {number, labels: [.labels[].name], title, body}')
|
||||
else
|
||||
echo "📋 获å<C2B7>–所æœ?issue(é™<C3A9>åˆ?100 个)..."
|
||||
ISSUES=$(gh issue list --state all --limit 100 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
|
||||
fi
|
||||
|
||||
# 临时文件
|
||||
echo "$ISSUES" > /tmp/issues.json
|
||||
|
||||
# 处ç<E2809E>†æ¯<C3A6>个 issue
|
||||
cat /tmp/issues.json | jq -c '.' | while read -r issue; do
|
||||
ISSUE_NUM=$(echo "$issue" | jq -r '.number')
|
||||
EXISTING_LABELS=$(echo "$issue" | jq -r '.labels | join(",")')
|
||||
TITLE=$(echo "$issue" | jq -r '.title')
|
||||
BODY=$(echo "$issue" | jq -r '.body')
|
||||
|
||||
echo ""
|
||||
echo "ðŸ”<C5B8> 处ç<E2809E>† Issue #$ISSUE_NUM: $TITLE"
|
||||
echo " çŽ°æœ‰æ ‡ç¾: $EXISTING_LABELS"
|
||||
|
||||
# è·³è¿‡å·²æœ‰æ ‡ç¾çš?issue
|
||||
if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
|
||||
echo " â<>ï¸<C3AF> è·³è¿‡ï¼ˆå·²æœ‰æ ‡ç¾ï¼‰"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 分æž<C3A6>å†…å®¹å¹¶æ‰“æ ‡ç¾
|
||||
LABELS_TO_ADD=""
|
||||
|
||||
# 检�bug
|
||||
if echo "$TITLE $BODY" | grep -iE "(bug|错误|崩溃|crash|error|exception|问题|fix)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD bug"
|
||||
echo " ðŸ<C3B0>› 检测到: bug"
|
||||
fi
|
||||
|
||||
# 检�feature request
|
||||
if echo "$TITLE $BODY" | grep -iE "(feature|功能|enhancement|improve|优化|建议|新增|æ·»åŠ |add)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD enhancement"
|
||||
echo " �检测到: enhancement"
|
||||
fi
|
||||
|
||||
# 检�question
|
||||
if echo "$TITLE $BODY" | grep -iE "(question|疑问|how to|如何|怎么|为什么|why|咨询|\?|�" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD question"
|
||||
echo " â<>?检测到: question"
|
||||
fi
|
||||
|
||||
# 检�documentation
|
||||
if echo "$TITLE $BODY" | grep -iE "(doc|文档|readme|guide|tutorial|教程|说明)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD documentation"
|
||||
echo " 📖 检测到: documentation"
|
||||
fi
|
||||
|
||||
# 检�performance
|
||||
if echo "$TITLE $BODY" | grep -iE "(performance|性能|slow|æ…¢|lag|å<>¡é¡¿|optimize|优化)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD performance"
|
||||
echo " �检测到: performance"
|
||||
fi
|
||||
|
||||
# 检�core
|
||||
if echo "$TITLE $BODY" | grep -iE "(@esengine/ecs-framework|packages/framework/core|core package|æ ¸å¿ƒåŒ?" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD core"
|
||||
echo " 🎯 检测到: core"
|
||||
fi
|
||||
|
||||
# 检�editor
|
||||
if echo "$TITLE $BODY" | grep -iE "(editor|编辑器|tauri)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD editor"
|
||||
echo " 🎨 检测到: editor"
|
||||
fi
|
||||
|
||||
# 检�network
|
||||
if echo "$TITLE $BODY" | grep -iE "(network|网络|multiplayer|多人|å<>Œæ¥)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD network"
|
||||
echo " ðŸŒ<C5B8> 检测到: network"
|
||||
fi
|
||||
|
||||
# 检�help wanted
|
||||
if echo "$TITLE $BODY" | grep -iE "(help wanted|需è¦<C3A8>帮助|求助)" > /dev/null; then
|
||||
LABELS_TO_ADD="$LABELS_TO_ADD help wanted"
|
||||
echo " 🆘 检测到: help wanted"
|
||||
fi
|
||||
|
||||
# æ·»åŠ æ ‡ç¾
|
||||
if [ ! -z "$LABELS_TO_ADD" ]; then
|
||||
echo " âœ?æ·»åŠ æ ‡ç¾: $LABELS_TO_ADD"
|
||||
for label in $LABELS_TO_ADD; do
|
||||
gh issue edit $ISSUE_NUM --add-label "$label" 2>&1 | grep -v "already exists" || true
|
||||
done
|
||||
echo " 💬 æ·»åŠ è¯´æ˜Žè¯„è®º..."
|
||||
gh issue comment $ISSUE_NUM --body $'🤖 è‡ªåŠ¨æ ‡ç¾ç³»ç»Ÿæ£€æµ‹åˆ°æ?issue å¹¶æ·»åŠ äº†ç›¸å…³æ ‡ç¾ã€‚如有误判,请告知维护者。\n\n🤖 Auto-labeling system detected and labeled this issue. Please let maintainers know if this is incorrect.' || true
|
||||
else
|
||||
echo " ℹï¸<C3AF> 未检测到明确类型"
|
||||
fi
|
||||
|
||||
# é<>¿å…<C3A5> API é™<C3A9>制
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "âœ?批é‡<C3A9>æ ‡ç¾å®Œæˆ<C3A6>ï¼?
|
||||
echo "查看结果: https://github.com/${{ github.repository }}/issues"
|
||||
146
.github/workflows/cleanup-dependabot.yml
vendored
146
.github/workflows/cleanup-dependabot.yml
vendored
@@ -1,146 +0,0 @@
|
||||
name: Cleanup Old Dependabot PRs
|
||||
|
||||
# 手动触发的 workflow,用于清理堆积的 Dependabot PR
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_old:
|
||||
description: '关闭多少天前创建的 PR(默认 7 天)'
|
||||
required: false
|
||||
default: '7'
|
||||
dry_run:
|
||||
description: '试运行模式(true=仅显示,不关闭)'
|
||||
required: false
|
||||
default: 'true'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: List and Close Old Dependabot PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
|
||||
const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
||||
|
||||
console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
|
||||
console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
|
||||
console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
|
||||
console.log('---');
|
||||
|
||||
// 获取所有 Dependabot PR
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const dependabotPRs = pulls.filter(pr =>
|
||||
pr.user.login === 'dependabot[bot]' &&
|
||||
new Date(pr.created_at) < cutoffDate
|
||||
);
|
||||
|
||||
console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
|
||||
console.log('');
|
||||
|
||||
if (dependabotPRs.length === 0) {
|
||||
console.log('✅ 没有需要清理的 PR');
|
||||
return;
|
||||
}
|
||||
|
||||
// 按类型分组
|
||||
const byType = {
|
||||
dev: [],
|
||||
prod: [],
|
||||
actions: [],
|
||||
other: []
|
||||
};
|
||||
|
||||
for (const pr of dependabotPRs) {
|
||||
const title = pr.title.toLowerCase();
|
||||
const labels = pr.labels.map(l => l.name);
|
||||
|
||||
let type = 'other';
|
||||
if (title.includes('dev-dependencies') || title.includes('development')) {
|
||||
type = 'dev';
|
||||
} else if (title.includes('production-dependencies')) {
|
||||
type = 'prod';
|
||||
} else if (labels.includes('github-actions')) {
|
||||
type = 'actions';
|
||||
}
|
||||
|
||||
byType[type].push(pr);
|
||||
}
|
||||
|
||||
console.log('📋 PR 分类统计:');
|
||||
console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
|
||||
console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
|
||||
console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
|
||||
console.log(` ❓ 其他: ${byType.other.length} 个`);
|
||||
console.log('');
|
||||
|
||||
// 处理每个 PR
|
||||
for (const pr of dependabotPRs) {
|
||||
const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
|
||||
|
||||
console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
|
||||
console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
|
||||
console.log(` 链接: ${pr.html_url}`);
|
||||
|
||||
if (!dryRun) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: `🤖 **自动关闭旧的 Dependabot PR**
|
||||
|
||||
此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
|
||||
|
||||
📌 **下一步:**
|
||||
- Dependabot 已配置为月度运行,届时会创建新的分组更新
|
||||
- 新的 Mergify 规则会智能处理不同类型的依赖更新
|
||||
- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
|
||||
- 生产依赖需要 CI 通过才会自动合并
|
||||
|
||||
如果需要立即应用此更新,请手动更新依赖。
|
||||
|
||||
---
|
||||
*此操作由仓库维护者手动触发的清理工作流执行*`
|
||||
});
|
||||
|
||||
console.log(' ✅ 已关闭并添加说明');
|
||||
} else {
|
||||
console.log(' ℹ️ 试运行模式 - 未执行操作');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('---');
|
||||
if (dryRun) {
|
||||
console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
|
||||
console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
|
||||
} else {
|
||||
console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
|
||||
}
|
||||
23
.github/workflows/issue-labeler.yml
vendored
23
.github/workflows/issue-labeler.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Issue Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Label Issues
|
||||
uses: github/issue-labeler@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler.yml
|
||||
enable-versioned-regex: 1
|
||||
28
.github/workflows/issue-translator.yml
vendored
28
.github/workflows/issue-translator.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Issue Translator
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Translate Issues
|
||||
uses: tomsun28/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: false
|
||||
# 设置为 true 会修改标题,false 只在评论中添加翻译
|
||||
CUSTOM_BOT_NOTE: |
|
||||
<details>
|
||||
<summary>🌏 Translation / 翻译</summary>
|
||||
|
||||
Bot detected the issue body's language is not English, translate it automatically.
|
||||
机器人检测到 issue 内容非英文,自动翻译。
|
||||
|
||||
</details>
|
||||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: Release NPM Packages
|
||||
|
||||
on:
|
||||
# æ ‡ç¾è§¦å<C2A6>‘:支æŒ?v* å’?{package}-v* æ ¼å¼<C3A5>
|
||||
# Tag trigger: supports v* and {package}-v* formats
|
||||
push:
|
||||
tags:
|
||||
@@ -15,12 +14,11 @@ on:
|
||||
- 'physics-rapier2d-v*'
|
||||
- 'worker-generator-v*'
|
||||
|
||||
# ä¿<EFBFBD>留手动触å<EFBFBD>‘选项
|
||||
# Keep manual trigger option
|
||||
# Manual trigger option
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: '选择è¦<EFBFBD>å<EFBFBD>‘布的åŒ?| Select package to publish'
|
||||
description: 'Select package to publish'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -33,7 +31,7 @@ on:
|
||||
- physics-rapier2d
|
||||
- worker-generator
|
||||
version_type:
|
||||
description: '版本更新类型 | Version bump type'
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -61,11 +59,10 @@ jobs:
|
||||
id: parse
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
# ä»Žæ ‡ç¾è§£æž<C3A6>包å<E280A6><C3A5>和版本 | Parse package and version from tag
|
||||
# Parse package and version from tag
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# è§£æž<C3A6>æ ¼å¼<C3A5>:v1.0.0 æˆ?package-v1.0.0
|
||||
# Parse format: v1.0.0 or package-v1.0.0
|
||||
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
|
||||
PACKAGE="core"
|
||||
@@ -82,10 +79,9 @@ jobs:
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "mode=tag" >> $GITHUB_OUTPUT
|
||||
echo "📦 Package: $PACKAGE"
|
||||
echo "📌 Version: $VERSION"
|
||||
echo "Package: $PACKAGE"
|
||||
echo "Version: $VERSION"
|
||||
else
|
||||
# 手动触å<C2A6>‘:从 package.json 读å<C2BB>–å¹?bump 版本
|
||||
# Manual trigger: read from package.json and bump version
|
||||
PACKAGE="${{ github.event.inputs.package }}"
|
||||
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
|
||||
@@ -112,7 +108,6 @@ jobs:
|
||||
PACKAGE="${{ steps.parse.outputs.package }}"
|
||||
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
|
||||
|
||||
# 获å<C2B7>– package.json ä¸çš„版本
|
||||
# Get version from package.json
|
||||
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
|
||||
|
||||
@@ -125,7 +120,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "�Version verified: $EXPECTED_VERSION"
|
||||
echo "Version verified: $EXPECTED_VERSION"
|
||||
|
||||
- name: Bump version (manual mode)
|
||||
if: steps.parse.outputs.mode == 'manual'
|
||||
@@ -147,7 +142,7 @@ jobs:
|
||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
||||
|
||||
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "📌 Bumped version: $CURRENT �$NEW_VERSION"
|
||||
echo "Bumped version: $CURRENT -> $NEW_VERSION"
|
||||
|
||||
- name: Set final version
|
||||
id: version
|
||||
@@ -188,19 +183,18 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ steps.parse.outputs.tag }}
|
||||
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
|
||||
# ä¸<C3A4>设置为 latest,latest ä¿<C3A4>ç•™ç»™ç¼–è¾‘å™¨çƒæ›´æ–°ä½¿ç”? # Don't mark as latest, reserve latest for editor auto-update
|
||||
make_latest: false
|
||||
body: |
|
||||
## 🚀 @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
|
||||
## @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
|
||||
|
||||
📦 **NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
**NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
|
||||
```bash
|
||||
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
|
||||
```
|
||||
|
||||
---
|
||||
*自动å<EFBFBD>‘布 | Auto-released by GitHub Actions*
|
||||
*Auto-released by GitHub Actions*
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -215,15 +209,16 @@ jobs:
|
||||
delete-branch: true
|
||||
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
|
||||
body: |
|
||||
## 🚀 Release v${{ steps.version.outputs.value }}
|
||||
## Release v${{ steps.version.outputs.value }}
|
||||
|
||||
æ?PR æ›´æ–° `@esengine/${{ steps.parse.outputs.package }}` 包的版本å<EFBFBD>?
|
||||
### å<>˜æ›´
|
||||
- âœ?å·²å<C2B2>‘布到 npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
- �更新 `packages/${{ steps.parse.outputs.package }}/package.json` �`${{ steps.version.outputs.value }}`
|
||||
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
|
||||
|
||||
### Changes
|
||||
- Published to npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
|
||||
- Updated `packages/${{ steps.parse.outputs.package }}/package.json` to `${{ steps.version.outputs.value }}`
|
||||
|
||||
---
|
||||
*æ?PR ç”±å<C2B1>‘布工作æµ<C3A6>自动创建*
|
||||
*This PR was automatically created by the release workflow*
|
||||
labels: |
|
||||
release
|
||||
${{ steps.parse.outputs.package }}
|
||||
|
||||
46
.github/workflows/size-limit.yml
vendored
46
.github/workflows/size-limit.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Size Limit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- 'packages/framework/core/src/**'
|
||||
- 'packages/framework/core/package.json'
|
||||
- '.size-limit.json'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build core package
|
||||
run: |
|
||||
cd packages/framework/core
|
||||
pnpm run build:npm
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
skip_step: install
|
||||
58
.github/workflows/welcome.yml
vendored
58
.github/workflows/welcome.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Welcome
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Welcome new contributors
|
||||
uses: actions/first-interaction@v1.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
👋 你好!感谢你提交第一个 issue!
|
||||
|
||||
我们会尽快查看并回复。同时,建议你:
|
||||
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/esengine)
|
||||
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
|
||||
---
|
||||
|
||||
👋 Hello! Thanks for opening your first issue!
|
||||
|
||||
We'll review it as soon as possible. Meanwhile, you might want to:
|
||||
- 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
|
||||
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/esengine)
|
||||
|
||||
pr-message: |
|
||||
👋 你好!感谢你提交第一个 Pull Request!
|
||||
|
||||
在我们 Review 之前,请确保:
|
||||
- ✅ 代码遵循项目规范
|
||||
- ✅ 通过所有测试
|
||||
- ✅ 更新了相关文档
|
||||
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
|
||||
|
||||
查看完整的[贡献指南](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md)。
|
||||
|
||||
---
|
||||
|
||||
👋 Hello! Thanks for your first Pull Request!
|
||||
|
||||
Before we review, please ensure:
|
||||
- ✅ Code follows project conventions
|
||||
- ✅ All tests pass
|
||||
- ✅ Documentation is updated
|
||||
- ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
|
||||
See the full [Contributing Guide](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md).
|
||||
51
packages/tools/cli/package.json
Normal file
51
packages/tools/cli/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@esengine/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"esengine": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "ts-node src/cli.ts",
|
||||
"clean": "rimraf dist",
|
||||
"prepublishOnly": "pnpm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"esengine",
|
||||
"ecs",
|
||||
"cli",
|
||||
"cocos",
|
||||
"laya",
|
||||
"game-framework"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^12.1.0",
|
||||
"prompts": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
254
packages/tools/cli/src/adapters/cocos.ts
Normal file
254
packages/tools/cli/src/adapters/cocos.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* @zh Cocos Creator 3.x 平台适配器
|
||||
* @en Cocos Creator 3.x platform adapter
|
||||
*/
|
||||
export const cocosAdapter: PlatformAdapter = {
|
||||
id: 'cocos',
|
||||
name: 'Cocos Creator 3.x',
|
||||
description: 'Generate ECS integration for Cocos Creator 3.x projects',
|
||||
|
||||
getDependencies() {
|
||||
return {
|
||||
'@esengine/ecs-framework': 'latest'
|
||||
};
|
||||
},
|
||||
|
||||
getDevDependencies() {
|
||||
return {};
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
return {};
|
||||
},
|
||||
|
||||
generateFiles(config: ProjectConfig): FileEntry[] {
|
||||
const files: FileEntry[] = [];
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/ECSManager.ts',
|
||||
content: generateECSManager(config)
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/components/PositionComponent.ts',
|
||||
content: generatePositionComponent()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/systems/MovementSystem.ts',
|
||||
content: generateMovementSystem()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/README.md',
|
||||
content: generateReadme(config)
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
};
|
||||
|
||||
function generateECSManager(config: ProjectConfig): string {
|
||||
return `import { _decorator, Component, director } from 'cc';
|
||||
import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
|
||||
import { MovementSystem } from './systems/MovementSystem';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* Game Scene - Define your game systems here
|
||||
*/
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = '${config.name}';
|
||||
this.addSystem(new MovementSystem());
|
||||
// Add more systems here...
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Create your initial entities here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ECS Manager - Bridge between Cocos Creator and ESEngine ECS
|
||||
*
|
||||
* Attach this component to a node in your scene.
|
||||
* All game logic should be implemented in ECS Systems.
|
||||
*/
|
||||
@ccclass('ECSManager')
|
||||
export class ECSManager extends Component {
|
||||
/** @zh 调试模式 @en Debug mode */
|
||||
@property({ tooltip: 'Enable debug mode for ECS framework' })
|
||||
debug = false;
|
||||
|
||||
/** @zh 跨场景保持 @en Keep across scenes */
|
||||
@property({ tooltip: 'Keep this node alive across scenes' })
|
||||
persistent = true;
|
||||
|
||||
/** @zh 启用远程调试 @en Enable remote debugging */
|
||||
@property({ tooltip: 'Connect to ECS debugger via WebSocket' })
|
||||
remoteDebug = false;
|
||||
|
||||
/** @zh WebSocket调试地址 @en WebSocket debug URL */
|
||||
@property({ tooltip: 'WebSocket URL for remote debugging' })
|
||||
debugUrl = 'ws://localhost:9229';
|
||||
|
||||
/** @zh 自动重连 @en Auto reconnect */
|
||||
@property({ tooltip: 'Auto reconnect when connection lost' })
|
||||
autoReconnect = true;
|
||||
|
||||
private static _instance: ECSManager | null = null;
|
||||
private _scene!: GameScene;
|
||||
|
||||
static get instance() { return ECSManager._instance; }
|
||||
get scene() { return this._scene; }
|
||||
|
||||
onLoad() {
|
||||
if (ECSManager._instance) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
ECSManager._instance = this;
|
||||
|
||||
if (this.persistent) {
|
||||
director.addPersistRootNode(this.node);
|
||||
}
|
||||
|
||||
const config: ICoreConfig = {
|
||||
debug: this.debug
|
||||
};
|
||||
|
||||
// 配置远程调试
|
||||
if (this.remoteDebug && this.debugUrl) {
|
||||
config.debugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: this.debugUrl,
|
||||
autoReconnect: this.autoReconnect,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Core.create(config);
|
||||
this._scene = new GameScene();
|
||||
Core.setScene(this._scene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (ECSManager._instance === this) {
|
||||
ECSManager._instance = null;
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generatePositionComponent(): string {
|
||||
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Position component - stores entity position
|
||||
*/
|
||||
@ECSComponent('Position')
|
||||
export class PositionComponent extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
|
||||
constructor(x = 0, y = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateMovementSystem(): string {
|
||||
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent';
|
||||
|
||||
/**
|
||||
* Movement system - processes entities with PositionComponent
|
||||
*
|
||||
* Customize this system for your game logic.
|
||||
*/
|
||||
@ECSSystem('MovementSystem')
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent)!;
|
||||
// Update position using Time.deltaTime
|
||||
// position.x += velocity.dx * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateReadme(config: ProjectConfig): string {
|
||||
return `# ${config.name} - ECS Module
|
||||
|
||||
This module integrates ESEngine ECS framework with Cocos Creator.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Attach \`ECSManager\` component to a node in your scene
|
||||
2. Create your own components in \`components/\` folder
|
||||
3. Create your systems in \`systems/\` folder
|
||||
4. Register systems in \`ECSManager.start()\`
|
||||
|
||||
## Creating Components
|
||||
|
||||
\`\`\`typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
export class MyComponent extends Component {
|
||||
// Your data here
|
||||
health: number = 100;
|
||||
|
||||
reset() {
|
||||
this.health = 100;
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Creating Systems
|
||||
|
||||
\`\`\`typescript
|
||||
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import { MyComponent } from '../components/MyComponent';
|
||||
|
||||
export class MySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(MyComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const comp = entity.getComponent(MyComponent)!;
|
||||
// Process entity
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ESEngine ECS Framework](https://github.com/esengine/esengine)
|
||||
`;
|
||||
}
|
||||
259
packages/tools/cli/src/adapters/cocos2.ts
Normal file
259
packages/tools/cli/src/adapters/cocos2.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* @zh Cocos Creator 2.x 平台适配器
|
||||
* @en Cocos Creator 2.x platform adapter
|
||||
*/
|
||||
export const cocos2Adapter: PlatformAdapter = {
|
||||
id: 'cocos2',
|
||||
name: 'Cocos Creator 2.x',
|
||||
description: 'Generate ECS integration for Cocos Creator 2.x projects',
|
||||
|
||||
getDependencies() {
|
||||
return {
|
||||
'@esengine/ecs-framework': 'latest'
|
||||
};
|
||||
},
|
||||
|
||||
getDevDependencies() {
|
||||
return {};
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
return {};
|
||||
},
|
||||
|
||||
generateFiles(config: ProjectConfig): FileEntry[] {
|
||||
const files: FileEntry[] = [];
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/ECSManager.ts',
|
||||
content: generateECSManager(config)
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/components/PositionComponent.ts',
|
||||
content: generatePositionComponent()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/systems/MovementSystem.ts',
|
||||
content: generateMovementSystem()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'assets/scripts/ecs/README.md',
|
||||
content: generateReadme(config)
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
};
|
||||
|
||||
function generateECSManager(config: ProjectConfig): string {
|
||||
return `import { Core, Scene, ICoreConfig } from '@esengine/ecs-framework';
|
||||
import { MovementSystem } from './systems/MovementSystem';
|
||||
|
||||
const { ccclass, property } = cc._decorator;
|
||||
|
||||
/**
|
||||
* Game Scene - Define your game systems here
|
||||
*/
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = '${config.name}';
|
||||
this.addSystem(new MovementSystem());
|
||||
// Add more systems here...
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Create your initial entities here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ECS Manager - Bridge between Cocos Creator 2.x and ESEngine ECS
|
||||
*
|
||||
* Attach this component to a node in your scene.
|
||||
* All game logic should be implemented in ECS Systems.
|
||||
*/
|
||||
@ccclass
|
||||
export default class ECSManager extends cc.Component {
|
||||
/** @zh 调试模式 @en Debug mode */
|
||||
@property({ tooltip: 'Enable debug mode for ECS framework' })
|
||||
debug: boolean = false;
|
||||
|
||||
/** @zh 跨场景保持 @en Keep across scenes */
|
||||
@property({ tooltip: 'Keep this node alive across scenes' })
|
||||
persistent: boolean = true;
|
||||
|
||||
/** @zh 启用远程调试 @en Enable remote debugging */
|
||||
@property({ tooltip: 'Connect to ECS debugger via WebSocket' })
|
||||
remoteDebug: boolean = false;
|
||||
|
||||
/** @zh WebSocket调试地址 @en WebSocket debug URL */
|
||||
@property({ tooltip: 'WebSocket URL for remote debugging' })
|
||||
debugUrl: string = 'ws://localhost:9229';
|
||||
|
||||
/** @zh 自动重连 @en Auto reconnect */
|
||||
@property({ tooltip: 'Auto reconnect when connection lost' })
|
||||
autoReconnect: boolean = true;
|
||||
|
||||
private static _instance: ECSManager | null = null;
|
||||
private _scene!: GameScene;
|
||||
|
||||
static get instance() { return ECSManager._instance; }
|
||||
get scene() { return this._scene; }
|
||||
|
||||
onLoad() {
|
||||
if (ECSManager._instance) {
|
||||
this.node.destroy();
|
||||
return;
|
||||
}
|
||||
ECSManager._instance = this;
|
||||
|
||||
if (this.persistent) {
|
||||
cc.game.addPersistRootNode(this.node);
|
||||
}
|
||||
|
||||
const config: ICoreConfig = {
|
||||
debug: this.debug
|
||||
};
|
||||
|
||||
// 配置远程调试
|
||||
if (this.remoteDebug && this.debugUrl) {
|
||||
config.debugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: this.debugUrl,
|
||||
autoReconnect: this.autoReconnect,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Core.create(config);
|
||||
this._scene = new GameScene();
|
||||
Core.setScene(this._scene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (ECSManager._instance === this) {
|
||||
ECSManager._instance = null;
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generatePositionComponent(): string {
|
||||
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Position component - stores entity position
|
||||
*/
|
||||
@ECSComponent('Position')
|
||||
export class PositionComponent extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
|
||||
constructor(x = 0, y = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateMovementSystem(): string {
|
||||
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent';
|
||||
|
||||
/**
|
||||
* Movement system - processes entities with PositionComponent
|
||||
*
|
||||
* Customize this system for your game logic.
|
||||
*/
|
||||
@ECSSystem('MovementSystem')
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent)!;
|
||||
// Update position using Time.deltaTime
|
||||
// position.x += velocity.dx * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateReadme(config: ProjectConfig): string {
|
||||
return `# ${config.name} - ECS Module
|
||||
|
||||
This module integrates ESEngine ECS framework with Cocos Creator 2.x.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Attach \`ECSManager\` component to a node in your scene
|
||||
2. Create your own components in \`components/\` folder
|
||||
3. Create your systems in \`systems/\` folder
|
||||
4. Register systems in \`ECSManager.onLoad()\`
|
||||
|
||||
## Cocos Creator 2.x Notes
|
||||
|
||||
- Use \`cc._decorator\` for decorators
|
||||
- Use \`cc.Component\` as base class
|
||||
- Use \`cc.game.addPersistRootNode()\` for persistent nodes
|
||||
|
||||
## Creating Components
|
||||
|
||||
\`\`\`typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
export class MyComponent extends Component {
|
||||
health: number = 100;
|
||||
|
||||
reset() {
|
||||
this.health = 100;
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Creating Systems
|
||||
|
||||
\`\`\`typescript
|
||||
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import { MyComponent } from '../components/MyComponent';
|
||||
|
||||
export class MySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(MyComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const comp = entity.getComponent(MyComponent)!;
|
||||
// Process entity
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ESEngine ECS Framework](https://github.com/esengine/esengine)
|
||||
- [Cocos Creator 2.x Docs](https://docs.cocos.com/creator/2.4/manual/)
|
||||
`;
|
||||
}
|
||||
54
packages/tools/cli/src/adapters/index.ts
Normal file
54
packages/tools/cli/src/adapters/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { cocosAdapter } from './cocos.js';
|
||||
import { cocos2Adapter } from './cocos2.js';
|
||||
import { layaAdapter } from './laya.js';
|
||||
import { nodejsAdapter } from './nodejs.js';
|
||||
import type { AdapterRegistry, PlatformAdapter, PlatformType } from './types.js';
|
||||
|
||||
export * from './types.js';
|
||||
export { cocosAdapter } from './cocos.js';
|
||||
export { cocos2Adapter } from './cocos2.js';
|
||||
export { layaAdapter } from './laya.js';
|
||||
export { nodejsAdapter } from './nodejs.js';
|
||||
|
||||
/**
|
||||
* @zh 平台适配器注册表
|
||||
* @en Platform adapter registry
|
||||
*/
|
||||
export const adapters: AdapterRegistry = {
|
||||
cocos: cocosAdapter,
|
||||
cocos2: cocos2Adapter,
|
||||
laya: layaAdapter,
|
||||
nodejs: nodejsAdapter
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 获取平台适配器
|
||||
* @en Get platform adapter
|
||||
*/
|
||||
export function getAdapter(platform: PlatformType): PlatformAdapter {
|
||||
const adapter = adapters[platform];
|
||||
if (!adapter) {
|
||||
throw new Error(`Unknown platform: ${platform}`);
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有可用平台
|
||||
* @en Get all available platforms
|
||||
*/
|
||||
export function getPlatforms(): PlatformType[] {
|
||||
return Object.keys(adapters) as PlatformType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取平台选项(用于交互式提示)
|
||||
* @en Get platform choices (for interactive prompts)
|
||||
*/
|
||||
export function getPlatformChoices(): Array<{ title: string; value: PlatformType; description: string }> {
|
||||
return Object.values(adapters).map((adapter) => ({
|
||||
title: adapter.name,
|
||||
value: adapter.id,
|
||||
description: adapter.description
|
||||
}));
|
||||
}
|
||||
245
packages/tools/cli/src/adapters/laya.ts
Normal file
245
packages/tools/cli/src/adapters/laya.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* @zh Laya 3.x 平台适配器
|
||||
* @en Laya 3.x platform adapter
|
||||
*/
|
||||
export const layaAdapter: PlatformAdapter = {
|
||||
id: 'laya',
|
||||
name: 'Laya 3.x',
|
||||
description: 'Generate ECS integration for LayaAir 3.x projects',
|
||||
|
||||
getDependencies() {
|
||||
return {
|
||||
'@esengine/ecs-framework': 'latest'
|
||||
};
|
||||
},
|
||||
|
||||
getDevDependencies() {
|
||||
return {};
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
return {};
|
||||
},
|
||||
|
||||
generateFiles(config: ProjectConfig): FileEntry[] {
|
||||
const files: FileEntry[] = [];
|
||||
|
||||
files.push({
|
||||
path: 'src/ecs/ECSManager.ts',
|
||||
content: generateECSManager(config)
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'src/ecs/components/PositionComponent.ts',
|
||||
content: generatePositionComponent()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'src/ecs/systems/MovementSystem.ts',
|
||||
content: generateMovementSystem()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'src/ecs/README.md',
|
||||
content: generateReadme(config)
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
};
|
||||
|
||||
function generateECSManager(config: ProjectConfig): string {
|
||||
return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
|
||||
import { MovementSystem } from './systems/MovementSystem';
|
||||
|
||||
const { regClass, property } = Laya;
|
||||
|
||||
/**
|
||||
* Game Scene - Define your game systems here
|
||||
*/
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = '${config.name}';
|
||||
this.addSystem(new MovementSystem());
|
||||
// Add more systems here...
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Create your initial entities here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ECS Manager - Bridge between LayaAir and ESEngine ECS
|
||||
*
|
||||
* Attach this script to a node in your scene via Laya IDE.
|
||||
* All game logic should be implemented in ECS Systems.
|
||||
*/
|
||||
@regClass()
|
||||
export class ECSManager extends Laya.Script {
|
||||
/** @zh 调试模式 @en Debug mode */
|
||||
@property({ type: Boolean, caption: 'Debug', tips: 'Enable debug mode for ECS framework' })
|
||||
debug = false;
|
||||
|
||||
/** @zh 启用远程调试 @en Enable remote debugging */
|
||||
@property({ type: Boolean, caption: 'Remote Debug', tips: 'Connect to ECS debugger via WebSocket' })
|
||||
remoteDebug = false;
|
||||
|
||||
/** @zh WebSocket调试地址 @en WebSocket debug URL */
|
||||
@property({ type: String, caption: 'Debug URL', tips: 'WebSocket URL for remote debugging (e.g., ws://localhost:9229)' })
|
||||
debugUrl = 'ws://localhost:9229';
|
||||
|
||||
/** @zh 自动重连 @en Auto reconnect */
|
||||
@property({ type: Boolean, caption: 'Auto Reconnect', tips: 'Auto reconnect when connection lost' })
|
||||
autoReconnect = true;
|
||||
|
||||
private static _instance: ECSManager | null = null;
|
||||
private _scene!: GameScene;
|
||||
|
||||
static get instance() { return ECSManager._instance; }
|
||||
get scene() { return this._scene; }
|
||||
|
||||
onAwake(): void {
|
||||
if (ECSManager._instance) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
ECSManager._instance = this;
|
||||
|
||||
const config: ICoreConfig = {
|
||||
debug: this.debug
|
||||
};
|
||||
|
||||
// 配置远程调试
|
||||
if (this.remoteDebug && this.debugUrl) {
|
||||
config.debugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: this.debugUrl,
|
||||
autoReconnect: this.autoReconnect,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Core.create(config);
|
||||
this._scene = new GameScene();
|
||||
Core.setScene(this._scene);
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
if (ECSManager._instance === this) {
|
||||
ECSManager._instance = null;
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generatePositionComponent(): string {
|
||||
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Position component - stores entity position
|
||||
*/
|
||||
@ECSComponent('Position')
|
||||
export class PositionComponent extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
|
||||
constructor(x = 0, y = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateMovementSystem(): string {
|
||||
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent';
|
||||
|
||||
/**
|
||||
* Movement system - processes entities with PositionComponent
|
||||
*
|
||||
* Customize this system for your game logic.
|
||||
*/
|
||||
@ECSSystem('MovementSystem')
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent)!;
|
||||
// Update position using Time.deltaTime
|
||||
// position.x += velocity.dx * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateReadme(config: ProjectConfig): string {
|
||||
return `# ${config.name} - ECS Module
|
||||
|
||||
This module integrates ESEngine ECS framework with LayaAir 3.x.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. In Laya IDE, attach \`ECSManager\` script to a node in your scene
|
||||
2. Create your own components in \`components/\` folder
|
||||
3. Create your systems in \`systems/\` folder
|
||||
4. Register systems in \`ECSManager.onAwake()\`
|
||||
|
||||
## Creating Components
|
||||
|
||||
\`\`\`typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
export class MyComponent extends Component {
|
||||
health: number = 100;
|
||||
|
||||
reset() {
|
||||
this.health = 100;
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Creating Systems
|
||||
|
||||
\`\`\`typescript
|
||||
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import { MyComponent } from '../components/MyComponent';
|
||||
|
||||
export class MySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(MyComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const comp = entity.getComponent(MyComponent)!;
|
||||
// Process entity
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ESEngine ECS Framework](https://github.com/esengine/esengine)
|
||||
- [LayaAir Documentation](https://layaair.com/)
|
||||
`;
|
||||
}
|
||||
348
packages/tools/cli/src/adapters/nodejs.ts
Normal file
348
packages/tools/cli/src/adapters/nodejs.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* @zh Node.js 平台适配器
|
||||
* @en Node.js platform adapter
|
||||
*/
|
||||
export const nodejsAdapter: PlatformAdapter = {
|
||||
id: 'nodejs',
|
||||
name: 'Node.js',
|
||||
description: 'Generate standalone Node.js project with ECS (for servers, CLI tools, simulations)',
|
||||
|
||||
getDependencies() {
|
||||
return {
|
||||
'@esengine/ecs-framework': 'latest'
|
||||
};
|
||||
},
|
||||
|
||||
getDevDependencies() {
|
||||
return {
|
||||
'@types/node': '^20.0.0',
|
||||
'tsx': '^4.0.0',
|
||||
'typescript': '^5.0.0'
|
||||
};
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
return {
|
||||
'dev': 'tsx watch src/index.ts',
|
||||
'start': 'tsx src/index.ts',
|
||||
'build': 'tsc',
|
||||
'build:start': 'tsc && node dist/index.js'
|
||||
};
|
||||
},
|
||||
|
||||
generateFiles(config: ProjectConfig): FileEntry[] {
|
||||
const files: FileEntry[] = [];
|
||||
|
||||
files.push({
|
||||
path: 'src/index.ts',
|
||||
content: generateIndex(config)
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'src/Game.ts',
|
||||
content: generateGame(config)
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'src/components/PositionComponent.ts',
|
||||
content: generatePositionComponent()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'src/systems/MovementSystem.ts',
|
||||
content: generateMovementSystem()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'tsconfig.json',
|
||||
content: generateTsConfig()
|
||||
});
|
||||
|
||||
files.push({
|
||||
path: 'README.md',
|
||||
content: generateReadme(config)
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
};
|
||||
|
||||
function generateIndex(config: ProjectConfig): string {
|
||||
return `import { Game } from './Game.js';
|
||||
|
||||
const game = new Game();
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\\nShutting down...');
|
||||
game.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
game.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start the game
|
||||
game.start();
|
||||
|
||||
console.log('[${config.name}] Game started. Press Ctrl+C to stop.');
|
||||
`;
|
||||
}
|
||||
|
||||
function generateGame(config: ProjectConfig): string {
|
||||
return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
|
||||
import { MovementSystem } from './systems/MovementSystem.js';
|
||||
|
||||
/**
|
||||
* Game configuration options
|
||||
*/
|
||||
export interface GameOptions {
|
||||
/** @zh 调试模式 @en Debug mode */
|
||||
debug?: boolean;
|
||||
/** @zh 目标帧率 @en Target FPS */
|
||||
targetFPS?: number;
|
||||
/** @zh 远程调试配置 @en Remote debug configuration */
|
||||
remoteDebug?: {
|
||||
/** @zh 启用远程调试 @en Enable remote debugging */
|
||||
enabled: boolean;
|
||||
/** @zh WebSocket地址 @en WebSocket URL */
|
||||
url: string;
|
||||
/** @zh 自动重连 @en Auto reconnect */
|
||||
autoReconnect?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Game Scene - Define your game systems here
|
||||
*/
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = '${config.name}';
|
||||
this.addSystem(new MovementSystem());
|
||||
// Add more systems here...
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
// Create your initial entities here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main game class with ECS game loop
|
||||
*
|
||||
* Features:
|
||||
* - Configurable debug mode and FPS
|
||||
* - Remote debugging via WebSocket
|
||||
* - Fixed timestep game loop
|
||||
* - Graceful start/stop
|
||||
*/
|
||||
export class Game {
|
||||
private readonly _scene: GameScene;
|
||||
private readonly _targetFPS: number;
|
||||
private _running = false;
|
||||
private _tickInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private _lastTime = 0;
|
||||
|
||||
get scene() { return this._scene; }
|
||||
get running() { return this._running; }
|
||||
|
||||
constructor(options: GameOptions = {}) {
|
||||
const { debug = false, targetFPS = 60, remoteDebug } = options;
|
||||
this._targetFPS = targetFPS;
|
||||
|
||||
const config: ICoreConfig = { debug };
|
||||
|
||||
// 配置远程调试
|
||||
if (remoteDebug?.enabled && remoteDebug.url) {
|
||||
config.debugConfig = {
|
||||
enabled: true,
|
||||
websocketUrl: remoteDebug.url,
|
||||
autoReconnect: remoteDebug.autoReconnect ?? true,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Core.create(config);
|
||||
this._scene = new GameScene();
|
||||
Core.setScene(this._scene);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this._running) return;
|
||||
this._running = true;
|
||||
this._lastTime = performance.now();
|
||||
|
||||
this._tickInterval = setInterval(() => {
|
||||
const now = performance.now();
|
||||
Core.update((now - this._lastTime) / 1000);
|
||||
this._lastTime = now;
|
||||
}, 1000 / this._targetFPS);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (!this._running) return;
|
||||
this._running = false;
|
||||
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = null;
|
||||
}
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generatePositionComponent(): string {
|
||||
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Position component - stores entity position
|
||||
*/
|
||||
@ECSComponent('Position')
|
||||
export class PositionComponent extends Component {
|
||||
x = 0;
|
||||
y = 0;
|
||||
|
||||
constructor(x = 0, y = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateMovementSystem(): string {
|
||||
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent.js';
|
||||
|
||||
/**
|
||||
* Movement system - processes entities with PositionComponent
|
||||
*
|
||||
* Customize this system for your game logic.
|
||||
*/
|
||||
@ECSSystem('MovementSystem')
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent)!;
|
||||
// Update position using Time.deltaTime
|
||||
// position.x += velocity.dx * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateTsConfig(): string {
|
||||
return `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function generateReadme(config: ProjectConfig): string {
|
||||
return `# ${config.name}
|
||||
|
||||
A Node.js project using ESEngine ECS framework.
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode (with hot reload)
|
||||
npm run dev
|
||||
|
||||
# Build and run
|
||||
npm run build:start
|
||||
\`\`\`
|
||||
|
||||
## Project Structure
|
||||
|
||||
\`\`\`
|
||||
src/
|
||||
├── index.ts # Entry point
|
||||
├── Game.ts # Game loop and ECS setup
|
||||
├── components/ # ECS components (data)
|
||||
│ └── PositionComponent.ts
|
||||
└── systems/ # ECS systems (logic)
|
||||
└── MovementSystem.ts
|
||||
\`\`\`
|
||||
|
||||
## Creating Components
|
||||
|
||||
\`\`\`typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
export class HealthComponent extends Component {
|
||||
current = 100;
|
||||
max = 100;
|
||||
|
||||
reset(): void {
|
||||
this.current = this.max;
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Creating Systems
|
||||
|
||||
\`\`\`typescript
|
||||
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import { HealthComponent } from '../components/HealthComponent.js';
|
||||
|
||||
export class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(HealthComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const health = entity.getComponent(HealthComponent)!;
|
||||
// Your logic here
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Game servers
|
||||
- CLI tools with complex logic
|
||||
- Simulations
|
||||
- Automated testing
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ESEngine ECS Framework](https://github.com/esengine/esengine)
|
||||
`;
|
||||
}
|
||||
77
packages/tools/cli/src/adapters/types.ts
Normal file
77
packages/tools/cli/src/adapters/types.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @zh 项目配置
|
||||
* @en Project configuration
|
||||
*/
|
||||
export interface ProjectConfig {
|
||||
/** @zh 项目名称 @en Project name */
|
||||
name: string;
|
||||
/** @zh 目标平台 @en Target platform */
|
||||
platform: PlatformType;
|
||||
/** @zh 项目路径 @en Project path */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 支持的平台类型
|
||||
* @en Supported platform types
|
||||
*/
|
||||
export type PlatformType = 'cocos' | 'cocos2' | 'laya' | 'nodejs';
|
||||
|
||||
/**
|
||||
* @zh 文件入口
|
||||
* @en File entry
|
||||
*/
|
||||
export interface FileEntry {
|
||||
/** @zh 相对路径 @en Relative path */
|
||||
path: string;
|
||||
/** @zh 文件内容 @en File content */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 平台适配器接口
|
||||
* @en Platform adapter interface
|
||||
*
|
||||
* @zh 每个平台只需实现这个接口,即可支持项目生成
|
||||
* @en Each platform only needs to implement this interface to support project generation
|
||||
*/
|
||||
export interface PlatformAdapter {
|
||||
/** @zh 平台标识 @en Platform identifier */
|
||||
readonly id: PlatformType;
|
||||
|
||||
/** @zh 平台显示名称 @en Platform display name */
|
||||
readonly name: string;
|
||||
|
||||
/** @zh 平台描述 @en Platform description */
|
||||
readonly description: string;
|
||||
|
||||
/**
|
||||
* @zh 获取平台特定的依赖
|
||||
* @en Get platform-specific dependencies
|
||||
*/
|
||||
getDependencies(): Record<string, string>;
|
||||
|
||||
/**
|
||||
* @zh 获取平台特定的开发依赖
|
||||
* @en Get platform-specific dev dependencies
|
||||
*/
|
||||
getDevDependencies(): Record<string, string>;
|
||||
|
||||
/**
|
||||
* @zh 生成平台特定的文件
|
||||
* @en Generate platform-specific files
|
||||
*/
|
||||
generateFiles(config: ProjectConfig): FileEntry[];
|
||||
|
||||
/**
|
||||
* @zh 获取 package.json 的 scripts
|
||||
* @en Get package.json scripts
|
||||
*/
|
||||
getScripts(): Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 平台适配器注册表类型
|
||||
* @en Platform adapter registry type
|
||||
*/
|
||||
export type AdapterRegistry = Record<PlatformType, PlatformAdapter>;
|
||||
320
packages/tools/cli/src/cli.ts
Normal file
320
packages/tools/cli/src/cli.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander';
|
||||
import prompts from 'prompts';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { getPlatformChoices, getPlatforms, getAdapter } from './adapters/index.js';
|
||||
import type { PlatformType, ProjectConfig } from './adapters/types.js';
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
|
||||
/**
|
||||
* @zh 打印 Logo
|
||||
* @en Print logo
|
||||
*/
|
||||
function printLogo(): void {
|
||||
console.log();
|
||||
console.log(chalk.cyan(' ╭──────────────────────────────────────╮'));
|
||||
console.log(chalk.cyan(' │ │'));
|
||||
console.log(chalk.cyan(' │ ') + chalk.bold.white('ESEngine CLI') + chalk.gray(` v${VERSION}`) + chalk.cyan(' │'));
|
||||
console.log(chalk.cyan(' │ │'));
|
||||
console.log(chalk.cyan(' ╰──────────────────────────────────────╯'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检测是否存在 *.laya 文件
|
||||
* @en Check if *.laya file exists
|
||||
*/
|
||||
function hasLayaProjectFile(cwd: string): boolean {
|
||||
try {
|
||||
const files = fs.readdirSync(cwd);
|
||||
return files.some(f => f.endsWith('.laya'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检测 Cocos Creator 版本
|
||||
* @en Detect Cocos Creator version
|
||||
*/
|
||||
function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null {
|
||||
// Cocos 3.x: 检查 cc.config.json 或 extensions 目录
|
||||
if (fs.existsSync(path.join(cwd, 'cc.config.json')) ||
|
||||
fs.existsSync(path.join(cwd, 'extensions'))) {
|
||||
return 'cocos';
|
||||
}
|
||||
|
||||
// 检查 project.json 中的版本号
|
||||
const projectJsonPath = path.join(cwd, 'project.json');
|
||||
if (fs.existsSync(projectJsonPath)) {
|
||||
try {
|
||||
const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
||||
// Cocos 2.x project.json 有 engine-version 字段
|
||||
if (project['engine-version'] || project.engine) {
|
||||
const version = project['engine-version'] || project.engine || '';
|
||||
// 2.x 版本格式: "cocos-creator-js-2.4.x" 或 "2.4.x"
|
||||
if (version.includes('2.') || version.startsWith('2')) {
|
||||
return 'cocos2';
|
||||
}
|
||||
}
|
||||
// 有 project.json 但没有版本信息,假设是 3.x
|
||||
return 'cocos';
|
||||
} catch {
|
||||
// 解析失败,假设是 3.x
|
||||
return 'cocos';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检测项目类型
|
||||
* @en Detect project type
|
||||
*/
|
||||
function detectProjectType(cwd: string): PlatformType | null {
|
||||
// Laya: 检查 *.laya 文件 或 .laya 目录 或 laya.json
|
||||
if (hasLayaProjectFile(cwd) ||
|
||||
fs.existsSync(path.join(cwd, '.laya')) ||
|
||||
fs.existsSync(path.join(cwd, 'laya.json'))) {
|
||||
return 'laya';
|
||||
}
|
||||
|
||||
// Cocos Creator: 检查 assets 目录
|
||||
if (fs.existsSync(path.join(cwd, 'assets'))) {
|
||||
const cocosVersion = detectCocosVersion(cwd);
|
||||
if (cocosVersion) {
|
||||
return cocosVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// Node.js: 检查 package.json
|
||||
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
||||
return 'nodejs';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检测包管理器
|
||||
* @en Detect package manager
|
||||
*/
|
||||
function detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {
|
||||
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
||||
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取或创建 package.json
|
||||
* @en Read or create package.json
|
||||
*/
|
||||
function readOrCreatePackageJson(packageJsonPath: string, projectName: string): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
const pkg = {
|
||||
name: projectName,
|
||||
version: '1.0.0',
|
||||
private: true,
|
||||
dependencies: {}
|
||||
};
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
|
||||
console.log(chalk.green(' ✓ Created package.json'));
|
||||
return pkg;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 安装依赖
|
||||
* @en Install dependencies
|
||||
*/
|
||||
function installDependencies(cwd: string, deps: Record<string, string>): boolean {
|
||||
const pm = detectPackageManager(cwd);
|
||||
const packageJsonPath = path.join(cwd, 'package.json');
|
||||
|
||||
// 读取或创建 package.json(原子操作,避免竞态条件)
|
||||
const pkg = readOrCreatePackageJson(packageJsonPath, path.basename(cwd));
|
||||
const pkgDeps = (pkg.dependencies || {}) as Record<string, string>;
|
||||
|
||||
let needsInstall = false;
|
||||
for (const [name, version] of Object.entries(deps)) {
|
||||
if (!pkgDeps[name]) {
|
||||
pkgDeps[name] = version;
|
||||
needsInstall = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsInstall) {
|
||||
console.log(chalk.gray(' Dependencies already configured.'));
|
||||
return true;
|
||||
}
|
||||
|
||||
pkg.dependencies = pkgDeps;
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
|
||||
|
||||
// 运行安装命令
|
||||
const installCmd = pm === 'pnpm' ? 'pnpm install' : pm === 'yarn' ? 'yarn' : 'npm install';
|
||||
console.log(chalk.gray(` Running ${installCmd}...`));
|
||||
|
||||
try {
|
||||
execSync(installCmd, { cwd, stdio: 'inherit' });
|
||||
return true;
|
||||
} catch {
|
||||
console.log(chalk.yellow(` ⚠ Failed to run ${installCmd}. Please run it manually.`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取项目名称
|
||||
* @en Get project name
|
||||
*/
|
||||
function getProjectName(cwd: string): string {
|
||||
const packageJsonPath = path.join(cwd, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
return pkg.name || path.basename(cwd);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return path.basename(cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 初始化 ECS 到现有项目
|
||||
* @en Initialize ECS into existing project
|
||||
*/
|
||||
async function initCommand(options: { platform?: string }): Promise<void> {
|
||||
printLogo();
|
||||
|
||||
const cwd = process.cwd();
|
||||
let platform = options.platform as PlatformType | undefined;
|
||||
|
||||
// 尝试自动检测项目类型
|
||||
const detected = detectProjectType(cwd);
|
||||
|
||||
if (!platform) {
|
||||
if (detected) {
|
||||
console.log(chalk.gray(` Detected: ${detected} project`));
|
||||
platform = detected;
|
||||
} else {
|
||||
// 交互式选择
|
||||
const response = await prompts({
|
||||
type: 'select',
|
||||
name: 'platform',
|
||||
message: 'Select platform:',
|
||||
choices: getPlatformChoices(),
|
||||
initial: 0
|
||||
}, {
|
||||
onCancel: () => {
|
||||
console.log(chalk.yellow('\n Cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
platform = response.platform;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证平台
|
||||
const validPlatforms = getPlatforms();
|
||||
if (!platform || !validPlatforms.includes(platform)) {
|
||||
console.log(chalk.red(`\n ✗ Invalid platform. Choose from: ${validPlatforms.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const projectName = getProjectName(cwd);
|
||||
const adapter = getAdapter(platform);
|
||||
|
||||
const config: ProjectConfig = {
|
||||
name: projectName,
|
||||
platform,
|
||||
path: cwd
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold('Adding ECS to your project...'));
|
||||
|
||||
// 生成文件
|
||||
const files = adapter.generateFiles(config);
|
||||
const createdFiles: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(cwd, file.path);
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
// 创建目录(recursive: true 不会因目录存在而失败)
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// 尝试写入文件(wx 模式:如果文件存在则失败,避免竞态条件)
|
||||
try {
|
||||
fs.writeFileSync(filePath, file.content, { encoding: 'utf-8', flag: 'wx' });
|
||||
createdFiles.push(file.path);
|
||||
console.log(chalk.green(` ✓ Created ${file.path}`));
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
|
||||
console.log(chalk.yellow(` ⚠ Skipped ${file.path} (already exists)`));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (createdFiles.length === 0) {
|
||||
console.log(chalk.yellow('\n No files created. ECS may already be set up.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 安装依赖
|
||||
console.log();
|
||||
console.log(chalk.bold('Installing dependencies...'));
|
||||
const deps = adapter.getDependencies();
|
||||
installDependencies(cwd, deps);
|
||||
|
||||
// 打印下一步
|
||||
console.log();
|
||||
console.log(chalk.bold('Done!'));
|
||||
console.log();
|
||||
|
||||
if (platform === 'cocos' || platform === 'cocos2') {
|
||||
console.log(chalk.gray(' Attach ECSManager to a node in your scene to start.'));
|
||||
} else if (platform === 'laya') {
|
||||
console.log(chalk.gray(' Attach ECSManager script to a node in Laya IDE to start.'));
|
||||
} else {
|
||||
console.log(chalk.gray(' Run `npm run dev` to start your game.'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Setup CLI
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('esengine')
|
||||
.description('CLI tool for adding ESEngine ECS to your project')
|
||||
.version(VERSION);
|
||||
|
||||
program
|
||||
.command('init')
|
||||
.description('Add ECS framework to your existing project')
|
||||
.option('-p, --platform <platform>', 'Target platform (cocos, cocos2, laya, nodejs)')
|
||||
.action(initCommand);
|
||||
|
||||
// Default command: run init
|
||||
program
|
||||
.action(() => {
|
||||
initCommand({});
|
||||
});
|
||||
|
||||
program.parse();
|
||||
8
packages/tools/cli/src/index.ts
Normal file
8
packages/tools/cli/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @zh ESEngine CLI - 为现有项目添加 ECS 框架
|
||||
* @en ESEngine CLI - Add ECS framework to existing projects
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './adapters/index.js';
|
||||
19
packages/tools/cli/tsconfig.json
Normal file
19
packages/tools/cli/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@@ -2032,6 +2032,31 @@ importers:
|
||||
specifier: ^4.5.4
|
||||
version: 4.5.4(@types/node@20.19.27)(rollup@4.54.0)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
||||
packages/tools/cli:
|
||||
dependencies:
|
||||
chalk:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
commander:
|
||||
specifier: ^12.1.0
|
||||
version: 12.1.0
|
||||
prompts:
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.19.0
|
||||
version: 20.19.27
|
||||
'@types/prompts':
|
||||
specifier: ^2.4.9
|
||||
version: 2.4.9
|
||||
rimraf:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.10
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/tools/sdk:
|
||||
dependencies:
|
||||
'@esengine/asset-system':
|
||||
@@ -4793,6 +4818,9 @@ packages:
|
||||
'@types/pako@2.0.4':
|
||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||
|
||||
'@types/prompts@2.4.9':
|
||||
resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
|
||||
|
||||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
@@ -5557,6 +5585,10 @@ packages:
|
||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
commander@12.1.0:
|
||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
@@ -13067,6 +13099,11 @@ snapshots:
|
||||
|
||||
'@types/pako@2.0.4': {}
|
||||
|
||||
'@types/prompts@2.4.9':
|
||||
dependencies:
|
||||
'@types/node': 20.19.27
|
||||
kleur: 3.0.3
|
||||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/qs@6.14.0': {}
|
||||
@@ -13936,6 +13973,8 @@ snapshots:
|
||||
|
||||
commander@11.1.0: {}
|
||||
|
||||
commander@12.1.0: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
Reference in New Issue
Block a user