From ca48e46215d3d2d251041e541efb236851a22ba1 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Wed, 25 Jan 2023 02:07:44 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=A2=9E=E5=8A=A0service=20action?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E8=84=9A=E6=9C=AC=E7=94=A8=E4=BA=8E=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90swagger=20jsdoc=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 61 +++++- server/package.json | 1 + .../swagger-jsdoc-generator/package.json | 29 +++ .../swagger-jsdoc-generator/src/index.ts | 31 +++ .../src/processService.ts | 205 ++++++++++++++++++ .../swagger-jsdoc-generator/src/utils.ts | 10 + .../swagger-jsdoc-generator/tsconfig.json | 11 + 7 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 server/packages/swagger-jsdoc-generator/package.json create mode 100644 server/packages/swagger-jsdoc-generator/src/index.ts create mode 100644 server/packages/swagger-jsdoc-generator/src/processService.ts create mode 100644 server/packages/swagger-jsdoc-generator/src/utils.ts create mode 100644 server/packages/swagger-jsdoc-generator/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ae597b4..2d5b8df1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -809,6 +809,7 @@ importers: socket.io-client: ^4.1.3 swagger-jsdoc: ^6.2.8 tailchat-server-sdk: workspace:* + tailchat-service-swagger-generator: workspace:^1.0.0 ts-jest: 27.1.4 ts-node: ^10.0.0 typescript: ^4.3.3 @@ -887,6 +888,7 @@ importers: prettier: 2.7.1 socket.io-client: 4.5.1 swagger-jsdoc: 6.2.8 + tailchat-service-swagger-generator: link:packages/swagger-jsdoc-generator ts-jest: 27.1.4_r5n7iohbfbguzk5ispbdybm75m vinyl-fs: 3.0.3 @@ -984,6 +986,21 @@ importers: devDependencies: typescript: 4.7.4 + server/packages/swagger-jsdoc-generator: + specifiers: + '@types/node': ^18.11.18 + globby: 11.1.0 + ts-morph: ^16.0.0 + ts-node: ^10.9.1 + typescript: ^4.9.4 + dependencies: + globby: 11.1.0 + ts-morph: 16.0.0 + ts-node: 10.9.1_awa2wsr5thmg3i7jqycphctjfq + devDependencies: + '@types/node': 18.11.18 + typescript: 4.9.4 + server/plugins/com.msgbyte.agora: specifiers: '@rollup/plugin-replace': ^5.0.2 @@ -11403,6 +11420,9 @@ packages: /@types/node/18.11.16: resolution: {integrity: sha512-6T7P5bDkRhqRxrQtwj7vru+bWTpelgtcETAZEUSdq0YISKz8WKdoBukQLYQQ6DFHvU9JRsbFq0JH5C51X2ZdnA==} + /@types/node/18.11.18: + resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + /@types/node/18.7.11: resolution: {integrity: sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==} dev: true @@ -16679,10 +16699,6 @@ packages: character-entities: 2.0.2 dev: false - /decode-uri-component/0.2.0: - resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} - engines: {node: '>=0.10'} - /decode-uri-component/0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -28466,7 +28482,7 @@ packages: resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} engines: {node: '>=0.10.0'} dependencies: - decode-uri-component: 0.2.0 + decode-uri-component: 0.2.2 object-assign: 4.1.1 strict-uri-encode: 1.1.0 dev: true @@ -28484,7 +28500,7 @@ packages: resolution: {integrity: sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==} engines: {node: '>=6'} dependencies: - decode-uri-component: 0.2.0 + decode-uri-component: 0.2.2 filter-obj: 1.1.0 split-on-first: 1.1.0 strict-uri-encode: 2.0.0 @@ -32364,7 +32380,7 @@ packages: deprecated: See https://github.com/lydell/source-map-resolve#deprecated dependencies: atob: 2.1.2 - decode-uri-component: 0.2.0 + decode-uri-component: 0.2.2 resolve-url: 0.2.1 source-map-url: 0.4.1 urix: 0.1.0 @@ -34117,6 +34133,37 @@ packages: tweetnacl: 1.0.3 dev: true + /ts-node/10.9.1_awa2wsr5thmg3i7jqycphctjfq: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 18.11.18 + acorn: 8.8.1 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: false + /ts-node/10.9.1_k2dsl7zculo2nmh5s33pladmoa: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true diff --git a/server/package.json b/server/package.json index 787b3bcc..c6299ec7 100644 --- a/server/package.json +++ b/server/package.json @@ -110,6 +110,7 @@ "prettier": "^2.3.2", "socket.io-client": "^4.1.3", "swagger-jsdoc": "^6.2.8", + "tailchat-service-swagger-generator": "workspace:^1.0.0", "ts-jest": "27.1.4", "vinyl-fs": "^3.0.3" }, diff --git a/server/packages/swagger-jsdoc-generator/package.json b/server/packages/swagger-jsdoc-generator/package.json new file mode 100644 index 00000000..0bd14f88 --- /dev/null +++ b/server/packages/swagger-jsdoc-generator/package.json @@ -0,0 +1,29 @@ +{ + "name": "tailchat-service-swagger-generator", + "private": true, + "version": "1.0.0", + "description": "", + "main": "index.js", + "bin": "./dist/index.js", + "scripts": { + "dev": "tsc --watch", + "prepare": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "msgbyte", + "moonrailgun", + "tailchat" + ], + "author": "moonrailgun ", + "license": "MIT", + "dependencies": { + "globby": "11.1.0", + "ts-morph": "^16.0.0", + "ts-node": "^10.9.1" + }, + "devDependencies": { + "@types/node": "^18.11.18", + "typescript": "^4.9.4" + } +} diff --git a/server/packages/swagger-jsdoc-generator/src/index.ts b/server/packages/swagger-jsdoc-generator/src/index.ts new file mode 100644 index 00000000..3883b5d7 --- /dev/null +++ b/server/packages/swagger-jsdoc-generator/src/index.ts @@ -0,0 +1,31 @@ +import { Project } from 'ts-morph'; +import path from 'path'; +import globby from 'globby'; +import { processService } from './processService'; + +/** + * https://ts-morph.com/setup/ + */ + +/** + * 扫描服务 + */ +async function scanServices() { + const serviceFiles = await globby('./services/**/*.service.ts'); + + console.time('parse project usage'); + const project = new Project({ + tsConfigFilePath: path.resolve(process.cwd(), './tsconfig.json'), + }); + console.timeEnd('parse project usage'); + + console.time('parse source usage'); + + // 单个测试 + const sourceFile = project.getSourceFileOrThrow(serviceFiles[0]); + processService(sourceFile); + + console.timeEnd('parse source usage'); +} + +scanServices(); diff --git a/server/packages/swagger-jsdoc-generator/src/processService.ts b/server/packages/swagger-jsdoc-generator/src/processService.ts new file mode 100644 index 00000000..0c5e2fdf --- /dev/null +++ b/server/packages/swagger-jsdoc-generator/src/processService.ts @@ -0,0 +1,205 @@ +import { SourceFile, SyntaxKind, TypeReferenceNode } from 'ts-morph'; +import { getMethodParameters } from './utils'; + +/** + * 解析并处理 service 文件, 把所有的 Action 都自动加上jsdoc + */ +export async function processService(sourceFile: SourceFile) { + const serviceNameDeclaration = sourceFile + .getDescendantsOfKind(SyntaxKind.GetAccessor) + .find((item) => item.getSymbol().getName() === 'serviceName'); + + if (!serviceNameDeclaration) { + // 没有定义 serviceName, 不是一个正确的服务,跳过 + return; + } + + // 目前只先视为一个文件只有一个service,不考虑多个 + const serviceName = serviceNameDeclaration + .getFirstDescendantByKind(SyntaxKind.ReturnStatement) + .getExpression() + .asKindOrThrow(SyntaxKind.StringLiteral) + .getLiteralText(); + + console.log('process service:', serviceName); + + const actions = findActionMethods(sourceFile); + + for (const action of actions) { + const jsdocs = action.getJsDocs(); + const len = jsdocs.length; + const lastJsDoc = jsdocs[len - 1]; // 最后一条记录 + if (lastJsDoc && lastJsDoc.getText().includes('@swagger')) { + continue; + } + + const actionName = action.getSymbol().getEscapedName(); + + const requestTypeReferenceNode = action + .getFirstChildByKind(SyntaxKind.Parameter) + .getFirstChildByKind(SyntaxKind.TypeReference); + + const text = generateOpenapiSchemaText( + serviceName, + actionName, + lastJsDoc?.getCommentText().replaceAll('\n', ' ') ?? '', + getPropertySignatureToSwagger(requestTypeReferenceNode) + ); + + if (lastJsDoc) { + // 移除以前的注释 + lastJsDoc.remove(); + } + + // 将之前的描述填写到里面 + action.addJsDoc(text); + } + + await sourceFile.save(); // 将改动保存到原文件中 +} + +/** + * 获取Action方法列表 + */ +function findActionMethods(sourceFile: SourceFile) { + const actions = sourceFile + .getDescendantsOfKind(SyntaxKind.MethodDeclaration) + .filter((item) => { + const parameters = getMethodParameters(item); + if ( + parameters.length === 1 && + ['TcPureContext', 'TcContext'].includes( + parameters[0].getType().getSymbol().getEscapedName() + ) + ) { + return true; + } + + return false; + }); + + return actions; +} + +interface SwaggerParamType { + name: string; + type: 'string' | 'integer' | 'string[]' | 'integer[]'; // https://graphql-faas.github.io/OpenAPI-Specification/versions/2.0.html#data-types +} + +function generateOpenapiSchemaText( + serviceName: string, + actionName: string, + description: string, + requestParams: SwaggerParamType[], + responseParams?: 'boolean' | SwaggerParamType[] +) { + actionName = actionName.replaceAll('.', '/'); + + const responseData = responseParams + ? responseParams === 'boolean' + ? `data: + type: boolean` + : `data: + type: object + properties: + ${generateProperties(responseParams, 4)}` + : ''; + + return `@swagger + +/api/${actionName}/${serviceName}: + post: + tags: + - ${actionName} + description: ${description} + requestBody: + content: + application/json: + schema: + type: object + properties: + ${generateProperties(requestParams, 14)} + responses: + 200: + content: + application/json: + schema: + type: object + properties: + code: + type: integer + example: 200 + message: + type: string + example: ok + ${paddingWithIndent(responseData, 16)} +`; +} + +function generateProperties( + params: SwaggerParamType[], + indent: number +): string { + return paddingWithIndent( + params + .map((p) => { + if (p.type.endsWith('[]')) { + const t = p.type.substring(0, p.type.length - 2); + return `${p.name}: + type: array + items: + type: ${t}`; + } else { + return `${p.name}: + type: ${p.type}`; + } + }) + .join('\n'), + indent + ); +} + +function paddingWithIndent(text: string, indent: number) { + let indentText = ''; + Array.from({ length: indent }).forEach(() => { + indentText += ' '; + }); + + return text.split('\n').join('\n' + indentText); +} + +/** + * 将ts的类型转换为swagger可以认识的类型 + */ +function getPropertySignatureToSwagger( + typeReferenceNode: TypeReferenceNode +): SwaggerParamType[] { + if (!typeReferenceNode) { + return []; + } + + return typeReferenceNode + .getDescendantsOfKind(SyntaxKind.PropertySignature) + .map((item) => { + const name = item.getName(); + const type = item.getType(); + + let typeText: SwaggerParamType['type'] = 'string'; + if (type.isArray()) { + typeText = 'string[]'; + + if (type.isNumber()) { + typeText = 'integer[]'; + } + } else { + if (type.isNumber()) { + typeText = 'integer'; + } + } + + return { + name, + type: typeText, + }; + }); +} diff --git a/server/packages/swagger-jsdoc-generator/src/utils.ts b/server/packages/swagger-jsdoc-generator/src/utils.ts new file mode 100644 index 00000000..84ae39f5 --- /dev/null +++ b/server/packages/swagger-jsdoc-generator/src/utils.ts @@ -0,0 +1,10 @@ +import { MethodDeclaration, ParameterDeclaration, SyntaxKind } from 'ts-morph'; + +/** + * 获取函数参数列表 + */ +export function getMethodParameters( + methodDeclaration: MethodDeclaration +): ParameterDeclaration[] { + return methodDeclaration.getChildrenOfKind(SyntaxKind.Parameter); +} diff --git a/server/packages/swagger-jsdoc-generator/tsconfig.json b/server/packages/swagger-jsdoc-generator/tsconfig.json new file mode 100644 index 00000000..5138ab83 --- /dev/null +++ b/server/packages/swagger-jsdoc-generator/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist", + "typeRoots": ["./node_modules/@types"], + }, + "include": ["./src/**/*"], + "exclude": ["./node_modules/**/*", "./dist/**/*"] +}