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 +10 -0
- package/README.md +4 -0
- package/USAGE.md +3 -0
- package/package.json +1 -1
- package/scripts/generate-all.js +105 -4
- package/templates/procedure-call.ejs +25 -1
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
|
+
"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": {
|
package/scripts/generate-all.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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) { %>
|