Files
esengine/packages/worker-generator/src/generator.ts

331 lines
11 KiB
TypeScript

/**
* 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;
}