refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
172
packages/tools/worker-generator/src/cli.ts
Normal file
172
packages/tools/worker-generator/src/cli.ts
Normal 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();
|
||||
330
packages/tools/worker-generator/src/generator.ts
Normal file
330
packages/tools/worker-generator/src/generator.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* 注意:映射文件不能放在 workers 目录,微信小游戏会把它当 JS 编译
|
||||
* Note: Mapping file should NOT be in workers dir, WeChat will try to compile it as JS
|
||||
*/
|
||||
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 project root
|
||||
const relativePath = path.relative(process.cwd(), item.outputPath).replace(/\\/g, '/');
|
||||
mapping.mappings[item.className] = relativePath;
|
||||
}
|
||||
|
||||
// 映射文件放在项目根目录,而不是 workers 目录
|
||||
// Put mapping file in project root, not in workers directory
|
||||
const mappingPath = path.join(process.cwd(), '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;
|
||||
}
|
||||
41
packages/tools/worker-generator/src/index.ts
Normal file
41
packages/tools/worker-generator/src/index.ts
Normal 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';
|
||||
273
packages/tools/worker-generator/src/parser.ts
Normal file
273
packages/tools/worker-generator/src/parser.ts
Normal 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;
|
||||
}
|
||||
85
packages/tools/worker-generator/src/types.ts
Normal file
85
packages/tools/worker-generator/src/types.ts
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user