@jayfong/x-server 2.30.3 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,131 +1,222 @@
1
- import * as parseComment from 'comment-parser';
1
+ import path from 'path';
2
2
  import createDebug from 'debug';
3
3
  import fs from 'fs-extra';
4
- import path from 'node:path';
5
- import * as ts from 'ts-morph';
6
- import { ii, pascalCase, snakeCase } from 'vtils';
4
+ import mo, { ts } from 'ts-morph';
5
+ import { ii, pascalCase } from 'vtils';
7
6
  import { HandlerMethodToHttpMethod } from "../core/http_method";
7
+
8
+ /** 注释 */
9
+
10
+ /** 接口传输数据 */
11
+
12
+ /** 接口数据 */
13
+
8
14
  export class ApiGenerator {
9
- constructor() {
15
+ constructor(cwd = process.cwd()) {
16
+ this.cwd = cwd;
10
17
  this.debug = createDebug('api');
11
- this.cwd = process.cwd();
12
- // cwd = '/Users/admin/Documents/jfWorks/x-server-test'
13
- this.project = void 0;
18
+ // 几种情况:
19
+ // /index.ts
20
+ // /user/index.ts
21
+ // /post.ts
22
+ // /user/list.ts
23
+ this.categoryUrlRe = /^.+\/src\/handlers\/(?:index\.ts|(.*)\/index\.ts|(.*)\.ts)$/;
24
+ this.checker = void 0;
14
25
  }
15
- getTypeBySymbol(symbol) {
16
- return symbol.getTypeAtLocation(symbol.getDeclarations()[0] || symbol.getValueDeclarationOrThrow());
26
+ async start() {
27
+ this.debug('启动项目...');
28
+ const tsConfigPath = path.join(this.cwd, 'tsconfig.json');
29
+ const handlersPath = path.join(this.cwd, 'node_modules/.x/handlers.ts');
30
+ const entryFile = path.join(this.cwd, 'src/generated/handlers.ts');
31
+
32
+ // 仅使用 ts-morph 初始化项目
33
+ const moProject = new mo.Project({
34
+ tsConfigFilePath: tsConfigPath
35
+ });
36
+ moProject.createSourceFile(entryFile, await fs.readFile(handlersPath, 'utf-8'));
37
+ const program = moProject.getProgram().compilerObject;
38
+ const checker = program.getTypeChecker();
39
+ this.checker = checker;
40
+ const moduleSymbol = checker.getSymbolAtLocation(program.getSourceFile(entryFile));
41
+ const categorySymbols = checker.getExportsOfModule(moduleSymbol);
42
+ const apiData = [];
43
+ for (const categorySymbol of categorySymbols) {
44
+ var _ref, _categoryUrlMatch$;
45
+ // 分类的类型
46
+ const categoryType = checker.getTypeOfSymbolAtLocation(categorySymbol, categorySymbol.getDeclarations()[0]);
47
+
48
+ // 处理器列表
49
+ const handlerSymbols = categoryType.getProperties();
50
+
51
+ // 分类源文件
52
+ const categorySourceFile = handlerSymbols[0].getDeclarations()[0].getSourceFile();
53
+
54
+ // 分类源文件路径
55
+ const categorySourceFilePath = categorySourceFile.fileName;
56
+
57
+ // 分类 URL
58
+ const categoryUrlMatch = categorySourceFilePath.match(this.categoryUrlRe);
59
+ let categoryUrl = (_ref = (_categoryUrlMatch$ = categoryUrlMatch[1]) != null ? _categoryUrlMatch$ : categoryUrlMatch[2]) != null ? _ref : '';
60
+ categoryUrl = categoryUrl === '' ? '/' : `/${categoryUrl}/`;
61
+ for (const handlerSymbol of handlerSymbols) {
62
+ // 处理器名称
63
+ const handlerName = handlerSymbol.getName();
64
+
65
+ // 处理器 URL
66
+ const handlerUrl = `${categoryUrl}${handlerName}`;
67
+
68
+ // 处理器注释
69
+ const handlerComment = this.getComment(handlerSymbol);
70
+ if (handlerComment.tags.has('private')) {
71
+ this.debug('跳过生成接口: %s', `${handlerUrl}`);
72
+ continue;
73
+ }
74
+ this.debug('生成接口: %s ...', handlerUrl);
75
+ const handlerNode = handlerSymbol.getDeclarations()[0];
76
+ const handlerType = checker.getTypeAtLocation(handlerNode);
77
+ const [requestType, responseType, methodType] = checker.getTypeArguments(handlerType);
78
+ const handlerCategory = pascalCase(categoryUrl) || 'Index';
79
+ const handlerDescription = handlerComment.description || handlerUrl;
80
+ const handlerMethod = methodType.value;
81
+ const httpMethod = HandlerMethodToHttpMethod[handlerMethod];
82
+ const requestData = this.getApiDto({
83
+ type: requestType
84
+ });
85
+ const responseData = this.getApiDto({
86
+ type: responseType
87
+ });
88
+ const requestDataJsonSchema = this.apiDataToJsonSchema(requestData);
89
+ const responseDataJsonSchema = this.apiDataToJsonSchema(responseData);
90
+ apiData.push({
91
+ category: handlerCategory,
92
+ name: handlerDescription,
93
+ path: handlerUrl,
94
+ handlerMethod: handlerMethod,
95
+ method: httpMethod,
96
+ requestData: requestData,
97
+ responseData: responseData,
98
+ requestDataJsonSchema: requestDataJsonSchema,
99
+ responseDataJsonSchema: responseDataJsonSchema
100
+ });
101
+ }
102
+ }
103
+ this.debug('写入文件...');
104
+ await Promise.all([fs.outputJSON(path.join(this.cwd, 'temp/api.json'), apiData, {
105
+ spaces: 2
106
+ }), fs.outputJSON(path.join(this.cwd, 'temp/yapi.json'), this.genYApiData(apiData), {
107
+ spaces: 2
108
+ })]);
17
109
  }
18
- getComment(declaration) {
19
- var _declaration$getLeadi;
20
- const text = ((declaration == null || (_declaration$getLeadi = declaration.getLeadingCommentRanges()[0]) == null ? void 0 : _declaration$getLeadi.getText()) || '').trim();
21
- const comment = parseComment.parse(text)[0];
22
- const description = (comment == null ? void 0 : comment.description) || '';
23
- const tags = new Map();
24
- comment == null ? void 0 : comment.tags.forEach(tag => {
25
- tags.set(tag.tag, tag.description);
110
+
111
+ // ref: https://github.com/styleguidist/react-docgen-typescript/blob/master/src/parser.ts#L777
112
+ getComment(symbol) {
113
+ if (!symbol) {
114
+ return {
115
+ existing: false,
116
+ description: '',
117
+ tags: new Map()
118
+ };
119
+ }
120
+ const description = ts.displayPartsToString(symbol.getDocumentationComment(this.checker)).trim();
121
+ const tags = symbol.getJsDocTags().map(item => {
122
+ return {
123
+ label: item.name,
124
+ value: ts.displayPartsToString(item.text).trim()
125
+ };
26
126
  });
27
127
  return {
28
- existing: !!comment,
128
+ existing: !!description,
29
129
  description: description,
30
- tags: tags
130
+ tags: tags.reduce((res, item) => {
131
+ res.set(item.label, item.value);
132
+ return res;
133
+ }, new Map())
31
134
  };
32
135
  }
33
- getCommentBySymbol(symbol) {
34
- var _this$getComment;
35
- return ((_this$getComment = this.getComment(symbol.getDeclarations()[0])) == null ? void 0 : _this$getComment.description) || '';
36
- }
37
- typeToApiData(type, _symbol) {
38
- var _type$getSymbol, _type$getSymbol2, _type$getSymbol3;
39
- // ws
40
- if (((_type$getSymbol = type.getSymbol()) == null ? void 0 : _type$getSymbol.getName()) === 'SocketStream') {
136
+ getApiDto({
137
+ type,
138
+ keySymbol = type.symbol || type.aliasSymbol
139
+ }) {
140
+ var _valueSymbol;
141
+ const hasTypeFlag = (flag, _type = type) => (_type.flags & flag) === flag;
142
+ let valueSymbol = type.symbol || type.aliasSymbol;
143
+ const keySymbolName = (keySymbol == null ? void 0 : keySymbol.getName()) || '';
144
+ let valueSymbolName = ((_valueSymbol = valueSymbol) == null ? void 0 : _valueSymbol.getName()) || '';
145
+ const comment = this.getComment(keySymbol);
146
+ let isRequired = keySymbol ? !(keySymbol.getFlags() & ts.SymbolFlags.Optional) : false;
147
+
148
+ // WS
149
+ if (valueSymbolName === 'SocketStream') {
41
150
  return {
42
151
  name: 'ws',
43
- desc: 'ws',
44
- required: false,
152
+ desc: comment.description,
153
+ required: isRequired,
45
154
  type: 'object',
46
- children: [],
47
- enum: []
155
+ enum: [],
156
+ children: []
48
157
  };
49
158
  }
50
159
 
51
- // XFile
52
- if (((_type$getSymbol2 = type.getSymbol()) == null ? void 0 : _type$getSymbol2.getName()) === 'MultipartFile') {
53
- const symbol = _symbol || type.getSymbol();
160
+ // 文件
161
+ if (valueSymbolName === 'MultipartFile') {
54
162
  return {
55
163
  name: 'file',
56
- desc: symbol && this.getCommentBySymbol(symbol) || '',
57
- required: !!symbol && !(symbol.getFlags() & ts.SymbolFlags.Optional),
164
+ desc: comment.description,
165
+ required: isRequired,
58
166
  type: 'file',
59
- children: [],
60
- enum: []
167
+ enum: [],
168
+ children: []
61
169
  };
62
170
  }
63
- let isRequired = true;
64
- let isUnion = type.isUnion();
65
- const unionTypes = isUnion ? type.getUnionTypes().filter(item => !item.isBooleanLiteral() && !item.isNull() && !item.isUndefined()) : [];
66
- isUnion = !!unionTypes.length;
171
+
172
+ // 联合类型
173
+ // 布尔、枚举也会用这表示
174
+ const rawUnionTypes = type.isUnion() && type.types || [];
175
+ const unionTypes = rawUnionTypes.filter(item => !hasTypeFlag(ts.TypeFlags.Null, item) && !hasTypeFlag(ts.TypeFlags.Undefined, item));
176
+ const isUnion = !!unionTypes.length;
67
177
  if (isUnion) {
68
- if (unionTypes.length === 1 && !unionTypes[0].isLiteral()) {
69
- isUnion = false;
178
+ var _this$checker$getBase;
179
+ if (unionTypes.length !== rawUnionTypes.length) {
180
+ isRequired = false;
70
181
  }
71
- // 兼容 prisma 生成的类型用 null 表示可选
72
- isRequired = unionTypes.length === type.getUnionTypes().length;
73
- // 必须用 getBaseTypeOfLiteralType 获取枚举字面量的原始类型
74
- type = unionTypes[0].getBaseTypeOfLiteralType();
182
+ type = unionTypes[0];
183
+ valueSymbol = type.symbol || type.aliasSymbol;
184
+ valueSymbolName = ((_this$checker$getBase = this.checker.getBaseTypeOfLiteralType(type).getSymbol()) == null ? void 0 : _this$checker$getBase.getName()) || '';
75
185
  }
76
- const isEnum = type.isEnum();
77
- const enumData = isEnum ?
78
- // @ts-ignore
79
- type.compilerType.types.reduce((res, item) => {
80
- res[item.getSymbol().getName()] = item.value;
81
- return res;
82
- }, {}) : {};
83
- const enumKeys = Object.keys(enumData);
84
- const enumValues = Object.values(enumData);
85
186
  const isIntersection = type.isIntersection();
86
- const intersectionTypes = isIntersection && type.getIntersectionTypes();
87
- const isArray = type.isArray();
88
- const isString = isEnum ? typeof enumValues[0] === 'string' : type.isString() || ['Date'].includes(((_type$getSymbol3 = type.getSymbol()) == null ? void 0 : _type$getSymbol3.getName()) || '');
89
- const isNumber = isEnum ? typeof enumValues[0] === 'number' : type.isNumber();
90
- const isBoolean = type.isBoolean() || type.isUnion() && type.getUnionTypes().some(item => item.isBooleanLiteral()) && type.getUnionTypes().every(item => item.isBooleanLiteral() || item.isNull() || item.isUndefined());
91
- const isObject = !isArray && !isString && !isNumber && !isBoolean && type.isObject() ||
92
- // 将交集类型视为对象
187
+ const intersectionTypes = isIntersection && type.types || [];
188
+ const isDate = valueSymbolName === 'Date';
189
+ const isBigIntLiteral = hasTypeFlag(ts.TypeFlags.BigIntLiteral);
190
+ const isBigInt = hasTypeFlag(ts.TypeFlags.BigInt) || isBigIntLiteral;
191
+ const isArray = valueSymbolName === 'Array' || valueSymbolName === 'ReadonlyArray';
192
+ const isStringLiteral = hasTypeFlag(ts.TypeFlags.StringLiteral);
193
+ const isString = hasTypeFlag(ts.TypeFlags.String) || isStringLiteral ||
194
+ // BigInt 类型视为字符串
195
+ isBigInt ||
196
+ // Date 类型视为字符串
197
+ isDate;
198
+ const isNumberLiteral = hasTypeFlag(ts.TypeFlags.NumberLiteral);
199
+ const isNumber = hasTypeFlag(ts.TypeFlags.Number) || isNumberLiteral;
200
+ const isBoolean = hasTypeFlag(ts.TypeFlags.Boolean) || hasTypeFlag(ts.TypeFlags.BooleanLiteral);
201
+ const isLiteral = isStringLiteral || isNumberLiteral || isBigIntLiteral;
202
+ const isObject = !isArray && !isString && !isNumber && !isBoolean && hasTypeFlag(ts.TypeFlags.Object) ||
203
+ // 交集类型视为对象
93
204
  isIntersection;
94
- const symbol = _symbol || type.getSymbol();
95
- const parentSymbol = symbol;
96
- const apiName = (symbol == null ? void 0 : symbol.getName()) || '__type';
97
- const apiDesc = [symbol && this.getCommentBySymbol(symbol), isEnum && `枚举:${enumKeys.map((key, index) => `${key}->${enumValues[index]}`).join('; ')}`].filter(Boolean).join('\n');
98
- const apiEnum = isUnion ? unionTypes.map(t => t.getLiteralValue()) : isEnum ? enumValues : [];
205
+ const apiName = keySymbolName || '__type';
206
+ const apiDesc = keySymbol && this.getComment(keySymbol).description || '';
207
+ const apiEnum = (isUnion ? unionTypes.map(item => item.value) : isLiteral ? [type.value] : []).filter(v => v != null);
99
208
  const apiType = isArray ? 'array' : isString ? 'string' : isNumber ? 'number' : isBoolean ? 'boolean' : 'object';
100
- const apiRequired = isRequired === false ? false : !!symbol && !(symbol.getFlags() & ts.SymbolFlags.Optional);
101
- const apiChildren = isArray ? [this.typeToApiData(type.getArrayElementTypeOrThrow())] : isObject ? ii(() => {
102
- const context = type._context;
103
- const compilerFactory = context.compilerFactory;
104
- const rawChecker = type.compilerType.checker;
105
- let symbols = [];
106
- if (intersectionTypes) {
107
- // https://github.com/microsoft/TypeScript/issues/38184
108
- symbols = rawChecker.getAllPossiblePropertiesOfTypes(intersectionTypes.map(item => item.compilerType))
109
- // https://github.com/dsherret/ts-morph/blob/a7072fcf6f9babb784b40f0326c80dea4563a4aa/packages/ts-morph/src/compiler/types/Type.ts#L296
110
- .map(symbol => compilerFactory.getSymbol(symbol));
111
- } else {
112
- // symbols = type.getApparentProperties()
113
- // https://github.com/microsoft/TypeScript/issues/38184
114
- symbols = rawChecker.getAllPossiblePropertiesOfTypes([type.compilerType])
115
- // https://github.com/dsherret/ts-morph/blob/a7072fcf6f9babb784b40f0326c80dea4563a4aa/packages/ts-morph/src/compiler/types/Type.ts#L296
116
- .map(symbol => compilerFactory.getSymbol(symbol));
117
- }
118
- return symbols.map(symbol => {
119
- return this.typeToApiData(!symbol.compilerSymbol.declarations ?
120
- // 对于复杂对象,没有定义的,通过 type 直接获取(在前面通过 getText 预处理得到)
121
- symbol.compilerSymbol.type ? compilerFactory.getType(symbol.compilerSymbol.type) :
122
- // fix: symbol.compilerSymbol.type 为 undefined
123
- // https://github.com/styleguidist/react-docgen-typescript/blob/master/src/parser.ts#L696
124
- compilerFactory.getType(rawChecker.getTypeOfSymbolAtLocation(
125
- // @ts-ignore
126
- symbol.compilerSymbol,
127
- // @ts-ignore
128
- parentSymbol.compilerSymbol.declarations[0])) : this.getTypeBySymbol(symbol), symbol);
209
+ const apiRequired = isRequired;
210
+ const apiChildren = isArray ? [this.getApiDto({
211
+ type: this.checker.getTypeArguments(type)[0]
212
+ })] : isObject ? ii(() => {
213
+ const childSymbols = this.checker.getAllPossiblePropertiesOfTypes(intersectionTypes.length ? intersectionTypes : [type]);
214
+ return childSymbols.map(childSymbol => {
215
+ const childType = this.checker.getTypeOfSymbol(childSymbol);
216
+ return this.getApiDto({
217
+ type: childType,
218
+ keySymbol: childSymbol
219
+ });
129
220
  });
130
221
  }) : [];
131
222
  return {
@@ -142,7 +233,6 @@ export class ApiGenerator {
142
233
  if (apiData.type === 'object') {
143
234
  jsonSchema.type = 'object';
144
235
  jsonSchema.properties = apiData.children.reduce((res, item) => {
145
- ;
146
236
  res[item.name] = this.apiDataToJsonSchema(item);
147
237
  return res;
148
238
  }, {});
@@ -219,62 +309,4 @@ export class ApiGenerator {
219
309
  list: data[cat]
220
310
  }));
221
311
  }
222
- async generate() {
223
- this.debug('启动项目...');
224
- this.project = new ts.Project({
225
- tsConfigFilePath: path.join(this.cwd, 'tsconfig.json')
226
- });
227
- this.debug('加载文件...');
228
- const sourceFile = this.project.createSourceFile(path.join(this.cwd, 'src/generated/handlers.ts'), await fs.readFile(path.join(this.cwd, 'node_modules/.x/handlers.ts'), 'utf-8'));
229
- this.debug('导出处理器...');
230
- const handlerGroup = sourceFile.getExportSymbols();
231
- const handles = [];
232
- this.debug('生成API文档...');
233
- for (const handlerList of handlerGroup) {
234
- const handlerListSourceFile = this.getTypeBySymbol(handlerList).getProperties()[0].getDeclarations()[0].getSourceFile();
235
- const basePath = `/${handlerListSourceFile.getFilePath().replace('.ts', '').split('/src/handlers/')[1].replace(/(^|\/)index$/, '/').split('/').map(v => snakeCase(v)).join('/')}/`.replace(/\/{2,}/g, '/');
236
- for (const handler of handlerListSourceFile.getVariableStatements().filter(item => item.isExported())) {
237
- // 重要:这一步必须,先调一遍 getText 获取看到的对象,后续对于复杂定义才不会报错
238
- handler.getDeclarations().forEach(exp => {
239
- exp.getType().getText();
240
- });
241
- const handlerExp = handler.getDeclarations()[0];
242
- const handlerPath = `${basePath}${handlerExp.getName()}`;
243
- this.debug('生成接口: %s ...', handlerPath);
244
- const [requestType, responseType, methodType] = handlerExp.getType().getTypeArguments();
245
- const handlerComment = this.getComment(handlerExp.getParent().getParent());
246
- if (handlerComment.tags.has('private')) {
247
- this.debug('跳过生成接口: %s', `${handlerPath}`);
248
- continue;
249
- }
250
- const handlerCategory = pascalCase(basePath) || 'Index';
251
- const handlerName = handlerComment.description || handlerPath;
252
- const handlerMethod = methodType.getLiteralValueOrThrow();
253
- const serverMethod = HandlerMethodToHttpMethod[handlerMethod];
254
- const requestData = this.typeToApiData(requestType);
255
- const responseData = this.typeToApiData(responseType);
256
- const requestDataJsonSchema = this.apiDataToJsonSchema(requestData);
257
- const responseDataJsonSchema = this.apiDataToJsonSchema(responseData);
258
- handles.push({
259
- category: handlerCategory,
260
- name: handlerName,
261
- path: handlerPath,
262
- handlerMethod: handlerMethod,
263
- method: serverMethod,
264
- requestData: requestData,
265
- responseData: responseData,
266
- requestDataJsonSchema: requestDataJsonSchema,
267
- responseDataJsonSchema: responseDataJsonSchema
268
- });
269
- }
270
- }
271
- this.debug('写入文件...');
272
- await Promise.all([fs.outputJSON(path.join(this.cwd, 'temp/api.json'), handles, {
273
- spaces: 2
274
- }), fs.outputJSON(path.join(this.cwd, 'temp/yapi.json'), this.genYApiData(handles), {
275
- spaces: 2
276
- })]);
277
- }
278
- }
279
-
280
- // new ApiGenerator().generate()
312
+ }
@@ -0,0 +1,44 @@
1
+ import createDebug from 'debug';
2
+ import type { JSONSchema4 } from 'json-schema';
3
+ import * as ts from 'ts-morph';
4
+ import type { XHandler } from '../core/types';
5
+ interface ApiData {
6
+ name: string;
7
+ desc: string;
8
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file';
9
+ enum: any[];
10
+ required: boolean;
11
+ children: ApiData[];
12
+ }
13
+ type ApiHandles = Array<{
14
+ category: string;
15
+ name: string;
16
+ path: string;
17
+ handlerMethod: XHandler.Method;
18
+ method: string;
19
+ requestData: ApiData;
20
+ responseData: ApiData;
21
+ requestDataJsonSchema: JSONSchema4;
22
+ responseDataJsonSchema: JSONSchema4;
23
+ }>;
24
+ export declare class ApiGenerator1 {
25
+ debug: createDebug.Debugger;
26
+ cwd: string;
27
+ project: ts.Project;
28
+ getTypeBySymbol(symbol: ts.Symbol): ts.Type;
29
+ getComment(declaration?: ts.Node): {
30
+ existing: boolean;
31
+ description: string;
32
+ tags: Map<string, string>;
33
+ };
34
+ getCommentBySymbol(symbol: ts.Symbol): string;
35
+ typeToApiData(type: ts.Type, _symbol?: ts.Symbol): ApiData;
36
+ apiDataToJsonSchema(apiData: ApiData, jsonSchema?: JSONSchema4): JSONSchema4;
37
+ genYApiData(handles: ApiHandles): {
38
+ name: string;
39
+ desc: string;
40
+ list: any;
41
+ }[];
42
+ generate(): Promise<void>;
43
+ }
44
+ export {};