jszy-swagger-doc-generator 1.4.0 → 1.5.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.
@@ -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
+ }