├── .npmrc ├── logo.png ├── src ├── translation │ ├── api │ │ ├── google │ │ │ ├── README.md │ │ │ ├── sign │ │ │ │ ├── encryption.d.ts │ │ │ │ ├── index.ts │ │ │ │ └── encryption.js │ │ │ ├── state.ts │ │ │ ├── index.ts │ │ │ ├── audio.ts │ │ │ ├── detect.ts │ │ │ └── translate.ts │ │ ├── baidu │ │ │ ├── index.ts │ │ │ ├── sign │ │ │ │ ├── index.ts │ │ │ │ ├── seed.ts │ │ │ │ └── sign.ts │ │ │ ├── audio.ts │ │ │ ├── detect.ts │ │ │ ├── type.ts │ │ │ ├── README.md │ │ │ ├── state.ts │ │ │ └── translate.ts │ │ └── types.ts │ ├── types.ts │ ├── index.ts │ ├── utils │ │ ├── invert.ts │ │ ├── make-request │ │ │ ├── types.ts │ │ │ ├── browser.ts │ │ │ └── index.ts │ │ ├── error.ts │ │ └── get.ts │ └── baiduToken.ts ├── index.ts ├── global.d.ts ├── tool │ ├── sleep.ts │ ├── clearObject.ts │ ├── isReference.ts │ ├── contentHash.ts │ ├── generateTypeAlias.test.ts │ ├── getRequestBodySchema.ts │ ├── types.ts │ ├── generateRequestFunctionName.ts │ ├── tsNodeToString.ts │ ├── isRequestFiltered.ts │ ├── shouldKeepRequest.ts │ ├── transformSwaggerPathToRouterPath.ts │ ├── generateTypeAlias.ts │ ├── prettierWrite.ts │ ├── getDefinition.ts │ ├── getRefDeep.ts │ ├── enumType.test.ts │ ├── log.ts │ ├── getSchemaDeep.ts │ ├── getCompilerOptions.ts │ ├── camelCase.ts │ ├── getRequiredDeep.ts │ ├── patchGlobalDefinitionMap.ts │ ├── renameImportStatementToRequester.ts │ ├── traverseSchema.ts │ ├── translate.ts │ ├── cleanName.ts │ ├── genericType.ts │ ├── enumType.ts │ └── assembleDoc.ts ├── content │ ├── projectIndex.ts │ ├── warningComment.ts │ └── requester.ts ├── step │ ├── processEOL.ts │ ├── prepareProjectDirectory.ts │ ├── index.ts │ ├── importAllDefinition.ts │ ├── writeProject.ts │ ├── checkCache.ts │ ├── cleanRefAndDefinitionName.ts │ ├── getUserConfig │ │ ├── cliOption.ts │ │ └── index.ts │ ├── fetchOpenapiData.ts │ ├── toJS.ts │ ├── collectRefsInRequestAndPatchDefinition.ts │ ├── generateRequestContent │ │ ├── generateRequestOptionType.ts │ │ ├── assembleRequestParam.ts │ │ ├── generateResponseType.ts │ │ ├── generateMockRequestContent.ts │ │ ├── generateMockData.ts │ │ └── index.ts │ ├── prepareWriteContent.ts │ ├── assembleSchemaToGlobal.ts │ ├── generateDefinitionContent.ts │ └── translateSchema.ts ├── constant.ts ├── main.ts ├── projectGlobalVariable.ts ├── source.ts ├── requester │ ├── axios.ts │ └── fetch.ts └── run.ts ├── doc └── pet.gif ├── .yarnrc ├── commitlint.config.js ├── example ├── invokeApi │ ├── package.json │ └── index.ts └── petProject │ ├── src │ ├── service │ │ ├── v3 │ │ │ ├── index.ts │ │ │ └── definition.ts │ │ ├── doc │ │ │ ├── index.ts │ │ │ └── definition.ts │ │ ├── pet │ │ │ ├── index.ts │ │ │ └── definition.ts │ │ ├── petv3 │ │ │ ├── index.ts │ │ │ └── definition.ts │ │ ├── nullable │ │ │ └── index.ts │ │ └── projectE │ │ │ ├── index.ts │ │ │ ├── definition.ts │ │ │ └── request.ts │ ├── tsg.config.ts │ └── requester.ts │ ├── package.json │ └── yarn.lock ├── .gitignore ├── script ├── js2mjs.sh └── coverageReportToMarkdown.ts ├── __tests__ ├── .eslintrc.js ├── tool │ ├── patchGlobalDefinitionMap.test.ts │ ├── camelCase.test.ts │ ├── renameImportStatementToRequester.test.ts │ ├── transformSwaggerPathToRouterPath.test.ts │ ├── getRefDeep.test.ts │ ├── traverseSchema.test.ts │ ├── cleanName.test.ts │ ├── translate.test.ts │ └── genericType.test.ts ├── __snapshots__ │ ├── source.test.ts.snap │ ├── tsMorph.test.ts.snap │ └── runByCommand.test.ts.snap ├── step │ ├── cleanRefAndDefinitionName.test.ts │ ├── generateRequestContent │ │ ├── __snapshots__ │ │ │ ├── generateMockData.test.ts.snap │ │ │ └── generateResponseType.test.ts.snap │ │ ├── index.test.ts │ │ ├── generateMockData.test.ts │ │ ├── generateResponseType.test.ts │ │ └── generateParameterType.test.ts │ ├── getUserConfig │ │ ├── cliOption.test.ts │ │ ├── index.test.ts │ │ └── initOption.test.ts │ ├── fetchSwagger.test.ts │ └── parseGenericType.test.ts ├── run.test.ts ├── source.test.ts ├── projectGlobalVariable.test.ts ├── ts.test.ts ├── petProject.test.ts ├── tsMorph.test.ts └── requester │ └── axios.test.ts ├── prettier.config.js ├── .markdownlint.json ├── .npmignore ├── tsconfig.build.json ├── tsconfig.json ├── badge ├── badge-lines.svg ├── badge-branches.svg ├── badge-functions.svg └── badge-statements.svg ├── DEV.md ├── .eslintrc.js └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superwf/ts-gear/HEAD/logo.png -------------------------------------------------------------------------------- /src/translation/api/google/README.md: -------------------------------------------------------------------------------- 1 | # 谷歌翻译接口 2 | 3 | 来自谷歌翻译扩展 4 | -------------------------------------------------------------------------------- /doc/pet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superwf/ts-gear/HEAD/doc/pet.gif -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Project, RequestParameter, Requester } from './type' 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org/" 2 | disable-self-update-check true 3 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'url-join' 2 | declare module 'cross-fetch/polyfill' 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /src/translation/types.ts: -------------------------------------------------------------------------------- 1 | /** 键和值都是字符串的对象 */ 2 | export interface StringObject { 3 | [prop: string]: string 4 | } 5 | -------------------------------------------------------------------------------- /src/translation/api/google/sign/encryption.d.ts: -------------------------------------------------------------------------------- 1 | export const window: { TKK: string } 2 | export function sM(text: string): string 3 | -------------------------------------------------------------------------------- /src/translation/index.ts: -------------------------------------------------------------------------------- 1 | import * as baidu from './api/baidu' 2 | import * as google from './api/google' 3 | 4 | export { baidu, google } 5 | -------------------------------------------------------------------------------- /example/invokeApi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invokeApi", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /src/translation/api/google/state.ts: -------------------------------------------------------------------------------- 1 | export function getRoot(com?: boolean) { 2 | return 'https://translate.google.c' + (com ? 'om' : 'n') 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vim 3 | *.log 4 | example/fixture/ignore.json 5 | example/service/ignore 6 | tmp 7 | coverage 8 | lib 9 | .idea 10 | -------------------------------------------------------------------------------- /script/js2mjs.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | FILES=`find lib -name "*.js"` 4 | 5 | for f in $FILES 6 | do 7 | mv $f `echo $f | sed 's/\.js/\.mjs/'` 8 | done 9 | -------------------------------------------------------------------------------- /src/tool/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (time: number) => 2 | new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(null) 5 | }, time) 6 | }) 7 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['jest'], 3 | extends: ['plugin:jest/recommended'], 4 | env: { 5 | 'jest/globals': true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | useTabs: false, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | arrowParens: 'avoid', 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/tool/patchGlobalDefinitionMap.test.ts: -------------------------------------------------------------------------------- 1 | import { patchGlobalDefinitionMap } from 'src/tool/patchGlobalDefinitionMap' 2 | 3 | it('patchGlobalDefinitionMap', () => {}) 4 | -------------------------------------------------------------------------------- /src/tool/clearObject.ts: -------------------------------------------------------------------------------- 1 | export const clearObject = (o: Record) => { 2 | Reflect.ownKeys(o).forEach(k => { 3 | Reflect.deleteProperty(o, k) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /src/translation/api/baidu/index.ts: -------------------------------------------------------------------------------- 1 | import translate from './translate' 2 | // import detect from './detect' 3 | // import audio from './audio' 4 | 5 | export { translate } 6 | -------------------------------------------------------------------------------- /src/tool/isReference.ts: -------------------------------------------------------------------------------- 1 | import type { Reference } from 'swagger-schema-official' 2 | 3 | export const isReference = (value: any): value is Reference => { 4 | return '$ref' in value 5 | } 6 | -------------------------------------------------------------------------------- /src/tool/contentHash.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto' 2 | 3 | export const contentHash = (content: string) => { 4 | return createHash('md5').update(content).digest('hex').toString() 5 | } 6 | -------------------------------------------------------------------------------- /src/tool/generateTypeAlias.test.ts: -------------------------------------------------------------------------------- 1 | import { generateTypeAlias } from './generateTypeAlias' 2 | 3 | it('generateTypeAlias', () => { 4 | expect(generateTypeAlias('A', 'B')).toBe('export type A = B;') 5 | }) 6 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-hard-tabs": true, 4 | "whitespace": false, 5 | "no-inline-html": false, 6 | "line-length": false, 7 | "MD022": false, 8 | "MD013": false 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | __tests__ 3 | jest.config.js 4 | commitlint.config.js 5 | node_modules 6 | pretty.config.js 7 | script 8 | *.vim 9 | *.log 10 | tmp 11 | coverage 12 | tsconfig.build.json 13 | tsconfig.json 14 | src 15 | -------------------------------------------------------------------------------- /src/content/projectIndex.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../constant' 2 | 3 | /** use a index.ts file to export all */ 4 | export const projectIndex = () => ["export * from './request'", "export * from './definition'"].join(config.EOL) 5 | -------------------------------------------------------------------------------- /example/petProject/src/service/v3/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export * from './request' 6 | export * from './definition' 7 | -------------------------------------------------------------------------------- /example/petProject/src/service/doc/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export * from "./request"; 6 | export * from "./definition"; 7 | -------------------------------------------------------------------------------- /example/petProject/src/service/pet/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export * from "./request"; 6 | export * from "./definition"; 7 | -------------------------------------------------------------------------------- /example/petProject/src/service/petv3/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export * from "./request"; 6 | export * from "./definition"; 7 | -------------------------------------------------------------------------------- /example/petProject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "lodash": "^4.17.15", 8 | "path-to-regexp": "^3.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/petProject/src/service/nullable/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export * from "./request"; 6 | export * from "./definition"; 7 | -------------------------------------------------------------------------------- /example/petProject/src/service/projectE/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export * from "./request"; 6 | export * from "./definition"; 7 | -------------------------------------------------------------------------------- /src/content/warningComment.ts: -------------------------------------------------------------------------------- 1 | export const warningComment = (EOL: string) => 2 | [ 3 | '/* eslint-disable */', 4 | '/* tslint:disable */', 5 | '/** Do not modify manually.', 6 | 'content is generated automatically by `ts-gear`. */', 7 | ].join(EOL) 8 | -------------------------------------------------------------------------------- /src/tool/getRequestBodySchema.ts: -------------------------------------------------------------------------------- 1 | import type { OperationObject } from 'openapi3-ts' 2 | 3 | export const getRequestBodySchema = (operation?: OperationObject): { $ref: string } => { 4 | if (operation?.requestBody?.$ref) { 5 | return { $ref: operation.requestBody.$ref } 6 | } 7 | 8 | return { $ref: '' } 9 | } 10 | -------------------------------------------------------------------------------- /src/tool/types.ts: -------------------------------------------------------------------------------- 1 | /** convert array to tuple type 2 | * used as type definition, not runtime function 3 | * 4 | * @example 5 | * ```typescript 6 | * const tabs = tuple('get', 'post') 7 | * type TAB_TYPE = typeof tabs[number] 8 | * ``` 9 | * 10 | * */ 11 | export const tuple = (...args: T) => args 12 | -------------------------------------------------------------------------------- /src/translation/utils/invert.ts: -------------------------------------------------------------------------------- 1 | import { StringObject } from '../types' 2 | 3 | /** 反转对象 */ 4 | export default function(obj: StringObject) { 5 | const result: StringObject = {} 6 | for (let key in obj) { 7 | if (obj.hasOwnProperty(key)) { 8 | result[obj[key]] = key 9 | } 10 | } 11 | return result 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/source.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`source start and harvest 1`] = ` 4 | "export class MyClass { 5 | name: string; 6 | } 7 | " 8 | `; 9 | 10 | exports[`source start and harvest again 1`] = ` 11 | "export class MyClass { 12 | name: string; 13 | } 14 | " 15 | `; 16 | -------------------------------------------------------------------------------- /src/step/processEOL.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os' 2 | import type { Project } from '../type' 3 | import { config } from '../constant' 4 | 5 | /** 将换行符号写入一个全局变量中存放 */ 6 | export const processEOL = (project: Project) => { 7 | if (project.EOL === 'auto') { 8 | config.EOL = EOL as any 9 | } 10 | if (!project.EOL) { 11 | config.EOL = '\n' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/translation/api/baidu/sign/index.ts: -------------------------------------------------------------------------------- 1 | import sign from './sign' 2 | import getSeed from './seed' 3 | 4 | /** 5 | * 获取查询百度网页翻译接口所需的 token 和 sign 6 | * @param text 要查询的文本 7 | */ 8 | export default async function getSign(text: string) { 9 | const { seed, token } = await getSeed() 10 | return { 11 | token, 12 | sign: sign(text, seed), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tool/generateRequestFunctionName.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, upperFirst } from 'lodash' 2 | import type { GenerateRequestFunctionNameParameter } from '../type' 3 | 4 | /** default generate request function method */ 5 | export const generateRequestFunctionName = ({ httpMethod, pathname }: GenerateRequestFunctionNameParameter) => 6 | `${httpMethod}${upperFirst(camelCase(pathname))}` 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "noEmit": false, 6 | "rootDir": "./src", 7 | "paths": { 8 | "src/*": [ 9 | "someWrongPathToMakeSureNoSrcUsed" 10 | ] 11 | } 12 | }, 13 | "include": [ 14 | "src" 15 | ], 16 | "exclude": [ 17 | "src/**/*.test.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/tool/tsNodeToString.ts: -------------------------------------------------------------------------------- 1 | /** try some ts methods */ 2 | import * as ts from 'typescript' 3 | 4 | const printer = ts.createPrinter({ 5 | newLine: ts.NewLineKind.LineFeed, 6 | }) 7 | const file = ts.createSourceFile('someFileName.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS) 8 | 9 | export const tsNodeToString = (node: ts.Node) => printer.printNode(ts.EmitHint.Unspecified, node, file) 10 | -------------------------------------------------------------------------------- /src/step/prepareProjectDirectory.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { join } from 'path' 3 | import { sync } from 'mkdirp' 4 | import type { Project } from '../type' 5 | 6 | export const prepareProjectDirectory = (project: Project, tsGearConfigPath: string) => { 7 | const dest = join(tsGearConfigPath, project.dest, project.name) 8 | if (!existsSync(dest)) { 9 | sync(dest) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/invokeApi/index.ts: -------------------------------------------------------------------------------- 1 | import type { Project } from 'ts-gear' 2 | import { processProject } from 'ts-gear' 3 | 4 | const project: Project = { 5 | name: 'pet', 6 | dest: './service', 7 | source: '../fixture/pet.json', 8 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 9 | // source: 'http://petstore.swagger.io/v2/swagger.json', 10 | } 11 | 12 | processProject(project) 13 | -------------------------------------------------------------------------------- /src/translation/utils/make-request/types.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQueryInput, ParsedUrlQuery } from 'querystring' 2 | import { StringObject } from '../../types' 3 | 4 | export interface RequestOptions { 5 | url: string 6 | query?: ParsedUrlQuery 7 | method?: 'get' | 'post' 8 | body?: ParsedUrlQueryInput 9 | type?: 'form' | 'json' 10 | headers?: StringObject 11 | responseType?: 'document' | 'json' | 'text' 12 | } 13 | -------------------------------------------------------------------------------- /src/tool/isRequestFiltered.ts: -------------------------------------------------------------------------------- 1 | import type { ApiFilter, SwaggerRequest } from '../type' 2 | 3 | export const isRequestFiltered = (request: SwaggerRequest, apiFilter?: ApiFilter) => { 4 | if (apiFilter) { 5 | if (typeof apiFilter === 'function') { 6 | if (!apiFilter(request)) { 7 | return false 8 | } 9 | } else if (!apiFilter.test(request.pathname)) { 10 | return false 11 | } 12 | } 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /src/tool/shouldKeepRequest.ts: -------------------------------------------------------------------------------- 1 | import type { ApiFilter, SwaggerRequest } from '../type' 2 | 3 | export const shouldKeepRequest = (request: SwaggerRequest, apiFilter?: ApiFilter) => { 4 | if (apiFilter) { 5 | if (typeof apiFilter === 'function') { 6 | if (!apiFilter(request)) { 7 | return false 8 | } 9 | } else if (!apiFilter.test(request.pathname)) { 10 | return false 11 | } 12 | } 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /src/tool/transformSwaggerPathToRouterPath.ts: -------------------------------------------------------------------------------- 1 | /** transform /abc/{id} to /abc/:id */ 2 | export const transformSwaggerPathToRouterPath = (v: string) => { 3 | if (v.includes('{')) { 4 | return v 5 | .split('/') 6 | .map(s => { 7 | const reg = /[{}]/g 8 | if (reg.test(s)) { 9 | return `:${s.replace(reg, '')}` 10 | } 11 | return s 12 | }) 13 | .join('/') 14 | } 15 | return v 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/tool/camelCase.test.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'src/tool/camelCase' 2 | 3 | describe('camelCase', () => { 4 | it('normal words', () => { 5 | expect(camelCase('abc')).toBe('Abc') 6 | expect(camelCase('aBc')).toBe('ABc') 7 | expect(camelCase('aBc DEF')).toBe('ABcDEF') 8 | expect(camelCase('aBc / dEF')).toBe('ABcDEF') 9 | }) 10 | 11 | it('unnormal charator words', () => { 12 | expect(camelCase('abc/def')).toBe('AbcDef') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /__tests__/tool/renameImportStatementToRequester.test.ts: -------------------------------------------------------------------------------- 1 | import { renameImportStatementToRequester } from '../../src/tool/renameImportStatementToRequester' 2 | 3 | it('rename import declaration name', () => { 4 | expect(renameImportStatementToRequester('import project from "../file"')).toBe('import requester from "../file"') 5 | expect(renameImportStatementToRequester('import { project } from "../file"')).toBe( 6 | 'import { project as requester } from "../file"', 7 | ) 8 | }) 9 | -------------------------------------------------------------------------------- /src/tool/generateTypeAlias.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { tsNodeToString } from './tsNodeToString' 3 | 4 | const { factory } = ts 5 | 6 | export const generateTypeAlias = (left: string, right: string) => { 7 | const node = factory.createTypeAliasDeclaration( 8 | [factory.createModifier(ts.SyntaxKind.ExportKeyword)], 9 | left, 10 | undefined, 11 | factory.createTypeReferenceNode(right, []), 12 | ) 13 | return tsNodeToString(node) 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/step/cleanRefAndDefinitionName.test.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import { cloneDeep } from 'lodash' 3 | import { cleanRefAndDefinitionName } from 'src/step/cleanRefAndDefinitionName' 4 | import * as petSchema from 'example/fixture/pet.json' 5 | 6 | describe('run step', () => { 7 | it('definition typescriptContent', () => { 8 | cleanRefAndDefinitionName(cloneDeep(petSchema) as Spec, false) 9 | expect(petSchema).toMatchSnapshot() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /__tests__/tool/transformSwaggerPathToRouterPath.test.ts: -------------------------------------------------------------------------------- 1 | import { transformSwaggerPathToRouterPath } from 'src/tool/transformSwaggerPathToRouterPath' 2 | 3 | describe('transformSwaggerPathToRouterPath', () => { 4 | it('path', () => { 5 | expect(transformSwaggerPathToRouterPath('/abc/{id}')).toBe('/abc/:id') 6 | expect(transformSwaggerPathToRouterPath('/{name}/{id}')).toBe('/:name/:id') 7 | 8 | expect(transformSwaggerPathToRouterPath('/def/{id}/edit')).toBe('/def/:id/edit') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/tool/prettierWrite.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs' 2 | import type { Options } from 'prettier' 3 | import { format } from 'prettier' 4 | 5 | /** 6 | * read from tsg config project prettier config 7 | * write formatted typescript data 8 | * */ 9 | export const prettierWrite = ({ file, data, option }: { file: string; data: string; option?: Options }) => { 10 | writeFileSync( 11 | file, 12 | format(data, { 13 | ...option, 14 | parser: 'typescript', 15 | }), 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/tool/getDefinition.ts: -------------------------------------------------------------------------------- 1 | import type { Spec, Schema } from 'swagger-schema-official' 2 | import type { OpenAPIObject } from 'openapi3-ts' 3 | 4 | type ReturnSchema = { [definitionsName: string]: Schema } 5 | 6 | /** 7 | * return definitions 8 | * */ 9 | export const getDefinition = (spec: Spec | OpenAPIObject): ReturnSchema => { 10 | if ('components' in spec) { 11 | const v3spec = spec as OpenAPIObject 12 | return v3spec.components?.schemas as ReturnSchema 13 | } 14 | return spec.definitions! 15 | } 16 | -------------------------------------------------------------------------------- /src/translation/utils/error.ts: -------------------------------------------------------------------------------- 1 | export const enum ERROR_CODE { 2 | NETWORK_TIMEOUT = 'NETWORK_TIMEOUT', // 查询接口时超时了 3 | NETWORK_ERROR = 'NETWORK_ERROR', // 查询时网络出问题了 4 | API_SERVER_ERROR = 'API_SERVER_ERROR', // 接口服务出问题了 5 | UNSUPPORTED_LANG = 'UNSUPPORTED_LANG', // 不支持的语种 6 | } 7 | 8 | export interface TranslateError extends Error { 9 | code: ERROR_CODE 10 | } 11 | 12 | export default function error(code: ERROR_CODE, msg?: string) { 13 | const e = new Error(msg) as TranslateError 14 | e.code = code 15 | return e 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/step/generateRequestContent/__snapshots__/generateMockData.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`sample make sample data 1`] = ` 4 | [ 5 | { 6 | "category": { 7 | "id": 0, 8 | "name": "string", 9 | "pet": "", 10 | }, 11 | "id": 0, 12 | "name": "doggie", 13 | "photoUrls": [ 14 | "string", 15 | ], 16 | "status": "available", 17 | "tags": [ 18 | { 19 | "id": 0, 20 | "name": "string", 21 | }, 22 | ], 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | import type { Project } from './type' 2 | 3 | export const configFileName = 'tsg.config' 4 | 5 | export const defaultUseMockResponseStatement = 'process.env.NODE_ENV === "test"' 6 | 7 | export const targetFileNames = { 8 | index: 'index.ts', 9 | definition: 'definition.ts', 10 | request: 'request.ts', 11 | mockRequest: 'mockRequest.ts', 12 | } 13 | 14 | export const config: { 15 | EOL: Required 16 | } = { 17 | EOL: '\n', 18 | } 19 | 20 | export const MIME_JSON = 'application/json' 21 | export const MIME_TEXT = 'text/plain' 22 | -------------------------------------------------------------------------------- /src/tool/getRefDeep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 深度获取内部的$ref对象 3 | * 用于兼容openapiv3,从requestBody中获取schema 4 | * */ 5 | export const getRefDeep = (obj?: Record): any => { 6 | if (!obj) { 7 | return {} 8 | } 9 | if (obj.$ref) { 10 | return obj 11 | } 12 | const keys = Object.getOwnPropertyNames(obj) 13 | if (keys.length > 0) { 14 | /* eslint-disable no-restricted-syntax */ 15 | for (const key of keys) { 16 | if (typeof obj[key] === 'object') { 17 | return getRefDeep(obj[key]) 18 | } 19 | } 20 | } 21 | return {} 22 | } 23 | -------------------------------------------------------------------------------- /src/tool/enumType.test.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import * as petSpec from 'example/fixture/pet.json' 3 | import { generateEnumName, generateEnumTypescriptContent } from 'src/tool/enumType' 4 | 5 | it('name', () => { 6 | expect( 7 | generateEnumName(['paths', '/pet/findByStatus', 'get', 'parameters', '0', 'items', 'enum'], petSpec as Spec), 8 | ).toBe('GetPetFindByStatusItems') 9 | }) 10 | 11 | it('content', () => { 12 | expect(generateEnumTypescriptContent('EnumA', [1, 2, 3, 'n'])).toBe('export type EnumA = 1 | 2 | 3 | "n";') 13 | }) 14 | -------------------------------------------------------------------------------- /src/tool/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import chalk from 'chalk' 3 | 4 | const { cyanBright, red, yellow } = chalk.bold 5 | 6 | /** log blue message in console */ 7 | export function info(...messages: string[]) { 8 | console.log(cyanBright(messages.join(''))) 9 | } 10 | 11 | /** log red message in console */ 12 | export function error(...messages: string[]) { 13 | console.log(red(messages.join(''))) 14 | } 15 | 16 | /** log yellow message in console */ 17 | export function warn(...messages: string[]) { 18 | console.log(yellow(messages.join(''))) 19 | } 20 | -------------------------------------------------------------------------------- /src/translation/utils/get.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 安全的获取一个变量上指定路径的值。 3 | * TODO: 使用 noshjs 代替 4 | */ 5 | export default function getValue(obj: any, pathArray: string | string[], defaultValue?: any) { 6 | if (obj == null) return defaultValue 7 | 8 | if (typeof pathArray === 'string') { 9 | pathArray = [pathArray] 10 | } 11 | 12 | let value = obj 13 | 14 | for (let i = 0; i < pathArray.length; i += 1) { 15 | const key = pathArray[i] 16 | value = value[key] 17 | if (value == null) { 18 | return defaultValue 19 | } 20 | } 21 | 22 | return value 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/tool/getRefDeep.test.ts: -------------------------------------------------------------------------------- 1 | import { getRefDeep } from '../../src/tool/getRefDeep' 2 | 3 | describe('getRefDeep', () => { 4 | it('获取$ref', () => { 5 | expect( 6 | getRefDeep({ 7 | description: 'OK', 8 | content: { 9 | '*/*': { 10 | schema: { 11 | $ref: 'ReplyVO', 12 | }, 13 | }, 14 | }, 15 | }), 16 | ).toEqual({ 17 | $ref: 'ReplyVO', 18 | }) 19 | 20 | expect( 21 | getRefDeep({ 22 | description: 'OK', 23 | }), 24 | ).toEqual({}) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/tool/getSchemaDeep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 深度获取内部的schema对象 3 | * 用于兼容openapiv3,从requestBody中获取schema 4 | * */ 5 | export const getSchemaDeep = (obj?: Record): any => { 6 | if (!obj) { 7 | return {} 8 | } 9 | if (obj.schema) { 10 | return obj.schema 11 | } 12 | const keys = Object.getOwnPropertyNames(obj) 13 | if (keys.length > 0) { 14 | /* eslint-disable no-restricted-syntax */ 15 | for (const key of keys) { 16 | if (typeof obj[key] === 'object') { 17 | return getSchemaDeep(obj[key]) 18 | } 19 | } 20 | } 21 | return {} 22 | } 23 | -------------------------------------------------------------------------------- /src/step/index.ts: -------------------------------------------------------------------------------- 1 | export * from './processEOL' 2 | export * from './getUserConfig' 3 | export * from './fetchOpenapiData' 4 | export * from './checkCache' 5 | export * from './prepareProjectDirectory' 6 | export * from './translateSchema' 7 | export * from './assembleSchemaToGlobal' 8 | export * from './cleanRefAndDefinitionName' 9 | export * from './parseGenericType' 10 | export * from './collectRefsInRequestAndPatchDefinition' 11 | export * from './generateDefinitionContent' 12 | export * from './generateRequestContent' 13 | export * from './prepareWriteContent' 14 | export * from './writeProject' 15 | export * from './toJS' 16 | -------------------------------------------------------------------------------- /src/tool/getCompilerOptions.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { existsSync, readFileSync } from 'fs' 3 | import { parse } from 'json5' 4 | import appRoot = require('app-root-path') 5 | import type * as ts from 'typescript' 6 | 7 | /** 8 | * get tsconfig compilerOptions from cwd project 9 | * if not exist, return empty object 10 | * */ 11 | export const getCompilerOptions = () => { 12 | const cwdTsconfigPath = join(appRoot.path, 'tsconfig.json') 13 | const tsConfig = existsSync(cwdTsconfigPath) ? parse(readFileSync(cwdTsconfigPath).toString()) : {} 14 | return (tsConfig.compilerOptions || {}) as ts.CompilerOptions 15 | } 16 | -------------------------------------------------------------------------------- /src/translation/api/google/index.ts: -------------------------------------------------------------------------------- 1 | // import detect from './detect' 2 | // import audio from './audio' 3 | // import translate from './translate' 4 | 5 | // export { detect, audio, translate } 6 | import * as vitaletsTranslate from '@vitalets/google-translate-api' 7 | import type { StringOrTranslateOptions } from '../types' 8 | 9 | export const translate = async (option: StringOrTranslateOptions) => { 10 | if (typeof option === 'string') { 11 | return vitaletsTranslate(option, { to: 'en' }).then(res => ({ result: [res.text] })) 12 | } 13 | return vitaletsTranslate(option.text, { to: 'en' }).then(res => ({ result: [res.text] })) 14 | } 15 | -------------------------------------------------------------------------------- /src/step/importAllDefinition.ts: -------------------------------------------------------------------------------- 1 | import { getGlobal } from '../projectGlobalVariable' 2 | import type { Project } from '../type' 3 | 4 | export const importAllDefinition = (project: Project) => { 5 | const { requestRefSet, requestEnumSet } = getGlobal(project) 6 | const refSet = new Set() 7 | ;[...Array.from(requestEnumSet), ...Array.from(requestRefSet)].forEach(name => { 8 | name 9 | .split(/<|>|,/) 10 | .filter(Boolean) 11 | .forEach(n => { 12 | refSet.add(n) 13 | }) 14 | }) 15 | const importNames = Array.from(refSet) 16 | return `import type { ${importNames.join(',')} } from './definition'` 17 | } 18 | -------------------------------------------------------------------------------- /src/tool/camelCase.ts: -------------------------------------------------------------------------------- 1 | import { upperFirst } from 'lodash' 2 | /** 3 | * lodash camelCase will convert non word first charator lower case 4 | * e.g. lodash camelCase will convert "PageVOListVO" to "PageVoListVo" 5 | * use this camelCase to convert only every words first charator upper case. 6 | * e.g. "PageVOListVO" to "PageVOListVO" 7 | * */ 8 | export const camelCase = (name: string) => { 9 | const invalidVariableCharatorReg = /[^a-z0-9]/i 10 | if (invalidVariableCharatorReg.test(name)) { 11 | return name 12 | .split(/[^a-z0-9]/i) 13 | .map(n => upperFirst(n)) 14 | .join('') 15 | } 16 | return upperFirst(name) 17 | } 18 | -------------------------------------------------------------------------------- /src/translation/api/google/audio.ts: -------------------------------------------------------------------------------- 1 | import { StringOrTranslateOptions } from '../types' 2 | import sign from './sign' 3 | import { getRoot } from './state' 4 | import detect from './detect' 5 | 6 | export default async function(options: StringOrTranslateOptions) { 7 | let { text, from = '', com = false } = 8 | typeof options === 'string' ? { text: options } : options 9 | 10 | if (!from) { 11 | from = await detect(text) 12 | } 13 | 14 | return `${getRoot(com)}/translate_tts?ie=UTF-8&q=${encodeURIComponent( 15 | text 16 | )}&tl=${from}&total=1&idx=0&textlen=${text.length}&tk=${await sign( 17 | text, 18 | com 19 | )}&client=webapp&prev=input` 20 | } 21 | -------------------------------------------------------------------------------- /example/petProject/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | lodash@^4.17.15: 6 | version "4.17.21" 7 | resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 8 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 9 | 10 | path-to-regexp@^3.0.0: 11 | version "3.2.0" 12 | resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" 13 | integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== 14 | -------------------------------------------------------------------------------- /src/tool/getRequiredDeep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 兼容openapiv3 3 | * 深度获取requestBody对象内是否有required选项 4 | * 生成请求参数的required属性用 5 | * */ 6 | export const getRequiredDeep = (obj?: Record): boolean => { 7 | if (!obj) { 8 | return false 9 | } 10 | if (typeof obj === 'object') { 11 | const keys = Object.getOwnPropertyNames(obj) 12 | if (keys.length > 0) { 13 | /* eslint-disable no-restricted-syntax */ 14 | for (const key of keys) { 15 | if (key === 'required' && obj[key] === true) { 16 | return true 17 | } 18 | if (typeof obj[key] === 'object') { 19 | return getRequiredDeep(obj[key]) 20 | } 21 | } 22 | } 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { register } from 'ts-node' 4 | import * as tsConfigPaths from 'tsconfig-paths' 5 | import { getCompilerOptions } from './tool/getCompilerOptions' 6 | import { runByCommand } from './run' 7 | 8 | const cwd = process.cwd() 9 | 10 | const compilerOptions = getCompilerOptions() 11 | 12 | if (compilerOptions.paths) { 13 | tsConfigPaths.register({ 14 | baseUrl: cwd, 15 | paths: compilerOptions.paths, 16 | }) 17 | } 18 | 19 | register({ 20 | typeCheck: true, 21 | compilerOptions: { 22 | ...compilerOptions, 23 | /** 必须使用commonjs,否则在nodejs环境跑不起来 */ 24 | module: 'commonjs', 25 | /** 5.0 cjs有这个参数就会编译失败 */ 26 | verbatimModuleSyntax: false, 27 | }, 28 | }) 29 | 30 | runByCommand() 31 | -------------------------------------------------------------------------------- /src/step/writeProject.ts: -------------------------------------------------------------------------------- 1 | import { prettierWrite } from '../tool/prettierWrite' 2 | import type { Project, PrepareToWrite } from '../type' 3 | 4 | /** 5 | * write to project dir 6 | */ 7 | export const writeProject = (project: Project, option: PrepareToWrite) => { 8 | prettierWrite({ data: option.definitionFileContent, file: option.definitionFile, option: project.prettierConfig }) 9 | 10 | prettierWrite({ data: option.requestFileContent, file: option.requestFile, option: project.prettierConfig }) 11 | if (project.shouldGenerateMock) { 12 | prettierWrite({ data: option.mockRequestFileContent, file: option.mockRequestFile, option: project.prettierConfig }) 13 | } 14 | 15 | prettierWrite({ data: option.indexFileContent, file: option.indexFile, option: project.prettierConfig }) 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/step/getUserConfig/cliOption.test.ts: -------------------------------------------------------------------------------- 1 | import { getCliOption } from 'src/step/getUserConfig/cliOption' 2 | 3 | describe('cli option', () => { 4 | const originLength = process.argv.length 5 | afterEach(() => { 6 | process.argv.length = originLength 7 | }) 8 | 9 | it('test -p option', () => { 10 | expect(getCliOption().names).toEqual([]) 11 | process.argv.push('-p', 'pet') 12 | expect(getCliOption().names).toEqual(['pet']) 13 | }) 14 | 15 | it('test -i', () => { 16 | process.argv.push('-i') 17 | expect(getCliOption().init).toBe(true) 18 | expect(getCliOption().names).toEqual([]) 19 | }) 20 | 21 | it('test --config src/tsg.ts', () => { 22 | process.argv.push('-c src/tsg.ts') 23 | expect(getCliOption().config).toBe('src/tsg.ts') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /__tests__/run.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { run } from 'src/run' 3 | 4 | describe('run', () => { 5 | it('with fixture', async () => { 6 | const cwd = process.cwd() 7 | await run({ 8 | projects: [ 9 | { 10 | name: 'projectE', 11 | dest: 'service', 12 | source: '../../../example/fixture/projectE.json', 13 | // source: '../../../example/fixture/pet.json', 14 | importRequesterStatement: 'import { requester } from "../../requester"', 15 | EOL: '\n', 16 | withBasePath: true, 17 | keepGeneric: true, 18 | stripBodyPropWhenOnlyOneBodyProp: true, 19 | }, 20 | ], 21 | appPath: join(cwd, 'example/petProject/src'), 22 | }) 23 | expect(1).toBe(1) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/step/checkCache.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import { readFileSync, existsSync, writeFileSync } from 'fs' 3 | import { join } from 'path' 4 | import appRoot = require('app-root-path') 5 | import { contentHash } from '../tool/contentHash' 6 | import type { Project } from '../type' 7 | 8 | export const checkCache = (project: Project, spec: any): boolean => { 9 | const cacheFile = join(appRoot.path, 'node_modules', '.cache') 10 | const hash = contentHash(JSON.stringify([project, spec])) 11 | if (!existsSync(cacheFile)) { 12 | writeFileSync(cacheFile, hash) 13 | return false 14 | } 15 | const cacheContent = readFileSync(cacheFile, { encoding: 'utf8' }) 16 | if (cacheContent === hash) { 17 | return true 18 | } 19 | writeFileSync(cacheFile, hash) 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /src/translation/api/google/sign/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 代码来自 https://github.com/matheuss/google-translate-token 3 | * 做了一些修改以适应本项目 4 | */ 5 | 6 | import request from '../../../utils/make-request' 7 | import { getRoot } from '../state' 8 | import { sM, window } from './encryption' 9 | 10 | async function updateTKK(com: boolean) { 11 | const now = Math.floor(Date.now() / 3600000) 12 | 13 | if (Number(window.TKK.split('.')[0]) === now) { 14 | return 15 | } 16 | 17 | const html = await request({ 18 | url: getRoot(com), 19 | responseType: 'text' 20 | }) 21 | const code = html.match(/tkk:'(\d+\.\d+)'/) 22 | if (code) { 23 | window.TKK = code[1] 24 | } 25 | } 26 | 27 | export default async function(text: string, com: boolean) { 28 | await updateTKK(com) 29 | return sM(text) 30 | } 31 | -------------------------------------------------------------------------------- /src/projectGlobalVariable.ts: -------------------------------------------------------------------------------- 1 | import type { ProjectGlobalMap, Project } from './type' 2 | import { clearObject } from './tool/clearObject' 3 | 4 | const projectGlobal: ProjectGlobalMap = {} 5 | 6 | // let currentProject: Project | undefined 7 | 8 | export const getGlobal = (project: Project) => { 9 | if (!projectGlobal[project.name]) { 10 | projectGlobal[project.name] = { 11 | definitionMap: {}, 12 | requestMap: {}, 13 | requestRefSet: new Set(), 14 | requestEnumSet: new Set(), 15 | enumMap: {}, 16 | } 17 | } 18 | return projectGlobal[project.name] 19 | } 20 | 21 | export const restore = (project: Project) => { 22 | const g = projectGlobal[project.name] 23 | clearObject(g.definitionMap) 24 | clearObject(g.requestMap) 25 | clearObject(g.enumMap) 26 | g.requestRefSet.clear() 27 | g.requestEnumSet.clear() 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "esnext"], 5 | "experimentalDecorators": true, 6 | "skipLibCheck": true, 7 | "allowSyntheticDefaultImports": false, 8 | "allowJs": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "noEmit": true, 15 | "removeComments": false, 16 | "sourceMap": false, 17 | "declaration": true, 18 | "declarationDir": "lib", 19 | "outDir": "lib", 20 | "rootDir": ".", 21 | "baseUrl": ".", 22 | "strictPropertyInitialization": false, 23 | "paths": { 24 | "src/*": ["./src/*"], 25 | "example/*": ["./example/*"] 26 | } 27 | }, 28 | "include": ["src", "__tests__", "jest.config.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/source.test.ts: -------------------------------------------------------------------------------- 1 | import { sow, harvest } from 'src/source' 2 | 3 | describe('source', () => { 4 | it('start and harvest', () => { 5 | const s = sow() 6 | s.addClass({ 7 | name: 'MyClass', 8 | isExported: true, 9 | properties: [ 10 | { 11 | name: 'name', 12 | type: 'string', 13 | }, 14 | ], 15 | }) 16 | const content = harvest(s) 17 | expect(content).toMatchSnapshot() 18 | }) 19 | 20 | it('start and harvest again', () => { 21 | const s = sow() 22 | const s1 = sow() 23 | s.addClass({ 24 | name: 'MyClass', 25 | isExported: true, 26 | properties: [ 27 | { 28 | name: 'name', 29 | type: 'string', 30 | }, 31 | ], 32 | }) 33 | harvest(s1) 34 | const content = harvest(s) 35 | expect(content).toMatchSnapshot() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /example/petProject/src/service/v3/definition.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | 6 | /** 7 | * @description 8 | * 新增【表字段信息】的参数 9 | */ 10 | export interface FieldDefAddDTO { 11 | /** 12 | * @description 13 | * 字段描述 14 | */ 15 | description?: string 16 | /** 17 | * @description 18 | * 字段Id 19 | */ 20 | fieldId: string 21 | /** 22 | * @description 23 | * 字段名称 24 | * @example 25 | * name1 26 | */ 27 | fieldName: string 28 | /** 29 | * @description 30 | * 关联 31 | */ 32 | tableId: string 33 | /** 34 | * @description 35 | * 字段类型:boolean,long,double,string,date 36 | */ 37 | type: string 38 | } 39 | 40 | export type ReplyVOPageVOFieldDefListVO = any 41 | export type ReplyVOFieldDefShowVO = any 42 | export type ReplyVO = any 43 | -------------------------------------------------------------------------------- /src/translation/api/google/detect.ts: -------------------------------------------------------------------------------- 1 | import type { StringOrTranslateOptions } from '../types' 2 | import request from '../../utils/make-request' 3 | import getError, { ERROR_CODE } from '../../utils/error' 4 | import { getRoot } from './state' 5 | import sign from './sign' 6 | 7 | export default async function (options: StringOrTranslateOptions) { 8 | const { text, com = false } = typeof options === 'string' ? { text: options } : options 9 | 10 | const result = await request({ 11 | url: `${getRoot(com)}/translate_a/single`, 12 | query: { 13 | client: 'webapp', 14 | sl: 'auto', 15 | tl: 'zh-CN', 16 | hl: 'zh-CN', 17 | ssel: '3', 18 | tsel: '0', 19 | kc: '0', 20 | tk: await sign(text, com), 21 | q: text, 22 | }, 23 | }) 24 | 25 | const src = result && result[2] 26 | if (src) return src 27 | throw getError(ERROR_CODE.UNSUPPORTED_LANG) 28 | } 29 | -------------------------------------------------------------------------------- /src/content/requester.ts: -------------------------------------------------------------------------------- 1 | import type { Project } from '../type' 2 | import { renameImportStatementToRequester } from '../tool/renameImportStatementToRequester' 3 | 4 | /** get tsg.config.ts file relative path to import in request 5 | * */ 6 | export const requester = (project: Project) => { 7 | if (project.importRequesterStatement) { 8 | const importStatement = renameImportStatementToRequester(project.importRequesterStatement) 9 | if (!importStatement) { 10 | throw new Error( 11 | `project: ${project.name} importRequesterStatement parse error, your statement is ${project.importRequesterStatement}, try to update to a "default import" or a "named import" statement with correct syntax`, 12 | ) 13 | } 14 | return { 15 | import: importStatement, 16 | code: '', 17 | } 18 | } 19 | 20 | throw new Error(`project: ${project.name} missing "importRequesterStatement" config`) 21 | } 22 | -------------------------------------------------------------------------------- /script/coverageReportToMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os' 2 | import { writeFileSync } from 'fs' 3 | import { createInterface } from 'readline' 4 | 5 | const rl = createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }) 9 | 10 | const result: string[] = [] 11 | let recording = false 12 | rl.on('line', line => { 13 | if ( 14 | line.startsWith('Statements') || 15 | line.startsWith('Branches') || 16 | line.startsWith('Functions') || 17 | line.startsWith('Lines') 18 | ) { 19 | result.push(`##${line}${EOL}${EOL}`) 20 | } 21 | if (line.startsWith('File')) { 22 | recording = true 23 | } 24 | if (line.startsWith('Done')) { 25 | recording = false 26 | } 27 | if (recording) { 28 | result.push(line + EOL) 29 | } 30 | }) 31 | rl.on('close', () => { 32 | result.pop() 33 | writeFileSync('./coverage.md', result.join(''), { 34 | encoding: 'utf8', 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/tsMorph.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`compile class 1`] = ` 4 | "class MyClass { 5 | public a: { 6 | name: string 7 | age: number 8 | }; 9 | } 10 | " 11 | `; 12 | 13 | exports[`compile function 1`] = ` 14 | "export function myFunc(param: number) { 15 | 16 | const [ url, option ] = interceptRequest(param) 17 | option.method = 'get' 18 | return fetch(url, option).then(interceptResponse) 19 | 20 | } 21 | " 22 | `; 23 | 24 | exports[`compile interface 1`] = ` 25 | "interface ITest { 26 | a: { 27 | name: string 28 | age: number 29 | }; 30 | } 31 | " 32 | `; 33 | 34 | exports[`compile structure 1`] = ` 35 | " 36 | interface A { 37 | b: { 38 | name: string 39 | } 40 | c; 41 | } 42 | " 43 | `; 44 | -------------------------------------------------------------------------------- /src/tool/patchGlobalDefinitionMap.ts: -------------------------------------------------------------------------------- 1 | import type { DefinitionMap } from '../type' 2 | 3 | /** 4 | * add a new type definition to definitionMap 5 | * */ 6 | export const patchGlobalDefinitionMap = ({ 7 | typeName, 8 | definitionMap, 9 | alias = 'any', 10 | originalName, 11 | }: { 12 | typeName: string 13 | definitionMap: DefinitionMap 14 | alias?: string 15 | originalName?: string 16 | }) => { 17 | if (!(typeName in definitionMap)) { 18 | definitionMap[typeName] = { 19 | originalName, 20 | typeName, 21 | typescriptContent: `export type ${typeName} = ${alias}`, 22 | } 23 | const originalDefinitionName = Object.getOwnPropertyNames(definitionMap).find( 24 | name => definitionMap[name].typeName === typeName, 25 | ) 26 | if (originalDefinitionName && definitionMap[originalDefinitionName]) { 27 | definitionMap[typeName].schema = definitionMap[originalDefinitionName].schema 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/tool/renameImportStatementToRequester.ts: -------------------------------------------------------------------------------- 1 | import { sow, harvest } from '../source' 2 | 3 | /** 4 | * read a import statement string 5 | * rename its default import to "requester" 6 | * or rename its first named import to alias "requester" 7 | * */ 8 | export const renameImportStatementToRequester = (importStatement: string) => { 9 | const sourceFile = sow(importStatement) 10 | const imports = sourceFile.getImportDeclarations() 11 | if (imports.length > 0) { 12 | const firstImport = imports[0] 13 | const defaultImport = firstImport.getDefaultImport() 14 | if (defaultImport) { 15 | defaultImport.rename('requester') 16 | return harvest(sourceFile) 17 | } 18 | 19 | const namedImports = firstImport.getNamedImports() 20 | if (namedImports) { 21 | namedImports[0].renameAlias('requester') 22 | // console.log(imp.getDefaultImport()) 23 | return harvest(sourceFile) 24 | } 25 | } 26 | return '' 27 | } 28 | -------------------------------------------------------------------------------- /src/translation/api/types.ts: -------------------------------------------------------------------------------- 1 | /** 单个音标的数据结构 */ 2 | export interface Phonetic { 3 | name: string // 语种的中文名称 4 | ttsURI: string // 此音标对应的语音地址 5 | value: string // 此语种对应的音标值 6 | } 7 | 8 | /** 统一的查询结果的数据结构 */ 9 | export interface TranslateResult { 10 | text: string // 此次查询的文本 11 | raw: any // 翻译接口提供的原始的、未经转换的查询结果 12 | link: string // 此翻译接口的在线查询地址 13 | from: string // 由翻译接口提供的源语种,可能会与查询对象的 from 不同 14 | to: string // 由翻译接口提供的目标语种,注意可能会与查询对象的 to 不同 15 | phonetic?: string | Phonetic[] // 若有多个音标(例如美音和英音),则使用数组描述 16 | dict?: string[] // 如果查询的是英文单词,有的翻译接口(例如有道翻译)会返回这个单词的详细释义 17 | result?: string[] // 翻译结果,可以有多条(一个段落对应一个翻译结果) 18 | } 19 | 20 | /** 统一的查询参数结构 */ 21 | export interface TranslateOptions { 22 | text: string 23 | // 待翻译文本的源语种 24 | from?: string 25 | // 想将文本翻译成哪个语种 26 | to?: string 27 | // 是否使用国际版谷歌翻译。仅对谷歌翻译生效。 28 | com?: boolean 29 | } 30 | 31 | /** 查询参数,既可以是字符串,也可以是对象 */ 32 | export type StringOrTranslateOptions = string | TranslateOptions 33 | -------------------------------------------------------------------------------- /badge/badge-lines.svg: -------------------------------------------------------------------------------- 1 | Coverage:linesCoverage:lines83.31%83.31% -------------------------------------------------------------------------------- /src/step/cleanRefAndDefinitionName.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import { traverseSchema } from '../tool/traverseSchema' 3 | import { cleanName } from '../tool/cleanName' 4 | import { getDefinition } from '../tool/getDefinition' 5 | 6 | /** use cleanName for all "$ref" and "definitions" names 7 | * mutate the spec data 8 | * */ 9 | export const cleanRefAndDefinitionName = (spec: Spec, keepGeneric: boolean) => { 10 | const definitions = getDefinition(spec) 11 | Object.getOwnPropertyNames(definitions).forEach(name => { 12 | const cleanedName = cleanName(name, keepGeneric) 13 | if (cleanedName !== name) { 14 | const origin = definitions[name] 15 | delete definitions[name] 16 | definitions[cleanedName] = origin 17 | } 18 | }) 19 | traverseSchema(spec, ({ value, parent, key }) => { 20 | if (key === '$ref' && typeof value === 'string') { 21 | parent.$ref = cleanName(value, keepGeneric) 22 | } 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /badge/badge-branches.svg: -------------------------------------------------------------------------------- 1 | Coverage:branchesCoverage:branches73.72%73.72% -------------------------------------------------------------------------------- /badge/badge-functions.svg: -------------------------------------------------------------------------------- 1 | Coverage:functionsCoverage:functions75.93%75.93% -------------------------------------------------------------------------------- /badge/badge-statements.svg: -------------------------------------------------------------------------------- 1 | Coverage:statementsCoverage:statements83.37%83.37% -------------------------------------------------------------------------------- /__tests__/tool/traverseSchema.test.ts: -------------------------------------------------------------------------------- 1 | import * as petSchema from 'example/fixture/pet.json' 2 | import { traverseSchema } from 'src/tool/traverseSchema' 3 | 4 | it('traverseSchema', () => { 5 | const fn = jest.fn() 6 | const obj1 = { a: 1 } 7 | traverseSchema(obj1, fn) 8 | expect(fn).toHaveBeenCalledTimes(1) 9 | expect(fn).toHaveBeenLastCalledWith({ 10 | value: 1, 11 | parent: obj1, 12 | key: 'a', 13 | path: ['a'], 14 | }) 15 | 16 | fn.mockReset() 17 | const obj2 = { a: { b: 2 } } 18 | traverseSchema(obj2, fn) 19 | expect(fn).toHaveBeenCalledTimes(2) 20 | expect(fn).toHaveBeenLastCalledWith({ 21 | value: 2, 22 | parent: obj2.a, 23 | key: 'b', 24 | path: ['a', 'b'], 25 | }) 26 | }) 27 | 28 | it('update node value in traverseSchema', () => { 29 | // const fn = jest.fn() 30 | traverseSchema(petSchema, node => { 31 | if (node.key === 'definitions') { 32 | node.value.Order = 111 33 | } 34 | }) 35 | expect(petSchema.definitions.Order).toBe(111) 36 | }) 37 | -------------------------------------------------------------------------------- /src/translation/api/baidu/audio.ts: -------------------------------------------------------------------------------- 1 | import { StringOrTranslateOptions } from '../types' 2 | import detect from './detect' 3 | import { root, standard2custom } from './state' 4 | import getError, { ERROR_CODE } from '../../utils/error' 5 | 6 | /** 7 | * 生成百度语音地址 8 | * @param text 要朗读的文本 9 | * @param lang 文本的语种,使用百度自定义的语种名称 10 | */ 11 | export function getAudioURI(text: string, lang: string) { 12 | return ( 13 | root + 14 | `/gettts?lan=${lang}&text=${encodeURIComponent(text)}&spd=3&source=web` 15 | ) 16 | } 17 | 18 | /** 19 | * 获取指定文本的网络语音地址 20 | */ 21 | export default async function(options: StringOrTranslateOptions) { 22 | let { text, from = undefined } = 23 | typeof options === 'string' ? { text: options } : options 24 | 25 | if (!from) { 26 | from = await detect(text) 27 | } 28 | 29 | let lang 30 | if (from === 'en-GB') { 31 | lang = 'uk' 32 | } else { 33 | lang = standard2custom[from] 34 | if (!lang) throw getError(ERROR_CODE.UNSUPPORTED_LANG) 35 | } 36 | return getAudioURI(text, lang) 37 | } 38 | -------------------------------------------------------------------------------- /__tests__/projectGlobalVariable.test.ts: -------------------------------------------------------------------------------- 1 | import { restore, getGlobal } from 'src/projectGlobalVariable' 2 | import type { Project } from 'src/type' 3 | 4 | describe('src/global', () => { 5 | const project: Project = { 6 | name: 'pet', 7 | dest: './service', 8 | source: 'fixture/pet.json', 9 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 10 | } 11 | it('restore', () => { 12 | const { definitionMap, requestMap, requestRefSet } = getGlobal(project) 13 | expect(definitionMap).toEqual({}) 14 | expect(requestMap).toEqual({}) 15 | expect(requestRefSet.size).toBe(0) 16 | 17 | definitionMap.aaa = { 18 | typeName: 'AAA', 19 | } 20 | requestMap.aaa = { 21 | pathname: 'aaa', 22 | httpMethod: 'get', 23 | responses: [] as any, 24 | schema: {} as any, 25 | } 26 | requestRefSet.add('a') 27 | restore(project) 28 | expect(requestRefSet.size).toBe(0) 29 | expect(definitionMap).toEqual({}) 30 | expect(requestMap).toEqual({}) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/translation/api/baidu/detect.ts: -------------------------------------------------------------------------------- 1 | import type { StringOrTranslateOptions } from '../types' 2 | import request from '../../utils/make-request' 3 | import getError, { ERROR_CODE } from '../../utils/error' 4 | import { root, custom2standard, fetchCookie, Cookie } from './state' 5 | 6 | interface DetectResult { 7 | error: number 8 | message: string 9 | lan: string 10 | } 11 | 12 | export default async function (options: StringOrTranslateOptions) { 13 | if (!Cookie.value) { 14 | await fetchCookie() 15 | } 16 | const query = (typeof options === 'string' ? options : options.text).slice(0, 73) 17 | const body: DetectResult = await request({ 18 | method: 'post', 19 | url: `${root}/langdetect`, 20 | body: { 21 | query, 22 | }, 23 | type: 'form', 24 | headers: { 25 | Cookie: Cookie.value, 26 | }, 27 | }) 28 | 29 | if (body.error === 0) { 30 | const iso689lang = custom2standard[body.lan] 31 | if (iso689lang) return iso689lang 32 | } 33 | 34 | throw getError(ERROR_CODE.UNSUPPORTED_LANG) 35 | } 36 | -------------------------------------------------------------------------------- /__tests__/step/generateRequestContent/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import { cloneDeep } from 'lodash' 3 | import { generateRequestContent } from 'src/step/generateRequestContent' 4 | import * as petSchema from 'example/fixture/pet.json' 5 | import projects from 'example/petProject/src/tsg.config' 6 | import * as step from 'src/step' 7 | import { restore } from 'src/projectGlobalVariable' 8 | import type { Project } from 'src/type' 9 | 10 | describe('src/step/generateRequestContent', () => { 11 | const project: Project = { 12 | name: 'pet', 13 | dest: './service', 14 | source: 'fixture/pet.json', 15 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 16 | } 17 | it('generateRequestContent', () => { 18 | const schema = cloneDeep(petSchema) as Spec 19 | step.cleanRefAndDefinitionName(schema, true) 20 | step.assembleSchemaToGlobal(schema, project) 21 | const content = generateRequestContent(schema, projects[0]) 22 | console.log(content) 23 | restore(project) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/translation/api/baidu/sign/seed.ts: -------------------------------------------------------------------------------- 1 | import request from '../../../utils/make-request' 2 | import { Cookie, fetchCookie } from '../state' 3 | import getError, { ERROR_CODE } from '../../../utils/error' 4 | 5 | const seedRegDoubleQuote = /window\.gtk\s=\s"([^']+)";/ 6 | const seedRegSingleQuote = /window\.gtk\s=\s'([^']+)';/ 7 | const tokenReg = /token:\s'([^']+)'/ 8 | 9 | export default async function fetchSeed() { 10 | if (!Cookie.value) { 11 | await fetchCookie() 12 | } 13 | // 尚不清楚 gtk 和 token 多久变一次,暂时在每次请求时都解析一遍 14 | const html = await request({ 15 | url: 'https://fanyi.baidu.com', 16 | headers: { 17 | Cookie: Cookie.value, 18 | }, 19 | responseType: 'text', 20 | }) 21 | let seed = html.match(seedRegDoubleQuote) 22 | if (!seed) { 23 | seed = html.match(seedRegSingleQuote) 24 | } 25 | if (seed) { 26 | const token = html.match(tokenReg) 27 | if (token) { 28 | return { 29 | seed: seed[1], 30 | token: token[1], 31 | } 32 | } 33 | } 34 | 35 | // 如果不能正确解析出 seed 和 token,则视为服务器错误 36 | throw getError(ERROR_CODE.API_SERVER_ERROR) 37 | } 38 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | # 开发过程 2 | 3 | 以下部分是一些开发记录,理解源码用。 4 | 5 | ## 设计 6 | 7 | ### 配置文件 8 | 9 | * 使用`ts`文件作为配置文件,用户填写配置时还能校验配置数据的类型。 10 | 11 | * 通过ts-node直接调用`src/run.ts`运行。 12 | 13 | ## 每个项目的swagger schema支持两种获取方式 14 | 15 | * 获取远程swagger获取json地址,source以http开头。 16 | 17 | * 获取本地json文件schema。 18 | 19 | ## 格式化 20 | 21 | * 将所有`$ref`与`definitions`中的键的名称统一化 22 | 23 | * pont的fixture中有中文definitions的情况,添加翻译中文部分为英文。 24 | 25 | * 去除所有空格。 26 | 27 | * 找到没有definitions中对应值的$ref的type,标记为any。 28 | 29 | * 生成新的schema对象数,其中所有的`$ref`与`definitions`的key都是转换好的,之后的操作在该新对象上进行,原始数据不需要再访问。 30 | 31 | ## 输出 32 | 33 | * 将所有`definitions`写入`definitions.ts`。 34 | 35 | * 将所有`paths`写入`paths.ts`,包括每个请求方法,参数与返回值结构。 36 | 37 | * 收集所有`paths`中的$ref依赖 38 | 开发过程中发现有些$ref在definitions没有对应项,都按any别名处理。 39 | 在每个请求函数中,将baseURL与接口url拼接。因为考虑跨域问题,没有将host也自动加上。 40 | 41 | * 最初想用fxios或axios作为请求库,但感觉尽量少依赖工具,直接用原生`fetch`依赖更少。 42 | 43 | interceptRequest,负责处理请求前的数据加工 44 | * 将请求参数中的路由参数替换到url中 45 | * 如果请求体是普通对象,用json格式化并添加json的http header 46 | * 如果请求体有formData项,自动添加成FormData 47 | 48 | interceptResponse,负责处理请求前的数据加工 49 | 50 | * 每个项目配套一个独立的`interceptor.ts`,起到每个请求方法请求前、后的数据加工作用。 51 | -------------------------------------------------------------------------------- /src/tool/traverseSchema.ts: -------------------------------------------------------------------------------- 1 | import * as traverse from 'traverse' 2 | import type { TraverseSchemaNode } from '../type' 3 | 4 | /** 5 | * recursively invoked on every schema node 6 | * update operation will modify the param data 7 | * @param the json schema object data 8 | * @param the function will be called recursively on each schema node 9 | * */ 10 | export const traverseSchema = (obj: { [k: string]: any }, func: (v: TraverseSchemaNode) => void): void => { 11 | traverse(obj).forEach(function traverseSchemaNode(this: any, value: any) { 12 | // check circular 13 | if (this.circular || !this.key || this.key === 'required') { 14 | return 15 | } 16 | const node: TraverseSchemaNode = { 17 | value, 18 | key: this.key, 19 | parent: (this.parent || {}).node, 20 | path: this.path, 21 | } 22 | func(node) 23 | }) 24 | } 25 | 26 | /** only travers "$ref" */ 27 | export const traverse$Ref = (obj: { [k: string]: any }, func: (v: string) => void): void => { 28 | traverseSchema(obj, ({ key, value }) => { 29 | if (key === '$ref' && typeof value === 'string') { 30 | func(value) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/translation/api/baidu/type.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface ResponseSymbol { 3 | parts: { 4 | part?: string // 单词属性,例如 'n.'、'vi.' 等 5 | part_name?: string // 部分单词没有 part,只有 part_name,例如 “Referer” 6 | means: string[] // 此单词属性下单词的释义 7 | }[] 8 | ph_am: string // 美国音标 9 | ph_en: string // 英国音标 10 | } 11 | export interface Response { 12 | error?: number // 查询失败时会有这个属性。997 表示 token 有误,此时应该获取 BAIDUID 后重试 13 | dict_result: { 14 | // 针对英语单词会提供词典数据。若当前翻译没有词典数据,则这个属性是一个空数组 15 | simple_means?: { 16 | symbols: [ResponseSymbol] // 虽然这是一个数组,但是它一直都只有一个元素 17 | 18 | exchange: { 19 | // 单词的其他变形 20 | word_done: '' | string[] // 过去分词 21 | word_er: '' | string[] 22 | word_est: '' | string[] 23 | word_ing: '' | string[] // 现在分词 24 | word_past: '' | string[] // 过去式 25 | word_pl: '' | string[] // 复数 26 | word_proto?: string[] // 词根,偶尔没有 27 | word_third: '' | string[] // 第三人称单数 28 | } 29 | } 30 | } 31 | 32 | trans_result: { 33 | data: { 34 | src: string 35 | dst: string 36 | }[] 37 | from: string 38 | to: string 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/tool/translate.ts: -------------------------------------------------------------------------------- 1 | import { baidu, google } from '../translation' 2 | import type { TranslationEngine } from '../type' 3 | import { sleep } from './sleep' 4 | import { info } from './log' 5 | 6 | export const translateEngines = { 7 | baidu, 8 | google, 9 | } 10 | 11 | type Option = { 12 | text: string 13 | engine: TranslationEngine 14 | interval?: number 15 | debug?: boolean 16 | } 17 | 18 | /** change the engine, the result will definitely be different. 19 | * better not use this. 20 | * */ 21 | export async function translate({ text, engine, interval = 0, debug = false }: Option) { 22 | try { 23 | if (interval > 0) { 24 | await sleep(interval * Math.random()) 25 | } 26 | if (debug) { 27 | info(`translating by ${engine}: "${text}"`) 28 | } 29 | const res = await translateEngines[engine].translate({ 30 | text, 31 | to: 'en', 32 | }) 33 | if (debug) { 34 | info(`translate result: "${String(res.result)}"`) 35 | } 36 | return res.result!.join('') 37 | } catch (e) { 38 | if (e instanceof Error) { 39 | throw new Error(`translate word "${text}" by engine "${engine}" fail, original error: ${e.toString()}`) 40 | } 41 | throw e 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /__tests__/step/generateRequestContent/generateMockData.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash' 2 | import type { Spec } from 'swagger-schema-official' 3 | import * as petSpec from 'example/fixture/pet.json' 4 | import { generateMockData } from 'src/step/generateRequestContent/generateMockData' 5 | import * as step from 'src/step' 6 | import type { Project } from 'src/type' 7 | import { getGlobal, restore } from 'src/projectGlobalVariable' 8 | 9 | describe('sample', () => { 10 | it('make sample data', () => { 11 | const project: Project = { 12 | name: 'projectPont', 13 | source: 'fixture/pontFixture.json', 14 | dest: './service', 15 | // keepGeneric: true, 16 | translationEngine: 'baidu', 17 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 18 | } 19 | 20 | const spec = cloneDeep(petSpec) as Spec 21 | step.cleanRefAndDefinitionName(spec, false) 22 | step.assembleSchemaToGlobal(spec, project) 23 | const { definitionMap, requestMap, enumMap } = getGlobal(project) 24 | 25 | const result = generateMockData(requestMap.getPetFindByStatus, definitionMap, enumMap) 26 | expect(result).toMatchSnapshot() 27 | 28 | restore(project) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /__tests__/ts.test.ts: -------------------------------------------------------------------------------- 1 | /** try some ts methods */ 2 | import * as ts from 'typescript' 3 | 4 | describe('ts', () => { 5 | it('ts', () => { 6 | const printer = ts.createPrinter({ 7 | newLine: ts.NewLineKind.LineFeed, 8 | }) 9 | const { factory } = ts 10 | function printNode(node: ts.Node) { 11 | const file = ts.createSourceFile('someFileName.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS) 12 | return printer.printNode(ts.EmitHint.Unspecified, node, file) 13 | } 14 | const s1 = factory.createUnionTypeNode([ 15 | factory.createLiteralTypeNode(factory.createStringLiteral('a')), 16 | factory.createLiteralTypeNode(factory.createStringLiteral('b')), 17 | ]) 18 | const s2 = factory.createTypeAliasDeclaration( 19 | [factory.createModifier(ts.SyntaxKind.ExportKeyword)], 20 | 'TypeA', 21 | undefined, 22 | s1, 23 | ) 24 | expect(printNode(s2)).toBe('export type TypeA = "a" | "b";') 25 | 26 | const s3 = factory.createTypeAliasDeclaration( 27 | [factory.createModifier(ts.SyntaxKind.ExportKeyword)], 28 | 'TypeB', 29 | undefined, 30 | factory.createTypeReferenceNode('TypeA', []), 31 | ) 32 | expect(printNode(s3)).toBe('export type TypeB = TypeA;') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /__tests__/tool/cleanName.test.ts: -------------------------------------------------------------------------------- 1 | import { cleanName } from 'src/tool/cleanName' 2 | 3 | describe('cleanName', () => { 4 | it('remove "#/definitions/"', () => { 5 | expect(cleanName('#/definitions/abc', true)).toBe('Abc') 6 | }) 7 | 8 | it('remove "#/components/schema/"', () => { 9 | expect(cleanName('#/components/schema/abc', true)).toBe('Abc') 10 | }) 11 | 12 | it('remove "#/definitions/abc«def»"', () => { 13 | expect(cleanName('#/definitions/abc«def»', true)).toBe('Abc') 14 | expect(cleanName('#/definitions/abc', true)).toBe('Abc') 15 | }) 16 | 17 | it('remove space and upper every word first charator', () => { 18 | expect(cleanName(' asdf asdf « def»', true)).toBe('AsdfAsdf') 19 | }) 20 | 21 | it('not keep generic symbol', () => { 22 | expect(cleanName(' asdf asdf « def»', false)).toBe('AsdfAsdfDef') 23 | expect(cleanName('#/definitions/abc«def»', false)).toBe('AbcDef') 24 | }) 25 | 26 | it('special charator', () => { 27 | expect(cleanName(' asdf 😝sdf « def»', false)).toBe('AsdfSdfDef') 28 | }) 29 | 30 | it('key words', () => { 31 | expect(cleanName('ReplyVO«List«Map«string,object»»»', true)).toBe('ReplyVO>>') 32 | expect(cleanName('Error', false)).toBe('TsgError') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /__tests__/tool/translate.test.ts: -------------------------------------------------------------------------------- 1 | import { baidu } from 'src/translation' 2 | import { translate } from 'src/tool/translate' 3 | 4 | // type Translate = typeof baidu.translate 5 | 6 | describe.skip('translate by engines', () => { 7 | // jest.mock('src/translation', () => ({ 8 | // baidu: { 9 | // translate: (_args: any) => 10 | // Promise.resolve({ 11 | // result: ['Output result « query parameters »'], 12 | // }) as ReturnType, 13 | // }, 14 | // google: { 15 | // translate: (_args: any) => 16 | // Promise.resolve({ 17 | // result: ['Output result'], 18 | // }) as ReturnType, 19 | // }, 20 | // })) 21 | it('translate by baidu', async () => { 22 | expect(await translate({ text: '输出结果«查询参数»', engine: 'baidu', debug: true, interval: 2000 })).toBe( 23 | "Output result 'Query parameters'", 24 | ) 25 | }, 5000) 26 | 27 | it('translate by google', async () => { 28 | expect(await translate({ text: '输出结果', engine: 'google', debug: true })).toBe('Output results') 29 | }, 5000) 30 | 31 | it('catch error', async () => { 32 | const origin = baidu.translate 33 | baidu.translate = jest.fn(() => { 34 | throw new Error('translate error') 35 | }) 36 | await expect(() => translate({ text: '输出结果«查询参数»', engine: 'baidu' })).rejects.toThrow() 37 | baidu.translate = origin 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /__tests__/step/generateRequestContent/generateResponseType.test.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import { cloneDeep } from 'lodash' 3 | import { generateResponseType } from 'src/step/generateRequestContent/generateResponseType' 4 | import * as petSpec from 'example/fixture/pet.json' 5 | import * as step from 'src/step' 6 | import { restore } from 'src/projectGlobalVariable' 7 | import type { Project } from 'src/type' 8 | 9 | describe('response type content', () => { 10 | const project: Project = { 11 | name: 'pet', 12 | dest: './service', 13 | source: 'fixture/pet.json', 14 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 15 | } 16 | 17 | it('response', () => { 18 | const schema = cloneDeep(petSpec) as Spec 19 | step.cleanRefAndDefinitionName(schema, true) 20 | step.assembleSchemaToGlobal(schema, project) 21 | let content = generateResponseType('GetXXX', schema.paths['/pet'].post!.responses, project) 22 | // console.log(content) 23 | expect(content).toMatchSnapshot() 24 | content = generateResponseType('GetXXX', schema.paths['/pet'].put!.responses, project) 25 | // console.log(content) 26 | expect(content).toMatchSnapshot() 27 | 28 | content = generateResponseType('GetXXX', schema.paths['/store/order'].post!.responses, project) 29 | // console.log(content) 30 | expect(content).toMatchSnapshot() 31 | restore(project) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/step/getUserConfig/cliOption.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander' 2 | import { configFileName } from '../../constant' 3 | 4 | function collectProjects(value: string) { 5 | return value.split(',') 6 | } 7 | 8 | type Result = { 9 | names: string[] 10 | init: boolean 11 | config: string 12 | } 13 | 14 | const program = new Command() 15 | 16 | /** 17 | * collect project names from cli 18 | * */ 19 | export const getCliOption = (): Result => { 20 | // eslint-disable-next-line 21 | const pkg = require('../../../package.json') 22 | program 23 | .version(pkg.version) 24 | .usage('tsg or tsg -p projectName') 25 | .option( 26 | '-p, --projects ', 27 | 'assign project name, more names use comma split, like projectA,projectB', 28 | collectProjects, 29 | ) 30 | .option('-i, --init', `create ${configFileName}.ts config file`) 31 | .option('-c, --config ', 'assign config file') 32 | .parse(process.argv) 33 | 34 | const options = program.opts() 35 | 36 | const result: Result = { 37 | names: [], 38 | init: Boolean(options.init), 39 | config: '', 40 | } 41 | const names = options.projects 42 | if (names) { 43 | result.names = names 44 | } 45 | if (options.config) { 46 | result.config = String(options.config).trim() 47 | } 48 | // if not delete commander cache 49 | // program will keep cache and break test 50 | delete options.projects 51 | delete options.init 52 | return result 53 | } 54 | -------------------------------------------------------------------------------- /src/translation/api/baidu/README.md: -------------------------------------------------------------------------------- 1 | # 百度网页翻译接口 2 | 3 | ## 翻译接口 4 | 5 | ``` 6 | 路径:https://fanyi.baidu.com/v2transapi 7 | 方法:POST 8 | 请求头: 9 | Content-Type: application/x-www-form-urlencoded; charset=UTF-8 10 | X-Requested-With: XMLHttpRequest 11 | Cookie: BAIDUID=<在无 Cookie 或清空了 Cookie 之后,访问 https://fanyi.baidu.com 会使用 Set-Cookie 响应头返回>; 12 | 请求体:query=<要翻译的文本>&from=<文本语种>&to=<目标语种>&transtype=translang&simple_means_flag=3&token=<见后文>&sign=<见后文> 13 | 响应体:<见 ./index.ts 中的 IResponse> 14 | ``` 15 | 16 | ### 如何计算翻译接口中的 token 和 sign 17 | 18 | `token` 可以从 https://fanyi.baidu.com 的 HTML 里提取出来,提取方法见 ./sign/seed.ts。注意:如果请求时没有带上 `BAIDUID` Cookie,那么得到的 token 是无效的! 19 | 20 | 为了解决这个问题,在 Node.js 端应该先请求一次网页,保存得到的 `BAIDUID`,然后后续再使用这个 Cookie 提取 token 和 seed;在浏览器端只需要提前请求一次网页就好,浏览器会自动设置 Cookie 并在后续的请求中带上 Cookie。 21 | 22 | 目前我的解决方案是在代码中硬编码了一个从浏览器中得到的 BAIDUID,不过最好的办法是在请求翻译接口得到 997 的 error 时自动获取一次 Cookie(在浏览器端就是自动请求一次网页)然后重试。 23 | 24 | 另外,`token` 似乎是根据 BAIDUID 来的,只要 BAIDUID 不变,token 就不会变;用户登录或者退出百度账号不会造成 BAIDUID 改变,另外这个 BAIDUID 的过期时间被设置成了一年,所以我估计直接在代码中硬编码一个 Cookie 应该没问题,但浏览器端无法得知用户什么时候会清空 Cookie,所以最好还是每次运行前都先请求一次。 25 | 26 | `sign` 需要先通过请求 https://fanyi.baidu.com 后从 HTML 中提取出来一个种子,然后用方法根据待翻译的文本和种子计算出来。种子应该是会在一定时间后刷新的,但不太清楚具体是多长时间。提取种子的方法见 ./sign/seed.ts,计算 sign 的方法见 ./sign/sign.ts。 27 | 28 | ## 检测语种接口 29 | 30 | ``` 31 | 路径:https://fanyi.baidu.com/langdetect 32 | 方法:POST 33 | 请求头: 34 | Content-Type: application/x-www-form-urlencoded; charset=UTF-8 35 | 请求体:query=<要检测语种的文本,最长 73 个字符> 36 | 响应体:<见 ./index.ts 中的 IDetectResult> 37 | ``` 38 | -------------------------------------------------------------------------------- /src/source.ts: -------------------------------------------------------------------------------- 1 | import type { SourceFile } from 'ts-morph' 2 | import { 3 | // OptionalKind, 4 | Project, 5 | // PropertySignatureStructure, 6 | ScriptTarget, 7 | } from 'ts-morph' 8 | 9 | const project = new Project({ 10 | useInMemoryFileSystem: true, 11 | compilerOptions: { 12 | target: ScriptTarget.ESNext, 13 | }, 14 | }) 15 | 16 | const fs = project.getFileSystem() 17 | 18 | // make virtualFileName uniq 19 | let virtualFileNameId = 0 20 | 21 | /** 使用ts-morph编译ts,隐藏细节,只暴露SourceFile */ 22 | export const compile = async (func: (s: SourceFile) => void, source?: string) => { 23 | const fileName = `file${(virtualFileNameId += 1)}.ts` 24 | const sourceFile = project.createSourceFile(fileName, source) 25 | func(sourceFile) 26 | await sourceFile.save() 27 | const result = fs.readFileSync(fileName) 28 | await sourceFile.deleteImmediately() 29 | project.removeSourceFile(sourceFile) 30 | return result 31 | } 32 | 33 | /** get SourceFile */ 34 | export const sow = (content?: string) => { 35 | const name = `file${(virtualFileNameId += 1)}.ts` 36 | const sourceFile = project.createSourceFile(name, content) 37 | ;(sourceFile as any).$$fileName = name 38 | return sourceFile 39 | } 40 | 41 | /** remove SourceFile and get typescript content */ 42 | export const harvest = (sourceFile: SourceFile) => { 43 | sourceFile.saveSync() 44 | const result = fs.readFileSync((sourceFile as any).$$fileName) 45 | sourceFile.deleteImmediatelySync() 46 | project.removeSourceFile(sourceFile) 47 | return result 48 | } 49 | -------------------------------------------------------------------------------- /src/step/fetchOpenapiData.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import 'cross-fetch/polyfill' 3 | import { parse } from 'json5' 4 | import { error, info } from '../tool/log' 5 | import type { Project } from '../type' 6 | 7 | /** 8 | * fetch remote spec if url starts with "http" 9 | * or use "require" read local file. 10 | * when remote swagger doc has auth, the best way is download the spec to local, and assign the local file path. 11 | * the second param ref is https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch 12 | * */ 13 | export const fetchOpenapiData = async (project: Project, tsGearConfigPath: string) => { 14 | const url = project.source 15 | if (url.startsWith('http')) { 16 | const verbose = `project: ${project.name} url: ${url}` 17 | info(`start fetching ${verbose}`) 18 | let fetchOption = project.fetchApiDocOption 19 | if (typeof fetchOption === 'function') { 20 | fetchOption = await fetchOption() 21 | } 22 | const res = await fetch(url, fetchOption) 23 | const swaggerSchema = parse(await res.text()) 24 | info(`got swagger spec doc from ${verbose}`) 25 | return swaggerSchema 26 | } 27 | const source = join(tsGearConfigPath, project.source) 28 | // use require for json file 29 | if (!source.endsWith('.json')) { 30 | const message = 'user config file should ends with `.json`' 31 | error(message) 32 | throw new Error(message) 33 | } 34 | /* eslint-disable-next-line global-require,import/no-dynamic-require */ 35 | return require(source) 36 | } 37 | -------------------------------------------------------------------------------- /src/translation/baiduToken.ts: -------------------------------------------------------------------------------- 1 | import 'cross-fetch/polyfill' 2 | 3 | export const fetchBaiduid = async () => { 4 | fetch('https://www.baidu.com/', { 5 | headers: { 6 | accept: 7 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 8 | 'Accept-Encoding': 'gzip, deflate, br', 9 | 'accept-language': 'zh-CN,zh;q=0.9', 10 | 'sec-fetch-dest': 'document', 11 | 'sec-fetch-mode': 'navigate', 12 | 'sec-fetch-site': 'none', 13 | 'sec-fetch-user': '?1', 14 | 'upgrade-insecure-requests': '1', 15 | Host: 'www.baidu.com', 16 | 'sec-ch-ua': '.Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103', 17 | 'sec-ch-ua-mobile': '?0', 18 | 'sec-ch-ua-platform': 'Windows', 19 | 'Sec-Fetch-Dest': 'document', 20 | 'Sec-Fetch-Mode': 'navigate', 21 | 'Sec-Fetch-Site': 'none', 22 | 'Sec-Fetch-User': '?1', 23 | 'Upgrade-Insecure-Requests': '1', 24 | 'User-Agent': 25 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 26 | }, 27 | referrerPolicy: 'strict-origin-when-cross-origin', 28 | body: null, 29 | method: 'GET', 30 | }).then(res => { 31 | const cookies = res.headers.get('set-cookie') 32 | const id = (cookies || '').split('; ').find(c => c.match('BAIDUID=')) 33 | if (id) { 34 | return id.split(', ')[1] 35 | } 36 | return '' 37 | }) 38 | } 39 | fetchBaiduid() 40 | -------------------------------------------------------------------------------- /src/translation/api/google/sign/encryption.js: -------------------------------------------------------------------------------- 1 | var window = { 2 | TKK: '0' 3 | } 4 | 5 | var yr = null 6 | var xr = function(a, b) { 7 | for (var c = 0; c < b.length - 2; c += 3) { 8 | var d = b.charAt(c + 2), 9 | d = 'a' <= d ? d.charCodeAt(0) - 87 : Number(d), 10 | d = '+' == b.charAt(c + 1) ? a >>> d : a << d 11 | a = '+' == b.charAt(c) ? (a + d) & 4294967295 : a ^ d 12 | } 13 | return a 14 | } 15 | 16 | function sM(a) { 17 | var b 18 | if (null !== yr) { 19 | b = yr 20 | } else { 21 | b = (yr = window.TKK || '') || '' 22 | } 23 | var d = b.split('.') 24 | b = Number(d[0]) || 0 25 | for (var e = [], f = 0, g = 0; g < a.length; g++) { 26 | var l = a.charCodeAt(g) 27 | 128 > l 28 | ? (e[f++] = l) 29 | : (2048 > l 30 | ? (e[f++] = (l >> 6) | 192) 31 | : (55296 == (l & 64512) && 32 | g + 1 < a.length && 33 | 56320 == (a.charCodeAt(g + 1) & 64512) 34 | ? ((l = 65536 + ((l & 1023) << 10) + (a.charCodeAt(++g) & 1023)), 35 | (e[f++] = (l >> 18) | 240), 36 | (e[f++] = ((l >> 12) & 63) | 128)) 37 | : (e[f++] = (l >> 12) | 224), 38 | (e[f++] = ((l >> 6) & 63) | 128)), 39 | (e[f++] = (l & 63) | 128)) 40 | } 41 | a = b 42 | for (f = 0; f < e.length; f++) (a += e[f]), (a = xr(a, '+-a^+6')) 43 | a = xr(a, '+-3^+b+-f') 44 | a ^= Number(d[1]) || 0 45 | 0 > a && (a = (a & 2147483647) + 2147483648) 46 | a %= 1e6 47 | return a.toString() + '.' + (a ^ b) 48 | } 49 | 50 | export { sM, window } 51 | -------------------------------------------------------------------------------- /src/step/toJS.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import * as ts from 'typescript' 3 | import { sync } from 'rimraf' 4 | import { targetFileNames } from '../constant' 5 | import type { Project } from '../type' 6 | import { info } from '../tool/log' 7 | 8 | export function toJS(project: Project, tsGearConfigPath: string): void { 9 | const compilerOptions: ts.CompilerOptions = { 10 | module: ts.ModuleKind.ESNext, 11 | target: ts.ScriptTarget.ESNext, 12 | } 13 | const targetPath = join(tsGearConfigPath, project.dest, project.name) 14 | const fileNames = [join(targetPath, targetFileNames.index)] 15 | const program = ts.createProgram(fileNames, compilerOptions) 16 | // 运行前先清除已有的js文件 17 | // sync(join(targetPath, '*.js')) 18 | const emitResult = program.emit() 19 | 20 | // const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics) 21 | 22 | // allDiagnostics.forEach(diagnostic => { 23 | // if (diagnostic.file) { 24 | // const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!) 25 | // const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') 26 | // console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`) 27 | // } else { 28 | // console.log(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')) 29 | // } 30 | // }) 31 | 32 | const exitCode = emitResult.emitSkipped ? 1 : 0 33 | if (exitCode === 0) { 34 | info(`project "${project.name}" transpiled to javascript success.`) 35 | // 成功后清除ts文件 36 | sync(join(targetPath, '*.ts')) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /__tests__/step/generateRequestContent/generateParameterType.test.ts: -------------------------------------------------------------------------------- 1 | import type { Parameter, Spec } from 'swagger-schema-official' 2 | import { cloneDeep } from 'lodash' 3 | import { generateRequestOptionType } from 'src/step/generateRequestContent/generateRequestOptionType' 4 | import * as petSpec from 'example/fixture/pet.json' 5 | import * as step from 'src/step' 6 | import { restore } from 'src/projectGlobalVariable' 7 | import type { Project } from 'src/type' 8 | 9 | describe('src/step/generateRequestContent/generateParameterType', () => { 10 | const project: Project = { 11 | name: 'pet', 12 | dest: './service', 13 | source: 'fixture/pet.json', 14 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 15 | } 16 | it('generateParameterType', () => { 17 | const spec = cloneDeep(petSpec) as Spec 18 | step.cleanRefAndDefinitionName(spec, true) 19 | step.assembleSchemaToGlobal(spec, project) 20 | let content = generateRequestOptionType('ReqParam', spec.paths['/pet'].post!.parameters as Parameter[], project) 21 | console.log(content) 22 | // content = generateParameterType('ReqParam', schema.paths['/pet/{petId}'].post!.parameters as Parameter[]) 23 | // console.log(content) 24 | 25 | // content = generateParameterType('ReqParam', schema.paths['/store/order'].post!.parameters as Parameter[]) 26 | // console.log(content) 27 | content = generateRequestOptionType( 28 | 'postUserCreateWithList', 29 | spec.paths['/user/createWithList'].post!.parameters as Parameter[], 30 | project, 31 | ) 32 | console.log(content) 33 | restore(project) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/tool/cleanName.ts: -------------------------------------------------------------------------------- 1 | const keyWords = Object.getOwnPropertyNames(global).filter(p => /[A-Z]/.test(p[0])) 2 | 3 | const isReserved = (v: string) => keyWords.includes(v) 4 | 5 | /** 6 | * clean type name for typescript definition 7 | * remove "#/definitions/" 8 | * remove "#/components/schema/" 9 | * remove all spaces 10 | * remove all non english charator, like "😝" 11 | * replace "«" with "<" when keepGeneric 12 | * replace "»" with ">" when keepGeneric 13 | * upper case each word first charator 14 | * */ 15 | export const cleanName = (name: string, keepGeneric: boolean) => { 16 | const word = name.replace(/^#\/.+\//, '').replace(/./g, (target, index, str) => { 17 | // console.log(target, index, str) 18 | if (/[a-z]/i.test(target) && (index === 0 || /[^a-z]/i.test(str[index - 1]))) { 19 | return target.toUpperCase() 20 | } 21 | if (/\s/.test(target)) { 22 | return '' 23 | } 24 | if (keepGeneric) { 25 | /* eslint-disable-next-line default-case */ 26 | switch (target) { 27 | case '«': 28 | case '<': 29 | return '<' 30 | case '»': 31 | case '>': 32 | return '>' 33 | } 34 | } 35 | if (/[^a-z]/i.test(target)) { 36 | return '' 37 | } 38 | return target 39 | }) 40 | 41 | // replace reserved key words, as Map, String 42 | if (isReserved(word)) { 43 | return `Tsg${word}` 44 | } 45 | const words = word.split(/\b/) 46 | const hasReserved = words.some(w => { 47 | return isReserved(w) 48 | }) 49 | if (!hasReserved) { 50 | return word 51 | } 52 | return words 53 | .map(w => { 54 | return isReserved(w) ? `Tsg${w}` : w 55 | }) 56 | .join('') 57 | } 58 | -------------------------------------------------------------------------------- /__tests__/step/getUserConfig/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-relative-packages */ 2 | import { join } from 'path' 3 | import { noop } from 'lodash' 4 | import { getUserConfig } from '../../../src/step/getUserConfig' 5 | import exampleProjects from 'example/petProject/src/tsg.config' 6 | 7 | describe('getUserConfig', () => { 8 | const originLength = process.argv.length 9 | const cwd = process.cwd() 10 | 11 | // restore process.argv 12 | afterEach(() => { 13 | process.argv.length = originLength 14 | process.chdir(cwd) 15 | }) 16 | 17 | describe('filter project names', () => { 18 | beforeEach(() => { 19 | process.chdir(join(cwd, 'example', 'petProject')) 20 | }) 21 | 22 | it('with project names in cli', async () => { 23 | expect(await getUserConfig()).toEqual({ 24 | projects: exampleProjects, 25 | tsGearConfigPath: join(process.cwd(), 'src'), 26 | }) 27 | process.argv.push('-p', 'pet,projectE') 28 | expect(await getUserConfig()).toEqual({ 29 | projects: [exampleProjects[0], exampleProjects[2]], 30 | tsGearConfigPath: join(process.cwd(), 'src'), 31 | }) 32 | }) 33 | 34 | it('with none exist project names in cli', async () => { 35 | const spy = jest.spyOn(console, 'log').mockImplementation(noop) 36 | expect(await getUserConfig()).toEqual({ 37 | projects: exampleProjects, 38 | tsGearConfigPath: join(process.cwd(), 'src'), 39 | }) 40 | process.argv.push('-p', 'noExistProjectName') 41 | expect(await getUserConfig()).toEqual({ projects: [], tsGearConfigPath: join(process.cwd(), 'src') }) 42 | expect(spy).toHaveBeenCalled() 43 | spy.mockRestore() 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /__tests__/step/fetchSwagger.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import * as fetchMock from 'fetch-mock' 3 | import * as step from 'src/step' 4 | import type { Project } from 'src/type' 5 | 6 | const getOnce = fetchMock.getOnce.bind(fetchMock) 7 | 8 | describe('fetchSwagger', () => { 9 | const cwd = process.cwd() 10 | it('file not ends with json', async () => { 11 | const project: Project = { 12 | name: 'abc', 13 | source: 'abc', 14 | dest: 'abc', 15 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 16 | } 17 | await expect(async () => { 18 | await step.fetchOpenapiData(project, '') 19 | }).rejects.toThrow('user config file should ends with `.json`') 20 | }) 21 | 22 | it('get json', async () => { 23 | const project: Project = { 24 | name: 'abc', 25 | source: join('example', 'petProject', 'package.json'), 26 | dest: 'abc', 27 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 28 | } 29 | const spec = await step.fetchOpenapiData(project, '') 30 | // eslint-disable-next-line @typescript-eslint/no-var-requires,global-require,import/no-dynamic-require 31 | expect(spec).toEqual(require(join(cwd, 'example', 'petProject', 'package.json'))) 32 | }) 33 | 34 | it('fetch remote spec', async () => { 35 | const project: Project = { 36 | name: 'abc', 37 | source: 'http://abc.com', 38 | dest: 'abc', 39 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 40 | } 41 | getOnce('http://abc.com', { ok: true }) 42 | const res = await step.fetchOpenapiData(project, '') 43 | expect(res).toEqual({ ok: true }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /__tests__/step/generateRequestContent/__snapshots__/generateResponseType.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`response type content response 1`] = ` 4 | { 5 | "responseTypeContent": "/** @description response type for GetXXX */ 6 | export interface GetXXXResponse { 7 | /** 8 | * @description 9 | * Invalid input 10 | */ 11 | 405: any; 12 | } 13 | ", 14 | "responseTypeName": "GetXXXResponse", 15 | "successTypeContent": "export type GetXXXResponseSuccess = any", 16 | "successTypeName": "GetXXXResponseSuccess", 17 | } 18 | `; 19 | 20 | exports[`response type content response 2`] = ` 21 | { 22 | "responseTypeContent": "/** @description response type for GetXXX */ 23 | export interface GetXXXResponse { 24 | /** 25 | * @description 26 | * Invalid ID supplied 27 | */ 28 | 400: any; 29 | /** 30 | * @description 31 | * Pet not found 32 | */ 33 | 404: any; 34 | /** 35 | * @description 36 | * Validation exception 37 | */ 38 | 405: any; 39 | } 40 | ", 41 | "responseTypeName": "GetXXXResponse", 42 | "successTypeContent": "export type GetXXXResponseSuccess = any", 43 | "successTypeName": "GetXXXResponseSuccess", 44 | } 45 | `; 46 | 47 | exports[`response type content response 3`] = ` 48 | { 49 | "responseTypeContent": "/** @description response type for GetXXX */ 50 | export interface GetXXXResponse { 51 | /** 52 | * @description 53 | * successful operation 54 | */ 55 | 200: Order; 56 | /** 57 | * @description 58 | * Invalid Order 59 | */ 60 | 400: any; 61 | } 62 | ", 63 | "responseTypeName": "GetXXXResponse", 64 | "successTypeContent": "export type GetXXXResponseSuccess = GetXXXResponse[200]", 65 | "successTypeName": "GetXXXResponseSuccess", 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /example/petProject/src/service/doc/definition.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export type GetPetFindByStatusItems = "available" | "pending" | "sold"; 6 | export type OrderStatus = "placed" | "approved" | "delivered"; 7 | export type PetStatus = GetPetFindByStatusItems; 8 | export interface Order { 9 | /** @format int64 */ 10 | id?: number; 11 | /** @format int64 */ 12 | petId?: number; 13 | /** @format int32 */ 14 | quantity?: number; 15 | /** @format date-time */ 16 | shipDate?: string; 17 | /** 18 | * @description 19 | * Order Status 20 | */ 21 | status?: OrderStatus; 22 | /** @default false */ 23 | complete?: boolean; 24 | } 25 | 26 | export interface User { 27 | /** @format int64 */ 28 | id?: number; 29 | username?: string; 30 | firstName?: string; 31 | lastName?: string; 32 | email?: string; 33 | password?: string; 34 | phone?: string; 35 | /** 36 | * @description 37 | * User Status 38 | * @format int32 39 | */ 40 | userStatus?: number; 41 | } 42 | 43 | export interface Category { 44 | /** @format int64 */ 45 | id?: number; 46 | pet?: Pet; 47 | name?: string; 48 | } 49 | 50 | export interface Tag { 51 | /** @format int64 */ 52 | id?: number; 53 | name?: string; 54 | } 55 | 56 | export interface Pet { 57 | /** @format int64 */ 58 | id?: number; 59 | category?: Category; 60 | /** 61 | * @example 62 | * doggie 63 | */ 64 | name: string; 65 | photoUrls: Array; 66 | tags?: Array; 67 | /** 68 | * @description 69 | * pet status in the store 70 | */ 71 | status?: PetStatus; 72 | } 73 | 74 | export interface ApiResponse { 75 | /** @format int32 */ 76 | code?: number; 77 | type?: string; 78 | message?: string; 79 | } 80 | -------------------------------------------------------------------------------- /example/petProject/src/service/pet/definition.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export type GetPetFindByStatusItems = "available" | "pending" | "sold"; 6 | export type OrderStatus = "placed" | "approved" | "delivered"; 7 | export type PetStatus = GetPetFindByStatusItems; 8 | export interface Order { 9 | /** @format int64 */ 10 | id?: number; 11 | /** @format int64 */ 12 | petId?: number; 13 | /** @format int32 */ 14 | quantity?: number; 15 | /** @format date-time */ 16 | shipDate?: string; 17 | /** 18 | * @description 19 | * Order Status 20 | */ 21 | status?: OrderStatus; 22 | /** @default false */ 23 | complete?: boolean; 24 | } 25 | 26 | export interface User { 27 | /** @format int64 */ 28 | id?: number; 29 | username?: string; 30 | firstName?: string; 31 | lastName?: string; 32 | email?: string; 33 | password?: string; 34 | phone?: string; 35 | /** 36 | * @description 37 | * User Status 38 | * @format int32 39 | */ 40 | userStatus?: number; 41 | } 42 | 43 | export interface Category { 44 | /** @format int64 */ 45 | id?: number; 46 | pet?: Pet; 47 | name?: string; 48 | } 49 | 50 | export interface Tag { 51 | /** @format int64 */ 52 | id?: number; 53 | name?: string; 54 | } 55 | 56 | export interface Pet { 57 | /** @format int64 */ 58 | id?: number; 59 | category?: Category; 60 | /** 61 | * @example 62 | * doggie 63 | */ 64 | name: string; 65 | photoUrls: Array; 66 | tags?: Array; 67 | /** 68 | * @description 69 | * pet status in the store 70 | */ 71 | status?: PetStatus; 72 | } 73 | 74 | export interface ApiResponse { 75 | /** @format int32 */ 76 | code?: number; 77 | type?: string; 78 | message?: string; 79 | } 80 | -------------------------------------------------------------------------------- /src/requester/axios.ts: -------------------------------------------------------------------------------- 1 | /** use axios fetch to request */ 2 | import axios from 'axios' 3 | import type { Method, AxiosInstance, AxiosRequestConfig } from 'axios' 4 | import { forEach } from 'lodash' 5 | import * as pathToRegexp from 'path-to-regexp' 6 | import type { RequestParameter, Requester } from '../type' 7 | 8 | /** transform parseUrl('/api/abc/:id', { path: { id: '123' } }) to '/api/abc/123' 9 | * */ 10 | export const parseUrl = (url: string, option?: RequestParameter): string => { 11 | if (option) { 12 | if (option.path) { 13 | Object.getOwnPropertyNames(option.path).forEach(k => { 14 | option.path[k] = encodeURIComponent(String(option.path[k])) 15 | }) 16 | url = pathToRegexp.compile(url)(option.path) 17 | } 18 | } 19 | return url 20 | } 21 | 22 | /** assign request body to axios option */ 23 | export function interceptRequest(url: string, option?: RequestParameter): [string, AxiosRequestConfig] { 24 | url = parseUrl(url, option) 25 | option = option || {} 26 | const requestOption: AxiosRequestConfig = { 27 | method: (option.method || 'get') as Method, 28 | } 29 | if (option.header) { 30 | requestOption.headers = option.header 31 | } 32 | if (option.body) { 33 | requestOption.data = option.body 34 | } 35 | if (option.formData) { 36 | const formData = new FormData() 37 | // 这种上传文件的情况,应该只有一维的键值对应,只用forEach处理第一层数据 38 | forEach(option.formData, (v: any, k: string) => { 39 | formData.append(k, v) 40 | }) 41 | requestOption.data = formData 42 | } 43 | return [url, requestOption] 44 | } 45 | 46 | export const requester = (ax?: AxiosInstance): Requester => { 47 | ax = ax || axios.create() 48 | return (apiUrl: string, param?: RequestParameter) => { 49 | const [url, option] = interceptRequest(apiUrl, param) 50 | return ax!(url, option) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/translation/api/baidu/sign/sign.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | var C: any = null 3 | 4 | /** 5 | * 从百度网页翻译中复制过来的计算签名的代码 6 | * @param text 要查询的文本 7 | * @param seed 从 ./seed.ts 获取到的 seed 8 | */ 9 | export default function(text: string, seed: string) { 10 | var o = text.length 11 | o > 30 && 12 | (text = 13 | '' + 14 | text.substr(0, 10) + 15 | text.substr(Math.floor(o / 2) - 5, 10) + 16 | text.substr(-10, 10)) 17 | var t = null !== C ? C : (C = seed || '') || '' 18 | for ( 19 | var e = t.split('.'), 20 | h = Number(e[0]) || 0, 21 | i = Number(e[1]) || 0, 22 | d = [], 23 | f = 0, 24 | g = 0; 25 | g < text.length; 26 | g++ 27 | ) { 28 | var m = text.charCodeAt(g) 29 | 128 > m 30 | ? (d[f++] = m) 31 | : (2048 > m 32 | ? (d[f++] = (m >> 6) | 192) 33 | : (55296 === (64512 & m) && 34 | g + 1 < text.length && 35 | 56320 === (64512 & text.charCodeAt(g + 1)) 36 | ? ((m = 37 | 65536 + ((1023 & m) << 10) + (1023 & text.charCodeAt(++g))), 38 | (d[f++] = (m >> 18) | 240), 39 | (d[f++] = ((m >> 12) & 63) | 128)) 40 | : (d[f++] = (m >> 12) | 224), 41 | (d[f++] = ((m >> 6) & 63) | 128)), 42 | (d[f++] = (63 & m) | 128)) 43 | } 44 | for (var S = h, u = '+-a^+6', l = '+-3^+b+-f', s = 0; s < d.length; s++) 45 | (S += d[s]), (S = a(S, u)) 46 | return ( 47 | (S = a(S, l)), 48 | (S ^= i), 49 | 0 > S && (S = (2147483647 & S) + 2147483648), 50 | (S %= 1e6), 51 | S.toString() + '.' + (S ^ h) 52 | ) 53 | } 54 | 55 | function a(r: any, o: any) { 56 | for (var t = 0; t < o.length - 2; t += 3) { 57 | var a = o.charAt(t + 2) 58 | ;(a = a >= 'a' ? a.charCodeAt(0) - 87 : Number(a)), 59 | (a = '+' === o.charAt(t + 1) ? r >>> a : r << a), 60 | (r = '+' === o.charAt(t) ? (r + a) & 4294967295 : r ^ a) 61 | } 62 | return r 63 | } 64 | -------------------------------------------------------------------------------- /__tests__/petProject.test.ts: -------------------------------------------------------------------------------- 1 | import 'cross-fetch/polyfill' 2 | import { deletePetPetId, getUserLogin } from 'example/petProject/src/service/pet/request' 3 | 4 | /** 在run的测试用例运行之后,已经生成了pet的service文件 */ 5 | describe('pet methods', () => { 6 | const originEnv = process.env.NODE_ENV 7 | beforeEach(() => { 8 | process.env.NODE_ENV = 'development' 9 | }) 10 | afterEach(() => { 11 | process.env.NODE_ENV = originEnv 12 | }) 13 | it('deletePetPetId, 替换path参数', async () => { 14 | const mockRes = { ok: true } 15 | const mockFetch = jest.fn(() => 16 | Promise.resolve( 17 | new Response(JSON.stringify(mockRes), { 18 | headers: { 'Content-Type': 'application/json' }, 19 | }), 20 | ), 21 | ) 22 | 23 | const g: any = global 24 | const originFetch: any = g.fetch 25 | 26 | g.fetch = mockFetch 27 | const res = await deletePetPetId({ 28 | path: { 29 | petId: 1, 30 | }, 31 | }) 32 | expect(mockFetch).toHaveBeenCalledTimes(1) 33 | expect(mockFetch).toHaveBeenLastCalledWith('/v2/pet/1', { 34 | method: 'delete', 35 | }) 36 | expect(res).toEqual(mockRes) 37 | g.fetch = originFetch 38 | }) 39 | 40 | it('getUserLogin, 有query', async () => { 41 | const mockRes = { ok: true } 42 | const mockFetch = jest.fn(() => 43 | Promise.resolve( 44 | new Response(JSON.stringify(mockRes), { 45 | headers: { 'Content-Type': 'application/json' }, 46 | }), 47 | ), 48 | ) 49 | const g: any = global 50 | const originFetch: any = g.fetch 51 | 52 | g.fetch = mockFetch 53 | const query = { 54 | username: 'a', 55 | password: 'b', 56 | } 57 | const res = await getUserLogin(query) 58 | expect(mockFetch).toHaveBeenCalledTimes(1) 59 | expect(mockFetch).toHaveBeenLastCalledWith(`/v2/user/login?${new URLSearchParams(query)}`, { 60 | method: 'get', 61 | }) 62 | expect(res).toEqual(mockRes) 63 | 64 | g.fetch = originFetch 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/translation/api/google/translate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StringOrTranslateOptions, 3 | TranslateOptions, 4 | TranslateResult 5 | } from '../types' 6 | import request from '../../utils/make-request' 7 | import { getRoot } from './state' 8 | import detect from './detect' 9 | import sign from './sign' 10 | 11 | export default async function(options: StringOrTranslateOptions) { 12 | let { text, com = false, from = '', to = '' } = 13 | typeof options === 'string' ? { text: options } : options 14 | 15 | if (!from) { 16 | from = await detect(options) 17 | } 18 | if (!to) { 19 | to = from.startsWith('zh') ? 'en' : 'zh-CN' 20 | } 21 | 22 | return transformRaw( 23 | await request({ 24 | url: getRoot(com) + '/translate_a/single', 25 | query: { 26 | client: 'webapp', 27 | sl: from, 28 | tl: to, 29 | hl: 'zh-CN', 30 | dt: ['at', 'bd', 'ex', 'ld', 'md', 'qca', 'rw', 'rm', 'ss', 't'], 31 | otf: '2', 32 | ssel: '3', 33 | tsel: '0', 34 | kc: '6', 35 | tk: await sign(text, com), 36 | q: text 37 | } 38 | }), 39 | { 40 | from, 41 | to, 42 | com, 43 | text 44 | } 45 | ) 46 | } 47 | 48 | function transformRaw(body: any[], queryObj: TranslateOptions) { 49 | const { text, com, to } = queryObj 50 | const googleFrom = body[2] 51 | 52 | const result: TranslateResult = { 53 | text, 54 | raw: body, 55 | from: googleFrom, 56 | to: to!, 57 | link: `${getRoot( 58 | com 59 | )}/#view=home&op=translate&sl=${googleFrom}&tl=${to}&text=${encodeURIComponent( 60 | text 61 | )}` 62 | } 63 | 64 | try { 65 | result.dict = body[1].map((arr: any[]) => { 66 | return arr[0] + ':' + arr[1].join(',') 67 | }) 68 | } catch (e) {} 69 | 70 | try { 71 | result.result = body[0] 72 | .map((arr: string[]) => arr[0]) 73 | .filter((x: string) => x) 74 | .map((x: string) => x.trim()) 75 | } catch (e) {} 76 | 77 | return result 78 | } 79 | -------------------------------------------------------------------------------- /__tests__/tsMorph.test.ts: -------------------------------------------------------------------------------- 1 | import { compile } from '../src/source' 2 | // import { PropertyDeclarationStructure } from 'ts-morph' 3 | 4 | describe('compile', () => { 5 | it('interface', async () => { 6 | const tsTemplate = await compile(source => { 7 | const inter = source.addInterface({ name: 'ITest' }) 8 | inter.addProperty({ 9 | name: 'a', 10 | type: `{ 11 | name: string 12 | age: number 13 | }`, 14 | }) 15 | }) 16 | expect(tsTemplate).toMatchSnapshot() 17 | }) 18 | 19 | it('class', async () => { 20 | const tsTemplate = await compile(source => { 21 | const inter = source.addClass({ name: 'MyClass' }) 22 | inter.addProperty({ 23 | name: 'a', 24 | leadingTrivia: 'public ', 25 | type: `{ 26 | name: string 27 | age: number 28 | }`, 29 | }) 30 | }) 31 | expect(tsTemplate).toMatchSnapshot() 32 | }) 33 | 34 | it('function', async () => { 35 | const tsTemplate = await compile(source => { 36 | source.addFunction({ 37 | name: 'myFunc', 38 | parameters: [ 39 | { 40 | name: 'param', 41 | type: 'number', 42 | }, 43 | ], 44 | isExported: true, 45 | statements: ` 46 | const [ url, option ] = interceptRequest(param) 47 | option.method = 'get' 48 | return fetch(url, option).then(interceptResponse) 49 | `, 50 | }) 51 | }) 52 | expect(tsTemplate).toMatchSnapshot() 53 | }) 54 | 55 | it('structure', async () => { 56 | const tsTemplate = await compile( 57 | source => { 58 | const a = source.getInterfaces()[0] 59 | // console.log(a.getStructure()) 60 | a.addProperty({ 61 | name: 'c', 62 | }) 63 | }, 64 | ` 65 | interface A { 66 | b: { 67 | name: string 68 | } 69 | } 70 | `, 71 | ) 72 | // console.log(tsTemplate) 73 | expect(tsTemplate).toMatchSnapshot() 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/step/collectRefsInRequestAndPatchDefinition.ts: -------------------------------------------------------------------------------- 1 | import { getGlobal } from '../projectGlobalVariable' 2 | import { traverse$Ref } from '../tool/traverseSchema' 3 | import type { Project } from '../type' 4 | import { patchGlobalDefinitionMap } from '../tool/patchGlobalDefinitionMap' 5 | import { shouldKeepRequest } from '../tool/shouldKeepRequest' 6 | 7 | /** 8 | * collect refs in paths 9 | * run after parseGenericType 10 | * */ 11 | export const collectRefsInRequestAndPatchDefinition = (project: Project) => { 12 | const { requestRefSet, requestMap, definitionMap } = getGlobal(project) 13 | const keepGeneric = project.keepGeneric !== false 14 | // when not keepGeneric, definition alse need to patch 15 | Object.getOwnPropertyNames(definitionMap).forEach(name => { 16 | const { schema } = definitionMap[name] 17 | if (schema) { 18 | traverse$Ref(schema, value => { 19 | if (keepGeneric) { 20 | value 21 | .split(/<|>|,/) 22 | .filter(Boolean) 23 | .forEach(typeName => { 24 | patchGlobalDefinitionMap({ typeName, definitionMap }) 25 | }) 26 | } else { 27 | patchGlobalDefinitionMap({ typeName: value, definitionMap }) 28 | } 29 | }) 30 | } 31 | }) 32 | // gather ref definition names from paths 33 | const { apiFilter } = project 34 | Object.getOwnPropertyNames(requestMap).forEach(name => { 35 | const request = requestMap[name] 36 | if (!shouldKeepRequest(request, apiFilter)) { 37 | return 38 | } 39 | const { schema } = request 40 | traverse$Ref(schema, value => { 41 | if (keepGeneric) { 42 | value 43 | .split(/<|>|,/) 44 | .filter(Boolean) 45 | .forEach(typeName => { 46 | requestRefSet.add(typeName) 47 | patchGlobalDefinitionMap({ typeName, definitionMap }) 48 | }) 49 | } else { 50 | requestRefSet.add(value) 51 | patchGlobalDefinitionMap({ typeName: value, definitionMap }) 52 | } 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /__tests__/step/getUserConfig/initOption.test.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, existsSync } from 'fs' 2 | import { join } from 'path' 3 | import * as prompts from 'prompts' 4 | import { getUserConfig } from 'src/step/getUserConfig' 5 | 6 | let overwrite = false 7 | let exists = false 8 | jest.mock('prompts', () => jest.fn(() => Promise.resolve({ overwrite }))) 9 | 10 | jest.mock('fs', () => ({ 11 | writeFileSync: jest.fn(), 12 | existsSync: jest.fn(() => exists), 13 | })) 14 | 15 | describe('init', () => { 16 | const originLength = process.argv.length 17 | const cwd = process.cwd() 18 | 19 | afterEach(() => { 20 | process.argv.length = originLength 21 | process.chdir(cwd) 22 | }) 23 | 24 | beforeEach(() => { 25 | const examplePath = join(cwd, 'example') 26 | process.chdir(examplePath) 27 | }) 28 | 29 | afterAll(() => { 30 | jest.unmock('prompts') 31 | jest.unmock('fs') 32 | }) 33 | 34 | it('init', async () => { 35 | process.argv.push('-i') 36 | expect(await getUserConfig()).toEqual({ projects: [], tsGearConfigPath: '' }) 37 | process.argv.length = originLength 38 | expect(prompts).not.toHaveBeenCalled() 39 | expect(existsSync).toHaveBeenCalledTimes(1) 40 | expect(writeFileSync).toHaveBeenCalledTimes(1) 41 | 42 | process.argv.push('--init') 43 | expect(await getUserConfig()).toEqual({ projects: [], tsGearConfigPath: '' }) 44 | expect(prompts).not.toHaveBeenCalled() 45 | expect(existsSync).toHaveBeenCalledTimes(2) 46 | expect(writeFileSync).toHaveBeenCalledTimes(2) 47 | 48 | exists = true 49 | expect(await getUserConfig()).toEqual({ projects: [], tsGearConfigPath: '' }) 50 | expect(prompts).toHaveBeenCalledTimes(1) 51 | expect(existsSync).toHaveBeenCalledTimes(3) 52 | expect(writeFileSync).toHaveBeenCalledTimes(2) 53 | 54 | overwrite = true 55 | expect(await getUserConfig()).toEqual({ projects: [], tsGearConfigPath: '' }) 56 | expect(prompts).toHaveBeenCalledTimes(2) 57 | expect(existsSync).toHaveBeenCalledTimes(4) 58 | expect(writeFileSync).toHaveBeenCalledTimes(3) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/step/getUserConfig/index.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import { writeFileSync, existsSync } from 'fs' 3 | import * as prompts from 'prompts' 4 | import type { Project } from '../../type' 5 | import { configFileName } from '../../constant' 6 | import { initConfig } from '../../content/initConfig' 7 | import { warn } from '../../tool/log' 8 | import { getCliOption } from './cliOption' 9 | 10 | /** 11 | * get user config 12 | * from cli option 13 | * */ 14 | export const getUserConfig = async () => { 15 | const cwd = process.cwd() 16 | const cliOption = getCliOption() 17 | const configFilePath = join(cwd, 'src', `${configFileName}.ts`) 18 | if (cliOption.init) { 19 | if (existsSync(configFilePath)) { 20 | const { overwrite } = await prompts({ 21 | type: 'confirm', 22 | name: 'overwrite', 23 | message: `${configFilePath} already exist, overwrite?`, 24 | initial: true, 25 | }) 26 | if (overwrite) { 27 | writeFileSync(configFilePath, initConfig) 28 | } 29 | } else { 30 | writeFileSync(configFilePath, initConfig) 31 | } 32 | return { 33 | projects: [], 34 | tsGearConfigPath: '', 35 | } 36 | } 37 | const tsGearConfigPath = join(cwd, cliOption.config || join('src', configFileName)) 38 | /* eslint-disable @typescript-eslint/no-var-requires,global-require,import/no-dynamic-require */ 39 | const config = require(tsGearConfigPath) 40 | /* eslint-enable @typescript-eslint/no-var-requires,global-require,import/no-dynamic-require */ 41 | let projects = (config.default ? config.default : config) as Project[] 42 | const projectNamesFromCommandLine = cliOption.names 43 | if (projectNamesFromCommandLine.length > 0) { 44 | projects = projects.filter(project => projectNamesFromCommandLine.some(name => name === project.name)) 45 | if (projects.length === 0) { 46 | warn(`your input names "${cliOption.names.join(', ')}" match 0 projects, checkout and retry.`) 47 | } 48 | } 49 | return { 50 | tsGearConfigPath: dirname(tsGearConfigPath), 51 | projects, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /__tests__/tool/genericType.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | hasGenericSymbol, 3 | removeGenericSymbol, 4 | parseGenericNames, 5 | getGenericNameFromNode, 6 | guessGenericTypeName, 7 | } from 'src/tool/genericType' 8 | 9 | describe('process generic type name', () => { 10 | it('hasGenericSymbol', () => { 11 | expect(hasGenericSymbol('A')).toBe(true) 12 | expect(hasGenericSymbol('AB')).toBe(false) 13 | }) 14 | 15 | it('removeGenericSymbol', () => { 16 | expect(removeGenericSymbol('A')).toBe('AB') 17 | expect(removeGenericSymbol('ABC')).toBe('ABC') 18 | }) 19 | 20 | it('parseGenericNames', () => { 21 | const r = parseGenericNames('A,F>>') 22 | // r.forEach(t => console.log(t)) 23 | const [A, B, C, D, G, F, E] = r 24 | expect(A.children!).toHaveLength(1) 25 | expect(A.children![0]).toBe(B) 26 | expect(B.parent).toBe(A) 27 | expect(B.children).toHaveLength(3) 28 | expect(B.children![0]).toBe(C) 29 | expect(B.children![1]).toBe(D) 30 | expect(B.children![2]).toBe(F) 31 | expect(D.parent).toBe(B) 32 | expect(G.parent).toBe(D) 33 | expect(E.parent).toBe(F) 34 | expect(F.parent).toBe(B) 35 | expect(F.children!).toHaveLength(1) 36 | expect(F.children![0]).toBe(E) 37 | }) 38 | 39 | it('getGenericNameFromNode', () => { 40 | const name = 'A,F>>' 41 | const r = parseGenericNames(name) 42 | expect(getGenericNameFromNode(r[0])).toBe(name) 43 | }) 44 | 45 | it('guessGenericTypeName', () => { 46 | const name = 'A>' 47 | const node = parseGenericNames(name) 48 | let guessedName = guessGenericTypeName(node[0], { 49 | A: { 50 | typeName: 'A', 51 | }, 52 | C: { 53 | typeName: 'C', 54 | }, 55 | }) 56 | expect(guessedName).toBe('A') 57 | 58 | guessedName = guessGenericTypeName(node[0], { 59 | A: { 60 | typeName: 'A', 61 | }, 62 | B: { 63 | typeName: 'B', 64 | }, 65 | C: { 66 | typeName: 'C', 67 | }, 68 | }) 69 | expect(guessedName).toBe('A>') 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /example/petProject/src/tsg.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-relative-packages */ 2 | import type { Options } from 'prettier' 3 | import type { Project } from '../../../src/type' 4 | 5 | const prettierConfig: Options = { 6 | semi: false, 7 | useTabs: false, 8 | singleQuote: true, 9 | trailingComma: 'all', 10 | // parser: 'babel', 11 | } 12 | 13 | const projects: Project[] = [ 14 | { 15 | name: 'pet', 16 | dest: 'service', 17 | source: '../../fixture/pet.json', 18 | // source: 'http://petstore.swagger.io/v2/swagger.json', 19 | importRequesterStatement: 'import { requester } from "../../requester"', 20 | simplifyRequestOption: true, 21 | EOL: '\n', 22 | withBasePath: true, 23 | }, 24 | { 25 | name: 'petv3', 26 | dest: 'service', 27 | source: 'https://petstore3.swagger.io/api/v3/openapi.json', 28 | importRequesterStatement: 'import { requester } from "../../requester"', 29 | // simplifyRequestOption: true, 30 | shouldGenerateMock: true, 31 | EOL: '\n', 32 | withBasePath: true, 33 | requestOptionUnionType: 'RequestInit', 34 | }, 35 | { 36 | name: 'projectE', 37 | dest: 'service', 38 | source: '../../fixture/projectE.json', 39 | importRequesterStatement: 'import { requester } from "fffxx"', 40 | EOL: '\n', 41 | simplifyRequestOption: true, 42 | // translationEngine: 'baidu', 43 | // translateIntervalPerWord: 2000, 44 | }, 45 | { 46 | name: 'v3', 47 | dest: 'service', 48 | source: '../../fixture/openapiv3.json', 49 | keepGeneric: true, 50 | importRequesterStatement: 'import { requester } from "fffxx"', 51 | shouldGenerateMock: true, 52 | useCache: false, 53 | prettierConfig, 54 | EOL: '\n', 55 | }, 56 | { 57 | name: 'nullable', 58 | dest: 'service', 59 | source: '../../fixture/nullable.json', 60 | importRequesterStatement: 'import { requester } from "../../requester"', 61 | nullableFalseAsRequired: true, 62 | // prettierConfig, 63 | // shouldExportRequestOptionType: true, 64 | // shouldExportResponseType: true, 65 | // shouldGenerateMock: true, 66 | // useCache: false, 67 | }, 68 | ] 69 | 70 | export default projects 71 | -------------------------------------------------------------------------------- /src/translation/utils/make-request/browser.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions } from './types' 2 | import { ParsedUrlQueryInput } from 'querystring' 3 | import getError, { ERROR_CODE } from '../error' 4 | 5 | /** 6 | * 将对象转换成查询字符串 7 | * TODO: 使用 noshjs 中的方法 8 | */ 9 | function qs(obj?: ParsedUrlQueryInput) { 10 | if (!obj) return '' 11 | const r = [] 12 | for (let key in obj) { 13 | const v = [].concat(obj[key] as never) 14 | r.push(...v.map(valStr => `${key}=${encodeURIComponent(valStr)}`)) 15 | } 16 | return r.join('&') 17 | } 18 | 19 | export default function(options: RequestOptions): Promise { 20 | const xhr = new XMLHttpRequest() 21 | const urlObj = new URL(options.url) 22 | 23 | urlObj.search += (urlObj.search ? '&' : '?') + qs(options.query) 24 | 25 | const { method = 'get' } = options 26 | 27 | xhr.open(method, urlObj.toString()) 28 | 29 | let body: string 30 | 31 | if (method === 'post') { 32 | switch (options.type) { 33 | case 'form': 34 | xhr.setRequestHeader( 35 | 'Content-Type', 36 | 'application/x-www-form-urlencoded; charset=UTF-8' 37 | ) 38 | body = qs(options.body) 39 | break 40 | 41 | case 'json': 42 | default: 43 | xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8') 44 | body = JSON.stringify(options.body) 45 | break 46 | } 47 | } 48 | 49 | const { headers } = options 50 | if (headers) { 51 | for (let header in headers) { 52 | xhr.setRequestHeader(header, headers[header]) 53 | } 54 | } 55 | 56 | xhr.responseType = options.responseType || 'json' 57 | 58 | return new Promise((resolve, reject) => { 59 | xhr.onload = () => { 60 | // 如果 responseType 设为 json 但服务器返回的数据无法解析成 json, 61 | // 则 response 是 null,其他无法解析的情况也是同理。 62 | // 另外,responseText 只能在 responseType 是 '' 或 'text' 访问。 63 | if (xhr.status !== 200 || xhr.response === null) { 64 | reject(getError(ERROR_CODE.API_SERVER_ERROR)) 65 | return 66 | } 67 | resolve(xhr.response) 68 | } 69 | 70 | xhr.onerror = () => { 71 | reject(getError(ERROR_CODE.NETWORK_ERROR, '网络错误')) 72 | } 73 | 74 | xhr.send(body) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /src/tool/genericType.ts: -------------------------------------------------------------------------------- 1 | import type { GenericNameNode, DefinitionMap } from '../type' 2 | 3 | export const hasGenericSymbol = (name: string) => { 4 | return name.includes('<') 5 | } 6 | 7 | export const removeGenericSymbol = (name: string) => name.replace(/<|>|,/g, '') 8 | 9 | /** process generic type name */ 10 | export const parseGenericNames = (name: string) => { 11 | let currentNode: GenericNameNode = { name: '', level: 0 } 12 | let parentNode: GenericNameNode | undefined = currentNode 13 | let nestLevel = 0 14 | const result: GenericNameNode[] = [currentNode] 15 | for (let i = 0; i < name.length; i += 1) { 16 | const c = name[i] 17 | if (c === '<') { 18 | currentNode.level = nestLevel 19 | parentNode = currentNode 20 | currentNode = { name: '', level: nestLevel, parent: parentNode } 21 | parentNode.children = parentNode.children || [] 22 | parentNode.children.push(currentNode) 23 | nestLevel += 1 24 | result.push(currentNode) 25 | } else if (c === '>') { 26 | parentNode = parentNode!.parent 27 | if (currentNode.name) { 28 | currentNode.level = nestLevel 29 | currentNode = { name: '', level: nestLevel, parent: parentNode } 30 | } 31 | nestLevel -= 1 32 | } else if (c === ',') { 33 | currentNode = { name: '', level: nestLevel, parent: parentNode } 34 | parentNode!.children!.push(currentNode) 35 | result.push(currentNode) 36 | } else { 37 | currentNode.name += c 38 | } 39 | } 40 | return result 41 | } 42 | 43 | /** from generic name node to name string 44 | * reverse of parseGenericNames 45 | * */ 46 | export const getGenericNameFromNode = (node: GenericNameNode): string => { 47 | const { name, children } = node 48 | if (!children) { 49 | return name 50 | } 51 | return `${name}<${children.map(c => getGenericNameFromNode(c)).join(',')}>` 52 | } 53 | 54 | /** try hard to keep every nest level generic name 55 | * if exist in definitionMap keep it 56 | * else remove generic symbol: <> 57 | * */ 58 | export const guessGenericTypeName = (node: GenericNameNode, definitionMap: DefinitionMap): string => { 59 | const name = getGenericNameFromNode(node) 60 | if (!(node.name in definitionMap)) { 61 | return removeGenericSymbol(name) 62 | } 63 | if (!node.children) { 64 | return node.name 65 | } 66 | return `${node.name}<${node.children.map(c => guessGenericTypeName(c, definitionMap)).join(',')}>` 67 | } 68 | -------------------------------------------------------------------------------- /src/tool/enumType.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import type { Spec } from 'swagger-schema-official' 3 | import { get, remove, upperFirst } from 'lodash' 4 | import { httpMethods } from '../type' 5 | import { cleanName } from './cleanName' 6 | import { tsNodeToString } from './tsNodeToString' 7 | 8 | const filterPaths = ['definitions', 'properties', 'parameters', 'responses', 'paths'] 9 | 10 | /** use traverse spec path to generate an available enum type name */ 11 | export const generateEnumName = (traversePath: string[], spec: Spec): string => { 12 | const path = [...traversePath] 13 | /** 最后一层是'enum',该层没用,所以弹出一层 */ 14 | path.pop() 15 | if (path[0] === 'paths') { 16 | /** 如果最后一层是schema,还需要弹出一层 */ 17 | if (path[path.length - 1] === 'schema') { 18 | path.pop() 19 | } 20 | if (path.includes('parameters')) { 21 | const parameterNode = get(spec, path) 22 | if (parameterNode.name) { 23 | const parameterIndex = path.findIndex(p => p === 'parameters') 24 | path[parameterIndex] = parameterNode.name 25 | } 26 | } 27 | path.find((p, index) => { 28 | if (httpMethods.includes(p as any)) { 29 | const requestPath = path[index - 1] 30 | path[index - 1] = p 31 | path[index] = requestPath 32 | return true 33 | } 34 | return false 35 | }) 36 | } 37 | remove(path, p => filterPaths.includes(p)) 38 | return path.map(p => upperFirst(cleanName(p, false))).join('') 39 | } 40 | 41 | /** convert enum member to enum type 42 | * @example 43 | * `[1,2,3]` => `1 | 2 | 3` 44 | * `['a', 'b', 'c']` => `'a' | 'b' | 'c'` 45 | * */ 46 | export const generateEnumTypescriptContent = (name: string, value: any[]) => { 47 | value = value.map(v => (v === null ? 'null' : v)) 48 | const { factory } = ts 49 | const contentNode = factory.createUnionTypeNode( 50 | value.map(v => 51 | typeof v === 'number' 52 | ? factory.createLiteralTypeNode( 53 | v < 0 54 | ? factory.createPrefixUnaryExpression( 55 | ts.SyntaxKind.MinusToken, 56 | ts.factory.createNumericLiteral(Math.abs(v)), 57 | ) 58 | : factory.createNumericLiteral(v), 59 | ) 60 | : factory.createLiteralTypeNode(factory.createStringLiteral(v)), 61 | ), 62 | ) 63 | const node = factory.createTypeAliasDeclaration( 64 | [factory.createModifier(ts.SyntaxKind.ExportKeyword)], 65 | name, 66 | undefined, 67 | contentNode, 68 | ) 69 | return tsNodeToString(node) 70 | } 71 | -------------------------------------------------------------------------------- /src/step/generateRequestContent/generateRequestOptionType.ts: -------------------------------------------------------------------------------- 1 | import type { Parameter, Reference } from 'swagger-schema-official' 2 | import { isEmpty, upperFirst } from 'lodash' 3 | import type { RequestParameterPosition, Project } from '../../type' 4 | import { schemaToTypescript } from '../../tool/schemaToTypescript' 5 | import { sow, harvest } from '../../source' 6 | import { assembleRequestParam } from './assembleRequestParam' 7 | 8 | /** 9 | * @param functionName request function parameter interface name 10 | * @param parameters swagger request parameters 11 | * */ 12 | export const generateRequestOptionType = ( 13 | functionName: string, 14 | parameters: Array, 15 | project: Project, 16 | ) => { 17 | const source = sow() 18 | const parameterTypeName = `${upperFirst(functionName)}Option` 19 | const assembledParameters = assembleRequestParam(parameters, project) 20 | let parameterRequired = false 21 | const positionSet = new Set( 22 | Object.getOwnPropertyNames(assembledParameters), 23 | ) as unknown as Set 24 | if (project.simplifyRequestOption && positionSet.size === 1) { 25 | const param = assembledParameters[Array.from(positionSet)[0]]! 26 | source.addTypeAlias({ 27 | isExported: project.shouldExportRequestOptionType === undefined || !!project.shouldExportRequestOptionType, 28 | name: parameterTypeName, 29 | type: schemaToTypescript(param, project), 30 | }) 31 | } else { 32 | ;(Object.getOwnPropertyNames(assembledParameters) as RequestParameterPosition[]).forEach(position => { 33 | const param = assembledParameters[position]! 34 | if (!parameterRequired) { 35 | parameterRequired = !isEmpty(param.required) 36 | } 37 | const inter = source.addInterface({ 38 | isExported: project.shouldExportRequestOptionType === undefined || !!project.shouldExportRequestOptionType, 39 | name: parameterTypeName, 40 | docs: [`@description request parameter type for ${functionName}`], 41 | }) 42 | let hasQuestionToken = false 43 | if (project.shouldForceSkipRequestHeaderOption && position === 'header') { 44 | hasQuestionToken = true 45 | } else { 46 | hasQuestionToken = isEmpty(param.required) 47 | } 48 | inter.addProperty({ 49 | name: position, 50 | type: schemaToTypescript(param, project), 51 | hasQuestionToken, 52 | docs: param.docs, 53 | }) 54 | }) 55 | } 56 | return { 57 | parameterTypeName, 58 | parameterTypeContent: harvest(source), 59 | parameterRequired, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/petProject/src/service/projectE/definition.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export type ReplyVOPageVOFollowRecordVOPetTypeListItems = "A" | "B"; 6 | export type ReplyVOPageVOFollowRecordVOPetTypeList = 7 | ReplyVOPageVOFollowRecordVOPetTypeListItems; 8 | export type BodyBuilder = any; 9 | 10 | export interface Data { 11 | /** 12 | * @description 13 | * 响应代码【0正确,非0错误】 14 | * @example 15 | * 000000 16 | */ 17 | code: string; 18 | /** 19 | * @description 20 | * 返回数据 21 | */ 22 | data?: PageVO; 23 | /** 24 | * @description 25 | * 结果描述 26 | * @example 27 | * success 28 | */ 29 | message: string; 30 | } 31 | 32 | export type PageVOListVO = PageVO; 33 | export type ReplyVOPageVOFollowRecordVO = ReplyVO>; 34 | export interface PageVO { 35 | /** 36 | * @description 37 | * 数据列表 38 | */ 39 | entities: Array; 40 | /** 41 | * @description 42 | * 总条数 43 | * @format int32 44 | * @example 45 | * 100 46 | */ 47 | entityCount: number; 48 | /** 49 | * @description 50 | * 开始序号 51 | * @format int32 52 | * @example 53 | * 0 54 | */ 55 | firstEntityIndex: number; 56 | /** 57 | * @description 58 | * 结束序号 59 | * @format int32 60 | * @example 61 | * 10 62 | */ 63 | lastEntityIndex: number; 64 | /** 65 | * @description 66 | * 总页数 67 | * @format int32 68 | * @example 69 | * 10 70 | */ 71 | pageCount: number; 72 | /** 73 | * @description 74 | * 页码 75 | * @format int32 76 | * @example 77 | * 1 78 | */ 79 | pageNo: number; 80 | /** 81 | * @description 82 | * 每页条数 83 | * @format int32 84 | * @example 85 | * 10 86 | */ 87 | pageSize: number; 88 | } 89 | 90 | export type ListVO = any; 91 | export interface ReplyVO { 92 | /** 93 | * @description 94 | * petTypeList 95 | */ 96 | petTypeList?: ReplyVOPageVOFollowRecordVOPetTypeList[]; 97 | /** 98 | * @description 99 | * 响应代码【0正确,非0错误】 100 | * @example 101 | * 000000 102 | */ 103 | code: string; 104 | /** 105 | * @description 106 | * 返回数据 107 | */ 108 | data?: PageVOFollowRecordVO; 109 | /** 110 | * @description 111 | * 结果描述 112 | * @example 113 | * success 114 | */ 115 | message: string; 116 | } 117 | 118 | export type FollowRecordVO = any; 119 | export type ReplyVOInt = any; 120 | export type PageVOFollowRecordVO = any; 121 | -------------------------------------------------------------------------------- /src/translation/api/baidu/state.ts: -------------------------------------------------------------------------------- 1 | import 'cross-fetch/polyfill' 2 | import type { StringObject } from '../../types' 3 | import invert from '../../utils/invert' 4 | 5 | export const root = 'https://fanyi.baidu.com' 6 | // fetch a new baiduid 7 | export const Cookie = { 8 | value: '', 9 | } 10 | 11 | export const fetchCookie = async () => { 12 | if (Cookie.value) { 13 | return Cookie.value 14 | } 15 | return fetch('https://www.baidu.com/', { 16 | headers: { 17 | accept: 18 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 19 | 'Accept-Encoding': 'gzip, deflate, br', 20 | 'accept-language': 'zh-CN,zh;q=0.9', 21 | 'sec-fetch-dest': 'document', 22 | 'sec-fetch-mode': 'navigate', 23 | 'sec-fetch-site': 'none', 24 | 'sec-fetch-user': '?1', 25 | 'upgrade-insecure-requests': '1', 26 | Host: 'www.baidu.com', 27 | 'sec-ch-ua': '.Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103', 28 | 'sec-ch-ua-mobile': '?0', 29 | 'sec-ch-ua-platform': 'Windows', 30 | 'Sec-Fetch-Dest': 'document', 31 | 'Sec-Fetch-Mode': 'navigate', 32 | 'Sec-Fetch-Site': 'none', 33 | 'Sec-Fetch-User': '?1', 34 | 'Upgrade-Insecure-Requests': '1', 35 | 'User-Agent': 36 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 37 | }, 38 | referrerPolicy: 'strict-origin-when-cross-origin', 39 | body: null, 40 | method: 'GET', 41 | }).then(res => { 42 | const cookies = res.headers.get('set-cookie') 43 | const id = (cookies || '').split('; ').find(c => c.match('BAIDUID=')) 44 | if (id) { 45 | const arr = id.split(', ') 46 | if (arr.length > 1) { 47 | ;[, Cookie.value] = id.split(', ') 48 | } else { 49 | Cookie.value = id 50 | } 51 | } 52 | return Cookie.value 53 | }) 54 | } 55 | 56 | /** 57 | * 百度支持的语种到百度自定义的语种名的映射,去掉了文言文。 58 | * @see http://api.fanyi.baidu.com/api/trans/product/apidoc#languageList 59 | */ 60 | export const standard2custom: StringObject = { 61 | en: 'en', 62 | th: 'th', 63 | ru: 'ru', 64 | pt: 'pt', 65 | el: 'el', 66 | nl: 'nl', 67 | pl: 'pl', 68 | bg: 'bul', 69 | et: 'est', 70 | da: 'dan', 71 | fi: 'fin', 72 | cs: 'cs', 73 | ro: 'rom', 74 | sl: 'slo', 75 | sv: 'swe', 76 | hu: 'hu', 77 | de: 'de', 78 | it: 'it', 79 | 'zh-CN': 'zh', 80 | 'zh-TW': 'cht', 81 | // 'zh-HK': 'yue', 82 | ja: 'jp', 83 | ko: 'kor', 84 | es: 'spa', 85 | fr: 'fra', 86 | ar: 'ara', 87 | } 88 | 89 | /** 百度自定义的语种名到标准语种名的映射 */ 90 | export const custom2standard = invert(standard2custom) 91 | -------------------------------------------------------------------------------- /src/translation/utils/make-request/index.ts: -------------------------------------------------------------------------------- 1 | import { request as requestHTTP } from 'http' 2 | import { request as requestHTTPs } from 'https' 3 | import { parse } from 'url' 4 | import { stringify } from 'querystring' 5 | import type { StringObject } from '../../types' 6 | import getError, { ERROR_CODE } from '../error' 7 | import type { RequestOptions } from './types' 8 | 9 | require('tls').DEFAULT_ECDH_CURVE = 'auto' 10 | 11 | export default function makerequest(options: RequestOptions): Promise { 12 | const { method = 'get' } = options 13 | const urlObj = parse(options.url, true) 14 | const qs = stringify(Object.assign(urlObj.query, options.query)) 15 | 16 | const headers: StringObject = { 17 | 'User-Agent': 18 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 19 | } 20 | 21 | let body: string 22 | 23 | if (method === 'post') { 24 | switch (options.type) { 25 | case 'form': 26 | headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' 27 | body = stringify(options.body) 28 | break 29 | 30 | case 'json': 31 | default: 32 | headers['Content-Type'] = 'application/json; charset=UTF-8' 33 | body = JSON.stringify(options.body) 34 | break 35 | } 36 | 37 | headers['Content-Length'] = String(Buffer.byteLength(body)) 38 | } 39 | 40 | Object.assign(headers, options.headers) 41 | 42 | const httpOptions = { 43 | hostname: urlObj.hostname, 44 | method, 45 | path: `${urlObj.pathname}?${qs}`, 46 | headers, 47 | auth: urlObj.auth, 48 | } 49 | 50 | const responseType = options.responseType || 'json' 51 | 52 | return new Promise((resolve, reject) => { 53 | const req = (urlObj.protocol === 'https:' ? requestHTTPs : requestHTTP)(httpOptions, res => { 54 | // 内置的翻译接口都以 200 作为响应码,所以不是 200 的一律视为错误 55 | if (res.statusCode !== 200) { 56 | reject(getError(ERROR_CODE.API_SERVER_ERROR)) 57 | return 58 | } 59 | 60 | res.setEncoding('utf8') 61 | let rawData = '' 62 | res.on('data', (chunk: string) => { 63 | rawData += chunk 64 | }) 65 | res.on('end', () => { 66 | // Node.js 端只支持 json,其余都作为 text 处理 67 | if (responseType === 'json') { 68 | try { 69 | resolve(JSON.parse(rawData)) 70 | } catch (e) { 71 | // 与浏览器端保持一致,在无法解析成 json 时报错 72 | reject(getError(ERROR_CODE.API_SERVER_ERROR)) 73 | } 74 | } else { 75 | resolve(rawData) 76 | } 77 | }) 78 | }) 79 | 80 | req.on('error', e => { 81 | reject(getError(ERROR_CODE.NETWORK_ERROR, e.message)) 82 | }) 83 | 84 | req.end(body) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /src/step/generateRequestContent/assembleRequestParam.ts: -------------------------------------------------------------------------------- 1 | import type { Parameter, BodyParameter, Reference } from 'swagger-schema-official' 2 | import type { ParameterPositionMap, Project } from '../../type' 3 | import { isReference } from '../../tool/isReference' 4 | import { assembleDoc } from '../../tool/assembleDoc' 5 | 6 | /** assemble parameters to type ParameterPositionMap 7 | * 8 | * NOTD: body has a useless nest 'body' property, will generate type as 9 | * { body: { body: Pet } } 10 | * remove it to generate as below 11 | * { body: Pet } 12 | * */ 13 | export const assembleRequestParam = (parameters: Array, project: Project) => { 14 | const bodyParamCount = parameters.filter(p => 'in' in p && p.in === 'body').length 15 | return parameters.reduce((map, parameter) => { 16 | // ? TODO never meet this case 17 | if (isReference(parameter)) { 18 | return map 19 | } 20 | if (parameter.in in map) { 21 | const positionParameter = map[parameter.in]! 22 | // once required, then required 23 | if (parameter.required && !positionParameter.required.includes(parameter.name)) { 24 | positionParameter.required.push(parameter.name) 25 | } 26 | if (positionParameter.properties) { 27 | positionParameter.properties![parameter.name] = parameter 28 | } 29 | /** 30 | * 为兼容openapiv3,在src/step/assembleSchemaToGlobal 31 | * 将formData的schema也放进来,与body的格式一致 32 | * */ 33 | } else if ( 34 | (parameter.in === 'formData' || (parameter.in === 'body' && parameter.name === 'body')) && 35 | (parameter as BodyParameter).schema 36 | ) { 37 | /** remove body nest structure */ 38 | map.body = { 39 | type: 'object', 40 | name: 'body', 41 | required: parameter.required ? [parameter.name] : [], 42 | schema: (parameter as BodyParameter).schema, 43 | docs: assembleDoc(parameter), 44 | } 45 | } else if ( 46 | (parameter.in === 'formData' || 47 | (parameter.in === 'body' && project.stripBodyPropWhenOnlyOneBodyProp && bodyParamCount === 1)) && 48 | (parameter as BodyParameter).schema 49 | ) { 50 | /** remove body nest structure */ 51 | map.body = { 52 | type: 'object', 53 | name: 'body', 54 | required: parameter.required ? [parameter.name] : [], 55 | schema: (parameter as BodyParameter).schema, 56 | docs: assembleDoc(parameter), 57 | } 58 | } else { 59 | map[parameter.in] = { 60 | type: 'object', 61 | name: parameter.in, 62 | required: parameter.required ? [parameter.name] : [], 63 | properties: { 64 | [parameter.name]: parameter, 65 | }, 66 | docs: assembleDoc(parameter), 67 | } 68 | } 69 | return map 70 | }, {}) 71 | } 72 | -------------------------------------------------------------------------------- /src/tool/assembleDoc.ts: -------------------------------------------------------------------------------- 1 | import type { Schema, Operation, Parameter } from 'swagger-schema-official' 2 | import type { SchemaObject } from 'openapi3-ts' 3 | import { config } from '../constant' 4 | 5 | /** add many possible properties to doc */ 6 | export const assembleDoc = (schema: Schema | Operation | Parameter | SchemaObject) => { 7 | const { EOL } = config 8 | if (typeof schema !== 'object') { 9 | return undefined 10 | } 11 | const docs: string[] = [] 12 | const hasDescription = 'description' in schema || 'summary' in schema 13 | if (hasDescription) { 14 | docs.push('@description') 15 | if ('description' in schema && schema.description) { 16 | docs.push(` ${String(schema.description)}`) 17 | } 18 | if ('summary' in schema && schema.summary) { 19 | docs.push(` ${String(schema.summary)}`) 20 | } 21 | } 22 | if ('format' in schema) { 23 | docs.push(`@format ${schema.format}`) 24 | } 25 | if ('tags' in schema && schema.tags) { 26 | docs.push(`@tags ${schema.tags.join()}`) 27 | } 28 | if ('default' in schema) { 29 | docs.push(`@default ${schema.default}`) 30 | } 31 | if ('produces' in schema) { 32 | docs.push(`@produces ${schema.produces}`) 33 | } 34 | if ('consumes' in schema) { 35 | docs.push(`@consumes ${schema.consumes}`) 36 | } 37 | const hasExample = 'example' in schema || 'readOnly' in schema || 'writeOnly' in schema 38 | const v3Schema = schema as SchemaObject 39 | if (hasExample) { 40 | docs.push(`@example`) 41 | if ('example' in schema) { 42 | docs.push(` ${schema.example}`) 43 | } 44 | if ('readOnly' in schema) { 45 | docs.push('@readonly') 46 | } 47 | if ('writeOnly' in schema) { 48 | docs.push('@writeonly') 49 | } 50 | } 51 | if ('deprecated' in schema && schema.deprecated) { 52 | docs.push('@deprecated') 53 | } 54 | if (v3Schema.not) { 55 | docs.push(`@not ${v3Schema.not}`) 56 | } 57 | if (v3Schema.anyOf) { 58 | docs.push(`@anyOf ${v3Schema.anyOf}`) 59 | } 60 | if (v3Schema.oneOf) { 61 | docs.push(`@oneOf ${v3Schema.oneOf}`) 62 | } 63 | if (docs.length === 0) { 64 | return undefined 65 | } 66 | // openapi v2 generate external properties starts with "x-" 67 | const keys = Object.getOwnPropertyNames(schema) as (keyof typeof schema)[] 68 | if (keys.some(k => k.startsWith('x-'))) { 69 | keys.forEach(k => { 70 | if (k.startsWith('x-')) { 71 | docs.push(`@${k} ${JSON.stringify(schema[k])}`) 72 | } 73 | }) 74 | } 75 | // openapi v3 generate "extensions" property 76 | if (v3Schema.extensions && typeof v3Schema.extensions === 'object') { 77 | Object.getOwnPropertyNames(v3Schema.extensions).forEach(k => { 78 | docs.push(`@${k} ${JSON.stringify(v3Schema.extensions[k])}`) 79 | }) 80 | } 81 | return [ 82 | docs 83 | .filter(Boolean) 84 | /** replace invalid comment charator */ 85 | .map(doc => doc.replace(/\*\/\*?/, '*')) 86 | .join(EOL), 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /src/step/generateRequestContent/generateResponseType.ts: -------------------------------------------------------------------------------- 1 | import { upperFirst } from 'lodash' 2 | import type { Response, Reference } from 'swagger-schema-official' 3 | import type { ResponseObject } from 'openapi3-ts' 4 | import { sow, harvest } from '../../source' 5 | import { schemaToTypescript } from '../../tool/schemaToTypescript' 6 | import { assembleDoc } from '../../tool/assembleDoc' 7 | import { getSchemaDeep } from '../../tool/getSchemaDeep' 8 | import type { AssembleResponse, Project } from '../../type' 9 | 10 | /** 11 | * when has responses spec, get an interface type and use the first 2xx member as successType 12 | * when has not responses, use any as successType 13 | * */ 14 | export const generateResponseType = ( 15 | functionName: string, 16 | responses: { [responseName: string]: Response | Reference }, 17 | project: Project, 18 | ): AssembleResponse => { 19 | const responseTypeName = `${upperFirst(functionName)}Response` 20 | 21 | const shouldExport = project.shouldExportResponseType === undefined || !!project.shouldExportResponseType 22 | 23 | // use first 2xx response type as success response type 24 | let successTypeContent = `${shouldExport ? 'export' : ''} type ${responseTypeName}Success = any` 25 | let responseTypeContent = `${shouldExport ? 'export' : ''} type ${responseTypeName} = any` 26 | const successTypeName = `${responseTypeName}Success` 27 | const responseStatuses = Object.getOwnPropertyNames(responses).sort() 28 | if (responseStatuses.length > 0) { 29 | const source = sow() 30 | const inter = source.addInterface({ 31 | name: responseTypeName, 32 | isExported: shouldExport, 33 | docs: [`@description response type for ${functionName}`], 34 | }) 35 | responseStatuses.forEach(status => { 36 | const statusRes = responses[status] 37 | /** 兼容openapiv3 */ 38 | if ('content' in statusRes) { 39 | const res = statusRes as ResponseObject 40 | res.schema = getSchemaDeep((statusRes as ResponseObject).content) 41 | } 42 | inter.addProperty({ 43 | name: String(status), 44 | type: schemaToTypescript(responses[status], project), 45 | docs: assembleDoc(responses[status]), 46 | }) 47 | }) 48 | responseTypeContent = harvest(source) 49 | const firstResponseStatus = responseStatuses[0] 50 | if (firstResponseStatus.startsWith('2') || firstResponseStatus === 'default') { 51 | if (Number.isNaN(Number(firstResponseStatus))) { 52 | successTypeContent = `${ 53 | shouldExport ? 'export' : '' 54 | } type ${responseTypeName}Success = ${responseTypeName}['${firstResponseStatus}']` 55 | } else { 56 | successTypeContent = `${ 57 | shouldExport ? 'export' : '' 58 | } type ${responseTypeName}Success = ${responseTypeName}[${firstResponseStatus}]` 59 | } 60 | } 61 | } 62 | return { 63 | responseTypeContent, 64 | successTypeContent, 65 | responseTypeName, 66 | successTypeName, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/step/prepareWriteContent.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { targetFileNames, config } from '../constant' 3 | import { requester } from '../content/requester' 4 | import type { Project, PrepareToWrite } from '../type' 5 | import { getGlobal } from '../projectGlobalVariable' 6 | import { warningComment } from '../content/warningComment' 7 | import { projectIndex } from '../content/projectIndex' 8 | import { importAllDefinition } from './importAllDefinition' 9 | 10 | /** 11 | * gather global typescript content 12 | */ 13 | export const prepareWriteContent = (project: Project, tsGearConfigPath: string): PrepareToWrite => { 14 | const { definitionMap, requestMap, enumMap } = getGlobal(project) 15 | const { EOL } = config 16 | const dest = join(tsGearConfigPath, project.dest, project.name) 17 | 18 | const definitionTypeNameSet = new Set() 19 | const definitionContent = Object.getOwnPropertyNames(definitionMap) 20 | .map(name => { 21 | // prevent repeat definition 22 | const typeName = definitionMap[name].typeName! 23 | if (definitionTypeNameSet.has(typeName!)) { 24 | return '' 25 | } 26 | definitionTypeNameSet.add(typeName) 27 | return definitionMap[name].typescriptContent 28 | }) 29 | .join(EOL) 30 | const enumContent = Object.values(enumMap) 31 | .map(({ typescriptContent }) => typescriptContent) 32 | .join(EOL) 33 | const definitionFile = join(dest, targetFileNames.definition) 34 | const definitionFileContent = [warningComment(EOL as string), enumContent, definitionContent].join(EOL) 35 | 36 | const requestContent = Object.getOwnPropertyNames(requestMap) 37 | .map(name => requestMap[name].typescriptContent) 38 | .join(EOL) 39 | const requesterResult = requester(project) 40 | const requestFile = join(dest, targetFileNames.request) 41 | const requestFileContent = [ 42 | warningComment(EOL as string), 43 | requesterResult.import, 44 | importAllDefinition(project), 45 | requesterResult.code, 46 | requestContent, 47 | ].join(EOL) 48 | let mockRequestFile = '' 49 | let mockRequestFileContent = '' 50 | if (project.shouldGenerateMock) { 51 | const mockRequestContent = Object.getOwnPropertyNames(requestMap) 52 | .map(name => requestMap[name].mockTypescriptContent) 53 | .join(EOL) 54 | mockRequestFile = join(dest, targetFileNames.mockRequest) 55 | mockRequestFileContent = [ 56 | warningComment(EOL as string), 57 | requesterResult.import, 58 | importAllDefinition(project), 59 | requesterResult.code, 60 | mockRequestContent, 61 | ].join(EOL) 62 | } 63 | 64 | const indexFile = join(dest, targetFileNames.index) 65 | const indexFileContent = [warningComment(EOL as string), projectIndex()].join(EOL) 66 | 67 | return { 68 | requestFile, 69 | requestFileContent, 70 | definitionFile, 71 | definitionFileContent, 72 | mockRequestFile, 73 | mockRequestFileContent, 74 | indexFile, 75 | indexFileContent, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | import * as step from './step' 2 | import { restore } from './projectGlobalVariable' 3 | import type { Project } from './type' 4 | import { info } from './tool/log' 5 | 6 | /** 7 | * run step by step 8 | * sequence could not be changed 9 | * every step depends on the pre step 10 | * */ 11 | export const processProject = async (project: Project, tsGearConfigPath: string): Promise => { 12 | step.processEOL(project) 13 | step.prepareProjectDirectory(project, tsGearConfigPath) 14 | const spec = await step.fetchOpenapiData(project, tsGearConfigPath) 15 | if (project.useCache && step.checkCache(project, spec)) { 16 | info( 17 | `cache hit, skip regenerate project(${project.name}), add "useCache: false" to your project in "tsg.config.ts" to disable cache`, 18 | ) 19 | return 20 | } 21 | 22 | if (project.translationEngine) { 23 | await step.translateSchema(spec, project) 24 | } 25 | const keepGeneric = project.keepGeneric !== false 26 | step.cleanRefAndDefinitionName(spec, keepGeneric) 27 | step.assembleSchemaToGlobal(spec, project) 28 | if (keepGeneric) { 29 | step.parseGenericType(project) 30 | } 31 | step.collectRefsInRequestAndPatchDefinition(project) 32 | step.generateDefinitionContent(project) 33 | step.generateRequestContent(spec, project) 34 | if (project.shouldGenerateMock) { 35 | step.generateMockRequestContent(spec, project) 36 | } 37 | const writeResult = step.prepareWriteContent(project, tsGearConfigPath) 38 | if (project.hooks?.beforeWriteTs) { 39 | await project.hooks?.beforeWriteTs({ 40 | project, 41 | ...writeResult, 42 | }) 43 | } 44 | step.writeProject(project, writeResult) 45 | 46 | if (project.hooks?.afterWriteTs) { 47 | await project.hooks?.afterWriteTs({ 48 | project, 49 | ...writeResult, 50 | }) 51 | } 52 | 53 | if (project.transformJS) { 54 | step.toJS(project, tsGearConfigPath) 55 | } 56 | 57 | restore(project) 58 | } 59 | 60 | /** 61 | * run from command line 62 | * */ 63 | export const runByCommand = async (): Promise => { 64 | const { projects, tsGearConfigPath } = await step.getUserConfig() 65 | // const shouldSerial = projects.some(k => k.nullableFalseAsRequired) 66 | // if (shouldSerial) { 67 | // await projects.reduce((p, project) => p.then(() => processProject(project, tsGearConfigPath)), Promise.resolve()) 68 | // } else { 69 | await Promise.all(projects.map(project => processProject(project, tsGearConfigPath))) 70 | // } 71 | } 72 | 73 | /** 74 | * same as runByCommand 75 | * should be used by nodejs env call 76 | * */ 77 | export const run = async ({ projects, appPath }: { projects: Project[]; appPath: string }): Promise => { 78 | // const shouldSerial = projects.some(k => k.nullableFalseAsRequired) 79 | // if (shouldSerial) { 80 | // await projects.reduce((p, project) => p.then(() => processProject(project, appPath)), Promise.resolve()) 81 | // } else { 82 | await Promise.all(projects.map(project => processProject(project, appPath))) 83 | // } 84 | } 85 | -------------------------------------------------------------------------------- /example/petProject/src/service/projectE/request.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | import { requester as requester } from "../../requester"; 6 | import type { ReplyVOInt, Data } from "./definition"; 7 | 8 | /** @description request parameter type for deleteApiDataboardBoardEs */ 9 | export interface DeleteApiDataboardBoardEsOption { 10 | /** 11 | * @description 12 | * 索引数组 13 | */ 14 | body?: Array; 15 | } 16 | 17 | /** @description response type for deleteApiDataboardBoardEs */ 18 | export interface DeleteApiDataboardBoardEsResponse { 19 | /** 20 | * @description 21 | * OK 22 | */ 23 | 200: ReplyVOInt; 24 | /** 25 | * @description 26 | * No Content 27 | */ 28 | 204: any; 29 | /** 30 | * @description 31 | * Unauthorized 32 | */ 33 | 401: any; 34 | /** 35 | * @description 36 | * Forbidden 37 | */ 38 | 403: any; 39 | } 40 | 41 | export type DeleteApiDataboardBoardEsResponseSuccess = 42 | DeleteApiDataboardBoardEsResponse[200]; 43 | /** 44 | * @description 45 | * 删除索引 46 | * @tags Es 47 | * @produces * 48 | */ 49 | export const deleteApiDataboardBoardEs = /* #__PURE__ */ (() => { 50 | const method = "delete"; 51 | const url = "/api/databoard/board/es"; 52 | function request( 53 | option?: DeleteApiDataboardBoardEsOption 54 | ): Promise { 55 | return requester(request.url, { 56 | basePath: "/", 57 | method: request.method, 58 | ...option, 59 | }) as unknown as Promise; 60 | } 61 | 62 | /** http method */ 63 | request.method = method; 64 | /** request url */ 65 | request.url = url; 66 | return request; 67 | })(); 68 | 69 | /** @description request parameter type for postApiCreate */ 70 | export interface PostApiCreateOption { 71 | /** 72 | * @description 73 | * dto 74 | */ 75 | body: Data; 76 | } 77 | 78 | /** @description response type for postApiCreate */ 79 | export interface PostApiCreateResponse { 80 | /** 81 | * @description 82 | * OK 83 | */ 84 | 200: ReplyVOInt; 85 | /** 86 | * @description 87 | * Created 88 | */ 89 | 201: any; 90 | /** 91 | * @description 92 | * Unauthorized 93 | */ 94 | 401: any; 95 | /** 96 | * @description 97 | * Forbidden 98 | */ 99 | 403: any; 100 | /** 101 | * @description 102 | * Not Found 103 | */ 104 | 404: any; 105 | } 106 | 107 | export type PostApiCreateResponseSuccess = PostApiCreateResponse[200]; 108 | /** 109 | * @description 110 | * ooo 111 | * @produces * 112 | * @consumes application/json 113 | */ 114 | export const postApiCreate = /* #__PURE__ */ (() => { 115 | const method = "post"; 116 | const url = "/api/create"; 117 | function request( 118 | option: PostApiCreateOption 119 | ): Promise { 120 | return requester(request.url, { 121 | basePath: "/", 122 | method: request.method, 123 | ...option, 124 | }) as unknown as Promise; 125 | } 126 | 127 | /** http method */ 128 | request.method = method; 129 | /** request url */ 130 | request.url = url; 131 | return request; 132 | })(); 133 | -------------------------------------------------------------------------------- /example/petProject/src/service/petv3/definition.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** Do not modify manually. 4 | content is generated automatically by `ts-gear`. */ 5 | export type GetPetFindByStatusStatus = "available" | "pending" | "sold"; 6 | export type ComponentsSchemasOrderStatus = "placed" | "approved" | "delivered"; 7 | export type ComponentsSchemasPetStatus = GetPetFindByStatusStatus; 8 | export interface Order { 9 | /** 10 | * @format int64 11 | * @example 12 | * 10 13 | */ 14 | id?: number; 15 | /** 16 | * @format int64 17 | * @example 18 | * 198772 19 | */ 20 | petId?: number; 21 | /** 22 | * @format int32 23 | * @example 24 | * 7 25 | */ 26 | quantity?: number; 27 | /** @format date-time */ 28 | shipDate?: string; 29 | /** 30 | * @description 31 | * Order Status 32 | * @example 33 | * approved 34 | */ 35 | status?: ComponentsSchemasOrderStatus; 36 | complete?: boolean; 37 | } 38 | 39 | export interface Customer { 40 | /** 41 | * @format int64 42 | * @example 43 | * 100000 44 | */ 45 | id?: number; 46 | /** 47 | * @example 48 | * fehguy 49 | */ 50 | username?: string; 51 | address?: Array
; 52 | } 53 | 54 | export interface Address { 55 | /** 56 | * @example 57 | * 437 Lytton 58 | */ 59 | street?: string; 60 | /** 61 | * @example 62 | * Palo Alto 63 | */ 64 | city?: string; 65 | /** 66 | * @example 67 | * CA 68 | */ 69 | state?: string; 70 | /** 71 | * @example 72 | * 94301 73 | */ 74 | zip?: string; 75 | } 76 | 77 | export interface Category { 78 | /** 79 | * @format int64 80 | * @example 81 | * 1 82 | */ 83 | id?: number; 84 | /** 85 | * @example 86 | * Dogs 87 | */ 88 | name?: string; 89 | } 90 | 91 | export interface User { 92 | /** 93 | * @format int64 94 | * @example 95 | * 10 96 | */ 97 | id?: number; 98 | /** 99 | * @example 100 | * theUser 101 | */ 102 | username?: string; 103 | /** 104 | * @example 105 | * John 106 | */ 107 | firstName?: string; 108 | /** 109 | * @example 110 | * James 111 | */ 112 | lastName?: string; 113 | /** 114 | * @example 115 | * john@email.com 116 | */ 117 | email?: string; 118 | /** 119 | * @example 120 | * 12345 121 | */ 122 | password?: string; 123 | /** 124 | * @example 125 | * 12345 126 | */ 127 | phone?: string; 128 | /** 129 | * @description 130 | * User Status 131 | * @format int32 132 | * @example 133 | * 1 134 | */ 135 | userStatus?: number; 136 | } 137 | 138 | export interface Tag { 139 | /** @format int64 */ 140 | id?: number; 141 | name?: string; 142 | } 143 | 144 | export interface Pet { 145 | /** 146 | * @format int64 147 | * @example 148 | * 10 149 | */ 150 | id?: number; 151 | /** 152 | * @example 153 | * doggie 154 | */ 155 | name: string; 156 | category?: Category; 157 | photoUrls: Array; 158 | tags?: Array; 159 | /** 160 | * @description 161 | * pet status in the store 162 | */ 163 | status?: ComponentsSchemasPetStatus; 164 | } 165 | 166 | export interface ApiResponse { 167 | /** @format int32 */ 168 | code?: number; 169 | type?: string; 170 | message?: string; 171 | } 172 | -------------------------------------------------------------------------------- /src/translation/api/baidu/translate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase,no-empty */ 2 | import type { StringOrTranslateOptions, TranslateResult } from '../types' 3 | import getValue from '../../utils/get' 4 | import request from '../../utils/make-request' 5 | import getError, { ERROR_CODE } from '../../utils/error' 6 | import sign from './sign' 7 | import { getAudioURI } from './audio' 8 | import detect from './detect' 9 | import { standard2custom, root, Cookie, fetchCookie, custom2standard } from './state' 10 | import type { ResponseSymbol, Response } from './type' 11 | 12 | function transformRaw(text: string, body: Response, from: string, to: string) { 13 | const transResult = body.trans_result 14 | const customFrom = getValue(transResult, 'from', from) 15 | const customTo = getValue(transResult, 'to', to) 16 | 17 | const result: TranslateResult = { 18 | text, 19 | raw: body, 20 | link: `${root}/#${customFrom}/${customTo}/${encodeURIComponent(text)}`, 21 | from: custom2standard[customFrom], 22 | to: custom2standard[customTo], 23 | } 24 | 25 | const symbols: ResponseSymbol = getValue(body, ['dict_result', 'simple_means', 'symbols', '0']) 26 | 27 | if (symbols) { 28 | // 解析音标 29 | const phonetic = [] 30 | const { ph_am, ph_en } = symbols 31 | if (ph_am) { 32 | phonetic.push({ 33 | name: '美', 34 | ttsURI: getAudioURI(text, 'en'), 35 | value: ph_am, 36 | }) 37 | } 38 | if (ph_en) { 39 | phonetic.push({ 40 | name: '英', 41 | ttsURI: getAudioURI(text, 'en-GB'), 42 | value: ph_en, 43 | }) 44 | } 45 | if (phonetic.length) { 46 | result.phonetic = phonetic 47 | } 48 | 49 | // 解析词典数据 50 | try { 51 | result.dict = symbols.parts.map(part => (part.part ? `${part.part} ` : '') + part.means.join(';')) 52 | } catch (e) {} 53 | } 54 | 55 | // 解析普通的翻译结果 56 | try { 57 | result.result = transResult.data.map(d => d.dst) 58 | } catch (e) {} 59 | 60 | if (!result.dict && !result.result) { 61 | throw getError(ERROR_CODE.API_SERVER_ERROR) 62 | } 63 | 64 | return result 65 | } 66 | 67 | export default async function translate(options: StringOrTranslateOptions) { 68 | if (!Cookie.value) { 69 | await fetchCookie() 70 | } 71 | // eslint-disable-next-line prefer-const 72 | let { from = undefined, to = undefined, text } = typeof options === 'string' ? { text: options } : options 73 | 74 | if (!from) { 75 | from = await detect(text) 76 | } 77 | 78 | if (!to) { 79 | to = from.startsWith('zh') ? 'en' : 'zh-CN' 80 | } 81 | 82 | const customFromLang = standard2custom[from] 83 | const customToLang = standard2custom[to] 84 | 85 | if (!customFromLang || !customToLang) { 86 | throw getError(ERROR_CODE.UNSUPPORTED_LANG) 87 | } 88 | const body = { 89 | from: customFromLang, 90 | to: customToLang, 91 | query: text, 92 | transtype: 'realtime', 93 | simple_means_flag: 3, 94 | dimain: 'common', 95 | ...(await sign(text)), 96 | } 97 | 98 | return transformRaw( 99 | text, 100 | await request({ 101 | url: `${root}/v2transapi`, 102 | type: 'form', 103 | method: 'post', 104 | body, 105 | headers: { 106 | 'X-Requested-With': 'XMLHttpRequest', 107 | Cookie: Cookie.value, 108 | }, 109 | }), 110 | customFromLang, 111 | customToLang, 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const reduce = require('lodash/reduce') 3 | const tsconfig = require('./tsconfig.json') 4 | 5 | /* eslint-enable @typescript-eslint/no-var-requires */ 6 | 7 | // 处理tsconfig的路径别名 8 | const pathAlias = reduce( 9 | tsconfig.compilerOptions.paths, 10 | (r, v, k) => { 11 | r.push([k.replace('/*', ''), v[0].replace('/*', '')]) 12 | return r 13 | }, 14 | [], 15 | ) 16 | 17 | module.exports = { 18 | parser: '@typescript-eslint/parser', 19 | plugins: ['import', 'prettier'], 20 | parserOptions: { 21 | ecmaVersion: 2018, 22 | sourceType: 'module', 23 | }, 24 | extends: [ 25 | 'airbnb-base', 26 | 'plugin:@typescript-eslint/recommended', 27 | 'prettier', 28 | // 'plugin:import/errors', 29 | // 'plugin:import/warnings', 30 | 'plugin:import/typescript', 31 | ], 32 | 33 | env: { 34 | browser: true, 35 | node: true, 36 | es6: true, 37 | }, 38 | rules: { 39 | 'prettier/prettier': [ 40 | 'error', 41 | { 42 | eslintIntegration: true, 43 | stylelintIntegration: true, 44 | printWidth: 120, 45 | useTabs: false, 46 | tabWidth: 2, 47 | singleQuote: true, 48 | semi: false, 49 | trailingComma: 'all', 50 | endOfLine: 'lf', 51 | arrowParens: 'avoid', 52 | }, 53 | { 54 | usePrettierrc: false, 55 | }, 56 | ], 57 | 'import/order': ['error', { 'newlines-between': 'never' }], 58 | // 'import/first': 0, 59 | '@typescript-eslint/consistent-type-imports': 'error', 60 | 61 | // '@typescript-eslint/camelcase': [ 62 | // 'error', 63 | // { 64 | // properties: 'always', 65 | // }, 66 | // ], 67 | 68 | '@typescript-eslint/no-extra-semi': 0, 69 | '@typescript-eslint/explicit-module-boundary-types': 0, 70 | 'import/prefer-default-export': 0, 71 | 'import/no-unresolved': ['error', { ignore: ['swagger-schema-official'] }], 72 | 'no-param-reassign': 0, 73 | 'import/extensions': [ 74 | 'error', 75 | 'ignorePackages', 76 | { 77 | js: 'never', 78 | jsx: 'never', 79 | ts: 'never', 80 | tsx: 'never', 81 | }, 82 | ], 83 | 'import/no-extraneous-dependencies': [ 84 | 'error', 85 | { 86 | devDependencies: ['jest.config.ts', '__tests__/**/*.ts', 'src/requester/*'], 87 | optionalDependencies: false, 88 | }, 89 | ], 90 | 91 | // let prettier handle indent 92 | '@typescript-eslint/indent': 0, 93 | // '@typescript-eslint/interface-name-prefix': [1, 'always'], 94 | '@typescript-eslint/no-explicit-any': 0, 95 | '@typescript-eslint/no-non-null-assertion': 0, 96 | '@typescript-eslint/explicit-function-return-type': 0, 97 | '@typescript-eslint/member-delimiter-style': ['error', { multiline: { delimiter: 'none' } }], 98 | 99 | // skip check var starts with "_" 100 | '@typescript-eslint/no-unused-vars': [ 101 | 'error', 102 | { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }, 103 | ], 104 | '@typescript-eslint/explicit-member-accessibility': ['error', { overrides: { constructors: 'no-public' } }], 105 | }, 106 | settings: { 107 | 'import/resolver': { 108 | alias: { 109 | map: pathAlias, 110 | extensions: ['.ts', '.js', '.jsx', '.json', '.tsx'], 111 | }, 112 | }, 113 | 'import/parsers': { 114 | '@typescript-eslint/parser': ['.ts', '.tsx'], 115 | }, 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /src/step/assembleSchemaToGlobal.ts: -------------------------------------------------------------------------------- 1 | import { forEach, findKey, isEqual } from 'lodash' 2 | import type { Spec } from 'swagger-schema-official' 3 | import { traverseSchema } from '../tool/traverseSchema' 4 | import { getDefinition } from '../tool/getDefinition' 5 | import type { Project } from '../type' 6 | import { httpMethods } from '../type' 7 | import { getGlobal } from '../projectGlobalVariable' 8 | import { generateEnumName, generateEnumTypescriptContent } from '../tool/enumType' 9 | import { generateRequestFunctionName } from '../tool/generateRequestFunctionName' 10 | import { getSchemaDeep } from '../tool/getSchemaDeep' 11 | import { getRequiredDeep } from '../tool/getRequiredDeep' 12 | import { generateTypeAlias } from '../tool/generateTypeAlias' 13 | // import type { OperationObject } from 'openapi3-ts' 14 | 15 | /** 16 | * collect definition to definitionMap 17 | * collect request to requestMap, skip deprecated 18 | * */ 19 | export const assembleSchemaToGlobal = (spec: Spec, project: Project) => { 20 | const { definitionMap, requestMap, enumMap, requestEnumSet } = getGlobal(project) 21 | const definitions = getDefinition(spec) 22 | traverseSchema(spec, ({ value, key, path, parent }) => { 23 | if (key === 'enum' && value) { 24 | const name = generateEnumName(path, spec) 25 | const existEnumName = findKey(enumMap, ({ originalContent }) => isEqual(originalContent, value)) 26 | parent[key] = name 27 | if (path[0] === 'paths') { 28 | requestEnumSet.add(name) 29 | } 30 | // add enum type alias 31 | if (existEnumName) { 32 | enumMap[name] = { 33 | originalContent: value, 34 | typescriptContent: generateTypeAlias(name, existEnumName), 35 | } 36 | } else { 37 | const tsContent = generateEnumTypescriptContent(name, value) 38 | enumMap[name] = { 39 | originalContent: value, 40 | typescriptContent: tsContent, 41 | } 42 | } 43 | } 44 | }) 45 | Object.getOwnPropertyNames(definitions).forEach(name => { 46 | definitionMap[name] = { 47 | typeName: name, 48 | schema: definitions[name], 49 | } 50 | }) 51 | forEach(spec.paths, (pathSchema /** Path */, pathname) => { 52 | const genFunctionName = project.generateRequestFunctionName || generateRequestFunctionName 53 | forEach(httpMethods, httpMethod => { 54 | const operation: any = pathSchema[httpMethod] 55 | if (operation && !operation.deprecated) { 56 | // parameters 有可能为空 57 | let { parameters = [] } = operation 58 | /** 59 | * 兼容openapiv3,将requestBody格式组装成与v2相同的数据结构 60 | * 这段代码不应该放到这里,以后有空抽离出去单独测试,如果有空的话 61 | * */ 62 | if ('requestBody' in operation) { 63 | const v3Parameters = [] as any[] 64 | const { requestBody } = operation 65 | const { content } = requestBody 66 | // openapi3 required in requestBody 67 | const required = 'required' in requestBody ? requestBody.required : getRequiredDeep(content) 68 | const schema = getSchemaDeep(content) 69 | v3Parameters.push({ 70 | in: content['multipart/form-data'] ? 'formData' : 'body', 71 | name: 'body', 72 | required, 73 | schema, 74 | }) 75 | // 合并v3参数 76 | parameters = parameters.concat(v3Parameters) 77 | } 78 | requestMap[ 79 | genFunctionName({ 80 | httpMethod, 81 | pathname, 82 | schema: spec, 83 | }) 84 | ] = { 85 | pathname, 86 | httpMethod, 87 | schema: operation!, 88 | responses: operation.responses, 89 | parameters, 90 | } 91 | } 92 | }) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/step/generateDefinitionContent.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | OptionalKind, 3 | PropertyDeclarationStructure, 4 | PropertySignatureStructure, 5 | InterfaceDeclarationStructure, 6 | ClassDeclarationStructure, 7 | } from 'ts-morph' 8 | import { Scope } from 'ts-morph' 9 | import { getHasQuestionToken, schemaToTypescript } from '../tool/schemaToTypescript' 10 | import { sow, harvest } from '../source' 11 | import type { Project } from '../type' 12 | import { getGlobal } from '../projectGlobalVariable' 13 | import { assembleDoc } from '../tool/assembleDoc' 14 | 15 | /** generate on definition ts content */ 16 | export const generateDefinitionContent = (project: Project) => { 17 | const { definitionMap } = getGlobal(project) 18 | Object.values(definitionMap).forEach(definition => { 19 | if (definition.typescriptContent || !definition.schema) { 20 | return 21 | } 22 | const source = sow() 23 | const title = definition.typeName! 24 | const schema = definition.schema! 25 | if (schema.type === 'object') { 26 | if (schema.properties) { 27 | const preferClass = Boolean(project.preferClass) 28 | const declarationOptin: OptionalKind & OptionalKind = 29 | { 30 | isExported: true, 31 | name: title, 32 | typeParameters: definition.typeParameters 33 | ? definition.typeParameters.map(t => ({ 34 | name: t, 35 | default: 'any', 36 | })) 37 | : undefined, 38 | docs: assembleDoc(schema), 39 | } 40 | const klass = preferClass ? source.addClass(declarationOptin) : source.addInterface(declarationOptin) 41 | Object.getOwnPropertyNames(schema.properties).forEach(name => { 42 | const property = schema!.properties![name] 43 | const propertyStructure: OptionalKind & 44 | OptionalKind = { 45 | name, 46 | type: schemaToTypescript(property, project), 47 | scope: preferClass ? Scope.Public : undefined, 48 | hasQuestionToken: getHasQuestionToken(name, property, project, schema.required), 49 | docs: assembleDoc(property), 50 | } 51 | /** interface property can not has default value 52 | so use class as type */ 53 | if (preferClass && Reflect.has(property, 'default')) { 54 | propertyStructure.initializer = String(property.default) 55 | } 56 | klass.addProperty(propertyStructure) 57 | }) 58 | // 没有properties,会有additionalProperties 59 | } else if (schema.additionalProperties) { 60 | const { additionalProperties } = schema 61 | // class doesn`t has "addIndexSignature", so use interface 62 | source.addInterface({ 63 | isExported: true, 64 | name: title, 65 | docs: typeof additionalProperties !== 'boolean' ? assembleDoc(additionalProperties) : [], 66 | indexSignatures: [ 67 | { 68 | keyName: 'key', 69 | keyType: 'string', 70 | returnType: additionalProperties === true ? 'any' : schemaToTypescript(additionalProperties, project), 71 | }, 72 | ], 73 | }) 74 | } else { 75 | source.addTypeAlias({ 76 | isExported: true, 77 | name: title, 78 | type: schemaToTypescript(schema, project), 79 | docs: assembleDoc(schema), 80 | }) 81 | } 82 | } else { 83 | source.addTypeAlias({ 84 | isExported: true, 85 | name: title, 86 | type: schemaToTypescript(schema, project), 87 | docs: assembleDoc(schema), 88 | }) 89 | } 90 | definition.typescriptContent = harvest(source) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-gear", 3 | "version": "4.13.0", 4 | "description": "swagger to typescript with mock data", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "tsg": "lib/main.js", 8 | "ts-gear": "lib/main.js" 9 | }, 10 | "scripts": { 11 | "test": "jest", 12 | "coverage": "cross-env NODE_ENV=test jest --coverage | ts-node script/coverageReportToMarkdown.ts && jest-coverage-badges output ./badge && open-cli coverage/lcov-report/index.html -- google-chrome", 13 | "build": "cross-env rimraf lib && cross-env tsc -p tsconfig.build.json && chmod +x lib/main.js", 14 | "release": "standard-version", 15 | "prepublishOnly": "yarn run-s build test release", 16 | "publish-rc": "yarn test && yarn build && yarn standard-version -- prerelease rc && npm publish --ignore-scripts" 17 | }, 18 | "types": "lib/index.d.ts", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/superwf/ts-gear.git" 22 | }, 23 | "peerDependencies": { 24 | "typescript": ">=4" 25 | }, 26 | "dependencies": { 27 | "@vitalets/google-translate-api": "^8.0.0", 28 | "app-root-path": "^3.0.0", 29 | "chalk": "^2.4.2", 30 | "commander": "^8.2.0", 31 | "cosmiconfig": "^5.2.1", 32 | "cross-fetch": "^3.1.5", 33 | "fs-extra": "^7.0.1", 34 | "json5": "^2.2.3", 35 | "lodash": "^4.17.21", 36 | "mkdirp": "^1.0.4", 37 | "openapi3-ts": "^1.4.0", 38 | "path-to-regexp": "^3.0.0", 39 | "prettier": "^2.2.1", 40 | "prompts": "^2.4.1", 41 | "rimraf": "^3.0.2", 42 | "swagger-schema-official": "^2.0.0-bab6bed", 43 | "traverse": "^0.6.6", 44 | "ts-morph": "^18.0.0", 45 | "ts-node": "^10.9.1", 46 | "tsconfig-paths": "^3.9.0", 47 | "url-join": "^4.0.1" 48 | }, 49 | "files": [ 50 | "*.md", 51 | "lib" 52 | ], 53 | "devDependencies": { 54 | "@commitlint/config-conventional": "^13.2.0", 55 | "@commitlint/prompt-cli": "^13.2.1", 56 | "@types/app-root-path": "^1.2.4", 57 | "@types/cosmiconfig": "^5.0.3", 58 | "@types/fs-extra": "^5.0.5", 59 | "@types/http-server": "^0.10.0", 60 | "@types/jest": "^29.5.1", 61 | "@types/lodash": "^4.14.110", 62 | "@types/mkdirp": "^1.0.0", 63 | "@types/moxios": "^0.4.9", 64 | "@types/node": "^11.13.8", 65 | "@types/prettier": "^2.2.0", 66 | "@types/prompts": "^2.0.5", 67 | "@types/rimraf": "^2.0.3", 68 | "@types/swagger-schema-official": "^2.0.21", 69 | "@types/traverse": "^0.6.25-alpha", 70 | "@typescript-eslint/eslint-plugin": "^5.59.5", 71 | "@typescript-eslint/parser": "^5.59.5", 72 | "axios": "^0.26.1", 73 | "commitlint": "^11.0.0", 74 | "cross-env": "^5.2.0", 75 | "eslint": "^8.20.0", 76 | "eslint-config-airbnb-base": "^15.0.0", 77 | "eslint-config-prettier": "^8.5.0", 78 | "eslint-import-resolver-alias": "^1.1.2", 79 | "eslint-plugin-import": "^2.26.0", 80 | "eslint-plugin-jest": "^26.6.0", 81 | "eslint-plugin-prettier": "^4.2.1", 82 | "fetch-mock": "^9.11.0", 83 | "husky": "^4.3.0", 84 | "jest": "^29.5.0", 85 | "jest-coverage-badges": "^1.1.2", 86 | "moxios": "^0.4.0", 87 | "npm-run-all": "^4.1.5", 88 | "open-cli": "^6.0.1", 89 | "readline": "^1.3.0", 90 | "standard-version": "^9.3.2", 91 | "ts-jest": "^29.1.0", 92 | "typescript": "^5.0.4" 93 | }, 94 | "keywords": [ 95 | "openapi", 96 | "swagger", 97 | "typescript", 98 | "json schema", 99 | "ts-gear", 100 | "code generator", 101 | "interface", 102 | "structure" 103 | ], 104 | "husky": { 105 | "hooks": { 106 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 107 | } 108 | }, 109 | "publishConfig": { 110 | "registry": "https://registry.npmjs.org" 111 | }, 112 | "author": "superwf", 113 | "license": "MIT" 114 | } 115 | -------------------------------------------------------------------------------- /__tests__/step/parseGenericType.test.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import { checkAndUpdateDefinitionTypeName, checkAndUpdateRequestRef } from 'src/step/parseGenericType' 3 | import * as step from 'src/step' 4 | import { getGlobal, restore } from 'src/projectGlobalVariable' 5 | // import { IDefinitionMap } from 'src/interface' 6 | 7 | const spec = { 8 | info: { contact: {}, license: { name: '' }, title: '', version: '' }, 9 | swagger: '2.0', 10 | paths: { 11 | '/api/req': { 12 | post: { 13 | parameters: [ 14 | { 15 | in: 'body', 16 | name: 'qo', 17 | schema: { 18 | $ref: 'ReplyVO>', 19 | }, 20 | }, 21 | ], 22 | responses: { 23 | '200': { 24 | description: 'OK', 25 | schema: { 26 | $ref: 'ReplyVO>', 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | definitions: { 34 | 'ReplyVO>': { 35 | type: 'object', 36 | properties: { 37 | code: { 38 | type: 'string', 39 | }, 40 | data: { 41 | $ref: 'List', 42 | }, 43 | message: { 44 | type: 'string', 45 | example: 'success', 46 | }, 47 | }, 48 | }, 49 | 'ReplyRole>': { 50 | type: 'object', 51 | properties: { 52 | code: { 53 | type: 'string', 54 | }, 55 | data: { 56 | $ref: 'L', 57 | }, 58 | user: { 59 | $ref: 'ReplyVO>', 60 | }, 61 | other: { 62 | $ref: 'ReplyVO', 63 | }, 64 | message: { 65 | type: 'string', 66 | example: 'success', 67 | }, 68 | }, 69 | }, 70 | 'ReplyVO>': { 71 | type: 'object', 72 | properties: { 73 | data: { 74 | type: 'array', 75 | description: '返回数据', 76 | items: { 77 | $ref: 'VV', 78 | originalRef: 'VV', 79 | }, 80 | }, 81 | }, 82 | }, 83 | 'Result>': { 84 | type: 'object', 85 | properties: { 86 | description: { 87 | type: 'string', 88 | }, 89 | result: { 90 | type: 'array', 91 | items: { 92 | $ref: 'Map', 93 | }, 94 | }, 95 | returnCode: { 96 | type: 'integer', 97 | format: 'int32', 98 | }, 99 | }, 100 | }, 101 | }, 102 | } as Spec 103 | 104 | describe('parse definition with generic type', () => { 105 | const project = { 106 | name: 'sample', 107 | dest: './service', 108 | source: 'fixture/ignore.json', 109 | keepGeneric: true, 110 | importRequesterStatement: 'import { requester } from "ts-gear/requester/fetch"', 111 | } 112 | 113 | beforeEach(() => { 114 | step.assembleSchemaToGlobal(spec, project) 115 | }) 116 | afterEach(() => { 117 | restore(project) 118 | }) 119 | 120 | it('checkAndUpdateDefinitionTypeName', () => { 121 | checkAndUpdateDefinitionTypeName(getGlobal(project)) 122 | const { definitionMap, requestMap } = getGlobal(project) 123 | // console.log(JSON.stringify(definitionMap, null, 2)) 124 | expect(definitionMap.LRole).toEqual({ 125 | typeName: 'LRole', 126 | typescriptContent: 'export type LRole = any', 127 | }) 128 | 129 | expect(definitionMap.ReplyVOListUser).toEqual({ 130 | typeName: 'ReplyVO', 131 | schema: spec.definitions!['ReplyVO>'], 132 | typeParameters: ['ListUser'], 133 | }) 134 | 135 | checkAndUpdateRequestRef(getGlobal(project)) 136 | 137 | console.log(JSON.stringify(definitionMap, null, 2)) 138 | console.log(JSON.stringify(requestMap, null, 2)) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /example/petProject/src/requester.ts: -------------------------------------------------------------------------------- 1 | /** use native fetch to request */ 2 | /* eslint-disable import/no-relative-packages */ 3 | import { URL } from 'url' 4 | import { forEach, isPlainObject } from 'lodash' 5 | import * as pathToRegexp from 'path-to-regexp' 6 | import type { RequestParameter, Requester } from '../../../lib' 7 | 8 | const jsonType = 'application/json' 9 | 10 | /** add query and path parameters to url 11 | * e.g. 12 | * parseUrl('/api/abc/:id', { path: { id: '123' }, query: { name: 'def' } }) => '/api/abc/123?name=def' 13 | * */ 14 | export const parseUrl = (url: string, option: RequestParameter): string => { 15 | if (option.path) { 16 | Object.getOwnPropertyNames(option.path).forEach(k => { 17 | option.path[k] = encodeURIComponent(String(option.path[k])) 18 | }) 19 | url = pathToRegexp.compile(url)(option.path) 20 | } 21 | if (option.query) { 22 | let onlyPathname = false 23 | if (!url.startsWith('http:') || !url.startsWith('https:')) { 24 | url = `http://fakehost${url}` 25 | onlyPathname = true 26 | } 27 | const urlObject = new URL(url) 28 | const search = new URLSearchParams(urlObject.search) 29 | Object.getOwnPropertyNames(option.query).forEach(k => { 30 | const v = option.query[k] 31 | if (Array.isArray(v)) { 32 | v.forEach((item, i) => { 33 | if (i === 0) { 34 | search.set(k, item) 35 | } else { 36 | search.append(k, item) 37 | } 38 | }) 39 | } else { 40 | search.set(k, v) 41 | } 42 | }) 43 | const searchString = search.toString() 44 | url = `${onlyPathname ? '' : urlObject.origin}${urlObject.pathname}${searchString ? '?' : ''}${searchString}` 45 | } 46 | return url 47 | } 48 | 49 | /** 请求拦截器 50 | * 每个请求的通用设置放到这里 51 | * 如果请求体是普通对象,用json格式化并添加json的http header 52 | * 如果请求体有formData项,自动添加成FormData 53 | * */ 54 | export function interceptRequest( 55 | url: string, 56 | option: RequestParameter & { 57 | requestInit?: RequestInit 58 | }, 59 | ): [string, RequestInit] { 60 | const { requestInit } = option 61 | url = parseUrl(url, option) 62 | const requestOption: RequestInit = { 63 | method: option.method, 64 | ...requestInit, 65 | // add the default request option here 66 | } 67 | if (option.header) { 68 | requestOption.headers = option.header 69 | } 70 | if (option.body) { 71 | let { body } = option 72 | // add application/json header when body is plain object 73 | // and auto json stringify the body 74 | if (isPlainObject(body)) { 75 | requestOption.headers = { 76 | 'Content-Type': jsonType, 77 | ...requestOption.headers, 78 | } 79 | body = JSON.stringify(body) 80 | requestOption.body = body 81 | } else { 82 | requestOption.body = option.body 83 | } 84 | } 85 | // body 与 formData 不能同时存在 86 | // 所以如果有formData时,直接给requestOption.body赋值即可 87 | if (option.formData) { 88 | const formData = new FormData() 89 | // 这种上传文件的情况,应该只有一维的键值对应,只用forEach处理第一层数据 90 | forEach(option.formData, (v: any, k: string) => { 91 | formData.append(k, v) 92 | }) 93 | requestOption.body = formData 94 | } 95 | return [url, requestOption] 96 | } 97 | 98 | /** 根据response的header处理各种返回数据 99 | * 目前只是转了json和text两种,需要其他自行添加 100 | * */ 101 | export function interceptResponse(res: Response) { 102 | if (!res.ok) { 103 | throw new Error(`response not ok, status: ${res.status}, ${res.statusText}, url: ${res.url}`) 104 | } 105 | const contentType = res.headers.get('Content-Type') 106 | if (contentType) { 107 | if (contentType.includes(jsonType)) { 108 | return res.json() 109 | } 110 | 111 | if (contentType.includes('text/plain')) { 112 | return res.text() 113 | } 114 | // 在此处添加处理更多的response类型 115 | } 116 | return res 117 | } 118 | 119 | /** native fetch wrappper */ 120 | export const requester: Requester = async (apiUrl: string, param?: any) => { 121 | const [url, option] = interceptRequest(apiUrl, { ...param }) 122 | return fetch(url, option).then(interceptResponse) 123 | } 124 | -------------------------------------------------------------------------------- /src/requester/fetch.ts: -------------------------------------------------------------------------------- 1 | /** use native fetch to request */ 2 | import { URL } from 'url' 3 | import { forEach, isPlainObject } from 'lodash' 4 | import * as pathToRegexp from 'path-to-regexp' 5 | import type { RequestParameter, Requester } from '../type' 6 | import { MIME_JSON, MIME_TEXT } from '../constant' 7 | 8 | /** add query and path parameters to url 9 | * e.g. 10 | * parseUrl('/api/abc/:id', { path: { id: '123' }, query: { name: 'def' } }) => '/api/abc/123?name=def' 11 | * */ 12 | export const parseUrl = (url: string, option: RequestParameter): string => { 13 | if (option.path) { 14 | Object.getOwnPropertyNames(option.path).forEach(k => { 15 | option.path[k] = encodeURIComponent(String(option.path[k])) 16 | }) 17 | url = pathToRegexp.compile(url)(option.path) 18 | } 19 | if (option.query) { 20 | let onlyPathname = false 21 | if (!url.startsWith('http:') || !url.startsWith('https:')) { 22 | url = `http://localhost${url}` 23 | onlyPathname = true 24 | } 25 | const urlObject = new URL(url) 26 | const search = new URLSearchParams(urlObject.search) 27 | Object.getOwnPropertyNames(option.query).forEach(k => { 28 | const v = option.query[k] 29 | if (Array.isArray(v)) { 30 | v.forEach((item, i) => { 31 | if (i === 0) { 32 | search.set(k, item) 33 | } else { 34 | search.append(k, item) 35 | } 36 | }) 37 | } else { 38 | search.set(k, v) 39 | } 40 | }) 41 | const searchString = search.toString() 42 | url = `${onlyPathname ? '' : urlObject.origin}${urlObject.pathname}${searchString ? '?' : ''}${searchString}` 43 | } 44 | return url 45 | } 46 | 47 | /** 请求拦截器 48 | * 每个请求的通用设置放到这里 49 | * 如果请求体是普通对象,用json格式化并添加json的http header 50 | * 如果请求体有formData项,自动添加成FormData 51 | * */ 52 | export function interceptRequest( 53 | url: string, 54 | option: RequestParameter & { 55 | requestInit?: RequestInit 56 | }, 57 | ): [string, RequestInit] { 58 | const { requestInit } = option 59 | url = parseUrl(url, option) 60 | const requestOption: RequestInit = { 61 | method: option.method, 62 | ...requestInit, 63 | // add the default request option here 64 | } 65 | if (option.header) { 66 | requestOption.headers = option.header 67 | } 68 | if (option.body) { 69 | let { body } = option 70 | // add application/json header when body is plain object 71 | // and auto json stringify the body 72 | if (isPlainObject(body)) { 73 | requestOption.headers = { 74 | 'Content-Type': MIME_JSON, 75 | ...requestOption.headers, 76 | } 77 | body = JSON.stringify(body) 78 | requestOption.body = body 79 | } else { 80 | requestOption.body = option.body 81 | } 82 | } 83 | // body 与 formData 不能同时存在 84 | // 所以如果有formData时,直接给requestOption.body赋值即可 85 | if (option.formData) { 86 | const formData = new FormData() 87 | // 这种上传文件的情况,应该只有一维的键值对应,只用forEach处理第一层数据 88 | forEach(option.formData, (v: any, k: string) => { 89 | formData.append(k, v) 90 | }) 91 | requestOption.body = formData 92 | } 93 | return [url, requestOption] 94 | } 95 | 96 | /** 根据response的header处理各种返回数据 97 | * 目前只是转了json和text两种,需要其他自行添加 98 | * */ 99 | export function interceptResponse(res: Response) { 100 | if (!res.ok) { 101 | throw new Error(`response not ok, status: ${res.status}, ${res.statusText}, url: ${res.url}`) 102 | } 103 | const contentType = res.headers.get('Content-Type') 104 | if (contentType) { 105 | if (contentType.includes(MIME_JSON)) { 106 | return res.json() 107 | } 108 | 109 | if (contentType.includes(MIME_TEXT)) { 110 | return res.text() 111 | } 112 | // add more response mime type logic here 113 | // 在此处添加处理更多的response类型处理逻辑 114 | } 115 | return res 116 | } 117 | 118 | /** native fetch wrappper */ 119 | export const requester = 120 | ( 121 | requestInit?: RequestInit & { 122 | basePath?: string 123 | }, 124 | ): Requester => 125 | async (apiUrl: string, param?: RequestParameter) => { 126 | const [url, option] = interceptRequest(apiUrl, { ...param, requestInit }) 127 | const basePath = requestInit?.basePath || '' 128 | return fetch(`${basePath}${url}`, option).then(interceptResponse) 129 | } 130 | 131 | export default requester 132 | -------------------------------------------------------------------------------- /__tests__/requester/axios.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as moxios from 'moxios' 3 | import { requester } from 'src/requester/axios' 4 | 5 | describe('requester fetch', () => { 6 | beforeEach(() => { 7 | moxios.install() 8 | }) 9 | 10 | afterEach(() => { 11 | moxios.uninstall() 12 | }) 13 | 14 | it('simple get', async () => { 15 | moxios.stubRequest('/abc', { status: 200, responseText: '{"ok":true}' }) 16 | const result = await requester()('/abc') 17 | expect(result.data).toEqual({ ok: true }) 18 | const req = moxios.requests.mostRecent() 19 | expect(req.url).toBe('/abc') 20 | }) 21 | 22 | it('get header', async () => { 23 | moxios.stubRequest('/abc', { status: 200, responseText: '{"ok":true}' }) 24 | const result = await requester()('/abc', { 25 | method: 'get', 26 | header: { 27 | custom: 'headerValue', 28 | }, 29 | }) 30 | expect(result.data).toEqual({ ok: true }) 31 | const req = moxios.requests.mostRecent() 32 | expect(req.url).toBe('/abc') 33 | expect(req.headers.custom).toBe('headerValue') 34 | }) 35 | 36 | it('with path', async () => { 37 | moxios.stubRequest('/abc/111', { status: 200, responseText: '{"ok":true}' }) 38 | const result = await requester()('/abc/:id', { 39 | method: 'get', 40 | path: { 41 | id: '111', 42 | }, 43 | }) 44 | expect(result.data).toEqual({ ok: true }) 45 | const req = moxios.requests.mostRecent() 46 | expect(req.url).toBe('/abc/111') 47 | }) 48 | 49 | it('with wrong path', async () => { 50 | await expect(async () => { 51 | await requester()('/abc/:id/:slot', { 52 | method: 'get', 53 | path: { 54 | id: '111', 55 | }, 56 | }) 57 | }).rejects.toThrow('Expected "slot" to be a string') 58 | }) 59 | 60 | it('with init option', async () => { 61 | moxios.stubRequest('/abc', { status: 200, responseText: '{"ok":true}' }) 62 | const result = await requester( 63 | axios.create({ 64 | headers: { 65 | custom: 'headerValue', 66 | }, 67 | }), 68 | )('/abc', { 69 | method: 'get', 70 | }) 71 | expect(result.data).toEqual({ ok: true }) 72 | const req = moxios.requests.mostRecent() 73 | expect(req.url).toBe('/abc') 74 | expect(req.headers.custom).toBe('headerValue') 75 | }) 76 | 77 | it('path', async () => { 78 | moxios.stubRequest('/abc/111/edit/abc', { status: 200, responseText: '{"ok":true}' }) 79 | const result = await requester()('/abc/:id/edit/:name', { 80 | method: 'get', 81 | path: { 82 | id: '111', 83 | name: 'abc', 84 | }, 85 | }) 86 | expect(result.data).toEqual({ ok: true }) 87 | const req = moxios.requests.mostRecent() 88 | expect(req.url).toBe('/abc/111/edit/abc') 89 | }) 90 | 91 | it('post body', async () => { 92 | moxios.stubRequest('/abc/111/edit/abc', { status: 200, responseText: '{"ok":true}' }) 93 | const result = await requester()('/abc/:id/edit/:name', { 94 | method: 'post', 95 | header: { 96 | value: 'A', 97 | }, 98 | path: { 99 | id: '111', 100 | name: 'abc', 101 | }, 102 | body: { 103 | name: 'def', 104 | }, 105 | }) 106 | expect(result.data).toEqual({ ok: true }) 107 | const req = moxios.requests.mostRecent() 108 | expect(req.url).toBe('/abc/111/edit/abc') 109 | expect(JSON.parse(req.config.data)).toEqual({ 110 | name: 'def', 111 | }) 112 | expect(req.config.headers?.value).toBe('A') 113 | }) 114 | 115 | it('put formData', async () => { 116 | ;(global as any).FormData = class { 117 | [k: string]: any 118 | 119 | public append(k: string, v: any) { 120 | this[k] = v 121 | } 122 | } 123 | moxios.stubRequest('/abc', { status: 200, responseText: 'ok' }) 124 | const res = await requester()('/abc', { 125 | method: 'put', 126 | formData: { 127 | formDataKey: 'formDataValue', 128 | }, 129 | }) 130 | const req = moxios.requests.mostRecent() 131 | expect(res.data).toBe('ok') 132 | const mockFormData = new FormData() 133 | mockFormData.append('formDataKey', 'formDataValue') 134 | expect(JSON.parse(req.config.data)).toEqual(mockFormData) 135 | 136 | delete (global as any).FormData 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /src/step/translateSchema.ts: -------------------------------------------------------------------------------- 1 | import type { Spec } from 'swagger-schema-official' 2 | import { find } from 'lodash' 3 | import { traverseSchema } from '../tool/traverseSchema' 4 | import type { TranslationEngine, WordsMap, Project } from '../type' 5 | import { translate } from '../tool/translate' 6 | 7 | export const cnReg = /[\u4e00-\u9fa5]/ 8 | 9 | /** gather all words those need to be translated in spec */ 10 | export const gatherNonEnglishWords = (spec: Spec) => { 11 | const originWordSet: Set = new Set() 12 | 13 | Object.getOwnPropertyNames(spec.definitions!).forEach(k => { 14 | k = String(k) 15 | if (cnReg.test(k)) { 16 | originWordSet.add(k) 17 | } 18 | }) 19 | 20 | traverseSchema(spec, ({ value, key }) => { 21 | if (key === '$ref' && typeof value === 'string') { 22 | if (cnReg.test(value)) { 23 | // in openapi v2 remove "#/definition/" prefix 24 | // in openapi v3 remove "#/components/schemas/" prefix 25 | originWordSet.add(value.replace(/^#\/.+\//, '')) 26 | } 27 | } 28 | }) 29 | return Array.from(originWordSet) 30 | } 31 | 32 | /** when the translation result repeat, add a unique number as suffix. 33 | * make every word uniq. 34 | * 这块过一段时间,自己也看不懂了,特此备注。 35 | * 源文字不同,但翻译目标的英文可能会一样 36 | * 比如“非常好”,“很好”,都翻译成 "very good",针对目标英文可能有重复,添加自增后缀数字区分 37 | */ 38 | let $wordCount = 1 39 | 40 | /** 41 | *generateTranslationMap param type 42 | * */ 43 | type Option = { 44 | words: string[] 45 | engine: TranslationEngine 46 | interval?: number 47 | serial?: boolean 48 | debug?: boolean 49 | } 50 | 51 | /** 52 | * generate a translation map, as 53 | * 54 | * @return translation map 55 | * { 56 | * "结果": "Result", 57 | * } 58 | * */ 59 | export const generateTranslationMap = async ({ words, engine, interval = 2000, serial, debug = false }: Option) => { 60 | const wordsMap: WordsMap = {} 61 | 62 | serial = serial ?? true 63 | 64 | if (words.length > 0) { 65 | if (serial) { 66 | // eslint-disable-next-line no-restricted-syntax 67 | for (const text of words) { 68 | // eslint-disable-next-line no-await-in-loop 69 | let newWord = String(await translate({ text, engine, interval, debug })) 70 | // if translated word repeat, add number as suffix 71 | if (find(wordsMap, v => v === newWord)) { 72 | newWord = `${newWord}${$wordCount}` 73 | $wordCount += 1 74 | } 75 | wordsMap[text] = newWord 76 | } 77 | } else { 78 | await Promise.all( 79 | words.map(async text => { 80 | let newWord = String(await translate({ text, engine, interval, debug })) 81 | // if translated word repeat, add number as suffix 82 | if (find(wordsMap, v => v === newWord)) { 83 | newWord = `${newWord}${$wordCount}` 84 | $wordCount += 1 85 | } 86 | wordsMap[text] = newWord 87 | }), 88 | ) 89 | } 90 | } 91 | return wordsMap 92 | } 93 | 94 | /** update words those need to be translated in spec */ 95 | export const updateSchema = (spec: Spec, wordsMap: WordsMap) => { 96 | const { definitions } = spec 97 | Object.getOwnPropertyNames(definitions!).forEach(k => { 98 | if (k in wordsMap) { 99 | definitions![wordsMap[k as string]] = definitions![k] 100 | delete definitions![k] 101 | } 102 | }) 103 | 104 | traverseSchema(spec, ({ value, parent, key }) => { 105 | if (key === '$ref' && typeof value === 'string') { 106 | const translatedWord = value.replace(/^#\/.+\//, '') 107 | if (translatedWord in wordsMap) { 108 | const matched = value.match(/^#\/.+\//) 109 | if (matched && matched.length > 0) { 110 | parent.$ref = `${matched[0]}${wordsMap[translatedWord]}` 111 | } else { 112 | parent.$ref = wordsMap[translatedWord] 113 | } 114 | } 115 | } 116 | }) 117 | } 118 | 119 | /** translate "$ref" value and keys in "definitions" in spec 120 | * just update the spec parame object 121 | * not return a new object 122 | * */ 123 | export const translateSchema = async (spec: Spec, project: Project) => { 124 | const { translationEngine, translateSerial } = project 125 | if (translationEngine) { 126 | const words = gatherNonEnglishWords(spec) 127 | const map = await generateTranslationMap({ 128 | words, 129 | engine: translationEngine, 130 | interval: project.translateIntervalPerWord, 131 | serial: translateSerial, 132 | debug: project.translateDebug, 133 | }) 134 | updateSchema(spec, map) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/step/generateRequestContent/generateMockRequestContent.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDeclarationStructure, OptionalKind, VariableDeclarationKind } from 'ts-morph' 2 | import * as join from 'url-join' 3 | import type { Spec } from 'swagger-schema-official' 4 | import type { Project } from '../../type' 5 | import { transformSwaggerPathToRouterPath } from '../../tool/transformSwaggerPathToRouterPath' 6 | import { harvest, sow } from '../../source' 7 | import { getGlobal } from '../../projectGlobalVariable' 8 | import { assembleDoc } from '../../tool/assembleDoc' 9 | import { shouldKeepRequest } from '../../tool/shouldKeepRequest' 10 | import { config } from '../../constant' 11 | import { generateMockData } from './generateMockData' 12 | import { generateResponseType } from './generateResponseType' 13 | import { generateRequestOptionType } from './generateRequestOptionType' 14 | 15 | /** 16 | * 为了将mock数据可以暴露出来单独使用,需要的时候配合fetch-mock一起使用 17 | * 且使返回mock数据的请求函数,与实际请求函数的签名保持一致 18 | * 最终采用了这种将mock函数生成单独文件的方法 19 | * 取消了将mock数据混合到实际请求函数中的做法 20 | * */ 21 | export const generateMockRequestContent = (spec: Spec, project: Project) => { 22 | const { apiFilter, requestOptionUnionType } = project 23 | const { requestMap, definitionMap, enumMap } = getGlobal(project) 24 | 25 | const { EOL } = config 26 | 27 | // const resultContent: string[] = [] 28 | Object.getOwnPropertyNames(requestMap).forEach(requestFunctionName => { 29 | const requestTypeScriptContent: string[] = [] 30 | const request = requestMap[requestFunctionName] 31 | const { httpMethod } = request 32 | if (!shouldKeepRequest(request, apiFilter)) { 33 | return 34 | } 35 | 36 | let parameterTypeName = '' 37 | let parameterRequired = false 38 | if (request.parameters && request.parameters.length > 0) { 39 | const parameterType = generateRequestOptionType(requestFunctionName, request.parameters, project) 40 | parameterTypeName = parameterType.parameterTypeName 41 | parameterRequired = parameterType.parameterRequired 42 | requestTypeScriptContent.push(parameterType.parameterTypeContent) 43 | } 44 | const responseType = generateResponseType(requestFunctionName, request.responses, project) 45 | requestTypeScriptContent.push(responseType.responseTypeContent) 46 | requestTypeScriptContent.push(responseType.successTypeContent) 47 | const source = sow() 48 | /** 生成mock data */ 49 | let mockFunctionContent = '' 50 | const mockRequestFunctionSource = sow() 51 | const functionStatment = `return Promise.resolve(mockData)` 52 | const functionData: OptionalKind = { 53 | isExported: false, 54 | returnType: `Promise<${responseType.successTypeName}>`, 55 | statements: functionStatment, 56 | } 57 | functionData.parameters = [] 58 | if (parameterTypeName) { 59 | const type = requestOptionUnionType ? `${parameterTypeName} & ${requestOptionUnionType}` : parameterTypeName 60 | functionData.parameters.push({ 61 | hasQuestionToken: !parameterRequired, 62 | name: 'option', 63 | type, 64 | }) 65 | } else if (requestOptionUnionType) { 66 | functionData.parameters.push({ 67 | hasQuestionToken: true, 68 | name: 'option', 69 | type: requestOptionUnionType, 70 | }) 71 | } 72 | const mockFunctionSource = sow() 73 | mockRequestFunctionSource.addFunction(functionData) 74 | mockFunctionSource.addVariableStatement({ 75 | declarationKind: 'const' as VariableDeclarationKind.Const, 76 | declarations: [ 77 | { 78 | name: 'mockRequest', 79 | initializer(writter) { 80 | writter.write(harvest(mockRequestFunctionSource)) 81 | }, 82 | }, 83 | ], 84 | }) 85 | mockFunctionContent = `const mockData = (${JSON.stringify( 86 | generateMockData(request, definitionMap, enumMap), 87 | )} as unknown as ${responseType.successTypeName}) 88 | ${harvest(mockFunctionSource)} 89 | mockRequest.method = method 90 | mockRequest.url = url 91 | mockRequest.mockData = mockData 92 | return mockRequest 93 | ` 94 | const urlPath = join(spec.basePath || '/', transformSwaggerPathToRouterPath(String(request.pathname))) 95 | const sourceContent = `/* #__PURE__ */ (() => { 96 | /** http method */ 97 | const method = '${httpMethod}' 98 | /** request url */ 99 | const url = '${urlPath}' 100 | ${mockFunctionContent} 101 | })()` 102 | source.addVariableStatement({ 103 | declarationKind: 'const' as VariableDeclarationKind.Const, 104 | docs: assembleDoc(request.schema), 105 | isExported: true, 106 | declarations: [ 107 | { 108 | name: requestFunctionName, 109 | initializer: sourceContent, 110 | }, 111 | ], 112 | }) 113 | requestTypeScriptContent.push(harvest(source)) 114 | /** store typescript content to requestMap */ 115 | request.mockTypescriptContent = requestTypeScriptContent.join(EOL) 116 | // resultContent.push(request.mockTypescriptContent) 117 | }) 118 | 119 | /** return value only for test and debug */ 120 | // return resultContent.join(EOL) 121 | } 122 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/runByCommand.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`run use cleaned spec 1`] = ` 4 | { 5 | "ABCOutputParameters": { 6 | "schema": { 7 | "properties": { 8 | "dueDay": { 9 | "description": "开卡日", 10 | "type": "string", 11 | }, 12 | "requestChannel": { 13 | "description": "进件渠道", 14 | "type": "string", 15 | }, 16 | }, 17 | "required": [ 18 | "dueDay", 19 | "requestChannel", 20 | ], 21 | "type": "object", 22 | }, 23 | "typeName": "ABCOutputParameters", 24 | }, 25 | "DataTransOutput": { 26 | "schema": { 27 | "description": "带数据的返回数据", 28 | "properties": { 29 | "data": { 30 | "description": "返回数据", 31 | "type": "object", 32 | }, 33 | "transCode": { 34 | "description": "错误码。 35 | 100000 成功 36 | 200000 入参不合法 37 | 400000 权限不足 38 | 500000 服务失败", 39 | "format": "int32", 40 | "type": "integer", 41 | }, 42 | "transMessage": { 43 | "description": "错误信息。成功:“成功” 失败:“失败对应的msg”", 44 | "type": "string", 45 | }, 46 | "transMessageDetail": { 47 | "description": "信息详情”", 48 | "type": "string", 49 | }, 50 | }, 51 | "required": [ 52 | "transCode", 53 | "transMessage", 54 | "transMessageDetail", 55 | ], 56 | "type": "object", 57 | }, 58 | "typeName": "DataTransOutput", 59 | }, 60 | "DatatransoutputOutputParameterVO": { 61 | "schema": { 62 | "description": "带数据的返回数据", 63 | "properties": { 64 | "data": { 65 | "$ref": "OutputParameterVo", 66 | "description": "返回数据", 67 | }, 68 | "transCode": { 69 | "description": "错误码。 70 | 100000 成功 71 | 200000 入参不合法 72 | 400000 权限不足 73 | 500000 服务失败", 74 | "format": "int32", 75 | "type": "integer", 76 | }, 77 | "transMessage": { 78 | "description": "错误信息。成功:“成功” 失败:“失败对应的msg”", 79 | "type": "string", 80 | }, 81 | "transMessageDetail": { 82 | "description": "信息详情”", 83 | "type": "string", 84 | }, 85 | }, 86 | "required": [ 87 | "transCode", 88 | "transMessage", 89 | "transMessageDetail", 90 | ], 91 | "type": "object", 92 | }, 93 | "typeName": "DatatransoutputOutputParameterVO", 94 | }, 95 | "GeneralRequestParameterQueryParameter": { 96 | "originalName": undefined, 97 | "schema": undefined, 98 | "typeName": "GeneralRequestParameterQueryParameter", 99 | "typescriptContent": "export type GeneralRequestParameterQueryParameter = any", 100 | }, 101 | "GeneralRequestParameterTokenOutputParameterVO": { 102 | "schema": { 103 | "properties": { 104 | "bizParamVo": { 105 | "$ref": "OutputParameterVo", 106 | }, 107 | }, 108 | "type": "object", 109 | }, 110 | "typeName": "GeneralRequestParameterTokenOutputParameterVO", 111 | }, 112 | "MixedChineseAndEnglishWithSpacesVo": { 113 | "schema": { 114 | "properties": { 115 | "dueDay": { 116 | "description": "开卡日", 117 | "type": "string", 118 | }, 119 | "requestChannel": { 120 | "description": "进件渠道", 121 | "type": "string", 122 | }, 123 | }, 124 | "required": [ 125 | "dueDay", 126 | "requestChannel", 127 | ], 128 | "type": "object", 129 | }, 130 | "typeName": "MixedChineseAndEnglishWithSpacesVo", 131 | }, 132 | "OutputParameterVo": { 133 | "schema": { 134 | "properties": { 135 | "dueDay": { 136 | "description": "开卡日", 137 | "type": "string", 138 | }, 139 | "requestChannel": { 140 | "description": "进件渠道", 141 | "type": "string", 142 | }, 143 | }, 144 | "required": [ 145 | "dueDay", 146 | "requestChannel", 147 | ], 148 | "type": "object", 149 | }, 150 | "typeName": "OutputParameterVo", 151 | }, 152 | "QueryParameters": { 153 | "schema": { 154 | "properties": { 155 | "data": { 156 | "additionalProperties": { 157 | "$ref": "DataTransOutput", 158 | "type": "array", 159 | }, 160 | "type": "object", 161 | }, 162 | "dueDay": { 163 | "description": "开卡日", 164 | "type": "string", 165 | }, 166 | "requestChannel": { 167 | "description": "进件渠道", 168 | "type": "string", 169 | }, 170 | }, 171 | "required": [ 172 | "dueDay", 173 | "requestChannel", 174 | ], 175 | "type": "object", 176 | }, 177 | "typeName": "QueryParameters", 178 | }, 179 | "ResultListMap": { 180 | "schema": { 181 | "properties": { 182 | "description": { 183 | "type": "string", 184 | }, 185 | "result": { 186 | "items": { 187 | "$ref": "Map", 188 | }, 189 | "type": "array", 190 | }, 191 | "returnCode": { 192 | "format": "int32", 193 | "type": "integer", 194 | }, 195 | }, 196 | "type": "object", 197 | }, 198 | "typeName": "ResultListMap", 199 | }, 200 | } 201 | `; 202 | -------------------------------------------------------------------------------- /src/step/generateRequestContent/generateMockData.ts: -------------------------------------------------------------------------------- 1 | /** copy from swagger-ui https://github.com/swagger-api/swagger-ui/blob/master/src/core/plugins/samples/fn.js 2 | * swagger-ui "license": "Apache-2.0" 3 | * modify js to ts and some ts-gear project part change 4 | * */ 5 | 6 | import type { Schema } from 'swagger-schema-official' 7 | import { find, isObject, isFunction, castArray } from 'lodash' 8 | import type { SwaggerRequest, DefinitionMap, EnumMap } from '../../type' 9 | 10 | // Deeply strips a specific key from an object. 11 | // 12 | // `predicate` can be used to discriminate the stripping further, 13 | // by preserving the key's place in the object based on its value. 14 | export function deeplyStripKey(input: any, keyToStrip: string, predicate: (...args: any) => boolean): any { 15 | if (typeof input !== 'object' || Array.isArray(input) || input === null || !keyToStrip) { 16 | return input 17 | } 18 | 19 | const obj = { ...input } 20 | 21 | Object.keys(obj).forEach(k => { 22 | if (k === keyToStrip && predicate(obj[k], k)) { 23 | delete obj[k] 24 | return 25 | } 26 | obj[k] = deeplyStripKey(obj[k], keyToStrip, predicate) 27 | }) 28 | 29 | return obj 30 | } 31 | 32 | function objectify(thing: T): T { 33 | if (!isObject(thing)) return {} as unknown as T 34 | return thing 35 | } 36 | 37 | const primitives = { 38 | string: () => 'string', 39 | string_email: () => 'user@example.com', 40 | 'string_date-time': () => new Date('2019-09-03').toISOString(), 41 | string_date: () => new Date('2019-09-03').toISOString().substring(0, 10), 42 | string_uuid: () => '3fa85f64-5717-4562-b3fc-2c963f66afa6', 43 | string_hostname: () => 'example.com', 44 | string_ipv4: () => '198.51.100.42', 45 | string_ipv6: () => '2001:0db8:5b96:0000:0000:426f:8e17:642a', 46 | number: () => 0, 47 | number_float: () => 0.0, 48 | integer: () => 0, 49 | boolean: (schema: any) => (typeof schema.default === 'boolean' ? schema.default : true), 50 | } 51 | 52 | type PrimitivesKeys = keyof typeof primitives 53 | 54 | const primitive = (schema: Schema): any => { 55 | schema = objectify(schema) 56 | const { type, format } = schema 57 | const key = `${type}_${format}` as PrimitivesKeys 58 | 59 | const fn = primitives[key] || primitives[type as PrimitivesKeys] 60 | 61 | if (isFunction(fn)) return fn(schema) 62 | 63 | throw new Error(`Unknown Type: ${schema.type}`) 64 | } 65 | 66 | /** 67 | * prevent schema circle reference 68 | * */ 69 | const schemaSet = new Set() 70 | 71 | export const sampleFromSchema = (schema: Schema, definitionMap: DefinitionMap, enumMap: EnumMap): any => { 72 | if (schemaSet.has(schema)) { 73 | return '' 74 | } 75 | schemaSet.add(schema) 76 | let { type } = objectify(schema) as any 77 | const { example, properties, additionalProperties, items, $ref, schema: schemaSchema } = objectify(schema) as any 78 | 79 | if (example !== undefined) { 80 | const r: any = deeplyStripKey( 81 | example, 82 | '$$ref', 83 | (val: any) => 84 | // do a couple of quick sanity tests to ensure the value 85 | // looks like a $$ref that swagger-client generates. 86 | typeof val === 'string' && val.indexOf('#') > -1, 87 | ) 88 | return r 89 | } 90 | 91 | if (!type) { 92 | if (properties) { 93 | type = 'object' 94 | } else if (items) { 95 | type = 'array' 96 | } else if ($ref) { 97 | const definitionSchema = definitionMap[$ref] && definitionMap[$ref].schema 98 | if (definitionSchema) { 99 | return sampleFromSchema(definitionSchema, definitionMap, enumMap) 100 | } 101 | return '' 102 | } else if (schemaSchema) { 103 | return sampleFromSchema(schemaSchema, definitionMap, enumMap) 104 | } else { 105 | return '' 106 | } 107 | } 108 | 109 | if (type === 'object') { 110 | const props = objectify(properties) 111 | const obj: any = {} 112 | Object.getOwnPropertyNames(props).forEach(name => { 113 | if (!(props[name] && props[name].deprecated)) { 114 | obj[name] = sampleFromSchema(props[name], definitionMap, enumMap) 115 | } 116 | }) 117 | 118 | if (additionalProperties === true) { 119 | obj.additionalProp1 = {} 120 | } else if (additionalProperties) { 121 | const additionalProps = objectify(additionalProperties) 122 | const additionalPropVal = sampleFromSchema(additionalProps, definitionMap, enumMap) 123 | 124 | for (let i = 1; i < 4; i += 1) { 125 | obj[`additionalProp${i}`] = additionalPropVal 126 | } 127 | } 128 | return obj 129 | } 130 | 131 | if (type === 'array') { 132 | if (Array.isArray(items.anyOf)) { 133 | return items.anyOf.map((i: Schema) => sampleFromSchema(i, definitionMap, enumMap)) 134 | } 135 | 136 | if (Array.isArray(items.oneOf)) { 137 | return items.oneOf.map((i: Schema) => sampleFromSchema(i, definitionMap, enumMap)) 138 | } 139 | 140 | return [sampleFromSchema(items, definitionMap, enumMap)] 141 | } 142 | 143 | if (schema.enum) { 144 | if (schema.default) { 145 | return schema.default 146 | } 147 | if (enumMap[String(schema.enum)]) { 148 | return castArray(enumMap[String(schema.enum)].originalContent)[0] 149 | } 150 | } 151 | 152 | if (type === 'file') { 153 | return '' 154 | } 155 | 156 | return primitive(schema) 157 | } 158 | 159 | export const generateMockData = (request: SwaggerRequest, definitionMap: DefinitionMap, enumMap: EnumMap) => { 160 | schemaSet.clear() 161 | if (request.responses) { 162 | const schema = find(request.responses, (v, k) => k === 'default' || k.startsWith('2')) 163 | if (schema) { 164 | return sampleFromSchema(schema, definitionMap, enumMap) 165 | } 166 | } 167 | return '' 168 | } 169 | -------------------------------------------------------------------------------- /src/step/generateRequestContent/index.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDeclarationStructure, OptionalKind, VariableDeclarationKind } from 'ts-morph' 2 | import type { Spec } from 'swagger-schema-official' 3 | import * as join from 'url-join' 4 | import type { Project } from '../../type' 5 | import { sow, harvest } from '../../source' 6 | import { transformSwaggerPathToRouterPath } from '../../tool/transformSwaggerPathToRouterPath' 7 | import { getGlobal } from '../../projectGlobalVariable' 8 | import { assembleDoc } from '../../tool/assembleDoc' 9 | import { config } from '../../constant' 10 | import { shouldKeepRequest } from '../../tool/shouldKeepRequest' 11 | import { generateResponseType } from './generateResponseType' 12 | import { generateRequestOptionType } from './generateRequestOptionType' 13 | 14 | /** from swagger spec paths assemble request functions */ 15 | export const generateRequestContent = (spec: Spec, project: Project) => { 16 | const { apiFilter, withBasePath, withHost, simplifyRequestOption, requestOptionUnionType } = project 17 | const { requestMap } = getGlobal(project) 18 | const { EOL } = config 19 | 20 | const { generateRequestFunction } = project 21 | 22 | const resultContent: string[] = [] 23 | Object.getOwnPropertyNames(requestMap).forEach(requestFunctionName => { 24 | const requestTypeScriptContent: string[] = [] 25 | const request = requestMap[requestFunctionName] 26 | const { httpMethod } = request 27 | if (!shouldKeepRequest(request, apiFilter)) { 28 | return 29 | } 30 | 31 | let parameterTypeName = '' 32 | let parameterRequired = false 33 | if (request.parameters && request.parameters.length > 0) { 34 | const parameterType = generateRequestOptionType(requestFunctionName, request.parameters, project) 35 | parameterTypeName = parameterType.parameterTypeName 36 | parameterRequired = parameterType.parameterRequired 37 | requestTypeScriptContent.push(parameterType.parameterTypeContent) 38 | } 39 | const responseType = generateResponseType(requestFunctionName, request.responses, project) 40 | requestTypeScriptContent.push(responseType.responseTypeContent) 41 | requestTypeScriptContent.push(responseType.successTypeContent) 42 | const basePath = project.withBasePath ? spec.basePath : null 43 | const urlPath = join(basePath || '/', transformSwaggerPathToRouterPath(String(request.pathname))) 44 | const source = sow() 45 | const requestFunctionSource = sow() 46 | let simpleOption = '' 47 | if (request.parameters) { 48 | const positionSet = new Set(request.parameters.map((p: any) => p.in)) 49 | if (simplifyRequestOption && positionSet.size === 1 && !requestOptionUnionType) { 50 | let position = Array.from(positionSet)[0] 51 | if (position === 'formData') { 52 | position = 'body' 53 | } 54 | simpleOption = `${position}: option` 55 | } 56 | } 57 | const requesterStatment = `return requester(request.url, {${[ 58 | withHost && spec.host ? `host: '${spec.host}'` : '', 59 | withBasePath && spec.basePath ? `basePath: '${spec.basePath}'` : '', 60 | 'method: request.method', 61 | simpleOption || (parameterTypeName || requestOptionUnionType ? '...option' : ''), 62 | ] 63 | .filter(Boolean) 64 | .join(',')}}) as unknown as Promise<${responseType.successTypeName}>` 65 | /** 生成mock data */ 66 | const functionStatment = requesterStatment 67 | const functionData: OptionalKind = { 68 | name: 'request', 69 | isExported: false, 70 | returnType: `Promise<${responseType.successTypeName}>`, 71 | statements: functionStatment, 72 | } 73 | functionData.parameters = [] 74 | if (parameterTypeName) { 75 | const type = requestOptionUnionType ? `${parameterTypeName} & ${requestOptionUnionType}` : parameterTypeName 76 | functionData.parameters.push({ 77 | hasQuestionToken: !parameterRequired, 78 | name: 'option', 79 | type, 80 | }) 81 | } else if (requestOptionUnionType) { 82 | functionData.parameters.push({ 83 | hasQuestionToken: true, 84 | name: 'option', 85 | type: requestOptionUnionType, 86 | }) 87 | } 88 | requestFunctionSource.addFunction(functionData) 89 | const sourceContent = `/* #__PURE__ */ (() => { 90 | const method = '${httpMethod}' 91 | const url = '${urlPath}' 92 | ${harvest(requestFunctionSource)} 93 | /** http method */ 94 | request.method = method 95 | /** request url */ 96 | request.url = url 97 | return request 98 | })()` 99 | source.addVariableStatement({ 100 | declarationKind: 'const' as VariableDeclarationKind.Const, 101 | docs: assembleDoc(request.schema), 102 | isExported: true, 103 | declarations: [ 104 | { 105 | name: requestFunctionName, 106 | initializer: generateRequestFunction 107 | ? generateRequestFunction({ 108 | httpMethod, 109 | pathname: request.pathname, 110 | schema: spec, 111 | originSource: sourceContent, 112 | project, 113 | parameterRequired, 114 | parameterTypeName, 115 | responseSuccessTypeName: responseType.successTypeName, 116 | }) 117 | : sourceContent, 118 | }, 119 | ], 120 | }) 121 | requestTypeScriptContent.push(harvest(source)) 122 | /** store typescript content to requestMap */ 123 | request.typescriptContent = requestTypeScriptContent.join(EOL) 124 | resultContent.push(request.typescriptContent) 125 | }) 126 | 127 | /** return value only for test and debug */ 128 | // return resultContent.join(EOL) 129 | } 130 | 131 | export * from './generateMockRequestContent' 132 | --------------------------------------------------------------------------------