feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI (#297)

* feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI

- 新增 @esengine/worker-generator 包,用于从 WorkerEntitySystem 生成 Worker 文件
- WorkerEntitySystem 添加 workerScriptPath 配置项,支持预编译 Worker 脚本
- CLI 工具支持 --wechat 模式,自动转换 ES6+ 为 ES5 语法
- 修复微信小游戏 Worker 消息格式差异(res 直接是数据,无需 .data)
- 更新中英文文档,添加微信小游戏支持章节

* docs: 更新 changelog,添加 v2.3.1 说明并标注 v2.3.0 为废弃

* fix: 修复 CI 检查问题

- 移除 cli.ts 中未使用的 toKebabCase 函数
- 修复 generator.ts 中正则表达式的 ReDoS 风险(使用 [ \t] 替代 \s*)
- 更新 changelog 版本号(2.3.1 -> 2.3.2)

* docs: 移除未发布版本的 changelog 条目

* fix(worker-generator): 使用 TypeScript 编译器替代手写正则进行 ES5 转换

- 修复 CodeQL 检测的 ReDoS 安全问题
- 使用 ts.transpileModule 进行安全可靠的代码转换
- 移除所有可能导致回溯的正则表达式
This commit is contained in:
YHH
2025-12-08 17:02:11 +08:00
committed by GitHub
parent 52bbccd53c
commit dfd0dfc7f9
18 changed files with 2595 additions and 546 deletions

View File

@@ -4,7 +4,21 @@
---
## v2.3.0 (2025-12-06)
## v2.3.1 (2025-12-07)
### Bug Fixes
- **类型导出修复**: 修复 v2.3.0 中的类型导出问题
- 解决 `ServiceToken` 跨包类型兼容性问题
- 修复 `editor-app``behavior-tree-editor` 中的类型引用
---
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。
>
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
### Features

View File

@@ -4,7 +4,19 @@ This document records the version update history of the `@esengine/ecs-framework
---
## v2.3.0 (2025-12-06)
## v2.3.1 (2025-12-07)
### Bug Fixes
- **Type export fix**: Fix type export issues in v2.3.0
- Resolve `ServiceToken` cross-package type compatibility issues
- Fix type references in `editor-app` and `behavior-tree-editor`
---
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
### Features

View File

@@ -0,0 +1,570 @@
# Worker System
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
## Core Features
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
- **Type Safety**: Full TypeScript support and type checking
## Basic Usage
### Simple Physics System Example
```typescript
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true, // Enable Worker parallel processing
workerCount: 8, // Worker count, auto-limited to hardware capacity
entitiesPerWorker: 100, // Entities per Worker
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
entityDataSize: 7, // Data size per entity
maxEntities: 10000, // Maximum entity count
systemConfig: { // Configuration passed to Worker
gravity: 100,
friction: 0.95
}
});
}
// Data extraction: Convert Entity to serializable data
protected extractEntityData(entity: Entity): PhysicsData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: position.x,
y: position.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass,
radius: physics.radius
};
}
// Worker processing function: Pure function executed in Worker
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
// Apply gravity
entity.vy += config.gravity * deltaTime;
// Update position
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// Apply friction
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
// Apply results: Apply Worker processing results back to Entity
protected applyResult(entity: Entity, result: PhysicsData): void {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
position.x = result.x;
position.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer optimization support
protected getDefaultEntityDataSize(): number {
return 7; // id, x, y, vx, vy, mass, radius
}
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = entityData.id;
this.sharedFloatArray[offset + 1] = entityData.x;
this.sharedFloatArray[offset + 2] = entityData.y;
this.sharedFloatArray[offset + 3] = entityData.vx;
this.sharedFloatArray[offset + 4] = entityData.vy;
this.sharedFloatArray[offset + 5] = entityData.mass;
this.sharedFloatArray[offset + 6] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
vx: this.sharedFloatArray[offset + 3],
vy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6]
};
}
}
```
## Configuration Options
The Worker system supports rich configuration options:
```typescript
interface WorkerSystemConfig {
/** Enable Worker parallel processing */
enableWorker?: boolean;
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
workerCount?: number;
/** Entities per Worker for load distribution control */
entitiesPerWorker?: number;
/** System configuration data passed to Worker */
systemConfig?: any;
/** Enable SharedArrayBuffer optimization */
useSharedArrayBuffer?: boolean;
/** Float32 count per entity in SharedArrayBuffer */
entityDataSize?: number;
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
maxEntities?: number;
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
workerScriptPath?: string;
}
```
### Configuration Recommendations
```typescript
constructor() {
super(matcher, {
// Decide based on task complexity
enableWorker: this.shouldUseWorker(),
// Worker count: System auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
// Entities per Worker (optional)
entitiesPerWorker: 200, // Precise load distribution control
// Enable SharedArrayBuffer for many simple calculations
useSharedArrayBuffer: this.entityCount > 1000,
// Set according to actual data structure
entityDataSize: 8, // Ensure it matches data structure
// Estimated maximum entity count
maxEntities: 10000,
// Global configuration passed to Worker
systemConfig: {
gravity: 9.8,
friction: 0.95,
worldBounds: { width: 1920, height: 1080 }
}
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get system info
getSystemInfo() {
const info = this.getWorkerInfo();
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
console.log(`Current mode: ${info.currentMode}`);
}
```
## Processing Modes
The Worker system supports two processing modes:
### 1. Traditional Worker Mode
Data is serialized and passed between main thread and Workers:
```typescript
// Suitable for: Complex computation logic, moderate entity count
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: false, // Use traditional mode
workerCount: 2
});
}
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
// Complex algorithm logic
return entities.map(entity => {
// AI decisions, pathfinding, etc.
return this.complexAILogic(entity, deltaTime);
});
}
```
### 2. SharedArrayBuffer Mode
Zero-copy data sharing, suitable for many simple calculations:
```typescript
// Suitable for: Many entities with simple calculations
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: true, // Enable shared memory
entityDataSize: 6,
maxEntities: 10000
});
}
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
const entitySize = 6;
for (let i = startIndex; i < endIndex; i++) {
const offset = i * entitySize;
// Read data
let x = sharedFloatArray[offset];
let y = sharedFloatArray[offset + 1];
let vx = sharedFloatArray[offset + 2];
let vy = sharedFloatArray[offset + 3];
// Physics calculations
vy += config.gravity * deltaTime;
x += vx * deltaTime;
y += vy * deltaTime;
// Write back data
sharedFloatArray[offset] = x;
sharedFloatArray[offset + 1] = y;
sharedFloatArray[offset + 2] = vx;
sharedFloatArray[offset + 3] = vy;
}
};
}
```
## Use Cases
The Worker system is particularly suitable for:
### 1. Physics Simulation
- **Gravity systems**: Gravity calculations for many entities
- **Collision detection**: Complex collision algorithms
- **Fluid simulation**: Particle fluid systems
- **Cloth simulation**: Vertex physics calculations
### 2. AI Computation
- **Pathfinding**: A*, Dijkstra algorithms
- **Behavior trees**: Complex AI decision logic
- **Swarm intelligence**: Boid, fish school algorithms
- **Neural networks**: Simple AI inference
### 3. Data Processing
- **Bulk entity updates**: State machines, lifecycle management
- **Statistical calculations**: Game data analysis
- **Image processing**: Texture generation, effect calculations
- **Audio processing**: Sound synthesis, spectrum analysis
## Best Practices
### 1. Worker Function Requirements
```typescript
// Recommended: Worker processing function is a pure function
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
// Only use parameters and standard JavaScript APIs
return entities.map(entity => {
// Pure computation logic, no external state dependencies
entity.y += entity.velocity * deltaTime;
return entity;
});
}
// Avoid: Using external references in Worker function
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
// this and external variables are not available in Worker
return entities.map(entity => {
entity.y += this.someProperty; // Error
return entity;
});
}
```
### 2. Data Design
```typescript
// Recommended: Reasonable data design
interface SimplePhysicsData {
x: number;
y: number;
vx: number;
vy: number;
// Keep data structure simple for easy serialization
}
// Avoid: Complex nested objects
interface ComplexData {
transform: {
position: { x: number; y: number };
rotation: { angle: number };
};
// Complex nested structures increase serialization overhead
}
```
### 3. Worker Count Control
```typescript
// Recommended: Flexible Worker configuration
constructor() {
super(matcher, {
// Specify needed Worker count, system auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers
entitiesPerWorker: 100, // 100 entities per Worker
enableWorker: this.shouldUseWorker(), // Conditional enable
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get actual Worker info
checkWorkerConfiguration() {
const info = this.getWorkerInfo();
console.log(`Requested Workers: 8`);
console.log(`Actual Workers: ${info.workerCount}`);
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
}
```
### 4. Performance Monitoring
```typescript
// Recommended: Performance monitoring
public getPerformanceMetrics(): WorkerPerformanceMetrics {
return {
...this.getWorkerInfo(),
entityCount: this.entities.length,
averageProcessTime: this.getAverageProcessTime(),
workerUtilization: this.getWorkerUtilization()
};
}
```
## Performance Optimization Tips
### 1. Compute Intensity Assessment
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
### 2. Data Transfer Optimization
- Use SharedArrayBuffer to reduce serialization overhead
- Keep data structures simple and flat
- Avoid frequent large data transfers
### 3. Degradation Strategy
Always provide main thread fallback to ensure normal operation in environments without Worker support.
### 4. Memory Management
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
### 5. Load Balancing
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
## WeChat Mini Game Support
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
### WeChat Mini Game Worker Limitations
| Feature | Browser | WeChat Mini Game |
|---------|---------|------------------|
| Dynamic scripts (Blob URL) | Supported | Not supported |
| Worker count | Multiple | Maximum 1 |
| Script source | Any | Must be in code package |
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
### Using Worker Generator CLI
#### 1. Install the Tool
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. Configure workerScriptPath
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// Physics calculation logic
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... other methods
}
```
#### 3. Generate Worker Files
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
```bash
# Basic usage
npx esengine-worker-gen --src ./src --wechat
# Full options
npx esengine-worker-gen \
--src ./src \ # Source directory
--wechat \ # Generate WeChat Mini Game compatible code
--mapping \ # Generate worker-mapping.json
--verbose # Verbose output
```
The CLI tool will:
1. Scan source directory for all `WorkerEntitySystem` subclasses
2. Read each class's `workerScriptPath` configuration
3. Extract `workerProcess` method body
4. Convert to ES5 syntax (WeChat Mini Game compatible)
5. Generate to configured path
#### 4. Configure game.json
Configure workers directory in WeChat Mini Game's `game.json`:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. Project Structure
```
your-game/
├── game.js
├── game.json # Configure "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # Auto-generated
└── worker-mapping.json # Auto-generated
```
### Temporarily Disabling Workers
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
#### Method 1: Configuration Disable
```typescript
constructor() {
super(matcher, {
enableWorker: false, // Disable Worker, use main thread processing
// ...
});
}
```
#### Method 2: Platform Adapter Disable
Return Worker not supported in custom platform adapter:
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // Return false to disable Worker
}
// ...
}
```
### Important Notes
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
```typescript
// Correct: Only use parameters
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// Wrong: Using this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Cannot access this in Worker
return e;
});
}
```
3. **Pass configuration data via `systemConfig`**, not class properties
4. **Developer tool warnings can be ignored**:
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
## Online Demo
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
The demo showcases:
- Multi-threaded physics computation
- Real-time performance comparison
- SharedArrayBuffer optimization
- Parallel processing of many entities
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.

View File

@@ -6,409 +6,198 @@
## 特性支持
-**Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json
-**SharedArrayBuffer**: 不支持
-**Transferable Objects**: 不支持(只支持可序列化对象)
-**高精度时间**: 使用 `Date.now()``wx.getPerformance()`
-**设备信息**: 完整的微信小游戏设备信息
| 特性 | 支持情况 | 说明 |
|------|----------|------|
| **Worker** | ✅ 支持 | 需要使用预编译文件,配置 `workerScriptPath` |
| **SharedArrayBuffer** | ❌ 不支持 | 微信小游戏环境不支持 |
| **Transferable Objects** | ❌ 不支持 | 只支持可序列化对象 |
| **高精度时间** | ✅ 支持 | 使用 `wx.getPerformance()` |
| **设备信息** | ✅ 支持 | 完整的微信小游戏设备信息 |
## 完整实现
## WorkerEntitySystem 使用方式
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig,
WeChatDeviceInfo
} from '@esengine/ecs-framework';
### 重要:微信小游戏 Worker 限制
/**
* 微信小游戏平台适配器
* 支持微信小游戏环境
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
微信小游戏的 Worker 有以下限制:
- **Worker 脚本必须在代码包内**,不能动态生成
- **必须在 `game.json` 中配置** `workers` 目录
- **最多只能创建 1 个 Worker**
constructor() {
// 获取微信小游戏版本信息
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.version || 'unknown';
}
因此,使用 `WorkerEntitySystem` 时有两种方式:
1. **推荐:使用 CLI 工具自动生成** Worker 文件
2. 手动创建 Worker 文件
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
// 微信小游戏支持Worker通过wx.createWorker创建
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
### 方式一:使用 CLI 工具自动生成(推荐)
/**
* 检查是否支持SharedArrayBuffer不支持
*/
public isSharedArrayBufferSupported(): boolean {
return false; // 微信小游戏不支持SharedArrayBuffer
}
我们提供了 `@esengine/worker-generator` 工具,可以自动从你的 TypeScript 代码中提取 `workerProcess` 函数并生成微信小游戏兼容的 Worker 文件。
/**
* 获取硬件并发数
*/
public getHardwareConcurrency(): number {
// 微信小游戏官方限制:最多只能创建 1 个 Worker
return 1;
}
#### 安装
/**
* 创建Worker
* @param script 脚本内容或文件路径
* @param options Worker创建选项
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏不支持Worker');
}
```bash
pnpm add -D @esengine/worker-generator
# 或
npm install --save-dev @esengine/worker-generator
```
try {
return new WeChatWorker(script, options);
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
#### 使用
/**
* 创建SharedArrayBuffer不支持
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null; // 微信小游戏不支持SharedArrayBuffer
}
```bash
# 扫描 src 目录,生成 Worker 文件到 workers 目录
npx esengine-worker-gen --src ./src --out ./workers --wechat
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
// 尝试使用微信的性能API否则使用Date.now()
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
return performance.now();
}
return Date.now();
}
# 查看帮助
npx esengine-worker-gen --help
```
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
supportsModuleWorker: false, // 不支持模块Worker
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 微信小游戏限制eval使用
requiresWorkerInit: false,
memoryLimit: this.getMemoryLimit(),
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'创建新Worker前必须先调用 Worker.terminate()',
'Worker脚本必须为项目内相对路径',
'需要在 game.json 中配置 workers 路径',
'使用 worker.onMessage() 而不是 self.onmessage',
'需要基础库 1.9.90 及以上版本'
]
},
extensions: {
platform: 'wechat-minigame',
systemInfo: this.systemInfo,
appId: this.systemInfo.host?.appId || 'unknown'
}
};
}
#### 参数说明
/**
* 获取微信小游戏设备信息
*/
public getDeviceInfo(): WeChatDeviceInfo {
return {
// 设备基础信息
brand: this.systemInfo.brand,
model: this.systemInfo.model,
platform: this.systemInfo.platform,
system: this.systemInfo.system,
benchmarkLevel: this.systemInfo.benchmarkLevel,
cpuType: this.systemInfo.cpuType,
memorySize: this.systemInfo.memorySize,
deviceAbi: this.systemInfo.deviceAbi,
abi: this.systemInfo.abi,
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-s, --src <dir>` | 源代码目录 | `./src` |
| `-o, --out <dir>` | 输出目录 | `./workers` |
| `-w, --wechat` | 生成微信小游戏兼容代码 | `false` |
| `-m, --mapping` | 生成 worker-mapping.json | `true` |
| `-t, --tsconfig <path>` | TypeScript 配置文件路径 | 自动查找 |
| `-v, --verbose` | 详细输出 | `false` |
// 窗口信息
screenWidth: this.systemInfo.screenWidth,
screenHeight: this.systemInfo.screenHeight,
screenTop: this.systemInfo.screenTop,
windowWidth: this.systemInfo.windowWidth,
windowHeight: this.systemInfo.windowHeight,
pixelRatio: this.systemInfo.pixelRatio,
statusBarHeight: this.systemInfo.statusBarHeight,
safeArea: this.systemInfo.safeArea,
#### 示例输出
// 应用信息
version: this.systemInfo.version,
language: this.systemInfo.language,
theme: this.systemInfo.theme,
SDKVersion: this.systemInfo.SDKVersion,
enableDebug: this.systemInfo.enableDebug,
fontSizeSetting: this.systemInfo.fontSizeSetting,
host: this.systemInfo.host
};
}
```
🔧 ESEngine Worker Generator
/**
* 异步获取完整的平台配置
*/
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
// 可以在这里添加异步获取设备性能信息的逻辑
const baseConfig = this.getPlatformConfig();
Source directory: /project/src
Output directory: /project/workers
WeChat mode: Yes
// 尝试获取设备性能信息
try {
const benchmarkLevel = await this.getBenchmarkLevel();
baseConfig.extensions = {
...baseConfig.extensions,
benchmarkLevel
};
} catch (error) {
console.warn('获取性能基准失败:', error);
}
Scanning for WorkerEntitySystem classes...
return baseConfig;
}
✓ Found 1 WorkerEntitySystem class(es):
- PhysicsSystem (src/systems/PhysicsSystem.ts)
/**
* 检查是否支持Transferable Objects
*/
private checkTransferableObjectsSupport(): boolean {
// 微信小游戏不支持 Transferable Objects
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
return false;
}
Generating Worker files...
/**
* 获取系统信息
*/
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
return {};
}
}
return {};
}
✓ Successfully generated 1 Worker file(s):
- PhysicsSystem -> workers/physics-system-worker.js
/**
* 获取内存限制
*/
private getMemoryLimit(): number {
// 微信小游戏通常有内存限制
const memorySize = this.systemInfo.memorySize;
if (memorySize) {
// 解析内存大小字符串(如 "4GB"
const match = memorySize.match(/(\d+)([GM]B)?/i);
if (match) {
const value = parseInt(match[1], 10);
const unit = match[2]?.toUpperCase();
📝 Usage:
1. Copy the generated files to your project's workers/ directory
2. Configure game.json (WeChat): { "workers": "workers" }
3. In your System constructor, add:
workerScriptPath: 'workers/physics-system-worker.js'
```
if (unit === 'GB') {
return value * 1024 * 1024 * 1024;
} else if (unit === 'MB') {
return value * 1024 * 1024;
}
}
}
#### 在构建流程中集成
// 默认限制为512MB
return 512 * 1024 * 1024;
}
/**
* 异步获取设备性能基准
*/
private async getBenchmarkLevel(): Promise<number> {
return new Promise((resolve) => {
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res: any) => {
resolve(res.benchmarkLevel || 0);
},
fail: () => {
resolve(0);
}
});
} else {
resolve(this.systemInfo.benchmarkLevel || 0);
}
});
}
}
/**
* 微信Worker封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
private scriptPath: string;
private isTemporaryFile: boolean = false;
constructor(script: string, options: WorkerCreationOptions = {}) {
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
throw new Error('微信小游戏不支持Worker');
}
try {
// 判断 script 是文件路径还是脚本内容
if (this.isFilePath(script)) {
// 直接使用文件路径
this.scriptPath = script;
this.isTemporaryFile = false;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true // 启用实验性Worker获得更好性能
});
} else {
// 微信小游戏不支持动态脚本内容,只能使用文件路径
// 将脚本内容写入文件系统
this.scriptPath = this.writeScriptToFile(script, options.name);
this.isTemporaryFile = true;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true
});
}
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
/**
* 判断是否为文件路径
*/
private isFilePath(script: string): boolean {
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
return script.endsWith('.js') &&
!script.includes('\n') &&
!script.includes(';') &&
script.length < 200; // 文件路径通常不会太长
}
/**
* 将脚本内容写入文件系统
* 注意微信小游戏不支持blob URL只能使用文件系统
*/
private writeScriptToFile(script: string, name?: string): string {
const fs = wx.getFileSystemManager();
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
try {
fs.writeFileSync(filePath, script, 'utf8');
return filePath;
} catch (error) {
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
}
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
this.worker.postMessage(message);
} catch (error) {
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
// 清理临时脚本文件
this.cleanupScriptFile();
} catch (error) {
console.error('终止微信Worker失败:', error);
}
}
}
/**
* 清理临时脚本文件
*/
private cleanupScriptFile(): void {
// 只清理临时创建的文件,不清理用户提供的文件路径
if (this.scriptPath && this.isTemporaryFile) {
try {
const fs = wx.getFileSystemManager();
fs.unlinkSync(this.scriptPath);
} catch (error) {
console.warn('清理Worker脚本文件失败:', error);
}
}
```json
// package.json
{
"scripts": {
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
"build": "pnpm build:workers && your-build-command"
}
}
```
## 使用方法
### 方式二:手动创建 Worker 文件
### 1. 复制代码
如果你不想使用 CLI 工具,也可以手动创建 Worker 文件。
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`
在项目中创建 `workers/entity-worker.js`
### 2. 注册适配器
```javascript
// workers/entity-worker.js
// 微信小游戏 WorkerEntitySystem 通用 Worker 模板
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
let sharedFloatArray = null;
// 检查是否在微信小游戏环境
if (typeof wx !== 'undefined') {
const wechatAdapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(wechatAdapter);
worker.onMessage(function(e) {
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
try {
// 处理 SharedArrayBuffer 初始化
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id, result: null });
return;
}
// 传统处理方式
if (entities) {
const result = workerProcess(entities, deltaTime, systemConfig);
// 处理 Promise 返回值
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id, error: error.message });
});
} else {
worker.postMessage({ id, result: result });
}
}
} catch (error) {
worker.postMessage({ id, error: error.message });
}
});
/**
* 实体处理函数 - 根据你的业务逻辑修改此函数
* @param {Array} entities - 实体数据数组
* @param {number} deltaTime - 帧间隔时间
* @param {Object} systemConfig - 系统配置
* @returns {Array} 处理后的实体数据
*/
function workerProcess(entities, deltaTime, systemConfig) {
// ====== 在这里编写你的处理逻辑 ======
// 示例:物理计算
return entities.map(function(entity) {
// 应用重力
entity.vy += (systemConfig.gravity || 100) * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= (systemConfig.friction || 0.95);
entity.vy *= (systemConfig.friction || 0.95);
return entity;
});
}
/**
* SharedArrayBuffer 处理函数(可选)
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
// ====== 根据需要实现 SharedArrayBuffer 处理逻辑 ======
// 注意:微信小游戏不支持 SharedArrayBuffer此函数通常不会被调用
}
```
### 3. WorkerEntitySystem 使用方式
### 步骤 2配置 game.json
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建
`game.json` 中添加 workers 配置
#### 基本使用方式(推荐)
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers"
}
```
### 步骤 3使用 WorkerEntitySystem
```typescript
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
@@ -426,13 +215,17 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: 1, // 微信小游戏限制只能创建1个Worker
systemConfig: { gravity: 100, friction: 0.95 }
workerCount: 1, // 微信小游戏限制只能创建 1 个 Worker
workerScriptPath: 'workers/entity-worker.js', // 指定预编译的 Worker 文件
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected getDefaultEntityDataSize(): number {
return 6; // id, x, y, vx, vy, mass
return 6;
}
protected extractEntityData(entity: Entity): PhysicsData {
@@ -450,20 +243,15 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
};
}
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
// 注意:在微信小游戏中,此方法不会被使用
// Worker 的处理逻辑在 workers/entity-worker.js 中的 workerProcess 函数里
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
@@ -477,201 +265,219 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer 相关方法(微信小游戏不支持,可省略)
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
}
```
#### 使用预先创建的 Worker 文件(可选
### 临时禁用 Worker降级到同步模式
如果你希望使用预先创建的 Worker 文件
如果遇到问题,可以临时禁用 Worker
```typescript
// 1. 在 game.json 中配置 Worker 路径
/*
{
"workers": "workers"
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: false, // 禁用 Worker使用主线程同步处理
// ... 其他配置
});
}
}
*/
```
// 2. 创建 workers/physics.js 文件
// workers/physics.js 内容:
/*
// 微信小游戏 Worker 使用标准的 self.onmessage
self.onmessage = function(e) {
const { type, id, entities, deltaTime, systemConfig } = e.data;
## 完整适配器实现
if (entities) {
// 处理物理计算
const results = entities.map(entity => {
entity.vy += systemConfig.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 微信小游戏平台适配器
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
constructor() {
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.SDKVersion || 'unknown';
}
public isWorkerSupported(): boolean {
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
public isSharedArrayBufferSupported(): boolean {
return false;
}
public getHardwareConcurrency(): number {
return 1; // 微信小游戏最多 1 个 Worker
}
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏环境不支持 Worker');
}
// scriptPath 必须是代码包内的文件路径
const worker = wx.createWorker(scriptPath, {
useExperimentalWorker: true
});
self.postMessage({ id, result: results });
return new WeChatWorker(worker);
}
};
*/
// 3. 通过平台适配器直接创建不推荐WorkerEntitySystem会自动处理
const adapter = PlatformManager.getInstance().getAdapter();
const worker = adapter.createWorker('workers/physics.js');
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null;
}
public getHighResTimestamp(): number {
if (typeof wx !== 'undefined' && wx.getPerformance) {
return wx.getPerformance().now();
}
return Date.now();
}
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1,
supportsModuleWorker: false,
supportsTransferableObjects: false,
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 重要:标记不支持动态脚本
requiresWorkerInit: false,
memoryLimit: 512 * 1024 * 1024,
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'Worker 脚本必须在代码包内',
'需要在 game.json 中配置 workers 路径',
'需要使用 workerScriptPath 配置'
]
},
extensions: {
platform: 'wechat-minigame',
sdkVersion: this.systemInfo.SDKVersion
}
};
}
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
}
}
return {};
}
}
/**
* 微信 Worker 封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
constructor(worker: any) {
this.worker = worker;
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker 已被终止');
}
this.worker.postMessage(message);
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
this.worker.terminate();
this._state = 'terminated';
}
}
}
```
### 4. 获取设备信息
## 注册适配器
```typescript
const manager = PlatformManager.getInstance();
if (manager.hasAdapter()) {
const adapter = manager.getAdapter();
console.log('微信设备信息:', adapter.getDeviceInfo());
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
// 在游戏启动时注册适配器
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(adapter);
}
```
## 官方文档参考
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
## 重要注意事项
### Worker 限制和配置
### Worker 限制
微信小游戏的 Worker 有以下限制:
- **数量限制**: 最多只能创建 1 个 Worker
- **版本要求**: 需要基础库 1.9.90 及以上版本
- **脚本支持**: 不支持 blob URL只能使用文件路径或写入文件系统
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
#### Worker 配置(可选)
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers",
"subpackages": []
}
```
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
| 限制项 | 说明 |
|--------|------|
| 数量限制 | 最多只能创建 1 个 Worker |
| 版本要求 | 需要基础库 1.9.90 及以上 |
| 脚本位置 | 必须在代码包内,不支持动态生成 |
| 生命周期 | 创建新 Worker 前必须先 terminate() |
### 内存限制
微信小游戏有严格的内存限制:
- 通常限制在 256MB - 512MB
- 需要及时释放不用的资源
- 避免内存泄漏
### API 限制
- 不支持 `eval()` 函数
- 不支持 `Function` 构造器
- DOM API 受限
- 文件系统 API 受限
## 性能优化建议
### 1. 分帧处理
- 建议监听内存警告:
```typescript
class FramedProcessor {
private tasks: (() => void)[] = [];
private isProcessing = false;
public addTask(task: () => void): void {
this.tasks.push(task);
if (!this.isProcessing) {
this.processNextFrame();
}
}
private processNextFrame(): void {
this.isProcessing = true;
const startTime = Date.now();
const frameTime = 16; // 16ms per frame
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
const task = this.tasks.shift();
if (task) task();
}
if (this.tasks.length > 0) {
setTimeout(() => this.processNextFrame(), 0);
} else {
this.isProcessing = false;
}
}
}
```
### 2. 内存管理
```typescript
class MemoryManager {
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
public static checkMemoryUsage(): void {
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
const memoryInfo = performance.getEntries().find(
(entry: any) => entry.entryType === 'memory'
);
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
console.warn('内存使用率过高,建议清理资源');
// 触发垃圾回收或资源清理
}
}
}
}
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
```
## 调试技巧
```typescript
// 检查微信小游戏环境
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
// 检查 Worker 配置
const adapter = PlatformManager.getInstance().getAdapter();
const config = adapter.getPlatformConfig();
console.log('微信版本:', adapter.version);
console.log('设备信息:', adapter.getDeviceInfo());
console.log('平台配置:', adapter.getPlatformConfig());
// 检查功能支持
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
}
```
## 微信小游戏特殊API
```typescript
// 获取设备性能等级
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res) => {
console.log('设备性能等级:', res.benchmarkLevel);
}
});
}
// 监听内存警告
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
}
console.log('Worker 支持:', adapter.isWorkerSupported());
console.log('最大 Worker 数:', config.maxWorkerCount);
console.log('平台限制:', config.limitations);
```

View File

@@ -145,6 +145,8 @@ interface WorkerSystemConfig {
entityDataSize?: number;
/** 最大实体数量用于预分配SharedArrayBuffer */
maxEntities?: number;
/** 预编译的Worker脚本路径用于微信小游戏等不支持动态脚本的平台 */
workerScriptPath?: string;
}
```
@@ -605,4 +607,166 @@ public getPerformanceMetrics(): WorkerPerformanceMetrics {
- SharedArrayBuffer优化
- 大量实体的并行处理
## 微信小游戏支持
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。
### 微信小游戏 Worker 限制
| 特性 | 浏览器 | 微信小游戏 |
|------|--------|-----------|
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
| Worker 数量 | 多个 | 最多 1 个 |
| 脚本来源 | 任意 | 必须是代码包内文件 |
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
### 使用 Worker Generator CLI
#### 1. 安装工具
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. 配置 workerScriptPath
在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// 物理计算逻辑
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... 其他方法
}
```
#### 3. 生成 Worker 文件
运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件:
```bash
# 基本用法
npx esengine-worker-gen --src ./src --wechat
# 完整选项
npx esengine-worker-gen \
--src ./src \ # 源码目录
--wechat \ # 生成微信小游戏兼容代码
--mapping \ # 生成 worker-mapping.json
--verbose # 详细输出
```
CLI 工具会:
1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类
2. 读取每个类的 `workerScriptPath` 配置
3. 提取 `workerProcess` 方法体
4. 转换为 ES5 语法(微信小游戏兼容)
5. 生成到配置的路径
#### 4. 配置 game.json
在微信小游戏的 `game.json` 中配置 workers 目录:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. 项目结构
```
your-game/
├── game.js
├── game.json # 配置 "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # 自动生成
└── worker-mapping.json # 自动生成
```
### 临时禁用 Worker
如果需要临时禁用 Worker例如调试时有两种方式
#### 方式 1配置禁用
```typescript
constructor() {
super(matcher, {
enableWorker: false, // 禁用 Worker使用主线程处理
// ...
});
}
```
#### 方式 2平台适配器禁用
在自定义平台适配器中返回不支持 Worker
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // 返回 false 禁用 Worker
}
// ...
}
```
### 注意事项
1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件
2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量:
```typescript
// ✅ 正确:只使用参数
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// ❌ 错误:使用 this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Worker 中无法访问 this
return e;
});
}
```
3. **配置数据通过 `systemConfig` 传递**,而不是类属性
4. **开发者工具中的警告可以忽略**
- `getNetworkType:fail not support` - 微信开发者工具内部行为
- `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现
Worker系统为ECS框架提供了强大的并行计算能力让你能够充分利用现代多核处理器的性能为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。

View File

@@ -123,7 +123,9 @@ class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
enableWorker,
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true
useSharedArrayBuffer: true,
// 微信小游戏等平台需要配置此路径CLI 工具会根据此路径生成 Worker 文件
workerScriptPath: 'workers/physics-worker.js'
}
);

View File

@@ -0,0 +1,277 @@
/**
* Auto-generated Worker file for PhysicsWorkerSystem
* 自动生成的 Worker 文件
*
* Source: F:/ecs-framework/examples/core-demos/src/demos/WorkerSystemDemo.ts
* Generated by @esengine/worker-generator
*
* 使用方式 | Usage:
* 1. 将此文件放入 workers/ 目录
* 2. 在 game.json 中配置 "workers": "workers"
* 3. 在 System 中配置 workerScriptPath: 'workers/physics-worker-system-worker.js'
*/
// 微信小游戏 Worker 环境
// WeChat Mini Game Worker environment
let sharedFloatArray = null;
const ENTITY_DATA_SIZE = 9;
worker.onMessage(function(res) {
// 微信小游戏 Worker 消息直接传递数据,不需要 .data
// WeChat Mini Game Worker passes data directly, no .data wrapper
var type = res.type;
var id = res.id;
var entities = res.entities;
var deltaTime = res.deltaTime;
var systemConfig = res.systemConfig;
var startIndex = res.startIndex;
var endIndex = res.endIndex;
var sharedBuffer = res.sharedBuffer;
try {
// 处理 SharedArrayBuffer 初始化
// Handle SharedArrayBuffer initialization
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
// Handle SharedArrayBuffer data
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id: id, result: null });
return;
}
// 传统处理方式
// Traditional processing
if (entities) {
var result = workerProcess(entities, deltaTime, systemConfig);
// 处理 Promise 返回值
// Handle Promise return value
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id: id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id: id, error: error.message });
});
} else {
worker.postMessage({ id: id, result: result });
}
}
} catch (error) {
worker.postMessage({ id: id, error: error.message });
}
});
/**
* 实体处理函数 - 从 PhysicsWorkerSystem.workerProcess 提取
* Entity processing function - extracted from PhysicsWorkerSystem.workerProcess
*/
function workerProcess(entities, deltaTime, systemConfig) {
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var config = systemConfig || this.physicsConfig;
var result = entities.map(function (e) { return (__assign({}, e)); });
for (var i = 0; i < result.length; i++) {
var entity = result[i];
entity.dy += config.gravity * deltaTime;
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
}
else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= config.groundFriction;
}
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
for (var i = 0; i < result.length; i++) {
for (var j = i + 1; j < result.length; j++) {
var ball1 = result[i];
var ball2 = result[j];
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
var nx = dx / distance;
var ny = dy / distance;
var overlap = minDistance - distance;
var separationX = nx * overlap * 0.5;
var separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
var relativeVelocityX = ball2.dx - ball1.dx;
var relativeVelocityY = ball2.dy - ball1.dy;
var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0)
continue;
var restitution = (ball1.bounce + ball2.bounce) * 0.5;
var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / ball1.mass + 1 / ball2.mass);
var impulseX = impulseScalar * nx;
var impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
var energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
/**
* SharedArrayBuffer 处理函数
* SharedArrayBuffer processing function
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
var config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
var actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) {
var offset = i * 9 + 9;
var id = sharedFloatArray[offset + 0];
if (id === 0)
continue;
var x = sharedFloatArray[offset + 1];
var y = sharedFloatArray[offset + 2];
var dx = sharedFloatArray[offset + 3];
var dy = sharedFloatArray[offset + 4];
var bounce = sharedFloatArray[offset + 6];
var friction = sharedFloatArray[offset + 7];
var radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞
if (x <= radius) {
x = radius;
dx = -dx * bounce;
}
else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
}
else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 碰撞检测
for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) {
var offset1 = i * 9 + 9;
var id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0)
continue;
var x1 = sharedFloatArray[offset1 + 1];
var y1 = sharedFloatArray[offset1 + 2];
var dx1 = sharedFloatArray[offset1 + 3];
var dy1 = sharedFloatArray[offset1 + 4];
var mass1 = sharedFloatArray[offset1 + 5];
var bounce1 = sharedFloatArray[offset1 + 6];
var radius1 = sharedFloatArray[offset1 + 8];
for (var j = 0; j < actualEntityCount; j++) {
if (i === j)
continue;
var offset2 = j * 9 + 9;
var id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0)
continue;
var x2 = sharedFloatArray[offset2 + 1];
var y2 = sharedFloatArray[offset2 + 2];
var dx2 = sharedFloatArray[offset2 + 3];
var dy2 = sharedFloatArray[offset2 + 4];
var mass2 = sharedFloatArray[offset2 + 5];
var bounce2 = sharedFloatArray[offset2 + 6];
var radius2 = sharedFloatArray[offset2 + 8];
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0)
continue;
var deltaX = x2 - x1;
var deltaY = y2 - y1;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var minDistance = radius1 + radius2;
if (distance < minDistance && distance > 0) {
var nx = deltaX / distance;
var ny = deltaY / distance;
var overlap = minDistance - distance;
var separationX = nx * overlap * 0.5;
var separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
var relativeVelocityX = dx2 - dx1;
var relativeVelocityY = dy2 - dy1;
var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0)
continue;
var restitution = (bounce1 + bounce2) * 0.5;
var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / mass1 + 1 / mass2);
var impulseX = impulseScalar * nx;
var impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
var energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
}

View File

@@ -0,0 +1,6 @@
{
"generatedAt": "2025-12-08T08:57:32.415Z",
"mappings": {
"PhysicsWorkerSystem": "physics-worker.js"
}
}

View File

@@ -34,6 +34,20 @@ export interface WorkerSystemConfig {
entityDataSize?: number;
/** 最大实体数量用于预分配SharedArrayBuffer */
maxEntities?: number;
/**
* 预编译 Worker 脚本路径(微信小游戏等不支持动态脚本的平台必需)
* Pre-compiled Worker script path (required for platforms like WeChat Mini Game that don't support dynamic scripts)
*
* @example
* ```typescript
* // 微信小游戏使用方式:
* // 1. 创建 Worker 文件: workers/physics-worker.js
* // 2. 在 game.json 配置 "workers": "workers"
* // 3. 指定路径:
* workerScriptPath: 'workers/physics-worker.js'
* ```
*/
workerScriptPath?: string;
}
@@ -188,9 +202,10 @@ export type SharedArrayBufferProcessFunction = (
* ```
*/
export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem {
protected config: Required<Omit<WorkerSystemConfig, 'systemConfig' | 'entitiesPerWorker'>> & {
protected config: Required<Omit<WorkerSystemConfig, 'systemConfig' | 'entitiesPerWorker' | 'workerScriptPath'>> & {
systemConfig?: any;
entitiesPerWorker?: number;
workerScriptPath?: string;
};
private workerPool: PlatformWorkerPool | null = null;
private isProcessing = false;
@@ -222,7 +237,8 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
...(config.entitiesPerWorker !== undefined && { entitiesPerWorker: config.entitiesPerWorker }),
useSharedArrayBuffer: config.useSharedArrayBuffer ?? this.isSharedArrayBufferSupported(),
entityDataSize: config.entityDataSize ?? this.getDefaultEntityDataSize(),
maxEntities: config.maxEntities ?? 10000
maxEntities: config.maxEntities ?? 10000,
...(config.workerScriptPath !== undefined && { workerScriptPath: config.workerScriptPath })
};
@@ -300,16 +316,34 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
*/
private initializeWorkerPool(): void {
try {
const script = this.createWorkerScript();
// 在WorkerEntitySystem中处理平台相关逻辑
const workers: PlatformWorker[] = [];
const platformConfig = this.platformAdapter.getPlatformConfig();
const fullScript = (platformConfig.workerScriptPrefix || '') + script;
const workers: PlatformWorker[] = [];
// 判断使用外部脚本路径还是动态生成脚本
// Determine whether to use external script path or dynamically generated script
let scriptOrPath: string;
if (this.config.workerScriptPath) {
// 使用预编译的外部 Worker 文件(微信小游戏等平台)
// Use pre-compiled external Worker file (for WeChat Mini Game, etc.)
scriptOrPath = this.config.workerScriptPath;
this.logger.info(`${this.systemName}: 使用外部Worker文件: ${scriptOrPath}`);
} else if (platformConfig.limitations?.noEval) {
// 平台不支持动态脚本,且未提供外部脚本路径
// Platform doesn't support dynamic scripts and no external script path provided
this.logger.error(`${this.systemName}: 当前平台不支持动态Worker脚本请配置 workerScriptPath 指定预编译的Worker文件`);
this.config.enableWorker = false;
return;
} else {
// 动态生成 Worker 脚本(浏览器等支持的平台)
// Dynamically generate Worker script (for browsers and other supported platforms)
const script = this.createWorkerScript();
scriptOrPath = (platformConfig.workerScriptPrefix || '') + script;
}
for (let i = 0; i < this.config.workerCount; i++) {
try {
const worker = this.platformAdapter.createWorker(fullScript, {
const worker = this.platformAdapter.createWorker(scriptOrPath, {
name: `WorkerEntitySystem-${i}`
});
workers.push(worker);

View File

@@ -0,0 +1,137 @@
# @esengine/worker-generator
CLI tool to generate Worker files from `WorkerEntitySystem` classes for WeChat Mini Game and other platforms that don't support dynamic Worker scripts.
## Why This Tool?
WeChat Mini Game has strict Worker limitations:
- Cannot create Workers from Blob URLs or dynamic scripts
- Worker scripts must be pre-compiled files in the code package
- Maximum 1 Worker allowed
This tool extracts your `workerProcess` method and generates compatible Worker files automatically.
## Installation
```bash
npm install -D @esengine/worker-generator
# or
pnpm add -D @esengine/worker-generator
```
## Usage
### 1. Configure your WorkerEntitySystem
Add `workerScriptPath` to specify where the Worker file should be generated:
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // Output path
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
}
```
### 2. Run the Generator
```bash
# Basic usage
npx esengine-worker-gen --src ./src --wechat
# Full options
npx esengine-worker-gen \
--src ./src \ # Source directory to scan
--out ./workers \ # Default output directory (if no workerScriptPath)
--wechat \ # Generate WeChat Mini Game compatible code (ES5)
--mapping \ # Generate worker-mapping.json
--verbose # Verbose output
```
### 3. Configure game.json (WeChat Mini Game)
```json
{
"workers": "workers"
}
```
## CLI Options
| Option | Description | Default |
|--------|-------------|---------|
| `-s, --src <dir>` | Source directory to scan | `./src` |
| `-o, --out <dir>` | Output directory for Worker files | `./workers` |
| `-w, --wechat` | Generate WeChat Mini Game compatible code | `false` |
| `-m, --mapping` | Generate worker-mapping.json file | `true` |
| `-t, --tsconfig <path>` | Path to tsconfig.json | Auto-detect |
| `-v, --verbose` | Verbose output | `false` |
## Output
The tool generates:
1. **Worker files** - JavaScript files containing the extracted `workerProcess` logic
2. **worker-mapping.json** - Mapping of class names to Worker file paths
Example output:
```
workers/
├── physics-worker.js
└── worker-mapping.json
```
## Important Notes
1. **Pure Functions**: Your `workerProcess` must be a pure function - it cannot use `this` or external variables
```typescript
// Correct
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime; // Use config parameter
return e;
});
}
// Wrong
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Cannot access this!
return e;
});
}
```
2. **Re-run after changes**: Run the generator again after modifying `workerProcess`
3. **ES5 Conversion**: When using `--wechat`, the tool converts:
- Arrow functions → regular functions
- `const`/`let` → `var`
- Spread operator → `Object.assign`
- Template literals → string concatenation
## License
MIT

View File

@@ -0,0 +1,56 @@
{
"name": "@esengine/worker-generator",
"version": "1.0.0",
"description": "CLI tool to generate Worker files from WorkerEntitySystem classes for WeChat Mini Game and other platforms",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"esengine-worker-gen": "./dist/cli.js"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"type-check": "tsc --noEmit",
"prepublishOnly": "pnpm run build"
},
"keywords": [
"esengine",
"ecs",
"worker",
"web-worker",
"wechat",
"minigame",
"code-generator",
"cli"
],
"author": "ESEngine Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/worker-generator"
},
"homepage": "https://github.com/esengine/ecs-framework/tree/master/packages/worker-generator",
"bugs": {
"url": "https://github.com/esengine/ecs-framework/issues"
},
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^11.1.0",
"ts-morph": "^21.0.1"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,172 @@
#!/usr/bin/env node
/**
* Worker Generator CLI
* 从 WorkerEntitySystem 子类生成 Worker 文件
* Generate Worker files from WorkerEntitySystem subclasses
*/
import { Command } from 'commander';
import chalk from 'chalk';
import * as path from 'path';
import * as fs from 'fs';
import { parseWorkerSystems } from './parser';
import { generateWorkerFiles } from './generator';
import type { GeneratorConfig } from './types';
const packageJson = require('../package.json');
const program = new Command();
program
.name('esengine-worker-gen')
.description('Generate Worker files from WorkerEntitySystem classes for WeChat Mini Game and other platforms')
.version(packageJson.version);
program
.option('-s, --src <dir>', 'Source directory to scan', './src')
.option('-o, --out <dir>', 'Output directory for Worker files', './workers')
.option('-w, --wechat', 'Generate WeChat Mini Game compatible code', false)
.option('-m, --mapping', 'Generate worker-mapping.json file', true)
.option('-t, --tsconfig <path>', 'Path to tsconfig.json')
.option('-v, --verbose', 'Verbose output', false)
.action((options) => {
run(options);
});
function run(options: {
src: string;
out: string;
wechat: boolean;
mapping: boolean;
tsconfig?: string;
verbose: boolean;
}) {
console.log(chalk.cyan('\n🔧 ESEngine Worker Generator\n'));
// 解析路径
// Resolve paths
const srcDir = path.resolve(process.cwd(), options.src);
const outDir = path.resolve(process.cwd(), options.out);
// 检查源目录是否存在
// Check if source directory exists
if (!fs.existsSync(srcDir)) {
console.error(chalk.red(`Error: Source directory not found: ${srcDir}`));
process.exit(1);
}
// 查找 tsconfig.json
// Find tsconfig.json
let tsConfigPath = options.tsconfig;
if (!tsConfigPath) {
const defaultTsConfig = path.join(process.cwd(), 'tsconfig.json');
if (fs.existsSync(defaultTsConfig)) {
tsConfigPath = defaultTsConfig;
}
}
const config: GeneratorConfig = {
srcDir,
outDir,
wechat: options.wechat,
generateMapping: options.mapping,
tsConfigPath,
verbose: options.verbose,
};
console.log(chalk.gray(`Source directory: ${srcDir}`));
console.log(chalk.gray(`Output directory: ${outDir}`));
console.log(chalk.gray(`WeChat mode: ${options.wechat ? 'Yes' : 'No'}`));
if (tsConfigPath) {
console.log(chalk.gray(`TypeScript config: ${tsConfigPath}`));
}
console.log();
// 解析源文件
// Parse source files
console.log(chalk.yellow('Scanning for WorkerEntitySystem classes...'));
const systems = parseWorkerSystems(config);
if (systems.length === 0) {
console.log(chalk.yellow('\n⚠ No WorkerEntitySystem subclasses found.'));
console.log(chalk.gray('Make sure your classes:'));
console.log(chalk.gray(' - Extend WorkerEntitySystem'));
console.log(chalk.gray(' - Have a workerProcess method'));
console.log(chalk.gray(' - Are in .ts files under the source directory'));
return;
}
console.log(chalk.green(`\n✓ Found ${systems.length} WorkerEntitySystem class(es):`));
for (const system of systems) {
const configStatus = system.workerScriptPath
? chalk.green(`✓ workerScriptPath: '${system.workerScriptPath}'`)
: chalk.yellow('⚠ No workerScriptPath configured');
console.log(chalk.gray(` - ${system.className}`));
console.log(chalk.gray(` ${path.relative(process.cwd(), system.filePath)}`));
console.log(` ${configStatus}`);
}
console.log();
// 生成 Worker 文件
// Generate Worker files
console.log(chalk.yellow('Generating Worker files...'));
const result = generateWorkerFiles(systems, config);
// 输出结果
// Output results
console.log();
if (result.success.length > 0) {
console.log(chalk.green(`✓ Successfully generated ${result.success.length} Worker file(s):`));
for (const item of result.success) {
const relativePath = path.relative(process.cwd(), item.outputPath).replace(/\\/g, '/');
if (item.configuredPath) {
console.log(chalk.green(`${item.className} -> ${relativePath}`));
} else {
console.log(chalk.yellow(`${item.className} -> ${relativePath} (需要配置 workerScriptPath)`));
}
}
}
if (result.errors.length > 0) {
console.log(chalk.red(`\n✗ Failed to generate ${result.errors.length} Worker file(s):`));
for (const item of result.errors) {
console.log(chalk.red(` - ${item.className}: ${item.error}`));
}
}
// 提示未配置 workerScriptPath 的类
// Remind about classes without workerScriptPath
if (result.skipped.length > 0) {
console.log(chalk.yellow('\n⚠ 以下类未配置 workerScriptPath请在构造函数中添加配置:'));
console.log(chalk.yellow(' The following classes need workerScriptPath configuration:\n'));
for (const item of result.skipped) {
console.log(chalk.white(` // ${item.className}`));
console.log(chalk.cyan(` super(matcher, {`));
console.log(chalk.cyan(` workerScriptPath: '${item.suggestedPath}',`));
console.log(chalk.cyan(` // ... 其他配置`));
console.log(chalk.cyan(` });`));
console.log();
}
}
// 使用提示(只有当有已配置路径的成功项时)
// Usage tips (only when there are success items with configured path)
const configuredSuccess = result.success.filter(item => item.configuredPath);
if (configuredSuccess.length > 0) {
console.log(chalk.green('\n✅ 已按照代码中的 workerScriptPath 配置生成 Worker 文件!'));
console.log(chalk.gray(' Worker files generated according to workerScriptPath in your code!'));
console.log(chalk.gray('\n 下一步 | Next steps:'));
console.log(chalk.gray(' 1. 确保 game.json 配置了 workers 目录'));
console.log(chalk.gray(' Ensure game.json has workers directory configured'));
if (options.mapping) {
console.log(chalk.gray('\n 已生成映射文件 | Mapping file generated:'));
console.log(chalk.white(` import mapping from '${path.relative(process.cwd(), outDir)}/worker-mapping.json'`));
}
}
console.log();
}
program.parse();

View File

@@ -0,0 +1,325 @@
/**
* Worker 文件生成器
* Worker file generator
*/
import * as fs from 'fs';
import * as path from 'path';
import type { WorkerSystemInfo, GeneratorConfig, GenerationResult, WorkerScriptMapping } from './types';
/**
* 生成 Worker 文件
* Generate Worker files
*/
export function generateWorkerFiles(
systems: WorkerSystemInfo[],
config: GeneratorConfig
): GenerationResult {
const result: GenerationResult = {
success: [],
errors: [],
skipped: [],
};
for (const system of systems) {
try {
// 优先使用用户配置的 workerScriptPath
// Prefer user-configured workerScriptPath
let outputPath: string;
if (system.workerScriptPath) {
// 用户已配置路径,使用该路径(相对于项目根目录)
// User has configured path, use it (relative to project root)
outputPath = path.resolve(process.cwd(), system.workerScriptPath);
if (config.verbose) {
console.log(` Using configured workerScriptPath: ${system.workerScriptPath}`);
}
} else {
// 未配置,使用默认输出目录
// Not configured, use default output directory
// 确保输出目录存在
if (!fs.existsSync(config.outDir)) {
fs.mkdirSync(config.outDir, { recursive: true });
}
const outputFileName = `${toKebabCase(system.className)}-worker.js`;
outputPath = path.join(config.outDir, outputFileName);
// 提示用户需要配置 workerScriptPath
// Remind user to configure workerScriptPath
result.skipped.push({
className: system.className,
suggestedPath: path.relative(process.cwd(), outputPath).replace(/\\/g, '/'),
reason: 'No workerScriptPath configured',
});
}
// 确保输出目录存在
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const workerCode = config.wechat
? generateWeChatWorkerCode(system)
: generateStandardWorkerCode(system);
fs.writeFileSync(outputPath, workerCode, 'utf8');
result.success.push({
className: system.className,
outputPath: outputPath,
configuredPath: system.workerScriptPath,
});
if (config.verbose) {
console.log(` Generated: ${outputPath}`);
}
} catch (error) {
result.errors.push({
className: system.className,
filePath: system.filePath,
error: error instanceof Error ? error.message : String(error),
});
}
}
// 生成映射文件
// Generate mapping file
if (config.generateMapping && result.success.length > 0) {
generateMappingFile(result.success, config);
}
return result;
}
/**
* 生成微信小游戏 Worker 代码
* Generate WeChat Mini Game Worker code
*/
function generateWeChatWorkerCode(system: WorkerSystemInfo): string {
const { workerProcessBody, workerProcessParams, sharedBufferProcessBody, entityDataSize } = system;
return `/**
* Auto-generated Worker file for ${system.className}
* 自动生成的 Worker 文件
*
* Source: ${system.filePath}
* Generated by @esengine/worker-generator
*
* 使用方式 | Usage:
* 1. 将此文件放入 workers/ 目录
* 2. 在 game.json 中配置 "workers": "workers"
* 3. 在 System 中配置 workerScriptPath: 'workers/${toKebabCase(system.className)}-worker.js'
*/
// 微信小游戏 Worker 环境
// WeChat Mini Game Worker environment
let sharedFloatArray = null;
const ENTITY_DATA_SIZE = ${entityDataSize || 8};
worker.onMessage(function(res) {
// 微信小游戏 Worker 消息直接传递数据,不需要 .data
// WeChat Mini Game Worker passes data directly, no .data wrapper
var type = res.type;
var id = res.id;
var entities = res.entities;
var deltaTime = res.deltaTime;
var systemConfig = res.systemConfig;
var startIndex = res.startIndex;
var endIndex = res.endIndex;
var sharedBuffer = res.sharedBuffer;
try {
// 处理 SharedArrayBuffer 初始化
// Handle SharedArrayBuffer initialization
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
// Handle SharedArrayBuffer data
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id: id, result: null });
return;
}
// 传统处理方式
// Traditional processing
if (entities) {
var result = workerProcess(entities, deltaTime, systemConfig);
// 处理 Promise 返回值
// Handle Promise return value
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id: id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id: id, error: error.message });
});
} else {
worker.postMessage({ id: id, result: result });
}
}
} catch (error) {
worker.postMessage({ id: id, error: error.message });
}
});
/**
* 实体处理函数 - 从 ${system.className}.workerProcess 提取
* Entity processing function - extracted from ${system.className}.workerProcess
*/
function workerProcess(${workerProcessParams.entities}, ${workerProcessParams.deltaTime}, ${workerProcessParams.config}) {
${convertToES5(workerProcessBody)}
}
/**
* SharedArrayBuffer 处理函数
* SharedArrayBuffer processing function
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
${sharedBufferProcessBody ? convertToES5(sharedBufferProcessBody) : '// No SharedArrayBuffer processing defined'}
}
`;
}
/**
* 生成标准 Worker 代码(用于浏览器等环境)
* Generate standard Worker code (for browsers, etc.)
*/
function generateStandardWorkerCode(system: WorkerSystemInfo): string {
const { workerProcessBody, workerProcessParams, sharedBufferProcessBody, entityDataSize } = system;
return `/**
* Auto-generated Worker file for ${system.className}
* 自动生成的 Worker 文件
*
* Source: ${system.filePath}
* Generated by @esengine/worker-generator
*/
let sharedFloatArray = null;
const ENTITY_DATA_SIZE = ${entityDataSize || 8};
self.onmessage = function(e) {
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
try {
// 处理 SharedArrayBuffer 初始化
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
self.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
self.postMessage({ id, result: null });
return;
}
// 传统处理方式
if (entities) {
const result = workerProcess(entities, deltaTime, systemConfig);
if (result && typeof result.then === 'function') {
result.then(finalResult => {
self.postMessage({ id, result: finalResult });
}).catch(error => {
self.postMessage({ id, error: error.message });
});
} else {
self.postMessage({ id, result });
}
}
} catch (error) {
self.postMessage({ id, error: error.message });
}
};
/**
* Entity processing function - extracted from ${system.className}.workerProcess
*/
function workerProcess(${workerProcessParams.entities}, ${workerProcessParams.deltaTime}, ${workerProcessParams.config}) {
${workerProcessBody}
}
/**
* SharedArrayBuffer processing function
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
${sharedBufferProcessBody || '// No SharedArrayBuffer processing defined'}
}
`;
}
/**
* 生成映射文件
* Generate mapping file
*/
function generateMappingFile(
success: Array<{ className: string; outputPath: string }>,
config: GeneratorConfig
): void {
const mapping: WorkerScriptMapping = {
generatedAt: new Date().toISOString(),
mappings: {},
};
for (const item of success) {
// 使用相对于输出目录的路径
// Use path relative to output directory
const relativePath = path.relative(config.outDir, item.outputPath).replace(/\\/g, '/');
mapping.mappings[item.className] = relativePath;
}
const mappingPath = path.join(config.outDir, 'worker-mapping.json');
fs.writeFileSync(mappingPath, JSON.stringify(mapping, null, 2), 'utf8');
if (config.verbose) {
console.log(` Generated mapping: ${mappingPath}`);
}
}
/**
* 将 camelCase/PascalCase 转换为 kebab-case
* Convert camelCase/PascalCase to kebab-case
*/
function toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
.toLowerCase();
}
/**
* ES6+ 到 ES5 转换(用于微信小游戏兼容性)
* ES6+ to ES5 conversion (for WeChat Mini Game compatibility)
*
* 使用 TypeScript 编译器进行安全的代码转换
* Uses TypeScript compiler for safe code transformation
*/
function convertToES5(code: string): string {
// 使用 ts-morph 已有的 TypeScript 依赖进行转换
// Use ts-morph's TypeScript dependency for transformation
const ts = require('typescript');
const result = ts.transpileModule(code, {
compilerOptions: {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.None,
removeComments: false,
// 不生成严格模式声明
noImplicitUseStrict: true,
}
});
return result.outputText;
}

View File

@@ -0,0 +1,41 @@
/**
* @esengine/worker-generator
*
* CLI tool to generate Worker files from WorkerEntitySystem classes
* for WeChat Mini Game and other platforms that require pre-compiled Worker scripts.
*
* 从 WorkerEntitySystem 子类生成 Worker 文件的 CLI 工具
* 用于微信小游戏等需要预编译 Worker 脚本的平台
*
* @example
* ```bash
* # CLI 使用 | CLI Usage
* npx esengine-worker-gen --src ./src --out ./workers --wechat
*
* # 或者 | Or
* pnpm esengine-worker-gen -s ./src -o ./workers -w
* ```
*
* @example
* ```typescript
* // API 使用 | API Usage
* import { parseWorkerSystems, generateWorkerFiles } from '@esengine/worker-generator';
*
* const systems = parseWorkerSystems({
* srcDir: './src',
* outDir: './workers',
* wechat: true,
* });
*
* const result = generateWorkerFiles(systems, config);
* ```
*/
export { parseWorkerSystems } from './parser';
export { generateWorkerFiles } from './generator';
export type {
WorkerSystemInfo,
GeneratorConfig,
GenerationResult,
WorkerScriptMapping,
} from './types';

View File

@@ -0,0 +1,273 @@
/**
* AST 解析器 - 提取 WorkerEntitySystem 子类信息
* AST Parser - Extract WorkerEntitySystem subclass information
*/
import { Project, SyntaxKind, ClassDeclaration, MethodDeclaration, Node } from 'ts-morph';
import * as path from 'path';
import type { WorkerSystemInfo, GeneratorConfig } from './types';
/**
* 解析项目中的 WorkerEntitySystem 子类
* Parse WorkerEntitySystem subclasses in the project
*/
export function parseWorkerSystems(config: GeneratorConfig): WorkerSystemInfo[] {
const project = new Project({
tsConfigFilePath: config.tsConfigPath,
skipAddingFilesFromTsConfig: true,
});
// 添加源文件
// Add source files
const globPattern = path.join(config.srcDir, '**/*.ts').replace(/\\/g, '/');
project.addSourceFilesAtPaths(globPattern);
const results: WorkerSystemInfo[] = [];
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
// 跳过 node_modules 和 .d.ts 文件
// Skip node_modules and .d.ts files
if (filePath.includes('node_modules') || filePath.endsWith('.d.ts')) {
continue;
}
for (const classDecl of sourceFile.getClasses()) {
const info = extractWorkerSystemInfo(classDecl, filePath, config.verbose);
if (info) {
results.push(info);
}
}
}
return results;
}
/**
* 检查类是否继承自 WorkerEntitySystem
* Check if class extends WorkerEntitySystem
*/
function isWorkerEntitySystemSubclass(classDecl: ClassDeclaration): boolean {
const extendsClause = classDecl.getExtends();
if (!extendsClause) {
return false;
}
const extendsText = extendsClause.getText();
// 直接检查是否继承 WorkerEntitySystem
// Directly check if extends WorkerEntitySystem
if (extendsText.startsWith('WorkerEntitySystem')) {
return true;
}
// 递归检查基类(如果需要)
// Recursively check base class (if needed)
// 这里简化处理,只检查直接继承
// Simplified: only check direct inheritance
return false;
}
/**
* 提取 WorkerEntitySystem 子类信息
* Extract WorkerEntitySystem subclass information
*/
function extractWorkerSystemInfo(
classDecl: ClassDeclaration,
filePath: string,
verbose?: boolean
): WorkerSystemInfo | null {
if (!isWorkerEntitySystemSubclass(classDecl)) {
return null;
}
const className = classDecl.getName();
if (!className) {
return null;
}
if (verbose) {
console.log(` Found WorkerEntitySystem: ${className} in ${filePath}`);
}
// 查找 workerProcess 方法
// Find workerProcess method
const workerProcessMethod = classDecl.getMethod('workerProcess');
if (!workerProcessMethod) {
if (verbose) {
console.log(` Warning: No workerProcess method found in ${className}`);
}
return null;
}
// 提取方法体
// Extract method body
const workerProcessBody = extractMethodBody(workerProcessMethod);
if (!workerProcessBody) {
if (verbose) {
console.log(` Warning: Could not extract workerProcess body from ${className}`);
}
return null;
}
// 提取参数名
// Extract parameter names
const params = workerProcessMethod.getParameters();
const workerProcessParams = {
entities: params[0]?.getName() || 'entities',
deltaTime: params[1]?.getName() || 'deltaTime',
config: params[2]?.getName() || 'config',
};
// 尝试提取 getSharedArrayBufferProcessFunction
// Try to extract getSharedArrayBufferProcessFunction
let sharedBufferProcessBody: string | undefined;
const sharedBufferMethod = classDecl.getMethod('getSharedArrayBufferProcessFunction');
if (sharedBufferMethod) {
sharedBufferProcessBody = extractSharedBufferFunctionBody(sharedBufferMethod);
}
// 尝试提取 entityDataSize
// Try to extract entityDataSize
let entityDataSize: number | undefined;
const getDefaultEntityDataSizeMethod = classDecl.getMethod('getDefaultEntityDataSize');
if (getDefaultEntityDataSizeMethod) {
entityDataSize = extractEntityDataSize(getDefaultEntityDataSizeMethod);
}
// 尝试从构造函数中提取 workerScriptPath 配置
// Try to extract workerScriptPath from constructor
const workerScriptPath = extractWorkerScriptPath(classDecl, verbose);
return {
className,
filePath,
workerProcessBody,
workerProcessParams,
sharedBufferProcessBody,
entityDataSize,
workerScriptPath,
};
}
/**
* 提取方法体(去掉方法签名,保留函数体内容)
* Extract method body (remove method signature, keep function body content)
*/
function extractMethodBody(method: MethodDeclaration): string | null {
const body = method.getBody();
if (!body) {
return null;
}
// 获取方法体的文本
// Get method body text
let bodyText = body.getText();
// 去掉外层的花括号
// Remove outer braces
if (bodyText.startsWith('{') && bodyText.endsWith('}')) {
bodyText = bodyText.slice(1, -1).trim();
}
return bodyText;
}
/**
* 提取 getSharedArrayBufferProcessFunction 返回的函数体
* Extract function body returned by getSharedArrayBufferProcessFunction
*/
function extractSharedBufferFunctionBody(method: MethodDeclaration): string | undefined {
const body = method.getBody();
if (!body) {
return undefined;
}
// 查找 return 语句中的函数表达式
// Find function expression in return statement
const returnStatements = body.getDescendantsOfKind(SyntaxKind.ReturnStatement);
for (const returnStmt of returnStatements) {
const expression = returnStmt.getExpression();
if (expression) {
// 检查是否是函数表达式或箭头函数
// Check if it's a function expression or arrow function
if (Node.isFunctionExpression(expression) || Node.isArrowFunction(expression)) {
const funcBody = expression.getBody();
if (funcBody) {
let bodyText = funcBody.getText();
if (bodyText.startsWith('{') && bodyText.endsWith('}')) {
bodyText = bodyText.slice(1, -1).trim();
}
return bodyText;
}
}
}
}
return undefined;
}
/**
* 提取 entityDataSize 值
* Extract entityDataSize value
*/
function extractEntityDataSize(method: MethodDeclaration): number | undefined {
const body = method.getBody();
if (!body) {
return undefined;
}
// 查找 return 语句
// Find return statement
const returnStatements = body.getDescendantsOfKind(SyntaxKind.ReturnStatement);
for (const returnStmt of returnStatements) {
const expression = returnStmt.getExpression();
if (expression && Node.isNumericLiteral(expression)) {
return parseInt(expression.getText(), 10);
}
}
return undefined;
}
/**
* 从构造函数中提取 workerScriptPath 配置
* Extract workerScriptPath from constructor
*/
function extractWorkerScriptPath(classDecl: ClassDeclaration, verbose?: boolean): string | undefined {
// 查找构造函数
// Find constructor
const constructors = classDecl.getConstructors();
if (constructors.length === 0) {
return undefined;
}
const constructor = constructors[0]!;
const body = constructor.getBody();
if (!body) {
return undefined;
}
const bodyText = body.getText();
// 使用正则表达式查找 workerScriptPath: 'xxx' 或 workerScriptPath: "xxx"
// Use regex to find workerScriptPath: 'xxx' or workerScriptPath: "xxx"
const patterns = [
/workerScriptPath\s*:\s*['"]([^'"]+)['"]/,
/workerScriptPath\s*:\s*`([^`]+)`/,
];
for (const pattern of patterns) {
const match = bodyText.match(pattern);
if (match && match[1]) {
if (verbose) {
console.log(` Found workerScriptPath: ${match[1]}`);
}
return match[1];
}
}
return undefined;
}

View File

@@ -0,0 +1,85 @@
/**
* Worker 生成器类型定义
* Type definitions for Worker generator
*/
/**
* 提取的 WorkerEntitySystem 信息
* Extracted WorkerEntitySystem information
*/
export interface WorkerSystemInfo {
/** 类名 | Class name */
className: string;
/** 源文件路径 | Source file path */
filePath: string;
/** workerProcess 方法体 | workerProcess method body */
workerProcessBody: string;
/** workerProcess 参数名 | workerProcess parameter names */
workerProcessParams: {
entities: string;
deltaTime: string;
config: string;
};
/** getSharedArrayBufferProcessFunction 方法体(可选)| getSharedArrayBufferProcessFunction body (optional) */
sharedBufferProcessBody?: string;
/** entityDataSize 值(如果是字面量)| entityDataSize value (if literal) */
entityDataSize?: number;
/** 用户配置的 workerScriptPath从构造函数中提取| User configured workerScriptPath */
workerScriptPath?: string;
}
/**
* 生成器配置
* Generator configuration
*/
export interface GeneratorConfig {
/** 源代码目录 | Source directory */
srcDir: string;
/** 输出目录 | Output directory */
outDir: string;
/** 是否使用微信小游戏格式 | Whether to use WeChat Mini Game format */
wechat?: boolean;
/** 是否生成映射文件 | Whether to generate mapping file */
generateMapping?: boolean;
/** TypeScript 配置文件路径 | TypeScript config file path */
tsConfigPath?: string;
/** 是否详细输出 | Verbose output */
verbose?: boolean;
}
/**
* 生成结果
* Generation result
*/
export interface GenerationResult {
/** 成功生成的文件 | Successfully generated files */
success: Array<{
className: string;
outputPath: string;
/** 用户配置的路径(如果有)| User configured path (if any) */
configuredPath?: string;
}>;
/** 失败的类 | Failed classes */
errors: Array<{
className: string;
filePath: string;
error: string;
}>;
/** 需要用户配置 workerScriptPath 的类 | Classes that need workerScriptPath configuration */
skipped: Array<{
className: string;
suggestedPath: string;
reason: string;
}>;
}
/**
* Worker 脚本映射
* Worker script mapping
*/
export interface WorkerScriptMapping {
/** 生成时间 | Generation timestamp */
generatedAt: string;
/** 映射表:类名 -> Worker 文件路径 | Mapping: class name -> Worker file path */
mappings: Record<string, string>;
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

61
pnpm-lock.yaml generated
View File

@@ -1563,6 +1563,25 @@ importers:
specifier: ^5.3.3
version: 5.9.3
packages/worker-generator:
dependencies:
chalk:
specifier: ^4.1.2
version: 4.1.2
commander:
specifier: ^11.1.0
version: 11.1.0
ts-morph:
specifier: ^21.0.1
version: 21.0.1
devDependencies:
'@types/node':
specifier: ^20.10.0
version: 20.19.25
typescript:
specifier: ^5.3.0
version: 5.9.3
packages/world-streaming:
dependencies:
'@esengine/ecs-framework':
@@ -4113,6 +4132,9 @@ packages:
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
'@ts-morph/common@0.22.0':
resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==}
'@tufjs/canonical-json@2.0.0':
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -4928,6 +4950,9 @@ packages:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
code-block-writer@12.0.0:
resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==}
coi-serviceworker@0.1.7:
resolution: {integrity: sha512-bjSUqEngCPOkErY2vbyWsaIGCNRODYzlNycaREVw5s12/C8SM+RnRUUeX6pZbTtov6C52ZLY/+tvHK+BDxuUuA==}
@@ -4962,6 +4987,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -6989,6 +7018,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
@@ -8411,6 +8445,9 @@ packages:
jest-util:
optional: true
ts-morph@21.0.1:
resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==}
tsconfig-paths@4.2.0:
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
engines: {node: '>=6'}
@@ -11896,6 +11933,13 @@ snapshots:
'@tootallnate/once@2.0.0': {}
'@ts-morph/common@0.22.0':
dependencies:
fast-glob: 3.3.3
minimatch: 9.0.5
mkdirp: 3.0.1
path-browserify: 1.0.1
'@tufjs/canonical-json@2.0.0': {}
'@tufjs/models@2.0.1':
@@ -12846,6 +12890,8 @@ snapshots:
co@4.6.0: {}
code-block-writer@12.0.0: {}
coi-serviceworker@0.1.7: {}
collect-v8-coverage@1.0.3: {}
@@ -12875,6 +12921,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
commander@11.1.0: {}
commander@2.20.3: {}
commander@4.1.1: {}
@@ -14393,7 +14441,7 @@ snapshots:
jest-diff@29.7.0:
dependencies:
chalk: 4.1.0
chalk: 4.1.2
diff-sequences: 29.6.3
jest-get-type: 29.6.3
pretty-format: 29.7.0
@@ -15543,6 +15591,8 @@ snapshots:
mkdirp@1.0.4: {}
mkdirp@3.0.1: {}
mlly@1.8.0:
dependencies:
acorn: 8.15.0
@@ -15730,7 +15780,7 @@ snapshots:
'@yarnpkg/parsers': 3.0.2
'@zkochan/js-yaml': 0.0.7
axios: 1.13.2
chalk: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.6.1
cliui: 8.0.1
@@ -15812,7 +15862,7 @@ snapshots:
ora@5.3.0:
dependencies:
bl: 4.1.0
chalk: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.6.1
is-interactive: 1.0.0
@@ -17045,6 +17095,11 @@ snapshots:
babel-jest: 29.7.0(@babel/core@7.28.5)
jest-util: 29.7.0
ts-morph@21.0.1:
dependencies:
'@ts-morph/common': 0.22.0
code-block-writer: 12.0.0
tsconfig-paths@4.2.0:
dependencies:
json5: 2.2.3