5feb24e 子仓库提交 git-subtree-dir: crop-x-new git-subtree-split: 5feb24e4e221308e6e146bb0fce87f1fb3e152e8
388 lines
10 KiB
JavaScript
388 lines
10 KiB
JavaScript
/**
|
||
* 简化的 API 生成脚本
|
||
*
|
||
* 这个脚本现在主要负责:
|
||
* 1. 使用 @hey-api/openapi-ts 命令生成客户端代码
|
||
* 2. 环境配置通过 openapi-ts.config.ts 处理
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// 从环境配置文件中读取 API_BASE_URL
|
||
function getApiBaseUrl() {
|
||
try {
|
||
// 首先尝试从 env/.env.dev 文件读取
|
||
const envDevPath = path.join(process.cwd(), 'env', '.env.dev');
|
||
if (fs.existsSync(envDevPath)) {
|
||
const envContent = fs.readFileSync(envDevPath, 'utf8');
|
||
const apiBaseUrlMatch = envContent.match(/API_BASE_URL=([^\r\n]+)/);
|
||
|
||
if (apiBaseUrlMatch && apiBaseUrlMatch[1]) {
|
||
return apiBaseUrlMatch[1].trim();
|
||
}
|
||
}
|
||
|
||
// 如果上面的文件不存在,尝试从 TypeScript 环境配置文件读取
|
||
const envConfigPath = path.join(process.cwd(), 'src', 'env', 'index.ts');
|
||
if (fs.existsSync(envConfigPath)) {
|
||
const envContent = fs.readFileSync(envConfigPath, 'utf8');
|
||
const devConfigMatch = envContent.match(/dev:\s*\{[\s\S]*?BACKEND_BASE_URL:\s*['"`]([^'"`]+)['"`]/);
|
||
|
||
if (devConfigMatch && devConfigMatch[1]) {
|
||
return devConfigMatch[1].trim();
|
||
}
|
||
}
|
||
|
||
throw new Error('无法找到 API_BASE_URL 或 BACKEND_BASE_URL 配置');
|
||
} catch (error) {
|
||
console.log(`读取环境配置失败: ${error.message}`);
|
||
return 'http://localhost:8080'; // 默认值
|
||
}
|
||
}
|
||
|
||
// 获取 API 基础 URL
|
||
const API_BASE_URL = getApiBaseUrl();
|
||
|
||
// 设置环境变量供后续使用
|
||
process.env.API_BASE_URL = API_BASE_URL;
|
||
|
||
// ANSI 颜色代码
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
blue: '\x1b[34m',
|
||
magenta: '\x1b[35m',
|
||
cyan: '\x1b[36m',
|
||
white: '\x1b[37m'
|
||
};
|
||
|
||
// 日志函数
|
||
function log(message, color = 'white') {
|
||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||
}
|
||
|
||
function logSuccess(message) {
|
||
log(`✓ ${message}`, 'green');
|
||
}
|
||
|
||
function logError(message) {
|
||
log(`✗ ${message}`, 'red');
|
||
}
|
||
|
||
function logWarning(message) {
|
||
log(`⚠ ${message}`, 'yellow');
|
||
}
|
||
|
||
function logInfo(message) {
|
||
log(`ℹ ${message}`, 'blue');
|
||
}
|
||
|
||
// 显示环境配置信息
|
||
logInfo(`当前环境: ${process.env.NODE_ENV || 'development'}`);
|
||
logInfo(`API 服务器: ${API_BASE_URL}`);
|
||
logInfo(`已从 env/.env.dev 读取 API_BASE_URL 配置`);
|
||
|
||
/**
|
||
* 检查自定义文件是否存在
|
||
* 如果某些文件已经被自定义,我们不希望覆盖它们
|
||
*/
|
||
function checkCustomFiles() {
|
||
const customFiles = [
|
||
'client.gen.ts' // 客户端文件通常是自定义的
|
||
];
|
||
|
||
const outputDir = path.join(process.cwd(), 'src', 'lib', 'api');
|
||
const existingCustomFiles = [];
|
||
|
||
for (const file of customFiles) {
|
||
const filePath = path.join(outputDir, file);
|
||
if (fs.existsSync(filePath)) {
|
||
// 检查文件是否包含自定义内容的标识
|
||
const content = fs.readFileSync(filePath, 'utf8');
|
||
|
||
// 检查是否有自定义配置的标识
|
||
const customIndicators = [
|
||
'getBaseUrl',
|
||
'createDynamicClient',
|
||
'getCurrentClientConfig',
|
||
// 可以添加更多自定义标识
|
||
];
|
||
|
||
const hasCustomContent = customIndicators.some(indicator =>
|
||
content.includes(indicator)
|
||
);
|
||
|
||
if (hasCustomContent) {
|
||
existingCustomFiles.push(file);
|
||
logWarning(`检测到自定义文件: ${file}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
return existingCustomFiles;
|
||
}
|
||
|
||
/**
|
||
* 备份自定义文件
|
||
*/
|
||
function backupCustomFiles(customFiles) {
|
||
if (customFiles.length === 0) return;
|
||
|
||
logInfo('备份自定义文件...');
|
||
const outputDir = path.join(process.cwd(), 'src', 'lib', 'api');
|
||
const backupDir = path.join(process.cwd(), 'src', 'lib', 'api', 'backup');
|
||
|
||
// 创建备份目录
|
||
if (!fs.existsSync(backupDir)) {
|
||
fs.mkdirSync(backupDir, { recursive: true });
|
||
}
|
||
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const timestampedBackupDir = path.join(backupDir, `backup-${timestamp}`);
|
||
fs.mkdirSync(timestampedBackupDir);
|
||
|
||
for (const file of customFiles) {
|
||
const srcPath = path.join(outputDir, file);
|
||
const destPath = path.join(timestampedBackupDir, file);
|
||
fs.copyFileSync(srcPath, destPath);
|
||
logInfo(` 备份: ${file} -> backup/${timestamp}/${file}`);
|
||
}
|
||
|
||
logSuccess(`自定义文件已备份到: backup/${timestamp}/`);
|
||
}
|
||
|
||
/**
|
||
* 恢复自定义文件
|
||
*/
|
||
function restoreCustomFiles(customFiles) {
|
||
if (customFiles.length === 0) return;
|
||
|
||
logInfo('恢复自定义文件...');
|
||
const outputDir = path.join(process.cwd(), 'src', 'lib', 'api');
|
||
const backupDir = path.join(process.cwd(), 'src', 'lib', 'api', 'backup');
|
||
|
||
// 找到最新的备份目录
|
||
const backupDirs = fs.existsSync(backupDir)
|
||
? fs.readdirSync(backupDir)
|
||
.filter(name => name.startsWith('backup-'))
|
||
.sort()
|
||
.reverse()
|
||
: [];
|
||
|
||
if (backupDirs.length === 0) {
|
||
logWarning('没有找到备份文件,跳过恢复');
|
||
return;
|
||
}
|
||
|
||
const latestBackupDir = path.join(backupDir, backupDirs[0]);
|
||
|
||
for (const file of customFiles) {
|
||
const srcPath = path.join(latestBackupDir, file);
|
||
const destPath = path.join(outputDir, file);
|
||
|
||
if (fs.existsSync(srcPath)) {
|
||
fs.copyFileSync(srcPath, destPath);
|
||
logInfo(` 恢复: ${file}`);
|
||
}
|
||
}
|
||
|
||
logSuccess('自定义文件已恢复');
|
||
}
|
||
|
||
/**
|
||
* 下载并保存 openapi.json 文件到本地
|
||
*/
|
||
function downloadOpenApiJson() {
|
||
return new Promise((resolve, reject) => {
|
||
const https = require('https');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const fileUrl = `${API_BASE_URL}/openapi.json`;
|
||
const outputPath = path.join(process.cwd(), 'scripts', 'openapi.json');
|
||
|
||
logInfo(`下载 OpenAPI 规范文件: ${fileUrl}`);
|
||
|
||
const startTime = Date.now();
|
||
|
||
// 创建 HTTPS 代理(如果需要,可以忽略 SSL 证书验证)
|
||
const agent = new https.Agent({
|
||
rejectUnauthorized: false // 跳过 SSL 证书验证
|
||
});
|
||
|
||
https.get(fileUrl, { agent }, (response) => {
|
||
if (response.statusCode !== 200) {
|
||
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
||
return;
|
||
}
|
||
|
||
let data = '';
|
||
|
||
response.on('data', (chunk) => {
|
||
data += chunk;
|
||
});
|
||
|
||
response.on('end', () => {
|
||
try {
|
||
// 验证 JSON 格式
|
||
JSON.parse(data);
|
||
|
||
// 写入文件
|
||
fs.writeFileSync(outputPath, data, 'utf8');
|
||
const executionTime = Date.now() - startTime;
|
||
logSuccess(`OpenAPI 规范已保存到: scripts/openapi.json (${executionTime}ms)`);
|
||
resolve();
|
||
} catch (error) {
|
||
reject(new Error(`JSON 格式错误: ${error.message}`));
|
||
}
|
||
});
|
||
}).on('error', (error) => {
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 使用 openapi-ts 命令生成客户端代码
|
||
*/
|
||
function generateWithOpenApiTS() {
|
||
return new Promise((resolve, reject) => {
|
||
logInfo('使用 @hey-api/openapi-ts 生成客户端代码...');
|
||
|
||
const { exec } = require('child_process');
|
||
const command = 'npx @hey-api/openapi-ts';
|
||
|
||
logInfo(`执行命令: ${command}`);
|
||
|
||
const startTime = Date.now();
|
||
|
||
exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => {
|
||
const executionTime = Date.now() - startTime;
|
||
|
||
if (error) {
|
||
logError(`openapi-ts 执行失败 (${executionTime}ms)`);
|
||
logError(`错误信息: ${error.message}`);
|
||
if (stderr) {
|
||
logError(`stderr: ${stderr}`);
|
||
}
|
||
reject(error);
|
||
return;
|
||
}
|
||
|
||
if (stderr) {
|
||
logWarning(`stderr: ${stderr}`);
|
||
}
|
||
|
||
logSuccess(`openapi-ts 执行成功 (${executionTime}ms)`);
|
||
if (stdout) {
|
||
logInfo(`输出: ${stdout}`);
|
||
}
|
||
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 验证生成的文件
|
||
*/
|
||
function validateGeneratedFiles() {
|
||
try {
|
||
logInfo('验证生成的文件...');
|
||
const outputDir = path.join(process.cwd(), 'src', 'lib', 'api');
|
||
|
||
if (!fs.existsSync(outputDir)) {
|
||
throw new Error('输出目录不存在');
|
||
}
|
||
|
||
const files = fs.readdirSync(outputDir);
|
||
logInfo(`输出目录包含 ${files.length} 个文件:`);
|
||
|
||
const requiredFiles = ['types.gen.ts', 'sdk.gen.ts', 'index.ts'];
|
||
const generatedFiles = [];
|
||
|
||
for (const file of files) {
|
||
const filePath = path.join(outputDir, file);
|
||
const stats = fs.statSync(filePath);
|
||
|
||
// 只显示文件,不显示目录
|
||
if (stats.isFile()) {
|
||
const size = (stats.size / 1024).toFixed(2);
|
||
logInfo(` 📄 ${file} (${size} KB)`);
|
||
generatedFiles.push(file);
|
||
}
|
||
}
|
||
|
||
// 检查必要文件
|
||
const missingFiles = requiredFiles.filter(file => !generatedFiles.includes(file));
|
||
if (missingFiles.length > 0) {
|
||
logWarning(`缺少期望的文件: ${missingFiles.join(', ')}`);
|
||
} else {
|
||
logSuccess('所有期望的文件都已生成');
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
logError(`验证生成的文件失败: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 主函数
|
||
*/
|
||
async function main() {
|
||
const startTime = Date.now();
|
||
|
||
log('='.repeat(60), 'cyan');
|
||
log('API 客户端代码生成脚本', 'cyan');
|
||
log('基于 @hey-api/openapi-ts', 'cyan');
|
||
log('='.repeat(60), 'cyan');
|
||
|
||
try {
|
||
// 检查是否有自定义文件
|
||
const customFiles = checkCustomFiles();
|
||
|
||
if (customFiles.length > 0) {
|
||
logInfo('检测到自定义文件,将在生成前进行备份');
|
||
backupCustomFiles(customFiles);
|
||
}
|
||
|
||
// 下载 openapi.json 文件到本地
|
||
await downloadOpenApiJson();
|
||
|
||
// 使用 openapi-ts 生成代码
|
||
await generateWithOpenApiTS();
|
||
|
||
// 恢复自定义文件
|
||
if (customFiles.length > 0) {
|
||
restoreCustomFiles(customFiles);
|
||
}
|
||
|
||
// 验证生成的文件
|
||
if (!validateGeneratedFiles()) {
|
||
logWarning('文件验证发现问题,但生成过程已完成');
|
||
}
|
||
|
||
const totalTime = Date.now() - startTime;
|
||
logSuccess(`API 客户端代码生成完成!总耗时: ${totalTime}ms`);
|
||
logSuccess('生成的文件位于 src/lib/api/ 目录');
|
||
|
||
if (customFiles.length > 0) {
|
||
logInfo('自定义文件已保持不变,避免覆盖手动配置');
|
||
}
|
||
|
||
} catch (error) {
|
||
const totalTime = Date.now() - startTime;
|
||
logError(`脚本执行失败 (${totalTime}ms): ${error.message}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// 执行主函数
|
||
if (require.main === module) {
|
||
main();
|
||
} |