cis-api-tool
Version:
根据 swagger/yapi/apifox 的接口定义生成 TypeScript/JavaScript 的接口类型及其请求函数代码。
568 lines (565 loc) • 20.4 kB
JavaScript
import { traverse } from "./function-BAT0oHZU.mjs";
import { Method, RequestBodyType, RequestFormItemType, Required, ResponseBodyType } from "./types-K2vxvtaP.mjs";
import { FileData } from "./helpers-BjgNrIWi.mjs";
import { createRequire } from "node:module";
import { URL } from "url";
import isEmpty from "lodash/isEmpty";
import consola from "consola";
import fs from "fs-extra";
import path from "path";
import castArray from "lodash/castArray";
import cloneDeep from "lodash/cloneDeep";
import memoize from "lodash/memoize";
import { compile } from "json-schema-to-typescript";
import JSON5 from "json5";
import forOwn from "lodash/forOwn";
import isArray from "lodash/isArray";
import isObject from "lodash/isObject";
import mapKeys from "lodash/mapKeys";
import nodeFetch from "node-fetch";
import prettier from "prettier";
import ProxyAgent from "proxy-agent";
import { pinyin } from "pinyin-pro";
import toJsonSchema from "to-json-schema";
//#region rolldown:runtime
var __require = /* @__PURE__ */ createRequire(import.meta.url);
//#endregion
//#region src/utils.ts
/**
* @description 抛出错误。
* @param msg 错误信息
*/
function throwError(...msg) {
/* istanbul ignore next */
throw new Error(msg.join(""));
}
/**
* @description 将路径统一为 unix 风格的路径。
* @param path 路径
* @returns unix 风格的路径
*/
function toUnixPath(path$1) {
return path$1.replace(/[/\\]+/g, "/");
}
/**
* @description 获得规范化的相对路径。
* @param from 来源路径
* @param to 去向路径
* @returns 相对路径
*/
function getNormalizedRelativePath(from, to) {
return toUnixPath(path.relative(path.dirname(from), to)).replace(/^(?=[^.])/, "./").replace(/\.(ts|js)x?$/i, "");
}
/**
* @description 原地遍历 JSONSchema。
*/
function traverseJsonSchema(jsonSchema, cb, currentPath = []) {
/* istanbul ignore if */
if (!isObject(jsonSchema)) return jsonSchema;
if (isArray(jsonSchema.properties)) jsonSchema.properties = jsonSchema.properties.reduce((props, js) => {
props[js.name] = js;
return props;
}, {});
cb(jsonSchema, currentPath);
if (jsonSchema.properties) forOwn(jsonSchema.properties, (item, key) => {
traverseJsonSchema(item, cb, [...currentPath, key]);
return void 0;
});
if (jsonSchema.items) castArray(jsonSchema.items).forEach((item, index) => traverseJsonSchema(item, cb, [...currentPath, index]));
if (jsonSchema.oneOf) jsonSchema.oneOf.forEach((item) => traverseJsonSchema(item, cb, currentPath));
if (jsonSchema.anyOf) jsonSchema.anyOf.forEach((item) => traverseJsonSchema(item, cb, currentPath));
if (jsonSchema.allOf) jsonSchema.allOf.forEach((item) => traverseJsonSchema(item, cb, currentPath));
return jsonSchema;
}
/**
* @description 原地处理 JSONSchema。
* @param jsonSchema 待处理的 JSONSchema
* @returns 处理后的 JSONSchema
*/
function processJsonSchema(jsonSchema, customTypeMapping) {
return traverseJsonSchema(jsonSchema, (jsonSchema$1) => {
delete jsonSchema$1.$ref;
delete jsonSchema$1.$$ref;
if (jsonSchema$1.type === "array" && Array.isArray(jsonSchema$1.items) && jsonSchema$1.items.length) jsonSchema$1.items = jsonSchema$1.items[0];
if (jsonSchema$1.type) {
const typeMapping = {
byte: "integer",
short: "integer",
int: "integer",
long: "integer",
float: "number",
double: "number",
bigdecimal: "number",
char: "string",
void: "null",
...mapKeys(customTypeMapping, (_, key) => key.toLowerCase())
};
const isMultiple = Array.isArray(jsonSchema$1.type);
const types = castArray(jsonSchema$1.type).map((type) => {
type = type.toLowerCase();
type = typeMapping[type] || type;
return type;
});
jsonSchema$1.type = isMultiple ? types : types[0];
}
if (jsonSchema$1.properties) {
forOwn(jsonSchema$1.properties, (_, prop) => {
const propDef = jsonSchema$1.properties[prop];
delete jsonSchema$1.properties[prop];
jsonSchema$1.properties[prop.trim()] = propDef;
});
if (Array.isArray(jsonSchema$1.required)) jsonSchema$1.required = jsonSchema$1.required.map((prop) => prop.trim());
}
return jsonSchema$1;
});
}
/**
* @description 获取适用于 JSTT 的 JSONSchema。
* @param jsonSchema 待处理的 JSONSchema
* @returns 适用于 JSTT 的 JSONSchema
*/
function jsonSchemaToJSTTJsonSchema(jsonSchema, typeName) {
if (jsonSchema) delete jsonSchema.description;
return traverseJsonSchema(jsonSchema, (jsonSchema$1, currentPath) => {
const refValue = jsonSchema$1.title == null ? jsonSchema$1.description : jsonSchema$1.title;
if (refValue?.startsWith("&")) {
const typeRelativePath = refValue.substring(1);
const typeAbsolutePath = toUnixPath(path.resolve(path.dirname(`/${currentPath.join("/")}`.replace(/\/{2,}/g, "/")), typeRelativePath).replace(/^[a-z]+:/i, ""));
const typeAbsolutePathArr = typeAbsolutePath.split("/").filter(Boolean);
let tsTypeLeft = "";
let tsTypeRight = typeName;
for (const key of typeAbsolutePathArr) {
tsTypeLeft += "NonNullable<";
tsTypeRight += `[${JSON.stringify(key)}]>`;
}
const tsType = `${tsTypeLeft}${tsTypeRight}`;
jsonSchema$1.tsType = tsType;
}
delete jsonSchema$1.title;
delete jsonSchema$1.id;
delete jsonSchema$1.minItems;
delete jsonSchema$1.maxItems;
if (jsonSchema$1.type === "object") jsonSchema$1.additionalProperties = false;
delete jsonSchema$1.default;
return jsonSchema$1;
});
}
/**
* @description 将 JSONSchema 字符串转为 JSONSchema 对象。
*jsonSchemaStringToJsonSchema @param str 要转换的 JSONSchema 字符串
* @returns 转换后的 JSONSchema 对象
*/
function jsonSchemaStringToJsonSchema(str, customTypeMapping) {
return processJsonSchema(JSON.parse(str), customTypeMapping);
}
/**
* @description 获得 JSON 数据的 JSONSchema 对象。
* @param json JSON 数据
* @returns JSONSchema 对象
*/
function jsonToJsonSchema(json, customTypeMapping) {
const schema = toJsonSchema(json, {
required: false,
arrays: { mode: "first" },
objects: { additionalProperties: false },
strings: { detectFormat: false },
postProcessFnc: (type, schema$1, value) => {
if (!schema$1.description && !!value && type !== "object") schema$1.description = JSON.stringify(value);
return schema$1;
}
});
delete schema.description;
return processJsonSchema(schema, customTypeMapping);
}
/**
* @description 获得 mockjs 模板的 JSONSchema 对象。
* @param template mockjs 模板
* @returns JSONSchema 对象
*/
function mockjsTemplateToJsonSchema(template, customTypeMapping) {
const actions = [];
const keyRe = /(.+)\|(?:\+(\d+)|([+-]?\d+-?[+-]?\d*)?(?:\.(\d+-?\d*))?)/;
const numberPatterns = [
"natural",
"integer",
"float",
"range",
"increment"
];
const boolPatterns = ["boolean", "bool"];
const normalizeValue = (value) => {
if (typeof value === "string" && value.startsWith("@")) {
const pattern = value.slice(1);
if (numberPatterns.some((p) => pattern.startsWith(p))) return 1;
if (boolPatterns.some((p) => pattern.startsWith(p))) return true;
}
return value;
};
traverse(template, (value, key, parent) => {
if (typeof key === "string") actions.push(() => {
delete parent[key];
parent[key.replace(keyRe, "$1")] = normalizeValue(value);
});
});
actions.forEach((action) => action());
return jsonToJsonSchema(template, customTypeMapping);
}
/**
* @description 获得属性定义列表的 JSONSchema 对象。
* @param propDefinitions 属性定义列表
* @returns JSONSchema 对象
*/
function propDefinitionsToJsonSchema(propDefinitions, customTypeMapping) {
return processJsonSchema({
type: "object",
required: propDefinitions.reduce((res, prop) => {
if (prop.required) res.push(prop.name);
return res;
}, []),
properties: propDefinitions.reduce((res, prop) => {
if (prop.schema) res[prop.name] = {
...prop.schema,
description: prop.comment,
...prop.schema.type === "file" ? { tsType: FileData.name } : {}
};
else res[prop.name] = {
type: prop.type,
description: prop.comment,
...prop.type === "file" ? { tsType: FileData.name } : {}
};
return res;
}, {})
}, customTypeMapping);
}
const JSTTOptions = {
bannerComment: "",
style: {
bracketSpacing: false,
printWidth: 120,
semi: true,
singleQuote: true,
tabWidth: 4,
trailingComma: "none",
useTabs: false
}
};
/**
* @description 根据 JSONSchema 对象生产 TypeScript 类型定义。
* @param jsonSchema JSONSchema 对象
* @param typeName 类型名称
* @returns TypeScript 类型定义
*/
async function jsonSchemaToType(jsonSchema, typeName) {
if (isEmpty(jsonSchema)) return `export interface ${typeName} {}`;
if (jsonSchema.__is_any__) {
delete jsonSchema.__is_any__;
return `export type ${typeName} = any`;
}
const fakeTypeName = "THISISAFAKETYPENAME";
const code = await compile(jsonSchemaToJSTTJsonSchema(cloneDeep(jsonSchema), typeName), fakeTypeName, JSTTOptions);
return code.replace(fakeTypeName, typeName).trim();
}
function getRequestDataJsonSchema(interfaceInfo, customTypeMapping) {
let jsonSchema;
if (isPostLikeMethod(interfaceInfo.method)) switch (interfaceInfo.req_body_type) {
case RequestBodyType.form:
jsonSchema = propDefinitionsToJsonSchema(interfaceInfo.req_body_form.map((item) => ({
name: item.name,
required: item.required === Required.true,
type: item.type === RequestFormItemType.file ? "file" : "string",
comment: item.desc
})), customTypeMapping);
break;
case RequestBodyType.json:
if (interfaceInfo.req_body_other) jsonSchema = interfaceInfo.req_body_is_json_schema ? jsonSchemaStringToJsonSchema(interfaceInfo.req_body_other, customTypeMapping) : jsonToJsonSchema(JSON5.parse(interfaceInfo.req_body_other), customTypeMapping);
break;
default:
/* istanbul ignore next */
break;
}
if (isArray(interfaceInfo.req_query) && interfaceInfo.req_query.length) {
const queryJsonSchema = propDefinitionsToJsonSchema(interfaceInfo.req_query.map((item) => ({
name: item.name,
required: item.required === Required.true,
type: item.type || "string",
comment: item.desc,
schema: item.schema
})), customTypeMapping);
/* istanbul ignore else */
if (jsonSchema) {
jsonSchema.properties = {
...jsonSchema.properties,
...queryJsonSchema.properties
};
jsonSchema.required = [...Array.isArray(jsonSchema.required) ? jsonSchema.required : [], ...Array.isArray(queryJsonSchema.required) ? queryJsonSchema.required : []];
} else jsonSchema = queryJsonSchema;
}
if (isArray(interfaceInfo.req_params) && interfaceInfo.req_params.length) {
const paramsJsonSchema = propDefinitionsToJsonSchema(interfaceInfo.req_params.map((item) => ({
name: item.name,
required: true,
type: item.type || "string",
comment: item.desc,
schema: item.schema
})), customTypeMapping);
/* istanbul ignore else */
if (jsonSchema) {
jsonSchema.properties = {
...jsonSchema.properties,
...paramsJsonSchema.properties
};
jsonSchema.required = [...Array.isArray(jsonSchema.required) ? jsonSchema.required : [], ...Array.isArray(paramsJsonSchema.required) ? paramsJsonSchema.required : []];
} else jsonSchema = paramsJsonSchema;
}
return jsonSchema || {};
}
/**
* @description 获得响应数据 JSONSchema 对象。
* @param interfaceInfo 接口信息
* @param customTypeMapping 自定义类型映射
* @param dataKey 数据键
* @returns 响应数据 JSONSchema 对象
*/
function getResponseDataJsonSchema(interfaceInfo, customTypeMapping, dataKey) {
let jsonSchema = {};
switch (interfaceInfo.res_body_type) {
case ResponseBodyType.json:
if (interfaceInfo.res_body) jsonSchema = interfaceInfo.res_body_is_json_schema ? jsonSchemaStringToJsonSchema(interfaceInfo.res_body, customTypeMapping) : mockjsTemplateToJsonSchema(JSON5.parse(interfaceInfo.res_body), customTypeMapping);
break;
default:
jsonSchema = { __is_any__: true };
break;
}
if (dataKey && jsonSchema) jsonSchema = reachJsonSchema(jsonSchema, dataKey);
return jsonSchema;
}
/**
* @description 获取 JSONSchema 对象的指定路径。
* @param jsonSchema JSONSchema 对象
* @param path 路径
* @returns 指定路径的 JSONSchema 对象
*/
function reachJsonSchema(jsonSchema, path$1) {
let last = jsonSchema;
for (const segment of castArray(path$1)) {
const _last = last.properties?.[segment];
if (!_last) return jsonSchema;
last = _last;
}
return last;
}
/**
* @description 根据权重排序。
* @param list 列表
* @returns 排序后的列表
*/
function sortByWeights(list) {
list.sort((a, b) => {
const x = a.weights.length > b.weights.length ? b : a;
const minLen = Math.min(a.weights.length, b.weights.length);
const maxLen = Math.max(a.weights.length, b.weights.length);
x.weights.push(...new Array(maxLen - minLen).fill(0));
const w = a.weights.reduce((w$1, _, i) => {
if (w$1 === 0) w$1 = a.weights[i] - b.weights[i];
return w$1;
}, 0);
return w;
});
return list;
}
/**
* @description 判断是否为 GET 类请求。
* @param method 请求方式
* @returns 是否为 GET 类请求
*/
function isGetLikeMethod(method) {
return method === Method.GET || method === Method.OPTIONS || method === Method.HEAD;
}
/**
* @description 判断是否为 POST 类请求。
* @param method 请求方式
* @returns 是否为 POST 类请求
*/
function isPostLikeMethod(method) {
return !isGetLikeMethod(method);
}
/**
* @description 获取 prettier 配置。
* @param cwd 当前工作目录
* @returns prettier 配置
*/
async function getPrettier(cwd) {
const projectPrettierPath = path.join(cwd, "node_modules/prettier");
if (await fs.pathExists(projectPrettierPath)) return __require(projectPrettierPath);
return __require("prettier");
}
/**
* @description 获取 prettier 配置。
* @returns prettier 配置
*/
async function getPrettierOptions() {
const prettierOptions = {
parser: "typescript",
printWidth: 120,
tabWidth: 4,
singleQuote: true,
semi: false,
trailingComma: "all",
bracketSpacing: false,
endOfLine: "lf"
};
if (process.env.JEST_WORKER_ID) return prettierOptions;
const [prettierConfigPathErr, prettierConfigPath] = await (async () => {
const [err, path$1] = await prettier.resolveConfigFile();
consola.debug("获取 prettier 配置路径", path$1);
return [err, path$1];
})();
if (prettierConfigPathErr || !prettierConfigPath) return prettierOptions;
const [prettierConfigErr, prettierConfig] = await (async () => {
const [err, config] = await prettier.resolveConfig(prettierConfigPath);
return [err, config];
})();
if (prettierConfigErr || !prettierConfig) return prettierOptions;
return {
...prettierOptions,
...prettierConfig,
parser: "typescript"
};
}
/**
* @description 获取缓存的 prettier 配置。
* @returns prettier 配置
*/
const getCachedPrettierOptions = memoize(getPrettierOptions);
/**
* @description 获取 HTTP 请求。
* @param url 请求 URL
* @param query 请求参数
* @returns 请求结果
*/
async function httpGet(url$1, query) {
const _url = new URL(url$1);
if (query) Object.keys(query).forEach((key) => {
_url.searchParams.set(key, query[key]);
});
url$1 = _url.toString();
const res = await nodeFetch(url$1, {
method: "GET",
agent: new ProxyAgent()
});
return res.json();
}
/**
* @description 生成请求函数名称
* @param interfaceInfo 接口信息
* @param changeCase 大小写转换函数
* @returns 请求函数名称
*/
function getRequestFunctionName(interfaceInfo, changeCase) {
const _method = interfaceInfo.method || "get";
const methodPrefix = _method.toLowerCase();
let path$1 = interfaceInfo.path;
path$1 = path$1.replace(/^\/+/, "").replace(/^api\/+/, "");
path$1 = path$1.replace(/\{([^}]+)\}/g, (_, param) => `By${changeCase.pascalCase(param)}`);
const pathSegments = path$1.split("/").filter(Boolean);
const pathPart = pathSegments.map((segment) => changeCase.pascalCase(segment)).join("");
return `${methodPrefix}${pathPart}Api`;
}
/**
* @description 生成请求数据类型名称
* @param interfaceInfo 接口信息
* @param changeCase 大小写转换函数
* @returns 请求数据类型名称
*/
function getRequestDataTypeName(interfaceInfo, changeCase) {
const _method = interfaceInfo.method || "get";
const methodPrefix = changeCase.pascalCase(_method.toLowerCase());
let path$1 = interfaceInfo.path;
path$1 = path$1.replace(/^\/+/, "").replace(/^api\/+/, "");
path$1 = path$1.replace(/\{([^}]+)\}/g, (_, param) => `By${changeCase.pascalCase(param)}`);
const pathSegments = path$1.split("/").filter(Boolean);
const pathPart = pathSegments.map((segment) => changeCase.pascalCase(segment)).join("");
return `${methodPrefix}${pathPart}RequestType`;
}
/**
* @description 生成响应数据类型名称
* @param interfaceInfo 接口信息
* @param changeCase 大小写转换函数
* @returns 响应数据类型名称
*/
function getReponseDataTypeName(interfaceInfo, changeCase) {
const _method = interfaceInfo.method || "get";
const methodPrefix = changeCase.pascalCase(_method.toLowerCase());
let path$1 = interfaceInfo.path;
path$1 = path$1.replace(/^\/+/, "").replace(/^api\/+/, "");
path$1 = path$1.replace(/\{([^}]+)\}/g, (_, param) => `By${changeCase.pascalCase(param)}`);
const pathSegments = path$1.split("/").filter(Boolean);
const pathPart = pathSegments.map((segment) => changeCase.pascalCase(segment)).join("");
return `${methodPrefix}${pathPart}ResponseType`;
}
function getOutputFilePath(interfaceInfo, changeCase, outputDir = "src/service") {
const dirName = interfaceInfo._category.name;
const dirNameCn = dirName.split("/").filter(Boolean).map((segment) => {
return segment.split("").map((item) => {
return changeCase.upperCaseFirst(changeCase.lowerCase(pinyin(item, { toneType: "none" }))).trim();
}).join("");
}).join("/");
return `${outputDir}/${dirNameCn}/index.ts`;
}
function transformPaths(pathsArray, outputDir = "src/service") {
const targetSegments = outputDir.split("/");
return pathsArray.map((originalPath) => {
const normalizedPath = path.normalize(originalPath);
const pathSegments = normalizedPath.split(path.sep);
let targetIndex = -1;
for (let i = 0; i < pathSegments.length - targetSegments.length + 1; i++) {
let found = true;
for (let j = 0; j < targetSegments.length; j++) if (pathSegments[i + j] !== targetSegments[j]) {
found = false;
break;
}
if (found) {
targetIndex = i;
break;
}
}
if (targetIndex === -1) return `// 无法处理路径: ${originalPath}`;
const relativeSegments = pathSegments.slice(targetIndex + targetSegments.length);
if (relativeSegments.length === 0) return `// 根目录: ${originalPath}`;
const relativePath = "./" + relativeSegments.join("/") + "/index";
return `export * from '${relativePath}'`;
}).filter(Boolean);
}
/**
* 将相对路径转换为 alias 路径
* @param relativePath 相对路径
* @param outputDir 输出目录
* @returns alias 路径
*/
function getAliasPath(relativePath, outputDir = "src/service") {
if (relativePath.startsWith("@/")) return relativePath;
const normalizedPath = relativePath.replace(/\\/g, "/");
if (normalizedPath.includes("/src/")) {
const lastSrcIndex = normalizedPath.lastIndexOf("/src/");
if (lastSrcIndex !== -1) {
const afterSrc = normalizedPath.substring(lastSrcIndex + 5);
const withoutExt = afterSrc.replace(/\.(ts|js|tsx|jsx)$/, "");
return `@/${withoutExt}`;
}
}
return relativePath;
}
/**
* 获取规范化的相对路径,支持 alias 路径
* @param from 源文件路径
* @param to 目标文件路径
* @param outputDir 输出目录
* @returns 规范化的路径
*/
function getNormalizedPathWithAlias(from, to, outputDir = "src/service") {
const aliasPath = getAliasPath(to);
if (aliasPath.startsWith("@/")) return aliasPath;
return getNormalizedRelativePath(from, to);
}
//#endregion
export { __require, getAliasPath, getCachedPrettierOptions, getNormalizedPathWithAlias, getNormalizedRelativePath, getOutputFilePath, getPrettier, getPrettierOptions, getReponseDataTypeName, getRequestDataJsonSchema, getRequestDataTypeName, getRequestFunctionName, getResponseDataJsonSchema, httpGet, isGetLikeMethod, isPostLikeMethod, jsonSchemaStringToJsonSchema, jsonSchemaToJSTTJsonSchema, jsonSchemaToType, jsonToJsonSchema, mockjsTemplateToJsonSchema, processJsonSchema, propDefinitionsToJsonSchema, reachJsonSchema, sortByWeights, throwError, toUnixPath, transformPaths, traverseJsonSchema };