jszy-swagger-doc-generator 1.1.1 → 1.4.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 +15 -6
- package/dist/cli.js +17 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +44 -12
- package/dist/index.js +394 -70
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/src/cli.ts +21 -5
- package/src/index.ts +432 -78
package/dist/index.js
CHANGED
|
@@ -287,7 +287,7 @@ class SwaggerDocGenerator {
|
|
|
287
287
|
return `export type ${typeName} = ${convertTypeToTs(schema, allSchemas)};\n`;
|
|
288
288
|
}
|
|
289
289
|
/**
|
|
290
|
-
* Generates React hooks from the paths in Swagger doc
|
|
290
|
+
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
291
291
|
*/
|
|
292
292
|
generateReactHooks(swaggerDoc) {
|
|
293
293
|
const hooksByTag = new Map();
|
|
@@ -304,10 +304,65 @@ class SwaggerDocGenerator {
|
|
|
304
304
|
endpointsByTag[tag].push({ path, method, endpointInfo });
|
|
305
305
|
});
|
|
306
306
|
});
|
|
307
|
-
// Generate hooks for each tag
|
|
307
|
+
// Generate hooks and types for each tag
|
|
308
308
|
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
// Generate TypeScript types for schemas used in this tag
|
|
310
|
+
let typesContent = `// ${toPascalCase(tag)} API Types\n\n`;
|
|
311
|
+
// First, find all directly used schemas in endpoints for this tag
|
|
312
|
+
const directlyUsedSchemas = new Set();
|
|
313
|
+
if (schemas) {
|
|
314
|
+
Object.entries(schemas).forEach(([typeName, schema]) => {
|
|
315
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
316
|
+
directlyUsedSchemas.add(typeName);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// Then, find all referenced schemas (schemas that are referenced by the directly used ones)
|
|
321
|
+
const allNeededSchemas = this.findAllReferencedSchemas(directlyUsedSchemas, schemas);
|
|
322
|
+
// Generate types for all needed schemas
|
|
323
|
+
if (schemas) {
|
|
324
|
+
for (const typeName of allNeededSchemas) {
|
|
325
|
+
const schema = schemas[typeName];
|
|
326
|
+
if (schema) {
|
|
327
|
+
typesContent += this.generateSingleTypeDefinition(typeName, schema, schemas);
|
|
328
|
+
typesContent += '\n';
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Generate hooks content
|
|
333
|
+
let hooksContent = `// ${toPascalCase(tag)} API Hooks\n`;
|
|
334
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
335
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
336
|
+
// Determine which types are actually used in this tag's endpoints and generate imports
|
|
337
|
+
const usedTypeNames = new Set();
|
|
338
|
+
// First, find all types that are directly used in endpoints for this tag
|
|
339
|
+
if (schemas) {
|
|
340
|
+
for (const [typeName, schema] of Object.entries(schemas)) {
|
|
341
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
342
|
+
usedTypeNames.add(typeName);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Then, add all transitive dependencies of the directly used types
|
|
347
|
+
const finalTypeNames = new Set();
|
|
348
|
+
for (const typeName of usedTypeNames) {
|
|
349
|
+
finalTypeNames.add(typeName);
|
|
350
|
+
// Find all types referenced by this type
|
|
351
|
+
const referencedTypes = this.findSchemaReferences(schemas[typeName], schemas);
|
|
352
|
+
for (const refName of referencedTypes) {
|
|
353
|
+
if (schemas[refName]) {
|
|
354
|
+
finalTypeNames.add(refName);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Add import statement only if there are types to import
|
|
359
|
+
if (finalTypeNames.size > 0) {
|
|
360
|
+
hooksContent += `import type { ${Array.from(finalTypeNames).join(', ')} } from './${toCamelCase(tag)}.types';\n\n`;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
hooksContent += `\n`;
|
|
364
|
+
}
|
|
365
|
+
// Generate parameter interfaces for this tag
|
|
311
366
|
const allParamInterfaces = [];
|
|
312
367
|
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
313
368
|
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
@@ -317,22 +372,176 @@ class SwaggerDocGenerator {
|
|
|
317
372
|
});
|
|
318
373
|
// Add all unique parameter interfaces
|
|
319
374
|
allParamInterfaces.forEach(interfaceCode => {
|
|
320
|
-
|
|
375
|
+
hooksContent += interfaceCode + '\n';
|
|
321
376
|
});
|
|
322
|
-
// Generate individual hooks
|
|
377
|
+
// Generate individual hooks using react-query and axios
|
|
323
378
|
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
324
|
-
const hookContent = this.
|
|
325
|
-
|
|
379
|
+
const hookContent = this.generateReactQueryHook(path, method, endpointInfo, schemas);
|
|
380
|
+
hooksContent += hookContent + '\n';
|
|
326
381
|
});
|
|
327
|
-
hooksByTag.set(tag,
|
|
382
|
+
hooksByTag.set(tag, { hooks: hooksContent, types: typesContent });
|
|
328
383
|
});
|
|
329
384
|
return hooksByTag;
|
|
330
385
|
}
|
|
331
386
|
/**
|
|
332
|
-
*
|
|
387
|
+
* Checks if a schema is used in any of the endpoints
|
|
388
|
+
*/
|
|
389
|
+
isSchemaUsedInEndpoints(schemaName, endpoints, allSchemas) {
|
|
390
|
+
for (const { endpointInfo } of endpoints) {
|
|
391
|
+
// Check if schema is used as response
|
|
392
|
+
if (endpointInfo.responses) {
|
|
393
|
+
for (const [, responseInfo] of Object.entries(endpointInfo.responses)) {
|
|
394
|
+
if (responseInfo.content) {
|
|
395
|
+
for (const [, contentInfo] of Object.entries(responseInfo.content)) {
|
|
396
|
+
if (contentInfo.schema) {
|
|
397
|
+
if (this.schemaContainsRef(contentInfo.schema, schemaName, allSchemas)) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Check if schema is used in parameters
|
|
406
|
+
if (endpointInfo.parameters) {
|
|
407
|
+
for (const param of endpointInfo.parameters) {
|
|
408
|
+
if (param.schema && this.schemaContainsRef(param.schema, schemaName, allSchemas)) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Check if schema is used in request body
|
|
414
|
+
if (endpointInfo.requestBody && endpointInfo.requestBody.content) {
|
|
415
|
+
for (const [, contentInfo] of Object.entries(endpointInfo.requestBody.content)) {
|
|
416
|
+
if (contentInfo.schema && this.schemaContainsRef(contentInfo.schema, schemaName, allSchemas)) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Checks if a schema contains a reference to another schema
|
|
426
|
+
*/
|
|
427
|
+
schemaContainsRef(schema, targetSchemaName, allSchemas) {
|
|
428
|
+
if (!schema)
|
|
429
|
+
return false;
|
|
430
|
+
// Check if this schema directly references the target
|
|
431
|
+
if (schema.$ref) {
|
|
432
|
+
const refTypeName = schema.$ref.split('/').pop();
|
|
433
|
+
if (refTypeName === targetSchemaName) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Recursively check nested properties
|
|
438
|
+
if (schema.properties) {
|
|
439
|
+
for (const [, propSchema] of Object.entries(schema.properties)) {
|
|
440
|
+
if (this.schemaContainsRef(propSchema, targetSchemaName, allSchemas)) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Check if it's an array schema
|
|
446
|
+
if (schema.items) {
|
|
447
|
+
if (this.schemaContainsRef(schema.items, targetSchemaName, allSchemas)) {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Check allOf, oneOf, anyOf
|
|
452
|
+
if (schema.allOf) {
|
|
453
|
+
for (const item of schema.allOf) {
|
|
454
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (schema.oneOf) {
|
|
460
|
+
for (const item of schema.oneOf) {
|
|
461
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (schema.anyOf) {
|
|
467
|
+
for (const item of schema.anyOf) {
|
|
468
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Find all referenced schemas from a set of directly used schemas
|
|
477
|
+
*/
|
|
478
|
+
findAllReferencedSchemas(initialSchemas, allSchemas) {
|
|
479
|
+
const result = new Set([...initialSchemas]); // Start with initial schemas
|
|
480
|
+
let changed = true;
|
|
481
|
+
while (changed) {
|
|
482
|
+
changed = false;
|
|
483
|
+
for (const typeName of [...result]) { // Use spread to create a new array to avoid concurrent modification
|
|
484
|
+
const schema = allSchemas[typeName];
|
|
485
|
+
if (schema) {
|
|
486
|
+
// Check for references in the schema
|
|
487
|
+
const referencedSchemas = this.findSchemaReferences(schema, allSchemas);
|
|
488
|
+
for (const refName of referencedSchemas) {
|
|
489
|
+
if (!result.has(refName) && allSchemas[refName]) {
|
|
490
|
+
result.add(refName);
|
|
491
|
+
changed = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Find schema references in a given schema
|
|
333
501
|
*/
|
|
334
|
-
|
|
335
|
-
|
|
502
|
+
findSchemaReferences(schema, allSchemas) {
|
|
503
|
+
const references = new Set();
|
|
504
|
+
if (!schema)
|
|
505
|
+
return references;
|
|
506
|
+
// Check direct $ref
|
|
507
|
+
if (schema.$ref) {
|
|
508
|
+
const refTypeName = schema.$ref.split('/').pop();
|
|
509
|
+
if (refTypeName && allSchemas[refTypeName]) {
|
|
510
|
+
references.add(refTypeName);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Check properties
|
|
514
|
+
if (schema.properties) {
|
|
515
|
+
Object.values(schema.properties).forEach((propSchema) => {
|
|
516
|
+
const nestedRefs = this.findSchemaReferences(propSchema, allSchemas);
|
|
517
|
+
nestedRefs.forEach(ref => references.add(ref));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// Check array items
|
|
521
|
+
if (schema.items) {
|
|
522
|
+
const itemRefs = this.findSchemaReferences(schema.items, allSchemas);
|
|
523
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
524
|
+
}
|
|
525
|
+
// Check allOf, oneOf, anyOf
|
|
526
|
+
if (schema.allOf) {
|
|
527
|
+
schema.allOf.forEach((item) => {
|
|
528
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
529
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (schema.oneOf) {
|
|
533
|
+
schema.oneOf.forEach((item) => {
|
|
534
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
535
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (schema.anyOf) {
|
|
539
|
+
schema.anyOf.forEach((item) => {
|
|
540
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
541
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return references;
|
|
336
545
|
}
|
|
337
546
|
/**
|
|
338
547
|
* Generates a parameter interface for an API endpoint
|
|
@@ -348,7 +557,22 @@ class SwaggerDocGenerator {
|
|
|
348
557
|
}
|
|
349
558
|
// Create a unique interface name based on the operation ID
|
|
350
559
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
351
|
-
|
|
560
|
+
// Extract action name from operationId to create cleaner parameter interface names
|
|
561
|
+
// e.g. configController_updateConfig -> UpdateConfigParams instead of ConfigController_updateConfigParams
|
|
562
|
+
let interfaceName;
|
|
563
|
+
if (operationId.includes('_')) {
|
|
564
|
+
const parts = operationId.split('_');
|
|
565
|
+
if (parts.length >= 2) {
|
|
566
|
+
// Use just the action part in the interface name
|
|
567
|
+
interfaceName = `${toPascalCase(parts[parts.length - 1])}Params`;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
575
|
+
}
|
|
352
576
|
let paramsInterface = `export interface ${interfaceName} {\n`;
|
|
353
577
|
// Add path parameters
|
|
354
578
|
pathParams.forEach((param) => {
|
|
@@ -366,24 +590,29 @@ class SwaggerDocGenerator {
|
|
|
366
590
|
return paramsInterface;
|
|
367
591
|
}
|
|
368
592
|
/**
|
|
369
|
-
* Generates a
|
|
593
|
+
* Generates a React Query hook using axios
|
|
370
594
|
*/
|
|
371
|
-
|
|
595
|
+
generateReactQueryHook(path, method, endpointInfo, schemas) {
|
|
372
596
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
373
|
-
|
|
597
|
+
// Extract action name from operationId to create cleaner hook names
|
|
598
|
+
// e.g. configController_updateConfig -> useUpdateConfig instead of useConfigController_updateConfig
|
|
599
|
+
let hookName = `use${toPascalCase(operationId)}`;
|
|
600
|
+
// Check if operationId follows pattern controller_action and simplify to action
|
|
601
|
+
if (operationId.includes('_')) {
|
|
602
|
+
const parts = operationId.split('_');
|
|
603
|
+
if (parts.length >= 2) {
|
|
604
|
+
// Use just the action part as the hook name
|
|
605
|
+
hookName = `use${toPascalCase(parts[parts.length - 1])}`;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
// For operationIds without underscores, keep the original naming
|
|
610
|
+
hookName = `use${toPascalCase(operationId)}`;
|
|
611
|
+
}
|
|
612
|
+
const hookType = method.toLowerCase() === 'get' ? 'useQuery' : 'useMutation';
|
|
374
613
|
// Use unique parameter interface name
|
|
375
614
|
const pathParams = endpointInfo.parameters?.filter((p) => p.in === 'path') || [];
|
|
376
615
|
const queryParams = endpointInfo.parameters?.filter((p) => p.in === 'query') || [];
|
|
377
|
-
let paramsDeclaration = '';
|
|
378
|
-
let paramsUsage = '{}';
|
|
379
|
-
const hasParams = pathParams.length > 0 || queryParams.length > 0;
|
|
380
|
-
if (hasParams) {
|
|
381
|
-
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
382
|
-
paramsDeclaration = `params: ${paramInterfaceName}`;
|
|
383
|
-
paramsUsage = 'params';
|
|
384
|
-
}
|
|
385
|
-
// Format the path for use in the code (handle path parameters)
|
|
386
|
-
const pathWithParams = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
387
616
|
// Determine response type
|
|
388
617
|
let responseType = 'any';
|
|
389
618
|
if (endpointInfo.responses && endpointInfo.responses['200']) {
|
|
@@ -393,60 +622,145 @@ class SwaggerDocGenerator {
|
|
|
393
622
|
}
|
|
394
623
|
}
|
|
395
624
|
// Generate request body parameter if needed
|
|
396
|
-
let
|
|
625
|
+
let requestBodyType = 'any';
|
|
626
|
+
let hasBody = false;
|
|
397
627
|
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
398
628
|
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
399
629
|
if (bodySchema) {
|
|
400
|
-
|
|
401
|
-
|
|
630
|
+
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
631
|
+
hasBody = true;
|
|
402
632
|
}
|
|
403
633
|
}
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
634
|
+
// Format the path for use in the code (handle path parameters) - without base URL
|
|
635
|
+
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
636
|
+
const axiosPath = `\`${formattedPath}\``;
|
|
637
|
+
// Generate the hook code
|
|
638
|
+
let hookCode = '';
|
|
639
|
+
if (method.toLowerCase() === 'get') {
|
|
640
|
+
// For GET requests, use useQuery
|
|
641
|
+
const hasParams = pathParams.length > 0 || queryParams.length > 0;
|
|
642
|
+
if (hasParams) {
|
|
643
|
+
// Generate simpler parameter interface name based on hook name instead of operationId
|
|
644
|
+
const paramInterfaceName = `${hookName.replace('use', '')}Params`;
|
|
645
|
+
hookCode += `export const ${hookName} = (params: ${paramInterfaceName}) => {\n`;
|
|
646
|
+
hookCode += ` return useQuery({\n`;
|
|
647
|
+
hookCode += ` queryKey: ['${operationId}', params],\n`;
|
|
648
|
+
hookCode += ` queryFn: async () => {\n`;
|
|
649
|
+
hookCode += ` const response = await axios.get<${responseType}>(${axiosPath}, { params });\n`;
|
|
650
|
+
hookCode += ` return response.data;\n`;
|
|
651
|
+
hookCode += ` },\n`;
|
|
652
|
+
hookCode += ` });\n`;
|
|
653
|
+
hookCode += `};\n`;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
hookCode += `export const ${hookName} = () => {\n`;
|
|
657
|
+
hookCode += ` return useQuery({\n`;
|
|
658
|
+
hookCode += ` queryKey: ['${operationId}'],\n`;
|
|
659
|
+
hookCode += ` queryFn: async () => {\n`;
|
|
660
|
+
hookCode += ` const response = await axios.get<${responseType}>(${axiosPath});\n`;
|
|
661
|
+
hookCode += ` return response.data;\n`;
|
|
662
|
+
hookCode += ` },\n`;
|
|
663
|
+
hookCode += ` });\n`;
|
|
664
|
+
hookCode += `};\n`;
|
|
665
|
+
}
|
|
418
666
|
}
|
|
419
667
|
else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
668
|
+
// For non-GET requests, use useMutation
|
|
669
|
+
const hasPathParams = pathParams.length > 0;
|
|
670
|
+
if (hasPathParams) {
|
|
671
|
+
// Generate simpler parameter interface name based on hook name instead of operationId
|
|
672
|
+
const paramInterfaceName = `${hookName.replace('use', '')}Params`;
|
|
673
|
+
hookCode += `export const ${hookName} = () => {\n`;
|
|
674
|
+
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
675
|
+
hookCode += ` return useMutation({\n`;
|
|
676
|
+
hookCode += ` mutationFn: async ({ params, data }: { params: ${paramInterfaceName}; data: ${requestBodyType} }) => {\n`;
|
|
677
|
+
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
678
|
+
hookCode += ` return response.data;\n`;
|
|
679
|
+
hookCode += ` },\n`;
|
|
680
|
+
hookCode += ` onSuccess: () => {\n`;
|
|
681
|
+
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
682
|
+
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
683
|
+
hookCode += ` },\n`;
|
|
684
|
+
hookCode += ` });\n`;
|
|
685
|
+
hookCode += `};\n`;
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
hookCode += `export const ${hookName} = () => {\n`;
|
|
689
|
+
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
690
|
+
hookCode += ` return useMutation({\n`;
|
|
691
|
+
hookCode += ` mutationFn: async (data: ${requestBodyType}) => {\n`;
|
|
692
|
+
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
693
|
+
hookCode += ` return response.data;\n`;
|
|
694
|
+
hookCode += ` },\n`;
|
|
695
|
+
hookCode += ` onSuccess: () => {\n`;
|
|
696
|
+
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
697
|
+
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
698
|
+
hookCode += ` },\n`;
|
|
699
|
+
hookCode += ` });\n`;
|
|
700
|
+
hookCode += `};\n`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
435
703
|
return hookCode;
|
|
436
704
|
}
|
|
437
|
-
/**
|
|
438
|
-
* Generates a single React hook for an API endpoint
|
|
439
|
-
*/
|
|
440
|
-
generateSingleHook(path, method, endpointInfo, schemas) {
|
|
441
|
-
// This method is kept for backward compatibility
|
|
442
|
-
return this.generateSingleHookWithUniqueName(path, method, endpointInfo, schemas);
|
|
443
|
-
}
|
|
444
705
|
/**
|
|
445
706
|
* Generate operation ID from path and method if not provided
|
|
446
707
|
*/
|
|
447
708
|
generateOperationId(path, method) {
|
|
448
709
|
return `${method.toLowerCase()}_${path.replace(/[\/{}]/g, '_')}`;
|
|
449
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Formats code using Prettier - sync version with child process
|
|
713
|
+
*/
|
|
714
|
+
formatCode(code, filepath) {
|
|
715
|
+
// Skip formatting in test environment to avoid ESM issues
|
|
716
|
+
if (process.env.NODE_ENV === 'test' || typeof jest !== 'undefined') {
|
|
717
|
+
return code;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
// Use execSync to run prettier as a separate process to avoid ESM issues
|
|
721
|
+
const { execSync } = require('child_process');
|
|
722
|
+
const { writeFileSync, readFileSync, unlinkSync } = require('fs');
|
|
723
|
+
const { join, extname } = require('path');
|
|
724
|
+
const { tmpdir } = require('os');
|
|
725
|
+
// Determine the file extension to use for the temp file
|
|
726
|
+
const fileExtension = extname(filepath) || '.txt';
|
|
727
|
+
const tempPath = join(tmpdir(), `prettier-tmp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExtension}`);
|
|
728
|
+
writeFileSync(tempPath, code, 'utf8');
|
|
729
|
+
// Format the file using prettier CLI
|
|
730
|
+
execSync(`npx prettier --write "${tempPath}" --single-quote --trailing-comma es5 --tab-width 2 --semi --print-width 80`, {
|
|
731
|
+
stdio: 'pipe'
|
|
732
|
+
});
|
|
733
|
+
// Read the formatted content back
|
|
734
|
+
const formattedCode = readFileSync(tempPath, 'utf8');
|
|
735
|
+
// Clean up the temporary file
|
|
736
|
+
unlinkSync(tempPath);
|
|
737
|
+
return formattedCode;
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
console.warn(`Failed to format ${filepath} with Prettier:`, error);
|
|
741
|
+
return code; // Return unformatted code if formatting fails
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Gets the parser based on file extension
|
|
746
|
+
*/
|
|
747
|
+
getParserForFile(filepath) {
|
|
748
|
+
const ext = path.extname(filepath);
|
|
749
|
+
switch (ext) {
|
|
750
|
+
case '.ts':
|
|
751
|
+
case '.tsx':
|
|
752
|
+
return 'typescript';
|
|
753
|
+
case '.js':
|
|
754
|
+
case '.jsx':
|
|
755
|
+
return 'babel';
|
|
756
|
+
case '.json':
|
|
757
|
+
return 'json';
|
|
758
|
+
case '.md':
|
|
759
|
+
return 'markdown';
|
|
760
|
+
default:
|
|
761
|
+
return 'typescript';
|
|
762
|
+
}
|
|
763
|
+
}
|
|
450
764
|
/**
|
|
451
765
|
* Saves the generated documentation to a file
|
|
452
766
|
*/
|
|
@@ -455,7 +769,8 @@ class SwaggerDocGenerator {
|
|
|
455
769
|
if (!fs.existsSync(dir)) {
|
|
456
770
|
fs.mkdirSync(dir, { recursive: true });
|
|
457
771
|
}
|
|
458
|
-
|
|
772
|
+
const formattedDocumentation = this.formatCode(documentation, outputPath);
|
|
773
|
+
fs.writeFileSync(outputPath, formattedDocumentation, 'utf8');
|
|
459
774
|
}
|
|
460
775
|
/**
|
|
461
776
|
* Saves the generated TypeScript types to a file
|
|
@@ -465,7 +780,8 @@ class SwaggerDocGenerator {
|
|
|
465
780
|
if (!fs.existsSync(dir)) {
|
|
466
781
|
fs.mkdirSync(dir, { recursive: true });
|
|
467
782
|
}
|
|
468
|
-
|
|
783
|
+
const formattedTypes = this.formatCode(types, outputPath);
|
|
784
|
+
fs.writeFileSync(outputPath, formattedTypes, 'utf8');
|
|
469
785
|
}
|
|
470
786
|
/**
|
|
471
787
|
* Saves the generated React hooks to files organized by tag
|
|
@@ -475,13 +791,21 @@ class SwaggerDocGenerator {
|
|
|
475
791
|
if (!fs.existsSync(dir)) {
|
|
476
792
|
fs.mkdirSync(dir, { recursive: true });
|
|
477
793
|
}
|
|
478
|
-
for (const [tag,
|
|
794
|
+
for (const [tag, { hooks, types }] of hooksByTag) {
|
|
479
795
|
const tagDir = path.join(outputDir, toCamelCase(tag));
|
|
480
796
|
if (!fs.existsSync(tagDir)) {
|
|
481
797
|
fs.mkdirSync(tagDir, { recursive: true });
|
|
482
798
|
}
|
|
483
|
-
|
|
484
|
-
|
|
799
|
+
// Save hooks to hooks file
|
|
800
|
+
const hooksFileName = path.join(tagDir, `${toCamelCase(tag)}.hooks.ts`);
|
|
801
|
+
const formattedHooks = this.formatCode(hooks, hooksFileName);
|
|
802
|
+
fs.writeFileSync(hooksFileName, formattedHooks, 'utf8');
|
|
803
|
+
// Save types to types file
|
|
804
|
+
if (types.trim()) { // Only save if there are types
|
|
805
|
+
const typesFileName = path.join(tagDir, `${toCamelCase(tag)}.types.ts`);
|
|
806
|
+
const formattedTypes = this.formatCode(types, typesFileName);
|
|
807
|
+
fs.writeFileSync(typesFileName, formattedTypes, 'utf8');
|
|
808
|
+
}
|
|
485
809
|
}
|
|
486
810
|
}
|
|
487
811
|
}
|