jszy-swagger-doc-generator 1.4.1 → 1.5.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 +327 -113
- package/dist/cli.js +34 -1
- package/dist/cli.js.map +1 -1
- package/dist/generators/swagger.generator.d.ts +90 -0
- package/dist/generators/swagger.generator.js +626 -0
- package/dist/generators/swagger.generator.js.map +1 -0
- package/dist/helpers/handlebars.helpers.d.ts +4 -0
- package/dist/helpers/handlebars.helpers.js +92 -0
- package/dist/helpers/handlebars.helpers.js.map +1 -0
- package/dist/helpers/string.helpers.d.ts +8 -0
- package/dist/helpers/string.helpers.js +25 -0
- package/dist/helpers/string.helpers.js.map +1 -0
- package/dist/helpers/template.helpers.d.ts +4 -0
- package/dist/helpers/template.helpers.js +105 -0
- package/dist/helpers/template.helpers.js.map +1 -0
- package/dist/helpers/type.helpers.d.ts +18 -0
- package/dist/helpers/type.helpers.js +229 -0
- package/dist/helpers/type.helpers.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +403 -158
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +6 -2
- package/src/cli.ts +35 -1
- package/src/generators/swagger.generator.ts +664 -0
- package/src/helpers/template.helpers.ts +72 -0
- package/src/helpers/type.helpers.ts +232 -0
- package/src/index.ts +435 -161
- package/src/types.ts +98 -0
- package/templates/hooks/individual-hook.hbs +55 -0
- package/templates/hooks/react-hook.hbs +14 -0
- package/templates/types/type-definition.hbs +5 -0
- package/test-openapi-swagger.json +454 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import axios, { AxiosResponse } from 'axios';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { SwaggerDoc, Parameter, HandlebarsContext } from '../types';
|
|
5
|
+
import { compileTemplate } from '../helpers/template.helpers';
|
|
6
|
+
import { convertTypeToTs, generateSingleTypeDefinition } from '../helpers/type.helpers';
|
|
7
|
+
|
|
8
|
+
// Define helper functions locally since we removed the helpers file
|
|
9
|
+
function toPascalCase(str: string): string {
|
|
10
|
+
return str
|
|
11
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
12
|
+
return index === 0 ? word.toUpperCase() : word.toUpperCase();
|
|
13
|
+
})
|
|
14
|
+
.replace(/\s+/g, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toCamelCase(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
20
|
+
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
|
21
|
+
})
|
|
22
|
+
.replace(/\s+/g, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class SwaggerDocGenerator {
|
|
26
|
+
/**
|
|
27
|
+
* Fetches the Swagger/OpenAPI JSON from a given URL
|
|
28
|
+
*/
|
|
29
|
+
async fetchSwaggerJSON(url: string): Promise<SwaggerDoc> {
|
|
30
|
+
try {
|
|
31
|
+
const response: AxiosResponse<SwaggerDoc> = await axios.get(url);
|
|
32
|
+
return response.data;
|
|
33
|
+
} catch (error: unknown) {
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
throw new Error(`Failed to fetch Swagger JSON from ${url}: ${error.message}`);
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error(`Failed to fetch Swagger JSON from ${url}: ${String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Loads Swagger JSON from a local file
|
|
44
|
+
*/
|
|
45
|
+
loadSwaggerFromFile(filePath: string): SwaggerDoc {
|
|
46
|
+
try {
|
|
47
|
+
const jsonData = fs.readFileSync(filePath, 'utf8');
|
|
48
|
+
return JSON.parse(jsonData);
|
|
49
|
+
} catch (error: unknown) {
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
throw new Error(`Failed to load Swagger JSON from ${filePath}: ${error.message}`);
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Failed to load Swagger JSON from ${filePath}: ${String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates frontend resources using Handlebars templates
|
|
60
|
+
*/
|
|
61
|
+
generateHandlebarsResources(swaggerDoc: SwaggerDoc, templatePaths: {
|
|
62
|
+
hooks?: string,
|
|
63
|
+
types?: string,
|
|
64
|
+
components?: string,
|
|
65
|
+
pages?: string
|
|
66
|
+
} = {}): Map<string, { hooks: string, types: string }> {
|
|
67
|
+
const resourcesByTag = new Map<string, { hooks: string, types: string }>();
|
|
68
|
+
const schemas = swaggerDoc.components?.schemas || {};
|
|
69
|
+
|
|
70
|
+
// Group endpoints by tag
|
|
71
|
+
const endpointsByTag: { [tag: string]: Array<{ path: string, method: string, endpointInfo: any }> } = {};
|
|
72
|
+
|
|
73
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
74
|
+
Object.entries(methods).forEach(([method, endpointInfo]: [string, any]) => {
|
|
75
|
+
// Determine the tag for this endpoint
|
|
76
|
+
const tag = (endpointInfo.tags && endpointInfo.tags[0]) ? endpointInfo.tags[0] : 'General';
|
|
77
|
+
|
|
78
|
+
if (!endpointsByTag[tag]) {
|
|
79
|
+
endpointsByTag[tag] = [];
|
|
80
|
+
}
|
|
81
|
+
endpointsByTag[tag].push({ path, method, endpointInfo });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Generate resources for each tag
|
|
86
|
+
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
87
|
+
// Prepare context for templates
|
|
88
|
+
const context: HandlebarsContext = {
|
|
89
|
+
title: swaggerDoc.info.title,
|
|
90
|
+
description: swaggerDoc.info.description || swaggerDoc.info.title,
|
|
91
|
+
version: swaggerDoc.info.version,
|
|
92
|
+
tag: tag,
|
|
93
|
+
endpoints: endpoints.map(e => ({
|
|
94
|
+
path: e.path,
|
|
95
|
+
method: e.method.toUpperCase(),
|
|
96
|
+
operationId: e.endpointInfo.operationId || this.generateOperationId(e.path, e.method),
|
|
97
|
+
summary: e.endpointInfo.summary,
|
|
98
|
+
description: e.endpointInfo.description,
|
|
99
|
+
parameters: e.endpointInfo.parameters || [],
|
|
100
|
+
responses: e.endpointInfo.responses,
|
|
101
|
+
requestBody: e.endpointInfo.requestBody
|
|
102
|
+
})),
|
|
103
|
+
schemas: schemas,
|
|
104
|
+
hasImportTypes: false,
|
|
105
|
+
usedTypeNames: [] as string[],
|
|
106
|
+
paramInterfaces: [] as string[],
|
|
107
|
+
hooks: [] as string[],
|
|
108
|
+
typeDefinitions: [] as string[]
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Find types used in this tag
|
|
112
|
+
const directlyUsedSchemas = new Set<string>();
|
|
113
|
+
if (schemas) {
|
|
114
|
+
Object.entries(schemas).forEach(([typeName, schema]) => {
|
|
115
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
116
|
+
directlyUsedSchemas.add(typeName);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const allNeededSchemas = this.findAllReferencedSchemas(directlyUsedSchemas, schemas);
|
|
122
|
+
|
|
123
|
+
// Generate TypeScript types
|
|
124
|
+
let typesContent = '';
|
|
125
|
+
if (schemas) {
|
|
126
|
+
for (const typeName of allNeededSchemas) {
|
|
127
|
+
const schema = schemas[typeName];
|
|
128
|
+
if (schema) {
|
|
129
|
+
const typeDef = generateSingleTypeDefinition(typeName, schema, schemas);
|
|
130
|
+
typesContent += typeDef + '\n';
|
|
131
|
+
context.typeDefinitions.push(typeDef);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if there are used types for import
|
|
137
|
+
if (allNeededSchemas.size > 0) {
|
|
138
|
+
context.hasImportTypes = true;
|
|
139
|
+
context.usedTypeNames = Array.from(allNeededSchemas);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Generate parameter interfaces
|
|
143
|
+
const allParamInterfaces: string[] = [];
|
|
144
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
145
|
+
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
146
|
+
if (paramInterface && !allParamInterfaces.includes(paramInterface)) {
|
|
147
|
+
allParamInterfaces.push(paramInterface);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
context.paramInterfaces = allParamInterfaces;
|
|
152
|
+
|
|
153
|
+
// Generate individual hooks
|
|
154
|
+
const allHooks: string[] = [];
|
|
155
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
156
|
+
const hookContent = this.generateReactQueryHook(path, method, endpointInfo, schemas);
|
|
157
|
+
allHooks.push(hookContent);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
context.hooks = allHooks;
|
|
161
|
+
|
|
162
|
+
// Generate resources using specified templates
|
|
163
|
+
let hooksContent = '';
|
|
164
|
+
if (templatePaths.hooks) {
|
|
165
|
+
try {
|
|
166
|
+
// Add utility functions to context for use in templates
|
|
167
|
+
context['camelCase'] = (str: string) => toCamelCase(str);
|
|
168
|
+
context['pascalCase'] = (str: string) => toPascalCase(str);
|
|
169
|
+
|
|
170
|
+
hooksContent = compileTemplate(templatePaths.hooks, context);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// If template doesn't exist or fails, fall back to default generation
|
|
173
|
+
console.warn(`Failed to compile hooks template: ${templatePaths.hooks}`, error);
|
|
174
|
+
// Use the existing method as fallback
|
|
175
|
+
hooksContent = `// ${toPascalCase(tag)} API Hooks\n`;
|
|
176
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
177
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
178
|
+
|
|
179
|
+
if (context.hasImportTypes) {
|
|
180
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${toCamelCase(tag)}.types';\n\n`;
|
|
181
|
+
} else {
|
|
182
|
+
hooksContent += `\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
186
|
+
hooksContent += interfaceCode + '\n';
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
allHooks.forEach(hookCode => {
|
|
190
|
+
hooksContent += hookCode + '\n';
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Default generation if no template is provided
|
|
195
|
+
hooksContent = `// ${toPascalCase(tag)} API Hooks\n`;
|
|
196
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
197
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
198
|
+
|
|
199
|
+
if (context.hasImportTypes) {
|
|
200
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${toCamelCase(tag)}.types';\n\n`;
|
|
201
|
+
} else {
|
|
202
|
+
hooksContent += `\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
206
|
+
hooksContent += interfaceCode + '\n';
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
allHooks.forEach(hookCode => {
|
|
210
|
+
hooksContent += hookCode + '\n';
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
resourcesByTag.set(tag, {
|
|
215
|
+
hooks: hooksContent,
|
|
216
|
+
types: typesContent
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return resourcesByTag;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Checks if a schema is used in any of the endpoints
|
|
225
|
+
*/
|
|
226
|
+
isSchemaUsedInEndpoints(schemaName: string, endpoints: Array<{ path: string, method: string, endpointInfo: any }>, allSchemas: { [key: string]: any }): boolean {
|
|
227
|
+
for (const { endpointInfo } of endpoints) {
|
|
228
|
+
// Check if schema is used as response
|
|
229
|
+
if (endpointInfo.responses) {
|
|
230
|
+
for (const [, responseInfo] of Object.entries(endpointInfo.responses) as [string, any]) {
|
|
231
|
+
if (responseInfo.content) {
|
|
232
|
+
for (const [, contentInfo] of Object.entries(responseInfo.content) as [string, any]) {
|
|
233
|
+
if (contentInfo.schema) {
|
|
234
|
+
if (this.schemaContainsRef(contentInfo.schema, schemaName, allSchemas)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if schema is used in parameters
|
|
244
|
+
if (endpointInfo.parameters) {
|
|
245
|
+
for (const param of endpointInfo.parameters) {
|
|
246
|
+
if (param.schema && this.schemaContainsRef(param.schema, schemaName, allSchemas)) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check if schema is used in request body
|
|
253
|
+
if (endpointInfo.requestBody && endpointInfo.requestBody.content) {
|
|
254
|
+
for (const [, contentInfo] of Object.entries(endpointInfo.requestBody.content) as [string, any]) {
|
|
255
|
+
if (contentInfo.schema && this.schemaContainsRef(contentInfo.schema, schemaName, allSchemas)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Checks if a schema contains a reference to another schema
|
|
266
|
+
*/
|
|
267
|
+
schemaContainsRef(schema: any, targetSchemaName: string, allSchemas: { [key: string]: any }): boolean {
|
|
268
|
+
if (!schema) return false;
|
|
269
|
+
|
|
270
|
+
// Check if this schema directly references the target
|
|
271
|
+
if (schema.$ref) {
|
|
272
|
+
const refTypeName = schema.$ref.split('/').pop();
|
|
273
|
+
if (refTypeName === targetSchemaName) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Recursively check nested properties
|
|
279
|
+
if (schema.properties) {
|
|
280
|
+
for (const [, propSchema] of Object.entries(schema.properties)) {
|
|
281
|
+
if (this.schemaContainsRef(propSchema as any, targetSchemaName, allSchemas)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if it's an array schema
|
|
288
|
+
if (schema.items) {
|
|
289
|
+
if (this.schemaContainsRef(schema.items, targetSchemaName, allSchemas)) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check allOf, oneOf, anyOf
|
|
295
|
+
if (schema.allOf) {
|
|
296
|
+
for (const item of schema.allOf) {
|
|
297
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (schema.oneOf) {
|
|
304
|
+
for (const item of schema.oneOf) {
|
|
305
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (schema.anyOf) {
|
|
312
|
+
for (const item of schema.anyOf) {
|
|
313
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Find all referenced schemas from a set of directly used schemas
|
|
324
|
+
*/
|
|
325
|
+
findAllReferencedSchemas(initialSchemas: Set<string>, allSchemas: { [key: string]: any }): Set<string> {
|
|
326
|
+
const result = new Set<string>([...initialSchemas]); // Start with initial schemas
|
|
327
|
+
let changed = true;
|
|
328
|
+
|
|
329
|
+
while (changed) {
|
|
330
|
+
changed = false;
|
|
331
|
+
|
|
332
|
+
for (const typeName of [...result]) { // Use spread to create a new array to avoid concurrent modification
|
|
333
|
+
const schema = allSchemas[typeName];
|
|
334
|
+
if (schema) {
|
|
335
|
+
// Check for references in the schema
|
|
336
|
+
const referencedSchemas = this.findSchemaReferences(schema, allSchemas);
|
|
337
|
+
for (const refName of referencedSchemas) {
|
|
338
|
+
if (!result.has(refName) && allSchemas[refName]) {
|
|
339
|
+
result.add(refName);
|
|
340
|
+
changed = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Find schema references in a given schema
|
|
352
|
+
*/
|
|
353
|
+
findSchemaReferences(schema: any, allSchemas: { [key: string]: any }): Set<string> {
|
|
354
|
+
const references = new Set<string>();
|
|
355
|
+
|
|
356
|
+
if (!schema) return references;
|
|
357
|
+
|
|
358
|
+
// Check direct $ref
|
|
359
|
+
if (schema.$ref) {
|
|
360
|
+
const refTypeName = schema.$ref.split('/').pop();
|
|
361
|
+
if (refTypeName && allSchemas[refTypeName]) {
|
|
362
|
+
references.add(refTypeName);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check properties
|
|
367
|
+
if (schema.properties) {
|
|
368
|
+
Object.values(schema.properties).forEach((propSchema: any) => {
|
|
369
|
+
const nestedRefs = this.findSchemaReferences(propSchema, allSchemas);
|
|
370
|
+
nestedRefs.forEach(ref => references.add(ref));
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check array items
|
|
375
|
+
if (schema.items) {
|
|
376
|
+
const itemRefs = this.findSchemaReferences(schema.items, allSchemas);
|
|
377
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check allOf, oneOf, anyOf
|
|
381
|
+
if (schema.allOf) {
|
|
382
|
+
schema.allOf.forEach((item: any) => {
|
|
383
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
384
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (schema.oneOf) {
|
|
389
|
+
schema.oneOf.forEach((item: any) => {
|
|
390
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
391
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (schema.anyOf) {
|
|
396
|
+
schema.anyOf.forEach((item: any) => {
|
|
397
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
398
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return references;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Generates a parameter interface for an API endpoint
|
|
407
|
+
*/
|
|
408
|
+
generateParamInterface(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
409
|
+
if (!endpointInfo.parameters || endpointInfo.parameters.length === 0) {
|
|
410
|
+
return '';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const pathParams = endpointInfo.parameters.filter((p: Parameter) => p.in === 'path');
|
|
414
|
+
const queryParams = endpointInfo.parameters.filter((p: Parameter) => p.in === 'query');
|
|
415
|
+
|
|
416
|
+
if (pathParams.length === 0 && queryParams.length === 0) {
|
|
417
|
+
return '';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Create a unique interface name based on the operation ID
|
|
421
|
+
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
422
|
+
|
|
423
|
+
// Extract action name from operationId to create cleaner parameter interface names
|
|
424
|
+
// e.g. configController_updateConfig -> UpdateConfigParams instead of ConfigController_updateConfigParams
|
|
425
|
+
let interfaceName: string;
|
|
426
|
+
if (operationId.includes('_')) {
|
|
427
|
+
const parts = operationId.split('_');
|
|
428
|
+
if (parts.length >= 2) {
|
|
429
|
+
// Use just the action part in the interface name
|
|
430
|
+
interfaceName = `${toPascalCase(parts[parts.length - 1])}Params`;
|
|
431
|
+
} else {
|
|
432
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let paramsInterface = `export interface ${interfaceName} {\n`;
|
|
439
|
+
|
|
440
|
+
// Add path parameters
|
|
441
|
+
pathParams.forEach((param: Parameter) => {
|
|
442
|
+
const required = param.required ? '' : '?';
|
|
443
|
+
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
444
|
+
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Add query parameters
|
|
448
|
+
queryParams.forEach((param: Parameter) => {
|
|
449
|
+
const required = param.required ? '' : '?';
|
|
450
|
+
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
451
|
+
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
paramsInterface += '}\n';
|
|
455
|
+
return paramsInterface;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Generates a React Query hook using axios
|
|
460
|
+
*/
|
|
461
|
+
generateReactQueryHook(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
462
|
+
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
463
|
+
|
|
464
|
+
// Extract action name from operationId to create cleaner hook names
|
|
465
|
+
// e.g. configController_updateConfig -> useUpdateConfig instead of useConfigController_updateConfig
|
|
466
|
+
let hookName = `use${toPascalCase(operationId)}`;
|
|
467
|
+
|
|
468
|
+
// Check if operationId follows pattern controller_action and simplify to action
|
|
469
|
+
if (operationId.includes('_')) {
|
|
470
|
+
const parts = operationId.split('_');
|
|
471
|
+
if (parts.length >= 2) {
|
|
472
|
+
// Use just the action part as the hook name
|
|
473
|
+
hookName = `use${toPascalCase(parts[parts.length - 1])}`;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
// For operationIds without underscores, keep the original naming
|
|
477
|
+
hookName = `use${toPascalCase(operationId)}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Use unique parameter interface name
|
|
481
|
+
const pathParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'path') || [];
|
|
482
|
+
const queryParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'query') || [];
|
|
483
|
+
|
|
484
|
+
// Determine response type by checking common success response codes
|
|
485
|
+
let responseType = 'any';
|
|
486
|
+
if (endpointInfo.responses) {
|
|
487
|
+
// Check for success responses in order of preference: 200, 201, 204, etc.
|
|
488
|
+
const successCodes = ['200', '201', '204', '202', '203', '205'];
|
|
489
|
+
for (const code of successCodes) {
|
|
490
|
+
if (endpointInfo.responses[code]) {
|
|
491
|
+
const responseSchema = endpointInfo.responses[code].content?.['application/json']?.schema;
|
|
492
|
+
if (responseSchema) {
|
|
493
|
+
responseType = convertTypeToTs(responseSchema, schemas);
|
|
494
|
+
break; // Use the first success response found
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Generate request body parameter if needed
|
|
501
|
+
let requestBodyType = 'any';
|
|
502
|
+
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
503
|
+
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
504
|
+
if (bodySchema) {
|
|
505
|
+
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Format the path for use in the code (handle path parameters) - without base URL
|
|
510
|
+
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
511
|
+
|
|
512
|
+
// Prepare data for the template
|
|
513
|
+
const hookData = {
|
|
514
|
+
hookName: hookName,
|
|
515
|
+
operationId: operationId,
|
|
516
|
+
method: method.toLowerCase(),
|
|
517
|
+
responseType: responseType,
|
|
518
|
+
requestBodyType: requestBodyType,
|
|
519
|
+
hasParams: pathParams.length > 0 || queryParams.length > 0,
|
|
520
|
+
hasPathParams: pathParams.length > 0,
|
|
521
|
+
paramInterfaceName: `${hookName.replace('use', '')}Params`,
|
|
522
|
+
formattedPath: formattedPath,
|
|
523
|
+
isGetRequest: method.toLowerCase() === 'get'
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// Load and compile the individual hook template
|
|
527
|
+
const fs = require('fs');
|
|
528
|
+
const pathModule = require('path');
|
|
529
|
+
const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'hooks', 'individual-hook.hbs');
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const templateSource = fs.readFileSync(templatePath, 'utf8');
|
|
533
|
+
const Handlebars = require('handlebars');
|
|
534
|
+
const template = Handlebars.compile(templateSource);
|
|
535
|
+
return template(hookData);
|
|
536
|
+
} catch (error: any) {
|
|
537
|
+
console.error(`Error reading template file: ${error.message}`);
|
|
538
|
+
return `// Error generating hook for ${operationId}: ${error.message}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Generate operation ID from path and method if not provided
|
|
545
|
+
*/
|
|
546
|
+
generateOperationId(path: string, method: string): string {
|
|
547
|
+
return `${method.toLowerCase()}_${path.replace(/[\/{}]/g, '_')}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Formats code using Prettier - sync version with child process
|
|
552
|
+
*/
|
|
553
|
+
private formatCode(code: string, filepath: string): string {
|
|
554
|
+
// Skip formatting in test environment to avoid ESM issues
|
|
555
|
+
if (process.env.NODE_ENV === 'test' || typeof jest !== 'undefined') {
|
|
556
|
+
return code;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
// Use execSync to run prettier as a separate process to avoid ESM issues
|
|
561
|
+
const { execSync } = require('child_process');
|
|
562
|
+
const { writeFileSync, readFileSync, unlinkSync } = require('fs');
|
|
563
|
+
const { join, extname } = require('path');
|
|
564
|
+
const { tmpdir } = require('os');
|
|
565
|
+
|
|
566
|
+
// Determine the file extension to use for the temp file
|
|
567
|
+
const fileExtension = extname(filepath) || '.txt';
|
|
568
|
+
const tempPath = join(tmpdir(), `prettier-tmp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExtension}`);
|
|
569
|
+
writeFileSync(tempPath, code, 'utf8');
|
|
570
|
+
|
|
571
|
+
// Format the file using prettier CLI
|
|
572
|
+
execSync(`npx prettier --write "${tempPath}" --single-quote --trailing-comma es5 --tab-width 2 --semi --print-width 80`, {
|
|
573
|
+
stdio: 'pipe'
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Read the formatted content back
|
|
577
|
+
const formattedCode = readFileSync(tempPath, 'utf8');
|
|
578
|
+
|
|
579
|
+
// Clean up the temporary file
|
|
580
|
+
unlinkSync(tempPath);
|
|
581
|
+
|
|
582
|
+
return formattedCode;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.warn(`Failed to format ${filepath} with Prettier:`, error);
|
|
585
|
+
return code; // Return unformatted code if formatting fails
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Gets the parser based on file extension
|
|
591
|
+
*/
|
|
592
|
+
private getParserForFile(filepath: string): string {
|
|
593
|
+
const ext = path.extname(filepath);
|
|
594
|
+
switch (ext) {
|
|
595
|
+
case '.ts':
|
|
596
|
+
case '.tsx':
|
|
597
|
+
return 'typescript';
|
|
598
|
+
case '.js':
|
|
599
|
+
case '.jsx':
|
|
600
|
+
return 'babel';
|
|
601
|
+
case '.json':
|
|
602
|
+
return 'json';
|
|
603
|
+
case '.md':
|
|
604
|
+
return 'markdown';
|
|
605
|
+
default:
|
|
606
|
+
return 'typescript';
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Saves the generated documentation to a file
|
|
612
|
+
*/
|
|
613
|
+
saveDocumentationToFile(documentation: string, outputPath: string): void {
|
|
614
|
+
const dir = path.dirname(outputPath);
|
|
615
|
+
if (!fs.existsSync(dir)) {
|
|
616
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const formattedDocumentation = this.formatCode(documentation, outputPath);
|
|
620
|
+
fs.writeFileSync(outputPath, formattedDocumentation, 'utf8');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Saves the generated TypeScript types to a file
|
|
625
|
+
*/
|
|
626
|
+
saveTypesToFile(types: string, outputPath: string): void {
|
|
627
|
+
const dir = path.dirname(outputPath);
|
|
628
|
+
if (!fs.existsSync(dir)) {
|
|
629
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const formattedTypes = this.formatCode(types, outputPath);
|
|
633
|
+
fs.writeFileSync(outputPath, formattedTypes, 'utf8');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Saves the generated React hooks to files organized by tag
|
|
638
|
+
*/
|
|
639
|
+
saveHooksByTag(hooksByTag: Map<string, { hooks: string, types: string }>, outputDir: string): void {
|
|
640
|
+
const dir = outputDir;
|
|
641
|
+
if (!fs.existsSync(dir)) {
|
|
642
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
for (const [tag, { hooks, types }] of hooksByTag) {
|
|
646
|
+
const tagDir = path.join(outputDir, toCamelCase(tag));
|
|
647
|
+
if (!fs.existsSync(tagDir)) {
|
|
648
|
+
fs.mkdirSync(tagDir, { recursive: true });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Save hooks to hooks file
|
|
652
|
+
const hooksFileName = path.join(tagDir, `${toCamelCase(tag)}.hooks.ts`);
|
|
653
|
+
const formattedHooks = this.formatCode(hooks, hooksFileName);
|
|
654
|
+
fs.writeFileSync(hooksFileName, formattedHooks, 'utf8');
|
|
655
|
+
|
|
656
|
+
// Save types to types file
|
|
657
|
+
if (types.trim()) { // Only save if there are types
|
|
658
|
+
const typesFileName = path.join(tagDir, `${toCamelCase(tag)}.types.ts`);
|
|
659
|
+
const formattedTypes = this.formatCode(types, typesFileName);
|
|
660
|
+
fs.writeFileSync(typesFileName, formattedTypes, 'utf8');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|