swgto-ts 0.1.1 → 2.0.1

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
@@ -7,13 +7,12 @@
7
7
  - 按路径前缀分组的请求文件
8
8
  - 按文档模块输出请求文件。单文档默认输出到 `services/`,多文档时多个模块目录平级
9
9
  - `outputDir/index.ts` 或 `outputDir/index.js`
10
- - 聚合声明文件 `outputDir/api.d.ts`
11
10
  - 类型文件 `outputDir/types.ts` 或带 JSDoc 的 `outputDir/types.js`
12
11
 
13
12
  ## 安装
14
13
 
15
14
  ```bash
16
- npm install swgto
15
+ npm install swgto-ts
17
16
  ```
18
17
 
19
18
  ## 配置
@@ -21,7 +20,7 @@ npm install swgto
21
20
  在项目根目录创建 `.swaggerts.config.ts`:
22
21
 
23
22
  ```ts
24
- import type { SwaggerTsConfig } from 'swgto';
23
+ import type { SwaggerTsConfig } from 'swgto-ts'
25
24
 
26
25
  const config: SwaggerTsConfig = {
27
26
  docUrls: 'https://example.com/openapi.json',
@@ -32,23 +31,20 @@ const config: SwaggerTsConfig = {
32
31
  cleanOutput: true,
33
32
  renameMethod: (apiPath, method) => `${method}_${apiPath.replace(/[\\/{}]/g, '_')}`,
34
33
  resolveRequestPath: (apiPath, method) => `/proxy${apiPath}`,
35
- };
34
+ }
36
35
 
37
- export default config;
36
+ export default config
38
37
  ```
39
38
 
40
39
  多文档模式下需要提供 `moduleName`:
41
40
 
42
41
  ```ts
43
42
  export default {
44
- docUrls: [
45
- 'https://example.com/user-openapi.json',
46
- 'https://example.com/order-openapi.json',
47
- ],
43
+ docUrls: ['https://example.com/user-openapi.json', 'https://example.com/order-openapi.json'],
48
44
  httpClientPath: '@/utils/request',
49
45
  outputDir: 'src/api',
50
- moduleName: (docUrl) => docUrl.includes('user') ? 'user' : 'order',
51
- };
46
+ moduleName: (docUrl) => (docUrl.includes('user') ? 'user' : 'order'),
47
+ }
52
48
  ```
53
49
 
54
50
  ## 使用
@@ -61,7 +57,6 @@ swgto
61
57
 
62
58
  ```text
63
59
  src/api/
64
- api.d.ts
65
60
  index.ts
66
61
  types.ts
67
62
  services/
@@ -73,14 +68,80 @@ src/api/
73
68
 
74
69
  ```ts
75
70
  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;
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
85
84
  }
86
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
+ ```
@@ -1,5 +1,5 @@
1
1
  // src/generate.ts
2
- import path3 from "path";
2
+ import path4 from "path";
3
3
 
4
4
  // src/config/loadConfig.ts
5
5
  import { existsSync } from "fs";
@@ -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(path5, allPaths, pathMethods) {
78
+ const segments = path5.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 === path5) 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 === path5) 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 !== path5 && 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
+ `;
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] ?? ''));
359
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,19 +668,85 @@ 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
+ }
677
+ }
678
+
679
+ // src/utils/snapshot.ts
680
+ import path3 from "path";
681
+ import { readFile, writeFile as writeFile2 } from "fs/promises";
682
+ function buildKey(op) {
683
+ return `${op.docUrl}|${op.method}|${op.path}`;
684
+ }
685
+ function toEntry(op) {
686
+ return {
687
+ key: buildKey(op),
688
+ method: op.method,
689
+ path: op.path,
690
+ functionName: op.functionName,
691
+ summary: op.summary,
692
+ moduleName: op.moduleName
693
+ };
489
694
  }
695
+ async function loadSnapshot(snapshotPath) {
696
+ const map = /* @__PURE__ */ new Map();
697
+ try {
698
+ const raw = await readFile(snapshotPath, "utf8");
699
+ const data = JSON.parse(raw);
700
+ if (data.version === 1) {
701
+ for (const entry of data.operations) {
702
+ map.set(entry.key, entry);
703
+ }
704
+ }
705
+ } catch {
706
+ }
707
+ return map;
708
+ }
709
+ async function saveSnapshot(snapshotPath, operations) {
710
+ const snapshot = {
711
+ version: 1,
712
+ operations: operations.map(toEntry)
713
+ };
714
+ await ensureDir(path3.dirname(snapshotPath));
715
+ await writeFile2(snapshotPath, JSON.stringify(snapshot, null, 2), "utf8");
716
+ }
717
+ function compareSnapshot(previous, current) {
718
+ const currentKeys = /* @__PURE__ */ new Set();
719
+ const newOperations = [];
720
+ for (const op of current) {
721
+ const key = buildKey(op);
722
+ currentKeys.add(key);
723
+ if (!previous.has(key)) {
724
+ newOperations.push(toEntry(op));
725
+ }
726
+ }
727
+ const removedOperations = [];
728
+ for (const [key, entry] of previous) {
729
+ if (!currentKeys.has(key)) {
730
+ removedOperations.push(entry);
731
+ }
732
+ }
733
+ return { newOperations, removedOperations };
734
+ }
735
+ var SNAPSHOT_FILE = ".swaggerts.cache.json";
490
736
 
491
737
  // src/generate.ts
492
- function getRootImportPath(fileKind, typeName) {
493
- return fileKind === "types" ? `../${typeName}` : "../api";
738
+ function getRootImportPath(typeName) {
739
+ return `../${typeName}`;
494
740
  }
495
741
  async function generateFromConfig(cwd = process.cwd()) {
496
742
  const { configPath, config } = await loadConfig(cwd);
497
743
  const documentMap = /* @__PURE__ */ new Map();
498
744
  const operations = [];
745
+ const snapshotPath = path4.join(cwd, config.outputDir, SNAPSHOT_FILE);
746
+ const previousSnapshot = await loadSnapshot(snapshotPath);
499
747
  if (config.cleanOutput) {
500
- await removeDir(path3.join(cwd, config.outputDir));
748
+ await removeDir(path4.join(cwd, config.outputDir));
749
+ console.log(`Cleaned output directory: ${config.outputDir}`);
501
750
  }
502
751
  for (const docUrl of config.docUrls) {
503
752
  const document = await loadOpenApiDocument(docUrl);
@@ -505,35 +754,51 @@ async function generateFromConfig(cwd = process.cwd()) {
505
754
  operations.push(...parsePaths(document, docUrl, config));
506
755
  }
507
756
  const grouped = groupByPrefix(operations);
757
+ const { newOperations, removedOperations } = compareSnapshot(previousSnapshot, operations);
508
758
  const files = [];
509
759
  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);
760
+ if (config.fileNaming === "module") {
761
+ const controllerMap = groupByController(moduleOperations);
762
+ for (const [controllerName, controllerOperations] of Object.entries(controllerMap)) {
763
+ const relativeFile = path4.join(config.outputDir, moduleName, `${controllerName}.${config.outputType}`);
764
+ const absoluteFile = path4.join(cwd, relativeFile);
765
+ const content = config.outputType === "ts" ? generateTsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
766
+ for (const op of controllerOperations) {
767
+ op.fileBaseName = controllerName;
768
+ }
769
+ await writeTextFile(absoluteFile, content);
770
+ files.push(relativeFile);
771
+ }
772
+ } else {
773
+ for (const operation of moduleOperations) {
774
+ const relativeFile = path4.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
775
+ const absoluteFile = path4.join(cwd, relativeFile);
776
+ const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
777
+ await writeTextFile(absoluteFile, content);
778
+ files.push(relativeFile);
779
+ }
516
780
  }
517
781
  }
518
782
  const typesContent = generateTypesFile(documentMap, operations, config);
519
- const apiDtsContent = generateApiDtsContent(operations, config);
520
783
  const indexContent = generateIndexFile(operations, config);
521
- 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
- const indexFile = path3.join(cwd, config.outputDir, `index.${config.outputType}`);
784
+ const typesFile = path4.join(cwd, config.outputDir, `${config.typeName}.${config.outputType === "ts" ? "ts" : "js"}`);
785
+ const indexFile = path4.join(cwd, config.outputDir, `index.${config.outputType}`);
524
786
  await writeTextFile(typesFile, typesContent);
525
- await writeTextFile(apiDtsFile, apiDtsContent);
526
787
  await writeTextFile(indexFile, indexContent);
527
788
  files.push(
528
- path3.relative(cwd, typesFile),
529
- path3.relative(cwd, apiDtsFile),
530
- path3.relative(cwd, indexFile)
789
+ path4.relative(cwd, typesFile),
790
+ path4.relative(cwd, indexFile)
531
791
  );
792
+ await saveSnapshot(snapshotPath, operations);
532
793
  return {
533
794
  configPath,
534
795
  files,
535
796
  operationCount: operations.length,
536
- moduleCount: Object.keys(grouped).length
797
+ moduleCount: Object.keys(grouped).length,
798
+ apiFileCount: files.length - 2,
799
+ // exclude types + index
800
+ newOperations,
801
+ removedOperations
537
802
  };
538
803
  }
539
804
 
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-ZURWUVPF.js";
5
5
 
6
6
  // src/cli.ts
7
7
  async function main() {
@@ -10,9 +10,26 @@ 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
+ if (result.newOperations.length > 0) {
17
+ console.log(`
18
+ \u65B0\u589E ${result.newOperations.length} \u4E2A API:`);
19
+ const maxWidth = Math.max(...result.newOperations.map((op) => `${op.method.toUpperCase()} ${op.path}`.length));
20
+ for (const op of result.newOperations) {
21
+ const label = `${op.method.toUpperCase()} ${op.path}`.padEnd(maxWidth);
22
+ const desc = op.summary ? ` ${op.summary}` : "";
23
+ console.log(` ${label} -> ${op.functionName}${desc}`);
24
+ }
25
+ }
26
+ if (result.removedOperations.length > 0) {
27
+ console.log(`
28
+ \u79FB\u9664 ${result.removedOperations.length} \u4E2A API:`);
29
+ for (const op of result.removedOperations) {
30
+ console.log(` ${op.method.toUpperCase()} ${op.path} (${op.functionName})`);
31
+ }
32
+ }
16
33
  } catch (error) {
17
34
  const message = error instanceof Error ? error.message : String(error);
18
35
  console.error(`swgto failed: ${message}`);
package/dist/index.d.ts CHANGED
@@ -1,11 +1,3 @@
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
1
  type OutputType = 'ts' | 'js';
10
2
  type RequestConfig = Record<string, unknown>;
11
3
  interface SwaggerTsConfig {
@@ -13,11 +5,36 @@ interface SwaggerTsConfig {
13
5
  httpClientPath: string;
14
6
  renameMethod?: (path: string, method: string) => string;
15
7
  resolveRequestPath?: (path: string, method: string, docUrl: string) => string;
8
+ ignoreUrl?: (path: string, method: string, docUrl: string) => boolean;
16
9
  outputDir?: string;
17
10
  moduleName?: (docUrl: string) => string;
18
11
  outputType?: OutputType;
19
12
  typeName?: string;
20
13
  cleanOutput?: boolean;
14
+ fileNaming?: 'module' | 'path';
15
+ flattenQueryParam?: boolean;
16
+ mergeParams?: boolean;
17
+ }
18
+
19
+ interface SnapshotEntry {
20
+ /** Unique key: docUrl | method | path */
21
+ key: string;
22
+ method: string;
23
+ path: string;
24
+ functionName: string;
25
+ summary?: string;
26
+ moduleName: string;
21
27
  }
22
28
 
29
+ interface GenerateResult {
30
+ configPath: string;
31
+ files: string[];
32
+ operationCount: number;
33
+ moduleCount: number;
34
+ apiFileCount: number;
35
+ newOperations: SnapshotEntry[];
36
+ removedOperations: SnapshotEntry[];
37
+ }
38
+ declare function generateFromConfig(cwd?: string): Promise<GenerateResult>;
39
+
23
40
  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-ZURWUVPF.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.1",
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",