UNPKG

cis-api-tool

Version:

根据 swagger/yapi/apifox 的接口定义生成 TypeScript/JavaScript 的接口类型及其请求函数代码。

568 lines (565 loc) 20.4 kB
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 };