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:
215
packages/tools/build-config/README.md
Normal file
215
packages/tools/build-config/README.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# @esengine/build-config
|
||||
|
||||
ES Engine 统一构建配置包,提供标准化的 Vite 配置预设和共享插件。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建新包
|
||||
|
||||
使用脚手架工具快速创建新包:
|
||||
|
||||
```bash
|
||||
# 交互式创建
|
||||
node scripts/create-package.mjs
|
||||
|
||||
# 或指定参数
|
||||
node scripts/create-package.mjs my-plugin --type plugin
|
||||
```
|
||||
|
||||
### 包类型
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `runtime-only` | 纯运行时库,不含编辑器代码 | core, math, components |
|
||||
| `plugin` | 插件包,同时有 runtime 和 editor 入口 | ui, tilemap, behavior-tree |
|
||||
| `editor-only` | 纯编辑器包,仅用于编辑器 | editor-core, node-editor |
|
||||
|
||||
## 使用预设
|
||||
|
||||
### 1. runtime-only(纯运行时包)
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { runtimeOnlyPreset } from '@esengine/build-config/presets';
|
||||
|
||||
export default runtimeOnlyPreset({
|
||||
root: __dirname
|
||||
});
|
||||
```
|
||||
|
||||
目录结构:
|
||||
```
|
||||
packages/my-lib/
|
||||
├── src/
|
||||
│ └── index.ts # 主入口
|
||||
├── vite.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### 2. plugin(插件包)
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { pluginPreset } from '@esengine/build-config/presets';
|
||||
|
||||
export default pluginPreset({
|
||||
root: __dirname,
|
||||
hasCSS: true // 如果有 CSS 文件
|
||||
});
|
||||
```
|
||||
|
||||
目录结构:
|
||||
```
|
||||
packages/my-plugin/
|
||||
├── src/
|
||||
│ ├── index.ts # 主入口(导出全部)
|
||||
│ ├── runtime.ts # 运行时入口(不含 React!)
|
||||
│ ├── MyRuntimeModule.ts
|
||||
│ └── editor/
|
||||
│ ├── index.ts # 编辑器模块
|
||||
│ └── MyPlugin.ts
|
||||
├── plugin.json # 插件描述文件
|
||||
├── vite.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
生成的 exports:
|
||||
```json
|
||||
{
|
||||
".": "./dist/index.js",
|
||||
"./runtime": "./dist/runtime.js",
|
||||
"./editor": "./dist/editor/index.js",
|
||||
"./plugin.json": "./plugin.json"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. editor-only(纯编辑器包)
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { editorOnlyPreset } from '@esengine/build-config/presets';
|
||||
|
||||
export default editorOnlyPreset({
|
||||
root: __dirname,
|
||||
hasReact: true,
|
||||
hasCSS: true
|
||||
});
|
||||
```
|
||||
|
||||
## 共享插件
|
||||
|
||||
### CSS 注入插件
|
||||
|
||||
将 CSS 内联到 JS 中,避免单独的 CSS 文件:
|
||||
|
||||
```typescript
|
||||
import { cssInjectPlugin } from '@esengine/build-config/plugins';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [cssInjectPlugin()]
|
||||
});
|
||||
```
|
||||
|
||||
### 阻止编辑器代码泄漏
|
||||
|
||||
在运行时构建中检测并阻止编辑器代码被打包:
|
||||
|
||||
```typescript
|
||||
import { blockEditorPlugin } from '@esengine/build-config/plugins';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
blockEditorPlugin({ bIsRuntimeBuild: true })
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Runtime vs Editor 分离规则
|
||||
|
||||
### ✅ runtime.ts 中可以:
|
||||
- 导入 @esengine/ecs-framework
|
||||
- 导入 @esengine/ecs-components
|
||||
- 导入其他包的 `/runtime` 路径
|
||||
|
||||
### ❌ runtime.ts 中不能:
|
||||
- 导入 `react`、`react-dom`
|
||||
- 导入 `@esengine/editor-core`
|
||||
- 导入 `lucide-react` 等 UI 库
|
||||
- 导入任何包的 `/editor` 路径
|
||||
|
||||
### 示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { UIRuntimeModule } from '@esengine/ui/runtime';
|
||||
|
||||
// ❌ 错误 - 会把编辑器代码打包进来
|
||||
import { UIPlugin } from '@esengine/ui'; // 主入口包含编辑器
|
||||
import { UIPlugin } from '@esengine/ui/editor'; // 直接导入编辑器
|
||||
import React from 'react'; // React 不应在运行时
|
||||
```
|
||||
|
||||
## 迁移现有包
|
||||
|
||||
### 从 Rollup 迁移到 Vite 预设
|
||||
|
||||
1. 安装依赖:
|
||||
```bash
|
||||
pnpm add -D @esengine/build-config vite vite-plugin-dts
|
||||
```
|
||||
|
||||
2. 替换 `rollup.config.js` 为 `vite.config.ts`:
|
||||
```typescript
|
||||
import { pluginPreset } from '@esengine/build-config/presets';
|
||||
|
||||
export default pluginPreset({
|
||||
root: __dirname,
|
||||
hasCSS: true
|
||||
});
|
||||
```
|
||||
|
||||
3. 更新 `package.json` 的 scripts:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 删除旧的 rollup 配置和依赖。
|
||||
|
||||
## API 参考
|
||||
|
||||
### runtimeOnlyPreset(options)
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| root | string | - | 包根目录(必填) |
|
||||
| entry | string | 'src/index.ts' | 入口文件 |
|
||||
| external | (string\|RegExp)[] | [] | 额外的外部依赖 |
|
||||
| viteConfig | Partial<UserConfig> | {} | 额外的 Vite 配置 |
|
||||
|
||||
### pluginPreset(options)
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| root | string | - | 包根目录(必填) |
|
||||
| entries.main | string | 'src/index.ts' | 主入口 |
|
||||
| entries.runtime | string | 'src/runtime.ts' | 运行时入口 |
|
||||
| entries.editor | string | 'src/editor/index.ts' | 编辑器入口 |
|
||||
| hasCSS | boolean | false | 是否包含 CSS |
|
||||
| hasPluginJson | boolean | true | 是否导出 plugin.json |
|
||||
| external | (string\|RegExp)[] | [] | 额外的外部依赖 |
|
||||
|
||||
### editorOnlyPreset(options)
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| root | string | - | 包根目录(必填) |
|
||||
| entry | string | 'src/index.ts' | 入口文件 |
|
||||
| hasReact | boolean | true | 是否包含 React |
|
||||
| hasCSS | boolean | false | 是否包含 CSS |
|
||||
| external | (string\|RegExp)[] | [] | 额外的外部依赖 |
|
||||
40
packages/tools/build-config/package.json
Normal file
40
packages/tools/build-config/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@esengine/build-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared build configuration for ES Engine packages",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./presets": "./src/presets/index.ts",
|
||||
"./presets/tsup": "./src/presets/plugin-tsup.ts",
|
||||
"./plugins": "./src/plugins/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"build",
|
||||
"tsup",
|
||||
"config"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-dts": "^4.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tsup": "^8.0.0",
|
||||
"vite": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
90
packages/tools/build-config/src/index.ts
Normal file
90
packages/tools/build-config/src/index.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @esengine/build-config
|
||||
*
|
||||
* 统一构建配置包,提供标准化的 Vite 配置预设和共享插件
|
||||
* Unified build configuration with standardized Vite presets and shared plugins
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 1. 纯运行时包 (core, math, components)
|
||||
* import { runtimeOnlyPreset } from '@esengine/build-config/presets';
|
||||
* export default runtimeOnlyPreset({ root: __dirname });
|
||||
*
|
||||
* // 2. 插件包 (ui, tilemap, behavior-tree)
|
||||
* import { pluginPreset } from '@esengine/build-config/presets';
|
||||
* export default pluginPreset({
|
||||
* root: __dirname,
|
||||
* hasCSS: true
|
||||
* });
|
||||
*
|
||||
* // 3. 纯编辑器包 (editor-core, node-editor)
|
||||
* import { editorOnlyPreset } from '@esengine/build-config/presets';
|
||||
* export default editorOnlyPreset({
|
||||
* root: __dirname,
|
||||
* hasReact: true
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## 包类型说明
|
||||
*
|
||||
* | 类型 | 说明 | 示例 |
|
||||
* |------|------|------|
|
||||
* | RuntimeOnly | 纯运行时库,不含编辑器代码 | core, math, components |
|
||||
* | Plugin | 插件包,同时有 runtime 和 editor 入口 | ui, tilemap, behavior-tree |
|
||||
* | EditorOnly | 纯编辑器包,仅用于编辑器 | editor-core, node-editor |
|
||||
*
|
||||
* ## 目录结构约定
|
||||
*
|
||||
* ### RuntimeOnly 包
|
||||
* ```
|
||||
* packages/my-lib/
|
||||
* ├── src/
|
||||
* │ └── index.ts # 主入口
|
||||
* ├── vite.config.ts
|
||||
* └── package.json
|
||||
* ```
|
||||
*
|
||||
* ### Plugin 包
|
||||
* ```
|
||||
* packages/my-plugin/
|
||||
* ├── src/
|
||||
* │ ├── index.ts # 主入口(编辑器环境)
|
||||
* │ ├── runtime.ts # 运行时入口(不含 React)
|
||||
* │ └── editor/
|
||||
* │ └── index.ts # 编辑器模块
|
||||
* ├── plugin.json # 插件描述文件
|
||||
* ├── vite.config.ts
|
||||
* └── package.json
|
||||
* ```
|
||||
*
|
||||
* ### EditorOnly 包
|
||||
* ```
|
||||
* packages/my-editor-tool/
|
||||
* ├── src/
|
||||
* │ └── index.ts # 主入口
|
||||
* ├── vite.config.ts
|
||||
* └── package.json
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Types
|
||||
export { EPackageType, STANDARD_EXTERNALS, EDITOR_ONLY_EXTERNALS } from './types';
|
||||
export type { PackageBuildConfig } from './types';
|
||||
|
||||
// Presets
|
||||
export {
|
||||
runtimeOnlyPreset,
|
||||
pluginPreset,
|
||||
standaloneRuntimeConfig,
|
||||
editorOnlyPreset
|
||||
} from './presets';
|
||||
export type {
|
||||
RuntimeOnlyOptions,
|
||||
PluginPackageOptions,
|
||||
StandaloneRuntimeOptions,
|
||||
EditorOnlyOptions
|
||||
} from './presets';
|
||||
|
||||
// Plugins
|
||||
export { cssInjectPlugin, blockEditorPlugin } from './plugins';
|
||||
export type { BlockEditorOptions } from './plugins';
|
||||
100
packages/tools/build-config/src/plugins/block-editor.ts
Normal file
100
packages/tools/build-config/src/plugins/block-editor.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Block Editor Plugin
|
||||
* 阻止编辑器代码泄漏插件
|
||||
*
|
||||
* 在运行时构建中检测并阻止编辑器代码被打包
|
||||
* Detects and blocks editor code from being bundled in runtime builds
|
||||
*/
|
||||
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export interface BlockEditorOptions {
|
||||
/** 是否为运行时构建 */
|
||||
bIsRuntimeBuild: boolean;
|
||||
/** 要阻止的模块模式 */
|
||||
blockedPatterns?: (string | RegExp)[];
|
||||
/** 是否只警告而不报错 */
|
||||
bWarnOnly?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_BLOCKED_PATTERNS: (string | RegExp)[] = [
|
||||
// React 相关
|
||||
/^react$/,
|
||||
/^react-dom$/,
|
||||
/^react\/jsx-runtime$/,
|
||||
/^lucide-react$/,
|
||||
|
||||
// 编辑器包
|
||||
/@esengine\/editor-core/,
|
||||
/@esengine\/node-editor/,
|
||||
|
||||
// 编辑器子路径
|
||||
/\/editor$/,
|
||||
/\/editor\//,
|
||||
];
|
||||
|
||||
/**
|
||||
* 创建阻止编辑器代码泄漏的插件
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { blockEditorPlugin } from '@esengine/build-config/plugins';
|
||||
*
|
||||
* // 在运行时构建中使用
|
||||
* export default defineConfig({
|
||||
* plugins: [
|
||||
* blockEditorPlugin({ bIsRuntimeBuild: true })
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function blockEditorPlugin(options: BlockEditorOptions): Plugin {
|
||||
const {
|
||||
bIsRuntimeBuild,
|
||||
blockedPatterns = DEFAULT_BLOCKED_PATTERNS,
|
||||
bWarnOnly = false
|
||||
} = options;
|
||||
|
||||
if (!bIsRuntimeBuild) {
|
||||
// 非运行时构建不需要此插件
|
||||
return { name: 'esengine:block-editor-noop' };
|
||||
}
|
||||
|
||||
const isBlocked = (source: string): boolean => {
|
||||
return blockedPatterns.some(pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return source === pattern || source.startsWith(pattern + '/');
|
||||
}
|
||||
return pattern.test(source);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'esengine:block-editor',
|
||||
enforce: 'pre',
|
||||
|
||||
resolveId(source: string, importer: string | undefined) {
|
||||
if (isBlocked(source)) {
|
||||
const message = `[block-editor] Editor dependency detected in runtime build:\n` +
|
||||
` Source: ${source}\n` +
|
||||
` Importer: ${importer || 'entry'}\n` +
|
||||
`\n` +
|
||||
` This usually means:\n` +
|
||||
` 1. A runtime module is importing from a non-/runtime path\n` +
|
||||
` 2. An editor-only dependency leaked into the dependency chain\n` +
|
||||
`\n` +
|
||||
` Fix: Change the import to use /runtime subpath, e.g.:\n` +
|
||||
` import { X } from '@esengine/ui/runtime' // ✓\n` +
|
||||
` import { X } from '@esengine/ui' // ✗`;
|
||||
|
||||
if (bWarnOnly) {
|
||||
console.warn('\x1b[33m' + message + '\x1b[0m');
|
||||
return { id: source, external: true };
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
71
packages/tools/build-config/src/plugins/css-inject.ts
Normal file
71
packages/tools/build-config/src/plugins/css-inject.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* CSS Inject Plugin
|
||||
* CSS 注入插件
|
||||
*
|
||||
* 将 CSS 内联到 JS 中,避免单独的 CSS 文件
|
||||
* Inlines CSS into JS to avoid separate CSS files
|
||||
*/
|
||||
|
||||
import type { Plugin } from 'vite';
|
||||
import type { OutputBundle, NormalizedOutputOptions, OutputAsset, OutputChunk } from 'rollup';
|
||||
|
||||
/**
|
||||
* 创建 CSS 注入插件
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cssInjectPlugin } from '@esengine/build-config/plugins';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* plugins: [cssInjectPlugin()]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function cssInjectPlugin(): Plugin {
|
||||
return {
|
||||
name: 'esengine:css-inject',
|
||||
apply: 'build',
|
||||
|
||||
generateBundle(_options: NormalizedOutputOptions, bundle: OutputBundle) {
|
||||
// 收集所有 CSS 内容
|
||||
const cssChunks: string[] = [];
|
||||
const cssFileNames: string[] = [];
|
||||
|
||||
for (const [fileName, chunk] of Object.entries(bundle)) {
|
||||
if (fileName.endsWith('.css') && chunk.type === 'asset') {
|
||||
cssChunks.push(chunk.source as string);
|
||||
cssFileNames.push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (cssChunks.length === 0) return;
|
||||
|
||||
// 合并所有 CSS
|
||||
const combinedCSS = cssChunks.join('\n');
|
||||
|
||||
// 创建注入代码
|
||||
const injectCode = `
|
||||
(function() {
|
||||
if (typeof document === 'undefined') return;
|
||||
var style = document.createElement('style');
|
||||
style.setAttribute('data-esengine', 'true');
|
||||
style.textContent = ${JSON.stringify(combinedCSS)};
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
`;
|
||||
|
||||
// 找到主入口 JS 文件并注入
|
||||
for (const chunk of Object.values(bundle)) {
|
||||
if (chunk.type === 'chunk' && chunk.isEntry) {
|
||||
chunk.code = injectCode + chunk.code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除独立的 CSS 文件
|
||||
for (const fileName of cssFileNames) {
|
||||
delete bundle[fileName];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
7
packages/tools/build-config/src/plugins/index.ts
Normal file
7
packages/tools/build-config/src/plugins/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Shared Vite Plugins
|
||||
* 共享 Vite 插件
|
||||
*/
|
||||
|
||||
export { cssInjectPlugin } from './css-inject';
|
||||
export { blockEditorPlugin, type BlockEditorOptions } from './block-editor';
|
||||
109
packages/tools/build-config/src/presets/editor-only.ts
Normal file
109
packages/tools/build-config/src/presets/editor-only.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Editor-Only Package Preset
|
||||
* 纯编辑器包预设
|
||||
*
|
||||
* 用于仅在编辑器环境使用的包
|
||||
* For packages only used in the editor environment
|
||||
*
|
||||
* Examples: editor-core, node-editor
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { STANDARD_EXTERNALS } from '../types';
|
||||
import { cssInjectPlugin } from '../plugins/css-inject';
|
||||
|
||||
export interface EditorOnlyOptions {
|
||||
/** 包根目录 (通常是 __dirname) */
|
||||
root: string;
|
||||
|
||||
/** 入口文件 (默认: src/index.ts) */
|
||||
entry?: string;
|
||||
|
||||
/** 是否包含 React 组件 (默认: true) */
|
||||
hasReact?: boolean;
|
||||
|
||||
/** 是否包含 CSS (默认: false) */
|
||||
hasCSS?: boolean;
|
||||
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
|
||||
/** 额外的 Vite 配置 */
|
||||
viteConfig?: Partial<UserConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯编辑器包的 Vite 配置
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // vite.config.ts
|
||||
* import { editorOnlyPreset } from '@esengine/build-config/presets';
|
||||
*
|
||||
* export default editorOnlyPreset({
|
||||
* root: __dirname,
|
||||
* hasReact: true,
|
||||
* hasCSS: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function editorOnlyPreset(options: EditorOnlyOptions): UserConfig {
|
||||
const {
|
||||
root,
|
||||
entry = 'src/index.ts',
|
||||
hasReact = true,
|
||||
hasCSS = false,
|
||||
external = [],
|
||||
viteConfig = {}
|
||||
} = options;
|
||||
|
||||
const plugins: any[] = [];
|
||||
|
||||
// React 支持
|
||||
if (hasReact) {
|
||||
plugins.push(react());
|
||||
}
|
||||
|
||||
// DTS 生成
|
||||
plugins.push(
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: false
|
||||
})
|
||||
);
|
||||
|
||||
// CSS 注入
|
||||
if (hasCSS) {
|
||||
plugins.push(cssInjectPlugin());
|
||||
}
|
||||
|
||||
return defineConfig({
|
||||
plugins,
|
||||
esbuild: hasReact ? { jsx: 'automatic' } : undefined,
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(root, entry),
|
||||
formats: ['es'],
|
||||
fileName: () => 'index.js'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
...external
|
||||
],
|
||||
output: {
|
||||
exports: 'named',
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
},
|
||||
...viteConfig
|
||||
});
|
||||
}
|
||||
10
packages/tools/build-config/src/presets/index.ts
Normal file
10
packages/tools/build-config/src/presets/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Build Presets
|
||||
* 构建预设
|
||||
*
|
||||
* 提供不同类型包的标准化 Vite 配置
|
||||
*/
|
||||
|
||||
export { runtimeOnlyPreset, type RuntimeOnlyOptions } from './runtime-only';
|
||||
export { pluginPreset, standaloneRuntimeConfig, type PluginPackageOptions, type StandaloneRuntimeOptions } from './plugin';
|
||||
export { editorOnlyPreset, type EditorOnlyOptions } from './editor-only';
|
||||
157
packages/tools/build-config/src/presets/plugin-tsup.ts
Normal file
157
packages/tools/build-config/src/presets/plugin-tsup.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Plugin Package Preset (tsup)
|
||||
* 插件包预设 - 基于 tsup/esbuild
|
||||
*
|
||||
* 用于同时包含运行时和编辑器模块的插件包
|
||||
* For plugin packages with both runtime and editor modules
|
||||
*
|
||||
* 生成三个入口点:
|
||||
* - index.js - 完整导出(编辑器环境)
|
||||
* - runtime.js - 纯运行时(游戏运行时环境,不含 React)
|
||||
* - editor/index.js - 编辑器模块
|
||||
*
|
||||
* Examples: ui, tilemap, behavior-tree, physics-rapier2d
|
||||
*/
|
||||
|
||||
import type { Options } from 'tsup';
|
||||
import { STANDARD_EXTERNALS } from '../types';
|
||||
|
||||
export interface PluginPackageOptions {
|
||||
/** 入口点配置 */
|
||||
entries?: {
|
||||
/** 主入口 (默认: src/index.ts) */
|
||||
main?: string;
|
||||
/** 运行时入口 (默认: src/runtime.ts) */
|
||||
runtime?: string;
|
||||
/** 编辑器入口 (默认: src/editor/index.ts) */
|
||||
editor?: string;
|
||||
};
|
||||
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
|
||||
/** 额外的 tsup 配置 */
|
||||
tsupConfig?: Partial<Options>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件包的 tsup 配置
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // tsup.config.ts
|
||||
* import { defineConfig } from 'tsup';
|
||||
* import { pluginPreset } from '@esengine/build-config/presets';
|
||||
*
|
||||
* export default defineConfig(pluginPreset());
|
||||
* ```
|
||||
*/
|
||||
export function pluginPreset(options: PluginPackageOptions = {}): Options {
|
||||
const {
|
||||
entries = {},
|
||||
external = [],
|
||||
tsupConfig = {}
|
||||
} = options;
|
||||
|
||||
const mainEntry = entries.main ?? 'src/index.ts';
|
||||
const runtimeEntry = entries.runtime ?? 'src/runtime.ts';
|
||||
const editorEntry = entries.editor ?? 'src/editor/index.ts';
|
||||
|
||||
// 合并外部依赖
|
||||
const allExternal = [
|
||||
...STANDARD_EXTERNALS,
|
||||
...external
|
||||
];
|
||||
|
||||
return {
|
||||
entry: {
|
||||
index: mainEntry,
|
||||
runtime: runtimeEntry,
|
||||
'editor/index': editorEntry
|
||||
},
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
splitting: false, // 禁用代码分割
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: allExternal,
|
||||
esbuildOptions(options) {
|
||||
options.jsx = 'automatic';
|
||||
},
|
||||
...tsupConfig
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯运行时包的 tsup 配置
|
||||
*/
|
||||
export interface RuntimeOnlyOptions {
|
||||
/** 入口文件 (默认: src/index.ts) */
|
||||
entry?: string;
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
/** 额外的 tsup 配置 */
|
||||
tsupConfig?: Partial<Options>;
|
||||
}
|
||||
|
||||
export function runtimeOnlyPreset(options: RuntimeOnlyOptions = {}): Options {
|
||||
const {
|
||||
entry = 'src/index.ts',
|
||||
external = [],
|
||||
tsupConfig = {}
|
||||
} = options;
|
||||
|
||||
return {
|
||||
entry: [entry],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
...external
|
||||
],
|
||||
...tsupConfig
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯编辑器包的 tsup 配置
|
||||
*/
|
||||
export interface EditorOnlyOptions {
|
||||
/** 入口文件 (默认: src/index.ts) */
|
||||
entry?: string;
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
/** 额外的 tsup 配置 */
|
||||
tsupConfig?: Partial<Options>;
|
||||
}
|
||||
|
||||
export function editorOnlyPreset(options: EditorOnlyOptions = {}): Options {
|
||||
const {
|
||||
entry = 'src/index.ts',
|
||||
external = [],
|
||||
tsupConfig = {}
|
||||
} = options;
|
||||
|
||||
return {
|
||||
entry: [entry],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
// 将 CSS 内联到 JS 中,运行时自动注入到 DOM
|
||||
// Inline CSS into JS, auto-inject to DOM at runtime
|
||||
injectStyle: true,
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
...external
|
||||
],
|
||||
esbuildOptions(options) {
|
||||
options.jsx = 'automatic';
|
||||
},
|
||||
...tsupConfig
|
||||
};
|
||||
}
|
||||
176
packages/tools/build-config/src/presets/plugin.ts
Normal file
176
packages/tools/build-config/src/presets/plugin.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Plugin Package Preset
|
||||
* 插件包预设
|
||||
*
|
||||
* 用于同时包含运行时和编辑器模块的插件包
|
||||
* For plugin packages with both runtime and editor modules
|
||||
*
|
||||
* 生成三个入口点:
|
||||
* - index.js - 完整导出(编辑器环境)
|
||||
* - runtime.js - 纯运行时(游戏运行时环境,不含 React)
|
||||
* - editor/index.js - 编辑器模块
|
||||
*
|
||||
* Examples: ui, tilemap, behavior-tree, physics-rapier2d
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { STANDARD_EXTERNALS, EDITOR_ONLY_EXTERNALS } from '../types';
|
||||
import { cssInjectPlugin } from '../plugins/css-inject';
|
||||
|
||||
export interface PluginPackageOptions {
|
||||
/** 包根目录 (通常是 __dirname) */
|
||||
root: string;
|
||||
|
||||
/** 入口点配置 */
|
||||
entries?: {
|
||||
/** 主入口 (默认: src/index.ts) */
|
||||
main?: string;
|
||||
/** 运行时入口 (默认: src/runtime.ts) */
|
||||
runtime?: string;
|
||||
/** 编辑器入口 (默认: src/editor/index.ts) */
|
||||
editor?: string;
|
||||
};
|
||||
|
||||
/** 是否包含 CSS (默认: false) */
|
||||
hasCSS?: boolean;
|
||||
|
||||
/** 是否生成 plugin.json 导出 (默认: true) */
|
||||
hasPluginJson?: boolean;
|
||||
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
|
||||
/** 额外的 Vite 配置 */
|
||||
viteConfig?: Partial<UserConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件包的 Vite 配置
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // vite.config.ts
|
||||
* import { pluginPreset } from '@esengine/build-config/presets';
|
||||
*
|
||||
* export default pluginPreset({
|
||||
* root: __dirname,
|
||||
* hasCSS: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function pluginPreset(options: PluginPackageOptions): UserConfig {
|
||||
const {
|
||||
root,
|
||||
entries = {},
|
||||
hasCSS = false,
|
||||
external = [],
|
||||
viteConfig = {}
|
||||
} = options;
|
||||
|
||||
const mainEntry = entries.main ?? 'src/index.ts';
|
||||
const runtimeEntry = entries.runtime ?? 'src/runtime.ts';
|
||||
const editorEntry = entries.editor ?? 'src/editor/index.ts';
|
||||
|
||||
// 构建入口点映射
|
||||
const entryPoints: Record<string, string> = {
|
||||
index: resolve(root, mainEntry),
|
||||
runtime: resolve(root, runtimeEntry),
|
||||
'editor/index': resolve(root, editorEntry)
|
||||
};
|
||||
|
||||
const plugins: any[] = [
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: false
|
||||
})
|
||||
];
|
||||
|
||||
// CSS 注入插件
|
||||
if (hasCSS) {
|
||||
plugins.push(cssInjectPlugin());
|
||||
}
|
||||
|
||||
return defineConfig({
|
||||
plugins,
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: entryPoints,
|
||||
formats: ['es'],
|
||||
fileName: (_format: string, entryName: string) => `${entryName}.js`
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
...external
|
||||
],
|
||||
output: {
|
||||
exports: 'named',
|
||||
preserveModules: false,
|
||||
// 禁用自动代码分割,所有共享代码内联到各入口
|
||||
manualChunks: () => undefined
|
||||
}
|
||||
},
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
},
|
||||
...viteConfig
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建独立运行时构建配置
|
||||
* 用于 platform-web 等需要生成独立 IIFE 运行时的场景
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // rollup.runtime.config.js
|
||||
* import { standaloneRuntimeConfig } from '@esengine/build-config/presets';
|
||||
*
|
||||
* export default standaloneRuntimeConfig({
|
||||
* root: __dirname,
|
||||
* entry: 'src/runtime.ts',
|
||||
* globalName: 'ECSRuntime'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface StandaloneRuntimeOptions {
|
||||
/** 包根目录 */
|
||||
root: string;
|
||||
/** 入口文件 */
|
||||
entry: string;
|
||||
/** 全局变量名 (IIFE 格式) */
|
||||
globalName: string;
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
}
|
||||
|
||||
export function standaloneRuntimeConfig(options: StandaloneRuntimeOptions) {
|
||||
const { root, entry, globalName, external = [] } = options;
|
||||
|
||||
// 返回 Rollup 配置(而非 Vite,因为需要 IIFE 格式)
|
||||
return {
|
||||
input: resolve(root, entry),
|
||||
output: {
|
||||
file: 'dist/runtime.browser.js',
|
||||
format: 'iife' as const,
|
||||
name: globalName,
|
||||
sourcemap: true,
|
||||
exports: 'default' as const
|
||||
},
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
...EDITOR_ONLY_EXTERNALS,
|
||||
...external
|
||||
],
|
||||
plugins: [
|
||||
// 需要在使用时传入 rollup 插件
|
||||
]
|
||||
};
|
||||
}
|
||||
78
packages/tools/build-config/src/presets/runtime-only.ts
Normal file
78
packages/tools/build-config/src/presets/runtime-only.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Runtime-Only Package Preset
|
||||
* 纯运行时包预设
|
||||
*
|
||||
* 用于不包含任何编辑器代码的基础库
|
||||
* For basic libraries without any editor code
|
||||
*
|
||||
* Examples: core, math, components, asset-system
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { STANDARD_EXTERNALS } from '../types';
|
||||
|
||||
export interface RuntimeOnlyOptions {
|
||||
/** 包根目录 (通常是 __dirname) */
|
||||
root: string;
|
||||
/** 入口文件 (默认: src/index.ts) */
|
||||
entry?: string;
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
/** 额外的 Vite 配置 */
|
||||
viteConfig?: Partial<UserConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯运行时包的 Vite 配置
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // vite.config.ts
|
||||
* import { runtimeOnlyPreset } from '@esengine/build-config/presets';
|
||||
*
|
||||
* export default runtimeOnlyPreset({
|
||||
* root: __dirname
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function runtimeOnlyPreset(options: RuntimeOnlyOptions): UserConfig {
|
||||
const {
|
||||
root,
|
||||
entry = 'src/index.ts',
|
||||
external = [],
|
||||
viteConfig = {}
|
||||
} = options;
|
||||
|
||||
return defineConfig({
|
||||
plugins: [
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: false
|
||||
})
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(root, entry),
|
||||
formats: ['es'],
|
||||
fileName: () => 'index.js'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
...external
|
||||
],
|
||||
output: {
|
||||
exports: 'named',
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
},
|
||||
...viteConfig
|
||||
});
|
||||
}
|
||||
110
packages/tools/build-config/src/types.ts
Normal file
110
packages/tools/build-config/src/types.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Build Configuration Types
|
||||
* 构建配置类型定义
|
||||
*/
|
||||
|
||||
import type { UserConfig } from 'vite';
|
||||
|
||||
/**
|
||||
* 包类型
|
||||
* Package types for different build configurations
|
||||
*/
|
||||
export const enum EPackageType {
|
||||
/**
|
||||
* 纯运行时库 - 不含任何编辑器代码
|
||||
* Pure runtime library - no editor dependencies
|
||||
*
|
||||
* Examples: core, math, components, asset-system
|
||||
*/
|
||||
RuntimeOnly = 'runtime-only',
|
||||
|
||||
/**
|
||||
* 插件包 - 同时包含运行时和编辑器模块
|
||||
* Plugin package - contains both runtime and editor modules
|
||||
*
|
||||
* Examples: ui, tilemap, behavior-tree, physics-rapier2d
|
||||
*/
|
||||
Plugin = 'plugin',
|
||||
|
||||
/**
|
||||
* 纯编辑器包 - 仅用于编辑器
|
||||
* Editor-only package - only used in editor
|
||||
*
|
||||
* Examples: editor-core, node-editor
|
||||
*/
|
||||
EditorOnly = 'editor-only',
|
||||
|
||||
/**
|
||||
* 应用包 - 最终应用(不发布到 npm)
|
||||
* Application package - final app (not published)
|
||||
*
|
||||
* Examples: editor-app
|
||||
*/
|
||||
Application = 'application'
|
||||
}
|
||||
|
||||
/**
|
||||
* 包构建配置
|
||||
*/
|
||||
export interface PackageBuildConfig {
|
||||
/** 包名 */
|
||||
name: string;
|
||||
|
||||
/** 包类型 */
|
||||
type: EPackageType;
|
||||
|
||||
/** 入口点配置 */
|
||||
entries?: {
|
||||
/** 主入口 (默认: src/index.ts) */
|
||||
main?: string;
|
||||
/** 运行时入口 (仅 Plugin 类型) */
|
||||
runtime?: string;
|
||||
/** 编辑器入口 (Plugin 和 EditorOnly 类型) */
|
||||
editor?: string;
|
||||
};
|
||||
|
||||
/** 额外的外部依赖 */
|
||||
external?: (string | RegExp)[];
|
||||
|
||||
/** 是否包含 CSS */
|
||||
hasCSS?: boolean;
|
||||
|
||||
/** 是否生成 plugin.json 导出 */
|
||||
hasPluginJson?: boolean;
|
||||
|
||||
/** 额外的 Vite 配置 */
|
||||
viteConfig?: Partial<UserConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准外部依赖列表
|
||||
* Standard external dependencies that should never be bundled
|
||||
*/
|
||||
export const STANDARD_EXTERNALS = [
|
||||
// React 生态
|
||||
'react',
|
||||
'react-dom',
|
||||
'react/jsx-runtime',
|
||||
'lucide-react',
|
||||
|
||||
// 状态管理
|
||||
'zustand',
|
||||
'immer',
|
||||
|
||||
// Tauri (由宿主应用提供) | Provided by host app
|
||||
/^@tauri-apps\//,
|
||||
|
||||
// 所有 @esengine 包
|
||||
/^@esengine\//,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 编辑器专用依赖(运行时构建必须排除)
|
||||
* Editor-only dependencies that must be excluded from runtime builds
|
||||
*/
|
||||
export const EDITOR_ONLY_EXTERNALS = [
|
||||
'@esengine/editor-core',
|
||||
'@esengine/node-editor',
|
||||
/\/editor$/,
|
||||
/\/editor\//,
|
||||
] as const;
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "{{fullName}}",
|
||||
"version": "1.0.0",
|
||||
"description": "{{description}}",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/runtime.d.ts",
|
||||
"import": "./dist/runtime.js"
|
||||
},
|
||||
"./editor": {
|
||||
"types": "./dist/editor/index.d.ts",
|
||||
"import": "./dist/editor/index.js"
|
||||
},
|
||||
"./plugin.json": "./plugin.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"plugin.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "{{depVersion}}",
|
||||
"@esengine/ecs-components": "{{depVersion}}",
|
||||
"@esengine/editor-core": "{{depVersion}}",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@esengine/editor-core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "{{depVersion}}",
|
||||
"@types/react": "^18.3.12",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"esengine",
|
||||
"plugin"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "{{fullName}}",
|
||||
"name": "{{displayName}}",
|
||||
"version": "1.0.0",
|
||||
"category": "{{category}}",
|
||||
"enabledByDefault": true,
|
||||
"isEnginePlugin": false,
|
||||
"modules": [
|
||||
{
|
||||
"name": "{{pascalName}}Runtime",
|
||||
"type": "runtime",
|
||||
"entry": "./src/runtime.ts"
|
||||
},
|
||||
{
|
||||
"name": "{{pascalName}}Editor",
|
||||
"type": "editor",
|
||||
"entry": "./src/editor/index.ts"
|
||||
}
|
||||
],
|
||||
"components": [],
|
||||
"dependencies": []
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* {{displayName}} Runtime Module
|
||||
*
|
||||
* 运行时模块 - 负责注册组件、服务和系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IRuntimeModule,
|
||||
IComponentRegistry,
|
||||
SystemContext
|
||||
} from '@esengine/ecs-components';
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
export class {{name}}RuntimeModule implements IRuntimeModule {
|
||||
/**
|
||||
* 注册组件到组件注册表
|
||||
*/
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
// registry.register(MyComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册服务到服务容器
|
||||
*/
|
||||
registerServices?(services: ServiceContainer): void {
|
||||
// services.registerSingleton(MyService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化回调
|
||||
*/
|
||||
async onInitialize?(): Promise<void> {
|
||||
// 执行初始化逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 为场景创建系统
|
||||
*/
|
||||
createSystems?(scene: IScene, context: SystemContext): void {
|
||||
// scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统创建完成后的回调,用于连接跨插件依赖
|
||||
*/
|
||||
onSystemsCreated?(scene: IScene, context: SystemContext): void {
|
||||
// 连接跨插件依赖
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* {{displayName}} Plugin
|
||||
*
|
||||
* 插件定义 - 注册编辑器模块(Inspector、工具等)
|
||||
*/
|
||||
|
||||
import type { IPlugin, ModuleManifest, IEditorModuleLoader } from '@esengine/editor-core';
|
||||
import { {{name}}RuntimeModule } from '../{{name}}RuntimeModule';
|
||||
|
||||
class {{name}}EditorModule implements IEditorModuleLoader {
|
||||
async install(): Promise<void> {
|
||||
// 注册组件 Inspector
|
||||
// registry.register('MyComponent', MyComponentInspector);
|
||||
}
|
||||
async uninstall(): Promise<void> {}
|
||||
}
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: '@esengine/{{name}}',
|
||||
name: '@esengine/{{name}}',
|
||||
displayName: '{{displayName}}',
|
||||
version: '1.0.0',
|
||||
description: '{{displayName}} plugin',
|
||||
category: '{{category}}',
|
||||
isCore: false,
|
||||
defaultEnabled: true,
|
||||
isEngineModule: false,
|
||||
dependencies: ['engine-core'],
|
||||
exports: {}
|
||||
};
|
||||
|
||||
export const {{name}}Plugin: IPlugin = {
|
||||
manifest,
|
||||
runtimeModule: new {{name}}RuntimeModule(),
|
||||
editorModule: new {{name}}EditorModule()
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @esengine/{{name}} Editor Module
|
||||
*
|
||||
* 编辑器模块 - 包含 Inspector、工具等编辑器专用代码
|
||||
* Editor module - contains Inspector, tools, and other editor-specific code
|
||||
*
|
||||
* This module can safely import React and editor-core packages.
|
||||
*/
|
||||
|
||||
export { {{name}}Plugin } from './{{name}}Plugin';
|
||||
|
||||
// Inspectors
|
||||
// export { MyComponentInspector } from './inspectors/MyComponentInspector';
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @esengine/{{name}}
|
||||
*
|
||||
* {{description}}
|
||||
*
|
||||
* 主入口 - 导出所有内容(包括编辑器模块)
|
||||
* Main entry - exports everything (including editor modules)
|
||||
*/
|
||||
|
||||
// Runtime exports (always available)
|
||||
export * from './runtime';
|
||||
|
||||
// Editor exports (only in editor environment)
|
||||
export { {{name}}Plugin } from './editor';
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @esengine/{{name}} Runtime Entry Point
|
||||
*
|
||||
* 运行时入口 - 仅导出运行时代码,不包含任何编辑器依赖
|
||||
* Runtime entry - exports only runtime code without any editor dependencies
|
||||
*
|
||||
* IMPORTANT: Do not import React or any editor packages here!
|
||||
*/
|
||||
|
||||
// Components
|
||||
// export { MyComponent } from './components/MyComponent';
|
||||
|
||||
// Systems
|
||||
// export { MySystem } from './systems/MySystem';
|
||||
|
||||
// Runtime Module
|
||||
export { {{name}}RuntimeModule } from './{{name}}RuntimeModule';
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { pluginPreset } from '@esengine/build-config/presets/tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...pluginPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "{{fullName}}",
|
||||
"version": "1.0.0",
|
||||
"description": "{{description}}",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "{{depVersion}}"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "{{depVersion}}",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @esengine/{{name}}
|
||||
*
|
||||
* {{description}}
|
||||
*/
|
||||
|
||||
// Export your public API here
|
||||
export {};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '@esengine/build-config/presets/tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
17
packages/tools/build-config/tsconfig.json
Normal file
17
packages/tools/build-config/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
38
packages/tools/sdk/module.json
Normal file
38
packages/tools/sdk/module.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "sdk",
|
||||
"name": "@esengine/sdk",
|
||||
"globalKey": "sdk",
|
||||
"displayName": "Unified SDK",
|
||||
"outputPath": "dist/index.js",
|
||||
"description": "Unified SDK entry point - import everything from a single package | 统一 SDK 入口 - 从单个包导入所有内容",
|
||||
"version": "1.0.0",
|
||||
"category": "Core",
|
||||
"icon": "Package",
|
||||
"tags": ["sdk", "unified", "entry"],
|
||||
"isCore": true,
|
||||
"defaultEnabled": true,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": ["web", "desktop", "mobile"],
|
||||
"hasRuntime": true,
|
||||
"dependencies": [
|
||||
"@esengine/ecs-framework",
|
||||
"@esengine/ecs-framework-math",
|
||||
"@esengine/engine-core",
|
||||
"@esengine/sprite",
|
||||
"@esengine/fairygui",
|
||||
"@esengine/audio",
|
||||
"@esengine/camera",
|
||||
"@esengine/particle",
|
||||
"@esengine/physics-rapier2d",
|
||||
"@esengine/tilemap",
|
||||
"@esengine/behavior-tree",
|
||||
"@esengine/asset-system"
|
||||
],
|
||||
"exports": {
|
||||
"components": [],
|
||||
"systems": [],
|
||||
"other": ["*"]
|
||||
},
|
||||
"requiresWasm": false
|
||||
}
|
||||
53
packages/tools/sdk/package.json
Normal file
53
packages/tools/sdk/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@esengine/sdk",
|
||||
"version": "1.0.0",
|
||||
"description": "Unified SDK entry point for ESEngine - single import for all engine modules",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/sprite": "workspace:*",
|
||||
"@esengine/fairygui": "workspace:*",
|
||||
"@esengine/audio": "workspace:*",
|
||||
"@esengine/camera": "workspace:*",
|
||||
"@esengine/particle": "workspace:*",
|
||||
"@esengine/physics-rapier2d": "workspace:*",
|
||||
"@esengine/tilemap": "workspace:*",
|
||||
"@esengine/behavior-tree": "workspace:*",
|
||||
"@esengine/asset-system": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"esengine",
|
||||
"sdk",
|
||||
"ecs",
|
||||
"game-engine",
|
||||
"unified-api"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT"
|
||||
}
|
||||
200
packages/tools/sdk/src/index.ts
Normal file
200
packages/tools/sdk/src/index.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @esengine/sdk - 统一 SDK 入口
|
||||
*
|
||||
* Unified SDK entry point for user scripts.
|
||||
* 用户脚本的统一 SDK 入口。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 统一导入所有引擎 API
|
||||
* // Unified import for all engine APIs
|
||||
* import { Component, Entity, Vector2, SpriteComponent } from '@esengine/sdk';
|
||||
* ```
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Core ECS Framework (@esengine/ecs-framework)
|
||||
// 核心 ECS 框架
|
||||
// ============================================================================
|
||||
export * from '@esengine/ecs-framework';
|
||||
|
||||
// ============================================================================
|
||||
// Math Library (@esengine/ecs-framework-math)
|
||||
// 数学库
|
||||
// ============================================================================
|
||||
export * from '@esengine/ecs-framework-math';
|
||||
|
||||
// ============================================================================
|
||||
// Engine Core (@esengine/engine-core)
|
||||
// 引擎核心
|
||||
// ============================================================================
|
||||
export * from '@esengine/engine-core';
|
||||
|
||||
// ============================================================================
|
||||
// Sprite System (@esengine/sprite)
|
||||
// 精灵系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/sprite';
|
||||
|
||||
// ============================================================================
|
||||
// FairyGUI System (@esengine/fairygui)
|
||||
// FairyGUI 系统
|
||||
// Note: Selective exports to avoid conflicts with ecs-framework and math
|
||||
// ============================================================================
|
||||
export {
|
||||
// ECS Integration
|
||||
FGUIComponent,
|
||||
FGUIRenderSystem,
|
||||
getFGUIRenderSystem,
|
||||
setFGUIRenderSystem,
|
||||
FGUIRuntimeModule,
|
||||
FGUIPlugin,
|
||||
// Core
|
||||
GObject,
|
||||
GComponent,
|
||||
GRoot,
|
||||
GGroup,
|
||||
Controller,
|
||||
Transition,
|
||||
UIConfig,
|
||||
getUIConfig,
|
||||
setUIConfig,
|
||||
UIObjectFactory,
|
||||
GObjectPool,
|
||||
DragDropManager,
|
||||
EScaleMode,
|
||||
EAlignMode,
|
||||
// Widgets
|
||||
GImage,
|
||||
GTextField,
|
||||
GGraph,
|
||||
GButton,
|
||||
GProgressBar,
|
||||
GSlider,
|
||||
GLoader,
|
||||
GList,
|
||||
GTextInput,
|
||||
EKeyboardType,
|
||||
PopupMenu,
|
||||
Window,
|
||||
// Package
|
||||
UIPackage,
|
||||
PackageItem,
|
||||
// Events
|
||||
EventDispatcher,
|
||||
FGUIEvents,
|
||||
// Render
|
||||
RenderCollector,
|
||||
RenderBridge,
|
||||
Canvas2DBackend,
|
||||
FGUIRenderDataProvider,
|
||||
createFGUIRenderDataProvider,
|
||||
// Tween
|
||||
GTween,
|
||||
GTweener,
|
||||
TweenManager,
|
||||
TweenValue,
|
||||
evaluateEase,
|
||||
// Asset
|
||||
FUIAssetLoader,
|
||||
fuiAssetLoader,
|
||||
// Field Types
|
||||
EButtonMode,
|
||||
EAutoSizeType,
|
||||
EAlignType,
|
||||
EVertAlignType,
|
||||
ELoaderFillType,
|
||||
EListLayoutType,
|
||||
EListSelectionMode,
|
||||
EOverflowType,
|
||||
EPackageItemType,
|
||||
EObjectType,
|
||||
EProgressTitleType,
|
||||
EScrollBarDisplayType,
|
||||
EScrollType,
|
||||
EFlipType,
|
||||
EChildrenRenderOrder,
|
||||
EGroupLayoutType,
|
||||
EPopupDirection,
|
||||
ERelationType,
|
||||
EFillMethod,
|
||||
EFillOrigin,
|
||||
EObjectPropID,
|
||||
EGearType,
|
||||
EEaseType,
|
||||
EBlendMode,
|
||||
ETransitionActionType,
|
||||
EGraphType,
|
||||
} from '@esengine/fairygui';
|
||||
|
||||
// Re-export conflicting types with FGUI prefix
|
||||
export {
|
||||
Timer as FGUITimer,
|
||||
Stage as FGUIStage,
|
||||
ServiceContainer as FGUIServiceContainer,
|
||||
Point as FGUIPoint,
|
||||
Rectangle as FGUIRectangle,
|
||||
} from '@esengine/fairygui';
|
||||
|
||||
export type {
|
||||
// FairyGUI types
|
||||
IFGUIComponentData,
|
||||
RenderSubmitCallback,
|
||||
ItemRenderer,
|
||||
ItemProvider,
|
||||
IUISource,
|
||||
TypedEventListener,
|
||||
EventListener,
|
||||
FGUIEventType,
|
||||
IEventContext,
|
||||
IInputEventData,
|
||||
IFUIAsset,
|
||||
IEngineRenderData,
|
||||
IFGUIRenderDataProvider,
|
||||
TextureResolverFn,
|
||||
TweenCallback,
|
||||
} from '@esengine/fairygui';
|
||||
|
||||
// ============================================================================
|
||||
// Audio System (@esengine/audio)
|
||||
// 音频系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/audio';
|
||||
|
||||
// ============================================================================
|
||||
// Camera System (@esengine/camera)
|
||||
// 摄像机系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/camera';
|
||||
|
||||
// ============================================================================
|
||||
// Particle System (@esengine/particle)
|
||||
// 粒子系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/particle';
|
||||
|
||||
// ============================================================================
|
||||
// Physics 2D System (@esengine/physics-rapier2d)
|
||||
// 2D 物理系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/physics-rapier2d';
|
||||
|
||||
// ============================================================================
|
||||
// Tilemap System (@esengine/tilemap)
|
||||
// 瓦片地图系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/tilemap';
|
||||
|
||||
// ============================================================================
|
||||
// Behavior Tree System (@esengine/behavior-tree)
|
||||
// 行为树系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/behavior-tree';
|
||||
|
||||
// ============================================================================
|
||||
// Asset System (@esengine/asset-system)
|
||||
// 资产系统
|
||||
// ============================================================================
|
||||
export * from '@esengine/asset-system';
|
||||
10
packages/tools/sdk/tsconfig.build.json
Normal file
10
packages/tools/sdk/tsconfig.build.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
53
packages/tools/sdk/tsconfig.json
Normal file
53
packages/tools/sdk/tsconfig.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/engine-core"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/math"
|
||||
},
|
||||
{
|
||||
"path": "../../rendering/sprite"
|
||||
},
|
||||
{
|
||||
"path": "../../rendering/fairygui"
|
||||
},
|
||||
{
|
||||
"path": "../../rendering/audio"
|
||||
},
|
||||
{
|
||||
"path": "../../rendering/camera"
|
||||
},
|
||||
{
|
||||
"path": "../../rendering/particle"
|
||||
},
|
||||
{
|
||||
"path": "../../physics/physics-rapier2d"
|
||||
},
|
||||
{
|
||||
"path": "../../rendering/tilemap"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/behavior-tree"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/asset-system"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/tools/sdk/tsup.config.ts
Normal file
7
packages/tools/sdk/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
137
packages/tools/worker-generator/README.md
Normal file
137
packages/tools/worker-generator/README.md
Normal 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
|
||||
56
packages/tools/worker-generator/package.json
Normal file
56
packages/tools/worker-generator/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@esengine/worker-generator",
|
||||
"version": "1.0.2",
|
||||
"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/esengine.git",
|
||||
"directory": "packages/worker-generator"
|
||||
},
|
||||
"homepage": "https://github.com/esengine/esengine/tree/master/packages/worker-generator",
|
||||
"bugs": {
|
||||
"url": "https://github.com/esengine/esengine/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.1.0",
|
||||
"ts-morph": "^21.0.1",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
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>;
|
||||
}
|
||||
20
packages/tools/worker-generator/tsconfig.json
Normal file
20
packages/tools/worker-generator/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user