chore: 增加service action扫描脚本用于自动生成swagger jsdoc文档

feat/uniplus
moonrailgun 2 years ago
parent fbf4205dfb
commit ca48e46215

@ -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

@ -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"
},

@ -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 <moonrailgun@gmail.com>",
"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"
}
}

@ -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();

@ -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);
}
/**
* tsswagger
*/
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,
};
});
}

@ -0,0 +1,10 @@
import { MethodDeclaration, ParameterDeclaration, SyntaxKind } from 'ts-morph';
/**
*
*/
export function getMethodParameters(
methodDeclaration: MethodDeclaration
): ParameterDeclaration[] {
return methodDeclaration.getChildrenOfKind(SyntaxKind.Parameter);
}

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": ["./node_modules/@types"],
},
"include": ["./src/**/*"],
"exclude": ["./node_modules/**/*", "./dist/**/*"]
}
Loading…
Cancel
Save