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 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
- - 聚合声明文件 `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
- ```
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
- requestTypeParts.push(`{ path: { ${pathFields.join(" ")} } }`);
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
- requestTypeParts.push(`{ query: { ${queryFields.join(" ")} } }`);
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, config) {
258
- const exports = operations.map((operation) => {
259
- return `export * from "./${operation.moduleName}/${operation.fileBaseName}";`;
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
- ${exports.join("\n")}
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
- lines.push("@returns {Promise<T>}");
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 generateJsRequestFile(operation, httpClientPath, typeImportPath) {
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 ? " params: params?.query,\n" : "";
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 pathLine = operation.pathParams.length ? " url: buildUrl(params?.path),\n" : ` url: ${JSON.stringify(operation.requestPath)},
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 generateTsRequestFile(operation, httpClientPath, typeImportPath) {
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
- ${buildFunctionDoc2(operation)}
354
- export async function ${operation.functionName}<T = ${defaultResponseType}>(${requestArg}): Promise<T> {
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.split("\n").map((line) => `${indent}${line}`));
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(/;/g, "").replace(/\?/g, "=");
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
- await rm(dirPath, { recursive: true, force: true });
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(fileKind, typeName) {
493
- return fileKind === "types" ? `../${typeName}` : "../api";
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
- for (const operation of moduleOperations) {
511
- const relativeFile = path3.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
512
- const absoluteFile = path3.join(cwd, relativeFile);
513
- const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath("types", config.typeName)) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath("api", config.typeName));
514
- await writeTextFile(absoluteFile, content);
515
- files.push(relativeFile);
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-5JY6MCOD.js";
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} API files in ${result.moduleCount} module(s).`);
14
- console.log(`Wrote ${result.files.length} files, including index and api.d.ts.`);
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  generateFromConfig
3
- } from "./chunk-5JY6MCOD.js";
3
+ } from "./chunk-4OK6R2K5.js";
4
4
  export {
5
5
  generateFromConfig
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swgto-ts",
3
- "version": "0.1.1",
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": "vitest run"
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",