gen-api-types 1.0.16 → 1.0.17
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.
- package/README.en.md +46 -12
- package/README.md +46 -12
- package/bin/index.js +0 -0
- package/package.json +4 -2
- package/src/argv/index.ts +1 -1
- package/src/cli/index.ts +40 -101
- package/src/constant/index.ts +2 -2
- package/src/decotators/index.ts +44 -21
- package/src/index.ts +3 -2
- package/src/plugins/index.ts +1 -0
- package/src/plugins/vite/index.ts +46 -0
- package/src/state/index.ts +45 -0
- package/src/utils/index.ts +39 -0
- package/tsconfig.json +3 -3
package/README.en.md
CHANGED
|
@@ -10,8 +10,9 @@ With this tool, we can mark request interface classes and methods through TypeSc
|
|
|
10
10
|
|
|
11
11
|
> Note:
|
|
12
12
|
>
|
|
13
|
-
> 1. Because this tool uses TypeScript
|
|
14
|
-
> 2.
|
|
13
|
+
> 1. Because this tool uses TypeScript method decorators, APIs must be written as **API classes + API methods**.
|
|
14
|
+
> 2. The CLI dynamically imports and executes marked API modules through the project's bundled `tsx` runtime. No global `tsx` installation is required.
|
|
15
|
+
> 3. API methods are executed for real in the CLI process. Make sure required environment variables, network access, and authentication are available.
|
|
15
16
|
|
|
16
17
|
#### Installation
|
|
17
18
|
|
|
@@ -23,14 +24,17 @@ npm install gen-api-types -D
|
|
|
23
24
|
|
|
24
25
|
#### Usage
|
|
25
26
|
|
|
26
|
-
##### 1. Mark
|
|
27
|
+
##### 1. Mark API classes and methods
|
|
27
28
|
|
|
28
29
|
```ts
|
|
29
30
|
import { gen_type_c, gen_type_m } from 'gen-api-types'
|
|
30
31
|
|
|
31
32
|
@gen_type_c()
|
|
32
33
|
export class TestApi {
|
|
33
|
-
@gen_type_m({
|
|
34
|
+
@gen_type_m({
|
|
35
|
+
args: [100],
|
|
36
|
+
typeName: 'XXX',
|
|
37
|
+
})
|
|
34
38
|
static async getList(id: number): Promise<XXX> {
|
|
35
39
|
return asleep(1000).then(() => {
|
|
36
40
|
return { name: 'zs', id }
|
|
@@ -38,7 +42,7 @@ export class TestApi {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
@gen_type_m()
|
|
41
|
-
|
|
45
|
+
getWeather(): Promise<Response_TestApi_getWeather> {
|
|
42
46
|
return fetch('http://t.weather.sojson.com/api/weather/city/101030100').then(r => r.json())
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -46,14 +50,26 @@ export class TestApi {
|
|
|
46
50
|
|
|
47
51
|
As shown in the code above:
|
|
48
52
|
|
|
49
|
-
- `@gen_type_c`
|
|
50
|
-
- `@gen_type_m`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
- `@gen_type_c()` marks an API class.
|
|
54
|
+
- `@gen_type_m()` marks a method to execute and convert.
|
|
55
|
+
- `args: any[]` contains the arguments passed to the method. Both static and non-static methods are supported.
|
|
56
|
+
- `typeName: string` is the generated type name. If omitted, it defaults to `Response_${ClassName}_${MethodName}`.
|
|
57
|
+
- Prefer the exported `gen_type_c` and `gen_type_m` aliases. `GatDecorator` is the internal container used for constant-based decorator names and is not required in business code.
|
|
58
|
+
|
|
59
|
+
Decorator options may span multiple lines:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
@gen_type_m({
|
|
63
|
+
args: [100],
|
|
64
|
+
typeName: 'XXX'
|
|
65
|
+
})
|
|
66
|
+
```
|
|
53
67
|
|
|
54
68
|
> Note:
|
|
55
69
|
|
|
56
|
-
If TypeScript reports the decorator error
|
|
70
|
+
If TypeScript reports the decorator error "The runtime will invoke the decorator with 2 arguments, but the decorator expects 3", set `compilerOptions.experimentalDecorators` to `true` in `tsconfig.json`.
|
|
71
|
+
|
|
72
|
+
The CLI currently applies a 3-second execution timeout to each API method. Timeouts, synchronous exceptions, and rejected promises are reported as execution failures. A timeout stops waiting for the result but cannot cancel an underlying request that has already started.
|
|
57
73
|
|
|
58
74
|
##### 2. Execute command
|
|
59
75
|
|
|
@@ -85,13 +101,13 @@ You can also use it by configuring scripts in package.json:
|
|
|
85
101
|
}
|
|
86
102
|
```
|
|
87
103
|
|
|
104
|
+
The CLI scans `.ts` files in the input directories, finds marked classes and methods, and dynamically imports the modules containing them. The decorators execute the API methods during module import; the declaration file is generated after all marked methods finish.
|
|
105
|
+
|
|
88
106
|
Command output:
|
|
89
107
|
|
|
90
108
|
```shell
|
|
91
109
|
🚀 Start generating API types...
|
|
92
110
|
sourceFilesGlob [ 'src\\**\\*.ts' ]
|
|
93
|
-
📋 Processing TestApi.getList ...
|
|
94
|
-
📋 Processing TestApi.getWeather ...
|
|
95
111
|
Request results:
|
|
96
112
|
┌────────────────┬──────────────────────────────────────┐
|
|
97
113
|
│ (index) │ Values │
|
|
@@ -148,6 +164,24 @@ export type XXX = { name: string };
|
|
|
148
164
|
export type Response_TestApi_getWeather = {...}
|
|
149
165
|
```
|
|
150
166
|
|
|
167
|
+
##### 4. Vite plugin
|
|
168
|
+
|
|
169
|
+
Decorators are needed for type generation, but they normally should not execute when the business application runs or builds. In a Vite project, use the plugin to remove `gen_type_c` and `gen_type_m` from the transformed business code:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// vite.config.ts
|
|
173
|
+
import { defineConfig } from 'vite'
|
|
174
|
+
import { removeGatDecorators } from 'gen-api-types'
|
|
175
|
+
|
|
176
|
+
export default defineConfig({
|
|
177
|
+
plugins: [removeGatDecorators()],
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The plugin processes `.ts` and `.tsx` files and supports single-line and multi-line decorator options. Use the `gen_type_c` and `gen_type_m` aliases in business code; direct `GatDecorator.gen_type_m()` calls do not match the plugin's current decorator names.
|
|
182
|
+
|
|
183
|
+
The Vite plugin only affects Vite's transform pipeline. It is not involved when the CLI imports and executes API modules.
|
|
184
|
+
|
|
151
185
|
#### VS Code Extension
|
|
152
186
|
|
|
153
187
|
If you use `gen-api-types` in VS Code, you can install the companion extension [gen-api-types-vsce](https://github.com/xuejiangping/gen-api-types-vsce) to generate API return types from the context menu.
|
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
> 注意:
|
|
12
12
|
>
|
|
13
|
-
> 1.
|
|
14
|
-
> 2.
|
|
13
|
+
> 1. 由于需要使用 TypeScript 方法装饰器,接口需要以 **API 类 + API 方法** 的形式书写。
|
|
14
|
+
> 2. CLI 会动态导入并执行标记的 API 模块,因此通过项目内置的 `tsx` 运行,不需要全局安装 `tsx`。
|
|
15
|
+
> 3. API 方法会在 CLI 进程中真实执行,请确保运行所需的环境变量、网络权限和鉴权配置已经准备好。
|
|
15
16
|
|
|
16
17
|
#### 安装教程
|
|
17
18
|
|
|
@@ -23,14 +24,17 @@ npm install gen-api-types -D
|
|
|
23
24
|
|
|
24
25
|
#### 使用说明
|
|
25
26
|
|
|
26
|
-
##### 1.
|
|
27
|
+
##### 1. 标记 API 类和方法
|
|
27
28
|
|
|
28
29
|
```ts
|
|
29
30
|
import { gen_type_c, gen_type_m } from 'gen-api-types'
|
|
30
31
|
|
|
31
32
|
@gen_type_c()
|
|
32
33
|
export class TestApi {
|
|
33
|
-
@gen_type_m({
|
|
34
|
+
@gen_type_m({
|
|
35
|
+
args: [100],
|
|
36
|
+
typeName: 'XXX',
|
|
37
|
+
})
|
|
34
38
|
static async getList(id: number): Promise<XXX> {
|
|
35
39
|
return asleep(1000).then(() => {
|
|
36
40
|
return { name: 'zs', id }
|
|
@@ -38,7 +42,7 @@ export class TestApi {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
@gen_type_m()
|
|
41
|
-
|
|
45
|
+
getWeather(): Promise<Response_TestApi_getWeather> {
|
|
42
46
|
return fetch('http://t.weather.sojson.com/api/weather/city/101030100').then(r => r.json())
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -46,14 +50,26 @@ export class TestApi {
|
|
|
46
50
|
|
|
47
51
|
如上面代码所示:
|
|
48
52
|
|
|
49
|
-
- `@gen_type_c
|
|
50
|
-
- `@gen_type_m
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
- `@gen_type_c()` 标记 API 类。
|
|
54
|
+
- `@gen_type_m()` 标记需要执行并生成类型的方法。
|
|
55
|
+
- `args: any[]` 是调用方法时传入的参数,支持静态方法和非静态方法。
|
|
56
|
+
- `typeName: string` 是生成的类型名称;不指定时默认为 `Response_${类名}_${方法名}`。
|
|
57
|
+
- 推荐使用导出的 `gen_type_c`、`gen_type_m` 别名。`GatDecorator` 是内部用于按常量名称注册装饰器的容器,不是业务代码必须使用的入口。
|
|
58
|
+
|
|
59
|
+
装饰器参数可以跨多行书写:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
@gen_type_m({
|
|
63
|
+
args: [100],
|
|
64
|
+
typeName: 'XXX'
|
|
65
|
+
})
|
|
66
|
+
```
|
|
53
67
|
|
|
54
68
|
> 注意:
|
|
55
69
|
|
|
56
|
-
若使用装饰器时
|
|
70
|
+
若使用装饰器时 TypeScript 报错“运行时将使用 2 个自变量调用修饰器,但修饰器需要 3 个”,请将 `tsconfig.json` 中的 `compilerOptions.experimentalDecorators` 设置为 `true`。
|
|
71
|
+
|
|
72
|
+
CLI 默认会为单个 API 方法设置 5 秒执行超时。超时、同步异常或 Promise rejection 都会被记录为该方法的执行失败;超时只能停止等待,不能取消已经发出的底层请求。
|
|
57
73
|
|
|
58
74
|
##### 2. 执行命令
|
|
59
75
|
|
|
@@ -85,13 +101,13 @@ Options:
|
|
|
85
101
|
}
|
|
86
102
|
```
|
|
87
103
|
|
|
104
|
+
CLI 会扫描输入目录中的 `.ts` 文件,找到标记的类和方法后,动态导入包含这些方法的模块。模块导入时装饰器会执行 API 方法,所有方法完成后再生成声明文件。
|
|
105
|
+
|
|
88
106
|
命令输出:
|
|
89
107
|
|
|
90
108
|
```shell
|
|
91
109
|
🚀 开始生成API类型...
|
|
92
110
|
sourceFilesGlob [ 'src\\**\\*.ts' ]
|
|
93
|
-
📋 处理 TestApi.getList ...
|
|
94
|
-
📋 处理 TestApi.getWeather ...
|
|
95
111
|
请求结果:
|
|
96
112
|
┌────────────────┬──────────────────────────────────────┐
|
|
97
113
|
│ (index) │ Values │
|
|
@@ -148,6 +164,24 @@ export type XXX = { name: string };
|
|
|
148
164
|
export type Response_TestApi_getWeather = {...}
|
|
149
165
|
```
|
|
150
166
|
|
|
167
|
+
##### 4. Vite 插件
|
|
168
|
+
|
|
169
|
+
装饰器只用于生成类型,业务项目正常运行或构建时通常不需要执行这些装饰器。Vite 项目可以使用插件移除 `gen_type_c` 和 `gen_type_m`,避免装饰器在业务运行时产生副作用:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// vite.config.ts
|
|
173
|
+
import { defineConfig } from 'vite'
|
|
174
|
+
import { removeGatDecorators } from 'gen-api-types'
|
|
175
|
+
|
|
176
|
+
export default defineConfig({
|
|
177
|
+
plugins: [removeGatDecorators()],
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
插件只处理 `.ts` 和 `.tsx` 文件,并支持单行或多行装饰器参数。推荐在业务代码中使用 `gen_type_c`、`gen_type_m` 别名;如果直接使用 `GatDecorator.gen_type_m()`,不会匹配插件当前的装饰器名称。
|
|
182
|
+
|
|
183
|
+
Vite 插件只影响 Vite 的转换流程,不参与 CLI 的 API 执行流程。
|
|
184
|
+
|
|
151
185
|
#### VS Code 插件
|
|
152
186
|
|
|
153
187
|
如果你在 VS Code 中使用 `gen-api-types` ,可以安装配套插件 [gen-api-types-vsce](https://github.com/xuejiangping/gen-api-types-vsce),通过右键菜单生成 API 返回类型。
|
package/bin/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gen-api-types",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "一个自动生成请求接口返回类型的 cli 小工具",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"test": "vitest",
|
|
20
20
|
"publish:patch": "npm version patch && git push --tags && npm publish",
|
|
21
21
|
"preinstall": "echo preinstall 。。。",
|
|
22
|
-
"postinstall": "echo postinstall 。。。"
|
|
22
|
+
"postinstall": "echo postinstall 。。。",
|
|
23
|
+
"gat": "gat -o test/output test/api "
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"generate types",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@types/node": "^24.3.1",
|
|
47
|
+
"vite": "^8.2.2",
|
|
46
48
|
"vitest": "^3.2.4"
|
|
47
49
|
}
|
|
48
50
|
}
|
package/src/argv/index.ts
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { Project } from 'ts-morph';
|
|
6
6
|
import { pathToFileURL } from 'url';
|
|
7
|
-
import { isExported, output_dir, output_file, positionals } from '../argv';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { isExported, output_dir, output_file, positionals, ts_config_path } from '../argv';
|
|
8
|
+
import { DECO_NAME_C, DECO_NAME_M } from '../constant';
|
|
9
|
+
import { ExecuteApiMethodResult, executeState } from '../state';
|
|
10
10
|
import { TypeTransformer } from '../transformer';
|
|
11
11
|
import { formatResultList } from '../utils';
|
|
12
12
|
|
|
@@ -19,37 +19,15 @@ const out_put_target = path.resolve(output_dir, output_file)
|
|
|
19
19
|
// console.log('sourceFilesGlob', sourceFilesGlob)
|
|
20
20
|
// debugger
|
|
21
21
|
|
|
22
|
-
type ApiMethodInfo = {
|
|
23
|
-
className: string,
|
|
24
|
-
methodName: string,
|
|
25
|
-
fullMethodName: string,
|
|
26
|
-
modulePath: string,
|
|
27
|
-
args: any[],
|
|
28
|
-
typeName: string,
|
|
29
|
-
isStatic: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parserDecoArgs(deco: Decorator): GenTypeOptions {
|
|
33
|
-
const errMsg = `装饰器参数错误`
|
|
34
|
-
try {
|
|
35
|
-
const optionStr = deco.getArguments()[0]?.getText()
|
|
36
|
-
if (!optionStr) return {}
|
|
37
|
-
const option = eval(`(()=>(${optionStr}))()`)
|
|
38
|
-
if (typeof option === 'object') return option
|
|
39
|
-
else throw new Error(errMsg)
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.warn('parserDecoArgs error', error)
|
|
42
|
-
return {}
|
|
43
|
-
}
|
|
44
22
|
|
|
45
|
-
|
|
46
|
-
function getApiMethodsInfo() {
|
|
23
|
+
function getModulePathSet() {
|
|
47
24
|
console.log('sourceFilesGlob', sourceFilesGlob)
|
|
48
|
-
const
|
|
25
|
+
const modulePathSet: Set<string> = new Set()
|
|
49
26
|
// 2. 使用ts-morph创建项目,便于解析源码
|
|
50
27
|
// const project = new Project({ tsConfigFilePath: ts_config_path });
|
|
51
|
-
const project = new Project({
|
|
52
|
-
|
|
28
|
+
const project = new Project({
|
|
29
|
+
tsConfigFilePath: ts_config_path
|
|
30
|
+
});
|
|
53
31
|
project.addSourceFilesAtPaths(sourceFilesGlob);
|
|
54
32
|
// console.log('project.getSourceFiles().length', project.getSourceFiles().length)
|
|
55
33
|
// debugger
|
|
@@ -58,7 +36,7 @@ function getApiMethodsInfo() {
|
|
|
58
36
|
|
|
59
37
|
const classes = sourceFile.getClasses();
|
|
60
38
|
for (const classDeclaration of classes) {
|
|
61
|
-
const c_deco = classDeclaration.getDecorator(
|
|
39
|
+
const c_deco = classDeclaration.getDecorator(DECO_NAME_C)
|
|
62
40
|
if (!c_deco) continue
|
|
63
41
|
const methods = classDeclaration.getMethods();
|
|
64
42
|
for (const method of methods) {
|
|
@@ -66,80 +44,30 @@ function getApiMethodsInfo() {
|
|
|
66
44
|
const methodName = method.getName();
|
|
67
45
|
const fullMethodName = `${className}.${methodName}`;
|
|
68
46
|
|
|
69
|
-
// if (!method.isStatic()) {
|
|
70
|
-
// console.warn(`⚠️ ${fullMethodName} is not static method,only static method can be transformed`)
|
|
71
|
-
// continue
|
|
72
|
-
// }
|
|
73
47
|
// 4. 检查方法是否被我们的装饰器标记
|
|
74
|
-
const m_deco = method.getDecorator(
|
|
48
|
+
const m_deco = method.getDecorator(DECO_NAME_M)
|
|
75
49
|
if (!m_deco) continue
|
|
50
|
+
executeState.emit(executeState.ADD_TASK, fullMethodName)
|
|
51
|
+
const modulePath = sourceFile.getFilePath()
|
|
52
|
+
if (!modulePathSet.has(modulePath)) modulePathSet.add(modulePath)
|
|
76
53
|
|
|
77
|
-
const { args = [], typeName = `Response_${className}_${methodName}` } = parserDecoArgs(m_deco)
|
|
78
|
-
apiMethodsInfo.push({
|
|
79
|
-
className, methodName, fullMethodName, modulePath: sourceFile.getFilePath(),
|
|
80
|
-
typeName, args, isStatic: method.isStatic()
|
|
81
|
-
})
|
|
82
54
|
|
|
83
55
|
}
|
|
84
56
|
}
|
|
85
57
|
}
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
type ExecuteApiMethodResult = {
|
|
90
|
-
data?: any, typeName: string, fullMethodName: string, error?: any
|
|
58
|
+
return modulePathSet
|
|
91
59
|
}
|
|
92
|
-
async function executeApiMethods(apiMethodsInfo: ApiMethodInfo[]): Promise<ExecuteApiMethodResult[]> {
|
|
93
|
-
const apiModuleMap = new Map<string, any>();
|
|
94
|
-
// debugger
|
|
95
|
-
const taskList: ExecuteApiMethodResult[] = []
|
|
96
|
-
for (const apiMethodInfo of apiMethodsInfo) {
|
|
97
|
-
const { className, methodName, fullMethodName, modulePath, args, typeName, isStatic } = apiMethodInfo
|
|
98
|
-
console.log(`📋 处理 ${fullMethodName} ...`);
|
|
99
|
-
let apiModule = null
|
|
100
|
-
// debugger
|
|
101
|
-
if (apiModuleMap.has(modulePath)) apiModule = apiModuleMap.get(modulePath)
|
|
102
|
-
else {
|
|
103
|
-
// apiModule = await import(modulePath)
|
|
104
|
-
// debugger
|
|
105
|
-
// console.log('modulePath', modulePath)
|
|
106
|
-
// console.log('pathToFileURL(modulePath).href', pathToFileURL(modulePath).href)
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
apiModule = await import(pathToFileURL(modulePath).href)
|
|
110
|
-
// console.log('apiMethod', apiModule)
|
|
111
|
-
if (apiModule) apiModuleMap.set(modulePath, apiModule)
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.log(`import ${modulePath} error \r\n`, error)
|
|
114
|
-
taskList.push({ error: `module error`, fullMethodName, typeName })
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const apiMethod = isStatic ?
|
|
119
|
-
apiModule?.[className][methodName]
|
|
120
|
-
: apiModule?.[className].prototype[methodName]
|
|
121
|
-
if (apiMethod && typeof apiMethod === 'function') {
|
|
122
|
-
try {
|
|
123
|
-
// console.log(`🔍 Calling ${fullMethodName} with args:`, args);
|
|
124
|
-
const result = apiMethod.apply(apiModule, args)
|
|
125
|
-
const data = await Promise.resolve(result)
|
|
126
|
-
taskList.push({ data, typeName, fullMethodName })
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error(`❌ ${fullMethodName} execute error:`, error)
|
|
129
|
-
taskList.push({ error, fullMethodName, typeName })
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
console.error(`❌ 无法获取 ${fullMethodName} 方法, 或不是可调用方法 `)
|
|
133
|
-
taskList.push({
|
|
134
|
-
error: `method error`, fullMethodName, typeName
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
60
|
|
|
140
61
|
|
|
141
|
-
|
|
142
|
-
|
|
62
|
+
/**
|
|
63
|
+
* 引入包含标记的方法的模块,触发装饰器执行,记录执行结果
|
|
64
|
+
* @param modulePathSet
|
|
65
|
+
* @returns
|
|
66
|
+
*/
|
|
67
|
+
async function importApiModule<T extends string>(modulePathSet: Set<T>) {
|
|
68
|
+
return Promise.all(
|
|
69
|
+
Array.from(modulePathSet).map(modulePath => import(pathToFileURL(modulePath).href))
|
|
70
|
+
)
|
|
143
71
|
|
|
144
72
|
}
|
|
145
73
|
|
|
@@ -162,13 +90,20 @@ function createDeclarationFile(successList: ExecuteApiMethodResult[]) {
|
|
|
162
90
|
|
|
163
91
|
|
|
164
92
|
async function main() {
|
|
93
|
+
console.log('🚀 开始生成API类型...');
|
|
94
|
+
const modulePathSet = getModulePathSet();
|
|
95
|
+
if (modulePathSet.size == 0) {
|
|
96
|
+
|
|
97
|
+
console.error('⚠️ 未找到需要转换的API,请检查api_dir 和 gen_type装饰器标注是否正确!')
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
165
100
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const executeList = await executeApiMethods(apiMethodsInfo);
|
|
101
|
+
await importApiModule(modulePathSet);
|
|
102
|
+
// executeState.addListener(executeState.TASKLIST_CLEAR, executeResultList=>{
|
|
103
|
+
// })
|
|
170
104
|
|
|
171
|
-
const
|
|
105
|
+
const executeResultList = await executeState.promise
|
|
106
|
+
const { successList: executeSuccessList, errorList: executeErrorList } = formatResultList(executeResultList)
|
|
172
107
|
if (executeErrorList.length) {
|
|
173
108
|
console.group('请求结果:')
|
|
174
109
|
console.table({
|
|
@@ -192,8 +127,12 @@ async function main() {
|
|
|
192
127
|
if (transformSuccessList.length) console.log('✅ API 类型生成完成:', out_put_target);
|
|
193
128
|
} catch (error) {
|
|
194
129
|
console.error('❌ 出错了', error)
|
|
130
|
+
process.exitCode = 1
|
|
195
131
|
}
|
|
196
132
|
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
197
136
|
}
|
|
198
137
|
|
|
199
138
|
main()
|
package/src/constant/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const GEN_TYPE_METADATA_KEY = Symbol('gen:type:metadata');
|
|
2
2
|
/** 标记方法的装饰器名称 */
|
|
3
|
-
export const
|
|
3
|
+
export const DECO_NAME_M = 'gen_type_m';
|
|
4
4
|
/** 标记类的装饰器名称 */
|
|
5
|
-
export const
|
|
5
|
+
export const DECO_NAME_C = 'gen_type_c';
|
package/src/decotators/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// import 'reflect-metadata';
|
|
2
2
|
|
|
3
|
+
import { DECO_NAME_C, DECO_NAME_M } from "../constant/index.ts";
|
|
4
|
+
import { executeState } from "../state/index.ts";
|
|
5
|
+
import { executeApiMethod } from "../utils/index.ts";
|
|
6
|
+
|
|
3
7
|
// 定义一个唯一的metadata key
|
|
4
8
|
|
|
5
9
|
|
|
@@ -7,26 +11,45 @@ export interface GenTypeOptions {
|
|
|
7
11
|
args?: any[];
|
|
8
12
|
typeName?: string;
|
|
9
13
|
}
|
|
10
|
-
/**
|
|
11
|
-
* 标记方法
|
|
12
|
-
*/
|
|
13
|
-
export function gen_type_m({ args = [], typeName }: GenTypeOptions = {}) {
|
|
14
|
-
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
15
|
-
// 只存储元数据,不执行任何逻辑
|
|
16
|
-
// Reflect.defineMetadata(
|
|
17
|
-
// GEN_TYPE_METADATA_KEY,
|
|
18
|
-
// { args, typeName },
|
|
19
|
-
// target,
|
|
20
|
-
// propertyKey
|
|
21
|
-
// );
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
14
|
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
}
|
|
16
|
+
const EXEC_TIMEOUT = 5_000
|
|
17
|
+
export class GatDecorator {
|
|
18
|
+
static [DECO_NAME_C]() {
|
|
19
|
+
return function <T>(target: T) {
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
static [DECO_NAME_M]({ args = [], typeName }: GenTypeOptions = {}) {
|
|
23
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
(async () => {
|
|
27
|
+
const className = typeof target == 'function' ? target.name : target.constructor.name
|
|
28
|
+
const fullMethodName = `${className}.${propertyKey}`
|
|
29
|
+
console.log('fullMethodName', fullMethodName)
|
|
30
|
+
typeName ??= `Response_${className}_${propertyKey}`
|
|
31
|
+
let resultInfo = null
|
|
32
|
+
try {
|
|
33
|
+
const apiMethod = descriptor.value as Function
|
|
34
|
+
// console.log(`🔍 Calling ${fullMethodName} with args:`, args);
|
|
35
|
+
|
|
36
|
+
const data = await executeApiMethod({
|
|
37
|
+
timeout: EXEC_TIMEOUT,
|
|
38
|
+
method: () => apiMethod.apply(target, args)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
resultInfo = { data, typeName, fullMethodName }
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`❌ ${fullMethodName} execute error:`, error)
|
|
44
|
+
resultInfo = { error, fullMethodName, typeName }
|
|
45
|
+
} finally {
|
|
46
|
+
executeState.emit(executeState.EXECUTE_END, resultInfo)
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const gen_type_c = GatDecorator[DECO_NAME_C]
|
|
55
|
+
export const gen_type_m = GatDecorator[DECO_NAME_M]
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vite/index.ts';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { DECO_NAME_C, DECO_NAME_M } from '../../constant/index.ts';
|
|
3
|
+
|
|
4
|
+
function escapeRegExp(value: string) {
|
|
5
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 去掉装饰器,用于在项目运行/构建时,gen-api-types的装饰器不会有任何副作用,影响到原来项目代码
|
|
9
|
+
* @param decorators
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
export function removeDecorators(decorators: string[]): Plugin {
|
|
13
|
+
const names = decorators.map(escapeRegExp).join('|');
|
|
14
|
+
|
|
15
|
+
const decoratorRegex = new RegExp(
|
|
16
|
+
`^[ \\t]*@(?:${names})(?:\\s*\\([\\s\\S]*?\\))?[ \\t]*\\r?\\n?`,
|
|
17
|
+
'gm'
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
name: 'removeDecorators',
|
|
22
|
+
enforce: 'pre',
|
|
23
|
+
|
|
24
|
+
transform(code, id) {
|
|
25
|
+
if (!/\.(ts|tsx)(\?.*)?$/.test(id)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const transformedCode = code.replace(decoratorRegex, '');
|
|
30
|
+
|
|
31
|
+
if (transformedCode === code) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
code: transformedCode,
|
|
37
|
+
map: null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function removeGatDecorators() {
|
|
44
|
+
const decorators = [DECO_NAME_M, DECO_NAME_C]
|
|
45
|
+
return removeDecorators(decorators)
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import EventEmitter from "events"
|
|
2
|
+
import { promiseWithResolvers } from "../utils/index.ts"
|
|
3
|
+
|
|
4
|
+
export type ExecuteApiMethodResult = {
|
|
5
|
+
data?: any, typeName: string, fullMethodName: string, error?: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export class ExecuteState extends EventEmitter {
|
|
14
|
+
|
|
15
|
+
EXECUTE_END = 'executeEnd'
|
|
16
|
+
TASKLIST_CLEAR = 'taskClear'
|
|
17
|
+
ADD_TASK = 'addTask'
|
|
18
|
+
private resultList: ExecuteApiMethodResult[] = []
|
|
19
|
+
private taskList: string[] = []
|
|
20
|
+
promiseWithResolvers = promiseWithResolvers<ExecuteApiMethodResult[]>()
|
|
21
|
+
constructor() {
|
|
22
|
+
super()
|
|
23
|
+
this.init()
|
|
24
|
+
}
|
|
25
|
+
init() {
|
|
26
|
+
this.addListener(this.EXECUTE_END, (result: ExecuteApiMethodResult) => {
|
|
27
|
+
this.resultList.push(result)
|
|
28
|
+
if (this.resultList.length === this.taskList.length) {
|
|
29
|
+
this.emit(this.TASKLIST_CLEAR, this.resultList)
|
|
30
|
+
this.promiseWithResolvers?.resolve(this.resultList)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
this.addListener(this.ADD_TASK, (taskName) => {
|
|
35
|
+
this.taskList.push(taskName)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get promise() {
|
|
40
|
+
return this.promiseWithResolvers?.promise
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const executeState = new ExecuteState()
|
package/src/utils/index.ts
CHANGED
|
@@ -6,3 +6,42 @@ export function formatResultList<T extends ResultInfoBase>(list: T[]) {
|
|
|
6
6
|
return acc
|
|
7
7
|
}, { successList: [] as Omit<T, 'error'>[], errorList: [] as T[] })
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
export function promiseWithResolvers<T>() {
|
|
11
|
+
let promise!: Promise<T>
|
|
12
|
+
let resolve!: (value: T | PromiseLike<T>) => void
|
|
13
|
+
let reject!: (reason?: any) => void
|
|
14
|
+
promise = new Promise((res, rej) => {
|
|
15
|
+
resolve = res
|
|
16
|
+
reject = rej
|
|
17
|
+
})
|
|
18
|
+
return {
|
|
19
|
+
promise, resolve, reject
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function executeApiMethod<T>({
|
|
24
|
+
method,
|
|
25
|
+
timeout = 10 * 1000
|
|
26
|
+
}: {
|
|
27
|
+
method: () => T | PromiseLike<T>
|
|
28
|
+
timeout?: number
|
|
29
|
+
}): Promise<T> {
|
|
30
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
31
|
+
|
|
32
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
reject(new Error(`API execution timed out after ${timeout}ms`))
|
|
35
|
+
}, timeout)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return await Promise.race([
|
|
40
|
+
Promise.resolve().then(method),
|
|
41
|
+
timeoutPromise
|
|
42
|
+
])
|
|
43
|
+
} finally {
|
|
44
|
+
if (timer) clearTimeout(timer)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
package/tsconfig.json
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
/* Modules */
|
|
31
31
|
"module": "esnext" /* Specify what module code is generated. */,
|
|
32
32
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
|
33
|
-
"moduleResolution": "
|
|
33
|
+
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
|
34
34
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
35
35
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
|
36
36
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
|
39
39
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
40
40
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
|
41
|
-
|
|
41
|
+
"allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */,
|
|
42
42
|
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
|
43
43
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
|
44
44
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
60
60
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
61
61
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
|
62
|
-
|
|
62
|
+
"noEmit": true /* Disable emitting files from a compilation. */,
|
|
63
63
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
|
64
64
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
|
65
65
|
// "removeComments": true, /* Disable emitting comments. */
|