jszy-swagger-doc-generator 1.1.1 → 1.4.0
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 +364 -68
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/src/cli.ts +21 -5
- package/src/index.ts +401 -76
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { SwaggerDocGenerator, SwaggerDoc } from './index';
|
|
|
4
4
|
import yargs from 'yargs';
|
|
5
5
|
import { hideBin } from 'yargs/helpers';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
7
8
|
|
|
8
9
|
const argv = yargs(hideBin(process.argv))
|
|
9
10
|
.usage('Usage: $0 [options]')
|
|
@@ -57,11 +58,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
57
58
|
|
|
58
59
|
async function run(): Promise<void> {
|
|
59
60
|
try {
|
|
60
|
-
//
|
|
61
|
+
// Clean the entire generated directory before doing anything
|
|
61
62
|
const generatedDir = './generated';
|
|
62
|
-
if (
|
|
63
|
-
fs.
|
|
64
|
-
console.log(`
|
|
63
|
+
if (fs.existsSync(generatedDir)) {
|
|
64
|
+
fs.rmSync(generatedDir, { recursive: true, force: true });
|
|
65
|
+
console.log(`Cleared generated directory: ${generatedDir}`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const generator = new SwaggerDocGenerator();
|
|
@@ -85,6 +86,11 @@ async function run(): Promise<void> {
|
|
|
85
86
|
|
|
86
87
|
// Check if we need to generate types
|
|
87
88
|
if (argv.generateTypes) {
|
|
89
|
+
// Create the generated directory if it doesn't exist
|
|
90
|
+
if (!fs.existsSync(generatedDir)) {
|
|
91
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
console.log('Generating TypeScript type definitions...');
|
|
89
95
|
const types = generator.generateTypeDefinitions(swaggerDoc);
|
|
90
96
|
const typesOutputPath = argv.typesOutput.endsWith('.ts') ? argv.typesOutput :
|
|
@@ -95,14 +101,24 @@ async function run(): Promise<void> {
|
|
|
95
101
|
|
|
96
102
|
// Check if we need to generate hooks
|
|
97
103
|
if (argv.generateHooks) {
|
|
104
|
+
// Create the generated directory if it doesn't exist
|
|
105
|
+
if (!fs.existsSync(generatedDir)) {
|
|
106
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
console.log('Generating React hooks...');
|
|
99
110
|
const hooksByTag = generator.generateReactHooks(swaggerDoc);
|
|
100
111
|
generator.saveHooksByTag(hooksByTag, argv.hooksOutput);
|
|
101
|
-
console.log(`React hooks generated successfully in: ${argv.hooksOutput}/`);
|
|
112
|
+
console.log(`React hooks and types generated successfully in: ${argv.hooksOutput}/`);
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
// Generate documentation if not generating types or hooks (for backward compatibility)
|
|
105
116
|
if (!argv.generateTypes && !argv.generateHooks) {
|
|
117
|
+
// Create the generated directory if it doesn't exist
|
|
118
|
+
if (!fs.existsSync(generatedDir)) {
|
|
119
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
console.log('Generating documentation...');
|
|
107
123
|
const documentation = generator.generateDocumentation(swaggerDoc);
|
|
108
124
|
generator.saveDocumentationToFile(documentation, argv.output);
|
package/src/index.ts
CHANGED
|
@@ -345,10 +345,10 @@ export class SwaggerDocGenerator {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
* Generates React hooks from the paths in Swagger doc
|
|
348
|
+
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
349
349
|
*/
|
|
350
|
-
generateReactHooks(swaggerDoc: SwaggerDoc): Map<string, string> {
|
|
351
|
-
const hooksByTag = new Map<string, string>();
|
|
350
|
+
generateReactHooks(swaggerDoc: SwaggerDoc): Map<string, { hooks: string, types: string }> {
|
|
351
|
+
const hooksByTag = new Map<string, { hooks: string, types: string }>();
|
|
352
352
|
const schemas = swaggerDoc.components?.schemas || {};
|
|
353
353
|
|
|
354
354
|
// Group endpoints by tag
|
|
@@ -366,11 +366,73 @@ export class SwaggerDocGenerator {
|
|
|
366
366
|
});
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
-
// Generate hooks for each tag
|
|
369
|
+
// Generate hooks and types for each tag
|
|
370
370
|
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
371
|
-
|
|
371
|
+
// Generate TypeScript types for schemas used in this tag
|
|
372
|
+
let typesContent = `// ${toPascalCase(tag)} API Types\n\n`;
|
|
373
|
+
|
|
374
|
+
// First, find all directly used schemas in endpoints for this tag
|
|
375
|
+
const directlyUsedSchemas = new Set<string>();
|
|
376
|
+
if (schemas) {
|
|
377
|
+
Object.entries(schemas).forEach(([typeName, schema]) => {
|
|
378
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
379
|
+
directlyUsedSchemas.add(typeName);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Then, find all referenced schemas (schemas that are referenced by the directly used ones)
|
|
385
|
+
const allNeededSchemas = this.findAllReferencedSchemas(directlyUsedSchemas, schemas);
|
|
386
|
+
|
|
387
|
+
// Generate types for all needed schemas
|
|
388
|
+
if (schemas) {
|
|
389
|
+
for (const typeName of allNeededSchemas) {
|
|
390
|
+
const schema = schemas[typeName];
|
|
391
|
+
if (schema) {
|
|
392
|
+
typesContent += this.generateSingleTypeDefinition(typeName, schema, schemas);
|
|
393
|
+
typesContent += '\n';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Generate hooks content
|
|
399
|
+
let hooksContent = `// ${toPascalCase(tag)} API Hooks\n`;
|
|
400
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
401
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
402
|
+
|
|
403
|
+
// Determine which types are actually used in this tag's endpoints and generate imports
|
|
404
|
+
const usedTypeNames = new Set<string>();
|
|
405
|
+
|
|
406
|
+
// First, find all types that are directly used in endpoints for this tag
|
|
407
|
+
if (schemas) {
|
|
408
|
+
for (const [typeName, schema] of Object.entries(schemas)) {
|
|
409
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
410
|
+
usedTypeNames.add(typeName);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Then, add all transitive dependencies of the directly used types
|
|
416
|
+
const finalTypeNames = new Set<string>();
|
|
417
|
+
for (const typeName of usedTypeNames) {
|
|
418
|
+
finalTypeNames.add(typeName);
|
|
419
|
+
// Find all types referenced by this type
|
|
420
|
+
const referencedTypes = this.findSchemaReferences(schemas[typeName], schemas);
|
|
421
|
+
for (const refName of referencedTypes) {
|
|
422
|
+
if (schemas[refName]) {
|
|
423
|
+
finalTypeNames.add(refName);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
372
427
|
|
|
373
|
-
//
|
|
428
|
+
// Add import statement only if there are types to import
|
|
429
|
+
if (finalTypeNames.size > 0) {
|
|
430
|
+
hooksContent += `import type { ${Array.from(finalTypeNames).join(', ')} } from './${toCamelCase(tag)}.types';\n\n`;
|
|
431
|
+
} else {
|
|
432
|
+
hooksContent += `\n`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Generate parameter interfaces for this tag
|
|
374
436
|
const allParamInterfaces: string[] = [];
|
|
375
437
|
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
376
438
|
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
@@ -381,26 +443,201 @@ export class SwaggerDocGenerator {
|
|
|
381
443
|
|
|
382
444
|
// Add all unique parameter interfaces
|
|
383
445
|
allParamInterfaces.forEach(interfaceCode => {
|
|
384
|
-
|
|
446
|
+
hooksContent += interfaceCode + '\n';
|
|
385
447
|
});
|
|
386
448
|
|
|
387
|
-
// Generate individual hooks
|
|
449
|
+
// Generate individual hooks using react-query and axios
|
|
388
450
|
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
389
|
-
const hookContent = this.
|
|
390
|
-
|
|
451
|
+
const hookContent = this.generateReactQueryHook(path, method, endpointInfo, schemas);
|
|
452
|
+
hooksContent += hookContent + '\n';
|
|
391
453
|
});
|
|
392
454
|
|
|
393
|
-
hooksByTag.set(tag,
|
|
455
|
+
hooksByTag.set(tag, { hooks: hooksContent, types: typesContent });
|
|
394
456
|
});
|
|
395
457
|
|
|
396
458
|
return hooksByTag;
|
|
397
459
|
}
|
|
398
460
|
|
|
399
461
|
/**
|
|
400
|
-
*
|
|
462
|
+
* Checks if a schema is used in any of the endpoints
|
|
463
|
+
*/
|
|
464
|
+
isSchemaUsedInEndpoints(schemaName: string, endpoints: Array<{ path: string, method: string, endpointInfo: any }>, allSchemas: { [key: string]: any }): boolean {
|
|
465
|
+
for (const { endpointInfo } of endpoints) {
|
|
466
|
+
// Check if schema is used as response
|
|
467
|
+
if (endpointInfo.responses) {
|
|
468
|
+
for (const [, responseInfo] of Object.entries(endpointInfo.responses) as [string, any]) {
|
|
469
|
+
if (responseInfo.content) {
|
|
470
|
+
for (const [, contentInfo] of Object.entries(responseInfo.content) as [string, any]) {
|
|
471
|
+
if (contentInfo.schema) {
|
|
472
|
+
if (this.schemaContainsRef(contentInfo.schema, schemaName, allSchemas)) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check if schema is used in parameters
|
|
482
|
+
if (endpointInfo.parameters) {
|
|
483
|
+
for (const param of endpointInfo.parameters) {
|
|
484
|
+
if (param.schema && this.schemaContainsRef(param.schema, schemaName, allSchemas)) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Check if schema is used in request body
|
|
491
|
+
if (endpointInfo.requestBody && endpointInfo.requestBody.content) {
|
|
492
|
+
for (const [, contentInfo] of Object.entries(endpointInfo.requestBody.content) as [string, any]) {
|
|
493
|
+
if (contentInfo.schema && this.schemaContainsRef(contentInfo.schema, schemaName, allSchemas)) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Checks if a schema contains a reference to another schema
|
|
504
|
+
*/
|
|
505
|
+
schemaContainsRef(schema: any, targetSchemaName: string, allSchemas: { [key: string]: any }): boolean {
|
|
506
|
+
if (!schema) return false;
|
|
507
|
+
|
|
508
|
+
// Check if this schema directly references the target
|
|
509
|
+
if (schema.$ref) {
|
|
510
|
+
const refTypeName = schema.$ref.split('/').pop();
|
|
511
|
+
if (refTypeName === targetSchemaName) {
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Recursively check nested properties
|
|
517
|
+
if (schema.properties) {
|
|
518
|
+
for (const [, propSchema] of Object.entries(schema.properties)) {
|
|
519
|
+
if (this.schemaContainsRef(propSchema as any, targetSchemaName, allSchemas)) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Check if it's an array schema
|
|
526
|
+
if (schema.items) {
|
|
527
|
+
if (this.schemaContainsRef(schema.items, targetSchemaName, allSchemas)) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Check allOf, oneOf, anyOf
|
|
533
|
+
if (schema.allOf) {
|
|
534
|
+
for (const item of schema.allOf) {
|
|
535
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (schema.oneOf) {
|
|
542
|
+
for (const item of schema.oneOf) {
|
|
543
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (schema.anyOf) {
|
|
550
|
+
for (const item of schema.anyOf) {
|
|
551
|
+
if (this.schemaContainsRef(item, targetSchemaName, allSchemas)) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Find all referenced schemas from a set of directly used schemas
|
|
562
|
+
*/
|
|
563
|
+
findAllReferencedSchemas(initialSchemas: Set<string>, allSchemas: { [key: string]: any }): Set<string> {
|
|
564
|
+
const result = new Set<string>([...initialSchemas]); // Start with initial schemas
|
|
565
|
+
let changed = true;
|
|
566
|
+
|
|
567
|
+
while (changed) {
|
|
568
|
+
changed = false;
|
|
569
|
+
|
|
570
|
+
for (const typeName of [...result]) { // Use spread to create a new array to avoid concurrent modification
|
|
571
|
+
const schema = allSchemas[typeName];
|
|
572
|
+
if (schema) {
|
|
573
|
+
// Check for references in the schema
|
|
574
|
+
const referencedSchemas = this.findSchemaReferences(schema, allSchemas);
|
|
575
|
+
for (const refName of referencedSchemas) {
|
|
576
|
+
if (!result.has(refName) && allSchemas[refName]) {
|
|
577
|
+
result.add(refName);
|
|
578
|
+
changed = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Find schema references in a given schema
|
|
401
590
|
*/
|
|
402
|
-
|
|
403
|
-
|
|
591
|
+
findSchemaReferences(schema: any, allSchemas: { [key: string]: any }): Set<string> {
|
|
592
|
+
const references = new Set<string>();
|
|
593
|
+
|
|
594
|
+
if (!schema) return references;
|
|
595
|
+
|
|
596
|
+
// Check direct $ref
|
|
597
|
+
if (schema.$ref) {
|
|
598
|
+
const refTypeName = schema.$ref.split('/').pop();
|
|
599
|
+
if (refTypeName && allSchemas[refTypeName]) {
|
|
600
|
+
references.add(refTypeName);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Check properties
|
|
605
|
+
if (schema.properties) {
|
|
606
|
+
Object.values(schema.properties).forEach((propSchema: any) => {
|
|
607
|
+
const nestedRefs = this.findSchemaReferences(propSchema, allSchemas);
|
|
608
|
+
nestedRefs.forEach(ref => references.add(ref));
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Check array items
|
|
613
|
+
if (schema.items) {
|
|
614
|
+
const itemRefs = this.findSchemaReferences(schema.items, allSchemas);
|
|
615
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Check allOf, oneOf, anyOf
|
|
619
|
+
if (schema.allOf) {
|
|
620
|
+
schema.allOf.forEach((item: any) => {
|
|
621
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
622
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (schema.oneOf) {
|
|
627
|
+
schema.oneOf.forEach((item: any) => {
|
|
628
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
629
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (schema.anyOf) {
|
|
634
|
+
schema.anyOf.forEach((item: any) => {
|
|
635
|
+
const itemRefs = this.findSchemaReferences(item, allSchemas);
|
|
636
|
+
itemRefs.forEach(ref => references.add(ref));
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return references;
|
|
404
641
|
}
|
|
405
642
|
|
|
406
643
|
/**
|
|
@@ -443,29 +680,17 @@ export class SwaggerDocGenerator {
|
|
|
443
680
|
}
|
|
444
681
|
|
|
445
682
|
/**
|
|
446
|
-
* Generates a
|
|
683
|
+
* Generates a React Query hook using axios
|
|
447
684
|
*/
|
|
448
|
-
|
|
685
|
+
generateReactQueryHook(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
449
686
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
450
687
|
const hookName = `use${toPascalCase(operationId)}`;
|
|
688
|
+
const hookType = method.toLowerCase() === 'get' ? 'useQuery' : 'useMutation';
|
|
451
689
|
|
|
452
690
|
// Use unique parameter interface name
|
|
453
691
|
const pathParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'path') || [];
|
|
454
692
|
const queryParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'query') || [];
|
|
455
693
|
|
|
456
|
-
let paramsDeclaration = '';
|
|
457
|
-
let paramsUsage = '{}';
|
|
458
|
-
const hasParams = pathParams.length > 0 || queryParams.length > 0;
|
|
459
|
-
|
|
460
|
-
if (hasParams) {
|
|
461
|
-
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
462
|
-
paramsDeclaration = `params: ${paramInterfaceName}`;
|
|
463
|
-
paramsUsage = 'params';
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Format the path for use in the code (handle path parameters)
|
|
467
|
-
const pathWithParams = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
468
|
-
|
|
469
694
|
// Determine response type
|
|
470
695
|
let responseType = 'any';
|
|
471
696
|
if (endpointInfo.responses && endpointInfo.responses['200']) {
|
|
@@ -476,66 +701,155 @@ export class SwaggerDocGenerator {
|
|
|
476
701
|
}
|
|
477
702
|
|
|
478
703
|
// Generate request body parameter if needed
|
|
479
|
-
let
|
|
704
|
+
let requestBodyType = 'any';
|
|
705
|
+
let hasBody = false;
|
|
480
706
|
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
481
707
|
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
482
708
|
if (bodySchema) {
|
|
483
|
-
|
|
484
|
-
|
|
709
|
+
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
710
|
+
hasBody = true;
|
|
485
711
|
}
|
|
486
712
|
}
|
|
487
713
|
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
714
|
+
// Format the path for use in the code (handle path parameters)
|
|
715
|
+
const pathWithParams = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
716
|
+
const axiosPath = `\`\${process.env.REACT_APP_API_BASE_URL || ''}${pathWithParams}\``;
|
|
717
|
+
|
|
718
|
+
// Generate the hook code
|
|
719
|
+
let hookCode = '';
|
|
720
|
+
|
|
721
|
+
if (method.toLowerCase() === 'get') {
|
|
722
|
+
// For GET requests, use useQuery
|
|
723
|
+
const hasParams = pathParams.length > 0 || queryParams.length > 0;
|
|
724
|
+
if (hasParams) {
|
|
725
|
+
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
726
|
+
hookCode += `export const ${hookName} = (params: ${paramInterfaceName}) => {\n`;
|
|
727
|
+
hookCode += ` return useQuery({\n`;
|
|
728
|
+
hookCode += ` queryKey: ['${operationId}', params],\n`;
|
|
729
|
+
hookCode += ` queryFn: async () => {\n`;
|
|
730
|
+
hookCode += ` const response = await axios.get<${responseType}>(${axiosPath}, { params });\n`;
|
|
731
|
+
hookCode += ` return response.data;\n`;
|
|
732
|
+
hookCode += ` },\n`;
|
|
733
|
+
hookCode += ` });\n`;
|
|
734
|
+
hookCode += `};\n`;
|
|
735
|
+
} else {
|
|
736
|
+
hookCode += `export const ${hookName} = () => {\n`;
|
|
737
|
+
hookCode += ` return useQuery({\n`;
|
|
738
|
+
hookCode += ` queryKey: ['${operationId}'],\n`;
|
|
739
|
+
hookCode += ` queryFn: async () => {\n`;
|
|
740
|
+
hookCode += ` const response = await axios.get<${responseType}>(${axiosPath});\n`;
|
|
741
|
+
hookCode += ` return response.data;\n`;
|
|
742
|
+
hookCode += ` },\n`;
|
|
743
|
+
hookCode += ` });\n`;
|
|
744
|
+
hookCode += `};\n`;
|
|
745
|
+
}
|
|
503
746
|
} else {
|
|
504
|
-
|
|
747
|
+
// For non-GET requests, use useMutation
|
|
748
|
+
const hasPathParams = pathParams.length > 0;
|
|
749
|
+
if (hasPathParams) {
|
|
750
|
+
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
751
|
+
hookCode += `export const ${hookName} = () => {\n`;
|
|
752
|
+
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
753
|
+
hookCode += ` return useMutation({\n`;
|
|
754
|
+
hookCode += ` mutationFn: async ({ params, data }: { params: ${paramInterfaceName}; data: ${requestBodyType} }) => {\n`;
|
|
755
|
+
// Format the path for use in the code (handle path parameters)
|
|
756
|
+
let formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
757
|
+
const pathWithParams = `\`\${process.env.REACT_APP_API_BASE_URL || ''}${formattedPath}\``;
|
|
758
|
+
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${pathWithParams}, data);\n`;
|
|
759
|
+
hookCode += ` return response.data;\n`;
|
|
760
|
+
hookCode += ` },\n`;
|
|
761
|
+
hookCode += ` onSuccess: () => {\n`;
|
|
762
|
+
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
763
|
+
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
764
|
+
hookCode += ` },\n`;
|
|
765
|
+
hookCode += ` });\n`;
|
|
766
|
+
hookCode += `};\n`;
|
|
767
|
+
} else {
|
|
768
|
+
hookCode += `export const ${hookName} = () => {\n`;
|
|
769
|
+
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
770
|
+
hookCode += ` return useMutation({\n`;
|
|
771
|
+
hookCode += ` mutationFn: async (data: ${requestBodyType}) => {\n`;
|
|
772
|
+
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
773
|
+
hookCode += ` return response.data;\n`;
|
|
774
|
+
hookCode += ` },\n`;
|
|
775
|
+
hookCode += ` onSuccess: () => {\n`;
|
|
776
|
+
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
777
|
+
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
778
|
+
hookCode += ` },\n`;
|
|
779
|
+
hookCode += ` });\n`;
|
|
780
|
+
hookCode += `};\n`;
|
|
781
|
+
}
|
|
505
782
|
}
|
|
506
783
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
hookCode += ` method: '${method.toUpperCase()}',\n`;
|
|
510
|
-
|
|
511
|
-
if (requestBodyParam) {
|
|
512
|
-
hookCode += ` headers: {\n 'Content-Type': 'application/json',\n },\n`;
|
|
513
|
-
hookCode += ` body: JSON.stringify(body),\n`;
|
|
514
|
-
}
|
|
784
|
+
return hookCode;
|
|
785
|
+
}
|
|
515
786
|
|
|
516
|
-
hookCode += ` };\n\n`;
|
|
517
|
-
hookCode += ` const result = await fetch(url, options);\n`;
|
|
518
|
-
hookCode += ` return result.json() as Promise<${responseType}>;\n`;
|
|
519
|
-
hookCode += ` };\n\n`;
|
|
520
|
-
hookCode += ` return { ${toCamelCase(operationId)}: apiCall };\n`;
|
|
521
|
-
hookCode += `};\n`;
|
|
522
787
|
|
|
523
|
-
|
|
788
|
+
/**
|
|
789
|
+
* Generate operation ID from path and method if not provided
|
|
790
|
+
*/
|
|
791
|
+
generateOperationId(path: string, method: string): string {
|
|
792
|
+
return `${method.toLowerCase()}_${path.replace(/[\/{}]/g, '_')}`;
|
|
524
793
|
}
|
|
525
794
|
|
|
526
795
|
/**
|
|
527
|
-
*
|
|
796
|
+
* Formats code using Prettier - sync version with child process
|
|
528
797
|
*/
|
|
529
|
-
|
|
530
|
-
//
|
|
531
|
-
|
|
798
|
+
private formatCode(code: string, filepath: string): string {
|
|
799
|
+
// Skip formatting in test environment to avoid ESM issues
|
|
800
|
+
if (process.env.NODE_ENV === 'test' || typeof jest !== 'undefined') {
|
|
801
|
+
return code;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
// Use execSync to run prettier as a separate process to avoid ESM issues
|
|
806
|
+
const { execSync } = require('child_process');
|
|
807
|
+
const { writeFileSync, readFileSync, unlinkSync } = require('fs');
|
|
808
|
+
const { join, extname } = require('path');
|
|
809
|
+
const { tmpdir } = require('os');
|
|
810
|
+
|
|
811
|
+
// Determine the file extension to use for the temp file
|
|
812
|
+
const fileExtension = extname(filepath) || '.txt';
|
|
813
|
+
const tempPath = join(tmpdir(), `prettier-tmp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExtension}`);
|
|
814
|
+
writeFileSync(tempPath, code, 'utf8');
|
|
815
|
+
|
|
816
|
+
// Format the file using prettier CLI
|
|
817
|
+
execSync(`npx prettier --write "${tempPath}" --single-quote --trailing-comma es5 --tab-width 2 --semi --print-width 80`, {
|
|
818
|
+
stdio: 'pipe'
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// Read the formatted content back
|
|
822
|
+
const formattedCode = readFileSync(tempPath, 'utf8');
|
|
823
|
+
|
|
824
|
+
// Clean up the temporary file
|
|
825
|
+
unlinkSync(tempPath);
|
|
826
|
+
|
|
827
|
+
return formattedCode;
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.warn(`Failed to format ${filepath} with Prettier:`, error);
|
|
830
|
+
return code; // Return unformatted code if formatting fails
|
|
831
|
+
}
|
|
532
832
|
}
|
|
533
833
|
|
|
534
834
|
/**
|
|
535
|
-
*
|
|
835
|
+
* Gets the parser based on file extension
|
|
536
836
|
*/
|
|
537
|
-
|
|
538
|
-
|
|
837
|
+
private getParserForFile(filepath: string): string {
|
|
838
|
+
const ext = path.extname(filepath);
|
|
839
|
+
switch (ext) {
|
|
840
|
+
case '.ts':
|
|
841
|
+
case '.tsx':
|
|
842
|
+
return 'typescript';
|
|
843
|
+
case '.js':
|
|
844
|
+
case '.jsx':
|
|
845
|
+
return 'babel';
|
|
846
|
+
case '.json':
|
|
847
|
+
return 'json';
|
|
848
|
+
case '.md':
|
|
849
|
+
return 'markdown';
|
|
850
|
+
default:
|
|
851
|
+
return 'typescript';
|
|
852
|
+
}
|
|
539
853
|
}
|
|
540
854
|
|
|
541
855
|
/**
|
|
@@ -547,7 +861,8 @@ export class SwaggerDocGenerator {
|
|
|
547
861
|
fs.mkdirSync(dir, { recursive: true });
|
|
548
862
|
}
|
|
549
863
|
|
|
550
|
-
|
|
864
|
+
const formattedDocumentation = this.formatCode(documentation, outputPath);
|
|
865
|
+
fs.writeFileSync(outputPath, formattedDocumentation, 'utf8');
|
|
551
866
|
}
|
|
552
867
|
|
|
553
868
|
/**
|
|
@@ -559,26 +874,36 @@ export class SwaggerDocGenerator {
|
|
|
559
874
|
fs.mkdirSync(dir, { recursive: true });
|
|
560
875
|
}
|
|
561
876
|
|
|
562
|
-
|
|
877
|
+
const formattedTypes = this.formatCode(types, outputPath);
|
|
878
|
+
fs.writeFileSync(outputPath, formattedTypes, 'utf8');
|
|
563
879
|
}
|
|
564
880
|
|
|
565
881
|
/**
|
|
566
882
|
* Saves the generated React hooks to files organized by tag
|
|
567
883
|
*/
|
|
568
|
-
saveHooksByTag(hooksByTag: Map<string, string>, outputDir: string): void {
|
|
884
|
+
saveHooksByTag(hooksByTag: Map<string, { hooks: string, types: string }>, outputDir: string): void {
|
|
569
885
|
const dir = outputDir;
|
|
570
886
|
if (!fs.existsSync(dir)) {
|
|
571
887
|
fs.mkdirSync(dir, { recursive: true });
|
|
572
888
|
}
|
|
573
889
|
|
|
574
|
-
for (const [tag,
|
|
890
|
+
for (const [tag, { hooks, types }] of hooksByTag) {
|
|
575
891
|
const tagDir = path.join(outputDir, toCamelCase(tag));
|
|
576
892
|
if (!fs.existsSync(tagDir)) {
|
|
577
893
|
fs.mkdirSync(tagDir, { recursive: true });
|
|
578
894
|
}
|
|
579
895
|
|
|
580
|
-
|
|
581
|
-
|
|
896
|
+
// Save hooks to hooks file
|
|
897
|
+
const hooksFileName = path.join(tagDir, `${toCamelCase(tag)}.hooks.ts`);
|
|
898
|
+
const formattedHooks = this.formatCode(hooks, hooksFileName);
|
|
899
|
+
fs.writeFileSync(hooksFileName, formattedHooks, 'utf8');
|
|
900
|
+
|
|
901
|
+
// Save types to types file
|
|
902
|
+
if (types.trim()) { // Only save if there are types
|
|
903
|
+
const typesFileName = path.join(tagDir, `${toCamelCase(tag)}.types.ts`);
|
|
904
|
+
const formattedTypes = this.formatCode(types, typesFileName);
|
|
905
|
+
fs.writeFileSync(typesFileName, formattedTypes, 'utf8');
|
|
906
|
+
}
|
|
582
907
|
}
|
|
583
908
|
}
|
|
584
909
|
}
|