swgto-ts 0.1.1 → 2.0.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.
- package/README.md +147 -86
- package/dist/{chunk-5JY6MCOD.js → chunk-4OK6R2K5.js} +256 -55
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,86 +1,147 @@
|
|
|
1
|
-
# swgto
|
|
2
|
-
|
|
3
|
-
`swgto` 是一个把 `OpenAPI 3.x` 文档转换成项目内请求函数的 Node.js 库和 CLI。
|
|
4
|
-
|
|
5
|
-
安装后会注册一个 `swgto` 命令。执行时它会在当前目录查找 `.swaggerts.config.ts` 或 `.swaggerts.config.js`,然后生成:
|
|
6
|
-
|
|
7
|
-
- 按路径前缀分组的请求文件
|
|
8
|
-
- 按文档模块输出请求文件。单文档默认输出到 `services/`,多文档时多个模块目录平级
|
|
9
|
-
- `outputDir/index.ts` 或 `outputDir/index.js`
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
# swgto
|
|
2
|
+
|
|
3
|
+
`swgto` 是一个把 `OpenAPI 3.x` 文档转换成项目内请求函数的 Node.js 库和 CLI。
|
|
4
|
+
|
|
5
|
+
安装后会注册一个 `swgto` 命令。执行时它会在当前目录查找 `.swaggerts.config.ts` 或 `.swaggerts.config.js`,然后生成:
|
|
6
|
+
|
|
7
|
+
- 按路径前缀分组的请求文件
|
|
8
|
+
- 按文档模块输出请求文件。单文档默认输出到 `services/`,多文档时多个模块目录平级
|
|
9
|
+
- `outputDir/index.ts` 或 `outputDir/index.js`
|
|
10
|
+
- 类型文件 `outputDir/types.ts` 或带 JSDoc 的 `outputDir/types.js`
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install swgto-ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 配置
|
|
19
|
+
|
|
20
|
+
在项目根目录创建 `.swaggerts.config.ts`:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import type { SwaggerTsConfig } from 'swgto-ts'
|
|
24
|
+
|
|
25
|
+
const config: SwaggerTsConfig = {
|
|
26
|
+
docUrls: 'https://example.com/openapi.json',
|
|
27
|
+
httpClientPath: '@/utils/request',
|
|
28
|
+
outputDir: 'src/api',
|
|
29
|
+
outputType: 'ts',
|
|
30
|
+
typeName: 'types',
|
|
31
|
+
cleanOutput: true,
|
|
32
|
+
renameMethod: (apiPath, method) => `${method}_${apiPath.replace(/[\\/{}]/g, '_')}`,
|
|
33
|
+
resolveRequestPath: (apiPath, method) => `/proxy${apiPath}`,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default config
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
多文档模式下需要提供 `moduleName`:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export default {
|
|
43
|
+
docUrls: ['https://example.com/user-openapi.json', 'https://example.com/order-openapi.json'],
|
|
44
|
+
httpClientPath: '@/utils/request',
|
|
45
|
+
outputDir: 'src/api',
|
|
46
|
+
moduleName: (docUrl) => (docUrl.includes('user') ? 'user' : 'order'),
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 使用
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
swgto
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 输出示例
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
src/api/
|
|
60
|
+
index.ts
|
|
61
|
+
types.ts
|
|
62
|
+
services/
|
|
63
|
+
get_user_list.ts
|
|
64
|
+
post_user_create.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 配置项
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export interface SwaggerTsConfig {
|
|
71
|
+
docUrls: string | string[]
|
|
72
|
+
httpClientPath: string
|
|
73
|
+
renameMethod?: (path: string, method: string) => string
|
|
74
|
+
resolveRequestPath?: (path: string, method: string, docUrl: string) => string
|
|
75
|
+
ignoreUrl?: (path: string, method: string, docUrl: string) => boolean
|
|
76
|
+
outputDir?: string
|
|
77
|
+
moduleName?: (docUrl: string) => string
|
|
78
|
+
outputType?: 'ts' | 'js'
|
|
79
|
+
typeName?: string
|
|
80
|
+
cleanOutput?: boolean
|
|
81
|
+
fileNaming?: 'module' | 'path'
|
|
82
|
+
flattenQueryParam?: boolean
|
|
83
|
+
mergeParams?: boolean
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 字段说明
|
|
88
|
+
|
|
89
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
90
|
+
| -------------------- | ----------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `docUrls` | `string \| string[]` | (必填) | OpenAPI 3.x 文档的 URL,支持单文档字符串或多文档数组 |
|
|
92
|
+
| `httpClientPath` | `string` | (必填) | 项目内 HTTP 请求工具的引入路径,生成的文件会从中 `import request` |
|
|
93
|
+
| `outputDir` | `string` | `'src/api'` | 代码输出目录,相对项目根目录 |
|
|
94
|
+
| `outputType` | `'ts' \| 'js'` | `'ts'` | 输出文件类型,`'ts'` 生成 `.ts` 文件,`'js'` 生成 `.js` 文件(带 JSDoc 类型注释) |
|
|
95
|
+
| `typeName` | `string` | `'types'` | 类型文件名(不含后缀),如 `'types'` 生成 `types.ts` 或 `types.js` |
|
|
96
|
+
| `fileNaming` | `'module' \| 'path'` | `'path'` | 文件组织方式:`'path'` 按接口路径每个文件一个函数;`'module'` 按控制器合并到同一个文件 |
|
|
97
|
+
| `moduleName` | `(docUrl) => string` | `'services'` | 多文档时,为每个文档指定模块目录名。多文档模式下**必填** |
|
|
98
|
+
| `renameMethod` | `(path, method) => string` | 见说明 | 自定义函数名生成规则。默认根据路径和方法名自动生成(如 `get_user_list`) |
|
|
99
|
+
| `resolveRequestPath` | `(path, method, docUrl) => string` | 原路径 | 自定义请求路径转换规则,可用于添加统一前缀等,如 `(path) => \`/proxy\${path}\`` |
|
|
100
|
+
| `ignoreUrl` | `(path, method, docUrl) => boolean` | 不过滤 | 过滤不需要生成代码的接口,返回 `true` 跳过当前接口 |
|
|
101
|
+
| `cleanOutput` | `boolean` | `false` | 生成前是否清空输出目录 |
|
|
102
|
+
| `flattenQueryParam` | `boolean` | `false` | 当 query 参数只有一个且为 `$ref` 引用类型时,直接用引用类型代替 `{ query: RefType }` |
|
|
103
|
+
| `mergeParams` | `boolean` | `false` | 合并参数层级:`true` 时展平 `path/query/body` 嵌套,直接使用 `params: { field1, field2 }` 代替 `params: { path: {...}, query: {...} }` |
|
|
104
|
+
|
|
105
|
+
### 函数名生成规则
|
|
106
|
+
|
|
107
|
+
默认函数名格式为 `{method}_{path_segments}`,如:
|
|
108
|
+
|
|
109
|
+
- `GET /user/list` → `get_user_list`
|
|
110
|
+
- `POST /user/create` → `post_user_create`
|
|
111
|
+
|
|
112
|
+
可通过 `renameMethod` 自定义:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
renameMethod: (path, method) => `${method}_${path.replace(/[\\/{}]/g, '_')}`,
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 文件组织方式
|
|
119
|
+
|
|
120
|
+
**`fileNaming: 'path'`(默认)**— 每个接口生成独立文件:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
src/api/moduleA/
|
|
124
|
+
get_user_list.ts
|
|
125
|
+
post_user_create.ts
|
|
126
|
+
get_user_detail.ts
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**`fileNaming: 'module'`**— 按控制器合并文件(自动识别 RESTful 资源路径分组):
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
src/api/moduleA/
|
|
133
|
+
user.ts // 包含 get_user_list, post_user_create, get_user_detail
|
|
134
|
+
order.ts // 包含 get_order_list, post_order_create
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 参数合并模式
|
|
138
|
+
|
|
139
|
+
`mergeParams: true` 时展平参数层级,适用于不想嵌套 `path/query/body` 的场景:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// mergeParams: false(默认),参数按来源嵌套
|
|
143
|
+
getUser({ path: { id: '1' }, query: { name: 'foo' } })
|
|
144
|
+
|
|
145
|
+
// mergeParams: true,参数展平
|
|
146
|
+
getUser({ id: '1', name: 'foo' })
|
|
147
|
+
```
|
|
@@ -39,7 +39,10 @@ async function loadConfig(cwd) {
|
|
|
39
39
|
outputDir: rawConfig.outputDir ?? "src/api",
|
|
40
40
|
outputType: rawConfig.outputType ?? "ts",
|
|
41
41
|
typeName: rawConfig.typeName ?? "types",
|
|
42
|
-
cleanOutput: rawConfig.cleanOutput ?? false
|
|
42
|
+
cleanOutput: rawConfig.cleanOutput ?? false,
|
|
43
|
+
fileNaming: rawConfig.fileNaming ?? "path",
|
|
44
|
+
flattenQueryParam: rawConfig.flattenQueryParam ?? false,
|
|
45
|
+
mergeParams: rawConfig.mergeParams ?? false
|
|
43
46
|
};
|
|
44
47
|
return { configPath, config };
|
|
45
48
|
}
|
|
@@ -53,6 +56,92 @@ function groupByPrefix(operations) {
|
|
|
53
56
|
}, {});
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
// src/core/groupByController.ts
|
|
60
|
+
function isPathParam(segment) {
|
|
61
|
+
return segment.startsWith("{") && segment.endsWith("}");
|
|
62
|
+
}
|
|
63
|
+
function resultName(segments, childIndex) {
|
|
64
|
+
if (childIndex >= 2) {
|
|
65
|
+
return segments[childIndex - 1] + "_" + segments[childIndex];
|
|
66
|
+
}
|
|
67
|
+
return segments[childIndex];
|
|
68
|
+
}
|
|
69
|
+
function getPathMethods(operations) {
|
|
70
|
+
const map = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const op of operations) {
|
|
72
|
+
if (!map.has(op.path)) map.set(op.path, /* @__PURE__ */ new Set());
|
|
73
|
+
map.get(op.path).add(op.method);
|
|
74
|
+
}
|
|
75
|
+
return map;
|
|
76
|
+
}
|
|
77
|
+
function deriveControllerName(path4, allPaths, pathMethods) {
|
|
78
|
+
const segments = path4.split("/").filter(Boolean);
|
|
79
|
+
if (segments.length === 0) return "index";
|
|
80
|
+
if (segments.length === 1) return segments[0];
|
|
81
|
+
let effectiveLen = segments.length;
|
|
82
|
+
while (effectiveLen > 1 && isPathParam(segments[effectiveLen - 1])) {
|
|
83
|
+
effectiveLen--;
|
|
84
|
+
}
|
|
85
|
+
if (effectiveLen === 1) return segments[0];
|
|
86
|
+
for (let depth = effectiveLen - 1; depth >= 0; depth--) {
|
|
87
|
+
const ancestorPath = "/" + segments.slice(0, depth).join("/");
|
|
88
|
+
const ancestorMethods = pathMethods.get(ancestorPath);
|
|
89
|
+
if (ancestorMethods && ancestorMethods.size > 1 && depth >= 1) {
|
|
90
|
+
return segments[depth - 1];
|
|
91
|
+
}
|
|
92
|
+
if (depth >= 1) {
|
|
93
|
+
if (allPaths.has(ancestorPath)) {
|
|
94
|
+
const ancestorController = deriveControllerName(ancestorPath, allPaths, pathMethods);
|
|
95
|
+
if (ancestorController !== segments[depth - 1]) {
|
|
96
|
+
return ancestorController;
|
|
97
|
+
}
|
|
98
|
+
return segments[depth - 1];
|
|
99
|
+
}
|
|
100
|
+
const hasDirectSibling = [...allPaths].some((p) => {
|
|
101
|
+
if (p === path4) return false;
|
|
102
|
+
if (!p.startsWith(ancestorPath + "/")) return false;
|
|
103
|
+
const remaining = p.slice(ancestorPath.length + 1);
|
|
104
|
+
return !remaining.includes("/");
|
|
105
|
+
});
|
|
106
|
+
if (hasDirectSibling) {
|
|
107
|
+
const siblingIsRest = [...allPaths].some((p) => {
|
|
108
|
+
if (p === path4) return false;
|
|
109
|
+
if (!p.startsWith(ancestorPath + "/")) return false;
|
|
110
|
+
const remaining = p.slice(ancestorPath.length + 1);
|
|
111
|
+
if (remaining.includes("/")) return false;
|
|
112
|
+
const methods = pathMethods.get(p);
|
|
113
|
+
return methods != null && methods.size > 1;
|
|
114
|
+
});
|
|
115
|
+
if (siblingIsRest) {
|
|
116
|
+
return resultName(segments, depth);
|
|
117
|
+
}
|
|
118
|
+
return segments[depth - 1];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const currentPath = "/" + segments.slice(0, depth + 1).join("/");
|
|
122
|
+
const hasChildren = [...allPaths].some(
|
|
123
|
+
(p) => p !== path4 && p.startsWith(currentPath + "/")
|
|
124
|
+
);
|
|
125
|
+
if (hasChildren) {
|
|
126
|
+
return resultName(segments, depth);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
130
|
+
if (!isPathParam(segments[i])) return resultName(segments, i);
|
|
131
|
+
}
|
|
132
|
+
return segments[segments.length - 1];
|
|
133
|
+
}
|
|
134
|
+
function groupByController(operations) {
|
|
135
|
+
const allPaths = new Set(operations.map((op) => op.path));
|
|
136
|
+
const pathMethods = getPathMethods(operations);
|
|
137
|
+
return operations.reduce((acc, operation) => {
|
|
138
|
+
const controllerName = deriveControllerName(operation.path, allPaths, pathMethods);
|
|
139
|
+
acc[controllerName] ??= [];
|
|
140
|
+
acc[controllerName].push(operation);
|
|
141
|
+
return acc;
|
|
142
|
+
}, {});
|
|
143
|
+
}
|
|
144
|
+
|
|
56
145
|
// src/generators/schemaToTs.ts
|
|
57
146
|
function formatPropertyName(name) {
|
|
58
147
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
@@ -189,6 +278,9 @@ function parsePaths(document, docUrl, config) {
|
|
|
189
278
|
continue;
|
|
190
279
|
}
|
|
191
280
|
const moduleName = config.moduleName?.(docUrl) ?? "services";
|
|
281
|
+
if (config.ignoreUrl?.(apiPath, method, docUrl)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
192
284
|
const functionName = sanitizeIdentifier(
|
|
193
285
|
config.renameMethod?.(apiPath, method) ?? buildDefaultMethodName(apiPath, method)
|
|
194
286
|
);
|
|
@@ -200,6 +292,12 @@ function parsePaths(document, docUrl, config) {
|
|
|
200
292
|
const hasRequestParams = queryParams.length > 0 || pathParams.length > 0 || Boolean(requestBodySchema);
|
|
201
293
|
const bodyType = requestBodySchema ? schemaToTs(requestBodySchema) : void 0;
|
|
202
294
|
const requestImportTypes = collectReferencedTypeNames(requestBodySchema);
|
|
295
|
+
for (const param of queryParams) {
|
|
296
|
+
requestImportTypes.push(...collectReferencedTypeNames(param.schema));
|
|
297
|
+
}
|
|
298
|
+
for (const param of pathParams) {
|
|
299
|
+
requestImportTypes.push(...collectReferencedTypeNames(param.schema));
|
|
300
|
+
}
|
|
203
301
|
const pathFields = pathParams.map((param) => `${param.name}: ${schemaToTs(param.schema)};`);
|
|
204
302
|
const queryFields = queryParams.map((param) => `${param.name}${param.required ? "" : "?"}: ${schemaToTs(param.schema)};`);
|
|
205
303
|
const requestTypeParts = [];
|
|
@@ -207,10 +305,26 @@ function parsePaths(document, docUrl, config) {
|
|
|
207
305
|
requestTypeParts.push(bodyType);
|
|
208
306
|
}
|
|
209
307
|
if (pathFields.length) {
|
|
210
|
-
|
|
308
|
+
if (config.mergeParams) {
|
|
309
|
+
requestTypeParts.push(`{ ${pathFields.join(" ")} }`);
|
|
310
|
+
} else {
|
|
311
|
+
requestTypeParts.push(`{ path: { ${pathFields.join(" ")} } }`);
|
|
312
|
+
}
|
|
211
313
|
}
|
|
212
314
|
if (queryFields.length) {
|
|
213
|
-
|
|
315
|
+
if (config.flattenQueryParam && queryParams.length === 1 && queryParams[0].schema?.$ref) {
|
|
316
|
+
if (config.mergeParams) {
|
|
317
|
+
requestTypeParts.push(schemaToTs(queryParams[0].schema));
|
|
318
|
+
} else {
|
|
319
|
+
requestTypeParts.push(`{ query: ${schemaToTs(queryParams[0].schema)} }`);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
if (config.mergeParams) {
|
|
323
|
+
requestTypeParts.push(`{ ${queryFields.join(" ")} }`);
|
|
324
|
+
} else {
|
|
325
|
+
requestTypeParts.push(`{ query: { ${queryFields.join(" ")} } }`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
214
328
|
}
|
|
215
329
|
const requestTypeExpression = hasRequestParams ? requestTypeParts.join(" & ") || "unknown" : void 0;
|
|
216
330
|
operations.push({
|
|
@@ -254,13 +368,14 @@ async function loadOpenApiDocument(docUrl) {
|
|
|
254
368
|
}
|
|
255
369
|
|
|
256
370
|
// src/generators/genIndex.ts
|
|
257
|
-
function generateIndexFile(operations,
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
371
|
+
function generateIndexFile(operations, _config) {
|
|
372
|
+
const exportSet = /* @__PURE__ */ new Set();
|
|
373
|
+
for (const operation of operations) {
|
|
374
|
+
exportSet.add(`export * from "./${operation.moduleName}/${operation.fileBaseName}";`);
|
|
375
|
+
}
|
|
261
376
|
return `/* eslint-disable */
|
|
262
377
|
// Auto-generated by swgto.
|
|
263
|
-
${
|
|
378
|
+
${[...exportSet].join("\n")}
|
|
264
379
|
`;
|
|
265
380
|
}
|
|
266
381
|
|
|
@@ -276,19 +391,43 @@ function buildFunctionDoc(operation, typeImportPath, requestParamType) {
|
|
|
276
391
|
lines.push(`@path ${operation.path}`);
|
|
277
392
|
lines.push(`@requestPath ${operation.requestPath}`);
|
|
278
393
|
lines.push(`@method ${operation.method.toUpperCase()}`);
|
|
279
|
-
lines.push("@template T");
|
|
280
394
|
lines.push(`@param ${requestParamType === "void" ? "[params]" : `{import(${JSON.stringify(typeImportPath)}).${requestParamType}} params`}`);
|
|
281
395
|
lines.push("@param [config]");
|
|
282
|
-
|
|
396
|
+
if (operation.responseTypeName) {
|
|
397
|
+
lines.push(`@returns {Promise<import(${JSON.stringify(typeImportPath)}).${operation.responseTypeName}>}`);
|
|
398
|
+
} else {
|
|
399
|
+
lines.push("@template T");
|
|
400
|
+
lines.push("@returns {Promise<T>}");
|
|
401
|
+
}
|
|
283
402
|
return ["/**", ...lines.map((line) => ` * ${line}`), " */"].join("\n");
|
|
284
403
|
}
|
|
285
|
-
function
|
|
404
|
+
function renderFunction(operation, useGenericUrl, mergeParams) {
|
|
405
|
+
const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
|
|
406
|
+
` : "";
|
|
407
|
+
const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
|
|
408
|
+
const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
|
|
409
|
+
` : "";
|
|
410
|
+
const pathArg = mergeParams ? "params" : "params?.path";
|
|
411
|
+
const urlValue = useGenericUrl && operation.pathParams.length ? `buildUrl(${JSON.stringify(operation.requestPath)}, ${pathArg})` : operation.pathParams.length ? `buildUrl(${pathArg})` : JSON.stringify(operation.requestPath);
|
|
412
|
+
const pathLine = ` url: ${urlValue},
|
|
413
|
+
`;
|
|
414
|
+
return `export async function ${operation.functionName}(params, config) {
|
|
415
|
+
return request({
|
|
416
|
+
${pathLine} method: ${JSON.stringify(operation.method)},
|
|
417
|
+
${queryLine}${bodyLine} ...config,
|
|
418
|
+
});
|
|
419
|
+
}`;
|
|
420
|
+
}
|
|
421
|
+
function generateJsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
|
|
286
422
|
const requestParamType = operation.requestTypeExpression ?? "void";
|
|
287
|
-
const queryLine = operation.queryParams.length ?
|
|
423
|
+
const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
|
|
424
|
+
` : "";
|
|
288
425
|
const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
|
|
289
|
-
const bodyLine = operation.requestBodySchema ? ` data: ${bodyOnly ? "params" : "params?.body"},
|
|
426
|
+
const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
|
|
290
427
|
` : "";
|
|
291
|
-
const
|
|
428
|
+
const pathArg = mergeParams ? "params" : "params?.path";
|
|
429
|
+
const pathLine = operation.pathParams.length ? ` url: buildUrl(${pathArg}),
|
|
430
|
+
` : ` url: ${JSON.stringify(operation.requestPath)},
|
|
292
431
|
`;
|
|
293
432
|
const buildUrlHelper = operation.pathParams.length ? `
|
|
294
433
|
function buildUrl(path) {
|
|
@@ -309,6 +448,26 @@ ${queryLine}${bodyLine} ...config,
|
|
|
309
448
|
}
|
|
310
449
|
`;
|
|
311
450
|
}
|
|
451
|
+
function generateJsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false) {
|
|
452
|
+
const needsBuildUrl = operations.some((op) => op.pathParams.length > 0);
|
|
453
|
+
const buildUrlHelper = needsBuildUrl ? `
|
|
454
|
+
function buildUrl(url, path) {
|
|
455
|
+
return url.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
|
|
456
|
+
}
|
|
457
|
+
` : "";
|
|
458
|
+
const functions = operations.map((op) => {
|
|
459
|
+
const doc = buildFunctionDoc(op, typeImportPath, op.requestTypeExpression ?? "void");
|
|
460
|
+
return `${doc}
|
|
461
|
+
|
|
462
|
+
${renderFunction(op, true, mergeParams)}`;
|
|
463
|
+
}).join("\n\n");
|
|
464
|
+
return `/* eslint-disable */
|
|
465
|
+
// Auto-generated by swgto.
|
|
466
|
+
import request from ${JSON.stringify(httpClientPath)};
|
|
467
|
+
${buildUrlHelper}
|
|
468
|
+
${functions}
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
312
471
|
|
|
313
472
|
// src/generators/genRequestTs.ts
|
|
314
473
|
function buildFunctionDoc2(operation) {
|
|
@@ -324,7 +483,26 @@ function buildFunctionDoc2(operation) {
|
|
|
324
483
|
}
|
|
325
484
|
return ["/**", ...lines.map((line) => ` * ${line}`), " */", ""].join("\n");
|
|
326
485
|
}
|
|
327
|
-
function
|
|
486
|
+
function renderFunction2(operation, useGenericUrl, mergeParams) {
|
|
487
|
+
const requestArg = operation.requestTypeExpression ? `params: ${operation.requestTypeExpression}, config?: RequestConfig` : "params?: void, config?: RequestConfig";
|
|
488
|
+
const defaultResponseType = operation.responseTypeName ?? "unknown";
|
|
489
|
+
const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
|
|
490
|
+
const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
|
|
491
|
+
` : "";
|
|
492
|
+
const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
|
|
493
|
+
` : "";
|
|
494
|
+
const pathArg = mergeParams ? "params" : "params?.path";
|
|
495
|
+
const urlValue = useGenericUrl && operation.pathParams.length ? `buildUrl(${JSON.stringify(operation.requestPath)}, ${pathArg})` : operation.pathParams.length ? `buildUrl(${pathArg})` : JSON.stringify(operation.requestPath);
|
|
496
|
+
const pathLine = ` url: ${urlValue},
|
|
497
|
+
`;
|
|
498
|
+
return `${buildFunctionDoc2(operation)}export async function ${operation.functionName}<T = ${defaultResponseType}>(${requestArg}): Promise<T> {
|
|
499
|
+
return request<T>({
|
|
500
|
+
${pathLine} method: ${JSON.stringify(operation.method)},
|
|
501
|
+
${queryLine}${bodyLine} ...config,
|
|
502
|
+
});
|
|
503
|
+
}`;
|
|
504
|
+
}
|
|
505
|
+
function generateTsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
|
|
328
506
|
const importTypes = [
|
|
329
507
|
...operation.requestImportTypes,
|
|
330
508
|
operation.responseTypeName,
|
|
@@ -332,14 +510,6 @@ function generateTsRequestFile(operation, httpClientPath, typeImportPath) {
|
|
|
332
510
|
].filter((value, index, array) => Boolean(value) && array.indexOf(value) === index);
|
|
333
511
|
const importLine = importTypes.length ? `import type { ${importTypes.join(", ")} } from ${JSON.stringify(typeImportPath)};
|
|
334
512
|
` : "";
|
|
335
|
-
const requestArg = operation.requestTypeExpression ? `params: ${operation.requestTypeExpression}, config?: RequestConfig` : "params?: void, config?: RequestConfig";
|
|
336
|
-
const defaultResponseType = operation.responseTypeName ?? "unknown";
|
|
337
|
-
const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
|
|
338
|
-
const bodyLine = operation.requestBodySchema ? ` data: ${bodyOnly ? "params" : "params?.body"},
|
|
339
|
-
` : "";
|
|
340
|
-
const queryLine = operation.queryParams.length ? " params: params?.query,\n" : "";
|
|
341
|
-
const pathLine = operation.pathParams.length ? " url: buildUrl(params?.path),\n" : ` url: ${JSON.stringify(operation.requestPath)},
|
|
342
|
-
`;
|
|
343
513
|
const buildUrlHelper = operation.pathParams.length ? `
|
|
344
514
|
function buildUrl(path?: Record<string, unknown>): string {
|
|
345
515
|
return ${JSON.stringify(operation.requestPath)}.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
|
|
@@ -350,13 +520,30 @@ function buildUrl(path?: Record<string, unknown>): string {
|
|
|
350
520
|
import request from ${JSON.stringify(httpClientPath)};
|
|
351
521
|
${importLine}
|
|
352
522
|
${buildUrlHelper}
|
|
353
|
-
${
|
|
354
|
-
|
|
355
|
-
return request<T>({
|
|
356
|
-
${pathLine} method: ${JSON.stringify(operation.method)},
|
|
357
|
-
${queryLine}${bodyLine} ...config,
|
|
358
|
-
});
|
|
523
|
+
${renderFunction2(operation, false, mergeParams)}
|
|
524
|
+
`;
|
|
359
525
|
}
|
|
526
|
+
function generateTsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false) {
|
|
527
|
+
const importTypeSet = /* @__PURE__ */ new Set();
|
|
528
|
+
for (const op of operations) {
|
|
529
|
+
for (const t of op.requestImportTypes) importTypeSet.add(t);
|
|
530
|
+
if (op.responseTypeName) importTypeSet.add(op.responseTypeName);
|
|
531
|
+
}
|
|
532
|
+
importTypeSet.add("RequestConfig");
|
|
533
|
+
const importLine = `import type { ${[...importTypeSet].join(", ")} } from ${JSON.stringify(typeImportPath)};
|
|
534
|
+
`;
|
|
535
|
+
const needsBuildUrl = operations.some((op) => op.pathParams.length > 0);
|
|
536
|
+
const buildUrlHelper = needsBuildUrl ? `
|
|
537
|
+
function buildUrl(url: string, path?: Record<string, unknown>): string {
|
|
538
|
+
return url.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
|
|
539
|
+
}
|
|
540
|
+
` : "";
|
|
541
|
+
const functions = operations.map((op) => renderFunction2(op, true, mergeParams)).join("\n\n");
|
|
542
|
+
return `/* eslint-disable */
|
|
543
|
+
// Auto-generated by swgto.
|
|
544
|
+
import request from ${JSON.stringify(httpClientPath)};
|
|
545
|
+
${importLine}${buildUrlHelper}
|
|
546
|
+
${functions}
|
|
360
547
|
`;
|
|
361
548
|
}
|
|
362
549
|
|
|
@@ -377,11 +564,17 @@ function buildSchemaDoc(schema) {
|
|
|
377
564
|
}
|
|
378
565
|
return formatDocLines(lines);
|
|
379
566
|
}
|
|
567
|
+
function jsdocToInline(doc, indent) {
|
|
568
|
+
return doc.split("\n").filter((line) => {
|
|
569
|
+
const t = line.trim();
|
|
570
|
+
return t !== "/**" && t !== "*/" && !t.startsWith("/**");
|
|
571
|
+
}).map((line) => `${indent}// ${line.replace(/^\s*\* ?/, "")}`);
|
|
572
|
+
}
|
|
380
573
|
function renderObjectFields(fields, indent = "") {
|
|
381
574
|
const lines = [];
|
|
382
575
|
for (const field of fields) {
|
|
383
576
|
if (field.doc) {
|
|
384
|
-
lines.push(...field.doc
|
|
577
|
+
lines.push(...jsdocToInline(field.doc, indent));
|
|
385
578
|
}
|
|
386
579
|
lines.push(`${indent}${field.name}${field.optional ? "?" : ""}: ${field.type};`);
|
|
387
580
|
}
|
|
@@ -422,7 +615,7 @@ function renderComponentSchemas(document) {
|
|
|
422
615
|
});
|
|
423
616
|
}
|
|
424
617
|
function toJSDocType(typeText) {
|
|
425
|
-
return typeText.replace(
|
|
618
|
+
return typeText.replace(/;\s*/g, ", ").replace(/,\s*\}/g, " }");
|
|
426
619
|
}
|
|
427
620
|
function generateTypesFile(documentMap, operations, config) {
|
|
428
621
|
if (config.outputType === "js") {
|
|
@@ -463,16 +656,6 @@ function generateTypesFile(documentMap, operations, config) {
|
|
|
463
656
|
return `${parts.filter(Boolean).join("\n\n")}
|
|
464
657
|
`;
|
|
465
658
|
}
|
|
466
|
-
function generateApiDtsContent(operations, config) {
|
|
467
|
-
const lines = ["// Auto-generated by swgto.", 'export * from "./index";', `export * from "./${config.typeName}";`];
|
|
468
|
-
for (const operation of operations) {
|
|
469
|
-
const paramsType = operation.requestTypeExpression ?? "void";
|
|
470
|
-
const responseType = operation.responseTypeName ?? "unknown";
|
|
471
|
-
lines.push(`export declare function ${operation.functionName}<T = ${responseType}>(params${paramsType === "void" ? "?" : ""}: ${paramsType}, config?: import("./index").RequestConfig): Promise<T>;`);
|
|
472
|
-
}
|
|
473
|
-
return `${lines.join("\n")}
|
|
474
|
-
`;
|
|
475
|
-
}
|
|
476
659
|
|
|
477
660
|
// src/utils/fs.ts
|
|
478
661
|
import { mkdir, rm, writeFile } from "fs/promises";
|
|
@@ -485,12 +668,17 @@ async function writeTextFile(filePath, content) {
|
|
|
485
668
|
await writeFile(filePath, content, "utf8");
|
|
486
669
|
}
|
|
487
670
|
async function removeDir(dirPath) {
|
|
488
|
-
|
|
671
|
+
try {
|
|
672
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
673
|
+
} catch {
|
|
674
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
675
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
676
|
+
}
|
|
489
677
|
}
|
|
490
678
|
|
|
491
679
|
// src/generate.ts
|
|
492
|
-
function getRootImportPath(
|
|
493
|
-
return
|
|
680
|
+
function getRootImportPath(typeName) {
|
|
681
|
+
return `../${typeName}`;
|
|
494
682
|
}
|
|
495
683
|
async function generateFromConfig(cwd = process.cwd()) {
|
|
496
684
|
const { configPath, config } = await loadConfig(cwd);
|
|
@@ -498,6 +686,7 @@ async function generateFromConfig(cwd = process.cwd()) {
|
|
|
498
686
|
const operations = [];
|
|
499
687
|
if (config.cleanOutput) {
|
|
500
688
|
await removeDir(path3.join(cwd, config.outputDir));
|
|
689
|
+
console.log(`Cleaned output directory: ${config.outputDir}`);
|
|
501
690
|
}
|
|
502
691
|
for (const docUrl of config.docUrls) {
|
|
503
692
|
const document = await loadOpenApiDocument(docUrl);
|
|
@@ -507,33 +696,45 @@ async function generateFromConfig(cwd = process.cwd()) {
|
|
|
507
696
|
const grouped = groupByPrefix(operations);
|
|
508
697
|
const files = [];
|
|
509
698
|
for (const [moduleName, moduleOperations] of Object.entries(grouped)) {
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
699
|
+
if (config.fileNaming === "module") {
|
|
700
|
+
const controllerMap = groupByController(moduleOperations);
|
|
701
|
+
for (const [controllerName, controllerOperations] of Object.entries(controllerMap)) {
|
|
702
|
+
const relativeFile = path3.join(config.outputDir, moduleName, `${controllerName}.${config.outputType}`);
|
|
703
|
+
const absoluteFile = path3.join(cwd, relativeFile);
|
|
704
|
+
const content = config.outputType === "ts" ? generateTsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
|
|
705
|
+
for (const op of controllerOperations) {
|
|
706
|
+
op.fileBaseName = controllerName;
|
|
707
|
+
}
|
|
708
|
+
await writeTextFile(absoluteFile, content);
|
|
709
|
+
files.push(relativeFile);
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
for (const operation of moduleOperations) {
|
|
713
|
+
const relativeFile = path3.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
|
|
714
|
+
const absoluteFile = path3.join(cwd, relativeFile);
|
|
715
|
+
const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
|
|
716
|
+
await writeTextFile(absoluteFile, content);
|
|
717
|
+
files.push(relativeFile);
|
|
718
|
+
}
|
|
516
719
|
}
|
|
517
720
|
}
|
|
518
721
|
const typesContent = generateTypesFile(documentMap, operations, config);
|
|
519
|
-
const apiDtsContent = generateApiDtsContent(operations, config);
|
|
520
722
|
const indexContent = generateIndexFile(operations, config);
|
|
521
723
|
const typesFile = path3.join(cwd, config.outputDir, `${config.typeName}.${config.outputType === "ts" ? "ts" : "js"}`);
|
|
522
|
-
const apiDtsFile = path3.join(cwd, config.outputDir, "api.d.ts");
|
|
523
724
|
const indexFile = path3.join(cwd, config.outputDir, `index.${config.outputType}`);
|
|
524
725
|
await writeTextFile(typesFile, typesContent);
|
|
525
|
-
await writeTextFile(apiDtsFile, apiDtsContent);
|
|
526
726
|
await writeTextFile(indexFile, indexContent);
|
|
527
727
|
files.push(
|
|
528
728
|
path3.relative(cwd, typesFile),
|
|
529
|
-
path3.relative(cwd, apiDtsFile),
|
|
530
729
|
path3.relative(cwd, indexFile)
|
|
531
730
|
);
|
|
532
731
|
return {
|
|
533
732
|
configPath,
|
|
534
733
|
files,
|
|
535
734
|
operationCount: operations.length,
|
|
536
|
-
moduleCount: Object.keys(grouped).length
|
|
735
|
+
moduleCount: Object.keys(grouped).length,
|
|
736
|
+
apiFileCount: files.length - 2
|
|
737
|
+
// exclude types + index
|
|
537
738
|
};
|
|
538
739
|
}
|
|
539
740
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
generateFromConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4OK6R2K5.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
async function main() {
|
|
@@ -10,8 +10,8 @@ async function main() {
|
|
|
10
10
|
const result = await generateFromConfig(process.cwd());
|
|
11
11
|
const elapsedMs = Date.now() - startedAt;
|
|
12
12
|
console.log(`swgto loaded config: ${result.configPath}`);
|
|
13
|
-
console.log(`Generated ${result.operationCount}
|
|
14
|
-
console.log(`Wrote ${result.files.length}
|
|
13
|
+
console.log(`Generated ${result.apiFileCount} api file(s), ${result.operationCount} operation(s) in ${result.moduleCount} module(s).`);
|
|
14
|
+
console.log(`Wrote ${result.files.length} file(s) total (including types, index).`);
|
|
15
15
|
console.log(`Done in ${elapsedMs}ms.`);
|
|
16
16
|
} catch (error) {
|
|
17
17
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ interface GenerateResult {
|
|
|
3
3
|
files: string[];
|
|
4
4
|
operationCount: number;
|
|
5
5
|
moduleCount: number;
|
|
6
|
+
apiFileCount: number;
|
|
6
7
|
}
|
|
7
8
|
declare function generateFromConfig(cwd?: string): Promise<GenerateResult>;
|
|
8
9
|
|
|
@@ -13,11 +14,15 @@ interface SwaggerTsConfig {
|
|
|
13
14
|
httpClientPath: string;
|
|
14
15
|
renameMethod?: (path: string, method: string) => string;
|
|
15
16
|
resolveRequestPath?: (path: string, method: string, docUrl: string) => string;
|
|
17
|
+
ignoreUrl?: (path: string, method: string, docUrl: string) => boolean;
|
|
16
18
|
outputDir?: string;
|
|
17
19
|
moduleName?: (docUrl: string) => string;
|
|
18
20
|
outputType?: OutputType;
|
|
19
21
|
typeName?: string;
|
|
20
22
|
cleanOutput?: boolean;
|
|
23
|
+
fileNaming?: 'module' | 'path';
|
|
24
|
+
flattenQueryParam?: boolean;
|
|
25
|
+
mergeParams?: boolean;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export { type GenerateResult, type RequestConfig, type SwaggerTsConfig, generateFromConfig };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swgto-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate API request files from OpenAPI 3.x documents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
|
18
18
|
"dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "tsx ./src/cli.ts"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"openapi",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"jiti": "^2.4.2"
|
|
32
|
+
"jiti": "^2.4.2",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"tsx": "^4.21.0"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"@types/node": "^24.6.0",
|