swagger-fsd-gen 1.0.3 → 1.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.4
4
+
5
+ - Added stale generated module pruning (enabled by default) so removed/renamed Swagger tags no longer leave old `entities/{module}/api/*` files behind.
6
+ - Added `--prune-stale <true|false>` option to control stale-file pruning behavior.
7
+ - Fixed axios template method generation to use correct axios signatures:
8
+ - `post/put/patch(url, data, config)`
9
+ - `delete(url, config)` with optional `data` in config
10
+ - `get(url, config)` and other non-body methods.
11
+ - This resolves mixed-client leftovers (for example stale `ky` modules) when regenerating with `--http-client axios`.
12
+
3
13
  ## 1.0.3
4
14
 
5
15
  - Fixed invalid TypeScript identifiers in generated mutation keys when API paths contain `-` (for example `undo-upload`, `applied-campaigns`, `notification-settings`, `custom-filters`).
package/README.md CHANGED
@@ -39,6 +39,9 @@ yarn generate-all --uri https://api.example.com/swagger.json --http-client axios
39
39
 
40
40
  # ky 클라이언트로 생성 (기본값)
41
41
  yarn generate-all --uri https://api.example.com/swagger.json --http-client ky
42
+
43
+ # stale 파일 정리를 끄고 생성
44
+ yarn generate-all --uri https://api.example.com/swagger.json --prune-stale false
42
45
  ```
43
46
 
44
47
  ### 2. package.json에 스크립트 추가 (권장)
@@ -98,6 +101,7 @@ src/
98
101
  | --username | Basic Auth 사용자명 | - |
99
102
  | --password | Basic Auth 비밀번호 | - |
100
103
  | --http-client | HTTP 클라이언트 선택 | ky (`axios`, `ky`) |
104
+ | --prune-stale | stale 생성 파일 정리 | true (`true`, `false`) |
101
105
  | --dto-output-path | DTO 파일 경로 | src/shared/api/dto.ts |
102
106
  | --api-output-path | API 클래스 경로 | src/entities/{moduleName}/api/index.ts |
103
107
  | --query-output-path | Query 훅 경로 | src/entities/{moduleName}/api/queries.ts |
package/USAGE.md CHANGED
@@ -46,6 +46,9 @@ generate-all --uri ./swagger/my-api.yml --http-client axios
46
46
  # ky 클라이언트로 생성 (기본값)
47
47
  generate-all --uri ./swagger/my-api.yml --http-client ky
48
48
 
49
+ # stale 생성 파일 정리 비활성화
50
+ generate-all --uri ./swagger/my-api.yml --prune-stale false
51
+
49
52
  # 인증이 필요한 경우
50
53
  generate-all --uri https://api.example.com/swagger.json --username admin --password secret
51
54
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swagger-fsd-gen",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "main": "index.js",
5
5
  "description": "Swagger API client generator that creates type-safe API clients using axios or ky and TanStack Query with Feature Sliced Design pattern. Automatically generates API client code from Swagger/OpenAPI specifications.",
6
6
  "scripts": {
@@ -80,6 +80,7 @@ const parseArguments = () => {
80
80
  "mutation-output-path",
81
81
  "project-template",
82
82
  ],
83
+ boolean: ["prune-stale"],
83
84
  alias: {
84
85
  u: "uri",
85
86
  un: "username",
@@ -91,6 +92,10 @@ const parseArguments = () => {
91
92
  qp: "query-output-path",
92
93
  mp: "mutation-output-path",
93
94
  pt: "project-template",
95
+ ps: "prune-stale",
96
+ },
97
+ default: {
98
+ "prune-stale": true,
94
99
  },
95
100
  });
96
101
 
@@ -105,6 +110,7 @@ const parseArguments = () => {
105
110
  queryOutputPath: argv["query-output-path"],
106
111
  mutationOutputPath: argv["mutation-output-path"],
107
112
  projectTemplate: argv["project-template"],
113
+ pruneStale: argv["prune-stale"],
108
114
  };
109
115
  };
110
116
 
@@ -197,6 +203,7 @@ const printUsage = (outputPaths) => {
197
203
  "[--api-output-path <api-output-path>] " +
198
204
  "[--query-output-path <query-output-path>] " +
199
205
  "[--mutation-output-path <mutation-output-path>] " +
206
+ "[--prune-stale <true|false>] " +
200
207
  "[--project-template <project-template>]"
201
208
  );
202
209
  console.error(
@@ -208,6 +215,79 @@ const printUsage = (outputPaths) => {
208
215
  );
209
216
  };
210
217
 
218
+ /**
219
+ * 모듈 파일명에서 모듈명을 추출
220
+ * @param {string} fileName
221
+ * @returns {string}
222
+ */
223
+ const extractModuleName = (fileName) => fileName.replace("Route", "").toLowerCase();
224
+
225
+ /**
226
+ * 템플릿 경로에서 현재 생성 대상이 아닌 모듈 파일을 정리
227
+ * @param {string} templatePath
228
+ * @param {Set<string>} generatedModules
229
+ */
230
+ const pruneStaleFilesByTemplate = async (templatePath, generatedModules) => {
231
+ if (!templatePath.includes("{moduleName}")) return;
232
+
233
+ const [prefix, suffix] = templatePath.split("{moduleName}");
234
+ const modulesRoot = prefix;
235
+
236
+ if (!fs.existsSync(modulesRoot)) return;
237
+
238
+ const moduleDirs = await fs.promises.readdir(modulesRoot, {
239
+ withFileTypes: true,
240
+ });
241
+
242
+ for (const dirent of moduleDirs) {
243
+ if (!dirent.isDirectory()) continue;
244
+
245
+ const moduleName = dirent.name;
246
+ if (generatedModules.has(moduleName)) continue;
247
+
248
+ const targetFilePath = path.join(modulesRoot, moduleName, suffix);
249
+ if (!fs.existsSync(targetFilePath)) continue;
250
+
251
+ await fs.promises.unlink(targetFilePath);
252
+ console.log(`🧹 Pruned stale generated file: ${targetFilePath}`);
253
+
254
+ const removableDirs = [
255
+ path.dirname(targetFilePath),
256
+ path.dirname(path.dirname(targetFilePath)),
257
+ ];
258
+
259
+ for (const candidateDir of removableDirs) {
260
+ if (!candidateDir.startsWith(modulesRoot)) continue;
261
+ if (!fs.existsSync(candidateDir)) continue;
262
+
263
+ const entries = await fs.promises.readdir(candidateDir);
264
+ if (entries.length === 0) {
265
+ await fs.promises.rmdir(candidateDir);
266
+ }
267
+ }
268
+ }
269
+ };
270
+
271
+ /**
272
+ * 이전 스웨거 기준 stale 생성 파일 정리
273
+ * @param {Object} outputPaths
274
+ * @param {Set<string>} generatedModules
275
+ */
276
+ const pruneStaleGeneratedFiles = async (outputPaths, generatedModules) => {
277
+ if (generatedModules.size === 0) return;
278
+
279
+ await pruneStaleFilesByTemplate(outputPaths.api.absolutePath, generatedModules);
280
+ await pruneStaleFilesByTemplate(
281
+ outputPaths.apiInstance.absolutePath,
282
+ generatedModules
283
+ );
284
+ await pruneStaleFilesByTemplate(outputPaths.query.absolutePath, generatedModules);
285
+ await pruneStaleFilesByTemplate(
286
+ outputPaths.mutation.absolutePath,
287
+ generatedModules
288
+ );
289
+ };
290
+
211
291
  /**
212
292
  * swagger-typescript-api를 사용하여 API 코드 생성
213
293
  * @param {Object} params - 생성 파라미터
@@ -290,6 +370,8 @@ const generateApiFunctionCode = async (args, outputPaths) => {
290
370
  prettier: { parser: "typescript" },
291
371
  });
292
372
 
373
+ const generatedModules = new Set();
374
+
293
375
  for (const { fileName, fileContent } of apiFunctionCode.files) {
294
376
  if (fileName === "http-client") continue;
295
377
 
@@ -300,7 +382,8 @@ const generateApiFunctionCode = async (args, outputPaths) => {
300
382
  formatWithProjectPrettier(outputPath);
301
383
  console.log(`✅ Generated DTO: ${outputPaths.dto.relativePath}`);
302
384
  } else {
303
- const moduleName = fileName.replace("Route", "").toLowerCase();
385
+ const moduleName = extractModuleName(fileName);
386
+ generatedModules.add(moduleName);
304
387
 
305
388
  if (fileName.match(/Route$/)) {
306
389
  outputPath = outputPaths.apiInstance.absolutePath.replace(
@@ -325,6 +408,8 @@ const generateApiFunctionCode = async (args, outputPaths) => {
325
408
  }
326
409
  }
327
410
  }
411
+
412
+ return generatedModules;
328
413
  };
329
414
 
330
415
  /**
@@ -351,10 +436,13 @@ const generateTanstackQueryCode = async (args, outputPaths) => {
351
436
  prettier: { parser: "typescript" },
352
437
  });
353
438
 
439
+ const generatedModules = new Set();
440
+
354
441
  for (const { fileName, fileContent } of tanstackQueryCode.files) {
355
442
  if (fileName === "http-client" || fileName === "data-contracts") continue;
356
443
 
357
- const moduleName = fileName.replace("Route", "").toLowerCase();
444
+ const moduleName = extractModuleName(fileName);
445
+ generatedModules.add(moduleName);
358
446
  let outputPath;
359
447
 
360
448
  if (fileName.match(/Route$/)) {
@@ -379,6 +467,8 @@ const generateTanstackQueryCode = async (args, outputPaths) => {
379
467
  );
380
468
  }
381
469
  }
470
+
471
+ return generatedModules;
382
472
  };
383
473
 
384
474
  /**
@@ -401,10 +491,21 @@ const main = async () => {
401
491
  console.log(`🌐 HTTP client mode: ${args.httpClient}`);
402
492
 
403
493
  // 1. API 클래스와 DTO 생성
404
- await generateApiFunctionCode(args, outputPaths);
494
+ const generatedApiModules = await generateApiFunctionCode(args, outputPaths);
405
495
 
406
496
  // 2. TanStack Query 훅 생성
407
- await generateTanstackQueryCode(args, outputPaths);
497
+ const generatedTanstackModules = await generateTanstackQueryCode(
498
+ args,
499
+ outputPaths
500
+ );
501
+
502
+ if (args.pruneStale) {
503
+ const generatedModules = new Set([
504
+ ...generatedApiModules,
505
+ ...generatedTanstackModules,
506
+ ]);
507
+ await pruneStaleGeneratedFiles(outputPaths, generatedModules);
508
+ }
408
509
 
409
510
  console.log("\n🎉 API client generation completed successfully!");
410
511
  console.log("\n📁 Generated files:");
@@ -94,7 +94,23 @@ const instance = <%= instanceParamName %> ?? this.instance;
94
94
  <% } %>
95
95
 
96
96
  <% if (isAxios) { %>
97
- return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`, {
97
+ <% if (['post', 'put', 'patch'].includes(route.request.method.toLowerCase())) { %>
98
+ return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(
99
+ `<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`,
100
+ <% if (payload) { %>
101
+ data,
102
+ <% } else { %>
103
+ undefined,
104
+ <% } %>
105
+ {
106
+ <% if (query) { %>
107
+ params, // 쿼리 파라미터 설정
108
+ <% } %>
109
+ ...options, // 추가 axios 옵션 병합
110
+ }
111
+ ).then((response) => response.data);
112
+ <% } else if (route.request.method.toLowerCase() === 'delete') { %>
113
+ return instance.delete<<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`, {
98
114
  <% if (query) { %>
99
115
  params, // 쿼리 파라미터 설정
100
116
  <% } %>
@@ -104,6 +120,14 @@ return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`
104
120
  ...options, // 추가 axios 옵션 병합
105
121
  }).then((response) => response.data);
106
122
  <% } else { %>
123
+ return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`, {
124
+ <% if (query) { %>
125
+ params, // 쿼리 파라미터 설정
126
+ <% } %>
127
+ ...options, // 추가 axios 옵션 병합
128
+ }).then((response) => response.data);
129
+ <% } %>
130
+ <% } else { %>
107
131
  // HTTP 요청 실행 및 JSON 응답 반환
108
132
  return instance.<%= route.request.method.toLowerCase() %><<%= responseType %>>(`<%= route.request.path.replace(/{/g, '{').replace(/}/g, '}').slice(1,) %>`,{
109
133
  <% if (query) { %>