typed-openapi 1.5.1 → 2.0.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.
- package/dist/{chunk-OVT6OLBK.js → chunk-4EZJSCLI.js} +341 -50
- package/dist/chunk-GD55PFNE.js +208 -0
- package/dist/cli.js +11 -5
- package/dist/index.d.ts +8 -3
- package/dist/index.js +1 -1
- package/dist/node.export.d.ts +10 -1
- package/dist/node.export.js +2 -2
- package/dist/{types-DLE5RaXi.d.ts → types-DsI2d-HE.d.ts} +2 -0
- package/package.json +6 -1
- package/src/cli.ts +13 -4
- package/src/default-fetcher.generator.ts +101 -0
- package/src/generate-client-files.ts +75 -15
- package/src/generator.ts +197 -26
- package/src/map-openapi-endpoints.ts +55 -4
- package/src/openapi-schema-to-ts.ts +3 -2
- package/src/tanstack-query.generator.ts +69 -25
- package/dist/chunk-O7DZWQK4.js +0 -68
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ERROR_STATUS_CODES,
|
|
3
|
+
DEFAULT_SUCCESS_STATUS_CODES,
|
|
4
|
+
allowedRuntimes,
|
|
5
|
+
generateFile,
|
|
6
|
+
generateTanstackQueryFile,
|
|
7
|
+
mapOpenApiEndpoints
|
|
8
|
+
} from "./chunk-4EZJSCLI.js";
|
|
9
|
+
import {
|
|
10
|
+
prettify
|
|
11
|
+
} from "./chunk-KAEXXJ7X.js";
|
|
12
|
+
|
|
13
|
+
// src/generate-client-files.ts
|
|
14
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
15
|
+
import { basename, join, dirname, isAbsolute } from "pathe";
|
|
16
|
+
import { type } from "arktype";
|
|
17
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
18
|
+
|
|
19
|
+
// src/default-fetcher.generator.ts
|
|
20
|
+
var generateDefaultFetcher = (options) => {
|
|
21
|
+
const {
|
|
22
|
+
envApiBaseUrl = "API_BASE_URL",
|
|
23
|
+
clientPath = "./openapi.client.ts",
|
|
24
|
+
fetcherName = "defaultFetcher",
|
|
25
|
+
apiName = "api"
|
|
26
|
+
} = options;
|
|
27
|
+
return `/**
|
|
28
|
+
* Generic API Client for typed-openapi generated code
|
|
29
|
+
*
|
|
30
|
+
* This is a simple, production-ready wrapper that you can copy and customize.
|
|
31
|
+
* It handles:
|
|
32
|
+
* - Path parameter replacement
|
|
33
|
+
* - Query parameter serialization
|
|
34
|
+
* - JSON request/response handling
|
|
35
|
+
* - Basic error handling
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* 1. Replace './generated/api' with your actual generated file path
|
|
39
|
+
* 2. Set your ${envApiBaseUrl}
|
|
40
|
+
* 3. Customize error handling and headers as needed
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
import { type Fetcher, createApiClient } from "./${clientPath}";
|
|
45
|
+
|
|
46
|
+
// Basic configuration
|
|
47
|
+
const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example.com";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Simple fetcher implementation without external dependencies
|
|
51
|
+
*/
|
|
52
|
+
export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
|
|
53
|
+
const headers = new Headers();
|
|
54
|
+
|
|
55
|
+
// Replace path parameters (supports both {param} and :param formats)
|
|
56
|
+
const actualUrl = replacePathParams(apiUrl, (params?.path ?? {}) as Record<string, string>);
|
|
57
|
+
const url = new URL(actualUrl);
|
|
58
|
+
|
|
59
|
+
// Handle query parameters
|
|
60
|
+
if (params?.query) {
|
|
61
|
+
const searchParams = new URLSearchParams();
|
|
62
|
+
Object.entries(params.query).forEach(([key, value]) => {
|
|
63
|
+
if (value != null) {
|
|
64
|
+
// Skip null/undefined values
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
value.forEach((val) => val != null && searchParams.append(key, String(val)));
|
|
67
|
+
} else {
|
|
68
|
+
searchParams.append(key, String(value));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
url.search = searchParams.toString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle request body for mutation methods
|
|
76
|
+
const body = ["post", "put", "patch", "delete"].includes(method.toLowerCase())
|
|
77
|
+
? JSON.stringify(params?.body)
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
if (body) {
|
|
81
|
+
headers.set("Content-Type", "application/json");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Add custom headers
|
|
85
|
+
if (params?.header) {
|
|
86
|
+
Object.entries(params.header).forEach(([key, value]) => {
|
|
87
|
+
if (value != null) {
|
|
88
|
+
headers.set(key, String(value));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: method.toUpperCase(),
|
|
95
|
+
...(body && { body }),
|
|
96
|
+
headers,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return response;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Replace path parameters in URL
|
|
104
|
+
* Supports both OpenAPI format {param} and Express format :param
|
|
105
|
+
*/
|
|
106
|
+
export function replacePathParams(url: string, params: Record<string, string>): string {
|
|
107
|
+
return url
|
|
108
|
+
.replace(/{(\\w+)}/g, function(_, key) { return params[key] || '{' + key + '}'; })
|
|
109
|
+
.replace(/:([a-zA-Z0-9_]+)/g, function(_, key) { return params[key] || ':' + key; });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const ${apiName} = createApiClient(${fetcherName}, API_BASE_URL);
|
|
113
|
+
`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// src/generate-client-files.ts
|
|
117
|
+
var cwd = process.cwd();
|
|
118
|
+
var now = /* @__PURE__ */ new Date();
|
|
119
|
+
async function ensureDir(dirPath) {
|
|
120
|
+
try {
|
|
121
|
+
await mkdir(dirPath, { recursive: true });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Error ensuring directory: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var optionsSchema = type({
|
|
127
|
+
"output?": "string",
|
|
128
|
+
runtime: allowedRuntimes,
|
|
129
|
+
tanstack: "boolean | string",
|
|
130
|
+
"defaultFetcher?": type({
|
|
131
|
+
"envApiBaseUrl?": "string",
|
|
132
|
+
"clientPath?": "string",
|
|
133
|
+
"fetcherName?": "string",
|
|
134
|
+
"apiName?": "string"
|
|
135
|
+
}),
|
|
136
|
+
schemasOnly: "boolean",
|
|
137
|
+
"includeClient?": "boolean | 'true' | 'false'",
|
|
138
|
+
"successStatusCodes?": "string",
|
|
139
|
+
"errorStatusCodes?": "string"
|
|
140
|
+
});
|
|
141
|
+
async function generateClientFiles(input, options) {
|
|
142
|
+
const openApiDoc = await SwaggerParser.bundle(input);
|
|
143
|
+
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
144
|
+
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
145
|
+
const successStatusCodes = options.successStatusCodes ? options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) : void 0;
|
|
146
|
+
const errorStatusCodes = options.errorStatusCodes ? options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) : void 0;
|
|
147
|
+
const includeClient = options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
|
|
148
|
+
const generatorOptions = {
|
|
149
|
+
...ctx,
|
|
150
|
+
runtime: options.runtime,
|
|
151
|
+
schemasOnly: options.schemasOnly,
|
|
152
|
+
nameTransform: options.nameTransform,
|
|
153
|
+
includeClient: includeClient ?? true,
|
|
154
|
+
successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
155
|
+
errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES
|
|
156
|
+
};
|
|
157
|
+
const content = await prettify(generateFile(generatorOptions));
|
|
158
|
+
const outputPath = join(
|
|
159
|
+
cwd,
|
|
160
|
+
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`
|
|
161
|
+
);
|
|
162
|
+
console.log("Generating client...", outputPath);
|
|
163
|
+
await ensureDir(dirname(outputPath));
|
|
164
|
+
await writeFile(outputPath, content);
|
|
165
|
+
if (options.tanstack) {
|
|
166
|
+
const tanstackContent = await generateTanstackQueryFile({
|
|
167
|
+
...generatorOptions,
|
|
168
|
+
relativeApiClientPath: "./" + basename(outputPath)
|
|
169
|
+
});
|
|
170
|
+
let tanstackOutputPath;
|
|
171
|
+
if (typeof options.tanstack === "string" && isAbsolute(options.tanstack)) {
|
|
172
|
+
tanstackOutputPath = options.tanstack;
|
|
173
|
+
} else {
|
|
174
|
+
tanstackOutputPath = join(
|
|
175
|
+
dirname(outputPath),
|
|
176
|
+
typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
180
|
+
await ensureDir(dirname(tanstackOutputPath));
|
|
181
|
+
await writeFile(tanstackOutputPath, tanstackContent);
|
|
182
|
+
}
|
|
183
|
+
if (options.defaultFetcher) {
|
|
184
|
+
const defaultFetcherContent = generateDefaultFetcher({
|
|
185
|
+
envApiBaseUrl: options.defaultFetcher.envApiBaseUrl,
|
|
186
|
+
clientPath: options.defaultFetcher.clientPath ?? basename(outputPath),
|
|
187
|
+
fetcherName: options.defaultFetcher.fetcherName,
|
|
188
|
+
apiName: options.defaultFetcher.apiName
|
|
189
|
+
});
|
|
190
|
+
let defaultFetcherOutputPath;
|
|
191
|
+
if (typeof options.defaultFetcher === "string" && isAbsolute(options.defaultFetcher)) {
|
|
192
|
+
defaultFetcherOutputPath = options.defaultFetcher;
|
|
193
|
+
} else {
|
|
194
|
+
defaultFetcherOutputPath = join(
|
|
195
|
+
dirname(outputPath),
|
|
196
|
+
typeof options.defaultFetcher === "string" ? options.defaultFetcher : `api.client.ts`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
console.log("Generating default fetcher...", defaultFetcherOutputPath);
|
|
200
|
+
await ensureDir(dirname(defaultFetcherOutputPath));
|
|
201
|
+
await writeFile(defaultFetcherOutputPath, defaultFetcherContent);
|
|
202
|
+
}
|
|
203
|
+
console.log(`Done in ${(/* @__PURE__ */ new Date()).getTime() - now.getTime()}ms !`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
generateClientFiles
|
|
208
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateClientFiles
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GD55PFNE.js";
|
|
4
4
|
import {
|
|
5
5
|
allowedRuntimes
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-4EZJSCLI.js";
|
|
7
7
|
import "./chunk-KAEXXJ7X.js";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
@@ -12,12 +12,18 @@ import { readFileSync } from "fs";
|
|
|
12
12
|
var { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
13
|
var cli = cac(name);
|
|
14
14
|
cli.command("<input>", "Generate").option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)").option(
|
|
15
|
-
"-r, --runtime <
|
|
15
|
+
"-r, --runtime <n>",
|
|
16
16
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
17
17
|
{ default: "none" }
|
|
18
|
-
).option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false }).option(
|
|
18
|
+
).option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false }).option("--include-client", "Include API client types and implementation (defaults to true)", { default: true }).option(
|
|
19
|
+
"--success-status-codes <codes>",
|
|
20
|
+
"Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)"
|
|
21
|
+
).option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)").option(
|
|
19
22
|
"--tanstack [name]",
|
|
20
|
-
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file"
|
|
23
|
+
"Generate tanstack client, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file"
|
|
24
|
+
).option(
|
|
25
|
+
"--default-fetcher [name]",
|
|
26
|
+
"Generate default fetcher, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file"
|
|
21
27
|
).action(async (input, _options) => {
|
|
22
28
|
return generateClientFiles(input, _options);
|
|
23
29
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReferenceObject } from 'openapi3-ts/oas31';
|
|
2
|
-
import { S as StringOrBox, O as OpenapiSchemaConvertContext, L as LibSchemaObject, B as BoxFactory, m as mapOpenApiEndpoints, N as NameTransformOptions, a as OpenapiSchemaConvertArgs, b as Box, A as AnyBoxDef } from './types-
|
|
3
|
-
export { q as AnyBox, j as BoxArray, f as BoxDefinition, i as BoxIntersection, o as BoxKeyword, n as BoxLiteral, p as BoxObject, k as BoxOptional, g as BoxParams, l as BoxRef, h as BoxUnion, c as Endpoint, E as EndpointParameters, F as FactoryCreator, G as GenericFactory, M as Method, R as RefInfo, e as RefResolver, W as WithSchema, d as createRefResolver } from './types-
|
|
2
|
+
import { S as StringOrBox, O as OpenapiSchemaConvertContext, L as LibSchemaObject, B as BoxFactory, m as mapOpenApiEndpoints, N as NameTransformOptions, a as OpenapiSchemaConvertArgs, b as Box, A as AnyBoxDef } from './types-DsI2d-HE.js';
|
|
3
|
+
export { q as AnyBox, j as BoxArray, f as BoxDefinition, i as BoxIntersection, o as BoxKeyword, n as BoxLiteral, p as BoxObject, k as BoxOptional, g as BoxParams, l as BoxRef, h as BoxUnion, c as Endpoint, E as EndpointParameters, F as FactoryCreator, G as GenericFactory, M as Method, R as RefInfo, e as RefResolver, W as WithSchema, d as createRefResolver } from './types-DsI2d-HE.js';
|
|
4
4
|
import * as arktype_internal_methods_string_ts from 'arktype/internal/methods/string.ts';
|
|
5
5
|
import * as Codegen from '@sinclair/typebox-codegen';
|
|
6
6
|
import 'openapi3-ts/oas30';
|
|
@@ -16,6 +16,9 @@ type GeneratorOptions$1 = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
|
16
16
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
17
17
|
schemasOnly?: boolean;
|
|
18
18
|
nameTransform?: NameTransformOptions | undefined;
|
|
19
|
+
successStatusCodes?: readonly number[];
|
|
20
|
+
errorStatusCodes?: readonly number[];
|
|
21
|
+
includeClient?: boolean;
|
|
19
22
|
};
|
|
20
23
|
declare const allowedRuntimes: arktype_internal_methods_string_ts.StringType<"none" | "arktype" | "io-ts" | "typebox" | "valibot" | "yup" | "zod", {}>;
|
|
21
24
|
type OutputRuntime = typeof allowedRuntimes.infer;
|
|
@@ -30,7 +33,9 @@ declare const runtimeValidationGenerator: {
|
|
|
30
33
|
declare const generateFile: (options: GeneratorOptions$1) => string;
|
|
31
34
|
|
|
32
35
|
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
|
|
33
|
-
type GeneratorContext = Required<GeneratorOptions
|
|
36
|
+
type GeneratorContext = Required<GeneratorOptions> & {
|
|
37
|
+
errorStatusCodes?: readonly number[];
|
|
38
|
+
};
|
|
34
39
|
declare const generateTanstackQueryFile: (ctx: GeneratorContext & {
|
|
35
40
|
relativeApiClientPath: string;
|
|
36
41
|
}) => Promise<string>;
|
package/dist/index.js
CHANGED
package/dist/node.export.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as arktype_internal_methods_object_ts from 'arktype/internal/methods/object.ts';
|
|
2
|
-
import { N as NameTransformOptions } from './types-
|
|
2
|
+
import { N as NameTransformOptions } from './types-DsI2d-HE.js';
|
|
3
3
|
import 'openapi3-ts/oas31';
|
|
4
4
|
import 'openapi3-ts/oas30';
|
|
5
5
|
|
|
@@ -8,6 +8,15 @@ declare const optionsSchema: arktype_internal_methods_object_ts.ObjectType<{
|
|
|
8
8
|
tanstack: string | boolean;
|
|
9
9
|
schemasOnly: boolean;
|
|
10
10
|
output?: string;
|
|
11
|
+
defaultFetcher?: {
|
|
12
|
+
envApiBaseUrl?: string;
|
|
13
|
+
clientPath?: string;
|
|
14
|
+
fetcherName?: string;
|
|
15
|
+
apiName?: string;
|
|
16
|
+
};
|
|
17
|
+
includeClient?: boolean | "false" | "true";
|
|
18
|
+
successStatusCodes?: string;
|
|
19
|
+
errorStatusCodes?: string;
|
|
11
20
|
}, {}>;
|
|
12
21
|
type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
13
22
|
nameTransform?: NameTransformOptions;
|
package/dist/node.export.js
CHANGED
|
@@ -100,6 +100,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
100
100
|
type DefaultEndpoint = {
|
|
101
101
|
parameters?: EndpointParameters | undefined;
|
|
102
102
|
response: AnyBox;
|
|
103
|
+
responses?: Record<string, AnyBox>;
|
|
103
104
|
responseHeaders?: Record<string, AnyBox>;
|
|
104
105
|
};
|
|
105
106
|
type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
@@ -114,6 +115,7 @@ type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
114
115
|
areParametersRequired: boolean;
|
|
115
116
|
};
|
|
116
117
|
response: TConfig["response"];
|
|
118
|
+
responses?: TConfig["responses"];
|
|
117
119
|
responseHeaders?: TConfig["responseHeaders"];
|
|
118
120
|
};
|
|
119
121
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@changesets/cli": "^2.29.4",
|
|
33
|
+
"@tanstack/react-query": "5.85.0",
|
|
33
34
|
"@types/node": "^22.15.17",
|
|
34
35
|
"@types/prettier": "3.0.0",
|
|
36
|
+
"msw": "2.10.5",
|
|
35
37
|
"tsup": "^8.4.0",
|
|
36
38
|
"typescript": "^5.8.3",
|
|
37
39
|
"vitest": "^3.1.3"
|
|
@@ -64,6 +66,9 @@
|
|
|
64
66
|
"dev": "tsup --watch",
|
|
65
67
|
"build": "tsup",
|
|
66
68
|
"test": "vitest",
|
|
69
|
+
"gen:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts --default-fetcher",
|
|
70
|
+
"test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
|
|
71
|
+
"test:runtime": "pnpm run gen:runtime && pnpm run test:runtime:run",
|
|
67
72
|
"fmt": "prettier --write src",
|
|
68
73
|
"typecheck": "tsc -b ./tsconfig.build.json"
|
|
69
74
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cac } from "cac";
|
|
2
|
-
|
|
3
2
|
import { readFileSync } from "fs";
|
|
4
3
|
import { generateClientFiles } from "./generate-client-files.ts";
|
|
5
4
|
import { allowedRuntimes } from "./generator.ts";
|
|
@@ -11,16 +10,26 @@ cli
|
|
|
11
10
|
.command("<input>", "Generate")
|
|
12
11
|
.option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)")
|
|
13
12
|
.option(
|
|
14
|
-
"-r, --runtime <
|
|
13
|
+
"-r, --runtime <n>",
|
|
15
14
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
16
15
|
{ default: "none" },
|
|
17
16
|
)
|
|
18
17
|
.option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false })
|
|
18
|
+
.option("--include-client", "Include API client types and implementation (defaults to true)", { default: true })
|
|
19
|
+
.option(
|
|
20
|
+
"--success-status-codes <codes>",
|
|
21
|
+
"Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)",
|
|
22
|
+
)
|
|
23
|
+
.option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)")
|
|
19
24
|
.option(
|
|
20
25
|
"--tanstack [name]",
|
|
21
|
-
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file",
|
|
26
|
+
"Generate tanstack client, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file",
|
|
27
|
+
)
|
|
28
|
+
.option(
|
|
29
|
+
"--default-fetcher [name]",
|
|
30
|
+
"Generate default fetcher, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file",
|
|
22
31
|
)
|
|
23
|
-
.action(async (input, _options) => {
|
|
32
|
+
.action(async (input: string, _options: any) => {
|
|
24
33
|
return generateClientFiles(input, _options);
|
|
25
34
|
});
|
|
26
35
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The contents of api-client.example.ts (kept in sync with the file)
|
|
2
|
+
export const generateDefaultFetcher = (options: {
|
|
3
|
+
envApiBaseUrl?: string | undefined;
|
|
4
|
+
clientPath?: string | undefined;
|
|
5
|
+
fetcherName?: string | undefined;
|
|
6
|
+
apiName?: string | undefined;
|
|
7
|
+
}) => {
|
|
8
|
+
const {
|
|
9
|
+
envApiBaseUrl = "API_BASE_URL",
|
|
10
|
+
clientPath = "./openapi.client.ts",
|
|
11
|
+
fetcherName = "defaultFetcher",
|
|
12
|
+
apiName = "api",
|
|
13
|
+
} = options;
|
|
14
|
+
return `/**
|
|
15
|
+
* Generic API Client for typed-openapi generated code
|
|
16
|
+
*
|
|
17
|
+
* This is a simple, production-ready wrapper that you can copy and customize.
|
|
18
|
+
* It handles:
|
|
19
|
+
* - Path parameter replacement
|
|
20
|
+
* - Query parameter serialization
|
|
21
|
+
* - JSON request/response handling
|
|
22
|
+
* - Basic error handling
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* 1. Replace './generated/api' with your actual generated file path
|
|
26
|
+
* 2. Set your ${envApiBaseUrl}
|
|
27
|
+
* 3. Customize error handling and headers as needed
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
import { type Fetcher, createApiClient } from "./${clientPath}";
|
|
32
|
+
|
|
33
|
+
// Basic configuration
|
|
34
|
+
const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example.com";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Simple fetcher implementation without external dependencies
|
|
38
|
+
*/
|
|
39
|
+
export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
|
|
42
|
+
// Replace path parameters (supports both {param} and :param formats)
|
|
43
|
+
const actualUrl = replacePathParams(apiUrl, (params?.path ?? {}) as Record<string, string>);
|
|
44
|
+
const url = new URL(actualUrl);
|
|
45
|
+
|
|
46
|
+
// Handle query parameters
|
|
47
|
+
if (params?.query) {
|
|
48
|
+
const searchParams = new URLSearchParams();
|
|
49
|
+
Object.entries(params.query).forEach(([key, value]) => {
|
|
50
|
+
if (value != null) {
|
|
51
|
+
// Skip null/undefined values
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
value.forEach((val) => val != null && searchParams.append(key, String(val)));
|
|
54
|
+
} else {
|
|
55
|
+
searchParams.append(key, String(value));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
url.search = searchParams.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle request body for mutation methods
|
|
63
|
+
const body = ["post", "put", "patch", "delete"].includes(method.toLowerCase())
|
|
64
|
+
? JSON.stringify(params?.body)
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
if (body) {
|
|
68
|
+
headers.set("Content-Type", "application/json");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add custom headers
|
|
72
|
+
if (params?.header) {
|
|
73
|
+
Object.entries(params.header).forEach(([key, value]) => {
|
|
74
|
+
if (value != null) {
|
|
75
|
+
headers.set(key, String(value));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const response = await fetch(url, {
|
|
81
|
+
method: method.toUpperCase(),
|
|
82
|
+
...(body && { body }),
|
|
83
|
+
headers,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return response;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Replace path parameters in URL
|
|
91
|
+
* Supports both OpenAPI format {param} and Express format :param
|
|
92
|
+
*/
|
|
93
|
+
export function replacePathParams(url: string, params: Record<string, string>): string {
|
|
94
|
+
return url
|
|
95
|
+
.replace(/\{(\\w+)\}/g, function(_, key) { return params[key] || '{' + key + '}'; })
|
|
96
|
+
.replace(/:([a-zA-Z0-9_]+)/g, function(_, key) { return params[key] || ':' + key; });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const ${apiName} = createApiClient(${fetcherName}, API_BASE_URL);
|
|
100
|
+
`;
|
|
101
|
+
};
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
2
2
|
import type { OpenAPIObject } from "openapi3-ts/oas31";
|
|
3
|
-
import { basename, join, dirname } from "pathe";
|
|
3
|
+
import { basename, join, dirname, isAbsolute } from "pathe";
|
|
4
4
|
import { type } from "arktype";
|
|
5
5
|
import { mkdir, writeFile } from "fs/promises";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
allowedRuntimes,
|
|
8
|
+
generateFile,
|
|
9
|
+
DEFAULT_SUCCESS_STATUS_CODES,
|
|
10
|
+
DEFAULT_ERROR_STATUS_CODES,
|
|
11
|
+
type GeneratorOptions,
|
|
12
|
+
} from "./generator.ts";
|
|
7
13
|
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
8
14
|
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
9
15
|
import { prettify } from "./format.ts";
|
|
10
16
|
import type { NameTransformOptions } from "./types.ts";
|
|
17
|
+
import { generateDefaultFetcher } from "./default-fetcher.generator.ts";
|
|
11
18
|
|
|
12
19
|
const cwd = process.cwd();
|
|
13
20
|
const now = new Date();
|
|
@@ -24,7 +31,16 @@ export const optionsSchema = type({
|
|
|
24
31
|
"output?": "string",
|
|
25
32
|
runtime: allowedRuntimes,
|
|
26
33
|
tanstack: "boolean | string",
|
|
34
|
+
"defaultFetcher?": type({
|
|
35
|
+
"envApiBaseUrl?": "string",
|
|
36
|
+
"clientPath?": "string",
|
|
37
|
+
"fetcherName?": "string",
|
|
38
|
+
"apiName?": "string",
|
|
39
|
+
}),
|
|
27
40
|
schemasOnly: "boolean",
|
|
41
|
+
"includeClient?": "boolean | 'true' | 'false'",
|
|
42
|
+
"successStatusCodes?": "string",
|
|
43
|
+
"errorStatusCodes?": "string",
|
|
28
44
|
});
|
|
29
45
|
|
|
30
46
|
type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
@@ -32,19 +48,37 @@ type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
|
32
48
|
};
|
|
33
49
|
|
|
34
50
|
export async function generateClientFiles(input: string, options: GenerateClientFilesOptions) {
|
|
51
|
+
// TODO CLI option to save that file?
|
|
35
52
|
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
|
|
36
53
|
|
|
37
54
|
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
38
55
|
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
// Parse success status codes if provided
|
|
58
|
+
const successStatusCodes = options.successStatusCodes
|
|
59
|
+
? (options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
62
|
+
// Parse error status codes if provided
|
|
63
|
+
const errorStatusCodes = options.errorStatusCodes
|
|
64
|
+
? (options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
// Convert string boolean to actual boolean
|
|
68
|
+
const includeClient =
|
|
69
|
+
options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
|
|
70
|
+
|
|
71
|
+
const generatorOptions: GeneratorOptions = {
|
|
72
|
+
...ctx,
|
|
73
|
+
runtime: options.runtime,
|
|
74
|
+
schemasOnly: options.schemasOnly,
|
|
75
|
+
nameTransform: options.nameTransform,
|
|
76
|
+
includeClient: includeClient ?? true,
|
|
77
|
+
successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
78
|
+
errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const content = await prettify(generateFile(generatorOptions));
|
|
48
82
|
const outputPath = join(
|
|
49
83
|
cwd,
|
|
50
84
|
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
|
|
@@ -56,17 +90,43 @@ export async function generateClientFiles(input: string, options: GenerateClient
|
|
|
56
90
|
|
|
57
91
|
if (options.tanstack) {
|
|
58
92
|
const tanstackContent = await generateTanstackQueryFile({
|
|
59
|
-
...
|
|
93
|
+
...generatorOptions,
|
|
60
94
|
relativeApiClientPath: "./" + basename(outputPath),
|
|
61
95
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
let tanstackOutputPath: string;
|
|
97
|
+
if (typeof options.tanstack === "string" && isAbsolute(options.tanstack)) {
|
|
98
|
+
tanstackOutputPath = options.tanstack;
|
|
99
|
+
} else {
|
|
100
|
+
tanstackOutputPath = join(
|
|
101
|
+
dirname(outputPath),
|
|
102
|
+
typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
66
105
|
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
67
106
|
await ensureDir(dirname(tanstackOutputPath));
|
|
68
107
|
await writeFile(tanstackOutputPath, tanstackContent);
|
|
69
108
|
}
|
|
70
109
|
|
|
110
|
+
if (options.defaultFetcher) {
|
|
111
|
+
const defaultFetcherContent = generateDefaultFetcher({
|
|
112
|
+
envApiBaseUrl: options.defaultFetcher.envApiBaseUrl,
|
|
113
|
+
clientPath: options.defaultFetcher.clientPath ?? basename(outputPath),
|
|
114
|
+
fetcherName: options.defaultFetcher.fetcherName,
|
|
115
|
+
apiName: options.defaultFetcher.apiName,
|
|
116
|
+
});
|
|
117
|
+
let defaultFetcherOutputPath: string;
|
|
118
|
+
if (typeof options.defaultFetcher === "string" && isAbsolute(options.defaultFetcher)) {
|
|
119
|
+
defaultFetcherOutputPath = options.defaultFetcher;
|
|
120
|
+
} else {
|
|
121
|
+
defaultFetcherOutputPath = join(
|
|
122
|
+
dirname(outputPath),
|
|
123
|
+
typeof options.defaultFetcher === "string" ? options.defaultFetcher : `api.client.ts`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
console.log("Generating default fetcher...", defaultFetcherOutputPath);
|
|
127
|
+
await ensureDir(dirname(defaultFetcherOutputPath));
|
|
128
|
+
await writeFile(defaultFetcherOutputPath, defaultFetcherContent);
|
|
129
|
+
}
|
|
130
|
+
|
|
71
131
|
console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
|
|
72
132
|
}
|