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:
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';
|
||||
Reference in New Issue
Block a user