feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)

* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
This commit is contained in:
YHH
2025-12-03 22:15:22 +08:00
committed by GitHub
parent caf7622aa0
commit 63f006ab62
496 changed files with 77601 additions and 4067 deletions

View File

@@ -0,0 +1,309 @@
/**
* Build Service.
* 构建服务。
*
* Manages build pipelines and executes build tasks.
* 管理构建管线和执行构建任务。
*/
import type { IService } from '@esengine/ecs-framework';
import type {
IBuildPipeline,
IBuildPipelineRegistry,
BuildPlatform,
BuildConfig,
BuildResult,
BuildProgress
} from './IBuildPipeline';
import { BuildStatus } from './IBuildPipeline';
/**
* Build task.
* 构建任务。
*/
export interface BuildTask {
/** Task ID | 任务 ID */
id: string;
/** Target platform | 目标平台 */
platform: BuildPlatform;
/** Build configuration | 构建配置 */
config: BuildConfig;
/** Current progress | 当前进度 */
progress: BuildProgress;
/** Start time | 开始时间 */
startTime: Date;
/** End time | 结束时间 */
endTime?: Date;
/** Abort controller | 中止控制器 */
abortController: AbortController;
}
/**
* Build Service.
* 构建服务。
*
* Provides build pipeline registration and build task management.
* 提供构建管线注册和构建任务管理。
*
* @example
* ```typescript
* const buildService = services.resolve(BuildService);
*
* // Register build pipeline | 注册构建管线
* buildService.registerPipeline(new WebBuildPipeline());
* buildService.registerPipeline(new WeChatBuildPipeline());
*
* // Execute build | 执行构建
* const result = await buildService.build({
* platform: BuildPlatform.Web,
* outputPath: './dist',
* isRelease: true,
* sourceMap: false
* }, (progress) => {
* console.log(`${progress.message} (${progress.progress}%)`);
* });
* ```
*/
export class BuildService implements IService, IBuildPipelineRegistry {
private _pipelines = new Map<BuildPlatform, IBuildPipeline>();
private _currentTask: BuildTask | null = null;
private _taskHistory: BuildTask[] = [];
private _maxHistorySize = 10;
/**
* Dispose service resources.
* 释放服务资源。
*/
dispose(): void {
this.cancelBuild();
this._pipelines.clear();
this._taskHistory = [];
}
/**
* Register build pipeline.
* 注册构建管线。
*
* @param pipeline - Build pipeline instance | 构建管线实例
*/
register(pipeline: IBuildPipeline): void {
if (this._pipelines.has(pipeline.platform)) {
console.warn(`[BuildService] Overwriting existing pipeline: ${pipeline.platform} | 覆盖已存在的构建管线: ${pipeline.platform}`);
}
this._pipelines.set(pipeline.platform, pipeline);
console.log(`[BuildService] Registered pipeline: ${pipeline.displayName} | 注册构建管线: ${pipeline.displayName}`);
}
/**
* Get build pipeline.
* 获取构建管线。
*
* @param platform - Target platform | 目标平台
* @returns Build pipeline, or undefined if not registered | 构建管线,如果未注册则返回 undefined
*/
get(platform: BuildPlatform): IBuildPipeline | undefined {
return this._pipelines.get(platform);
}
/**
* Get all registered build pipelines.
* 获取所有已注册的构建管线。
*
* @returns Build pipeline list | 构建管线列表
*/
getAll(): IBuildPipeline[] {
return Array.from(this._pipelines.values());
}
/**
* Check if platform is registered.
* 检查平台是否已注册。
*
* @param platform - Target platform | 目标平台
* @returns Whether registered | 是否已注册
*/
has(platform: BuildPlatform): boolean {
return this._pipelines.has(platform);
}
/**
* Get available build platforms.
* 获取可用的构建平台。
*
* Checks availability of each registered platform.
* 检查每个已注册平台的可用性。
*
* @returns Available platforms and their status | 可用平台及其状态
*/
async getAvailablePlatforms(): Promise<Array<{
platform: BuildPlatform;
displayName: string;
description?: string;
available: boolean;
reason?: string;
}>> {
const results = [];
for (const pipeline of this._pipelines.values()) {
const availability = await pipeline.checkAvailability();
results.push({
platform: pipeline.platform,
displayName: pipeline.displayName,
description: pipeline.description,
available: availability.available,
reason: availability.reason
});
}
return results;
}
/**
* Execute build.
* 执行构建。
*
* @param config - Build configuration | 构建配置
* @param onProgress - Progress callback | 进度回调
* @returns Build result | 构建结果
*/
async build(
config: BuildConfig,
onProgress?: (progress: BuildProgress) => void
): Promise<BuildResult> {
// Check if there's an ongoing build | 检查是否有正在进行的构建
if (this._currentTask) {
throw new Error('A build task is already in progress | 已有构建任务正在进行中');
}
// Get build pipeline | 获取构建管线
const pipeline = this._pipelines.get(config.platform);
if (!pipeline) {
throw new Error(`Pipeline not found for platform ${config.platform} | 未找到平台 ${config.platform} 的构建管线`);
}
// Validate configuration | 验证配置
const errors = pipeline.validateConfig(config);
if (errors.length > 0) {
throw new Error(`Invalid build configuration | 构建配置无效:\n${errors.join('\n')}`);
}
// Create build task | 创建构建任务
const abortController = new AbortController();
const task: BuildTask = {
id: this._generateTaskId(),
platform: config.platform,
config,
progress: {
status: 'preparing' as BuildStatus,
message: 'Preparing build... | 准备构建...',
progress: 0,
currentStep: 0,
totalSteps: 0,
warnings: []
},
startTime: new Date(),
abortController
};
this._currentTask = task;
try {
// Execute build | 执行构建
const result = await pipeline.build(
config,
(progress) => {
task.progress = progress;
onProgress?.(progress);
},
abortController.signal
);
// Update task status | 更新任务状态
task.endTime = new Date();
task.progress.status = result.success ? BuildStatus.Completed : BuildStatus.Failed;
// Add to history | 添加到历史
this._addToHistory(task);
return result;
} catch (error) {
// Handle error | 处理错误
task.endTime = new Date();
task.progress.status = BuildStatus.Failed;
task.progress.error = error instanceof Error ? error.message : String(error);
this._addToHistory(task);
return {
success: false,
platform: config.platform,
outputPath: config.outputPath,
duration: task.endTime.getTime() - task.startTime.getTime(),
outputFiles: [],
warnings: task.progress.warnings,
error: task.progress.error
};
} finally {
this._currentTask = null;
}
}
/**
* Cancel current build.
* 取消当前构建。
*/
cancelBuild(): void {
if (this._currentTask) {
this._currentTask.abortController.abort();
this._currentTask.progress.status = BuildStatus.Cancelled;
console.log('[BuildService] Build cancelled | 构建已取消');
}
}
/**
* Get current build task.
* 获取当前构建任务。
*
* @returns Current task, or null if none | 当前任务,如果没有则返回 null
*/
getCurrentTask(): BuildTask | null {
return this._currentTask;
}
/**
* Get build history.
* 获取构建历史。
*
* @returns History task list (newest first) | 历史任务列表(最新的在前)
*/
getHistory(): BuildTask[] {
return [...this._taskHistory];
}
/**
* Clear build history.
* 清除构建历史。
*/
clearHistory(): void {
this._taskHistory = [];
}
/**
* Generate task ID.
* 生成任务 ID。
*/
private _generateTaskId(): string {
return `build-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Add task to history.
* 添加任务到历史。
*/
private _addToHistory(task: BuildTask): void {
this._taskHistory.unshift(task);
if (this._taskHistory.length > this._maxHistorySize) {
this._taskHistory.pop();
}
}
}

View File

@@ -0,0 +1,319 @@
/**
* Build Pipeline Interface.
* 构建管线接口。
*
* Defines the common process and configuration for platform builds.
* 定义平台构建的通用流程和配置。
*/
/**
* Build target platform.
* 构建目标平台。
*/
export enum BuildPlatform {
/** Web/H5 browser | Web/H5 浏览器 */
Web = 'web',
/** WeChat MiniGame | 微信小游戏 */
WeChatMiniGame = 'wechat-minigame',
/** ByteDance MiniGame | 字节跳动小游戏 */
ByteDanceMiniGame = 'bytedance-minigame',
/** Alipay MiniGame | 支付宝小游戏 */
AlipayMiniGame = 'alipay-minigame',
/** Desktop application (Tauri) | 桌面应用 (Tauri) */
Desktop = 'desktop',
/** Android | Android */
Android = 'android',
/** iOS | iOS */
iOS = 'ios'
}
/**
* Build status.
* 构建状态。
*/
export enum BuildStatus {
/** Idle | 空闲 */
Idle = 'idle',
/** Preparing | 准备中 */
Preparing = 'preparing',
/** Compiling | 编译中 */
Compiling = 'compiling',
/** Packaging assets | 打包资源 */
Packaging = 'packaging',
/** Copying files | 复制文件 */
Copying = 'copying',
/** Post-processing | 后处理 */
PostProcessing = 'post-processing',
/** Completed | 完成 */
Completed = 'completed',
/** Failed | 失败 */
Failed = 'failed',
/** Cancelled | 已取消 */
Cancelled = 'cancelled'
}
/**
* Build progress information.
* 构建进度信息。
*/
export interface BuildProgress {
/** Current status | 当前状态 */
status: BuildStatus;
/** Current step description | 当前步骤描述 */
message: string;
/** Overall progress (0-100) | 总体进度 (0-100) */
progress: number;
/** Current step index | 当前步骤索引 */
currentStep: number;
/** Total step count | 总步骤数 */
totalSteps: number;
/** Warning list | 警告列表 */
warnings: string[];
/** Error message (if failed) | 错误信息(如果失败) */
error?: string;
}
/**
* Build configuration base class.
* 构建配置基类。
*/
export interface BuildConfig {
/** Target platform | 目标平台 */
platform: BuildPlatform;
/** Output directory | 输出目录 */
outputPath: string;
/** Whether release build (compression, optimization) | 是否为发布构建(压缩、优化) */
isRelease: boolean;
/** Whether to generate source map | 是否生成 source map */
sourceMap: boolean;
/** Scene list to include (empty means all) | 要包含的场景列表(空表示全部) */
scenes?: string[];
/** Plugin list to include (empty means all enabled) | 要包含的插件列表(空表示全部启用的) */
plugins?: string[];
/**
* Enabled module IDs (whitelist approach).
* 启用的模块 ID 列表(白名单方式)。
* If set, only these modules will be included.
* 如果设置,只会包含这些模块。
*/
enabledModules?: string[];
/**
* Disabled module IDs (blacklist approach).
* 禁用的模块 ID 列表(黑名单方式)。
* If set, all modules EXCEPT these will be included.
* 如果设置,会包含除了这些之外的所有模块。
* Takes precedence over enabledModules.
* 优先于 enabledModules。
*/
disabledModules?: string[];
}
/**
* Web platform build configuration.
* Web 平台构建配置。
*/
export interface WebBuildConfig extends BuildConfig {
platform: BuildPlatform.Web;
/** Output format | 输出格式 */
format: 'iife' | 'esm';
/**
* Whether to bundle all modules into a single JS file.
* 是否将所有模块打包成单个 JS 文件。
* - true: Bundle into one runtime.browser.js (smaller total size, single request)
* - false: Keep modules separate (better caching, parallel loading)
* Default: true
*/
bundleModules: boolean;
/** Whether to generate HTML file | 是否生成 HTML 文件 */
generateHtml: boolean;
/** HTML template path | HTML 模板路径 */
htmlTemplate?: string;
}
/**
* WeChat MiniGame build configuration.
* 微信小游戏构建配置。
*/
export interface WeChatBuildConfig extends BuildConfig {
platform: BuildPlatform.WeChatMiniGame;
/** AppID | AppID */
appId: string;
/** Whether to use subpackages | 是否分包 */
useSubpackages: boolean;
/** Main package size limit (KB) | 主包大小限制 (KB) */
mainPackageLimit: number;
/** Whether to enable plugins | 是否启用插件 */
usePlugins: boolean;
}
/**
* Build result.
* 构建结果。
*/
export interface BuildResult {
/** Whether successful | 是否成功 */
success: boolean;
/** Target platform | 目标平台 */
platform: BuildPlatform;
/** Output directory | 输出目录 */
outputPath: string;
/** Build duration (milliseconds) | 构建耗时(毫秒) */
duration: number;
/** Output file list | 输出文件列表 */
outputFiles: string[];
/** Warning list | 警告列表 */
warnings: string[];
/** Error message (if failed) | 错误信息(如果失败) */
error?: string;
/** Build statistics | 构建统计 */
stats?: {
/** Total file size (bytes) | 总文件大小 (bytes) */
totalSize: number;
/** JS file size | JS 文件大小 */
jsSize: number;
/** WASM file size | WASM 文件大小 */
wasmSize: number;
/** Asset file size | 资源文件大小 */
assetsSize: number;
};
}
/**
* Build step.
* 构建步骤。
*/
export interface BuildStep {
/** Step ID | 步骤 ID */
id: string;
/** Step name | 步骤名称 */
name: string;
/** Execute function | 执行函数 */
execute: (context: BuildContext) => Promise<void>;
/** Whether skippable | 是否可跳过 */
optional?: boolean;
}
/**
* Build context.
* 构建上下文。
*
* Shared state during the build process.
* 在构建过程中共享的状态。
*/
export interface BuildContext {
/** Build configuration | 构建配置 */
config: BuildConfig;
/** Project root directory | 项目根目录 */
projectRoot: string;
/** Temporary directory | 临时目录 */
tempDir: string;
/** Output directory | 输出目录 */
outputDir: string;
/** Progress report callback | 进度报告回调 */
reportProgress: (message: string, progress?: number) => void;
/** Add warning | 添加警告 */
addWarning: (warning: string) => void;
/** Abort signal | 中止信号 */
abortSignal: AbortSignal;
/** Shared data (passed between steps) | 共享数据(步骤间传递) */
data: Map<string, any>;
}
/**
* Build pipeline interface.
* 构建管线接口。
*
* Each platform implements its own build pipeline.
* 每个平台实现自己的构建管线。
*/
export interface IBuildPipeline {
/** Platform identifier | 平台标识 */
readonly platform: BuildPlatform;
/** Platform display name | 平台显示名称 */
readonly displayName: string;
/** Platform icon | 平台图标 */
readonly icon?: string;
/** Platform description | 平台描述 */
readonly description?: string;
/**
* Get default configuration.
* 获取默认配置。
*/
getDefaultConfig(): BuildConfig;
/**
* Validate configuration.
* 验证配置是否有效。
*
* @param config - Build configuration | 构建配置
* @returns Validation error list (empty means valid) | 验证错误列表(空表示有效)
*/
validateConfig(config: BuildConfig): string[];
/**
* Get build steps.
* 获取构建步骤。
*
* @param config - Build configuration | 构建配置
* @returns Build step list | 构建步骤列表
*/
getSteps(config: BuildConfig): BuildStep[];
/**
* Execute build.
* 执行构建。
*
* @param config - Build configuration | 构建配置
* @param onProgress - Progress callback | 进度回调
* @param abortSignal - Abort signal | 中止信号
* @returns Build result | 构建结果
*/
build(
config: BuildConfig,
onProgress?: (progress: BuildProgress) => void,
abortSignal?: AbortSignal
): Promise<BuildResult>;
/**
* Check platform availability.
* 检查平台是否可用。
*
* For example, check if necessary tools are installed.
* 例如检查必要的工具是否安装。
*/
checkAvailability(): Promise<{ available: boolean; reason?: string }>;
}
/**
* Build pipeline registry interface.
* 构建管线注册表接口。
*/
export interface IBuildPipelineRegistry {
/**
* Register build pipeline.
* 注册构建管线。
*/
register(pipeline: IBuildPipeline): void;
/**
* Get build pipeline.
* 获取构建管线。
*/
get(platform: BuildPlatform): IBuildPipeline | undefined;
/**
* Get all registered pipelines.
* 获取所有已注册的管线。
*/
getAll(): IBuildPipeline[];
/**
* Check if platform is registered.
* 检查平台是否已注册。
*/
has(platform: BuildPlatform): boolean;
}

View File

@@ -0,0 +1,26 @@
/**
* Build System.
* 构建系统。
*
* Provides cross-platform project build capabilities.
* 提供跨平台的项目构建能力。
*/
export {
BuildPlatform,
BuildStatus,
type BuildProgress,
type BuildConfig,
type WebBuildConfig,
type WeChatBuildConfig,
type BuildResult,
type BuildStep,
type BuildContext,
type IBuildPipeline,
type IBuildPipelineRegistry
} from './IBuildPipeline';
export { BuildService, type BuildTask } from './BuildService';
// Build pipelines | 构建管线
export { WebBuildPipeline, WeChatBuildPipeline, type IBuildFileSystem } from './pipelines';

View File

@@ -0,0 +1,730 @@
/**
* WeChat MiniGame Build Pipeline.
* 微信小游戏构建管线。
*
* Packages the project as a format that can run on WeChat MiniGame platform.
* 将项目打包为可在微信小游戏平台运行的格式。
*/
import type {
IBuildPipeline,
BuildConfig,
BuildResult,
BuildProgress,
BuildStep,
BuildContext,
WeChatBuildConfig
} from '../IBuildPipeline';
import { BuildPlatform, BuildStatus } from '../IBuildPipeline';
import type { IBuildFileSystem } from './WebBuildPipeline';
/**
* WASM file configuration to be copied.
* 需要复制的 WASM 文件配置。
*/
interface WasmFileConfig {
/** Source file path (relative to node_modules) | 源文件路径(相对于 node_modules */
source: string;
/** Target file path (relative to output directory) | 目标文件路径(相对于输出目录) */
target: string;
/** Description | 描述 */
description: string;
}
/**
* WeChat MiniGame Build Pipeline.
* 微信小游戏构建管线。
*
* Build steps:
* 构建步骤:
* 1. Prepare output directory | 准备输出目录
* 2. Compile TypeScript | 编译 TypeScript
* 3. Bundle runtime (using WeChat adapter) | 打包运行时(使用微信适配器)
* 4. Copy WASM files | 复制 WASM 文件
* 5. Copy asset files | 复制资源文件
* 6. Generate game.json | 生成 game.json
* 7. Generate game.js | 生成 game.js
* 8. Post-process | 后处理
*/
export class WeChatBuildPipeline implements IBuildPipeline {
readonly platform = BuildPlatform.WeChatMiniGame;
readonly displayName = 'WeChat MiniGame | 微信小游戏';
readonly description = 'Build as a format that can run on WeChat MiniGame platform | 构建为可在微信小游戏平台运行的格式';
readonly icon = 'message-circle';
private _fileSystem: IBuildFileSystem | null = null;
/**
* WASM file list to be copied.
* 需要复制的 WASM 文件列表。
*/
private readonly _wasmFiles: WasmFileConfig[] = [
{
source: '@dimforge/rapier2d/rapier_wasm2d_bg.wasm',
target: 'wasm/rapier2d_bg.wasm',
description: 'Rapier2D Physics Engine | Rapier2D 物理引擎'
}
// More WASM files can be added here | 可以在这里添加更多 WASM 文件
];
/**
* Set build file system service.
* 设置构建文件系统服务。
*
* @param fileSystem - Build file system service | 构建文件系统服务
*/
setFileSystem(fileSystem: IBuildFileSystem): void {
this._fileSystem = fileSystem;
}
/**
* Get default configuration.
* 获取默认配置。
*/
getDefaultConfig(): WeChatBuildConfig {
return {
platform: BuildPlatform.WeChatMiniGame,
outputPath: './dist/wechat',
isRelease: true,
sourceMap: false,
appId: '',
useSubpackages: false,
mainPackageLimit: 4096, // 4MB
usePlugins: false
};
}
/**
* Validate configuration.
* 验证配置。
*
* @param config - Build configuration | 构建配置
* @returns Validation error list | 验证错误列表
*/
validateConfig(config: BuildConfig): string[] {
const errors: string[] = [];
const wxConfig = config as WeChatBuildConfig;
if (!wxConfig.outputPath) {
errors.push('Output path cannot be empty | 输出路径不能为空');
}
if (!wxConfig.appId) {
errors.push('AppID cannot be empty | AppID 不能为空');
} else if (!/^wx[a-f0-9]{16}$/.test(wxConfig.appId)) {
errors.push('AppID format is incorrect (should be 18 characters starting with wx) | AppID 格式不正确(应为 wx 开头的18位字符');
}
if (wxConfig.mainPackageLimit < 1024) {
errors.push('Main package size limit cannot be less than 1MB | 主包大小限制不能小于 1MB');
}
return errors;
}
/**
* Get build steps.
* 获取构建步骤。
*
* @param config - Build configuration | 构建配置
* @returns Build step list | 构建步骤列表
*/
getSteps(config: BuildConfig): BuildStep[] {
const wxConfig = config as WeChatBuildConfig;
const steps: BuildStep[] = [
{
id: 'prepare',
name: 'Prepare output directory | 准备输出目录',
execute: this._prepareOutputDir.bind(this)
},
{
id: 'compile',
name: 'Compile TypeScript | 编译 TypeScript',
execute: this._compileTypeScript.bind(this)
},
{
id: 'bundle-runtime',
name: 'Bundle runtime | 打包运行时',
execute: this._bundleRuntime.bind(this)
},
{
id: 'copy-wasm',
name: 'Copy WASM files | 复制 WASM 文件',
execute: this._copyWasmFiles.bind(this)
},
{
id: 'copy-assets',
name: 'Copy asset files | 复制资源文件',
execute: this._copyAssets.bind(this)
},
{
id: 'generate-game-json',
name: 'Generate game.json | 生成 game.json',
execute: this._generateGameJson.bind(this)
},
{
id: 'generate-game-js',
name: 'Generate game.js | 生成 game.js',
execute: this._generateGameJs.bind(this)
},
{
id: 'generate-project-config',
name: 'Generate project.config.json | 生成 project.config.json',
execute: this._generateProjectConfig.bind(this)
}
];
if (wxConfig.useSubpackages) {
steps.push({
id: 'split-subpackages',
name: 'Process subpackages | 分包处理',
execute: this._splitSubpackages.bind(this)
});
}
if (wxConfig.isRelease) {
steps.push({
id: 'optimize',
name: 'Optimize and compress | 优化压缩',
execute: this._optimize.bind(this),
optional: true
});
}
return steps;
}
/**
* Execute build.
* 执行构建。
*
* @param config - Build configuration | 构建配置
* @param onProgress - Progress callback | 进度回调
* @param abortSignal - Abort signal | 中止信号
* @returns Build result | 构建结果
*/
async build(
config: BuildConfig,
onProgress?: (progress: BuildProgress) => void,
abortSignal?: AbortSignal
): Promise<BuildResult> {
const startTime = Date.now();
const warnings: string[] = [];
const outputFiles: string[] = [];
const steps = this.getSteps(config);
const totalSteps = steps.length;
// Infer project root from output path | 从输出路径推断项目根目录
// outputPath is typically: /path/to/project/build/wechat-minigame
// So we go up 2 levels to get project root | 所以我们向上2级获取项目根目录
const outputPathParts = config.outputPath.replace(/\\/g, '/').split('/');
const buildIndex = outputPathParts.lastIndexOf('build');
const projectRoot = buildIndex > 0
? outputPathParts.slice(0, buildIndex).join('/')
: '.';
// Create build context | 创建构建上下文
const context: BuildContext = {
config,
projectRoot,
tempDir: `${projectRoot}/temp/build-wechat`,
outputDir: config.outputPath,
reportProgress: (message, progress) => {
// Handled below | 在下面处理
},
addWarning: (warning) => {
warnings.push(warning);
},
abortSignal: abortSignal || new AbortController().signal,
data: new Map()
};
// Store file system and WASM config for subsequent steps | 存储文件系统和 WASM 配置供后续步骤使用
context.data.set('fileSystem', this._fileSystem);
context.data.set('wasmFiles', this._wasmFiles);
try {
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (abortSignal?.aborted) {
return {
success: false,
platform: config.platform,
outputPath: config.outputPath,
duration: Date.now() - startTime,
outputFiles,
warnings,
error: 'Build cancelled | 构建已取消'
};
}
onProgress?.({
status: this._getStatusForStep(step.id),
message: step.name,
progress: Math.round((i / totalSteps) * 100),
currentStep: i + 1,
totalSteps,
warnings
});
await step.execute(context);
}
// Get output stats | 获取输出统计
let stats: BuildResult['stats'] | undefined;
if (this._fileSystem) {
try {
const totalSize = await this._fileSystem.getDirectorySize(config.outputPath);
stats = {
totalSize,
jsSize: 0,
wasmSize: 0,
assetsSize: 0
};
} catch {
// Ignore stats error | 忽略统计错误
}
}
onProgress?.({
status: BuildStatus.Completed,
message: 'Build completed | 构建完成',
progress: 100,
currentStep: totalSteps,
totalSteps,
warnings
});
return {
success: true,
platform: config.platform,
outputPath: config.outputPath,
duration: Date.now() - startTime,
outputFiles,
warnings,
stats
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
onProgress?.({
status: BuildStatus.Failed,
message: 'Build failed | 构建失败',
progress: 0,
currentStep: 0,
totalSteps,
warnings,
error: errorMessage
});
return {
success: false,
platform: config.platform,
outputPath: config.outputPath,
duration: Date.now() - startTime,
outputFiles,
warnings,
error: errorMessage
};
}
}
/**
* Check availability.
* 检查可用性。
*/
async checkAvailability(): Promise<{ available: boolean; reason?: string }> {
// TODO: Check if WeChat DevTools is installed | 检查微信开发者工具是否安装
return { available: true };
}
/**
* Get build status for step.
* 获取步骤的构建状态。
*/
private _getStatusForStep(stepId: string): BuildStatus {
switch (stepId) {
case 'prepare':
return BuildStatus.Preparing;
case 'compile':
case 'bundle-runtime':
return BuildStatus.Compiling;
case 'copy-wasm':
case 'copy-assets':
return BuildStatus.Copying;
case 'generate-game-json':
case 'generate-game-js':
case 'generate-project-config':
case 'split-subpackages':
case 'optimize':
return BuildStatus.PostProcessing;
default:
return BuildStatus.Compiling;
}
}
// ==================== Build Step Implementations | 构建步骤实现 ====================
/**
* Prepare output directory.
* 准备输出目录。
*/
private async _prepareOutputDir(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
if (fs) {
await fs.prepareBuildDirectory(context.outputDir);
console.log('[WeChatBuild] Prepared output directory | 准备输出目录:', context.outputDir);
} else {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
}
}
/**
* Compile TypeScript.
* 编译 TypeScript。
*/
private async _compileTypeScript(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
const wxConfig = context.config as WeChatBuildConfig;
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
// Find user script entry point | 查找用户脚本入口点
const scriptsDir = `${context.projectRoot}/scripts`;
const hasScripts = await fs.pathExists(scriptsDir);
if (!hasScripts) {
console.log('[WeChatBuild] No scripts directory found | 未找到脚本目录');
return;
}
// Find entry file | 查找入口文件
const possibleEntries = ['index.ts', 'main.ts', 'game.ts', 'index.js', 'main.js'];
let entryFile: string | null = null;
for (const entry of possibleEntries) {
const entryPath = `${scriptsDir}/${entry}`;
if (await fs.pathExists(entryPath)) {
entryFile = entryPath;
break;
}
}
if (!entryFile) {
console.log('[WeChatBuild] No entry file found | 未找到入口文件');
return;
}
// Bundle user scripts for WeChat | 为微信打包用户脚本
const result = await fs.bundleScripts({
entryPoints: [entryFile],
outputDir: `${context.outputDir}/libs`,
format: 'iife', // WeChat uses iife format | 微信使用 iife 格式
bundleName: 'user-code',
minify: wxConfig.isRelease,
sourceMap: wxConfig.sourceMap,
external: ['@esengine/ecs-framework', '@esengine/core'],
projectRoot: context.projectRoot,
define: {
'process.env.NODE_ENV': wxConfig.isRelease ? '"production"' : '"development"',
'wx': 'wx' // WeChat global | 微信全局对象
}
});
if (!result.success) {
throw new Error(`User code compilation failed | 用户代码编译失败: ${result.error}`);
}
result.warnings.forEach(w => context.addWarning(w));
console.log('[WeChatBuild] Compiled TypeScript | 编译 TypeScript:', result.outputFile);
}
/**
* Bundle runtime.
* 打包运行时。
*/
private async _bundleRuntime(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
// Copy pre-built runtime files with WeChat adapter | 复制带微信适配器的预构建运行时文件
const runtimeSrc = `${context.projectRoot}/node_modules/@esengine/platform-wechat/dist`;
const runtimeDst = `${context.outputDir}/libs`;
const hasWxRuntime = await fs.pathExists(runtimeSrc);
if (hasWxRuntime) {
const count = await fs.copyDirectory(runtimeSrc, runtimeDst, ['*.js']);
console.log(`[WeChatBuild] Copied WeChat runtime | 复制微信运行时: ${count} files`);
} else {
// Fallback to standard runtime | 回退到标准运行时
const stdRuntimeSrc = `${context.projectRoot}/node_modules/@esengine/ecs-framework/dist`;
const hasStdRuntime = await fs.pathExists(stdRuntimeSrc);
if (hasStdRuntime) {
const count = await fs.copyDirectory(stdRuntimeSrc, runtimeDst, ['*.js']);
console.log(`[WeChatBuild] Copied standard runtime | 复制标准运行时: ${count} files`);
context.addWarning('Using standard runtime, some WeChat-specific features may not work | 使用标准运行时,部分微信特有功能可能不可用');
} else {
context.addWarning('Runtime not found | 未找到运行时');
}
}
}
/**
* Copy WASM files.
* 复制 WASM 文件。
*
* This is a critical step for WeChat MiniGame build.
* 这是微信小游戏构建的关键步骤。
*/
private async _copyWasmFiles(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
const wasmFiles = context.data.get('wasmFiles') as WasmFileConfig[];
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
console.log('[WeChatBuild] Copying WASM files | 复制 WASM 文件:');
for (const file of wasmFiles) {
const sourcePath = `${context.projectRoot}/node_modules/${file.source}`;
const targetPath = `${context.outputDir}/${file.target}`;
const exists = await fs.pathExists(sourcePath);
if (exists) {
await fs.copyFile(sourcePath, targetPath);
console.log(` - ${file.description}: ${file.source} -> ${file.target}`);
} else {
context.addWarning(`WASM file not found | 未找到 WASM 文件: ${file.source}`);
}
}
// Copy engine WASM | 复制引擎 WASM
const engineWasmSrc = `${context.projectRoot}/node_modules/@esengine/es-engine/pkg`;
const hasEngineWasm = await fs.pathExists(engineWasmSrc);
if (hasEngineWasm) {
const count = await fs.copyDirectory(engineWasmSrc, `${context.outputDir}/wasm`, ['*.wasm']);
console.log(`[WeChatBuild] Copied engine WASM | 复制引擎 WASM: ${count} files`);
}
context.addWarning(
'iOS WeChat requires WXWebAssembly for loading WASM | iOS 微信需要使用 WXWebAssembly 加载 WASM'
);
}
/**
* Copy asset files.
* 复制资源文件。
*/
private async _copyAssets(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
// Copy scenes | 复制场景
const scenesDir = `${context.projectRoot}/scenes`;
if (await fs.pathExists(scenesDir)) {
const count = await fs.copyDirectory(scenesDir, `${context.outputDir}/scenes`);
console.log(`[WeChatBuild] Copied scenes | 复制场景: ${count} files`);
}
// Copy assets | 复制资源
const assetsDir = `${context.projectRoot}/assets`;
if (await fs.pathExists(assetsDir)) {
const count = await fs.copyDirectory(assetsDir, `${context.outputDir}/assets`);
console.log(`[WeChatBuild] Copied assets | 复制资源: ${count} files`);
}
}
/**
* Generate game.json.
* 生成 game.json。
*/
private async _generateGameJson(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
const wxConfig = context.config as WeChatBuildConfig;
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
const gameJson: Record<string, unknown> = {
deviceOrientation: 'portrait',
showStatusBar: false,
networkTimeout: {
request: 60000,
connectSocket: 60000,
uploadFile: 60000,
downloadFile: 60000
},
// Declare WebAssembly usage | 声明使用 WebAssembly
enableWebAssembly: true
};
if (wxConfig.useSubpackages) {
gameJson.subpackages = [];
}
if (wxConfig.usePlugins) {
gameJson.plugins = {};
}
const jsonContent = JSON.stringify(gameJson, null, 2);
await fs.writeJsonFile(`${context.outputDir}/game.json`, jsonContent);
console.log('[WeChatBuild] Generated game.json | 生成 game.json');
}
/**
* Generate game.js.
* 生成 game.js。
*/
private async _generateGameJs(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
const gameJs = `/**
* WeChat MiniGame entry point.
* 微信小游戏入口。
*
* Auto-generated, do not modify manually.
* 自动生成,请勿手动修改。
*/
// WeChat adapter | 微信适配器
require('./libs/weapp-adapter.js');
// Load runtime | 加载运行时
require('./libs/esengine-runtime.js');
// Load user code | 加载用户代码
require('./libs/user-code.js');
// Initialize game | 初始化游戏
(async function() {
try {
// Load WASM (use WXWebAssembly on iOS) | 加载 WASMiOS 上使用 WXWebAssembly
const isIOS = wx.getSystemInfoSync().platform === 'ios';
if (isIOS) {
// iOS uses WXWebAssembly | iOS 使用 WXWebAssembly
await ECS.initWasm('./wasm/es_engine_bg.wasm', { useWXWebAssembly: true });
} else {
await ECS.initWasm('./wasm/es_engine_bg.wasm');
}
// Create runtime | 创建运行时
const canvas = wx.createCanvas();
const runtime = ECS.createRuntime({
canvas: canvas,
platform: 'wechat'
});
// Load scene and start | 加载场景并启动
await runtime.loadScene('./scenes/main.ecs');
runtime.start();
console.log('[Game] Started successfully | 游戏启动成功');
} catch (error) {
console.error('[Game] Failed to start | 启动失败:', error);
}
})();
`;
await fs.writeFile(`${context.outputDir}/game.js`, gameJs);
console.log('[WeChatBuild] Generated game.js | 生成 game.js');
}
/**
* Generate project.config.json.
* 生成 project.config.json。
*/
private async _generateProjectConfig(context: BuildContext): Promise<void> {
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
const wxConfig = context.config as WeChatBuildConfig;
if (!fs) {
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
return;
}
const projectConfig = {
description: 'ESEngine Game',
packOptions: {
ignore: [],
include: []
},
setting: {
urlCheck: false,
es6: true,
enhance: true,
postcss: false,
preloadBackgroundData: false,
minified: wxConfig.isRelease,
newFeature: true,
autoAudits: false,
coverView: true,
showShadowRootInWxmlPanel: true,
scopeDataCheck: false,
checkInvalidKey: true,
checkSiteMap: true,
uploadWithSourceMap: !wxConfig.isRelease,
compileHotReLoad: false,
babelSetting: {
ignore: [],
disablePlugins: [],
outputPath: ''
}
},
compileType: 'game',
libVersion: '2.25.0',
appid: wxConfig.appId,
projectname: 'ESEngine Game',
condition: {}
};
const jsonContent = JSON.stringify(projectConfig, null, 2);
await fs.writeJsonFile(`${context.outputDir}/project.config.json`, jsonContent);
console.log('[WeChatBuild] Generated project.config.json | 生成 project.config.json');
}
/**
* Process subpackages.
* 分包处理。
*/
private async _splitSubpackages(context: BuildContext): Promise<void> {
const wxConfig = context.config as WeChatBuildConfig;
console.log('[WeChatBuild] Processing subpackages, limit | 分包处理,限制:', wxConfig.mainPackageLimit, 'KB');
// TODO: Implement automatic subpackage splitting based on file sizes
// 实现基于文件大小的自动分包
context.addWarning('Subpackage splitting is not fully implemented yet | 分包功能尚未完全实现');
}
/**
* Optimize and compress.
* 优化压缩。
*/
private async _optimize(context: BuildContext): Promise<void> {
console.log('[WeChatBuild] Optimization complete | 优化完成');
// Minification is done during bundling | 压缩在打包时已完成
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
/**
* Build Pipelines.
* 构建管线。
*/
export { WebBuildPipeline, type IBuildFileSystem } from './WebBuildPipeline';
export { WeChatBuildPipeline } from './WeChatBuildPipeline';