zod-codegen 1.6.3 → 1.7.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.
Files changed (119) hide show
  1. package/.github/workflows/ci.yml +50 -48
  2. package/.github/workflows/release.yml +13 -3
  3. package/.husky/commit-msg +1 -1
  4. package/.husky/pre-commit +1 -1
  5. package/.lintstagedrc.json +5 -1
  6. package/.nvmrc +1 -1
  7. package/.prettierrc.json +12 -5
  8. package/CHANGELOG.md +11 -0
  9. package/CONTRIBUTING.md +12 -12
  10. package/EXAMPLES.md +135 -57
  11. package/PERFORMANCE.md +4 -4
  12. package/README.md +87 -64
  13. package/SECURITY.md +1 -1
  14. package/dist/src/cli.js +11 -18
  15. package/dist/src/generator.d.ts +2 -2
  16. package/dist/src/generator.d.ts.map +1 -1
  17. package/dist/src/generator.js +5 -3
  18. package/dist/src/interfaces/code-generator.d.ts.map +1 -1
  19. package/dist/src/services/code-generator.service.d.ts +3 -1
  20. package/dist/src/services/code-generator.service.d.ts.map +1 -1
  21. package/dist/src/services/code-generator.service.js +236 -219
  22. package/dist/src/services/file-reader.service.d.ts.map +1 -1
  23. package/dist/src/services/file-reader.service.js +1 -1
  24. package/dist/src/services/file-writer.service.d.ts.map +1 -1
  25. package/dist/src/services/file-writer.service.js +2 -2
  26. package/dist/src/services/import-builder.service.d.ts.map +1 -1
  27. package/dist/src/services/import-builder.service.js +3 -3
  28. package/dist/src/services/type-builder.service.d.ts.map +1 -1
  29. package/dist/src/types/generator-options.d.ts.map +1 -1
  30. package/dist/src/types/openapi.d.ts.map +1 -1
  31. package/dist/src/types/openapi.js +20 -20
  32. package/dist/src/utils/error-handler.d.ts.map +1 -1
  33. package/dist/src/utils/naming-convention.d.ts.map +1 -1
  34. package/dist/src/utils/naming-convention.js +6 -3
  35. package/dist/src/utils/signal-handler.d.ts.map +1 -1
  36. package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
  37. package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
  38. package/dist/tests/integration/cli-comprehensive.test.js +110 -0
  39. package/dist/tests/integration/cli.test.d.ts +2 -0
  40. package/dist/tests/integration/cli.test.d.ts.map +1 -0
  41. package/dist/tests/integration/cli.test.js +25 -0
  42. package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
  43. package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
  44. package/dist/tests/integration/error-scenarios.test.js +169 -0
  45. package/dist/tests/integration/snapshots.test.d.ts +2 -0
  46. package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
  47. package/dist/tests/integration/snapshots.test.js +100 -0
  48. package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
  49. package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
  50. package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
  51. package/dist/tests/unit/code-generator.test.d.ts +2 -0
  52. package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
  53. package/dist/tests/unit/code-generator.test.js +1364 -0
  54. package/dist/tests/unit/file-reader.test.d.ts +2 -0
  55. package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
  56. package/dist/tests/unit/file-reader.test.js +125 -0
  57. package/dist/tests/unit/generator.test.d.ts +2 -0
  58. package/dist/tests/unit/generator.test.d.ts.map +1 -0
  59. package/dist/tests/unit/generator.test.js +119 -0
  60. package/dist/tests/unit/naming-convention.test.d.ts +2 -0
  61. package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
  62. package/dist/tests/unit/naming-convention.test.js +256 -0
  63. package/dist/tests/unit/reporter.test.d.ts +2 -0
  64. package/dist/tests/unit/reporter.test.d.ts.map +1 -0
  65. package/dist/tests/unit/reporter.test.js +44 -0
  66. package/dist/tests/unit/type-builder.test.d.ts +2 -0
  67. package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
  68. package/dist/tests/unit/type-builder.test.js +108 -0
  69. package/dist/vitest.config.d.ts.map +1 -1
  70. package/dist/vitest.config.js +10 -20
  71. package/eslint.config.mjs +38 -28
  72. package/examples/.gitkeep +1 -1
  73. package/examples/README.md +4 -2
  74. package/examples/petstore/README.md +18 -17
  75. package/examples/petstore/{type.ts → api.ts} +158 -74
  76. package/examples/petstore/authenticated-usage.ts +6 -4
  77. package/examples/petstore/basic-usage.ts +4 -3
  78. package/examples/petstore/error-handling-usage.ts +84 -0
  79. package/examples/petstore/retry-handler-usage.ts +11 -18
  80. package/examples/petstore/server-variables-usage.ts +10 -10
  81. package/examples/pokeapi/README.md +8 -8
  82. package/examples/pokeapi/api.ts +218 -0
  83. package/examples/pokeapi/basic-usage.ts +3 -2
  84. package/examples/pokeapi/custom-client.ts +5 -4
  85. package/package.json +17 -21
  86. package/src/cli.ts +20 -25
  87. package/src/generator.ts +13 -11
  88. package/src/interfaces/code-generator.ts +1 -1
  89. package/src/services/code-generator.service.ts +799 -1120
  90. package/src/services/file-reader.service.ts +6 -5
  91. package/src/services/file-writer.service.ts +7 -7
  92. package/src/services/import-builder.service.ts +9 -13
  93. package/src/services/type-builder.service.ts +8 -19
  94. package/src/types/generator-options.ts +1 -1
  95. package/src/types/openapi.ts +22 -22
  96. package/src/utils/error-handler.ts +2 -2
  97. package/src/utils/naming-convention.ts +13 -10
  98. package/src/utils/reporter.ts +2 -2
  99. package/src/utils/signal-handler.ts +7 -8
  100. package/tests/integration/cli-comprehensive.test.ts +38 -32
  101. package/tests/integration/cli.test.ts +5 -5
  102. package/tests/integration/error-scenarios.test.ts +20 -26
  103. package/tests/integration/snapshots.test.ts +19 -23
  104. package/tests/unit/code-generator-edge-cases.test.ts +133 -133
  105. package/tests/unit/code-generator.test.ts +431 -330
  106. package/tests/unit/file-reader.test.ts +14 -14
  107. package/tests/unit/generator.test.ts +30 -18
  108. package/tests/unit/naming-convention.test.ts +27 -27
  109. package/tests/unit/type-builder.test.ts +2 -2
  110. package/tsconfig.json +5 -3
  111. package/vitest.config.ts +11 -21
  112. package/dist/scripts/update-manifest.d.ts +0 -14
  113. package/dist/scripts/update-manifest.d.ts.map +0 -1
  114. package/dist/scripts/update-manifest.js +0 -33
  115. package/dist/src/assets/manifest.json +0 -5
  116. package/examples/pokeapi/type.ts +0 -109
  117. package/generated/type.ts +0 -371
  118. package/scripts/update-manifest.ts +0 -49
  119. package/src/assets/manifest.json +0 -5
@@ -7,48 +7,48 @@
7
7
  * For a real example, generate a client from an OpenAPI spec with server variables.
8
8
  */
9
9
 
10
- import {SwaggerPetstoreOpenAPI30, ClientOptions} from './type';
10
+ import SwaggerPetstoreOpenAPI30 from './api';
11
11
 
12
12
  async function main() {
13
13
  // Example 1: Use default server (first server from OpenAPI spec)
14
14
  console.log('📡 Example 1: Using default server\n');
15
- const defaultClient = new SwaggerPetstoreOpenAPI30({});
15
+ const _defaultClient = new SwaggerPetstoreOpenAPI30({});
16
16
  console.log('Default client created');
17
17
 
18
18
  // Example 2: Override with custom baseUrl
19
19
  console.log('\n📡 Example 2: Overriding with custom baseUrl\n');
20
- const customClient = new SwaggerPetstoreOpenAPI30({
21
- baseUrl: 'https://custom-api.example.com/v3',
20
+ const _customClient = new SwaggerPetstoreOpenAPI30({
21
+ baseUrl: 'https://custom-api.example.com/v3'
22
22
  });
23
23
  console.log('Custom client created');
24
24
 
25
25
  // Example 3: Select different server by index (if multiple servers exist)
26
26
  console.log('\n📡 Example 3: Selecting server by index\n');
27
- const indexedClient = new SwaggerPetstoreOpenAPI30({
28
- serverIndex: 0, // Use first server
27
+ const _indexedClient = new SwaggerPetstoreOpenAPI30({
28
+ serverIndex: 0 // Use first server
29
29
  });
30
30
  console.log('Indexed client created');
31
31
 
32
32
  // Example 4: Using server variables (if your OpenAPI spec has templated URLs)
33
33
  // For example: https://{environment}.example.com:{port}/v{version}
34
34
  console.log('\n📡 Example 4: Using server variables\n');
35
- const variableClient = new SwaggerPetstoreOpenAPI30({
35
+ const _variableClient = new SwaggerPetstoreOpenAPI30({
36
36
  serverIndex: 0,
37
37
  serverVariables: {
38
38
  // environment: 'api.staging', // Uncomment if your spec has these variables
39
39
  // port: '8443',
40
40
  // version: '2',
41
- },
41
+ }
42
42
  });
43
43
  console.log('Variable client created');
44
44
 
45
45
  // Example 5: Combining server selection with custom baseUrl override
46
46
  console.log('\n📡 Example 5: Combining options\n');
47
- const combinedClient = new SwaggerPetstoreOpenAPI30({
47
+ const _combinedClient = new SwaggerPetstoreOpenAPI30({
48
48
  serverIndex: 0,
49
49
  serverVariables: {
50
50
  // Add variables here if your spec supports them
51
- },
51
+ }
52
52
  // baseUrl takes precedence if provided
53
53
  // baseUrl: 'https://override.example.com',
54
54
  });
@@ -15,10 +15,10 @@ zod-codegen --input ./samples/pokeapi-openapi.json --output ./examples/pokeapi
15
15
  ## Basic Usage
16
16
 
17
17
  ```typescript
18
- import {PokAPI, defaultBaseUrl} from './type';
18
+ import PokAPI from './api';
19
19
 
20
20
  // Create a client instance
21
- const client = new PokAPI(defaultBaseUrl);
21
+ const client = new PokAPI({});
22
22
 
23
23
  // Use the generated methods
24
24
  const pokemon = await client.getPokemonById('pikachu');
@@ -30,10 +30,10 @@ console.log('Pokemon:', pokemon);
30
30
  See [basic-usage.ts](./basic-usage.ts) for a complete example:
31
31
 
32
32
  ```typescript
33
- import {PokAPI, defaultBaseUrl} from './type';
33
+ import PokAPI from './api';
34
34
 
35
35
  async function getPokemonInfo() {
36
- const client = new PokAPI(defaultBaseUrl);
36
+ const client = new PokAPI({});
37
37
 
38
38
  try {
39
39
  // Get a specific Pokémon by ID or name
@@ -73,7 +73,7 @@ npx ts-node examples/pokeapi/basic-usage.ts
73
73
  See [custom-client.ts](./custom-client.ts) for a complete example:
74
74
 
75
75
  ```typescript
76
- import {PokAPI, defaultBaseUrl} from './type';
76
+ import PokAPI from './api';
77
77
 
78
78
  class CustomPokeAPI extends PokAPI {
79
79
  protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>> {
@@ -83,15 +83,15 @@ class CustomPokeAPI extends PokAPI {
83
83
  headers: {
84
84
  ...((options.headers as Record<string, string>) || {}),
85
85
  'User-Agent': 'MyPokemonApp/1.0.0',
86
- Accept: 'application/json',
86
+ 'Accept': 'application/json'
87
87
  },
88
88
  mode: 'cors',
89
- cache: 'default',
89
+ cache: 'default'
90
90
  };
91
91
  }
92
92
  }
93
93
 
94
- const client = new CustomPokeAPI(defaultBaseUrl);
94
+ const client = new CustomPokeAPI({});
95
95
  ```
96
96
 
97
97
  **Run the example:**
@@ -0,0 +1,218 @@
1
+ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
+ // Built with zod-codegen@1.6.3
3
+ // Latest edit: Mon, 09 Mar 2026 20:11:48 GMT
4
+ // Source file: ./samples/pokeapi-openapi.json
5
+ /* eslint-disable */
6
+ // @ts-nocheck
7
+
8
+ // Imports
9
+ import { z } from 'zod';
10
+
11
+ // Explicit type declarations
12
+ export interface NamedAPIResource {
13
+ name: string;
14
+ url: string;
15
+ }
16
+ export interface PokemonType {
17
+ slot?: number;
18
+ type?: NamedAPIResource;
19
+ }
20
+ export interface PokemonAbility {
21
+ ability?: NamedAPIResource;
22
+ is_hidden?: boolean;
23
+ slot?: number;
24
+ }
25
+ export interface PokemonSprites {
26
+ front_default?: string;
27
+ front_shiny?: string;
28
+ back_default?: string;
29
+ back_shiny?: string;
30
+ }
31
+ export interface Pokemon {
32
+ id: number;
33
+ name: string;
34
+ height?: number;
35
+ weight?: number;
36
+ base_experience?: number;
37
+ types?: PokemonType[];
38
+ abilities?: PokemonAbility[];
39
+ sprites?: PokemonSprites;
40
+ }
41
+ export interface PokemonListResponse {
42
+ count: number;
43
+ next?: string | null;
44
+ previous?: string | null;
45
+ results: NamedAPIResource[];
46
+ }
47
+
48
+ // Components schemas
49
+ export const NamedAPIResource: z.ZodType<NamedAPIResource> = z.object({
50
+ name: z.string(),
51
+ url: z.url()
52
+ });
53
+ export const PokemonType: z.ZodType<PokemonType> = z.object({
54
+ slot: z.number().int().optional(),
55
+ type: NamedAPIResource.optional()
56
+ });
57
+ export const PokemonAbility: z.ZodType<PokemonAbility> = z.object({
58
+ ability: NamedAPIResource.optional(),
59
+ is_hidden: z.boolean().optional(),
60
+ slot: z.number().int().optional()
61
+ });
62
+ export const PokemonSprites: z.ZodType<PokemonSprites> = z.object({
63
+ front_default: z.url().optional(),
64
+ front_shiny: z.url().optional(),
65
+ back_default: z.url().optional(),
66
+ back_shiny: z.url().optional()
67
+ });
68
+ export const Pokemon: z.ZodType<Pokemon> = z.object({
69
+ id: z.number().int(),
70
+ name: z.string(),
71
+ height: z.number().int().optional(),
72
+ weight: z.number().int().optional(),
73
+ base_experience: z.number().int().optional(),
74
+ types: z.array(PokemonType).optional(),
75
+ abilities: z.array(PokemonAbility).optional(),
76
+ sprites: PokemonSprites.optional()
77
+ });
78
+ export const PokemonListResponse: z.ZodType<PokemonListResponse> = z.object({
79
+ count: z.number().int(),
80
+ next: z.url().optional(),
81
+ previous: z.url().optional(),
82
+ results: z.array(NamedAPIResource)
83
+ });
84
+ export const serverConfigurations = [
85
+ {
86
+ url: 'https://pokeapi.co/api/v2',
87
+ description: 'Pok\u00E9API production server'
88
+ }
89
+ ];
90
+ export const defaultBaseUrl = 'https://pokeapi.co/api/v2';
91
+ export type ClientOptions = {
92
+ baseUrl?: string;
93
+ serverIndex?: number;
94
+ serverVariables?: Record<string, string>;
95
+ };
96
+ function resolveServerUrl(serverIndex?: number | undefined, serverVariables?: Record<string, string> = {}): string {
97
+ const configs = [
98
+ {
99
+ url: 'https://pokeapi.co/api/v2'
100
+ }
101
+ ];
102
+ const idx = serverIndex ?? 0;
103
+ if (idx < configs.length) {
104
+ const config = configs[idx];
105
+ let url = config.url;
106
+ if (config.variables && serverVariables) {
107
+ for (const [key, value] of Object.entries(serverVariables)) {
108
+ url = url.replace(new RegExp('\\{' + key + '\\}', 'g'), value);
109
+ }
110
+ }
111
+ return url;
112
+ }
113
+ return 'https://pokeapi.co/api/v2';
114
+ }
115
+ export class ResponseValidationError<T> extends Error {
116
+ readonly response: Response;
117
+ readonly error: z.ZodError<T>;
118
+ constructor(message: string, response: Response, error: z.ZodError<T>) {
119
+ super(message);
120
+ this.name = 'ResponseValidationError' as const;
121
+ this.response = response;
122
+ this.error = error;
123
+ }
124
+ get data(): T {
125
+ return this.response.json() as T;
126
+ }
127
+ }
128
+
129
+ // Client class
130
+ export default class PokAPI {
131
+ readonly #baseUrl: string;
132
+ constructor(options: ClientOptions) {
133
+ const resolvedUrl = options.baseUrl !== null ? options.baseUrl : resolveServerUrl(options.serverIndex, options.serverVariables);
134
+ this.#baseUrl = resolvedUrl;
135
+ }
136
+ protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>> {
137
+ return {};
138
+ }
139
+ protected async handleResponse<T>(
140
+ response: Response,
141
+ method: string,
142
+ path: string,
143
+ options: _params___Record_string__string___number___boolean___data___unknown__contentType___string__headers___Record_string__string__
144
+ ): Promise<Response> {
145
+ return response;
146
+ }
147
+ protected async makeRequest<T>(
148
+ method: string,
149
+ path: string,
150
+ options: _params___Record_string__string___number___boolean___data___unknown__contentType___string__headers___Record_string__string__ = {}
151
+ ): Promise<T> {
152
+ const baseUrl = new URL(path, this.#baseUrl);
153
+ const url =
154
+ options.params && Object.keys(options.params).length > 0
155
+ ? (() => {
156
+ Object.entries(options.params)
157
+ .filter(([, value]) => value !== undefined)
158
+ .forEach(([key, value]) => {
159
+ baseUrl.searchParams.set(key, String(value));
160
+ });
161
+ return baseUrl.toString();
162
+ })()
163
+ : baseUrl.toString();
164
+ const baseOptions = this.getBaseRequestOptions();
165
+ const contentType = options.contentType === 'application/x-www-form-urlencoded' ? 'application/x-www-form-urlencoded' : 'application/json';
166
+ const baseHeaders = baseOptions.headers !== undefined ? baseOptions.headers : {};
167
+ const headers = Object.assign({}, baseHeaders, { 'Content-Type': contentType }, options.headers !== undefined ? options.headers : {});
168
+ const body =
169
+ options.data !== undefined
170
+ ? options.contentType === 'application/x-www-form-urlencoded'
171
+ ? (() => {
172
+ const params = new URLSearchParams();
173
+ Object.entries(options.data).forEach(([key, value]) => {
174
+ params.set(key, String(value));
175
+ });
176
+ return params.toString();
177
+ })()
178
+ : JSON.stringify(options.data)
179
+ : null;
180
+ const rawResponse = await fetch(url, Object.assign({}, baseOptions, { method, headers: headers, body: body }));
181
+ const response = await this.handleResponse<T>(rawResponse, method, path, options);
182
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
183
+ return await response.json();
184
+ }
185
+ /**
186
+ * Get Pokémon by ID or name
187
+ *
188
+ * @param id Pokémon ID or name
189
+ * @returns {Pokemon}
190
+ */
191
+ async getPokemonById(id: string): Promise<Pokemon> {
192
+ const response = await this.makeRequest('GET', `/pokemon/${id}`, {});
193
+ const parsedPokemon = Pokemon.safeParse(response);
194
+ if (!parsedPokemon.success) {
195
+ throw new ResponseValidationError<Pokemon>(`Invalid pokemon: ${parsedPokemon.error.errors.map((error) => error.message).join(', ')}`, response, parsedPokemon.error);
196
+ }
197
+ return parsedPokemon.data;
198
+ }
199
+ /**
200
+ * Get list of Pokémon
201
+ *
202
+ * @param limit
203
+ * @param offset
204
+ * @returns {PokemonListResponse}
205
+ */
206
+ async getPokemonList(limit?: number, offset?: number): Promise<PokemonListResponse> {
207
+ const response = await this.makeRequest('GET', '/pokemon', { params: { limit: limit, offset: offset } });
208
+ const parsedPokemonListResponse = PokemonListResponse.safeParse(response);
209
+ if (!parsedPokemonListResponse.success) {
210
+ throw new ResponseValidationError<PokemonListResponse>(
211
+ `Invalid pokemonListResponse: ${parsedPokemonListResponse.error.errors.map((error) => error.message).join(', ')}`,
212
+ response,
213
+ parsedPokemonListResponse.error
214
+ );
215
+ }
216
+ return parsedPokemonListResponse.data;
217
+ }
218
+ }
@@ -4,10 +4,10 @@
4
4
  * Run with: npx ts-node examples/pokeapi/basic-usage.ts
5
5
  */
6
6
 
7
- import {PokAPI, defaultBaseUrl} from './type';
7
+ import PokAPI from './api';
8
8
 
9
9
  async function main() {
10
- const client = new PokAPI(defaultBaseUrl);
10
+ const client = new PokAPI({});
11
11
 
12
12
  try {
13
13
  console.log('🔍 Fetching Pokémon data...\n');
@@ -50,6 +50,7 @@ async function main() {
50
50
  } else {
51
51
  console.error('❌ Unknown error:', error);
52
52
  }
53
+
53
54
  process.exit(1);
54
55
  }
55
56
  }
@@ -4,7 +4,7 @@
4
4
  * Run with: npx ts-node examples/pokeapi/custom-client.ts
5
5
  */
6
6
 
7
- import {PokAPI, defaultBaseUrl} from './type';
7
+ import PokAPI from './api';
8
8
 
9
9
  class CustomPokeAPIClient extends PokAPI {
10
10
  protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>> {
@@ -14,16 +14,16 @@ class CustomPokeAPIClient extends PokAPI {
14
14
  headers: {
15
15
  ...((options.headers as Record<string, string>) || {}),
16
16
  'User-Agent': 'MyPokemonApp/1.0.0 (https://myapp.com)',
17
- Accept: 'application/json',
17
+ 'Accept': 'application/json'
18
18
  },
19
19
  mode: 'cors',
20
- cache: 'default',
20
+ cache: 'default'
21
21
  };
22
22
  }
23
23
  }
24
24
 
25
25
  async function main() {
26
- const client = new CustomPokeAPIClient(defaultBaseUrl);
26
+ const client = new CustomPokeAPIClient({});
27
27
 
28
28
  try {
29
29
  console.log('🔍 Fetching Pokémon with custom client...\n');
@@ -49,6 +49,7 @@ async function main() {
49
49
  } else {
50
50
  console.error('❌ Unknown error:', error);
51
51
  }
52
+
52
53
  process.exit(1);
53
54
  }
54
55
  }
package/package.json CHANGED
@@ -8,16 +8,15 @@
8
8
  "@apidevtools/swagger-parser": "^12.1.0",
9
9
  "debug": "^4.4.3",
10
10
  "js-yaml": "^4.1.1",
11
- "jsonpath": "^1.1.1",
12
11
  "loud-rejection": "^2.2.0",
13
12
  "openapi-types": "^12.1.3",
14
- "openapi-typescript": "^7.10.1",
13
+ "openapi-typescript": "^7.13.0",
15
14
  "path-to-regexp": "^8.3.0",
16
- "prettier": "^3.8.0",
15
+ "prettier": "^3.8.1",
17
16
  "typescript": "^5.9.3",
18
17
  "url-pattern": "^1.0.3",
19
18
  "yargs": "^18.0.0",
20
- "zod": "^4.3.5"
19
+ "zod": "^4.3.6"
21
20
  },
22
21
  "description": "A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications",
23
22
  "keywords": [
@@ -32,28 +31,27 @@
32
31
  "validation"
33
32
  ],
34
33
  "devDependencies": {
35
- "@commitlint/cli": "^20.3.0",
36
- "@commitlint/config-conventional": "^20.3.0",
37
- "@eslint/js": "^9.39.2",
34
+ "@commitlint/cli": "^20.4.3",
35
+ "@commitlint/config-conventional": "^20.4.3",
36
+ "@eslint/js": "^10.0.1",
38
37
  "@semantic-release/changelog": "^6.0.3",
39
38
  "@semantic-release/git": "^10.0.1",
40
39
  "@types/debug": "^4.1.12",
41
40
  "@types/jest": "^30.0.0",
42
41
  "@types/js-yaml": "^4.0.9",
43
- "@types/jsonpath": "^0.2.4",
44
- "@types/node": "^25.0.9",
42
+ "@types/node": "^25.3.5",
45
43
  "@types/yargs": "^17.0.35",
46
- "@vitest/coverage-v8": "^4.0.17",
44
+ "@vitest/coverage-v8": "^4.0.18",
47
45
  "cross-env": "^10.1.0",
48
- "eslint": "^9.39.2",
46
+ "eslint": "^10.0.3",
49
47
  "eslint-config-prettier": "^10.1.8",
50
48
  "husky": "^9.1.7",
51
- "lint-staged": "^16.2.7",
52
- "semantic-release": "^25.0.2",
49
+ "lint-staged": "^16.3.2",
50
+ "semantic-release": "^25.0.3",
53
51
  "ts-node": "^10.9.2",
54
- "typescript-eslint": "^8.53.0",
55
- "undici": "^7.18.2",
56
- "vitest": "^4.0.14"
52
+ "typescript-eslint": "^8.56.1",
53
+ "undici": "^7.22.0",
54
+ "vitest": "^4.0.18"
57
55
  },
58
56
  "homepage": "https://github.com/julienandreu/zod-codegen",
59
57
  "license": "Apache-2.0",
@@ -75,7 +73,6 @@
75
73
  "node": ">=18.0.0"
76
74
  },
77
75
  "overrides": {
78
- "tar": "^7.5.3",
79
76
  "diff": "^8.0.3",
80
77
  "@actions/http-client": {
81
78
  "undici": "^7.18.0"
@@ -83,8 +80,8 @@
83
80
  },
84
81
  "scripts": {
85
82
  "audit:fix": "npm audit fix",
86
- "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'",
87
- "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",
83
+ "build": "NODE_OPTIONS='--no-deprecation' sh -c 'rm -rf dist && tsc --project tsconfig.json && ts-node scripts/add-js-extensions.ts && chmod +x ./dist/src/cli.js'",
84
+ "build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && ts-node scripts/add-js-extensions.ts && chmod +x ./dist/src/cli.js",
88
85
  "build:watch": "tsc --project tsconfig.json --watch",
89
86
  "dev": "npm run build && node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ./examples/petstore && npm run format && npm run lint",
90
87
  "lint": "eslint src --fix",
@@ -96,7 +93,6 @@
96
93
  "test:watch": "vitest",
97
94
  "test:coverage": "vitest run --coverage",
98
95
  "test:ui": "vitest --ui",
99
- "manifest:update": "ts-node scripts/update-manifest.ts",
100
96
  "prepare": "husky",
101
97
  "prepublishOnly": "npm run build && npm run test && npm run lint:check && npm run type-check",
102
98
  "clean": "rm -rf dist coverage node_modules/.cache",
@@ -105,5 +101,5 @@
105
101
  "release": "semantic-release",
106
102
  "release:dry": "semantic-release --dry-run"
107
103
  },
108
- "version": "1.6.3"
104
+ "version": "1.7.0"
109
105
  }
package/src/cli.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import yargs from 'yargs';
4
- import {hideBin} from 'yargs/helpers';
5
- import {Generator, type GeneratorOptions, type NamingConvention} from './generator';
6
- import {readFileSync} from 'node:fs';
7
- import {fileURLToPath} from 'node:url';
8
- import {dirname, join} from 'node:path';
7
+ import { hideBin } from 'yargs/helpers';
8
+ import { Generator, type GeneratorOptions, type NamingConvention } from './generator';
9
9
 
10
- import loudRejection from 'loud-rejection';
11
- import {handleErrors} from './utils/error-handler';
12
- import {handleSignals} from './utils/signal-handler';
13
10
  import debug from 'debug';
14
- import {Reporter} from './utils/reporter';
11
+ import loudRejection from 'loud-rejection';
12
+ import { handleErrors } from './utils/error-handler';
13
+ import { Reporter } from './utils/reporter';
14
+ import { handleSignals } from './utils/signal-handler';
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  // Read package.json from the project root
@@ -22,7 +22,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
22
22
  // Try multiple paths to ensure we find package.json
23
23
  const possiblePaths = [
24
24
  join(__dirname, '..', '..', 'package.json'), // dist/src/cli.js or node_modules/pkg/dist/src/cli.js
25
- join(__dirname, '..', 'package.json'), // src/cli.ts
25
+ join(__dirname, '..', 'package.json') // src/cli.ts
26
26
  ];
27
27
 
28
28
  let packageJsonPath: string | undefined;
@@ -46,7 +46,7 @@ const packageData = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
46
46
  description: string;
47
47
  };
48
48
 
49
- const {name, description, version} = packageData;
49
+ const { name, description, version } = packageData;
50
50
  const reporter = new Reporter(process.stdout, process.stderr);
51
51
  const startTime = process.hrtime.bigint();
52
52
 
@@ -64,26 +64,26 @@ const argv = yargs(hideBin(process.argv))
64
64
  alias: 'i',
65
65
  type: 'string',
66
66
  description: 'Path or URL to OpenAPI file',
67
- demandOption: true,
67
+ demandOption: true
68
68
  })
69
69
  .option('output', {
70
70
  alias: 'o',
71
71
  type: 'string',
72
- description: 'Directory to output the generated files',
73
- default: 'generated',
72
+ description: 'Output directory (writes to <output>/api.ts) or path to the generated file (e.g. ./dist/api.ts). Default: generated',
73
+ default: 'api.ts'
74
74
  })
75
75
  .option('naming-convention', {
76
76
  alias: 'n',
77
77
  type: 'string',
78
78
  description: 'Naming convention to apply to operation IDs',
79
79
  choices: ['camelCase', 'PascalCase', 'snake_case', 'kebab-case', 'SCREAMING_SNAKE_CASE', 'SCREAMING-KEBAB-CASE'],
80
- default: undefined,
80
+ default: undefined
81
81
  })
82
82
  .strict()
83
83
  .help()
84
84
  .parseSync();
85
85
 
86
- const {input, output, namingConvention} = argv;
86
+ const { input, output, namingConvention } = argv;
87
87
 
88
88
  /**
89
89
  * Type guard to validate that a string is a valid naming convention.
@@ -96,20 +96,14 @@ function isValidNamingConvention(value: string | undefined): value is NamingConv
96
96
  if (value === undefined) {
97
97
  return false;
98
98
  }
99
- const validConventions: readonly NamingConvention[] = [
100
- 'camelCase',
101
- 'PascalCase',
102
- 'snake_case',
103
- 'kebab-case',
104
- 'SCREAMING_SNAKE_CASE',
105
- 'SCREAMING-KEBAB-CASE',
106
- ] as const;
99
+
100
+ const validConventions: readonly NamingConvention[] = ['camelCase', 'PascalCase', 'snake_case', 'kebab-case', 'SCREAMING_SNAKE_CASE', 'SCREAMING-KEBAB-CASE'] as const;
107
101
  return validConventions.includes(value as NamingConvention);
108
102
  }
109
103
 
110
104
  void (async () => {
111
105
  try {
112
- const options: GeneratorOptions = isValidNamingConvention(namingConvention) ? {namingConvention} : {};
106
+ const options: GeneratorOptions = isValidNamingConvention(namingConvention) ? { namingConvention } : {};
113
107
 
114
108
  const generator = new Generator(name, version, reporter, input, output, options);
115
109
  const exitCode = await generator.run();
@@ -120,6 +114,7 @@ void (async () => {
120
114
  } else {
121
115
  reporter.error('An unknown fatal error occurred');
122
116
  }
117
+
123
118
  process.exit(1);
124
119
  }
125
120
  })();
package/src/generator.ts CHANGED
@@ -1,13 +1,14 @@
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';
1
+ import { extname, resolve } from 'node:path';
2
+ import { TypeScriptCodeGeneratorService } from './services/code-generator.service';
3
+ import { OpenApiFileParserService, SyncFileReaderService } from './services/file-reader.service';
4
+ import { SyncFileWriterService } from './services/file-writer.service';
5
+ import type { GeneratorOptions } from './types/generator-options';
6
+ import type { OpenApiSpecType } from './types/openapi';
7
+ import type { Reporter } from './utils/reporter';
7
8
 
8
9
  // Re-export types for library users
9
- export type {GeneratorOptions} from './types/generator-options';
10
- export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention';
10
+ export type { GeneratorOptions } from './types/generator-options';
11
+ export type { NamingConvention, OperationDetails, OperationNameTransformer } from './utils/naming-convention';
11
12
 
12
13
  /**
13
14
  * Main generator class for creating TypeScript code from OpenAPI specifications.
@@ -50,7 +51,7 @@ export class Generator {
50
51
  * @param _version - The version of the application/library (used in generated file headers)
51
52
  * @param reporter - Reporter instance for logging messages and errors
52
53
  * @param inputPath - Path or URL to the OpenAPI specification file
53
- * @param _outputDir - Directory where generated files will be written
54
+ * @param _outputDir - Output directory (writes to <dir>/api.ts) or path to the generated file (e.g. ./dist/api.ts)
54
55
  * @param options - Optional configuration for code generation
55
56
  */
56
57
  constructor(
@@ -59,10 +60,11 @@ export class Generator {
59
60
  private readonly reporter: Reporter,
60
61
  private readonly inputPath: string,
61
62
  private readonly _outputDir: string,
62
- options: GeneratorOptions = {},
63
+ options: GeneratorOptions = {}
63
64
  ) {
64
65
  this.fileWriter = new SyncFileWriterService(this._name, this._version, inputPath);
65
- this.outputPath = this.fileWriter.resolveOutputPath(this._outputDir);
66
+ const ext = extname(this._outputDir);
67
+ this.outputPath = ext === '.ts' || ext === '.tsx' ? resolve(this._outputDir) : this.fileWriter.resolveOutputPath(this._outputDir);
66
68
  this.codeGenerator = new TypeScriptCodeGeneratorService(options);
67
69
  }
68
70
 
@@ -1,4 +1,4 @@
1
- import type {OpenApiSpecType} from '../types/openapi';
1
+ import type { OpenApiSpecType } from '../types/openapi';
2
2
 
3
3
  export interface CodeGenerator {
4
4
  generate(spec: OpenApiSpecType): string;