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

@@ -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"]
}