Files
esengine/scripts/create-package.mjs
YHH b42a7b4e43 Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能
2025-12-01 22:28:51 +08:00

308 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* create-package - ES Engine 包脚手架工具
*
* 使用方法:
* node scripts/create-package.mjs <package-name> [options]
*
* 选项:
* --type, -t <type> 包类型: runtime-only | plugin | editor-only
* --scope, -s <scope> npm scope (默认: @esengine)
* --external 外部模式,用于用户在自己项目中创建插件
*
* 示例:
* # 内部开发 (monorepo 内,使用 workspace:*)
* node scripts/create-package.mjs my-plugin --type plugin
*
* # 外部用户 (独立项目,使用版本号)
* node scripts/create-package.mjs my-plugin --type plugin --external --scope @mycompany
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import readline from 'readline';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const PACKAGES_DIR = path.join(ROOT_DIR, 'packages');
const TEMPLATES_DIR = path.join(PACKAGES_DIR, 'build-config', 'templates');
const PACKAGE_TYPES = {
'runtime-only': {
description: '纯运行时库 (不含编辑器代码)',
examples: 'core, math, components'
},
'plugin': {
description: '插件包 (同时包含 runtime 和 editor 模块)',
examples: 'ui, tilemap, behavior-tree'
},
'editor-only': {
description: '纯编辑器包 (仅用于编辑器)',
examples: 'editor-core, node-editor'
}
};
const CATEGORIES = [
'core',
'rendering',
'physics',
'ai',
'ui',
'audio',
'input',
'networking',
'tools',
'other'
];
// 默认 scope
const DEFAULT_SCOPE = '@esengine';
// ES Engine 核心包的最低版本要求
const ESENGINE_VERSION = '^2.0.0';
function parseArgs(args) {
const result = {
name: null,
type: null,
scope: null,
bExternal: false // 外部模式标记
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--type' || args[i] === '-t') {
result.type = args[i + 1];
i++;
} else if (args[i] === '--scope' || args[i] === '-s') {
result.scope = args[i + 1];
i++;
} else if (args[i] === '--external' || args[i] === '-e') {
result.bExternal = true;
} else if (!args[i].startsWith('-')) {
result.name = args[i];
}
}
return result;
}
function createReadlineInterface() {
return readline.createInterface({
input: process.stdin,
output: process.stdout
});
}
async function question(rl, prompt) {
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
resolve(answer.trim());
});
});
}
async function selectOption(rl, prompt, options) {
console.log(prompt);
options.forEach((opt, i) => {
console.log(` ${i + 1}. ${opt.label}${opt.description ? ` - ${opt.description}` : ''}`);
});
while (true) {
const answer = await question(rl, '请选择 (输入数字): ');
const index = parseInt(answer, 10) - 1;
if (index >= 0 && index < options.length) {
return options[index].value;
}
console.log('无效选择,请重试');
}
}
function toPascalCase(str) {
return str
.split(/[-_]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
function processTemplate(content, variables) {
let result = content;
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
}
return result;
}
function copyTemplateDir(srcDir, destDir, variables) {
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name);
let destName = entry.name.replace('.template', '');
// 处理文件名中的模板变量
destName = processTemplate(destName, variables);
const destPath = path.join(destDir, destName);
if (entry.isDirectory()) {
copyTemplateDir(srcPath, destPath, variables);
} else {
const content = fs.readFileSync(srcPath, 'utf-8');
const processedContent = processTemplate(content, variables);
fs.writeFileSync(destPath, processedContent, 'utf-8');
console.log(` 创建: ${path.relative(PACKAGES_DIR, destPath)}`);
}
}
}
async function main() {
console.log('\n🚀 ES Engine 包创建工具\n');
const args = parseArgs(process.argv.slice(2));
const rl = createReadlineInterface();
try {
// 0. 确定模式:内部 (monorepo) 还是外部 (独立项目)
let bExternal = args.bExternal;
if (!args.bExternal && !args.scope) {
// 自动检测:如果在 monorepo 根目录运行,默认内部模式
const bIsMonorepo = fs.existsSync(path.join(ROOT_DIR, 'pnpm-workspace.yaml'));
if (!bIsMonorepo) {
bExternal = true;
}
}
// 1. 获取 scope
let scope = args.scope;
if (!scope) {
if (bExternal) {
// 外部模式必须指定 scope
scope = await question(rl, 'npm scope (例如: @mycompany): ');
if (!scope) {
console.error('❌ 外部模式必须指定 scope');
process.exit(1);
}
} else {
scope = DEFAULT_SCOPE;
}
}
// 确保 scope 以 @ 开头
if (!scope.startsWith('@')) {
scope = '@' + scope;
}
// 2. 获取包名
let packageName = args.name;
if (!packageName) {
packageName = await question(rl, '包名 (例如: my-plugin): ');
}
if (!packageName || !/^[a-z][a-z0-9-]*$/.test(packageName)) {
console.error('❌ 无效的包名,必须以小写字母开头,只能包含小写字母、数字和连字符');
process.exit(1);
}
// 检查包是否已存在
const packageDir = path.join(PACKAGES_DIR, packageName);
if (fs.existsSync(packageDir)) {
console.error(`❌ 包 ${packageName} 已存在`);
process.exit(1);
}
// 3. 获取包类型
let packageType = args.type;
if (!packageType || !PACKAGE_TYPES[packageType]) {
packageType = await selectOption(rl, '\n选择包类型:', [
{ value: 'runtime-only', label: 'runtime-only', description: PACKAGE_TYPES['runtime-only'].description },
{ value: 'plugin', label: 'plugin', description: PACKAGE_TYPES['plugin'].description },
{ value: 'editor-only', label: 'editor-only', description: PACKAGE_TYPES['editor-only'].description }
]);
}
// 4. 获取描述
const description = await question(rl, '\n包描述: ') || `${packageName} package`;
// 5. 获取显示名称
const displayName = await question(rl, `\n显示名称 (默认: ${toPascalCase(packageName)}): `) || toPascalCase(packageName);
// 6. 插件类型需要选择分类
let category = 'other';
if (packageType === 'plugin') {
category = await selectOption(rl, '\n选择插件分类:', CATEGORIES.map(c => ({ value: c, label: c })));
}
const modeLabel = bExternal ? '外部 (npm install)' : '内部 (workspace:*)';
console.log('\n📦 创建包...\n');
console.log(` 模式: ${modeLabel}`);
console.log(` Scope: ${scope}`);
console.log(` Full name: ${scope}/${packageName}\n`);
// 准备模板变量
// 依赖版本:内部用 workspace:*,外部用具体版本
const depVersion = bExternal ? ESENGINE_VERSION : 'workspace:*';
const variables = {
scope,
name: packageName,
fullName: `${scope}/${packageName}`,
pascalName: toPascalCase(packageName),
description,
displayName,
category,
depVersion // 用于 package.json 中的依赖版本
};
// 复制模板
const templateDir = path.join(TEMPLATES_DIR, packageType);
if (!fs.existsSync(templateDir)) {
console.error(`❌ 模板目录不存在: ${templateDir}`);
process.exit(1);
}
copyTemplateDir(templateDir, packageDir, variables);
// 重命名特殊文件
if (packageType === 'plugin') {
// RuntimeModule.ts.template -> {name}RuntimeModule.ts
const runtimeModuleSrc = path.join(packageDir, 'src', 'RuntimeModule.ts');
const runtimeModuleDest = path.join(packageDir, 'src', `${toPascalCase(packageName)}RuntimeModule.ts`);
if (fs.existsSync(runtimeModuleSrc)) {
fs.renameSync(runtimeModuleSrc, runtimeModuleDest);
}
// Plugin.ts.template -> {name}Plugin.ts
const pluginSrc = path.join(packageDir, 'src', 'editor', 'Plugin.ts');
const pluginDest = path.join(packageDir, 'src', 'editor', `${toPascalCase(packageName)}Plugin.ts`);
if (fs.existsSync(pluginSrc)) {
fs.renameSync(pluginSrc, pluginDest);
}
}
console.log('\n✅ 包创建成功!\n');
console.log('下一步:');
console.log(` 1. cd packages/${packageName}`);
console.log(` 2. pnpm install`);
console.log(` 3. 开始编写代码`);
console.log(` 4. pnpm run build`);
if (packageType === 'plugin') {
console.log('\n插件开发提示:');
console.log(' - runtime.ts: 纯运行时代码 (不能导入 React!)');
console.log(' - editor/: 编辑器模块 (可以使用 React)');
console.log(' - plugin.json: 插件描述文件');
}
} finally {
rl.close();
}
}
main().catch(console.error);