yapi-ts-builder
Version:
基于 yapi-to-typescript 实现的 YApi 接口定义生成工具
414 lines (409 loc) • 21.4 kB
JavaScript
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()));
}
}