swgto-ts 0.1.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 +86 -0
- package/dist/chunk-RRFCLHNU.js +560 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +6 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,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/api.d.ts`
|
|
11
|
+
- 类型文件 `outputDir/types.ts` 或带 JSDoc 的 `outputDir/types.js`
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install swgto
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 配置
|
|
20
|
+
|
|
21
|
+
在项目根目录创建 `.swaggerts.config.ts`:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import type { SwaggerTsConfig } from 'swgto';
|
|
25
|
+
|
|
26
|
+
const config: SwaggerTsConfig = {
|
|
27
|
+
docUrls: 'https://example.com/openapi.json',
|
|
28
|
+
httpClientPath: '@/utils/request',
|
|
29
|
+
outputDir: 'src/api',
|
|
30
|
+
outputType: 'ts',
|
|
31
|
+
typeName: 'types',
|
|
32
|
+
cleanOutput: true,
|
|
33
|
+
renameMethod: (apiPath, method) => `${method}_${apiPath.replace(/[\\/{}]/g, '_')}`,
|
|
34
|
+
resolveRequestPath: (apiPath, method) => `/proxy${apiPath}`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default config;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
多文档模式下需要提供 `moduleName`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export default {
|
|
44
|
+
docUrls: [
|
|
45
|
+
'https://example.com/user-openapi.json',
|
|
46
|
+
'https://example.com/order-openapi.json',
|
|
47
|
+
],
|
|
48
|
+
httpClientPath: '@/utils/request',
|
|
49
|
+
outputDir: 'src/api',
|
|
50
|
+
moduleName: (docUrl) => docUrl.includes('user') ? 'user' : 'order',
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 使用
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
swgto
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 输出示例
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
src/api/
|
|
64
|
+
api.d.ts
|
|
65
|
+
index.ts
|
|
66
|
+
types.ts
|
|
67
|
+
services/
|
|
68
|
+
get_user_list.ts
|
|
69
|
+
post_user_create.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 配置项
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
export interface SwaggerTsConfig {
|
|
76
|
+
docUrls: string | string[];
|
|
77
|
+
httpClientPath: string;
|
|
78
|
+
renameMethod?: (path: string, method: string) => string;
|
|
79
|
+
resolveRequestPath?: (path: string, method: string, docUrl: string) => string;
|
|
80
|
+
outputDir?: string;
|
|
81
|
+
moduleName?: (docUrl: string) => string;
|
|
82
|
+
outputType?: 'ts' | 'js';
|
|
83
|
+
typeName?: string;
|
|
84
|
+
cleanOutput?: boolean;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// src/generate.ts
|
|
2
|
+
import path3 from "path";
|
|
3
|
+
|
|
4
|
+
// src/config/loadConfig.ts
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { pathToFileURL } from "url";
|
|
8
|
+
import { createJiti } from "jiti";
|
|
9
|
+
var CONFIG_FILES = [".swaggerts.config.ts", ".swaggerts.config.js"];
|
|
10
|
+
function assertConfig(config) {
|
|
11
|
+
if (!config || typeof config !== "object") {
|
|
12
|
+
throw new Error("`.swaggerts.config` must export a config object.");
|
|
13
|
+
}
|
|
14
|
+
if (!config.docUrls || typeof config.docUrls !== "string" && !Array.isArray(config.docUrls)) {
|
|
15
|
+
throw new Error("`docUrls` must be a string or string array.");
|
|
16
|
+
}
|
|
17
|
+
if (!config.httpClientPath) {
|
|
18
|
+
throw new Error("`httpClientPath` is required.");
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(config.docUrls) && config.docUrls.length > 1 && typeof config.moduleName !== "function") {
|
|
21
|
+
throw new Error("`moduleName(docUrl)` is required when `docUrls` contains multiple documents.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function loadConfig(cwd) {
|
|
25
|
+
const configPath = CONFIG_FILES.map((fileName) => path.join(cwd, fileName)).find((filePath) => existsSync(filePath));
|
|
26
|
+
if (!configPath) {
|
|
27
|
+
throw new Error("Cannot find `.swaggerts.config.ts` or `.swaggerts.config.js` in current directory.");
|
|
28
|
+
}
|
|
29
|
+
const jiti = createJiti(pathToFileURL(configPath).href, {
|
|
30
|
+
interopDefault: true
|
|
31
|
+
});
|
|
32
|
+
const loaded = await jiti.import(configPath);
|
|
33
|
+
const rawConfig = loaded?.default ?? loaded;
|
|
34
|
+
assertConfig(rawConfig);
|
|
35
|
+
const docUrls = Array.isArray(rawConfig.docUrls) ? rawConfig.docUrls : [rawConfig.docUrls];
|
|
36
|
+
const config = {
|
|
37
|
+
...rawConfig,
|
|
38
|
+
docUrls,
|
|
39
|
+
outputDir: rawConfig.outputDir ?? "src/api",
|
|
40
|
+
outputType: rawConfig.outputType ?? "ts",
|
|
41
|
+
typeName: rawConfig.typeName ?? "types",
|
|
42
|
+
cleanOutput: rawConfig.cleanOutput ?? false
|
|
43
|
+
};
|
|
44
|
+
return { configPath, config };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/core/groupByPrefix.ts
|
|
48
|
+
function groupByPrefix(operations) {
|
|
49
|
+
return operations.reduce((acc, operation) => {
|
|
50
|
+
acc[operation.moduleName] ??= [];
|
|
51
|
+
acc[operation.moduleName].push(operation);
|
|
52
|
+
return acc;
|
|
53
|
+
}, {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/utils/naming.ts
|
|
57
|
+
function toPascalCase(value) {
|
|
58
|
+
return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join("");
|
|
59
|
+
}
|
|
60
|
+
function sanitizePathSegment(value) {
|
|
61
|
+
return value.replace(/^\//, "").replace(/\{|\}/g, "").replace(/[^a-zA-Z0-9/_-]/g, "").replace(/\/+/g, "/");
|
|
62
|
+
}
|
|
63
|
+
function sanitizeIdentifier(value) {
|
|
64
|
+
const normalized = value.replace(/[^a-zA-Z0-9_$]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
65
|
+
if (!normalized) {
|
|
66
|
+
return "generated_api";
|
|
67
|
+
}
|
|
68
|
+
return /^[0-9]/.test(normalized) ? `api_${normalized}` : normalized;
|
|
69
|
+
}
|
|
70
|
+
function buildDefaultMethodName(apiPath, method) {
|
|
71
|
+
const cleaned = sanitizePathSegment(apiPath).replace(/\//g, "_").replace(/_+/g, "_");
|
|
72
|
+
return sanitizeIdentifier([method.toLowerCase(), cleaned || "root"].join("_"));
|
|
73
|
+
}
|
|
74
|
+
function buildTypeName(functionName, suffix) {
|
|
75
|
+
return `${toPascalCase(functionName)}${suffix}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/core/parsePaths.ts
|
|
79
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
80
|
+
function getPrimarySchema(content) {
|
|
81
|
+
if (!content) {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
const jsonLikeKey = Object.keys(content).find((key) => key.includes("json")) ?? Object.keys(content)[0];
|
|
85
|
+
return jsonLikeKey ? content[jsonLikeKey]?.schema : void 0;
|
|
86
|
+
}
|
|
87
|
+
function getSuccessResponse(responses) {
|
|
88
|
+
if (!responses) {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
const successCode = ["200", "201", "202", "default"].find((code) => responses[code]);
|
|
92
|
+
return successCode ? getPrimarySchema(responses[successCode]?.content) : void 0;
|
|
93
|
+
}
|
|
94
|
+
function parsePaths(document, docUrl, config) {
|
|
95
|
+
const operations = [];
|
|
96
|
+
for (const [apiPath, pathItem] of Object.entries(document.paths ?? {})) {
|
|
97
|
+
for (const [method, operation] of Object.entries(pathItem ?? {})) {
|
|
98
|
+
if (!HTTP_METHODS.includes(method) || !operation) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const moduleName = config.moduleName?.(docUrl) ?? "services";
|
|
102
|
+
const functionName = sanitizeIdentifier(
|
|
103
|
+
config.renameMethod?.(apiPath, method) ?? buildDefaultMethodName(apiPath, method)
|
|
104
|
+
);
|
|
105
|
+
const requestPath = config.resolveRequestPath?.(apiPath, method, docUrl) ?? apiPath;
|
|
106
|
+
const queryParams = (operation.parameters ?? []).filter((item) => item?.in === "query");
|
|
107
|
+
const pathParams = (operation.parameters ?? []).filter((item) => item?.in === "path");
|
|
108
|
+
const requestBodySchema = getPrimarySchema(operation.requestBody?.content);
|
|
109
|
+
const responseSchema = getSuccessResponse(operation.responses);
|
|
110
|
+
const hasRequestParams = queryParams.length > 0 || pathParams.length > 0 || Boolean(requestBodySchema);
|
|
111
|
+
const requestTypeName = hasRequestParams ? buildTypeName(functionName, "Request") : void 0;
|
|
112
|
+
operations.push({
|
|
113
|
+
docUrl,
|
|
114
|
+
moduleName,
|
|
115
|
+
path: apiPath,
|
|
116
|
+
requestPath,
|
|
117
|
+
method,
|
|
118
|
+
functionName,
|
|
119
|
+
operationId: operation.operationId,
|
|
120
|
+
summary: operation.summary,
|
|
121
|
+
description: operation.description,
|
|
122
|
+
queryParams,
|
|
123
|
+
pathParams,
|
|
124
|
+
requestBodySchema,
|
|
125
|
+
responseSchema,
|
|
126
|
+
requestTypeName,
|
|
127
|
+
responseTypeName: responseSchema ? buildTypeName(functionName, "Response") : void 0,
|
|
128
|
+
fileBaseName: functionName
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return operations;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/fetch/loadOpenApi.ts
|
|
136
|
+
async function loadOpenApiDocument(docUrl) {
|
|
137
|
+
const response = await fetch(docUrl);
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Failed to fetch OpenAPI document: ${docUrl} (${response.status})`);
|
|
140
|
+
}
|
|
141
|
+
const document = await response.json();
|
|
142
|
+
if (!document.openapi?.startsWith("3.")) {
|
|
143
|
+
throw new Error(`Only OpenAPI 3.x is supported: ${docUrl}`);
|
|
144
|
+
}
|
|
145
|
+
if (!document.paths || typeof document.paths !== "object") {
|
|
146
|
+
throw new Error(`OpenAPI document does not contain valid paths: ${docUrl}`);
|
|
147
|
+
}
|
|
148
|
+
return document;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/generators/genIndex.ts
|
|
152
|
+
function generateIndexFile(operations, config) {
|
|
153
|
+
const exports = operations.map((operation) => {
|
|
154
|
+
return `export * from "./${operation.moduleName}/${operation.fileBaseName}";`;
|
|
155
|
+
});
|
|
156
|
+
return `/* eslint-disable */
|
|
157
|
+
// Auto-generated by swgto.
|
|
158
|
+
${exports.join("\n")}
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/generators/genRequestJs.ts
|
|
163
|
+
function buildFunctionDoc(operation, typeImportPath, requestParamType) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
if (operation.summary) {
|
|
166
|
+
lines.push(operation.summary);
|
|
167
|
+
}
|
|
168
|
+
if (operation.description) {
|
|
169
|
+
lines.push(operation.description);
|
|
170
|
+
}
|
|
171
|
+
lines.push(`@path ${operation.path}`);
|
|
172
|
+
lines.push(`@requestPath ${operation.requestPath}`);
|
|
173
|
+
lines.push(`@method ${operation.method.toUpperCase()}`);
|
|
174
|
+
lines.push("@template T");
|
|
175
|
+
lines.push(`@param ${requestParamType === "void" ? "[params]" : `{import(${JSON.stringify(typeImportPath)}).${requestParamType}} params`}`);
|
|
176
|
+
lines.push("@param [config]");
|
|
177
|
+
lines.push("@returns {Promise<T>}");
|
|
178
|
+
return ["/**", ...lines.map((line) => ` * ${line}`), " */"].join("\n");
|
|
179
|
+
}
|
|
180
|
+
function generateJsRequestFile(operation, httpClientPath, typeImportPath) {
|
|
181
|
+
const requestParamType = operation.requestTypeName ?? "void";
|
|
182
|
+
const queryLine = operation.queryParams.length ? " params: params?.query,\n" : "";
|
|
183
|
+
const bodyLine = operation.requestBodySchema ? " data: params?.body,\n" : "";
|
|
184
|
+
const pathLine = operation.pathParams.length ? " url: buildUrl(params?.path),\n" : ` url: ${JSON.stringify(operation.requestPath)},
|
|
185
|
+
`;
|
|
186
|
+
const buildUrlHelper = operation.pathParams.length ? `
|
|
187
|
+
function buildUrl(path) {
|
|
188
|
+
return ${JSON.stringify(operation.requestPath)}.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
|
|
189
|
+
}
|
|
190
|
+
` : "";
|
|
191
|
+
return `/* eslint-disable */
|
|
192
|
+
// Auto-generated by swgto.
|
|
193
|
+
import request from ${JSON.stringify(httpClientPath)};
|
|
194
|
+
${buildUrlHelper}
|
|
195
|
+
|
|
196
|
+
${buildFunctionDoc(operation, typeImportPath, requestParamType)}
|
|
197
|
+
export async function ${operation.functionName}(params, config) {
|
|
198
|
+
return request({
|
|
199
|
+
${pathLine} method: ${JSON.stringify(operation.method)},
|
|
200
|
+
${queryLine}${bodyLine} ...config,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/generators/genRequestTs.ts
|
|
207
|
+
function buildFunctionDoc2(operation) {
|
|
208
|
+
const lines = [
|
|
209
|
+
operation.summary,
|
|
210
|
+
operation.description,
|
|
211
|
+
`@path ${operation.path}`,
|
|
212
|
+
`@requestPath ${operation.requestPath}`,
|
|
213
|
+
`@method ${operation.method.toUpperCase()}`
|
|
214
|
+
].filter((value) => Boolean(value));
|
|
215
|
+
if (!lines.length) {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
return ["/**", ...lines.map((line) => ` * ${line}`), " */", ""].join("\n");
|
|
219
|
+
}
|
|
220
|
+
function generateTsRequestFile(operation, httpClientPath, typeImportPath) {
|
|
221
|
+
const importTypes = [
|
|
222
|
+
operation.requestTypeName,
|
|
223
|
+
operation.responseTypeName,
|
|
224
|
+
"RequestConfig"
|
|
225
|
+
].filter((value, index, array) => Boolean(value) && array.indexOf(value) === index);
|
|
226
|
+
const importLine = importTypes.length ? `import type { ${importTypes.join(", ")} } from ${JSON.stringify(typeImportPath)};
|
|
227
|
+
` : "";
|
|
228
|
+
const requestArg = operation.requestTypeName ? `params: ${operation.requestTypeName}, config?: RequestConfig` : "params?: void, config?: RequestConfig";
|
|
229
|
+
const defaultResponseType = operation.responseTypeName ?? "unknown";
|
|
230
|
+
const bodyLine = operation.requestBodySchema ? " data: params?.body,\n" : "";
|
|
231
|
+
const queryLine = operation.queryParams.length ? " params: params?.query,\n" : "";
|
|
232
|
+
const pathLine = operation.pathParams.length ? " url: buildUrl(params?.path),\n" : ` url: ${JSON.stringify(operation.requestPath)},
|
|
233
|
+
`;
|
|
234
|
+
const buildUrlHelper = operation.pathParams.length ? `
|
|
235
|
+
function buildUrl(path?: Record<string, unknown>): string {
|
|
236
|
+
return ${JSON.stringify(operation.requestPath)}.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
|
|
237
|
+
}
|
|
238
|
+
` : "";
|
|
239
|
+
return `/* eslint-disable */
|
|
240
|
+
// Auto-generated by swgto.
|
|
241
|
+
import request from ${JSON.stringify(httpClientPath)};
|
|
242
|
+
${importLine}
|
|
243
|
+
${buildUrlHelper}
|
|
244
|
+
${buildFunctionDoc2(operation)}
|
|
245
|
+
export async function ${operation.functionName}<T = ${defaultResponseType}>(${requestArg}): Promise<T> {
|
|
246
|
+
return request<T>({
|
|
247
|
+
${pathLine} method: ${JSON.stringify(operation.method)},
|
|
248
|
+
${queryLine}${bodyLine} ...config,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/generators/schemaToTs.ts
|
|
255
|
+
function formatPropertyName(name) {
|
|
256
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
257
|
+
}
|
|
258
|
+
function refToTypeName(ref) {
|
|
259
|
+
const parts = ref.split("/");
|
|
260
|
+
return parts[parts.length - 1] || "unknown";
|
|
261
|
+
}
|
|
262
|
+
function schemaToTs(schema) {
|
|
263
|
+
if (!schema) {
|
|
264
|
+
return "unknown";
|
|
265
|
+
}
|
|
266
|
+
if (schema.$ref) {
|
|
267
|
+
return refToTypeName(schema.$ref);
|
|
268
|
+
}
|
|
269
|
+
if (schema.enum?.length) {
|
|
270
|
+
return schema.enum.map((item) => JSON.stringify(item)).join(" | ");
|
|
271
|
+
}
|
|
272
|
+
if (schema.anyOf?.length) {
|
|
273
|
+
return schema.anyOf.map((item) => schemaToTs(item)).join(" | ");
|
|
274
|
+
}
|
|
275
|
+
if (schema.oneOf?.length) {
|
|
276
|
+
return schema.oneOf.map((item) => schemaToTs(item)).join(" | ");
|
|
277
|
+
}
|
|
278
|
+
if (schema.allOf?.length) {
|
|
279
|
+
return schema.allOf.map((item) => schemaToTs(item)).join(" & ");
|
|
280
|
+
}
|
|
281
|
+
if (schema.type === "array") {
|
|
282
|
+
return `${schemaToTs(schema.items)}[]`;
|
|
283
|
+
}
|
|
284
|
+
if (schema.type === "object" || schema.properties) {
|
|
285
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
286
|
+
const properties = Object.entries(schema.properties ?? {}).map(([key, value]) => {
|
|
287
|
+
const optional = requiredSet.has(key) ? "" : "?";
|
|
288
|
+
return `${formatPropertyName(key)}${optional}: ${schemaToTs(value)};`;
|
|
289
|
+
});
|
|
290
|
+
if (!properties.length && schema.additionalProperties) {
|
|
291
|
+
const valueType = schema.additionalProperties === true ? "unknown" : schemaToTs(schema.additionalProperties);
|
|
292
|
+
return `{ [key: string]: ${valueType} }`;
|
|
293
|
+
}
|
|
294
|
+
return `{ ${properties.join(" ")} }`;
|
|
295
|
+
}
|
|
296
|
+
switch (schema.type) {
|
|
297
|
+
case "integer":
|
|
298
|
+
case "number":
|
|
299
|
+
return "number";
|
|
300
|
+
case "boolean":
|
|
301
|
+
return "boolean";
|
|
302
|
+
case "string":
|
|
303
|
+
return "string";
|
|
304
|
+
case "null":
|
|
305
|
+
return "null";
|
|
306
|
+
default:
|
|
307
|
+
return "unknown";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function toTypePropertyName(name) {
|
|
311
|
+
return formatPropertyName(name);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/generators/genTypes.ts
|
|
315
|
+
function formatDocLines(lines) {
|
|
316
|
+
if (!lines.length) {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
return ["/**", ...lines.map((line) => ` * ${line}`), " */"].join("\n");
|
|
320
|
+
}
|
|
321
|
+
function buildSchemaDoc(schema) {
|
|
322
|
+
const lines = [];
|
|
323
|
+
if (schema?.description) {
|
|
324
|
+
lines.push(schema.description);
|
|
325
|
+
}
|
|
326
|
+
if (schema && schema.example !== void 0) {
|
|
327
|
+
lines.push(`@example ${JSON.stringify(schema.example)}`);
|
|
328
|
+
}
|
|
329
|
+
return formatDocLines(lines);
|
|
330
|
+
}
|
|
331
|
+
function buildParameterDoc(parameter, fallback) {
|
|
332
|
+
const lines = [parameter.description ?? fallback];
|
|
333
|
+
if (parameter.required) {
|
|
334
|
+
lines.push("@required");
|
|
335
|
+
}
|
|
336
|
+
if (parameter.example !== void 0) {
|
|
337
|
+
lines.push(`@example ${JSON.stringify(parameter.example)}`);
|
|
338
|
+
}
|
|
339
|
+
return formatDocLines(lines);
|
|
340
|
+
}
|
|
341
|
+
function renderObjectFields(fields, indent = "") {
|
|
342
|
+
const lines = [];
|
|
343
|
+
for (const field of fields) {
|
|
344
|
+
if (field.doc) {
|
|
345
|
+
lines.push(...field.doc.split("\n").map((line) => `${indent}${line}`));
|
|
346
|
+
}
|
|
347
|
+
lines.push(`${indent}${field.name}${field.optional ? "?" : ""}: ${field.type};`);
|
|
348
|
+
}
|
|
349
|
+
return lines;
|
|
350
|
+
}
|
|
351
|
+
function renderComponentSchema(name, schema) {
|
|
352
|
+
const typedSchema = schema;
|
|
353
|
+
const doc = buildSchemaDoc(typedSchema);
|
|
354
|
+
if (typedSchema.type === "object" || typedSchema.properties) {
|
|
355
|
+
const requiredSet = new Set(typedSchema.required ?? []);
|
|
356
|
+
const fields = Object.entries(typedSchema.properties ?? {}).map(([key, value]) => ({
|
|
357
|
+
name: toTypePropertyName(key),
|
|
358
|
+
type: schemaToTs(value),
|
|
359
|
+
optional: !requiredSet.has(key),
|
|
360
|
+
doc: buildSchemaDoc(value)
|
|
361
|
+
}));
|
|
362
|
+
const body = renderObjectFields(fields, " ").join("\n");
|
|
363
|
+
return `${doc ? `${doc}
|
|
364
|
+
` : ""}export interface ${name} {
|
|
365
|
+
${body}
|
|
366
|
+
}`;
|
|
367
|
+
}
|
|
368
|
+
return `${doc ? `${doc}
|
|
369
|
+
` : ""}export type ${name} = ${schemaToTs(schema)};`;
|
|
370
|
+
}
|
|
371
|
+
function renderOperationTypes(operation) {
|
|
372
|
+
const blocks = [];
|
|
373
|
+
if (operation.requestTypeName) {
|
|
374
|
+
const fields = [];
|
|
375
|
+
const pathFields = [];
|
|
376
|
+
const queryFields = [];
|
|
377
|
+
for (const param of operation.pathParams) {
|
|
378
|
+
pathFields.push({
|
|
379
|
+
name: toTypePropertyName(param.name),
|
|
380
|
+
type: schemaToTs(param.schema),
|
|
381
|
+
optional: false,
|
|
382
|
+
doc: buildParameterDoc(param, `Path parameter: ${param.name}`)
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
for (const param of operation.queryParams) {
|
|
386
|
+
queryFields.push({
|
|
387
|
+
name: toTypePropertyName(param.name),
|
|
388
|
+
type: schemaToTs(param.schema),
|
|
389
|
+
optional: !param.required,
|
|
390
|
+
doc: buildParameterDoc(param, `Query parameter: ${param.name}`)
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (operation.requestBodySchema) {
|
|
394
|
+
const doc = buildSchemaDoc(operation.requestBodySchema);
|
|
395
|
+
if (doc) {
|
|
396
|
+
fields.push(...doc.split("\n"));
|
|
397
|
+
}
|
|
398
|
+
fields.push(`body${operation.requestBodySchema.nullable ? "?" : ""}: ${schemaToTs(operation.requestBodySchema)};`);
|
|
399
|
+
}
|
|
400
|
+
if (pathFields.length) {
|
|
401
|
+
fields.push("/** Path parameters */");
|
|
402
|
+
fields.push("path: {");
|
|
403
|
+
fields.push(...renderObjectFields(pathFields, " "));
|
|
404
|
+
fields.push("};");
|
|
405
|
+
}
|
|
406
|
+
if (queryFields.length) {
|
|
407
|
+
fields.push("/** Query parameters */");
|
|
408
|
+
fields.push("query: {");
|
|
409
|
+
fields.push(...renderObjectFields(queryFields, " "));
|
|
410
|
+
fields.push("};");
|
|
411
|
+
}
|
|
412
|
+
const requestDoc = formatDocLines(
|
|
413
|
+
[
|
|
414
|
+
operation.summary,
|
|
415
|
+
operation.description
|
|
416
|
+
].filter((value) => Boolean(value))
|
|
417
|
+
);
|
|
418
|
+
blocks.push(`${requestDoc ? `${requestDoc}
|
|
419
|
+
` : ""}export interface ${operation.requestTypeName} {
|
|
420
|
+
${fields.map((line) => ` ${line}`).join("\n")}
|
|
421
|
+
}`);
|
|
422
|
+
}
|
|
423
|
+
if (operation.responseTypeName) {
|
|
424
|
+
const responseDoc = buildSchemaDoc(operation.responseSchema);
|
|
425
|
+
blocks.push(`${responseDoc ? `${responseDoc}
|
|
426
|
+
` : ""}export type ${operation.responseTypeName} = ${schemaToTs(operation.responseSchema)};`);
|
|
427
|
+
}
|
|
428
|
+
return blocks;
|
|
429
|
+
}
|
|
430
|
+
function renderComponentSchemas(document) {
|
|
431
|
+
return Object.entries(document.components?.schemas ?? {}).map(([name, schema]) => {
|
|
432
|
+
return renderComponentSchema(name, schema);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function toJSDocType(typeText) {
|
|
436
|
+
return typeText.replace(/;/g, "").replace(/\?/g, "=");
|
|
437
|
+
}
|
|
438
|
+
function generateTypesFile(documentMap, operations, config) {
|
|
439
|
+
if (config.outputType === "js") {
|
|
440
|
+
const parts2 = [
|
|
441
|
+
"/* eslint-disable */",
|
|
442
|
+
"// Auto-generated by swgto.",
|
|
443
|
+
"/** @typedef {Record<string, unknown>} RequestConfig */"
|
|
444
|
+
];
|
|
445
|
+
for (const [docUrl, document] of documentMap.entries()) {
|
|
446
|
+
const moduleName = config.moduleName?.(docUrl) ?? "services";
|
|
447
|
+
parts2.push(`// Types from ${moduleName}`);
|
|
448
|
+
for (const [name, schema] of Object.entries(document.components?.schemas ?? {})) {
|
|
449
|
+
parts2.push(`/** @typedef {${toJSDocType(schemaToTs(schema))}} ${name} */`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const operation of operations) {
|
|
453
|
+
if (operation.requestTypeName) {
|
|
454
|
+
const rendered = renderOperationTypes(operation).find((line) => line.startsWith(`export interface ${operation.requestTypeName}`));
|
|
455
|
+
if (rendered) {
|
|
456
|
+
const body = rendered.replace(`export interface ${operation.requestTypeName} `, "").trim();
|
|
457
|
+
parts2.push(`/** @typedef ${body} ${operation.requestTypeName} */`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (operation.responseTypeName) {
|
|
461
|
+
parts2.push(`/** @typedef {${toJSDocType(schemaToTs(operation.responseSchema))}} ${operation.responseTypeName} */`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
parts2.push("export {};");
|
|
465
|
+
return `${parts2.filter(Boolean).join("\n\n")}
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
const parts = [
|
|
469
|
+
"/* eslint-disable */",
|
|
470
|
+
"// Auto-generated by swgto.",
|
|
471
|
+
"export type RequestConfig = Record<string, unknown>;"
|
|
472
|
+
];
|
|
473
|
+
for (const [docUrl, document] of documentMap.entries()) {
|
|
474
|
+
const moduleName = config.moduleName?.(docUrl) ?? "services";
|
|
475
|
+
parts.push(`// Types from ${moduleName}`);
|
|
476
|
+
parts.push(...renderComponentSchemas(document));
|
|
477
|
+
}
|
|
478
|
+
for (const operation of operations) {
|
|
479
|
+
parts.push(...renderOperationTypes(operation));
|
|
480
|
+
}
|
|
481
|
+
return `${parts.filter(Boolean).join("\n\n")}
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
function generateApiDtsContent(operations, config) {
|
|
485
|
+
const lines = ["// Auto-generated by swgto.", 'export * from "./index";', `export * from "./${config.typeName}";`];
|
|
486
|
+
for (const operation of operations) {
|
|
487
|
+
const paramsType = operation.requestTypeName ?? "void";
|
|
488
|
+
const responseType = operation.responseTypeName ?? "unknown";
|
|
489
|
+
lines.push(`export declare function ${operation.functionName}<T = ${responseType}>(params${paramsType === "void" ? "?" : ""}: ${paramsType}, config?: import("./index").RequestConfig): Promise<T>;`);
|
|
490
|
+
}
|
|
491
|
+
return `${lines.join("\n")}
|
|
492
|
+
`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/utils/fs.ts
|
|
496
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
497
|
+
import path2 from "path";
|
|
498
|
+
async function ensureDir(dirPath) {
|
|
499
|
+
await mkdir(dirPath, { recursive: true });
|
|
500
|
+
}
|
|
501
|
+
async function writeTextFile(filePath, content) {
|
|
502
|
+
await ensureDir(path2.dirname(filePath));
|
|
503
|
+
await writeFile(filePath, content, "utf8");
|
|
504
|
+
}
|
|
505
|
+
async function removeDir(dirPath) {
|
|
506
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/generate.ts
|
|
510
|
+
function getRootImportPath(fileKind, typeName) {
|
|
511
|
+
return fileKind === "types" ? `../${typeName}` : "../api";
|
|
512
|
+
}
|
|
513
|
+
async function generateFromConfig(cwd = process.cwd()) {
|
|
514
|
+
const { configPath, config } = await loadConfig(cwd);
|
|
515
|
+
const documentMap = /* @__PURE__ */ new Map();
|
|
516
|
+
const operations = [];
|
|
517
|
+
if (config.cleanOutput) {
|
|
518
|
+
await removeDir(path3.join(cwd, config.outputDir));
|
|
519
|
+
}
|
|
520
|
+
for (const docUrl of config.docUrls) {
|
|
521
|
+
const document = await loadOpenApiDocument(docUrl);
|
|
522
|
+
documentMap.set(docUrl, document);
|
|
523
|
+
operations.push(...parsePaths(document, docUrl, config));
|
|
524
|
+
}
|
|
525
|
+
const grouped = groupByPrefix(operations);
|
|
526
|
+
const files = [];
|
|
527
|
+
for (const [moduleName, moduleOperations] of Object.entries(grouped)) {
|
|
528
|
+
for (const operation of moduleOperations) {
|
|
529
|
+
const relativeFile = path3.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
|
|
530
|
+
const absoluteFile = path3.join(cwd, relativeFile);
|
|
531
|
+
const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath("types", config.typeName)) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath("api", config.typeName));
|
|
532
|
+
await writeTextFile(absoluteFile, content);
|
|
533
|
+
files.push(relativeFile);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const typesContent = generateTypesFile(documentMap, operations, config);
|
|
537
|
+
const apiDtsContent = generateApiDtsContent(operations, config);
|
|
538
|
+
const indexContent = generateIndexFile(operations, config);
|
|
539
|
+
const typesFile = path3.join(cwd, config.outputDir, `${config.typeName}.${config.outputType === "ts" ? "ts" : "js"}`);
|
|
540
|
+
const apiDtsFile = path3.join(cwd, config.outputDir, "api.d.ts");
|
|
541
|
+
const indexFile = path3.join(cwd, config.outputDir, `index.${config.outputType}`);
|
|
542
|
+
await writeTextFile(typesFile, typesContent);
|
|
543
|
+
await writeTextFile(apiDtsFile, apiDtsContent);
|
|
544
|
+
await writeTextFile(indexFile, indexContent);
|
|
545
|
+
files.push(
|
|
546
|
+
path3.relative(cwd, typesFile),
|
|
547
|
+
path3.relative(cwd, apiDtsFile),
|
|
548
|
+
path3.relative(cwd, indexFile)
|
|
549
|
+
);
|
|
550
|
+
return {
|
|
551
|
+
configPath,
|
|
552
|
+
files,
|
|
553
|
+
operationCount: operations.length,
|
|
554
|
+
moduleCount: Object.keys(grouped).length
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export {
|
|
559
|
+
generateFromConfig
|
|
560
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
generateFromConfig
|
|
4
|
+
} from "./chunk-RRFCLHNU.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
async function main() {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
try {
|
|
10
|
+
const result = await generateFromConfig(process.cwd());
|
|
11
|
+
const elapsedMs = Date.now() - startedAt;
|
|
12
|
+
console.log(`swgto loaded config: ${result.configPath}`);
|
|
13
|
+
console.log(`Generated ${result.operationCount} API files in ${result.moduleCount} module(s).`);
|
|
14
|
+
console.log(`Wrote ${result.files.length} files, including index and api.d.ts.`);
|
|
15
|
+
console.log(`Done in ${elapsedMs}ms.`);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
console.error(`swgto failed: ${message}`);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
void main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface GenerateResult {
|
|
2
|
+
configPath: string;
|
|
3
|
+
files: string[];
|
|
4
|
+
operationCount: number;
|
|
5
|
+
moduleCount: number;
|
|
6
|
+
}
|
|
7
|
+
declare function generateFromConfig(cwd?: string): Promise<GenerateResult>;
|
|
8
|
+
|
|
9
|
+
type OutputType = 'ts' | 'js';
|
|
10
|
+
type RequestConfig = Record<string, unknown>;
|
|
11
|
+
interface SwaggerTsConfig {
|
|
12
|
+
docUrls: string | string[];
|
|
13
|
+
httpClientPath: string;
|
|
14
|
+
renameMethod?: (path: string, method: string) => string;
|
|
15
|
+
resolveRequestPath?: (path: string, method: string, docUrl: string) => string;
|
|
16
|
+
outputDir?: string;
|
|
17
|
+
moduleName?: (docUrl: string) => string;
|
|
18
|
+
outputType?: OutputType;
|
|
19
|
+
typeName?: string;
|
|
20
|
+
cleanOutput?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { type GenerateResult, type RequestConfig, type SwaggerTsConfig, generateFromConfig };
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swgto-ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate API request files from OpenAPI 3.x documents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"swgto": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
|
18
|
+
"dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
|
|
19
|
+
"test": "vitest run"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"openapi",
|
|
23
|
+
"swagger",
|
|
24
|
+
"typescript",
|
|
25
|
+
"generator",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"jiti": "^2.4.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^24.6.0",
|
|
36
|
+
"tsup": "^8.5.0",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vitest": "^3.2.4"
|
|
39
|
+
}
|
|
40
|
+
}
|