@toktokhan-dev/cli-plugin-gen-api-react-query 0.1.10 → 0.2.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/dist/index.d.ts +10 -0
- package/dist/index.js +441 -79
- package/package.json +4 -4
- package/templates/custom-axios/data-contracts.eta +3 -4
- package/templates/custom-fetch/data-contracts.eta +3 -4
- package/templates/custom-fetch/http-client.eta +1 -1
- package/templates/my/react-query-type.eta +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -63,11 +63,21 @@ type GenerateSwaggerApiConfig = SwaggerSchemaOption & {
|
|
|
63
63
|
* @default false
|
|
64
64
|
*/
|
|
65
65
|
ignoreTlsError?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* data-contracts.ts를 모듈별로 분할할지 여부입니다.
|
|
68
|
+
* true일 때 각 모듈 폴더에 <module>.contracts.ts가 생성되고,
|
|
69
|
+
* 공유 타입은 @types/common-contracts.ts에 생성됩니다.
|
|
70
|
+
* @default false
|
|
71
|
+
*/
|
|
72
|
+
splitDataContracts?: boolean;
|
|
66
73
|
};
|
|
67
74
|
/**
|
|
68
75
|
* @category Commands
|
|
69
76
|
*/
|
|
70
77
|
declare const genApi: _toktokhan_dev_cli.MyCommand<GenerateSwaggerApiConfig, "gen:api">;
|
|
78
|
+
/**
|
|
79
|
+
* 스마트 타입 병합 함수들
|
|
80
|
+
*/
|
|
71
81
|
declare function mergeTypeScriptContent(existing: string, newContent: string): string;
|
|
72
82
|
|
|
73
83
|
export { genApi, mergeTypeScriptContent };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineCommand } from '@toktokhan-dev/cli';
|
|
2
|
-
import { createPackageRoot,
|
|
2
|
+
import { createPackageRoot, withLoading, cwd } from '@toktokhan-dev/node';
|
|
3
3
|
import omit from 'lodash/omit.js';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { generateApi } from 'swagger-typescript-api';
|
|
@@ -43,6 +43,8 @@ const parseSwagger = (config) => generateApi({
|
|
|
43
43
|
input: config.swaggerSchemaUrl,
|
|
44
44
|
httpClientType: config.httpClientType, // "axios" or "fetch"
|
|
45
45
|
typeSuffix: 'Type',
|
|
46
|
+
sortTypes: true,
|
|
47
|
+
sortRoutes: true,
|
|
46
48
|
prettier: {
|
|
47
49
|
printWidth: 120,
|
|
48
50
|
},
|
|
@@ -74,10 +76,288 @@ const parseSwagger = (config) => generateApi({
|
|
|
74
76
|
},
|
|
75
77
|
});
|
|
76
78
|
|
|
79
|
+
/**
|
|
80
|
+
* data-contracts.ts의 렌더링된 내용을 타입 블록 단위로 파싱합니다.
|
|
81
|
+
* 각 export type/interface/enum/const를 독립된 블록으로 분리합니다.
|
|
82
|
+
*
|
|
83
|
+
* lookahead로 `\nexport\s`를 사용하여 export function/class 등
|
|
84
|
+
* 비표준 export도 블록 경계로 인식합니다.
|
|
85
|
+
*/
|
|
86
|
+
const TYPE_BLOCK_REGEX = /(export\s+(?:type|interface|enum|const)\s+(\w+)[\s\S]*?)(?=\nexport\s|$)/g;
|
|
87
|
+
function parseTypeDefinitions(content) {
|
|
88
|
+
const types = {};
|
|
89
|
+
const typeRegex = new RegExp(TYPE_BLOCK_REGEX.source, TYPE_BLOCK_REGEX.flags);
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
92
|
+
const typeName = match[2];
|
|
93
|
+
const typeContent = match[1].trim();
|
|
94
|
+
types[typeName] = typeContent;
|
|
95
|
+
}
|
|
96
|
+
return types;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 코드에서 주석과 문자열 리터럴을 제거합니다.
|
|
101
|
+
* false positive 의존성 감지를 방지합니다. (예: 주석 내 타입명 언급)
|
|
102
|
+
*/
|
|
103
|
+
function stripNonCode(code) {
|
|
104
|
+
return code
|
|
105
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
106
|
+
.replace(/\/\/.*$/gm, '')
|
|
107
|
+
.replace(/'[^']*'/g, '""')
|
|
108
|
+
.replace(/"[^"]*"/g, '""')
|
|
109
|
+
.replace(/`[^`]*`/g, '""');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 렌더링된 타입 블록들 간의 의존성 그래프를 구축합니다.
|
|
113
|
+
*
|
|
114
|
+
* 각 타입 블록의 TypeScript 코드에서 다른 타입 이름이 참조되는지를
|
|
115
|
+
* 단어 경계 기반 정규식으로 감지합니다.
|
|
116
|
+
* 주석과 문자열 리터럴은 제거 후 매칭하여 false positive를 줄입니다.
|
|
117
|
+
*
|
|
118
|
+
* @param parsedTypes - parseTypeDefinitions의 결과 (typeName -> renderedBlock)
|
|
119
|
+
* @returns Map<typeName, Set<referencedTypeName>> - 직접 의존성 그래프
|
|
120
|
+
*/
|
|
121
|
+
function buildDependencyGraph(parsedTypes) {
|
|
122
|
+
const knownTypes = Object.keys(parsedTypes);
|
|
123
|
+
const graph = new Map();
|
|
124
|
+
// Pre-compile regexes once (O(n) instead of O(n²) compilations)
|
|
125
|
+
const compiledPatterns = new Map();
|
|
126
|
+
for (const known of knownTypes) {
|
|
127
|
+
const escaped = known.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
128
|
+
compiledPatterns.set(known, new RegExp(`\\b${escaped}\\b`));
|
|
129
|
+
}
|
|
130
|
+
for (const [typeName, typeBlock] of Object.entries(parsedTypes)) {
|
|
131
|
+
const deps = new Set();
|
|
132
|
+
const cleanBlock = stripNonCode(typeBlock);
|
|
133
|
+
for (const known of knownTypes) {
|
|
134
|
+
if (known !== typeName && compiledPatterns.get(known).test(cleanBlock)) {
|
|
135
|
+
deps.add(known);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
graph.set(typeName, deps);
|
|
139
|
+
}
|
|
140
|
+
return graph;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 직접 의존성 그래프에서 전이적 의존성을 포함한 전체 의존성을 계산합니다.
|
|
144
|
+
* A -> B -> C면, A의 의존성은 {B, C}가 됩니다.
|
|
145
|
+
*/
|
|
146
|
+
function getTransitiveDependencies(graph, typeName, visited = new Set()) {
|
|
147
|
+
if (visited.has(typeName))
|
|
148
|
+
return new Set();
|
|
149
|
+
visited.add(typeName);
|
|
150
|
+
const directDeps = graph.get(typeName) || new Set();
|
|
151
|
+
const allDeps = new Set(directDeps);
|
|
152
|
+
for (const dep of directDeps) {
|
|
153
|
+
const transitiveDeps = getTransitiveDependencies(graph, dep, visited);
|
|
154
|
+
for (const td of transitiveDeps) {
|
|
155
|
+
allDeps.add(td);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return allDeps;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* route의 type 문자열에서 알려진 타입 이름들을 추출합니다.
|
|
163
|
+
* 제네릭, 유니온, 인터섹션, 배열 등에서 타입 이름을 추출합니다.
|
|
164
|
+
*
|
|
165
|
+
* 예: "PaginatedResponse<UserType>" -> ["PaginatedResponse", "UserType"]
|
|
166
|
+
*/
|
|
167
|
+
function extractTypeNames(typeString, knownTypes) {
|
|
168
|
+
if (!typeString)
|
|
169
|
+
return [];
|
|
170
|
+
const identifiers = typeString.match(/\b[A-Z]\w+/g) || [];
|
|
171
|
+
return identifiers.filter((id) => knownTypes.has(id));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* swagger-typescript-api의 routes.combined에서 타입-모듈 매핑을 구축합니다.
|
|
175
|
+
*
|
|
176
|
+
* 각 route의 response.type, response.errorType, request.payload.type,
|
|
177
|
+
* request.query.type에서 타입 이름을 추출하여, 어느 모듈에서 사용하는지 매핑합니다.
|
|
178
|
+
*
|
|
179
|
+
* @returns Map<typeName, Set<moduleName>> (moduleName은 PascalCase로 정규화)
|
|
180
|
+
*/
|
|
181
|
+
function buildTypeToModuleMap(combined, knownTypeNames) {
|
|
182
|
+
const typeToModules = new Map();
|
|
183
|
+
if (!combined)
|
|
184
|
+
return typeToModules;
|
|
185
|
+
for (const moduleGroup of combined) {
|
|
186
|
+
// moduleName을 PascalCase로 정규화 (write-swagger.ts의 filename과 일치시키기 위해)
|
|
187
|
+
const normalizedModuleName = upperFirst(camelCase(moduleGroup.moduleName));
|
|
188
|
+
for (const route of moduleGroup.routes) {
|
|
189
|
+
// ParsedRoute의 .d.ts는 request: Request, response: Response (DOM 타입)으로 선언되어 있으나
|
|
190
|
+
// 런타임에는 swagger-typescript-api 내부 객체임. as any 캐스팅 필요.
|
|
191
|
+
const r = route;
|
|
192
|
+
const typeStrings = [
|
|
193
|
+
r.response?.type,
|
|
194
|
+
r.response?.errorType,
|
|
195
|
+
r.request?.payload?.type,
|
|
196
|
+
r.request?.query?.type,
|
|
197
|
+
];
|
|
198
|
+
// request.parameters에서도 타입 추출 (path params 등)
|
|
199
|
+
if (r.request?.parameters) {
|
|
200
|
+
for (const param of Object.values(r.request.parameters)) {
|
|
201
|
+
if (param?.type) {
|
|
202
|
+
typeStrings.push(param.type);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const typeStr of typeStrings) {
|
|
207
|
+
const extracted = extractTypeNames(typeStr, knownTypeNames);
|
|
208
|
+
for (const typeName of extracted) {
|
|
209
|
+
if (!typeToModules.has(typeName)) {
|
|
210
|
+
typeToModules.set(typeName, new Set());
|
|
211
|
+
}
|
|
212
|
+
typeToModules.get(typeName).add(normalizedModuleName);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return typeToModules;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* data-contracts.ts의 타입들을 모듈별/공유로 분류합니다.
|
|
222
|
+
*
|
|
223
|
+
* 분류 알고리즘:
|
|
224
|
+
* 1. 직접 매핑: route에서 참조하는 타입 → 해당 모듈에 소속
|
|
225
|
+
* 2. 의존성 전파: 타입이 참조하는 다른 타입도 같은 모듈에 소속
|
|
226
|
+
* 3. 공유 판별: 2개 이상 모듈에 소속된 타입 → shared
|
|
227
|
+
* 4. Orphan 처리: 어디에도 소속되지 않은 타입 → shared
|
|
228
|
+
* 5. 전이적 공유: shared 타입이 참조하는 타입도 shared로 격상
|
|
229
|
+
* 6. enumMap 동반 이동: *Map const는 base type과 동일 파일에 배치
|
|
230
|
+
*/
|
|
231
|
+
function classifyTypes(dataContractsContent, routesCombined) {
|
|
232
|
+
const parsedTypes = parseTypeDefinitions(dataContractsContent);
|
|
233
|
+
const allTypeNames = new Set(Object.keys(parsedTypes));
|
|
234
|
+
// Step 1: 타입-모듈 직접 매핑
|
|
235
|
+
const typeToModules = buildTypeToModuleMap(routesCombined, allTypeNames);
|
|
236
|
+
// Step 2: 의존성 그래프 구축 + 의존성 전파
|
|
237
|
+
const depGraph = buildDependencyGraph(parsedTypes);
|
|
238
|
+
// 의존성 전파: 각 타입의 전이적 의존성도 같은 모듈에 추가
|
|
239
|
+
for (const [typeName, modules] of typeToModules.entries()) {
|
|
240
|
+
const transitiveDeps = getTransitiveDependencies(depGraph, typeName);
|
|
241
|
+
for (const dep of transitiveDeps) {
|
|
242
|
+
if (!typeToModules.has(dep)) {
|
|
243
|
+
typeToModules.set(dep, new Set());
|
|
244
|
+
}
|
|
245
|
+
for (const mod of modules) {
|
|
246
|
+
typeToModules.get(dep).add(mod);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Step 3: 분류 (module-exclusive vs shared vs orphan)
|
|
251
|
+
const moduleTypesMap = new Map();
|
|
252
|
+
const sharedTypesSet = new Set();
|
|
253
|
+
for (const typeName of allTypeNames) {
|
|
254
|
+
const modules = typeToModules.get(typeName);
|
|
255
|
+
if (!modules || modules.size === 0) {
|
|
256
|
+
// Orphan: 어디서도 참조되지 않음 → shared (안전 기본값)
|
|
257
|
+
sharedTypesSet.add(typeName);
|
|
258
|
+
}
|
|
259
|
+
else if (modules.size === 1) {
|
|
260
|
+
// Module-exclusive: 한 모듈에서만 사용
|
|
261
|
+
const moduleName = [...modules][0];
|
|
262
|
+
if (!moduleTypesMap.has(moduleName)) {
|
|
263
|
+
moduleTypesMap.set(moduleName, new Set());
|
|
264
|
+
}
|
|
265
|
+
moduleTypesMap.get(moduleName).add(typeName);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Shared: 2개 이상 모듈에서 사용
|
|
269
|
+
sharedTypesSet.add(typeName);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Step 5: 전이적 공유 — shared 타입이 참조하는 타입도 shared로 격상
|
|
273
|
+
// 순회 중 Set에 추가하지 않고 별도 배열에 수집 후 일괄 적용
|
|
274
|
+
let changed = true;
|
|
275
|
+
while (changed) {
|
|
276
|
+
changed = false;
|
|
277
|
+
const toPromote = [];
|
|
278
|
+
for (const sharedType of sharedTypesSet) {
|
|
279
|
+
const deps = depGraph.get(sharedType) || new Set();
|
|
280
|
+
for (const dep of deps) {
|
|
281
|
+
if (!sharedTypesSet.has(dep)) {
|
|
282
|
+
toPromote.push(dep);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const dep of toPromote) {
|
|
287
|
+
sharedTypesSet.add(dep);
|
|
288
|
+
for (const [, moduleSet] of moduleTypesMap) {
|
|
289
|
+
moduleSet.delete(dep);
|
|
290
|
+
}
|
|
291
|
+
changed = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Step 6: enumMap 동반 이동 — *Map const는 base type과 동일 위치에 배치
|
|
295
|
+
for (const typeName of allTypeNames) {
|
|
296
|
+
if (typeName.endsWith('Map')) {
|
|
297
|
+
const baseTypeName = typeName.slice(0, -3); // FooTypeMap -> FooType
|
|
298
|
+
if (!allTypeNames.has(baseTypeName))
|
|
299
|
+
continue;
|
|
300
|
+
// base type이 있는 위치를 찾아서 Map도 같은 곳으로 이동
|
|
301
|
+
if (sharedTypesSet.has(baseTypeName)) {
|
|
302
|
+
// base가 shared → Map도 shared
|
|
303
|
+
sharedTypesSet.add(typeName);
|
|
304
|
+
for (const [, moduleSet] of moduleTypesMap) {
|
|
305
|
+
moduleSet.delete(typeName);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// base가 특정 모듈에 있음 → Map도 같은 모듈로
|
|
310
|
+
for (const [moduleName, moduleSet] of moduleTypesMap) {
|
|
311
|
+
if (moduleSet.has(baseTypeName)) {
|
|
312
|
+
moduleSet.add(typeName);
|
|
313
|
+
// 다른 모듈이나 shared에서 제거
|
|
314
|
+
sharedTypesSet.delete(typeName);
|
|
315
|
+
for (const [otherMod, otherSet] of moduleTypesMap) {
|
|
316
|
+
if (otherMod !== moduleName) {
|
|
317
|
+
otherSet.delete(typeName);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// 빈 모듈 제거
|
|
327
|
+
for (const [moduleName, typeSet] of moduleTypesMap) {
|
|
328
|
+
if (typeSet.size === 0) {
|
|
329
|
+
moduleTypesMap.delete(moduleName);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Set → Array 변환
|
|
333
|
+
const moduleTypes = new Map();
|
|
334
|
+
for (const [moduleName, typeSet] of moduleTypesMap) {
|
|
335
|
+
moduleTypes.set(moduleName, [...typeSet]);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
moduleTypes,
|
|
339
|
+
sharedTypes: [...sharedTypesSet],
|
|
340
|
+
parsedTypes,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
77
344
|
const { TYPE_FILE, UTIL_FILE, QUERY_HOOK_INDICATOR, USE_SUSPENSE_QUERY_HOOK_INDICATOR, } = GENERATE_SWAGGER_DATA;
|
|
78
|
-
const writeSwaggerApiFile = (params) => {
|
|
345
|
+
const writeSwaggerApiFile = async (params) => {
|
|
79
346
|
const { input, output, spinner, config } = params;
|
|
80
|
-
|
|
347
|
+
// === Pre-analysis (splitDataContracts 모드) ===
|
|
348
|
+
// for...of 전에 classification을 완료하여 import 후처리에 사용
|
|
349
|
+
let classificationResult = null;
|
|
350
|
+
if (config.splitDataContracts) {
|
|
351
|
+
const dataContractsFile = input.files.find((f) => f.fileName + f.fileExtension === 'data-contracts.ts');
|
|
352
|
+
if (dataContractsFile?.fileContent) {
|
|
353
|
+
// GenerateApiOutput은 configuration을 런타임에 포함하지만 타입 정의에서 미노출
|
|
354
|
+
const configuration = input.configuration;
|
|
355
|
+
const routesCombined = configuration?.routes?.combined;
|
|
356
|
+
classificationResult = classifyTypes(dataContractsFile.fileContent, routesCombined);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// === Pass 1: for...of 루프 (기존 로직 + splitDataContracts 분기) ===
|
|
360
|
+
for (const { fileName, fileContent: content, fileExtension } of input.files) {
|
|
81
361
|
const name = fileName + fileExtension;
|
|
82
362
|
try {
|
|
83
363
|
const isTypeFile = TYPE_FILE.includes(name);
|
|
@@ -85,6 +365,13 @@ const writeSwaggerApiFile = (params) => {
|
|
|
85
365
|
const isHttpClient = name === 'http-client.ts';
|
|
86
366
|
const isApiFile = content?.includes(QUERY_HOOK_INDICATOR);
|
|
87
367
|
const filename = name.replace('.ts', '');
|
|
368
|
+
// splitDataContracts: data-contracts.ts 쓰기 억제
|
|
369
|
+
if (isTypeFile &&
|
|
370
|
+
name === 'data-contracts.ts' &&
|
|
371
|
+
config.splitDataContracts) {
|
|
372
|
+
// 디스크에 쓰지 않음 — pre-analysis에서 이미 content를 처리함
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
88
375
|
const getTargetFolder = () => {
|
|
89
376
|
if (isUtilFile)
|
|
90
377
|
return path.resolve(output, '@utils');
|
|
@@ -100,72 +387,156 @@ const writeSwaggerApiFile = (params) => {
|
|
|
100
387
|
spinner.info(`generated: ${targetFolder}`);
|
|
101
388
|
if (isHttpClient) {
|
|
102
389
|
generate(path.resolve(targetFolder, 'index.ts'), content);
|
|
103
|
-
|
|
390
|
+
continue;
|
|
104
391
|
}
|
|
105
392
|
if (isApiFile) {
|
|
106
|
-
|
|
107
|
-
|
|
393
|
+
// splitDataContracts: import 후처리 (splitHookContents 호출 전)
|
|
394
|
+
let processedContent = content;
|
|
395
|
+
if (config.splitDataContracts && classificationResult) {
|
|
396
|
+
processedContent = rewriteDataContractsImport(content, filename, classificationResult);
|
|
397
|
+
}
|
|
398
|
+
const { apiContents, hookParts } = splitHookContents(filename, processedContent);
|
|
399
|
+
generate(path.resolve(targetFolder, `${filename}.api.ts`), apiContents);
|
|
108
400
|
if (config.includeReactQuery) {
|
|
109
|
-
|
|
401
|
+
generate(path.resolve(targetFolder, `${filename}.query.ts`), hookParts[0]);
|
|
110
402
|
}
|
|
111
403
|
if (config.includeReactSuspenseQuery) {
|
|
112
|
-
|
|
404
|
+
generate(path.resolve(targetFolder, `${filename}.suspenseQuery.ts`), hookParts[1]);
|
|
113
405
|
}
|
|
114
|
-
|
|
406
|
+
continue;
|
|
115
407
|
}
|
|
116
408
|
generate(path.resolve(targetFolder, name), content);
|
|
117
409
|
}
|
|
118
410
|
catch (err) {
|
|
119
411
|
console.error(err);
|
|
120
412
|
}
|
|
121
|
-
}
|
|
413
|
+
}
|
|
414
|
+
// === Post-step: 분할 contracts 파일 생성 (동기 작업이므로 async forEach 영향 없음) ===
|
|
415
|
+
if (config.splitDataContracts && classificationResult) {
|
|
416
|
+
const { moduleTypes, sharedTypes, parsedTypes } = classificationResult;
|
|
417
|
+
// 모듈별 contracts 파일 생성
|
|
418
|
+
for (const [moduleName, typeNames] of moduleTypes.entries()) {
|
|
419
|
+
const moduleFolder = path.resolve(output, moduleName);
|
|
420
|
+
fs.mkdirSync(moduleFolder, { recursive: true });
|
|
421
|
+
const moduleContent = buildContractsFileContent(typeNames, parsedTypes, sharedTypes);
|
|
422
|
+
generate(path.resolve(moduleFolder, `${moduleName}.contracts.ts`), moduleContent);
|
|
423
|
+
}
|
|
424
|
+
// common-contracts 파일 생성 (공유 타입이 있을 때만)
|
|
425
|
+
if (sharedTypes.length > 0) {
|
|
426
|
+
const commonFolder = path.resolve(output, '@types');
|
|
427
|
+
fs.mkdirSync(commonFolder, { recursive: true });
|
|
428
|
+
const commonContent = buildContractsFileContent(sharedTypes, parsedTypes);
|
|
429
|
+
generate(path.resolve(commonFolder, 'common-contracts.ts'), commonContent);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
122
432
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
433
|
+
/**
|
|
434
|
+
* 타입 이름 목록과 파싱된 타입 블록으로 contracts 파일 내용을 생성합니다.
|
|
435
|
+
* @internal — exported for testing
|
|
436
|
+
*/
|
|
437
|
+
function buildContractsFileContent(typeNames, parsedTypes, sharedTypeNames) {
|
|
438
|
+
const header = `/* eslint-disable */
|
|
439
|
+
/* tslint:disable */
|
|
440
|
+
/**
|
|
441
|
+
* !DO NOT EDIT THIS FILE!
|
|
442
|
+
*
|
|
443
|
+
* This file was auto-generated by tok-cli.config.ts 에서 설정된 gen:api 명령어로 생성되었습니다.
|
|
444
|
+
*/\n`;
|
|
445
|
+
const blocks = typeNames.map((name) => parsedTypes[name]).filter(Boolean);
|
|
446
|
+
const bodyContent = blocks.join('\n\n');
|
|
447
|
+
// module contracts가 shared type을 참조하면 import 추가
|
|
448
|
+
let importSection = '';
|
|
449
|
+
if (sharedTypeNames && sharedTypeNames.length > 0) {
|
|
450
|
+
const referencedShared = sharedTypeNames.filter((sharedType) => {
|
|
451
|
+
const escaped = sharedType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
452
|
+
return new RegExp(`\\b${escaped}\\b`).test(bodyContent);
|
|
130
453
|
});
|
|
454
|
+
if (referencedShared.length > 0) {
|
|
455
|
+
importSection =
|
|
456
|
+
`import { ${referencedShared.join(', ')} } from '../@types/common-contracts';\n\n`;
|
|
457
|
+
}
|
|
131
458
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
459
|
+
return header + '\n' + importSection + bodyContent + '\n';
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* api 파일의 data-contracts import를 모듈별 contracts + common-contracts로 교체합니다.
|
|
463
|
+
*
|
|
464
|
+
* import의 `[^}]+`는 negated character class이므로 멀티라인도 매칭됩니다.
|
|
465
|
+
* (prettier가 줄바꿈해도 안전)
|
|
466
|
+
* @internal — exported for testing
|
|
467
|
+
*/
|
|
468
|
+
function rewriteDataContractsImport(content, filename, classification) {
|
|
469
|
+
const moduleTypeNames = classification.moduleTypes.get(filename) || [];
|
|
470
|
+
const sharedTypeNames = classification.sharedTypes;
|
|
471
|
+
// 현재 api 파일에서 실제로 사용하는 타입만 필터링
|
|
472
|
+
const allImportedTypes = extractImportedTypeNames(content);
|
|
473
|
+
const usedModuleTypes = moduleTypeNames.filter((t) => allImportedTypes.has(t));
|
|
474
|
+
const usedSharedTypes = sharedTypeNames.filter((t) => allImportedTypes.has(t));
|
|
475
|
+
// 기존 data-contracts import 라인을 찾아서 교체
|
|
476
|
+
const importRegex = /import\s*\{[^}]+\}\s*from\s*['"]\.\.\/[@]types\/data-contracts['"];?/;
|
|
477
|
+
const newImports = [];
|
|
478
|
+
if (usedModuleTypes.length > 0) {
|
|
479
|
+
newImports.push(`import { ${usedModuleTypes.join(', ')} } from './${filename}.contracts';`);
|
|
480
|
+
}
|
|
481
|
+
if (usedSharedTypes.length > 0) {
|
|
482
|
+
newImports.push(`import { ${usedSharedTypes.join(', ')} } from '../@types/common-contracts';`);
|
|
135
483
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
484
|
+
// import가 하나도 없으면 빈 문자열로 교체 (사용하지 않는 타입만 있던 경우)
|
|
485
|
+
if (newImports.length === 0) {
|
|
486
|
+
return content.replace(importRegex, '');
|
|
487
|
+
}
|
|
488
|
+
return content.replace(importRegex, newImports.join('\n'));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* api 파일 content에서 data-contracts import 구문의 타입 이름들을 추출합니다.
|
|
492
|
+
* @internal — exported for testing
|
|
493
|
+
*/
|
|
494
|
+
function extractImportedTypeNames(content) {
|
|
495
|
+
const importMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]\.\.\/[@]types\/data-contracts['"];?/);
|
|
496
|
+
if (!importMatch)
|
|
497
|
+
return new Set();
|
|
498
|
+
return new Set(importMatch[1]
|
|
499
|
+
.split(',')
|
|
500
|
+
.map((s) => s.trim())
|
|
501
|
+
.filter(Boolean));
|
|
141
502
|
}
|
|
142
503
|
function generate(path, contents) {
|
|
143
|
-
// 기존 파일이 있으면 읽어서 병합
|
|
144
504
|
let existingContent = '';
|
|
145
505
|
try {
|
|
146
506
|
if (fs.existsSync(path)) {
|
|
147
507
|
existingContent = fs.readFileSync(path, 'utf8');
|
|
148
|
-
console.log('🔧 [SMART-MERGE] Found existing file, merging:', path);
|
|
149
508
|
}
|
|
150
509
|
}
|
|
151
510
|
catch (err) {
|
|
152
|
-
|
|
511
|
+
// no existing file
|
|
153
512
|
}
|
|
154
|
-
// 기존 내용이 있으면 병합
|
|
155
513
|
if (existingContent) {
|
|
156
|
-
// 스마트 병합: 중복 타입 제거
|
|
157
514
|
const mergedContent = mergeTypeScriptContent(existingContent, contents);
|
|
158
515
|
fs.writeFileSync(path, mergedContent);
|
|
159
|
-
console.log('🔧 [SMART-MERGE] Smart merged content for:', path);
|
|
160
516
|
}
|
|
161
517
|
else {
|
|
162
518
|
fs.writeFileSync(path, contents);
|
|
163
|
-
console.log('🔧 [SMART-MERGE] Created new file:', path);
|
|
164
519
|
}
|
|
165
520
|
}
|
|
166
521
|
function splitHookContents(filename, content) {
|
|
167
|
-
const
|
|
168
|
-
|
|
522
|
+
const indicatorIdx = content.indexOf(QUERY_HOOK_INDICATOR);
|
|
523
|
+
if (indicatorIdx === -1) {
|
|
524
|
+
throw new Error(`[splitHookContents] QUERY_HOOK_INDICATOR not found in ${filename}. ` +
|
|
525
|
+
`Ensure the template includes the indicator comment.`);
|
|
526
|
+
}
|
|
527
|
+
const _apiContent = content.slice(0, indicatorIdx);
|
|
528
|
+
const _hookContent = content.slice(indicatorIdx + QUERY_HOOK_INDICATOR.length);
|
|
529
|
+
const suspenseIdx = _hookContent.indexOf(USE_SUSPENSE_QUERY_HOOK_INDICATOR);
|
|
530
|
+
let _hookParts;
|
|
531
|
+
if (suspenseIdx === -1) {
|
|
532
|
+
_hookParts = [_hookContent, ''];
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
_hookParts = [
|
|
536
|
+
_hookContent.slice(0, suspenseIdx),
|
|
537
|
+
_hookContent.slice(suspenseIdx + USE_SUSPENSE_QUERY_HOOK_INDICATOR.length),
|
|
538
|
+
];
|
|
539
|
+
}
|
|
169
540
|
const lastImport = getLastImportLine(content);
|
|
170
541
|
const lines = content.split('\n');
|
|
171
542
|
const importArea = [
|
|
@@ -177,12 +548,16 @@ function splitHookContents(filename, content) {
|
|
|
177
548
|
hookParts: _hookParts.map((d) => importArea + d),
|
|
178
549
|
};
|
|
179
550
|
}
|
|
551
|
+
/** @internal — exported for testing */
|
|
180
552
|
function getLastImportLine(content) {
|
|
181
|
-
|
|
553
|
+
const importLines = content
|
|
182
554
|
.split('\n')
|
|
183
555
|
.map((line, idx) => ({ idx, has: /from ('|").*('|");/.test(line) }))
|
|
184
556
|
.filter(({ has }) => has)
|
|
185
|
-
.map(({ idx }) => idx)
|
|
557
|
+
.map(({ idx }) => idx);
|
|
558
|
+
if (importLines.length === 0)
|
|
559
|
+
return 0;
|
|
560
|
+
return Math.max(...importLines) + 1;
|
|
186
561
|
}
|
|
187
562
|
|
|
188
563
|
/**
|
|
@@ -206,6 +581,7 @@ const genApi = defineCommand({
|
|
|
206
581
|
},
|
|
207
582
|
],
|
|
208
583
|
ignoreTlsError: false,
|
|
584
|
+
splitDataContracts: false,
|
|
209
585
|
},
|
|
210
586
|
run: async (config) => {
|
|
211
587
|
if (config.ignoreTlsError) {
|
|
@@ -225,7 +601,6 @@ const genApi = defineCommand({
|
|
|
225
601
|
}
|
|
226
602
|
return [];
|
|
227
603
|
})();
|
|
228
|
-
console.log('🔧 [MULTI-URL] Processing URLs:', urls);
|
|
229
604
|
const coverPath = (config, url) => {
|
|
230
605
|
const { httpClientType, output } = config;
|
|
231
606
|
const { AXIOS_DEFAULT_INSTANCE_PATH, FETCH_DEFAULT_INSTANCE_PATH } = GENERATE_SWAGGER_DATA;
|
|
@@ -243,7 +618,6 @@ const genApi = defineCommand({
|
|
|
243
618
|
// 각 URL별로 순차 처리
|
|
244
619
|
for (let i = 0; i < urls.length; i++) {
|
|
245
620
|
const url = urls[i];
|
|
246
|
-
console.log(`🔧 [MULTI-URL] Processing URL ${i + 1}/${urls.length}: ${url}`);
|
|
247
621
|
const covered = coverPath(config, url);
|
|
248
622
|
const parsed = await withLoading(`Parse Swagger ${i + 1}/${urls.length}`, 'swaggerSchemaUrl' in covered ? covered.swaggerSchemaUrl : '', () => {
|
|
249
623
|
return parseSwagger(omit(covered, 'swaggerSchemaUrls'));
|
|
@@ -252,9 +626,9 @@ const genApi = defineCommand({
|
|
|
252
626
|
console.error(`Failed to generate api for URL ${i + 1}: swagger parse error.`);
|
|
253
627
|
continue;
|
|
254
628
|
}
|
|
255
|
-
withLoading('Write Swagger API', //
|
|
629
|
+
await withLoading('Write Swagger API', //
|
|
256
630
|
covered.output, (spinner) => {
|
|
257
|
-
writeSwaggerApiFile({
|
|
631
|
+
return writeSwaggerApiFile({
|
|
258
632
|
input: parsed,
|
|
259
633
|
output: covered.output,
|
|
260
634
|
spinner,
|
|
@@ -265,7 +639,9 @@ const genApi = defineCommand({
|
|
|
265
639
|
await withLoading('Prettier format', covered.output, async () => {
|
|
266
640
|
const fs = await import('fs');
|
|
267
641
|
const pathMod = await import('path');
|
|
268
|
-
const { prettierString } = await import('@toktokhan-dev/node');
|
|
642
|
+
const { prettierString, findFileToTop } = await import('@toktokhan-dev/node');
|
|
643
|
+
// output 디렉토리 기준으로 prettier config를 탐색 (cwd 의존 제거)
|
|
644
|
+
const configPath = findFileToTop(covered.output, '.prettierrc.js') || 'auto';
|
|
269
645
|
const listTsFiles = (dir) => {
|
|
270
646
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
271
647
|
const files = [];
|
|
@@ -284,9 +660,19 @@ const genApi = defineCommand({
|
|
|
284
660
|
for (const file of files) {
|
|
285
661
|
try {
|
|
286
662
|
const raw = fs.readFileSync(file, 'utf8');
|
|
287
|
-
|
|
663
|
+
let organized = raw;
|
|
664
|
+
try {
|
|
665
|
+
organized = await prettierString(raw, {
|
|
666
|
+
parser: 'babel-ts',
|
|
667
|
+
plugins: ['prettier-plugin-organize-imports'],
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// prettier-plugin-organize-imports not installed, skip
|
|
672
|
+
}
|
|
673
|
+
const formatted = await prettierString(organized, {
|
|
288
674
|
parser: 'typescript',
|
|
289
|
-
configPath
|
|
675
|
+
configPath,
|
|
290
676
|
});
|
|
291
677
|
fs.writeFileSync(file, formatted);
|
|
292
678
|
}
|
|
@@ -301,25 +687,12 @@ const genApi = defineCommand({
|
|
|
301
687
|
/**
|
|
302
688
|
* 스마트 타입 병합 함수들
|
|
303
689
|
*/
|
|
304
|
-
// 타입 정의 파싱 함수
|
|
305
|
-
function parseTypeDefinitions(content) {
|
|
306
|
-
const types = {};
|
|
307
|
-
// export type, interface, enum, const 패턴 매칭
|
|
308
|
-
const typeRegex = /(export\s+(?:type|interface|enum|const)\s+(\w+)[\s\S]*?)(?=export\s+(?:type|interface|enum|const)\s+\w+|$)/g;
|
|
309
|
-
let match;
|
|
310
|
-
while ((match = typeRegex.exec(content)) !== null) {
|
|
311
|
-
const typeName = match[2];
|
|
312
|
-
const typeContent = match[1].trim();
|
|
313
|
-
types[typeName] = typeContent;
|
|
314
|
-
}
|
|
315
|
-
return types;
|
|
316
|
-
}
|
|
317
|
-
// (deprecated) 타입 문자열 변환 로직은 병합 로직 내에서 직접 조립합니다.
|
|
318
690
|
// 스마트 타입 병합 함수
|
|
319
691
|
function mergeTypeScriptContent(existing, newContent) {
|
|
320
692
|
// 1) import 구문 보존 및 병합 (양쪽 모두에서 수집)
|
|
321
|
-
|
|
322
|
-
const
|
|
693
|
+
// 멀티라인 import도 매칭: import { \n A, \n B \n } from '...';
|
|
694
|
+
const importRegex = /^\s*import\s+[\s\S]*?from\s*['"][^'"]*['"];?/gm;
|
|
695
|
+
const sideEffectImportRegex = /^\s*import\s*['"][^'"]+['"];\s*$/gm;
|
|
323
696
|
const collectImports = (content) => {
|
|
324
697
|
const imports = new Set();
|
|
325
698
|
const matchedA = content.match(importRegex) ?? [];
|
|
@@ -335,28 +708,17 @@ function mergeTypeScriptContent(existing, newContent) {
|
|
|
335
708
|
// import 병합: 새 파일 기준 우선 순서 + 기존에만 있는 import 추가
|
|
336
709
|
const mergedImportSet = new Set(newImports);
|
|
337
710
|
existingImports.forEach((imp) => mergedImportSet.add(imp));
|
|
338
|
-
const mergedImports = Array.from(mergedImportSet);
|
|
711
|
+
const mergedImports = Array.from(mergedImportSet).sort();
|
|
339
712
|
// 2) 타입 선언 병합 (중복 제거)
|
|
340
|
-
const typeBlockRegex = /(export\s+(?:type|interface|enum|const)\s+\w+[\s\S]*?)(?=export\s+(?:type|interface|enum|const)\s+\w+|$)/g;
|
|
341
713
|
const existingTypes = parseTypeDefinitions(existingBody);
|
|
342
714
|
const newTypes = parseTypeDefinitions(newBody);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const mergedTypes = { ...existingTypes };
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
console.log('🔧 [SMART-MERGE] Skipping duplicate type:', typeName);
|
|
351
|
-
skippedCount++;
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
mergedTypes[typeName] = typeContent;
|
|
355
|
-
addedCount++;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
console.log('🔧 [SMART-MERGE] Added types:', addedCount, 'Skipped duplicates:', skippedCount);
|
|
359
|
-
const mergedTypesString = Object.values(mergedTypes).join('\n\n');
|
|
715
|
+
// 새 타입 기준으로 병합: 새 버전이 source of truth (swagger 스키마)
|
|
716
|
+
// 기존에만 있는 타입은 보존 (사용자가 수동 추가한 것)
|
|
717
|
+
const mergedTypes = { ...existingTypes, ...newTypes };
|
|
718
|
+
const mergedTypesString = Object.entries(mergedTypes)
|
|
719
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
720
|
+
.map(([, v]) => v)
|
|
721
|
+
.join('\n\n');
|
|
360
722
|
// 3) 기타 코드(타입/임포트 외)는 "새로운 내용"을 기준으로 유지
|
|
361
723
|
const removeHeaderComment = (content) => {
|
|
362
724
|
// 파일 상단의 모든 블록 코멘트와 라인 코멘트를 제거 (재귀적 처리)
|
|
@@ -372,7 +734,7 @@ function mergeTypeScriptContent(existing, newContent) {
|
|
|
372
734
|
return removeLineComment(removeBlockComment(content));
|
|
373
735
|
};
|
|
374
736
|
// 새 본문에서 타입 블록 제거 후 남은 코드
|
|
375
|
-
const newBodyWithoutTypes = (newBody || '').replace(
|
|
737
|
+
const newBodyWithoutTypes = (newBody || '').replace(new RegExp(TYPE_BLOCK_REGEX.source, TYPE_BLOCK_REGEX.flags), '');
|
|
376
738
|
let otherCodeFromNew = removeHeaderComment(newBodyWithoutTypes).trim();
|
|
377
739
|
// 본문 내에 남아있는 "!DO NOT EDIT THIS FILE" 주석 블록들을 모두 제거
|
|
378
740
|
otherCodeFromNew = otherCodeFromNew.replace(/\/\*\*?\s*\*\s*!DO NOT EDIT THIS FILE[\s\S]*?\*\//g, '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toktokhan-dev/cli-plugin-gen-api-react-query",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI plugin for generating API hooks with React Query built by TOKTOKHAN.DEV",
|
|
5
5
|
"author": "TOKTOKHAN.DEV <fe-system@toktokhan.dev>",
|
|
6
6
|
"license": "ISC",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"lodash": "^4.17.21",
|
|
39
39
|
"prettier-plugin-organize-imports": "^3.2.4",
|
|
40
40
|
"swagger-typescript-api": "13.0.22",
|
|
41
|
-
"@toktokhan-dev/
|
|
42
|
-
"@toktokhan-dev/
|
|
41
|
+
"@toktokhan-dev/cli": "0.0.11",
|
|
42
|
+
"@toktokhan-dev/node": "0.0.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/lodash": "^4.17.0",
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"build:watch": "rollup -c --watch",
|
|
51
51
|
"api-extractor": "api-extractor run --local --verbose",
|
|
52
52
|
"play": "pnpm build:watch & node --watch play/playground.js",
|
|
53
|
-
"test": "
|
|
53
|
+
"test": "jest --passWithNoTests"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
return `${keyName} : "${enumNames?.[idx]}"`;
|
|
37
37
|
}).join(", ");
|
|
38
38
|
|
|
39
|
-
return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\
|
|
39
|
+
return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\n${map} \n } as const`;
|
|
40
40
|
|
|
41
41
|
},
|
|
42
42
|
interface: (contract) => {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
content = content.replace(field, `${field} | null`)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
return `interface ${contract.name} {\
|
|
52
|
+
return `interface ${contract.name} {\n${content}}`;
|
|
53
53
|
},
|
|
54
54
|
type: (contract) => {
|
|
55
55
|
return `type ${contract.name} = ${contract.content}`;
|
|
@@ -83,8 +83,7 @@
|
|
|
83
83
|
<% if (description.length) { %>
|
|
84
84
|
/**
|
|
85
85
|
<%~ description.map(part => `* ${part}`).join("\n") %>
|
|
86
|
-
|
|
87
|
-
*/
|
|
86
|
+
*/
|
|
88
87
|
<% } %>
|
|
89
88
|
export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
|
90
89
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
return `${keyName} : "${enumNames?.[idx]}"`;
|
|
37
37
|
}).join(", ");
|
|
38
38
|
|
|
39
|
-
return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\
|
|
39
|
+
return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\n${map} \n } as const`;
|
|
40
40
|
|
|
41
41
|
},
|
|
42
42
|
interface: (contract) => {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
content = content.replace(field, `${field} | null`)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
return `interface ${contract.name} {\
|
|
52
|
+
return `interface ${contract.name} {\n${content}}`;
|
|
53
53
|
},
|
|
54
54
|
type: (contract) => {
|
|
55
55
|
return `type ${contract.name} = ${contract.content}`;
|
|
@@ -85,8 +85,7 @@
|
|
|
85
85
|
<% if (description.length) { %>
|
|
86
86
|
/**
|
|
87
87
|
<%~ description.map(part => `* ${part}`).join("\n") %>
|
|
88
|
-
|
|
89
|
-
*/
|
|
88
|
+
*/
|
|
90
89
|
<% } %>
|
|
91
90
|
export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
|
92
91
|
|
|
@@ -60,7 +60,7 @@ export enum ContentType {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export class HttpClient<SecurityDataType = unknown> {
|
|
63
|
-
public baseUrl: string = "<%~ apiConfig.baseUrl %>";
|
|
63
|
+
public baseUrl: string = "<%~ apiConfig.baseUrl.replace(/\/$/, '') %>";
|
|
64
64
|
private securityData: SecurityDataType | null = null;
|
|
65
65
|
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
|
66
66
|
private abortControllers = new Map<CancelToken, AbortController>();
|
|
@@ -44,7 +44,7 @@ export type InfiniteQueryHookParams<
|
|
|
44
44
|
> = {
|
|
45
45
|
options?: Partial<
|
|
46
46
|
Omit<
|
|
47
|
-
UseInfiniteQueryOptions<OriginData, Error, TData,
|
|
47
|
+
UseInfiniteQueryOptions<OriginData, Error, TData, any, TPageParam>,
|
|
48
48
|
'queryKey' | 'queryFn'
|
|
49
49
|
>
|
|
50
50
|
>;
|
|
@@ -85,7 +85,7 @@ export type SuspenseInfiniteQueryHookParams<
|
|
|
85
85
|
> = {
|
|
86
86
|
options?: Partial<
|
|
87
87
|
Omit<
|
|
88
|
-
UseSuspenseInfiniteQueryOptions<OriginData, Error, TData,
|
|
88
|
+
UseSuspenseInfiniteQueryOptions<OriginData, Error, TData, any, TPageParam>,
|
|
89
89
|
'queryKey' | 'queryFn'
|
|
90
90
|
>
|
|
91
91
|
>;
|