Files
esengine/packages/editor-app/vite.config.ts

533 lines
18 KiB
TypeScript
Raw Normal View History

import { defineConfig, Plugin } from 'vite';
import react from '@vitejs/plugin-react-swc';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
2025-10-15 17:15:05 +08:00
import fs from 'fs';
import path from 'path';
2025-10-14 22:53:26 +08:00
function copyPluginModulesPlugin(): Plugin {
const modulePaths = [
{ name: 'editor-runtime', path: path.resolve(__dirname, '../editor-runtime/dist') },
{ name: 'behavior-tree', path: path.resolve(__dirname, '../behavior-tree/dist') },
];
/**
* JS
*/
function copyJsFilesRecursively(srcDir: string, destDir: string, relativePath: string = '') {
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name);
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
// 递归复制子目录
const subDestDir = path.join(destDir, entry.name);
if (!fs.existsSync(subDestDir)) {
fs.mkdirSync(subDestDir, { recursive: true });
}
copyJsFilesRecursively(srcPath, subDestDir, relPath);
} else if (entry.name.endsWith('.js')) {
// 复制 JS 文件
const destPath = path.join(destDir, entry.name);
fs.copyFileSync(srcPath, destPath);
console.log(`[copy-plugin-modules] Copied ${relPath}`);
}
}
}
return {
name: 'copy-plugin-modules',
writeBundle(options) {
const outDir = options.dir || 'dist';
const assetsDir = path.join(outDir, 'assets');
if (!fs.existsSync(assetsDir)) {
fs.mkdirSync(assetsDir, { recursive: true });
}
for (const mod of modulePaths) {
if (!fs.existsSync(mod.path)) {
console.warn(`[copy-plugin-modules] ${mod.name} dist not found: ${mod.path}`);
continue;
}
copyJsFilesRecursively(mod.path, assetsDir);
}
}
};
}
2025-10-14 22:53:26 +08:00
/**
* Plugin to copy engine modules after each build.
*
*/
function copyEngineModulesPlugin(): Plugin {
const packagesDir = path.resolve(__dirname, '..');
function getEngineModules() {
const modules: Array<{
id: string;
name: string;
displayName: string;
packageDir: string;
moduleJsonPath: string;
distPath: string;
editorPackage?: string;
isCore: boolean;
category: string;
}> = [];
let packages: string[];
try {
packages = fs.readdirSync(packagesDir);
} catch {
return modules;
}
for (const pkg of packages) {
const pkgDir = path.join(packagesDir, pkg);
const moduleJsonPath = path.join(pkgDir, 'module.json');
try {
if (!fs.statSync(pkgDir).isDirectory()) continue;
} catch {
continue;
}
if (!fs.existsSync(moduleJsonPath)) continue;
try {
const moduleJson = JSON.parse(fs.readFileSync(moduleJsonPath, 'utf-8'));
if (moduleJson.isEngineModule !== false) {
// Use outputPath from module.json, default to "dist/index.js"
const outputPath = moduleJson.outputPath || 'dist/index.js';
const distPath = path.join(pkgDir, outputPath);
modules.push({
id: moduleJson.id || pkg,
name: moduleJson.name || `@esengine/${pkg}`,
displayName: moduleJson.displayName || pkg,
packageDir: pkgDir,
moduleJsonPath,
distPath,
editorPackage: moduleJson.editorPackage,
isCore: moduleJson.isCore || false,
category: moduleJson.category || 'Other'
});
}
} catch {
// Ignore parse errors
}
}
return modules;
}
return {
name: 'copy-engine-modules',
writeBundle(options) {
const outDir = options.dir || 'dist';
const engineDir = path.join(outDir, 'engine');
// Clean and recreate engine directory
if (fs.existsSync(engineDir)) {
fs.rmSync(engineDir, { recursive: true });
}
fs.mkdirSync(engineDir, { recursive: true });
const modules = getEngineModules();
const moduleInfos: Array<{
id: string;
name: string;
displayName: string;
hasRuntime: boolean;
editorPackage?: string;
isCore: boolean;
category: string;
jsSize?: number;
requiresWasm?: boolean;
wasmSize?: number;
wasmFiles?: string[];
}> = [];
const editorPackages = new Set<string>();
/**
* Calculate total WASM file size in a directory.
* WASM
*/
function getWasmSize(pkgDir: string): number {
let totalSize = 0;
const checkDirs = [
pkgDir,
path.join(pkgDir, 'pkg'),
path.join(pkgDir, 'dist')
];
for (const dir of checkDirs) {
if (!fs.existsSync(dir)) continue;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (file.endsWith('.wasm')) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
totalSize += stat.size;
}
}
} catch {
// Ignore errors
}
}
return totalSize;
}
console.log(`[copy-engine-modules] Copying ${modules.length} modules to dist/engine/`);
for (const module of modules) {
const moduleOutputDir = path.join(engineDir, module.id);
fs.mkdirSync(moduleOutputDir, { recursive: true });
// Read full module.json for additional fields
// 读取完整 module.json 获取额外字段
let moduleJson: Record<string, unknown> = {};
try {
moduleJson = JSON.parse(fs.readFileSync(module.moduleJsonPath, 'utf-8'));
} catch {
// Ignore parse errors
}
// Copy module.json
fs.copyFileSync(module.moduleJsonPath, path.join(moduleOutputDir, 'module.json'));
// Copy dist/index.js if exists
let hasRuntime = false;
let jsSize = 0;
if (fs.existsSync(module.distPath)) {
fs.copyFileSync(module.distPath, path.join(moduleOutputDir, 'index.js'));
// Get JS file size
jsSize = fs.statSync(module.distPath).size;
// Copy source map if exists
const sourceMapPath = module.distPath + '.map';
if (fs.existsSync(sourceMapPath)) {
fs.copyFileSync(sourceMapPath, path.join(moduleOutputDir, 'index.js.map'));
}
hasRuntime = true;
// Copy additional included files (e.g., chunks)
// 复制额外包含的文件(如 chunk
const includes = moduleJson.includes as string[] | undefined;
if (includes && includes.length > 0) {
const distDir = path.dirname(module.distPath);
for (const pattern of includes) {
// Convert glob pattern to regex
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`);
// Find matching files in dist directory
if (fs.existsSync(distDir)) {
const files = fs.readdirSync(distDir);
for (const file of files) {
if (regex.test(file)) {
const srcFile = path.join(distDir, file);
const destFile = path.join(moduleOutputDir, file);
fs.copyFileSync(srcFile, destFile);
jsSize += fs.statSync(srcFile).size;
// Copy source map for included file if exists
const mapFile = srcFile + '.map';
if (fs.existsSync(mapFile)) {
fs.copyFileSync(mapFile, destFile + '.map');
}
console.log(`[copy-engine-modules] Copied include to ${module.id}/: ${file}`);
}
}
}
}
}
}
// Calculate WASM size and copy WASM files if module requires WASM
// 如果模块需要 WASM计算 WASM 大小并复制 WASM 文件
const requiresWasm = moduleJson.requiresWasm === true;
let wasmSize = 0;
const copiedWasmFiles: string[] = [];
if (requiresWasm) {
wasmSize = getWasmSize(module.packageDir);
if (wasmSize > 0) {
console.log(`[copy-engine-modules] ${module.id}: WASM size = ${(wasmSize / 1024).toFixed(1)} KB`);
}
// Copy WASM files from wasmPaths defined in module.json
// wasmPaths 现在是相对于源包目录的路径,如 "rapier_wasm2d_bg.wasm"
// 需要找到实际的 WASM 文件并复制到输出的模块目录
const wasmPaths = moduleJson.wasmPaths as string[] | undefined;
if (wasmPaths && wasmPaths.length > 0) {
for (const wasmRelPath of wasmPaths) {
const wasmFileName = path.basename(wasmRelPath);
// 查找源 WASM 文件的可能位置
// wasmPaths 里配置的是相对路径,实际文件在源包里
// 对于 @esengine/rapier2dWASM 在 packages/rapier2d/pkg/ 下
const possibleSrcPaths = [
// 直接在包目录下(如果 wasmRelPath 就是文件名)
path.join(module.packageDir, wasmRelPath),
// 在包的 pkg 目录下wasm-pack 输出)
path.join(module.packageDir, 'pkg', wasmFileName),
// 在包的 dist 目录下
path.join(module.packageDir, 'dist', wasmFileName),
];
// 对于依赖其他包 WASM 的情况,检查依赖包
// 例如 physics-rapier2d 依赖 rapier2d 的 WASM
const depMatch = moduleJson.name?.toString().match(/@esengine\/(.+)/);
if (depMatch) {
// 检查同名的依赖包(去掉 physics- 前缀)
const baseName = depMatch[1].replace('physics-', '');
possibleSrcPaths.push(
path.join(packagesDir, baseName, 'pkg', wasmFileName),
path.join(packagesDir, baseName, wasmFileName)
);
}
let copied = false;
for (const srcPath of possibleSrcPaths) {
if (fs.existsSync(srcPath)) {
const destPath = path.join(moduleOutputDir, wasmFileName);
fs.copyFileSync(srcPath, destPath);
copiedWasmFiles.push(wasmFileName);
console.log(`[copy-engine-modules] Copied WASM to ${module.id}/: ${wasmFileName}`);
copied = true;
break;
}
}
if (!copied) {
console.warn(`[copy-engine-modules] WASM file not found: ${wasmRelPath} (tried ${possibleSrcPaths.length} paths)`);
}
}
}
// Copy pkg directory if exists (for WASM JS bindings like rapier2d)
// 如果存在 pkg 目录则复制(用于 WASM JS 绑定如 rapier2d
// The JS and WASM files must be in the same directory for import.meta.url to work
// JS 和 WASM 文件必须在同一目录才能让 import.meta.url 正常工作
const pkgDir = path.join(module.packageDir, 'pkg');
if (fs.existsSync(pkgDir)) {
const pkgOutputDir = path.join(moduleOutputDir, 'pkg');
fs.mkdirSync(pkgOutputDir, { recursive: true });
const pkgFiles = fs.readdirSync(pkgDir);
for (const file of pkgFiles) {
// Copy both JS and WASM files to pkg directory
// 将 JS 和 WASM 文件都复制到 pkg 目录
if (file.endsWith('.js') || file.endsWith('.wasm')) {
const srcFile = path.join(pkgDir, file);
const destFile = path.join(pkgOutputDir, file);
fs.copyFileSync(srcFile, destFile);
console.log(`[copy-engine-modules] Copied pkg to ${module.id}/pkg/: ${file}`);
}
}
}
}
moduleInfos.push({
id: module.id,
name: module.name,
displayName: module.displayName,
hasRuntime,
editorPackage: module.editorPackage,
isCore: module.isCore,
category: module.category,
// Only include jsSize if there's actual runtime code
// 只有实际有运行时代码时才包含 jsSize
jsSize: jsSize > 0 ? jsSize : undefined,
requiresWasm: requiresWasm || undefined,
wasmSize: wasmSize > 0 ? wasmSize : undefined,
// WASM files that were copied to dist/wasm/
// 复制到 dist/wasm/ 的 WASM 文件
wasmFiles: copiedWasmFiles.length > 0 ? copiedWasmFiles : undefined
});
if (module.editorPackage) {
editorPackages.add(module.editorPackage);
}
}
// Copy editor packages
for (const editorPkg of editorPackages) {
const match = editorPkg.match(/@esengine\/(.+)/);
if (!match) continue;
const pkgName = match[1];
const pkgDir = path.join(packagesDir, pkgName);
const distPath = path.join(pkgDir, 'dist', 'index.js');
if (!fs.existsSync(distPath)) continue;
const editorOutputDir = path.join(engineDir, pkgName);
fs.mkdirSync(editorOutputDir, { recursive: true });
fs.copyFileSync(distPath, path.join(editorOutputDir, 'index.js'));
const sourceMapPath = distPath + '.map';
if (fs.existsSync(sourceMapPath)) {
fs.copyFileSync(sourceMapPath, path.join(editorOutputDir, 'index.js.map'));
}
}
// Create index.json
const indexData = {
version: '1.0.0',
generatedAt: new Date().toISOString(),
modules: moduleInfos
};
fs.writeFileSync(
path.join(engineDir, 'index.json'),
JSON.stringify(indexData, null, 2)
);
console.log(`[copy-engine-modules] Done! Created dist/engine/index.json`);
}
};
}
const host = process.env.TAURI_DEV_HOST;
const wasmPackages: string[] = [];
/**
* WASM
*/
function hasWasmFiles(dirPath: string): boolean {
try {
const files = fs.readdirSync(dirPath);
for (const file of files) {
if (file.endsWith('.wasm')) return true;
if (file === 'pkg') {
const pkgPath = path.join(dirPath, file);
const pkgFiles = fs.readdirSync(pkgPath);
if (pkgFiles.some(f => f.endsWith('.wasm'))) return true;
}
}
} catch {
// Ignore errors
}
return false;
}
2025-10-15 17:15:05 +08:00
/**
* packages WASM
*/
function detectWasmPackages() {
2025-10-15 17:15:05 +08:00
const packagesDir = path.resolve(__dirname, '..');
if (!fs.existsSync(packagesDir)) return;
2025-10-15 17:15:05 +08:00
const packageDirs = fs.readdirSync(packagesDir).filter(dir => {
const stat = fs.statSync(path.join(packagesDir, dir));
return stat.isDirectory();
});
for (const dir of packageDirs) {
const packageJsonPath = path.join(packagesDir, dir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const packageName = packageJson.name;
const packageDir = path.join(packagesDir, dir);
if (packageName && hasWasmFiles(packageDir)) {
wasmPackages.push(packageName);
console.log(`[Vite] Detected WASM package: ${packageName}`);
}
} catch {
// Ignore errors
2025-10-15 17:15:05 +08:00
}
}
}
// 扫描 node_modules
const nodeModulesDir = path.resolve(__dirname, 'node_modules');
if (fs.existsSync(nodeModulesDir)) {
scanNodeModulesForWasm(nodeModulesDir);
}
}
function scanNodeModulesForWasm(nodeModulesDir: string) {
try {
const entries = fs.readdirSync(nodeModulesDir);
for (const entry of entries) {
if (entry.startsWith('.')) continue;
const entryPath = path.join(nodeModulesDir, entry);
const stat = fs.statSync(entryPath);
if (!stat.isDirectory()) continue;
if (entry.startsWith('@')) {
const scopedPackages = fs.readdirSync(entryPath);
for (const scopedPkg of scopedPackages) {
const scopedPath = path.join(entryPath, scopedPkg);
if (fs.statSync(scopedPath).isDirectory()) {
if (!wasmPackages.includes(`${entry}/${scopedPkg}`) && hasWasmFiles(scopedPath)) {
wasmPackages.push(`${entry}/${scopedPkg}`);
console.log(`[Vite] Detected WASM package in node_modules: ${entry}/${scopedPkg}`);
}
}
}
} else {
if (!wasmPackages.includes(entry) && hasWasmFiles(entryPath)) {
wasmPackages.push(entry);
console.log(`[Vite] Detected WASM package in node_modules: ${entry}`);
}
}
}
} catch {
// Ignore errors
}
}
detectWasmPackages();
2025-10-15 17:15:05 +08:00
2025-10-14 22:53:26 +08:00
export default defineConfig({
plugins: [
wasm(),
topLevelAwait(),
...react({
tsDecorators: true,
}),
copyPluginModulesPlugin(),
copyEngineModulesPlugin(),
],
2025-10-14 22:53:26 +08:00
clearScreen: false,
server: {
host: host || false,
port: 5173,
strictPort: true,
hmr: host
? {
protocol: 'ws',
host,
port: 5183,
}
: undefined,
2025-10-15 17:15:05 +08:00
fs: {
strict: false,
},
2025-10-14 22:53:26 +08:00
},
envPrefix: ['VITE_', 'TAURI_'],
build: {
target: 'es2021',
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
esbuild: {
// 保留类名和函数名,用于跨包插件服务匹配
keepNames: true,
},
optimizeDeps: {
include: ['tslib', 'react', 'react-dom', 'zustand', 'lucide-react'],
exclude: wasmPackages,
},
2025-10-14 22:53:26 +08:00
});