UNPKG

yapi-ts-builder

Version:

基于 yapi-to-typescript 实现的 YApi 接口定义生成工具

414 lines (409 loc) 21.4 kB
import * as changeCase from 'change-case'; import dayjs from 'dayjs'; import fs from 'fs-extra'; import path from 'path'; import { castArray, cloneDeepFast, dedent, groupBy, isEmpty, isFunction, last, memoize, omit, uniq, values, } from 'vtils'; import { getCachedPrettierOptions, getPrettier, getRequestDataJsonSchema, getResponseDataJsonSchema, httpGet, jsonSchemaToType, sortByWeights, throwError, } from './utils'; export class Generator { constructor(config, options = { cwd: process.cwd() }) { this.options = options; /** 配置 */ this.config = []; this.disposes = []; this.fetchProject = memoize(async ({ serverUrl, token }) => { /** 获取项目的基础信息 */ const projectInfo = await this.fetchApi(`${serverUrl}/api/project/get`, { token: token, }); const basePath = `/${projectInfo.basepath || '/'}` .replace(/\/+$/, '') .replace(/^\/+/, '/'); projectInfo.basepath = basePath; // 实现项目在 YApi 上的地址 projectInfo._url = `${serverUrl}/project/${projectInfo._id}/interface/api`; return projectInfo; }, ({ serverUrl, token }) => `${serverUrl}|${token}`); this.fetchExport = memoize(async ({ serverUrl, token }) => { const projectInfo = await this.fetchProject({ serverUrl, token }); const categoryList = await this.fetchApi(`${serverUrl}/api/plugin/export`, { type: 'json', status: 'all', isWiki: 'false', token: token, }); return categoryList.map(cat => { var _a, _b, _c, _d; const projectId = ((_b = (_a = cat.list) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.project_id) || 0; const catId = ((_d = (_c = cat.list) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.catid) || 0; // 实现分类在 YApi 上的地址 cat._url = `${serverUrl}/project/${projectId}/interface/api/cat_${catId}`; cat.list = (cat.list || []).map(item => { const interfaceId = item._id; // 实现接口在 YApi 上的地址 item._url = `${serverUrl}/project/${projectId}/interface/api/${interfaceId}`; item.path = `${projectInfo.basepath}${item.path}`; return item; }); return cat; }); }, ({ serverUrl, token }) => `${serverUrl}|${token}`); // config 可能是对象或数组,统一为数组 this.config = castArray(config); } async prepare() { this.config = await Promise.all( // config 可能是对象或数组,统一为数组 this.config.map(async (item) => { if (item.serverUrl) { // 去除地址后面的 / // fix: https://github.com/fjc0k/yapi-to-typescript/issues/22 item.serverUrl = item.serverUrl.replace(/\/+$/, ''); } return item; })); } async generate() { const outputFileList = Object.create(null); const usedTypeNames = {}; await Promise.all(this.config.map(async (serverConfig, serverIndex) => { /** * 将 projects 中的 token 数组展开,例如: * - 输入 * serverConfig.projects = [ * { name: '项目1', token: 'token1' }, * { name: '项目2', token: ['token2', 'token3'] } * ] * - 输出 * projects = [ * { name: '项目1', token: 'token1' }, * { name: '项目2', token: 'token2' }, * { name: '项目2', token: 'token3' } * ] */ const projects = serverConfig.projects.reduce((projects, project) => { projects.push( // 确保 token 是数组形式 ...castArray(project.token).map(token => ({ ...project, token: token, }))); return projects; }, []); return Promise.all(projects.map(async (projectConfig, projectIndex) => { const projectInfo = await this.fetchProjectInfo({ ...serverConfig, ...projectConfig, }); await Promise.all(projectConfig.categories.map(async (categoryConfig, categoryIndex) => { // 分类处理,castArray数组化 let categoryIds = castArray(categoryConfig.id); // 全部分类 if (categoryIds.includes(0)) { categoryIds.push(...projectInfo.cats.map(cat => cat._id)); } // 唯一化 categoryIds = uniq(categoryIds); // 去掉被排除的分类 const excludedCategoryIds = categoryIds .filter(id => id < 0) .map(Math.abs); categoryIds = categoryIds.filter(id => !excludedCategoryIds.includes(Math.abs(id))); // 删除不存在的分类 categoryIds = categoryIds.filter(id => !!projectInfo.cats.find(cat => cat._id === id)); // 顺序化 categoryIds = categoryIds.sort(); const codes = (await Promise.all(categoryIds.map(async (id, categoryIndex2) => { categoryConfig = { ...categoryConfig, id: id, }; const syntheticalConfig = { ...serverConfig, ...projectConfig, ...categoryConfig, mockUrl: projectInfo.getMockUrl(), }; syntheticalConfig.target = 'typescript'; syntheticalConfig.devUrl = projectInfo.getDevUrl(syntheticalConfig.devEnvName); syntheticalConfig.prodUrl = projectInfo.getProdUrl(syntheticalConfig.prodEnvName); // 获取当前类目下的接口列表 let interfaceList = await this.fetchInterfaceList(syntheticalConfig); interfaceList = interfaceList .map(interfaceInfo => { // 实现 _project 字段 interfaceInfo._project = omit(projectInfo, [ 'cats', 'getMockUrl', 'getDevUrl', 'getProdUrl', ]); // 预处理 const _interfaceInfo = isFunction(syntheticalConfig.preproccessInterface) ? syntheticalConfig.preproccessInterface(cloneDeepFast(interfaceInfo), changeCase, syntheticalConfig) : interfaceInfo; return _interfaceInfo; }) .filter(Boolean); interfaceList.sort((a, b) => a._id - b._id); const interfaceCodes = await Promise.all(interfaceList.map(async (interfaceInfo) => { // 生成分类唯一标识 const outputFilePath = path.resolve(this.options.cwd, typeof syntheticalConfig.outputFilePath === 'function' ? syntheticalConfig.outputFilePath(interfaceInfo, changeCase) : syntheticalConfig.outputFilePath); // 生成分类唯一标识 const categoryUID = `_${serverIndex}_${projectIndex}_${categoryIndex}_${categoryIndex2}`; // 生成接口代码 const code = await this.generateInterfaceCode(syntheticalConfig, interfaceInfo, categoryUID, usedTypeNames); const weights = [ serverIndex, projectIndex, categoryIndex, categoryIndex2, ]; return { categoryUID, outputFilePath, weights, code, _category: interfaceInfo._category, }; })); // 接口代码分组,将接口代码按输出文件路径分组,相同路径的接口代码会被组织在一起 const groupedInterfaceCodes = groupBy(interfaceCodes, item => item.outputFilePath); // 看不懂下面这段代码 return Object.keys(groupedInterfaceCodes).map(outputFilePath => { const categoryCode = [ ...uniq(sortByWeights(groupedInterfaceCodes[outputFilePath]).map(item => item.categoryUID)).map(() => ''), ...sortByWeights(groupedInterfaceCodes[outputFilePath]).map(item => item.code), ] .filter(Boolean) .join('\n\n'); if (!outputFileList[outputFilePath]) { outputFileList[outputFilePath] = { _category: groupedInterfaceCodes[outputFilePath][0] ._category, content: [], }; } return { outputFilePath: outputFilePath, code: categoryCode, weights: last(sortByWeights(groupedInterfaceCodes[outputFilePath])).weights, }; }); }))).flat(); for (const groupedCodes of values(groupBy(codes, item => item.outputFilePath))) { // 根据权重对同一文件内的代码进行排序 sortByWeights(groupedCodes); // 将排序后的代码内容添加到对应文件的 content 数组中 outputFileList[groupedCodes[0].outputFilePath].content.push(...groupedCodes.map(item => item.code)); } })); })); })); // // 遍历输出文件列表 // for (const path of Object.keys(outputFileList)) { // const content = outputFileList[path].content // // 去掉重复ts类型定义 // outputFileList[path].content = deduplicateTypeDefinitions(content) // } return outputFileList; } async write(outputFileList) { return Promise.all(Object.keys(outputFileList).map(async (outputFilePath) => { const { content, _category } = outputFileList[outputFilePath]; // 支持 .jsx? 后缀 outputFilePath = outputFilePath.replace(/\.js(x)?$/, '.ts$1'); // 始终写入主文件 const rawOutputContent = dedent ` /* tslint:disable */ /* eslint-disable */ /* 该文件由 yapi-ts-builder 自动生成,请勿直接修改!!! */ /* 每个文件对应Yapi上的一个分类 [${_category === null || _category === void 0 ? void 0 : _category.name}↗](${_category === null || _category === void 0 ? void 0 : _category._url}) */ ${dedent ` // @ts-ignore type FileData = File ${content.join('\n\n').trim()}`} `; // ref: https://prettier.io/docs/en/options.html const prettier = await getPrettier(this.options.cwd); // 此处需用 await 以兼容 Prettier 3 const prettyOutputContent = await prettier.format(rawOutputContent, { ...(await getCachedPrettierOptions()), filepath: outputFilePath, }); // 暂时跳过 prettier 格式化 // const prettyOutputContent = rawOutputContent; const outputContent = `${dedent ` /* prettier-ignore-start */ ${prettyOutputContent} /* prettier-ignore-end */ `}\n`; await fs.outputFile(outputFilePath, outputContent); })); } async fetchApi(url, query) { const res = await httpGet(url, query); /* istanbul ignore next */ if (res && res.errcode) { throwError(`${res.errmsg} [请求地址: ${url}] [请求参数: ${new URLSearchParams(query).toString()}]`); } return res.data || res; } /** 获取分类的接口列表 */ async fetchInterfaceList({ serverUrl, token, id, }) { const category = ((await this.fetchExport({ serverUrl, token })) || []).find(cat => !isEmpty(cat) && !isEmpty(cat.list) && cat.list[0].catid === id); if (category) { category.list.forEach(interfaceInfo => { // 实现 _category 字段 interfaceInfo._category = omit(category, ['list']); }); } return category ? category.list : []; } /** 获取项目信息 */ async fetchProjectInfo(syntheticalConfig) { /** 获取项目的基础信息 */ const projectInfo = await this.fetchProject(syntheticalConfig); /** 获取项目的菜单列表 */ const projectCats = await this.fetchApi(`${syntheticalConfig.serverUrl}/api/interface/getCatMenu`, { token: syntheticalConfig.token, project_id: projectInfo._id, }); return { ...projectInfo, cats: projectCats, getMockUrl: () => `${syntheticalConfig.serverUrl}/mock/${projectInfo._id}`, getDevUrl: (devEnvName) => { const env = projectInfo.env.find(e => e.name === devEnvName); return (env && env.domain) /* istanbul ignore next */ || ''; }, getProdUrl: (prodEnvName) => { const env = projectInfo.env.find(e => e.name === prodEnvName); return (env && env.domain) /* istanbul ignore next */ || ''; }, }; } /** 生成单个api的ts代码 */ async generateInterfaceCode(syntheticalConfig, // 综合配置参数 interfaceInfo, // 接口信息 categoryUID, // 分类唯一标识 usedTypeNames = {}) { // 扩展接口信息,添加解析后的路径(解析为一个对象,包含路径的各个部分) const extendedInterfaceInfo = { ...interfaceInfo, parsedPath: path.parse(interfaceInfo.path), }; // 生成各种命名,支持自定义命名函数。 // 生成请求函数名称 const requestFunctionName = changeCase.camelCase(extendedInterfaceInfo.parsedPath.name); // 生成请求数据类型名称 let requestDataTypeName = isFunction(syntheticalConfig.getRequestDataTypeName) ? await syntheticalConfig.getRequestDataTypeName(extendedInterfaceInfo, changeCase) : changeCase.pascalCase(`${requestFunctionName}Request`); if (usedTypeNames[requestDataTypeName]) { requestDataTypeName = `${requestDataTypeName}_${extendedInterfaceInfo === null || extendedInterfaceInfo === void 0 ? void 0 : extendedInterfaceInfo._id}`; } usedTypeNames[requestDataTypeName] = true; // 生成响应数据类型名称 let responseDataTypeName = isFunction(syntheticalConfig.getResponseDataTypeName) ? await syntheticalConfig.getResponseDataTypeName(extendedInterfaceInfo, changeCase) : changeCase.pascalCase(`${requestFunctionName}Response`); if (usedTypeNames[responseDataTypeName]) { responseDataTypeName = `${responseDataTypeName}_${extendedInterfaceInfo === null || extendedInterfaceInfo === void 0 ? void 0 : extendedInterfaceInfo._id}`; } usedTypeNames[responseDataTypeName] = true; // 生成请求数据的JSON Schema const requestDataJsonSchema = getRequestDataJsonSchema(extendedInterfaceInfo, syntheticalConfig.customTypeMapping || {}); // 将JSON Schema转换为TypeScript类型 const requestDataType = await jsonSchemaToType(requestDataJsonSchema, requestDataTypeName, usedTypeNames); // 生成响应数据的JSON Schema const responseDataJsonSchema = getResponseDataJsonSchema(extendedInterfaceInfo, syntheticalConfig.customTypeMapping || {}, syntheticalConfig.dataKey); // 将JSON Schema转换为TypeScript类型 const responseDataType = await jsonSchemaToType(responseDataJsonSchema, responseDataTypeName, usedTypeNames); // 接口注释 const genComment = (genTitle) => { // 解构注释配置选项 const { enabled: isEnabled = true, title: hasTitle = true, category: hasCategory = true, tag: hasTag = true, requestHeader: hasRequestHeader = true, updateTime: hasUpdateTime = true, link: hasLink = true, extraTags, } = { ...syntheticalConfig.comment, }; // 如果注释被禁用则返回空字符串 if (!isEnabled) { return ''; } // 转义标题中的 / const escapedTitle = String(extendedInterfaceInfo.title).replace(/\//g, '\\/'); // 生成描述文本 const description = hasLink ? `[${escapedTitle}↗](${extendedInterfaceInfo._url})` : escapedTitle; // 生成注释摘要数组 const summary = [ hasCategory && { label: '分类', value: hasLink ? `[${extendedInterfaceInfo._category.name}↗](${extendedInterfaceInfo._category._url})` : extendedInterfaceInfo._category.name, }, hasTag && { label: '标签', value: extendedInterfaceInfo.tag.map(tag => `\`${tag}\``), }, hasRequestHeader && { label: '请求地址', value: `\`${extendedInterfaceInfo.method.toUpperCase()} ${extendedInterfaceInfo.path}\``, }, hasUpdateTime && { label: '更新时间', value: process.env.JEST_WORKER_ID // 测试时使用 unix 时间戳 ? String(extendedInterfaceInfo.up_time) : /* istanbul ignore next */ `\`${dayjs(extendedInterfaceInfo.up_time * 1000).format('YYYY-MM-DD HH:mm:ss')}\``, }, ]; // 处理额外标签 if (typeof extraTags === 'function') { const tags = extraTags(extendedInterfaceInfo); for (const tag of tags) { ; (tag.position === 'start' ? summary.unshift : summary.push).call(summary, { label: tag.name, value: tag.value, }); } } // 生成标题注释 const titleComment = hasTitle ? dedent ` * ${genTitle(description)} * ` : ''; // 生成额外注释 const extraComment = summary .filter(item => typeof item !== 'boolean' && !isEmpty(item.value)) .map(item => { const _item = item; return `* @${_item.label} ${castArray(_item.value).join(', ')}`; }) .join('\n'); // 返回完整的注释文本 return dedent ` /** ${[titleComment, extraComment].filter(Boolean).join('\n')} */ `; }; // 返回生成的代码 return dedent ` ${genComment(title => `${title} 的 **请求类型**`)} ${requestDataType.trim()} ${genComment(title => `${title} 的 **返回类型**`)} ${responseDataType.trim()} `; } async destroy() { return Promise.all(this.disposes.map(async (dispose) => dispose())); } }