UNPKG

yapi-ts-builder

Version:

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

651 lines (650 loc) 26.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.httpGet = exports.getCachedPrettierOptions = exports.getPrettierOptions = exports.getPrettier = exports.isPostLikeMethod = exports.isGetLikeMethod = exports.sortByWeights = exports.reachJsonSchema = exports.getResponseDataJsonSchema = exports.getRequestDataJsonSchema = exports.jsonSchemaToType = exports.propDefinitionsToJsonSchema = exports.mockjsTemplateToJsonSchema = exports.jsonToJsonSchema = exports.jsonSchemaStringToJsonSchema = exports.jsonSchemaToJSTTJsonSchema = exports.toUnixPath = exports.processJsonSchema = exports.traverseJsonSchema = exports.throwError = void 0; const changeCase = __importStar(require("change-case")); const fs_extra_1 = __importDefault(require("fs-extra")); const json_schema_to_typescript_1 = require("json-schema-to-typescript"); const json5_1 = __importDefault(require("json5")); const node_fetch_1 = __importDefault(require("node-fetch")); const path_1 = __importDefault(require("path")); const prettier_1 = __importDefault(require("prettier")); const proxy_agent_1 = __importDefault(require("proxy-agent")); const to_json_schema_1 = __importDefault(require("to-json-schema")); const url_1 = require("url"); const vtils_1 = require("vtils"); const helpers_1 = require("./helpers"); const types_1 = require("./types"); /** * 抛出错误。 * * @param msg 错误信息 */ function throwError(...msg) { /* istanbul ignore next */ throw new Error(msg.join('')); } exports.throwError = throwError; /** * 原地遍历 JSONSchema。 */ function traverseJsonSchema(jsonSchema, cb, currentPath = []) { /* istanbul ignore if */ if (!(0, vtils_1.isObject)(jsonSchema)) return jsonSchema; // Mock.toJSONSchema 产生的 properties 为数组,然而 JSONSchema4 的 properties 为对象 if ((0, vtils_1.isArray)(jsonSchema.properties)) { jsonSchema.properties = jsonSchema.properties.reduce((props, js) => { props[js.name] = js; return props; }, {}); } // 处理传入的 JSONSchema cb(jsonSchema, currentPath); // 继续处理对象的子元素 if (jsonSchema.properties) { (0, vtils_1.forOwn)(jsonSchema.properties, (item, key) => traverseJsonSchema(item, cb, [...currentPath, key])); } // 继续处理数组的子元素 if (jsonSchema.items) { (0, vtils_1.castArray)(jsonSchema.items).forEach((item, index) => traverseJsonSchema(item, cb, [...currentPath, index])); } // 处理 oneOf if (jsonSchema.oneOf) { jsonSchema.oneOf.forEach(item => traverseJsonSchema(item, cb, currentPath)); } // 处理 anyOf if (jsonSchema.anyOf) { jsonSchema.anyOf.forEach(item => traverseJsonSchema(item, cb, currentPath)); } // 处理 allOf if (jsonSchema.allOf) { jsonSchema.allOf.forEach(item => traverseJsonSchema(item, cb, currentPath)); } return jsonSchema; } exports.traverseJsonSchema = traverseJsonSchema; /** * 原地处理 JSONSchema。 * * @param jsonSchema 待处理的 JSONSchema * @returns 处理后的 JSONSchema */ function processJsonSchema(jsonSchema, customTypeMapping) { return traverseJsonSchema(jsonSchema, jsonSchema => { // 数组只取第一个判断类型 if (jsonSchema.type === 'array' && Array.isArray(jsonSchema.items) && jsonSchema.items.length) { jsonSchema.items = jsonSchema.items[0]; } // 处理类型名称为标准的 JSONSchema 类型名称 if (jsonSchema.type) { // 类型映射表,键都为小写 const typeMapping = { byte: 'integer', short: 'integer', int: 'integer', long: 'integer', float: 'number', double: 'number', bigdecimal: 'number', char: 'string', void: 'null', ...(0, vtils_1.mapKeys)(customTypeMapping, (_, key) => key.toLowerCase()), }; const isMultiple = Array.isArray(jsonSchema.type); const types = (0, vtils_1.castArray)(jsonSchema.type).map(type => { // 所有类型转成小写,如:String -> string type = type.toLowerCase(); // 映射为标准的 JSONSchema 类型 type = typeMapping[type] || type; return type; }); jsonSchema.type = isMultiple ? types : types[0]; } // 移除字段名称首尾空格 if (jsonSchema.properties) { (0, vtils_1.forOwn)(jsonSchema.properties, (_, prop) => { const propDef = jsonSchema.properties[prop]; delete jsonSchema.properties[prop]; jsonSchema.properties[prop.trim()] = propDef; }); if (Array.isArray(jsonSchema.required)) { jsonSchema.required = jsonSchema.required.map(prop => prop.trim()); } } return jsonSchema; }); } exports.processJsonSchema = processJsonSchema; /** * 将路径统一为 unix 风格的路径。 * * @param path 路径 * @returns unix 风格的路径 */ function toUnixPath(path) { return path.replace(/[/\\]+/g, '/'); } exports.toUnixPath = toUnixPath; /** * 获取适用于 JSTT 的 JSONSchema。 * * @param jsonSchema 待处理的 JSONSchema * @returns 适用于 JSTT 的 JSONSchema */ function jsonSchemaToJSTTJsonSchema(jsonSchema, typeName) { if (jsonSchema) { // 去除最外层的 description 以防止 JSTT 提取它作为类型的注释 delete jsonSchema.description; } return traverseJsonSchema(jsonSchema, (jsonSchema, currentPath) => { // 支持类型引用 const refValue = // YApi 低版本不支持配置 title,可以在 description 里配置 jsonSchema.title == null ? jsonSchema.description : jsonSchema.title; if (refValue === null || refValue === void 0 ? void 0 : refValue.startsWith('&')) { const typeRelativePath = refValue.substring(1); const typeAbsolutePath = toUnixPath(path_1.default .resolve(path_1.default.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}`; // 自定义的 TypeScript 类型表达式,解决标准 JSON Schema 无法表达复杂类型的限制 jsonSchema.tsType = tsType; } // 去除 title 和 id,防止 json-schema-to-typescript 提取它们作为接口名 delete jsonSchema.title; delete jsonSchema.id; // 忽略数组长度限制 delete jsonSchema.minItems; delete jsonSchema.maxItems; if (jsonSchema.type === 'object') { // 将 additionalProperties 设为 false jsonSchema.additionalProperties = false; } // 删除 default,防止 json-schema-to-typescript 根据它推测类型 delete jsonSchema.default; return jsonSchema; }); } exports.jsonSchemaToJSTTJsonSchema = jsonSchemaToJSTTJsonSchema; /** * 将 JSONSchema 字符串转为 JSONSchema 对象。 * * @param str 要转换的 JSONSchema 字符串 * @returns 转换后的 JSONSchema 对象 */ function jsonSchemaStringToJsonSchema(str, customTypeMapping) { return processJsonSchema(JSON.parse(str), customTypeMapping); } exports.jsonSchemaStringToJsonSchema = jsonSchemaStringToJsonSchema; /** * 获得 JSON 数据的 JSONSchema 对象。 * * @param json JSON 数据 * @returns JSONSchema 对象 */ function jsonToJsonSchema(json, customTypeMapping) { const schema = (0, to_json_schema_1.default)(json, { required: false, arrays: { mode: 'first', }, objects: { additionalProperties: false, }, strings: { detectFormat: false, }, postProcessFnc: (type, schema, value) => { if (!schema.description && !!value && type !== 'object') { schema.description = JSON.stringify(value); } return schema; }, }); delete schema.description; return processJsonSchema(schema, customTypeMapping); } exports.jsonToJsonSchema = jsonToJsonSchema; /** * 获得 mockjs 模板的 JSONSchema 对象。 * * @param template mockjs 模板 * @returns JSONSchema 对象 */ function mockjsTemplateToJsonSchema(template, customTypeMapping) { const actions = []; // https://github.com/nuysoft/Mock/blob/refactoring/src/mock/constant.js#L27 const keyRe = /(.+)\|(?:\+(\d+)|([+-]?\d+-?[+-]?\d*)?(?:\.(\d+-?\d*))?)/; // https://github.com/nuysoft/Mock/wiki/Mock.Random 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; }; (0, vtils_1.traverse)(template, (value, key, parent) => { if (typeof key === 'string') { actions.push(() => { delete parent[key]; parent[ // https://github.com/nuysoft/Mock/blob/refactoring/src/mock/schema/schema.js#L16 key.replace(keyRe, '$1')] = normalizeValue(value); }); } }); actions.forEach(action => action()); return jsonToJsonSchema(template, customTypeMapping); } exports.mockjsTemplateToJsonSchema = mockjsTemplateToJsonSchema; /** * 获得属性定义列表的 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) => { res[prop.name] = { type: prop.type, description: prop.comment, ...(prop.type === 'file' ? { tsType: helpers_1.FileData.name } : {}), }; return res; }, {}), }, customTypeMapping); } exports.propDefinitionsToJsonSchema = propDefinitionsToJsonSchema; const JSTTOptions = { bannerComment: '', enableConstEnums: false, style: { bracketSpacing: false, printWidth: 120, semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'none', useTabs: false, }, }; /** * 解析枚举描述 * @param enumValues 枚举值 * @param enumDesc 枚举描述 * @returns 枚举值与描述的映射 */ function parseEnumDescriptions(enumValues, enumDesc) { const tsEnumNames = []; const enumDescriptions = []; if (enumDesc) { // 分隔符 const separator = enumDesc.includes(';') ? ';' : '\n'; const lines = enumDesc.split(separator); for (const line of lines) { const [name, comment] = line.split(':').map(s => s.trim()); if (name) { tsEnumNames.push(name); } if (comment) { enumDescriptions.push(comment); } } } return { tsEnumNames, enumDescriptions, }; } // 【关键改动】处理 JSONSchema 的嵌套类型(递归提取所有嵌套结构为独立的接口类型) function extractNestedTypes(schema, parentName, definitions = {}, usedTypeNames = {}) { var _a; // 处理当前 schema 的 properties if (schema.type === 'object' && schema.properties) { const newProperties = {}; for (const [key, value] of Object.entries(schema.properties)) { if (value && typeof value === 'object' && value.enum && value.enumDesc && value.enumDesc.includes('枚举生成格式')) { const pascalCaseKey = changeCase.pascalCase(key); // 处理枚举类型 const enumName = usedTypeNames[pascalCaseKey] ? `${parentName}_${pascalCaseKey}` : pascalCaseKey; usedTypeNames[enumName] = true; const { tsEnumNames, enumDescriptions } = parseEnumDescriptions(value.enum, (_a = value.enumDesc) === null || _a === void 0 ? void 0 : _a.slice(6)); // 生成枚举定义 definitions[enumName] = { type: value === null || value === void 0 ? void 0 : value.type, enum: value.enum, description: value.description, tsEnumNames: tsEnumNames, // 添加枚举别名 }; // 在当前 schema 中引用枚举类型 newProperties[key] = { $ref: `#/definitions/${enumName}` }; } else if (value && typeof value === 'object' && value.type === 'object') { // 为嵌套对象生成独立的接口名称(如果存在重复,则拼接父级名称) const pascalCaseKey = changeCase.pascalCase(key); const nestedInterfaceName = usedTypeNames[pascalCaseKey] ? `${parentName}_${pascalCaseKey}` : pascalCaseKey; usedTypeNames[nestedInterfaceName] = true; // 递归提取嵌套结构 definitions[nestedInterfaceName] = extractNestedTypes(value, nestedInterfaceName, definitions, usedTypeNames); // 在当前 schema 中引用嵌套接口 newProperties[key] = { $ref: `#/definitions/${nestedInterfaceName}` }; } else if (value && typeof value === 'object' && value.type === 'array') { // 处理数组类型的嵌套对象 const arrayItem = value.items; if (arrayItem && typeof arrayItem === 'object' && arrayItem.type === 'object') { const pascalCaseKey = `${changeCase.pascalCase(key)}_Item`; const nestedInterfaceName = usedTypeNames[pascalCaseKey] ? `${parentName}_${pascalCaseKey}` : pascalCaseKey; usedTypeNames[nestedInterfaceName] = true; definitions[nestedInterfaceName] = extractNestedTypes(arrayItem, nestedInterfaceName, definitions, usedTypeNames); newProperties[key] = { type: 'array', items: { $ref: `#/definitions/${nestedInterfaceName}` }, }; } else { newProperties[key] = value; } } else { // 普通字段直接保留 newProperties[key] = value; } } schema.properties = newProperties; } // 将提取的 definitions 添加到 schema schema.definitions = { ...(schema.definitions || {}), ...definitions, }; return schema; } /** * 根据 JSONSchema 对象生产 TypeScript 类型定义。 * * @param jsonSchema JSONSchema 对象 * @param typeName 类型名称 * @returns TypeScript 类型定义 */ async function jsonSchemaToType(jsonSchema, typeName, usedTypeNames = {}) { if ((0, vtils_1.isEmpty)(jsonSchema)) { return `export interface ${typeName} {}`; } if (jsonSchema.__is_any__) { delete jsonSchema.__is_any__; return `export type ${typeName} = any`; } // JSTT 会转换 typeName,因此传入一个全大写的假 typeName,生成代码后再替换回真正的 typeName const fakeTypeName = 'THISISAFAKETYPENAME'; const schema = jsonSchemaToJSTTJsonSchema((0, vtils_1.cloneDeepFast)(jsonSchema), typeName); // 提取所有嵌套结构为独立的接口schema & 枚举支持 const transformedSchema = extractNestedTypes(schema, typeName, {}, usedTypeNames); const code = await (0, json_schema_to_typescript_1.compile)(transformedSchema, fakeTypeName, JSTTOptions); return code.replace(fakeTypeName, typeName).trim(); } exports.jsonSchemaToType = jsonSchemaToType; /** 没有读懂 */ function getRequestDataJsonSchema(interfaceInfo, customTypeMapping) { let jsonSchema; // 处理表单数据(仅 POST 类接口) if (isPostLikeMethod(interfaceInfo.method)) { switch (interfaceInfo.req_body_type) { case types_1.RequestBodyType.form: jsonSchema = propDefinitionsToJsonSchema(interfaceInfo.req_body_form.map(item => ({ name: item.name, required: item.required === types_1.Required.true, type: (item.type === types_1.RequestFormItemType.file ? 'file' : 'string'), comment: item.desc, })), customTypeMapping); break; case types_1.RequestBodyType.json: if (interfaceInfo.req_body_other) { jsonSchema = interfaceInfo.req_body_is_json_schema ? jsonSchemaStringToJsonSchema(interfaceInfo.req_body_other, customTypeMapping) : jsonToJsonSchema(json5_1.default.parse(interfaceInfo.req_body_other), customTypeMapping); } break; default: /* istanbul ignore next */ break; } } // 处理查询数据 if ((0, vtils_1.isArray)(interfaceInfo.req_query) && interfaceInfo.req_query.length) { const queryJsonSchema = propDefinitionsToJsonSchema(interfaceInfo.req_query.map(item => ({ name: item.name, required: item.required === types_1.Required.true, type: item.type || 'string', comment: item.desc, })), 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 ((0, vtils_1.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, })), 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 || {}; } exports.getRequestDataJsonSchema = getRequestDataJsonSchema; /** 没有读懂 */ function getResponseDataJsonSchema(interfaceInfo, customTypeMapping, dataKey) { let jsonSchema = {}; switch (interfaceInfo.res_body_type) { case types_1.ResponseBodyType.json: if (interfaceInfo.res_body) { jsonSchema = interfaceInfo.res_body_is_json_schema ? jsonSchemaStringToJsonSchema(interfaceInfo.res_body, customTypeMapping) : mockjsTemplateToJsonSchema(json5_1.default.parse(interfaceInfo.res_body), customTypeMapping); } break; default: jsonSchema = { __is_any__: true }; break; } if (dataKey && jsonSchema) { jsonSchema = reachJsonSchema(jsonSchema, dataKey); } return jsonSchema; } exports.getResponseDataJsonSchema = getResponseDataJsonSchema; function reachJsonSchema(jsonSchema, path) { var _a; let last = jsonSchema; for (const segment of (0, vtils_1.castArray)(path)) { const _last = (_a = last.properties) === null || _a === void 0 ? void 0 : _a[segment]; if (!_last) { return jsonSchema; } last = _last; } return last; } exports.reachJsonSchema = reachJsonSchema; 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, _, i) => { if (w === 0) { w = a.weights[i] - b.weights[i]; } return w; }, 0); return w; }); return list; } exports.sortByWeights = sortByWeights; function isGetLikeMethod(method) { return (method === types_1.Method.GET || method === types_1.Method.OPTIONS || method === types_1.Method.HEAD); } exports.isGetLikeMethod = isGetLikeMethod; function isPostLikeMethod(method) { return !isGetLikeMethod(method); } exports.isPostLikeMethod = isPostLikeMethod; async function getPrettier(cwd) { const projectPrettierPath = path_1.default.join(cwd, 'node_modules/prettier'); if (await fs_extra_1.default.pathExists(projectPrettierPath)) { return require(projectPrettierPath); } return require('prettier'); } exports.getPrettier = getPrettier; async function getPrettierOptions() { const prettierOptions = { parser: 'typescript', printWidth: 120, tabWidth: 2, singleQuote: true, semi: false, trailingComma: 'all', bracketSpacing: false, endOfLine: 'lf', }; // 测试时跳过本地配置的解析 if (process.env.JEST_WORKER_ID) { return prettierOptions; } const [prettierConfigPathErr, prettierConfigPath] = await (0, vtils_1.run)(() => prettier_1.default.resolveConfigFile()); if (prettierConfigPathErr || !prettierConfigPath) { return prettierOptions; } const [prettierConfigErr, prettierConfig] = await (0, vtils_1.run)(() => prettier_1.default.resolveConfig(prettierConfigPath)); if (prettierConfigErr || !prettierConfig) { return prettierOptions; } return { ...prettierOptions, ...prettierConfig, parser: 'typescript', }; } exports.getPrettierOptions = getPrettierOptions; exports.getCachedPrettierOptions = (0, vtils_1.memoize)(getPrettierOptions); async function httpGet(url, query) { const _url = new url_1.URL(url); if (query) { Object.keys(query).forEach(key => { _url.searchParams.set(key, query[key]); }); } url = _url.toString(); const res = await (0, node_fetch_1.default)(url, { method: 'GET', agent: new proxy_agent_1.default(), }); return res.json(); } exports.httpGet = httpGet;