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:
325
packages/worker-generator/src/generator.ts
Normal file
325
packages/worker-generator/src/generator.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user