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:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,88 @@
/**
* 微信小游戏音频子系统
*/
import type {
IPlatformAudioSubsystem,
IPlatformAudioContext
} from '@esengine/platform-common';
import { getWx, promisify } from '../utils';
/**
* 微信音频上下文包装
*/
class WeChatAudioContext implements IPlatformAudioContext {
private _ctx: WechatMinigame.InnerAudioContext;
constructor(ctx: WechatMinigame.InnerAudioContext) {
this._ctx = ctx;
}
get src(): string { return this._ctx.src; }
set src(value: string) { this._ctx.src = value; }
get autoplay(): boolean { return this._ctx.autoplay; }
set autoplay(value: boolean) { this._ctx.autoplay = value; }
get loop(): boolean { return this._ctx.loop; }
set loop(value: boolean) { this._ctx.loop = value; }
get volume(): number { return this._ctx.volume; }
set volume(value: number) { this._ctx.volume = value; }
get duration(): number { return this._ctx.duration; }
get currentTime(): number { return this._ctx.currentTime; }
get paused(): boolean { return this._ctx.paused; }
get buffered(): number { return this._ctx.buffered; }
play(): void { this._ctx.play(); }
pause(): void { this._ctx.pause(); }
stop(): void { this._ctx.stop(); }
seek(position: number): void { this._ctx.seek(position); }
destroy(): void { this._ctx.destroy(); }
onPlay(callback: () => void): void { this._ctx.onPlay(callback); }
onPause(callback: () => void): void { this._ctx.onPause(callback); }
onStop(callback: () => void): void { this._ctx.onStop(callback); }
onEnded(callback: () => void): void { this._ctx.onEnded(callback); }
onError(callback: (error: { errCode: number; errMsg: string }) => void): void {
this._ctx.onError(callback as any);
}
onTimeUpdate(callback: () => void): void { this._ctx.onTimeUpdate(callback); }
onCanplay(callback: () => void): void { this._ctx.onCanplay(callback); }
onSeeking(callback: () => void): void { this._ctx.onSeeking(callback); }
onSeeked(callback: () => void): void { this._ctx.onSeeked(callback); }
offPlay(callback: () => void): void { this._ctx.offPlay(callback); }
offPause(callback: () => void): void { this._ctx.offPause(callback); }
offStop(callback: () => void): void { this._ctx.offStop(callback); }
offEnded(callback: () => void): void { this._ctx.offEnded(callback); }
offError(callback: (error: { errCode: number; errMsg: string }) => void): void {
this._ctx.offError(callback as any);
}
offTimeUpdate(callback: () => void): void { this._ctx.offTimeUpdate(callback); }
}
/**
* 微信小游戏音频子系统实现
*/
export class WeChatAudioSubsystem implements IPlatformAudioSubsystem {
createAudioContext(_options?: { useWebAudioImplement?: boolean }): IPlatformAudioContext {
const ctx = getWx().createInnerAudioContext({
useWebAudioImplement: _options?.useWebAudioImplement
});
return new WeChatAudioContext(ctx);
}
getSupportedFormats(): string[] {
return ['mp3', 'wav', 'aac', 'm4a'];
}
async setInnerAudioOption(options: {
mixWithOther?: boolean;
obeyMuteSwitch?: boolean;
speakerOn?: boolean;
}): Promise<void> {
return promisify(getWx().setInnerAudioOption.bind(getWx()), options);
}
}

View File

@@ -0,0 +1,209 @@
/**
* 微信小游戏 Canvas 子系统
*/
import type {
IPlatformCanvasSubsystem,
IPlatformCanvas,
IPlatformImage,
TempFilePathOptions,
CanvasContextAttributes
} from '@esengine/platform-common';
import { getWx } from '../utils';
/**
* 微信小游戏 Canvas 包装
*/
class WeChatCanvas implements IPlatformCanvas {
private _canvas: WechatMinigame.Canvas;
constructor(canvas: WechatMinigame.Canvas) {
this._canvas = canvas;
}
get width(): number {
return this._canvas.width;
}
set width(value: number) {
this._canvas.width = value;
}
get height(): number {
return this._canvas.height;
}
set height(value: number) {
this._canvas.height = value;
}
getContext(
contextType: '2d' | 'webgl' | 'webgl2',
contextAttributes?: CanvasContextAttributes
): RenderingContext | null {
const wxAttributes: WechatMinigame.ContextAttributes | undefined = contextAttributes ? {
alpha: typeof contextAttributes.alpha === 'boolean'
? (contextAttributes.alpha ? 1 : 0)
: contextAttributes.alpha,
antialias: contextAttributes.antialias,
preserveDrawingBuffer: contextAttributes.preserveDrawingBuffer,
antialiasSamples: contextAttributes.antialiasSamples
} : undefined;
return this._canvas.getContext(contextType, wxAttributes);
}
toDataURL(): string {
return this._canvas.toDataURL();
}
toTempFilePath(options: TempFilePathOptions): void {
this._canvas.toTempFilePath({
x: options.x,
y: options.y,
width: options.width,
height: options.height,
destWidth: options.destWidth,
destHeight: options.destHeight,
fileType: options.fileType,
quality: options.quality,
success: options.success,
fail: options.fail,
complete: options.complete
});
}
/**
* 获取原始微信 Canvas 对象
*/
getNativeCanvas(): WechatMinigame.Canvas {
return this._canvas;
}
}
/**
* 微信小游戏 Image 包装
*/
class WeChatImage implements IPlatformImage {
private _image: WechatMinigame.Image;
constructor(image: WechatMinigame.Image) {
this._image = image;
}
get src(): string {
return this._image.src;
}
set src(value: string) {
this._image.src = value;
}
get width(): number {
return this._image.width;
}
get height(): number {
return this._image.height;
}
get onload(): (() => void) | null {
return this._image.onload as (() => void) | null;
}
set onload(value: (() => void) | null) {
this._image.onload = value as any;
}
get onerror(): ((error: any) => void) | null {
return this._image.onerror as ((error: any) => void) | null;
}
set onerror(value: ((error: any) => void) | null) {
this._image.onerror = value as any;
}
/**
* 获取原始微信 Image 对象
*/
getNativeImage(): WechatMinigame.Image {
return this._image;
}
}
/**
* 微信小游戏 Canvas 子系统实现
*/
export class WeChatCanvasSubsystem implements IPlatformCanvasSubsystem {
private _mainCanvas: WeChatCanvas | null = null;
private _windowInfo: WechatMinigame.WindowInfo;
constructor() {
this._windowInfo = getWx().getWindowInfo();
}
createCanvas(width?: number, height?: number): IPlatformCanvas {
const canvas = getWx().createCanvas();
// 设置尺寸
if (width !== undefined) {
canvas.width = width;
}
if (height !== undefined) {
canvas.height = height;
}
const wrappedCanvas = new WeChatCanvas(canvas);
// 首次创建的是主 Canvas
if (!this._mainCanvas) {
this._mainCanvas = wrappedCanvas;
}
return wrappedCanvas;
}
createImage(): IPlatformImage {
const image = getWx().createImage();
return new WeChatImage(image);
}
createImageData(width: number, height: number): ImageData {
// 微信小游戏 3.4.10+ 支持 createImageData
if (typeof getWx().createImageData === 'function') {
return getWx().createImageData(width, height) as unknown as ImageData;
}
// 降级方案:创建标准 ImageData
const data = new Uint8ClampedArray(width * height * 4);
return {
data,
width,
height,
colorSpace: 'srgb'
} as ImageData;
}
getScreenWidth(): number {
return this._windowInfo.screenWidth;
}
getScreenHeight(): number {
return this._windowInfo.screenHeight;
}
getDevicePixelRatio(): number {
return this._windowInfo.pixelRatio;
}
getMainCanvas(): IPlatformCanvas | null {
return this._mainCanvas;
}
getWindowWidth(): number {
return this._windowInfo.windowWidth;
}
getWindowHeight(): number {
return this._windowInfo.windowHeight;
}
}

View File

@@ -0,0 +1,204 @@
/**
* 微信小游戏文件系统子系统
*/
import type {
IPlatformFileSubsystem,
FileInfo
} from '@esengine/platform-common';
import { getWx } from '../utils';
/**
* 微信小游戏文件系统子系统实现
*/
export class WeChatFileSubsystem implements IPlatformFileSubsystem {
private _fs: WechatMinigame.FileSystemManager;
constructor() {
this._fs = getWx().getFileSystemManager();
}
async readFile(options: {
filePath: string;
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
position?: number;
length?: number;
}): Promise<string | ArrayBuffer> {
return new Promise((resolve, reject) => {
this._fs.readFile({
filePath: options.filePath,
encoding: options.encoding as any,
position: options.position,
length: options.length,
success: (res) => resolve(res.data),
fail: reject
});
});
}
readFileSync(
filePath: string,
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8',
position?: number,
length?: number
): string | ArrayBuffer {
return this._fs.readFileSync(filePath, encoding as any, position, length);
}
async writeFile(options: {
filePath: string;
data: string | ArrayBuffer;
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.writeFile({
filePath: options.filePath,
data: options.data,
encoding: options.encoding as any,
success: () => resolve(),
fail: reject
});
});
}
writeFileSync(
filePath: string,
data: string | ArrayBuffer,
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8'
): void {
this._fs.writeFileSync(filePath, data, encoding as any);
}
async appendFile(options: {
filePath: string;
data: string | ArrayBuffer;
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.appendFile({
filePath: options.filePath,
data: options.data,
encoding: options.encoding as any,
success: () => resolve(),
fail: reject
});
});
}
async unlink(filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.unlink({
filePath,
success: () => resolve(),
fail: reject
});
});
}
async mkdir(options: {
dirPath: string;
recursive?: boolean;
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.mkdir({
dirPath: options.dirPath,
recursive: options.recursive,
success: () => resolve(),
fail: reject
});
});
}
async rmdir(options: {
dirPath: string;
recursive?: boolean;
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.rmdir({
dirPath: options.dirPath,
recursive: options.recursive,
success: () => resolve(),
fail: reject
});
});
}
async readdir(dirPath: string): Promise<string[]> {
return new Promise((resolve, reject) => {
this._fs.readdir({
dirPath,
success: (res) => resolve(res.files),
fail: reject
});
});
}
async stat(path: string): Promise<FileInfo> {
return new Promise((resolve, reject) => {
this._fs.stat({
path,
success: (res) => {
const stats = res.stats as WechatMinigame.Stats;
resolve({
size: stats.size,
createTime: stats.lastAccessedTime,
modifyTime: stats.lastModifiedTime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile()
});
},
fail: reject
});
});
}
async access(path: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.access({
path,
success: () => resolve(),
fail: reject
});
});
}
async rename(oldPath: string, newPath: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.rename({
oldPath,
newPath,
success: () => resolve(),
fail: reject
});
});
}
async copyFile(srcPath: string, destPath: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.copyFile({
srcPath,
destPath,
success: () => resolve(),
fail: reject
});
});
}
getUserDataPath(): string {
return `${getWx().env.USER_DATA_PATH}`;
}
async unzip(options: {
zipFilePath: string;
targetPath: string;
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.unzip({
zipFilePath: options.zipFilePath,
targetPath: options.targetPath,
success: () => resolve(),
fail: reject
});
});
}
}

View File

@@ -0,0 +1,77 @@
/**
* 微信小游戏输入子系统
*/
import type {
IPlatformInputSubsystem,
TouchHandler,
TouchEvent
} from '@esengine/platform-common';
import { getWx } from '../utils';
/**
* 微信小游戏输入子系统实现
*/
export class WeChatInputSubsystem implements IPlatformInputSubsystem {
onTouchStart(handler: TouchHandler): void {
getWx().onTouchStart((res) => {
handler(this.convertTouchEvent(res));
});
}
onTouchMove(handler: TouchHandler): void {
getWx().onTouchMove((res) => {
handler(this.convertTouchEvent(res));
});
}
onTouchEnd(handler: TouchHandler): void {
getWx().onTouchEnd((res) => {
handler(this.convertTouchEvent(res));
});
}
onTouchCancel(handler: TouchHandler): void {
getWx().onTouchCancel((res) => {
handler(this.convertTouchEvent(res));
});
}
offTouchStart(handler: TouchHandler): void {
getWx().offTouchStart(handler as any);
}
offTouchMove(handler: TouchHandler): void {
getWx().offTouchMove(handler as any);
}
offTouchEnd(handler: TouchHandler): void {
getWx().offTouchEnd(handler as any);
}
offTouchCancel(handler: TouchHandler): void {
getWx().offTouchCancel(handler as any);
}
supportsPressure(): boolean {
return true;
}
private convertTouchEvent(res: WechatMinigame.OnTouchStartListenerResult): TouchEvent {
return {
touches: res.touches.map((t: WechatMinigame.Touch) => ({
identifier: t.identifier,
x: t.clientX,
y: t.clientY,
force: t.force
})),
changedTouches: res.changedTouches.map((t: WechatMinigame.Touch) => ({
identifier: t.identifier,
x: t.clientX,
y: t.clientY,
force: t.force
})),
timeStamp: res.timeStamp
};
}
}

View File

@@ -0,0 +1,181 @@
/**
* 微信小游戏网络子系统
*/
import type {
IPlatformNetworkSubsystem,
RequestConfig,
RequestResponse,
IDownloadTask,
IUploadTask,
IPlatformWebSocket
} from '@esengine/platform-common';
import { getWx, promisify } from '../utils';
/**
* 微信 WebSocket 包装
*/
class WeChatWebSocket implements IPlatformWebSocket {
private _task: WechatMinigame.SocketTask;
constructor(task: WechatMinigame.SocketTask) {
this._task = task;
}
send(data: string | ArrayBuffer): void {
this._task.send({ data });
}
close(code?: number, reason?: string): void {
this._task.close({ code, reason });
}
onOpen(callback: (res: { header: Record<string, string> }) => void): void {
this._task.onOpen(callback as any);
}
onClose(callback: (res: { code: number; reason: string }) => void): void {
this._task.onClose(callback as any);
}
onError(callback: (error: any) => void): void {
this._task.onError(callback);
}
onMessage(callback: (res: { data: string | ArrayBuffer }) => void): void {
this._task.onMessage(callback as any);
}
}
/**
* 微信小游戏网络子系统实现
*/
export class WeChatNetworkSubsystem implements IPlatformNetworkSubsystem {
async request<T = any>(config: RequestConfig): Promise<RequestResponse<T>> {
return new Promise((resolve, reject) => {
getWx().request({
url: config.url,
method: config.method as any,
data: config.data,
header: config.header,
timeout: config.timeout,
dataType: config.dataType as any,
responseType: config.responseType as any,
success: (res) => {
resolve({
data: res.data as T,
statusCode: res.statusCode,
header: res.header as Record<string, string>
});
},
fail: reject
});
});
}
downloadFile(options: {
url: string;
filePath?: string;
header?: Record<string, string>;
timeout?: number;
}): Promise<{ tempFilePath: string; filePath?: string; statusCode: number }> & IDownloadTask {
const task = getWx().downloadFile({
url: options.url,
filePath: options.filePath,
header: options.header,
timeout: options.timeout,
success: () => {},
fail: () => {}
});
const promise = new Promise<{ tempFilePath: string; filePath?: string; statusCode: number }>((resolve, reject) => {
task.onProgressUpdate(() => {});
getWx().downloadFile({
...options,
success: (res) => resolve({
tempFilePath: res.tempFilePath,
filePath: res.filePath,
statusCode: res.statusCode
}),
fail: reject
});
});
return Object.assign(promise, {
abort: () => task.abort(),
onProgressUpdate: (callback: any) => task.onProgressUpdate(callback),
offProgressUpdate: (callback: any) => task.offProgressUpdate(callback)
});
}
uploadFile(options: {
url: string;
filePath: string;
name: string;
header?: Record<string, string>;
formData?: Record<string, any>;
timeout?: number;
}): Promise<{ data: string; statusCode: number }> & IUploadTask {
const task = getWx().uploadFile({
url: options.url,
filePath: options.filePath,
name: options.name,
header: options.header,
formData: options.formData,
timeout: options.timeout,
success: () => {},
fail: () => {}
});
const promise = new Promise<{ data: string; statusCode: number }>((resolve, reject) => {
getWx().uploadFile({
...options,
success: (res) => resolve({
data: res.data,
statusCode: res.statusCode
}),
fail: reject
});
});
return Object.assign(promise, {
abort: () => task.abort(),
onProgressUpdate: (callback: any) => task.onProgressUpdate(callback),
offProgressUpdate: (callback: any) => task.offProgressUpdate(callback)
});
}
connectSocket(options: {
url: string;
header?: Record<string, string>;
protocols?: string[];
timeout?: number;
}): IPlatformWebSocket {
const task = getWx().connectSocket({
url: options.url,
header: options.header,
protocols: options.protocols,
timeout: options.timeout
});
return new WeChatWebSocket(task);
}
async getNetworkType(): Promise<'wifi' | '2g' | '3g' | '4g' | '5g' | 'unknown' | 'none'> {
const res = await promisify<{ networkType: string }>(
getWx().getNetworkType.bind(getWx()),
{}
);
return res.networkType as any;
}
onNetworkStatusChange(callback: (res: {
isConnected: boolean;
networkType: string;
}) => void): void {
getWx().onNetworkStatusChange(callback);
}
offNetworkStatusChange(callback: Function): void {
getWx().offNetworkStatusChange(callback as any);
}
}

View File

@@ -0,0 +1,67 @@
/**
* 微信小游戏存储子系统
*/
import type {
IPlatformStorageSubsystem,
StorageInfo
} from '@esengine/platform-common';
import { getWx, promisify } from '../utils';
/**
* 微信小游戏存储子系统实现
*/
export class WeChatStorageSubsystem implements IPlatformStorageSubsystem {
getStorageSync<T = any>(key: string): T | undefined {
try {
return getWx().getStorageSync<T>(key);
} catch {
return undefined;
}
}
setStorageSync<T = any>(key: string, value: T): void {
getWx().setStorageSync(key, value);
}
removeStorageSync(key: string): void {
getWx().removeStorageSync(key);
}
clearStorageSync(): void {
getWx().clearStorageSync();
}
getStorageInfoSync(): StorageInfo {
const info = getWx().getStorageInfoSync();
return {
keys: info.keys,
currentSize: info.currentSize,
limitSize: info.limitSize
};
}
async getStorage<T = any>(key: string): Promise<T | undefined> {
try {
const res = await promisify<{ data: T }>(
getWx().getStorage.bind(getWx()),
{ key }
);
return res.data;
} catch {
return undefined;
}
}
async setStorage<T = any>(key: string, value: T): Promise<void> {
await promisify(getWx().setStorage.bind(getWx()), { key, data: value });
}
async removeStorage(key: string): Promise<void> {
await promisify(getWx().removeStorage.bind(getWx()), { key });
}
async clearStorage(): Promise<void> {
await promisify(getWx().clearStorage.bind(getWx()), {});
}
}

View File

@@ -0,0 +1,65 @@
/**
* 微信小游戏 WASM 子系统
*/
import type {
IPlatformWASMSubsystem,
IWASMInstance,
WASMImports,
WASMExports
} from '@esengine/platform-common';
/**
* 微信小游戏 WASM 子系统实现
*/
export class WeChatWASMSubsystem implements IPlatformWASMSubsystem {
async instantiate(path: string, imports?: WASMImports): Promise<IWASMInstance> {
// 微信小游戏使用 WXWebAssembly.instantiate
// path 应该是相对于小游戏根目录的 .wasm 文件路径
if (typeof WXWebAssembly === 'undefined') {
throw new Error('当前微信基础库版本不支持 WebAssembly');
}
const wxImports: WXWebAssembly.Imports | undefined = imports as WXWebAssembly.Imports | undefined;
const instance = await WXWebAssembly.instantiate(path, wxImports);
return {
exports: instance.exports as WASMExports
};
}
isSupported(): boolean {
return typeof WXWebAssembly !== 'undefined';
}
/**
* 获取 WASM 内存
* 用于 Rust/WASM 引擎的内存交互
*/
createMemory(initial: number, maximum?: number): WebAssembly.Memory {
if (typeof WXWebAssembly === 'undefined') {
throw new Error('当前微信基础库版本不支持 WebAssembly');
}
return new WXWebAssembly.Memory({
initial,
maximum,
shared: false // 微信小游戏不支持 shared memory
});
}
/**
* 创建 WASM Table
*/
createTable(initial: number, maximum?: number): WebAssembly.Table {
if (typeof WXWebAssembly === 'undefined') {
throw new Error('当前微信基础库版本不支持 WebAssembly');
}
return new WXWebAssembly.Table({
element: 'anyfunc',
initial,
maximum
});
}
}