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.
- package/.github/workflows/ci.yml +50 -48
- package/.github/workflows/release.yml +13 -3
- package/.husky/commit-msg +1 -1
- package/.husky/pre-commit +1 -1
- package/.lintstagedrc.json +5 -1
- package/.nvmrc +1 -1
- package/.prettierrc.json +12 -5
- package/CHANGELOG.md +11 -0
- package/CONTRIBUTING.md +12 -12
- package/EXAMPLES.md +135 -57
- package/PERFORMANCE.md +4 -4
- package/README.md +87 -64
- package/SECURITY.md +1 -1
- package/dist/src/cli.js +11 -18
- package/dist/src/generator.d.ts +2 -2
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/generator.js +5 -3
- package/dist/src/interfaces/code-generator.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.d.ts +3 -1
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +236 -219
- package/dist/src/services/file-reader.service.d.ts.map +1 -1
- package/dist/src/services/file-reader.service.js +1 -1
- package/dist/src/services/file-writer.service.d.ts.map +1 -1
- package/dist/src/services/file-writer.service.js +2 -2
- package/dist/src/services/import-builder.service.d.ts.map +1 -1
- package/dist/src/services/import-builder.service.js +3 -3
- package/dist/src/services/type-builder.service.d.ts.map +1 -1
- package/dist/src/types/generator-options.d.ts.map +1 -1
- package/dist/src/types/openapi.d.ts.map +1 -1
- package/dist/src/types/openapi.js +20 -20
- package/dist/src/utils/error-handler.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.d.ts.map +1 -1
- package/dist/src/utils/naming-convention.js +6 -3
- package/dist/src/utils/signal-handler.d.ts.map +1 -1
- package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
- package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
- package/dist/tests/integration/cli-comprehensive.test.js +110 -0
- package/dist/tests/integration/cli.test.d.ts +2 -0
- package/dist/tests/integration/cli.test.d.ts.map +1 -0
- package/dist/tests/integration/cli.test.js +25 -0
- package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
- package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
- package/dist/tests/integration/error-scenarios.test.js +169 -0
- package/dist/tests/integration/snapshots.test.d.ts +2 -0
- package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
- package/dist/tests/integration/snapshots.test.js +100 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
- package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
- package/dist/tests/unit/code-generator.test.d.ts +2 -0
- package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
- package/dist/tests/unit/code-generator.test.js +1364 -0
- package/dist/tests/unit/file-reader.test.d.ts +2 -0
- package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
- package/dist/tests/unit/file-reader.test.js +125 -0
- package/dist/tests/unit/generator.test.d.ts +2 -0
- package/dist/tests/unit/generator.test.d.ts.map +1 -0
- package/dist/tests/unit/generator.test.js +119 -0
- package/dist/tests/unit/naming-convention.test.d.ts +2 -0
- package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
- package/dist/tests/unit/naming-convention.test.js +256 -0
- package/dist/tests/unit/reporter.test.d.ts +2 -0
- package/dist/tests/unit/reporter.test.d.ts.map +1 -0
- package/dist/tests/unit/reporter.test.js +44 -0
- package/dist/tests/unit/type-builder.test.d.ts +2 -0
- package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/type-builder.test.js +108 -0
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +10 -20
- package/eslint.config.mjs +38 -28
- package/examples/.gitkeep +1 -1
- package/examples/README.md +4 -2
- package/examples/petstore/README.md +18 -17
- package/examples/petstore/{type.ts → api.ts} +158 -74
- package/examples/petstore/authenticated-usage.ts +6 -4
- package/examples/petstore/basic-usage.ts +4 -3
- package/examples/petstore/error-handling-usage.ts +84 -0
- package/examples/petstore/retry-handler-usage.ts +11 -18
- package/examples/petstore/server-variables-usage.ts +10 -10
- package/examples/pokeapi/README.md +8 -8
- package/examples/pokeapi/api.ts +218 -0
- package/examples/pokeapi/basic-usage.ts +3 -2
- package/examples/pokeapi/custom-client.ts +5 -4
- package/package.json +17 -21
- package/src/cli.ts +20 -25
- package/src/generator.ts +13 -11
- package/src/interfaces/code-generator.ts +1 -1
- package/src/services/code-generator.service.ts +799 -1120
- package/src/services/file-reader.service.ts +6 -5
- package/src/services/file-writer.service.ts +7 -7
- package/src/services/import-builder.service.ts +9 -13
- package/src/services/type-builder.service.ts +8 -19
- package/src/types/generator-options.ts +1 -1
- package/src/types/openapi.ts +22 -22
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/naming-convention.ts +13 -10
- package/src/utils/reporter.ts +2 -2
- package/src/utils/signal-handler.ts +7 -8
- package/tests/integration/cli-comprehensive.test.ts +38 -32
- package/tests/integration/cli.test.ts +5 -5
- package/tests/integration/error-scenarios.test.ts +20 -26
- package/tests/integration/snapshots.test.ts +19 -23
- package/tests/unit/code-generator-edge-cases.test.ts +133 -133
- package/tests/unit/code-generator.test.ts +431 -330
- package/tests/unit/file-reader.test.ts +14 -14
- package/tests/unit/generator.test.ts +30 -18
- package/tests/unit/naming-convention.test.ts +27 -27
- package/tests/unit/type-builder.test.ts +2 -2
- package/tsconfig.json +5 -3
- package/vitest.config.ts +11 -21
- package/dist/scripts/update-manifest.d.ts +0 -14
- package/dist/scripts/update-manifest.d.ts.map +0 -1
- package/dist/scripts/update-manifest.js +0 -33
- package/dist/src/assets/manifest.json +0 -5
- package/examples/pokeapi/type.ts +0 -109
- package/generated/type.ts +0 -371
- package/scripts/update-manifest.ts +0 -49
- 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
|
|
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
|
|
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
|
|
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
|
|
28
|
-
serverIndex: 0
|
|
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
|
|
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
|
|
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
|
|
18
|
+
import PokAPI from './api';
|
|
19
19
|
|
|
20
20
|
// Create a client instance
|
|
21
|
-
const client = new PokAPI(
|
|
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
|
|
33
|
+
import PokAPI from './api';
|
|
34
34
|
|
|
35
35
|
async function getPokemonInfo() {
|
|
36
|
-
const client = new PokAPI(
|
|
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
|
|
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(
|
|
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
|
|
7
|
+
import PokAPI from './api';
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
|
-
const client = new PokAPI(
|
|
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
|
|
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(
|
|
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.
|
|
13
|
+
"openapi-typescript": "^7.13.0",
|
|
15
14
|
"path-to-regexp": "^8.3.0",
|
|
16
|
-
"prettier": "^3.8.
|
|
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.
|
|
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
|
|
36
|
-
"@commitlint/config-conventional": "^20.3
|
|
37
|
-
"@eslint/js": "^
|
|
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/
|
|
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.
|
|
44
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
47
45
|
"cross-env": "^10.1.0",
|
|
48
|
-
"eslint": "^
|
|
46
|
+
"eslint": "^10.0.3",
|
|
49
47
|
"eslint-config-prettier": "^10.1.8",
|
|
50
48
|
"husky": "^9.1.7",
|
|
51
|
-
"lint-staged": "^16.2
|
|
52
|
-
"semantic-release": "^25.0.
|
|
49
|
+
"lint-staged": "^16.3.2",
|
|
50
|
+
"semantic-release": "^25.0.3",
|
|
53
51
|
"ts-node": "^10.9.2",
|
|
54
|
-
"typescript-eslint": "^8.
|
|
55
|
-
"undici": "^7.
|
|
56
|
-
"vitest": "^4.0.
|
|
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 &&
|
|
87
|
-
"build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && ts-node scripts/add-js-extensions.ts &&
|
|
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.
|
|
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
|
|
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')
|
|
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: '
|
|
73
|
-
default: '
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 -
|
|
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
|
-
|
|
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
|
|