UNPKG

watch-code-generator

Version:

一个灵活的代码生成器,支持自定义模板和交互式界面

384 lines (322 loc) 11.7 kB
const axios = require('axios'); const fs = require('fs-extra'); const path = require('path'); const artTemplate = require('art-template'); const chalk = require('chalk'); const inquirer = require('inquirer'); // 颜色辅助函数 const success = text => chalk.green(text); const error = text => chalk.red(text); const warning = text => chalk.yellow(text); const info = text => chalk.blue(text); class Generator { constructor(config) { this.config = config; this.apis = []; } /** * 创建自定义API * @param {Object} customApi - 自定义API信息 * @returns {Object} - 创建的API对象 */ createCustomApi(customApi) { const apiPath = customApi.path.slice(1).replace(/\//g, '_'); const api = this.config.apiRefactor({ name: customApi.name, path: customApi.path, apiPath, tags: customApi.tags || '', methods: customApi.methods || ['get'], requestBody: customApi.parameters ? this.formatParameters(customApi.parameters) : null, responseBody: customApi.responseFields ? this.formatParameters(customApi.responseFields) : null, parameters: customApi.parameters || [] }); this.apis.push(api); return api; } /** * 格式化参数为API所需格式 * @param {Array} parameters - 参数数组 * @returns {Object} - 格式化后的参数对象 */ formatParameters(parameters) { const properties = {}; parameters.forEach(param => { properties[param.name] = { type: param.type || 'string', title: param.title || param.name, description: param.description || '', required: param.required || false, format: param.format || '' }; }); return properties; } async init() { const spinner = require('ora')('加载 OpenAPI 数据...').start(); try { const response = await axios.get(this.config.openapiUrl); this.apis = this.parseApis(response.data); spinner.succeed(success('✔ OpenAPI 数据加载成功')); } catch (e) { spinner.fail(error(`✖ OpenAPI 请求失败: ${e.message}`)); if (e.code === 'ENOTFOUND') { console.log(warning(' 可能原因: 网络连接问题或URL不正确')); console.log(info(' 建议: 检查网络连接和OpenAPI URL配置')); } else if (e.response && e.response.status) { console.log(warning(` 服务器返回状态码: ${e.response.status}`)); console.log(info(' 建议: 检查API密钥或访问权限')); } throw e; } } async generatePermission(permissions) { const spinner = require('ora')('正在生成权限类型定义...').start(); try { if (!Array.isArray(permissions) || permissions.length === 0) { throw new Error('请提供权限列表数组'); } const enumObject = permissions.reduce((acc, curr) => { const key = curr .replace(/^\//, '') .replace(/\//g, '_'); acc[key] = `'${curr}'` return acc }, {}); const typeContent = `// Auto-generated by generatePermissionTypes export const PermissionEnums = { ${Object.entries(enumObject) .map(([key, value]) => `${key}: ${value}`) .join(',\n ')} } as const; export type PermissionType = typeof PermissionEnums[keyof typeof PermissionEnums] declare module 'vue' { export interface ComponentCustomProperties { $hasPermission: (permission: PermissionType) => boolean; } } // 为指令声明类型 declare module '@vue/runtime-core' { export interface HTMLAttributes { 'v-permission'?: PermissionType | PermissionType[]; } } ` const typesPath = path.join(process.cwd(), 'src', 'permissions.ts'); fs.ensureDirSync(path.dirname(typesPath)); fs.writeFileSync(typesPath, typeContent, 'utf8'); spinner.succeed(success('✔ 权限类型定义文件生成成功:src/permissions.ts')); } catch (error) { spinner.fail(error('生成权限类型定义文件失败:', error)); } } parseApis(openapiData) { const permissions = Object.keys(openapiData.paths || {}); // 生成权限类型定义文件 this.generatePermission(permissions); // 解析OpenAPI数据 const paths = openapiData.paths || {}; const schemas = openapiData.components?.schemas || {}; return Object.entries(paths).map(([path, methods]) => { const methodsArray = Object.keys(methods); const firstMethod = Object.values(methods)[0] || {}; const tags = (firstMethod.tags || []).join('-'); const name = firstMethod.summary || firstMethod.description || firstMethod.operationId || path.split('/').pop(); // 解析请求体schema let requestBody = null; let requestBodySchema = null; if (firstMethod.requestBody?.content?.['application/json']?.schema) { const schema = firstMethod.requestBody.content['application/json'].schema; requestBodySchema = schema; requestBody = this.resolveSchema(schema, schemas); } // 解析响应体schema let responseBody = null; let responseBodySchema = null; if (firstMethod.responses?.['200']?.content?.['*/*']?.schema) { const schema = firstMethod.responses['200'].content['*/*'].schema; responseBodySchema = schema; responseBody = this.resolveSchema(schema, schemas); } const apiPath = path.slice(1).replace(/\//g, '_'); return this.config.apiRefactor({ name, path, apiPath, requestBodySchema, // 存储原始schema responseBodySchema, // 存储原始schema tags, methods: methodsArray, requestBody, responseBody, },paths); }); } resolveSchema(schema, schemas) { if (schema.$ref) { const refPath = schema.$ref.split('/').pop(); schema = schemas[refPath]; } if (schema.type === 'object') { const properties = {}; const required = schema.required || []; Object.entries(schema.properties || {}).forEach(([key, prop]) => { properties[key] = { type: prop.type, title: prop.title || key, description: prop.description || '', required: required.includes(key), format: prop.format }; }); return properties; } return null; } async generateFile(templatePath, data, outputPath, options = {}) { try { if (!fs.existsSync(templatePath)) { throw new Error(`模板文件不存在: ${templatePath}`); } let template = fs.readFileSync(templatePath, 'utf-8'); // 从文件路径生成路由路径 data.routeName = outputPath .replace(/^.*?src\/views/, '') // 移除 src/views 前的所有内容 .replace(/\.page\.vue$/, '') // 移除 .page.vue 后缀 .replace(/\.vue$/, ''); // 移除 .vue 后缀 if (!data.routeName.startsWith('/')) { data.routeName = '/' + data.routeName; } // 打印模板渲染数据 console.log(info('\n模板渲染数据:'), JSON.stringify(data, null, 2)); // 打印路径 console.log(info('\n文件路径:'), outputPath); const content = artTemplate.render(template, data); if (fs.existsSync(outputPath) && !options.overwriteAll) { const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: warning(`文件已存在: ${outputPath}\n是否覆盖?`), default: false }]); if (!overwrite) { console.log(warning('\n⚠ 用户取消了文件生成')); return false; } } fs.ensureDirSync(path.dirname(outputPath)); fs.writeFileSync(outputPath, content); return true; } catch (e) { const errorMessage = e.code === 'EACCES' ? `文件生成失败: 没有写入权限 (${outputPath})` : e.code === 'ENOENT' ? `文件生成失败: 路径不存在 (${outputPath})` : `文件生成失败: ${e.message}`; throw new Error(errorMessage); } } generate(api, type, customPath) { const basePath = customPath || `${this.config.outputDir}${api.path}`; const templatePath = process.cwd() + this.config.templates[type]; const suffix = this.config.fileSuffix[type]; let outputPath; // 移除路径中可能存在的.vue后缀 const cleanBasePath = basePath.replace(/\.[^/.]+$/, ''); switch(type) { case 'filter': case 'modal': const fileName = `${api.apiPath}_${type}${suffix}`; outputPath = cleanBasePath.includes('/components/') ? `${cleanBasePath}${suffix}` : `${path.dirname(cleanBasePath)}/components/${fileName}`; break; default: outputPath = `${cleanBasePath}${suffix}`; } const result = this.generateFile(templatePath, { api }, outputPath); if (result === false) { return false; } return outputPath; } async batchGenerate(apis, types) { const fileList = []; // 收集所有将要生成的文件路径 for (const api of apis) { for (const type of types) { const basePath = `${this.config.outputDir}${api.path}`; const templatePath = this.config.templates[type]; const suffix = this.config.fileSuffix[type]; const cleanBasePath = basePath.replace(/\.[^/.]+$/, ''); let outputPath; switch(type) { case 'filter': case 'modal': const fileName = `${api.apiPath}_${type}${suffix}`; outputPath = cleanBasePath.includes('/components/') ? `${cleanBasePath}${suffix}` : `${path.dirname(cleanBasePath)}/components/${fileName}`; break; default: outputPath = `${cleanBasePath}${suffix}`; } fileList.push({ api, type, templatePath, outputPath }); } } // 检查是否有文件已存在 const existingFiles = fileList.filter(({ outputPath }) => fs.existsSync(outputPath)); let overwriteAll = false; if (existingFiles.length > 0) { console.log(warning('\n以下文件已存在:')); existingFiles.forEach(({ outputPath }) => console.log(warning(outputPath))); const { overwriteChoice } = await inquirer.prompt([{ type: 'list', name: 'overwriteChoice', message: warning('如何处理已存在的文件?'), choices: [ { name: '全部覆盖', value: 'all' }, { name: '取消生成', value: 'cancel' } ] }]); if (overwriteChoice === 'cancel') { throw new Error('用户取消了批量生成'); } overwriteAll = overwriteChoice === 'all'; } // 批量生成文件 const results = []; const spinner = require('ora')('正在批量生成文件...').start(); const total = fileList.length; let current = 0; for (const { api, type, templatePath, outputPath } of fileList) { try { await this.generateFile(templatePath, { api }, outputPath, { overwriteAll }); results.push({ success: true, outputPath, type }); } catch (e) { results.push({ success: false, outputPath, type, error: e.message }); } current++; spinner.text = info(`正在批量生成文件... (${current}/${total})`); } spinner.stop(); return results; } } module.exports = Generator;