zod-codegen 1.5.0 → 1.6.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.
Files changed (101) hide show
  1. package/.github/workflows/ci.yml +6 -0
  2. package/.github/workflows/release.yml +3 -3
  3. package/CHANGELOG.md +37 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/EXAMPLES.md +91 -12
  6. package/README.md +11 -4
  7. package/dist/scripts/add-js-extensions.d.ts +2 -0
  8. package/dist/scripts/add-js-extensions.d.ts.map +1 -0
  9. package/dist/scripts/add-js-extensions.js +66 -0
  10. package/dist/scripts/update-manifest.d.ts +14 -0
  11. package/dist/scripts/update-manifest.d.ts.map +1 -0
  12. package/dist/scripts/update-manifest.js +33 -0
  13. package/dist/src/assets/manifest.json +1 -1
  14. package/dist/src/cli.js +3 -3
  15. package/dist/src/generator.d.ts +46 -4
  16. package/dist/src/generator.d.ts.map +1 -1
  17. package/dist/src/generator.js +43 -1
  18. package/dist/src/interfaces/code-generator.d.ts +1 -1
  19. package/dist/src/interfaces/code-generator.d.ts.map +1 -1
  20. package/dist/src/services/code-generator.service.d.ts +5 -3
  21. package/dist/src/services/code-generator.service.d.ts.map +1 -1
  22. package/dist/src/services/code-generator.service.js +69 -1
  23. package/dist/src/services/file-reader.service.d.ts +2 -2
  24. package/dist/src/services/file-reader.service.d.ts.map +1 -1
  25. package/dist/src/services/file-writer.service.d.ts +1 -1
  26. package/dist/src/services/file-writer.service.d.ts.map +1 -1
  27. package/dist/src/services/import-builder.service.d.ts +1 -1
  28. package/dist/src/services/import-builder.service.d.ts.map +1 -1
  29. package/dist/src/services/import-builder.service.js +1 -1
  30. package/dist/src/services/type-builder.service.d.ts +1 -1
  31. package/dist/src/services/type-builder.service.d.ts.map +1 -1
  32. package/dist/src/types/generator-options.d.ts +1 -1
  33. package/dist/src/types/generator-options.d.ts.map +1 -1
  34. package/dist/src/utils/error-handler.d.ts +3 -2
  35. package/dist/src/utils/error-handler.d.ts.map +1 -1
  36. package/dist/src/utils/error-handler.js +4 -4
  37. package/dist/src/utils/reporter.d.ts +3 -2
  38. package/dist/src/utils/reporter.d.ts.map +1 -1
  39. package/dist/src/utils/reporter.js +7 -5
  40. package/dist/src/utils/signal-handler.d.ts +3 -2
  41. package/dist/src/utils/signal-handler.d.ts.map +1 -1
  42. package/dist/src/utils/signal-handler.js +4 -4
  43. package/examples/README.md +10 -1
  44. package/examples/petstore/README.md +6 -6
  45. package/examples/petstore/authenticated-usage.ts +1 -1
  46. package/examples/petstore/basic-usage.ts +1 -1
  47. package/examples/petstore/retry-handler-usage.ts +173 -0
  48. package/examples/petstore/server-variables-usage.ts +1 -1
  49. package/examples/petstore/type.ts +68 -47
  50. package/examples/pokeapi/README.md +3 -3
  51. package/examples/pokeapi/basic-usage.ts +1 -1
  52. package/examples/pokeapi/custom-client.ts +1 -1
  53. package/generated/type.ts +323 -0
  54. package/package.json +10 -13
  55. package/scripts/add-js-extensions.ts +79 -0
  56. package/scripts/update-manifest.ts +4 -2
  57. package/src/assets/manifest.json +1 -1
  58. package/src/cli.ts +7 -7
  59. package/src/generator.ts +51 -9
  60. package/src/interfaces/code-generator.ts +1 -1
  61. package/src/services/code-generator.service.ts +114 -8
  62. package/src/services/file-reader.service.ts +3 -3
  63. package/src/services/file-writer.service.ts +1 -1
  64. package/src/services/import-builder.service.ts +2 -2
  65. package/src/services/type-builder.service.ts +1 -1
  66. package/src/types/generator-options.ts +1 -1
  67. package/src/utils/error-handler.ts +6 -5
  68. package/src/utils/reporter.ts +6 -3
  69. package/src/utils/signal-handler.ts +10 -8
  70. package/tests/integration/cli-comprehensive.test.ts +123 -0
  71. package/tests/integration/cli.test.ts +2 -2
  72. package/tests/integration/error-scenarios.test.ts +240 -0
  73. package/tests/integration/snapshots.test.ts +131 -0
  74. package/tests/unit/code-generator-edge-cases.test.ts +551 -0
  75. package/tests/unit/code-generator.test.ts +385 -2
  76. package/tests/unit/file-reader.test.ts +16 -1
  77. package/tests/unit/generator.test.ts +19 -2
  78. package/tests/unit/naming-convention.test.ts +30 -1
  79. package/tests/unit/reporter.test.ts +63 -0
  80. package/tests/unit/type-builder.test.ts +131 -0
  81. package/tsconfig.json +3 -3
  82. package/dist/src/http/fetch-client.d.ts +0 -15
  83. package/dist/src/http/fetch-client.d.ts.map +0 -1
  84. package/dist/src/http/fetch-client.js +0 -140
  85. package/dist/src/polyfills/fetch.d.ts +0 -5
  86. package/dist/src/polyfills/fetch.d.ts.map +0 -1
  87. package/dist/src/polyfills/fetch.js +0 -18
  88. package/dist/src/types/http.d.ts +0 -25
  89. package/dist/src/types/http.d.ts.map +0 -1
  90. package/dist/src/types/http.js +0 -10
  91. package/dist/src/utils/manifest.d.ts +0 -8
  92. package/dist/src/utils/manifest.d.ts.map +0 -1
  93. package/dist/src/utils/manifest.js +0 -9
  94. package/dist/src/utils/tty.d.ts +0 -2
  95. package/dist/src/utils/tty.d.ts.map +0 -1
  96. package/dist/src/utils/tty.js +0 -3
  97. package/src/http/fetch-client.ts +0 -181
  98. package/src/polyfills/fetch.ts +0 -26
  99. package/src/types/http.ts +0 -35
  100. package/src/utils/manifest.ts +0 -17
  101. package/src/utils/tty.ts +0 -3
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "typescript": "^5.9.3",
18
18
  "url-pattern": "^1.0.3",
19
19
  "yargs": "^18.0.0",
20
- "zod": "^4.2.1"
20
+ "zod": "^4.3.5"
21
21
  },
22
22
  "description": "A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications",
23
23
  "keywords": [
@@ -32,8 +32,8 @@
32
32
  "validation"
33
33
  ],
34
34
  "devDependencies": {
35
- "@commitlint/cli": "^20.1.0",
36
- "@commitlint/config-conventional": "^20.0.0",
35
+ "@commitlint/cli": "^20.3.0",
36
+ "@commitlint/config-conventional": "^20.3.0",
37
37
  "@eslint/js": "^9.39.2",
38
38
  "@semantic-release/changelog": "^6.0.3",
39
39
  "@semantic-release/git": "^10.0.1",
@@ -41,9 +41,9 @@
41
41
  "@types/jest": "^30.0.0",
42
42
  "@types/js-yaml": "^4.0.9",
43
43
  "@types/jsonpath": "^0.2.4",
44
- "@types/node": "^25.0.2",
44
+ "@types/node": "^25.0.3",
45
45
  "@types/yargs": "^17.0.35",
46
- "@vitest/coverage-v8": "^4.0.14",
46
+ "@vitest/coverage-v8": "^4.0.16",
47
47
  "cross-env": "^10.1.0",
48
48
  "eslint": "^9.39.2",
49
49
  "eslint-config-prettier": "^10.1.8",
@@ -51,13 +51,10 @@
51
51
  "lint-staged": "^16.2.7",
52
52
  "semantic-release": "^25.0.2",
53
53
  "ts-node": "^10.9.2",
54
- "typescript-eslint": "^8.46.4",
54
+ "typescript-eslint": "^8.51.0",
55
55
  "undici": "^7.16.0",
56
56
  "vitest": "^4.0.14"
57
57
  },
58
- "optionalDependencies": {
59
- "undici": "^7.16.0"
60
- },
61
58
  "homepage": "https://github.com/julienandreu/zod-codegen",
62
59
  "license": "Apache-2.0",
63
60
  "name": "zod-codegen",
@@ -75,7 +72,7 @@
75
72
  "url": "git+ssh://git@github.com/julienandreu/zod-codegen.git"
76
73
  },
77
74
  "engines": {
78
- "node": ">=24.11.1"
75
+ "node": ">=18.0.0"
79
76
  },
80
77
  "overrides": {
81
78
  "npm": {
@@ -88,8 +85,8 @@
88
85
  },
89
86
  "scripts": {
90
87
  "audit:fix": "npm audit fix",
91
- "build": "NODE_OPTIONS='--no-deprecation' sh -c 'rm -rf dist && tsc --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js'",
92
- "build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
88
+ "build": "NODE_OPTIONS='--no-deprecation' sh -c 'rm -rf dist && tsc --project tsconfig.json && ts-node scripts/add-js-extensions.ts && mkdir -p dist/src/assets && cp -r src/assets/. dist/src/assets/ && chmod +x ./dist/src/cli.js'",
89
+ "build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && ts-node scripts/add-js-extensions.ts && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
93
90
  "build:watch": "tsc --project tsconfig.json --watch",
94
91
  "dev": "npm run build && node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ./examples/petstore && npm run format && npm run lint",
95
92
  "lint": "eslint src --fix",
@@ -110,5 +107,5 @@
110
107
  "release": "semantic-release",
111
108
  "release:dry": "semantic-release --dry-run"
112
109
  },
113
- "version": "1.5.0"
110
+ "version": "1.6.1"
114
111
  }
@@ -0,0 +1,79 @@
1
+ import {readdirSync, readFileSync, statSync, writeFileSync} from 'node:fs';
2
+ import {extname, join} from 'node:path';
3
+
4
+ /**
5
+ * Recursively finds all .js files in a directory
6
+ */
7
+ function findJsFiles(dir: string): string[] {
8
+ const files: string[] = [];
9
+ const entries = readdirSync(dir);
10
+
11
+ for (const entry of entries) {
12
+ const fullPath = join(dir, entry);
13
+ const stat = statSync(fullPath);
14
+
15
+ if (stat.isDirectory()) {
16
+ files.push(...findJsFiles(fullPath));
17
+ } else if (extname(entry) === '.js') {
18
+ files.push(fullPath);
19
+ }
20
+ }
21
+
22
+ return files;
23
+ }
24
+
25
+ /**
26
+ * Adds .js extensions to relative imports in a JavaScript file
27
+ */
28
+ function addJsExtensions(content: string): string {
29
+ // Match relative imports: './something' or '../something' but not './something.js' or node: imports
30
+ // Handles both single and double quotes
31
+ // Also handles type imports: import type { ... } from './something'
32
+ const importRegex = /from\s+(['"])(\.\.?\/[^'"]+?)(['"])/g;
33
+
34
+ return content.replace(importRegex, (match, quote: string, importPath: string, endQuote: string) => {
35
+ // Skip if already has an extension
36
+ if (importPath.endsWith('.js') || importPath.endsWith('.json')) {
37
+ return match;
38
+ }
39
+
40
+ // Add .js extension
41
+ return `from ${quote}${importPath}.js${endQuote}`;
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Main function to process all JavaScript files in dist/src
47
+ */
48
+ function main(): void {
49
+ const distDir = join(process.cwd(), 'dist', 'src');
50
+
51
+ try {
52
+ const jsFiles = findJsFiles(distDir);
53
+
54
+ if (jsFiles.length === 0) {
55
+ console.warn('No .js files found in dist/src');
56
+ process.exit(0);
57
+ }
58
+
59
+ let processedCount = 0;
60
+
61
+ for (const filePath of jsFiles) {
62
+ const content = readFileSync(filePath, 'utf-8');
63
+ const updatedContent = addJsExtensions(content);
64
+
65
+ if (content !== updatedContent) {
66
+ writeFileSync(filePath, updatedContent, 'utf-8');
67
+ processedCount++;
68
+ }
69
+ }
70
+
71
+ console.log(`✅ Added .js extensions to ${String(processedCount)} file(s)`);
72
+ process.exit(0);
73
+ } catch (error) {
74
+ console.error('❌ Error processing files:', error);
75
+ process.exit(1);
76
+ }
77
+ }
78
+
79
+ main();
@@ -1,5 +1,6 @@
1
- import {readFileSync, writeFileSync} from 'fs';
2
- import {resolve} from 'path';
1
+ import {readFileSync, writeFileSync} from 'node:fs';
2
+ import {dirname, resolve} from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
3
4
  import {z} from 'zod';
4
5
 
5
6
  interface PackageJson {
@@ -30,6 +31,7 @@ export function isPackageJson(input: unknown): input is PackageJson {
30
31
  return success;
31
32
  }
32
33
 
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
35
  const sourcePath = resolve(__dirname, '..', 'package.json');
34
36
 
35
37
  const data: unknown = JSON.parse(readFileSync(sourcePath, 'utf8'));
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "zod-codegen",
3
- "version": "1.0.1",
3
+ "version": "1.5.0",
4
4
  "description": "A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications"
5
5
  }
package/src/cli.ts CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  import yargs from 'yargs';
4
4
  import {hideBin} from 'yargs/helpers';
5
- import {Generator, type GeneratorOptions, type NamingConvention} from './generator.js';
5
+ import {Generator, type GeneratorOptions, type NamingConvention} from './generator';
6
6
  import {readFileSync} from 'node:fs';
7
7
  import {fileURLToPath} from 'node:url';
8
8
  import {dirname, join} from 'node:path';
9
9
 
10
10
  import loudRejection from 'loud-rejection';
11
- import {handleErrors} from './utils/error-handler.js';
12
- import {handleSignals} from './utils/signal-handler.js';
11
+ import {handleErrors} from './utils/error-handler';
12
+ import {handleSignals} from './utils/signal-handler';
13
13
  import debug from 'debug';
14
- import {Reporter} from './utils/reporter.js';
14
+ import {Reporter} from './utils/reporter';
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  // Read package.json from the project root
@@ -47,14 +47,14 @@ const packageData = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
47
47
  };
48
48
 
49
49
  const {name, description, version} = packageData;
50
- const reporter = new Reporter(process.stdout);
50
+ const reporter = new Reporter(process.stdout, process.stderr);
51
51
  const startTime = process.hrtime.bigint();
52
52
 
53
53
  debug(`${name}:${String(process.pid)}`);
54
54
 
55
55
  loudRejection();
56
- handleSignals(process, startTime);
57
- handleErrors(process, startTime);
56
+ handleSignals(process, startTime, reporter);
57
+ handleErrors(process, startTime, reporter);
58
58
 
59
59
  const argv = yargs(hideBin(process.argv))
60
60
  .scriptName(name)
package/src/generator.ts CHANGED
@@ -1,14 +1,41 @@
1
- import type {Reporter} from './utils/reporter.js';
2
- import type {OpenApiSpecType} from './types/openapi.js';
3
- import type {GeneratorOptions} from './types/generator-options.js';
4
- import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service.js';
5
- import {TypeScriptCodeGeneratorService} from './services/code-generator.service.js';
6
- import {SyncFileWriterService} from './services/file-writer.service.js';
1
+ import type {Reporter} from './utils/reporter';
2
+ import type {OpenApiSpecType} from './types/openapi';
3
+ import type {GeneratorOptions} from './types/generator-options';
4
+ import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service';
5
+ import {TypeScriptCodeGeneratorService} from './services/code-generator.service';
6
+ import {SyncFileWriterService} from './services/file-writer.service';
7
7
 
8
8
  // Re-export types for library users
9
- export type {GeneratorOptions} from './types/generator-options.js';
10
- export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention.js';
9
+ export type {GeneratorOptions} from './types/generator-options';
10
+ export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention';
11
11
 
12
+ /**
13
+ * Main generator class for creating TypeScript code from OpenAPI specifications.
14
+ *
15
+ * This class orchestrates the code generation process:
16
+ * 1. Reads the OpenAPI specification file (local or remote)
17
+ * 2. Parses and validates the specification
18
+ * 3. Generates TypeScript code with Zod schemas and type-safe API client
19
+ * 4. Writes the generated code to the output directory
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import {Generator} from 'zod-codegen';
24
+ * import {Reporter} from './utils/reporter';
25
+ *
26
+ * const reporter = new Reporter(process.stdout, process.stderr);
27
+ * const generator = new Generator(
28
+ * 'my-app',
29
+ * '1.0.0',
30
+ * reporter,
31
+ * './openapi.yaml',
32
+ * './generated',
33
+ * {namingConvention: 'camelCase'}
34
+ * );
35
+ *
36
+ * const exitCode = await generator.run();
37
+ * ```
38
+ */
12
39
  export class Generator {
13
40
  private readonly fileReader = new SyncFileReaderService();
14
41
  private readonly fileParser = new OpenApiFileParserService();
@@ -16,6 +43,16 @@ export class Generator {
16
43
  private readonly fileWriter: SyncFileWriterService;
17
44
  private readonly outputPath: string;
18
45
 
46
+ /**
47
+ * Creates a new Generator instance.
48
+ *
49
+ * @param _name - The name of the application/library (used in generated file headers)
50
+ * @param _version - The version of the application/library (used in generated file headers)
51
+ * @param reporter - Reporter instance for logging messages and errors
52
+ * @param inputPath - Path or URL to the OpenAPI specification file
53
+ * @param _outputDir - Directory where generated files will be written
54
+ * @param options - Optional configuration for code generation
55
+ */
19
56
  constructor(
20
57
  private readonly _name: string,
21
58
  private readonly _version: string,
@@ -29,6 +66,11 @@ export class Generator {
29
66
  this.codeGenerator = new TypeScriptCodeGeneratorService(options);
30
67
  }
31
68
 
69
+ /**
70
+ * Executes the code generation process.
71
+ *
72
+ * @returns Promise that resolves to an exit code (0 for success, 1 for failure)
73
+ */
32
74
  async run(): Promise<number> {
33
75
  try {
34
76
  const rawSource = await this.readFile();
@@ -46,7 +88,7 @@ export class Generator {
46
88
  this.reporter.error('❌ An unknown error occurred');
47
89
  }
48
90
 
49
- return Promise.resolve(1);
91
+ return 1;
50
92
  }
51
93
  }
52
94
 
@@ -1,4 +1,4 @@
1
- import type {OpenApiSpecType} from '../types/openapi.js';
1
+ import type {OpenApiSpecType} from '../types/openapi';
2
2
 
3
3
  export interface CodeGenerator {
4
4
  generate(spec: OpenApiSpecType): string;
@@ -1,18 +1,18 @@
1
1
  import jp from 'jsonpath';
2
2
  import * as ts from 'typescript';
3
3
  import {z} from 'zod';
4
- import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator.js';
5
- import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi.js';
6
- import type {GeneratorOptions} from '../types/generator-options.js';
7
- import {MethodSchema, Reference, SchemaProperties} from '../types/openapi.js';
8
- import {TypeScriptImportBuilderService} from './import-builder.service.js';
9
- import {TypeScriptTypeBuilderService} from './type-builder.service.js';
4
+ import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator';
5
+ import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi';
6
+ import type {GeneratorOptions} from '../types/generator-options';
7
+ import {MethodSchema, Reference, SchemaProperties} from '../types/openapi';
8
+ import {TypeScriptImportBuilderService} from './import-builder.service';
9
+ import {TypeScriptTypeBuilderService} from './type-builder.service';
10
10
  import {
11
11
  type NamingConvention,
12
12
  type OperationDetails,
13
13
  type OperationNameTransformer,
14
14
  transformNamingConvention,
15
- } from '../utils/naming-convention.js';
15
+ } from '../utils/naming-convention';
16
16
 
17
17
  export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuilder {
18
18
  private readonly typeBuilder = new TypeScriptTypeBuilderService();
@@ -161,6 +161,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
161
161
  this.typeBuilder.createProperty('#baseUrl', 'string', true),
162
162
  this.buildConstructor(openapi),
163
163
  this.buildGetBaseRequestOptionsMethod(),
164
+ this.buildHandleResponseMethod(),
164
165
  this.buildHttpRequestMethod(),
165
166
  ...methods,
166
167
  ],
@@ -290,6 +291,29 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
290
291
  );
291
292
  }
292
293
 
294
+ private buildHandleResponseMethod(): ts.MethodDeclaration {
295
+ return ts.factory.createMethodDeclaration(
296
+ [ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword), ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
297
+ undefined,
298
+ ts.factory.createIdentifier('handleResponse'),
299
+ undefined,
300
+ [this.typeBuilder.createGenericType('T')],
301
+ [
302
+ this.typeBuilder.createParameter('response', 'Response'),
303
+ this.typeBuilder.createParameter('method', 'string'),
304
+ this.typeBuilder.createParameter('path', 'string'),
305
+ this.typeBuilder.createParameter(
306
+ 'options',
307
+ '{params?: Record<string, string | number | boolean>; data?: unknown; contentType?: string; headers?: Record<string, string>}',
308
+ ),
309
+ ],
310
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
311
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Response'), undefined),
312
+ ]),
313
+ ts.factory.createBlock([ts.factory.createReturnStatement(ts.factory.createIdentifier('response'))], true),
314
+ );
315
+ }
316
+
293
317
  private buildHttpRequestMethod(): ts.MethodDeclaration {
294
318
  return ts.factory.createMethodDeclaration(
295
319
  [ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword), ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
@@ -812,7 +836,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
812
836
  ts.factory.createVariableDeclarationList(
813
837
  [
814
838
  ts.factory.createVariableDeclaration(
815
- ts.factory.createIdentifier('response'),
839
+ ts.factory.createIdentifier('rawResponse'),
816
840
  undefined,
817
841
  undefined,
818
842
  ts.factory.createAwaitExpression(
@@ -853,6 +877,35 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
853
877
  ts.NodeFlags.Const,
854
878
  ),
855
879
  ),
880
+ // Handle response through hook (allows subclasses to intercept and modify response)
881
+ ts.factory.createVariableStatement(
882
+ undefined,
883
+ ts.factory.createVariableDeclarationList(
884
+ [
885
+ ts.factory.createVariableDeclaration(
886
+ ts.factory.createIdentifier('response'),
887
+ undefined,
888
+ undefined,
889
+ ts.factory.createAwaitExpression(
890
+ ts.factory.createCallExpression(
891
+ ts.factory.createPropertyAccessExpression(
892
+ ts.factory.createThis(),
893
+ ts.factory.createIdentifier('handleResponse'),
894
+ ),
895
+ [ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('T'), undefined)],
896
+ [
897
+ ts.factory.createIdentifier('rawResponse'),
898
+ ts.factory.createIdentifier('method'),
899
+ ts.factory.createIdentifier('path'),
900
+ ts.factory.createIdentifier('options'),
901
+ ],
902
+ ),
903
+ ),
904
+ ),
905
+ ],
906
+ ts.NodeFlags.Const,
907
+ ),
908
+ ),
856
909
  // Check response status
857
910
  ts.factory.createIfStatement(
858
911
  ts.factory.createPrefixUnaryExpression(
@@ -907,6 +960,28 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
907
960
  openapi: OpenApiSpecType,
908
961
  schemas: Record<string, ts.VariableStatement>,
909
962
  ): ts.MethodDeclaration[] {
963
+ // Track operation IDs to detect duplicates
964
+ const operationIdMap = new Map<string, {method: string; path: string}[]>();
965
+
966
+ // First pass: collect all operation IDs and their methods/paths
967
+ Object.entries(openapi.paths).forEach(([path, pathItem]) => {
968
+ Object.entries(pathItem)
969
+ .filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method))
970
+ .forEach(([method, methodSchema]) => {
971
+ const safeMethodSchema = MethodSchema.parse(methodSchema);
972
+ if (safeMethodSchema.operationId) {
973
+ const operationId = safeMethodSchema.operationId;
974
+ const existing = operationIdMap.get(operationId);
975
+ if (existing) {
976
+ existing.push({method, path});
977
+ } else {
978
+ operationIdMap.set(operationId, [{method, path}]);
979
+ }
980
+ }
981
+ });
982
+ });
983
+
984
+ // Second pass: build methods, appending method name for HEAD/OPTIONS or when duplicates exist
910
985
  return Object.entries(openapi.paths).reduce<ts.MethodDeclaration[]>((endpoints, [path, pathItem]) => {
911
986
  const methods = Object.entries(pathItem)
912
987
  .filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method))
@@ -917,6 +992,26 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
917
992
  return null;
918
993
  }
919
994
 
995
+ const operationId = safeMethodSchema.operationId;
996
+ const methodLower = method.toLowerCase();
997
+
998
+ // Check if this operationId is used by multiple methods
999
+ const operations = operationIdMap.get(operationId);
1000
+ const hasDuplicates = operations !== undefined && operations.length > 1;
1001
+
1002
+ // For HEAD/OPTIONS or when duplicates exist, we need to ensure uniqueness
1003
+ // We'll handle this in transformOperationName by appending the method
1004
+ // But we need to mark it here so transformOperationName knows to append
1005
+ if (hasDuplicates || methodLower === 'head' || methodLower === 'options') {
1006
+ // Temporarily modify the operationId to include method for uniqueness
1007
+ // This will be handled in transformOperationName
1008
+ const modifiedSchema = {
1009
+ ...safeMethodSchema,
1010
+ operationId: `${operationId}_${methodLower}`,
1011
+ };
1012
+ return this.buildEndpointMethod(method, path, modifiedSchema, schemas);
1013
+ }
1014
+
920
1015
  return this.buildEndpointMethod(method, path, safeMethodSchema, schemas);
921
1016
  })
922
1017
  .filter((method): method is ts.MethodDeclaration => method !== null);
@@ -928,6 +1023,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
928
1023
  /**
929
1024
  * Transforms operation ID according to the configured naming convention or transformer
930
1025
  * Ensures the result is a valid TypeScript identifier
1026
+ * For HEAD and OPTIONS methods, appends the method name to ensure uniqueness when same operationId is used
931
1027
  */
932
1028
  private transformOperationName(operationId: string, method: string, path: string, schema: MethodSchemaType): string {
933
1029
  let transformed: string;
@@ -951,6 +1047,16 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
951
1047
  transformed = operationId;
952
1048
  }
953
1049
 
1050
+ // For HEAD and OPTIONS methods, append method name to ensure uniqueness
1051
+ // This prevents duplicate method names when GET and HEAD share the same operationId
1052
+ const methodLower = method.toLowerCase();
1053
+ if (methodLower === 'head' || methodLower === 'options') {
1054
+ // Only append if not already present to avoid double-appending
1055
+ if (!transformed.toLowerCase().endsWith(`_${methodLower}`)) {
1056
+ transformed = `${transformed}_${methodLower}`;
1057
+ }
1058
+ }
1059
+
954
1060
  // Sanitize to ensure valid TypeScript identifier (handles edge cases from custom transformers)
955
1061
  return this.typeBuilder.sanitizeIdentifier(transformed);
956
1062
  }
@@ -1,8 +1,8 @@
1
1
  import {readFileSync} from 'node:fs';
2
2
  import {load} from 'js-yaml';
3
- import type {OpenApiFileParser, OpenApiFileReader} from '../interfaces/file-reader.js';
4
- import type {OpenApiSpecType} from '../types/openapi.js';
5
- import {OpenApiSpec} from '../types/openapi.js';
3
+ import type {OpenApiFileParser, OpenApiFileReader} from '../interfaces/file-reader';
4
+ import type {OpenApiSpecType} from '../types/openapi';
5
+ import {OpenApiSpec} from '../types/openapi';
6
6
 
7
7
  export class SyncFileReaderService implements OpenApiFileReader {
8
8
  async readFile(path: string): Promise<string> {
@@ -1,6 +1,6 @@
1
1
  import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
2
2
  import {dirname, resolve} from 'node:path';
3
- import type {FileWriter} from '../interfaces/code-generator.js';
3
+ import type {FileWriter} from '../interfaces/code-generator';
4
4
 
5
5
  export class SyncFileWriterService implements FileWriter {
6
6
  constructor(
@@ -1,6 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  import {z} from 'zod';
3
- import type {ImportBuilder} from '../interfaces/code-generator.js';
3
+ import type {ImportBuilder} from '../interfaces/code-generator';
4
4
 
5
5
  const IsTypeImport = z.boolean();
6
6
  const ImportedElement = z.record(z.string(), IsTypeImport);
@@ -33,7 +33,7 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
33
33
  const namedImports =
34
34
  namedImportList.length > 0
35
35
  ? ts.factory.createNamedImports(
36
- namedImportList.map(([name, isTypeImport = false]) => {
36
+ namedImportList.map(([name, isTypeImport]) => {
37
37
  return ts.factory.createImportSpecifier(isTypeImport, undefined, ts.factory.createIdentifier(name));
38
38
  }),
39
39
  )
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import type {TypeBuilder} from '../interfaces/code-generator.js';
2
+ import type {TypeBuilder} from '../interfaces/code-generator';
3
3
 
4
4
  export class TypeScriptTypeBuilderService implements TypeBuilder {
5
5
  buildType(type: string): ts.TypeNode {
@@ -1,4 +1,4 @@
1
- import type {NamingConvention, OperationNameTransformer} from '../utils/naming-convention.js';
1
+ import type {NamingConvention, OperationNameTransformer} from '../utils/naming-convention';
2
2
 
3
3
  /**
4
4
  * Configuration options for the Generator class.
@@ -1,13 +1,14 @@
1
- import {getExecutionTime} from './execution-time.js';
1
+ import {getExecutionTime} from './execution-time';
2
+ import type {Reporter} from './reporter';
2
3
 
3
- export const errorReceived = (process: NodeJS.Process, startTime: bigint) => (): void => {
4
- console.log(`Done after ${String(getExecutionTime(startTime))}s`);
4
+ export const errorReceived = (process: NodeJS.Process, startTime: bigint, reporter: Reporter) => (): void => {
5
+ reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
5
6
  process.exit(1);
6
7
  };
7
8
 
8
- export const handleErrors = (process: NodeJS.Process, startTime: bigint): void => {
9
+ export const handleErrors = (process: NodeJS.Process, startTime: bigint, reporter: Reporter): void => {
9
10
  const catchErrors: string[] = ['unhandledRejection', 'uncaughtException'];
10
11
  catchErrors.forEach((event) => {
11
- process.on(event, errorReceived(process, startTime));
12
+ process.on(event, errorReceived(process, startTime, reporter));
12
13
  });
13
14
  };
@@ -1,16 +1,19 @@
1
1
  import {format} from 'node:util';
2
2
 
3
3
  export class Reporter {
4
- constructor(private readonly _stdout: NodeJS.WriteStream) {
4
+ constructor(
5
+ private readonly stdout: NodeJS.WriteStream,
6
+ private readonly stderr: NodeJS.WriteStream = stdout,
7
+ ) {
5
8
  this.log = this.log.bind(this);
6
9
  this.error = this.error.bind(this);
7
10
  }
8
11
 
9
12
  log(...args: readonly unknown[]): void {
10
- this._stdout.write(format(...args) + '\n');
13
+ this.stdout.write(format(...args) + '\n');
11
14
  }
12
15
 
13
16
  error(...args: readonly unknown[]): void {
14
- this._stdout.write(format(...args) + '\n');
17
+ this.stderr.write(format(...args) + '\n');
15
18
  }
16
19
  }
@@ -1,14 +1,16 @@
1
- import {getExecutionTime} from './execution-time.js';
1
+ import {getExecutionTime} from './execution-time';
2
+ import type {Reporter} from './reporter';
2
3
 
3
- export const signalReceived = (process: NodeJS.Process, startTime: bigint, event: NodeJS.Signals) => (): void => {
4
- console.log(`Done after ${String(getExecutionTime(startTime))}s`);
5
- process.kill(process.pid, event);
6
- process.exit(1);
7
- };
4
+ export const signalReceived =
5
+ (process: NodeJS.Process, startTime: bigint, event: NodeJS.Signals, reporter: Reporter) => (): void => {
6
+ reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
7
+ process.kill(process.pid, event);
8
+ process.exit(1);
9
+ };
8
10
 
9
- export const handleSignals = (process: NodeJS.Process, startTime: bigint): void => {
11
+ export const handleSignals = (process: NodeJS.Process, startTime: bigint, reporter: Reporter): void => {
10
12
  const catchSignals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
11
13
  catchSignals.forEach((event) => {
12
- process.once(event, signalReceived(process, startTime, event));
14
+ process.once(event, signalReceived(process, startTime, event, reporter));
13
15
  });
14
16
  };