typed-openapi 2.0.0 → 2.0.2
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-E6A7N4ND.js → chunk-KF4JBLDM.js} +30 -16
- package/dist/chunk-Q6LQYDKL.js +208 -0
- package/dist/cli.js +6 -3
- package/dist/index.js +1 -1
- package/dist/node.export.d.ts +6 -0
- package/dist/node.export.js +2 -2
- package/package.json +3 -3
- package/src/cli.ts +5 -1
- package/src/default-fetcher.generator.ts +101 -0
- package/src/generate-client-files.ts +39 -5
- package/src/generator.ts +20 -14
- package/src/map-openapi-endpoints.ts +9 -2
- package/src/openapi-schema-to-ts.ts +3 -2
- package/dist/chunk-MCVYB63W.js +0 -78
|
@@ -150,7 +150,9 @@ var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => {
|
|
|
150
150
|
meta
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
|
-
additionalProperties = t.
|
|
153
|
+
additionalProperties = t.literal(
|
|
154
|
+
`Record<string, ${additionalPropertiesType ? additionalPropertiesType.value : t.any().value}>`
|
|
155
|
+
);
|
|
154
156
|
}
|
|
155
157
|
const hasRequiredArray = schema.required && schema.required.length > 0;
|
|
156
158
|
const isPartial = !schema.required?.length;
|
|
@@ -428,8 +430,18 @@ var generateSchemaList = ({ refs, runtime }) => {
|
|
|
428
430
|
${runtime === "none" ? "}" : ""}
|
|
429
431
|
`;
|
|
430
432
|
};
|
|
431
|
-
var parameterObjectToString = (parameters) => {
|
|
432
|
-
if (parameters instanceof Box)
|
|
433
|
+
var parameterObjectToString = (parameters, ctx) => {
|
|
434
|
+
if (parameters instanceof Box) {
|
|
435
|
+
if (ctx.runtime === "none") {
|
|
436
|
+
return parameters.recompute((box) => {
|
|
437
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
438
|
+
box.value = `Schemas.${box.value}`;
|
|
439
|
+
}
|
|
440
|
+
return box;
|
|
441
|
+
}).value;
|
|
442
|
+
}
|
|
443
|
+
return parameters.value;
|
|
444
|
+
}
|
|
433
445
|
let str = "{";
|
|
434
446
|
for (const [key, box] of Object.entries(parameters)) {
|
|
435
447
|
str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value},
|
|
@@ -478,16 +490,17 @@ var generateEndpointSchemaList = (ctx) => {
|
|
|
478
490
|
path: "${endpoint.path}",
|
|
479
491
|
requestFormat: "${endpoint.requestFormat}",
|
|
480
492
|
${endpoint.meta.hasParameters ? `parameters: {
|
|
481
|
-
${parameters.query ? `query: ${parameterObjectToString(parameters.query)},` : ""}
|
|
482
|
-
${parameters.path ? `path: ${parameterObjectToString(parameters.path)},` : ""}
|
|
483
|
-
${parameters.header ? `header: ${parameterObjectToString(parameters.header)},` : ""}
|
|
493
|
+
${parameters.query ? `query: ${parameterObjectToString(parameters.query, ctx)},` : ""}
|
|
494
|
+
${parameters.path ? `path: ${parameterObjectToString(parameters.path, ctx)},` : ""}
|
|
495
|
+
${parameters.header ? `header: ${parameterObjectToString(parameters.header, ctx)},` : ""}
|
|
484
496
|
${parameters.body ? `body: ${parameterObjectToString(
|
|
485
497
|
ctx.runtime === "none" ? parameters.body.recompute((box) => {
|
|
486
498
|
if (Box.isReference(box) && !box.params.generics) {
|
|
487
499
|
box.value = `Schemas.${box.value}`;
|
|
488
500
|
}
|
|
489
501
|
return box;
|
|
490
|
-
}) : parameters.body
|
|
502
|
+
}) : parameters.body,
|
|
503
|
+
ctx
|
|
491
504
|
)},` : ""}
|
|
492
505
|
}` : "parameters: never,"}
|
|
493
506
|
response: ${ctx.runtime === "none" ? endpoint.response.recompute((box) => {
|
|
@@ -708,13 +721,7 @@ export class ApiClient {
|
|
|
708
721
|
return withResponse ? typedResponse : data;
|
|
709
722
|
});
|
|
710
723
|
|
|
711
|
-
return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with(
|
|
712
|
-
"arktype",
|
|
713
|
-
"io-ts",
|
|
714
|
-
"typebox",
|
|
715
|
-
"valibot",
|
|
716
|
-
() => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`
|
|
717
|
-
).otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
724
|
+
return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`).otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
718
725
|
}
|
|
719
726
|
// </ApiClient.${method}>
|
|
720
727
|
` : "";
|
|
@@ -1128,10 +1135,17 @@ var mapOpenApiEndpoints = (doc, options) => {
|
|
|
1128
1135
|
endpoint.requestFormat = match2(matchingMediaType).with("application/octet-stream", () => "binary").with("multipart/form-data", () => "form-data").with("application/x-www-form-urlencoded", () => "form-url").with(P.string.includes("json"), () => "json").otherwise(() => "text");
|
|
1129
1136
|
}
|
|
1130
1137
|
if (params) {
|
|
1131
|
-
const t = createBoxFactory({}, ctx);
|
|
1132
1138
|
const filtered_params = ["query", "path", "header"];
|
|
1133
1139
|
for (const k of filtered_params) {
|
|
1134
1140
|
if (params[k] && lists[k].length) {
|
|
1141
|
+
const properties = Object.entries(params[k]).reduce(
|
|
1142
|
+
(acc, [key, value]) => {
|
|
1143
|
+
if (value.schema) acc[key] = value.schema;
|
|
1144
|
+
return acc;
|
|
1145
|
+
},
|
|
1146
|
+
{}
|
|
1147
|
+
);
|
|
1148
|
+
const t = createBoxFactory({ type: "object", properties }, ctx);
|
|
1135
1149
|
if (lists[k].every((param) => !param.required)) {
|
|
1136
1150
|
params[k] = t.reference("Partial", [t.object(params[k])]);
|
|
1137
1151
|
} else {
|
|
@@ -1219,7 +1233,7 @@ var allowedParamMediaTypes = [
|
|
|
1219
1233
|
"*/*"
|
|
1220
1234
|
];
|
|
1221
1235
|
var isAllowedParamMediaTypes = (mediaType) => mediaType.includes("application/") && mediaType.includes("json") || allowedParamMediaTypes.includes(mediaType) || mediaType.includes("text/");
|
|
1222
|
-
var isResponseMediaType = (mediaType) => mediaType === "application/json";
|
|
1236
|
+
var isResponseMediaType = (mediaType) => mediaType === "*/*" || mediaType.includes("application/") && mediaType.includes("json");
|
|
1223
1237
|
var getAlias = ({ path, method, operation }) => sanitizeName(
|
|
1224
1238
|
(method + "_" + capitalize3(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
|
|
1225
1239
|
"endpoint"
|
|
@@ -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-KF4JBLDM.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-Q6LQYDKL.js";
|
|
4
4
|
import {
|
|
5
5
|
allowedRuntimes
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-KF4JBLDM.js";
|
|
7
7
|
import "./chunk-KAEXXJ7X.js";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
@@ -20,7 +20,10 @@ cli.command("<input>", "Generate").option("-o, --output <path>", "Output path fo
|
|
|
20
20
|
"Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)"
|
|
21
21
|
).option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)").option(
|
|
22
22
|
"--tanstack [name]",
|
|
23
|
-
"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"
|
|
24
27
|
).action(async (input, _options) => {
|
|
25
28
|
return generateClientFiles(input, _options);
|
|
26
29
|
});
|
package/dist/index.js
CHANGED
package/dist/node.export.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ 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
|
+
};
|
|
11
17
|
includeClient?: boolean | "false" | "true";
|
|
12
18
|
successStatusCodes?: string;
|
|
13
19
|
errorStatusCodes?: string;
|
package/dist/node.export.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
"dev": "tsup --watch",
|
|
67
67
|
"build": "tsup",
|
|
68
68
|
"test": "vitest",
|
|
69
|
-
"
|
|
69
|
+
"gen:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts --default-fetcher",
|
|
70
70
|
"test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
|
|
71
|
-
"test:runtime": "pnpm run
|
|
71
|
+
"test:runtime": "pnpm run gen:runtime && pnpm run test:runtime:run",
|
|
72
72
|
"fmt": "prettier --write src",
|
|
73
73
|
"typecheck": "tsc -b ./tsconfig.build.json"
|
|
74
74
|
}
|
package/src/cli.ts
CHANGED
|
@@ -23,7 +23,11 @@ cli
|
|
|
23
23
|
.option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)")
|
|
24
24
|
.option(
|
|
25
25
|
"--tanstack [name]",
|
|
26
|
-
"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",
|
|
27
31
|
)
|
|
28
32
|
.action(async (input: string, _options: any) => {
|
|
29
33
|
return generateClientFiles(input, _options);
|
|
@@ -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,6 +1,6 @@
|
|
|
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
6
|
import {
|
|
@@ -14,6 +14,7 @@ import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
|
14
14
|
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
15
15
|
import { prettify } from "./format.ts";
|
|
16
16
|
import type { NameTransformOptions } from "./types.ts";
|
|
17
|
+
import { generateDefaultFetcher } from "./default-fetcher.generator.ts";
|
|
17
18
|
|
|
18
19
|
const cwd = process.cwd();
|
|
19
20
|
const now = new Date();
|
|
@@ -30,6 +31,12 @@ export const optionsSchema = type({
|
|
|
30
31
|
"output?": "string",
|
|
31
32
|
runtime: allowedRuntimes,
|
|
32
33
|
tanstack: "boolean | string",
|
|
34
|
+
"defaultFetcher?": type({
|
|
35
|
+
"envApiBaseUrl?": "string",
|
|
36
|
+
"clientPath?": "string",
|
|
37
|
+
"fetcherName?": "string",
|
|
38
|
+
"apiName?": "string",
|
|
39
|
+
}),
|
|
33
40
|
schemasOnly: "boolean",
|
|
34
41
|
"includeClient?": "boolean | 'true' | 'false'",
|
|
35
42
|
"successStatusCodes?": "string",
|
|
@@ -41,6 +48,7 @@ type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
|
41
48
|
};
|
|
42
49
|
|
|
43
50
|
export async function generateClientFiles(input: string, options: GenerateClientFilesOptions) {
|
|
51
|
+
// TODO CLI option to save that file?
|
|
44
52
|
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
|
|
45
53
|
|
|
46
54
|
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
@@ -85,14 +93,40 @@ export async function generateClientFiles(input: string, options: GenerateClient
|
|
|
85
93
|
...generatorOptions,
|
|
86
94
|
relativeApiClientPath: "./" + basename(outputPath),
|
|
87
95
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
105
|
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
93
106
|
await ensureDir(dirname(tanstackOutputPath));
|
|
94
107
|
await writeFile(tanstackOutputPath, tanstackContent);
|
|
95
108
|
}
|
|
96
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
|
+
|
|
97
131
|
console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
|
|
98
132
|
}
|
package/src/generator.ts
CHANGED
|
@@ -149,8 +149,19 @@ const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
|
|
|
149
149
|
);
|
|
150
150
|
};
|
|
151
151
|
|
|
152
|
-
const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, AnyBox
|
|
153
|
-
if (parameters instanceof Box)
|
|
152
|
+
const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, AnyBox>, ctx: GeneratorContext) => {
|
|
153
|
+
if (parameters instanceof Box) {
|
|
154
|
+
if (ctx.runtime === "none") {
|
|
155
|
+
return parameters.recompute((box) => {
|
|
156
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
157
|
+
box.value = `Schemas.${box.value}`;
|
|
158
|
+
}
|
|
159
|
+
return box;
|
|
160
|
+
}).value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return parameters.value;
|
|
164
|
+
}
|
|
154
165
|
|
|
155
166
|
let str = "{";
|
|
156
167
|
for (const [key, box] of Object.entries(parameters)) {
|
|
@@ -210,9 +221,9 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
210
221
|
${
|
|
211
222
|
endpoint.meta.hasParameters
|
|
212
223
|
? `parameters: {
|
|
213
|
-
${parameters.query ? `query: ${parameterObjectToString(parameters.query)},` : ""}
|
|
214
|
-
${parameters.path ? `path: ${parameterObjectToString(parameters.path)},` : ""}
|
|
215
|
-
${parameters.header ? `header: ${parameterObjectToString(parameters.header)},` : ""}
|
|
224
|
+
${parameters.query ? `query: ${parameterObjectToString(parameters.query, ctx)},` : ""}
|
|
225
|
+
${parameters.path ? `path: ${parameterObjectToString(parameters.path, ctx)},` : ""}
|
|
226
|
+
${parameters.header ? `header: ${parameterObjectToString(parameters.header, ctx)},` : ""}
|
|
216
227
|
${
|
|
217
228
|
parameters.body
|
|
218
229
|
? `body: ${parameterObjectToString(
|
|
@@ -224,6 +235,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
224
235
|
return box;
|
|
225
236
|
})
|
|
226
237
|
: parameters.body,
|
|
238
|
+
ctx,
|
|
227
239
|
)},`
|
|
228
240
|
: ""
|
|
229
241
|
}
|
|
@@ -487,15 +499,9 @@ export class ApiClient {
|
|
|
487
499
|
});
|
|
488
500
|
|
|
489
501
|
return promise ${match(ctx.runtime)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
"io-ts",
|
|
494
|
-
"typebox",
|
|
495
|
-
"valibot",
|
|
496
|
-
() => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`,
|
|
497
|
-
)
|
|
498
|
-
.otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
502
|
+
.with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
|
|
503
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`)
|
|
504
|
+
.otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
499
505
|
}
|
|
500
506
|
// </ApiClient.${method}>
|
|
501
507
|
`
|
|
@@ -105,13 +105,20 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
105
105
|
|
|
106
106
|
// Make parameters optional if all or some of them are not required
|
|
107
107
|
if (params) {
|
|
108
|
-
const t = createBoxFactory({}, ctx);
|
|
109
108
|
const filtered_params = ["query", "path", "header"] as Array<
|
|
110
109
|
keyof Pick<typeof params, "query" | "path" | "header">
|
|
111
110
|
>;
|
|
112
111
|
|
|
113
112
|
for (const k of filtered_params) {
|
|
114
113
|
if (params[k] && lists[k].length) {
|
|
114
|
+
const properties = Object.entries(params[k]!).reduce(
|
|
115
|
+
(acc, [key, value]) => {
|
|
116
|
+
if (value.schema) acc[key] = value.schema;
|
|
117
|
+
return acc;
|
|
118
|
+
},
|
|
119
|
+
{} as Record<string, NonNullable<AnyBox["schema"]>>,
|
|
120
|
+
);
|
|
121
|
+
const t = createBoxFactory({ type: "object", properties: properties }, ctx);
|
|
115
122
|
if (lists[k].every((param) => !param.required)) {
|
|
116
123
|
params[k] = t.reference("Partial", [t.object(params[k]!)]) as any;
|
|
117
124
|
} else {
|
|
@@ -226,7 +233,7 @@ const isAllowedParamMediaTypes = (
|
|
|
226
233
|
allowedParamMediaTypes.includes(mediaType as any) ||
|
|
227
234
|
mediaType.includes("text/");
|
|
228
235
|
|
|
229
|
-
const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
|
|
236
|
+
const isResponseMediaType = (mediaType: string) => mediaType === "*/*" || (mediaType.includes("application/") && mediaType.includes("json"));
|
|
230
237
|
const getAlias = ({ path, method, operation }: Endpoint) =>
|
|
231
238
|
sanitizeName(
|
|
232
239
|
(method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
|
|
@@ -16,7 +16,6 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
16
16
|
const getTs = () => {
|
|
17
17
|
if (isReferenceObject(schema)) {
|
|
18
18
|
const refInfo = ctx.refs.getInfosByRef(schema.$ref);
|
|
19
|
-
|
|
20
19
|
return t.reference(refInfo.normalized);
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -153,7 +152,9 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
153
152
|
});
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
additionalProperties = t.
|
|
155
|
+
additionalProperties = t.literal(
|
|
156
|
+
`Record<string, ${additionalPropertiesType ? additionalPropertiesType.value : t.any().value}>`,
|
|
157
|
+
);
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
const hasRequiredArray = schema.required && schema.required.length > 0;
|
package/dist/chunk-MCVYB63W.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_ERROR_STATUS_CODES,
|
|
3
|
-
DEFAULT_SUCCESS_STATUS_CODES,
|
|
4
|
-
allowedRuntimes,
|
|
5
|
-
generateFile,
|
|
6
|
-
generateTanstackQueryFile,
|
|
7
|
-
mapOpenApiEndpoints
|
|
8
|
-
} from "./chunk-E6A7N4ND.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 } from "pathe";
|
|
16
|
-
import { type } from "arktype";
|
|
17
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
18
|
-
var cwd = process.cwd();
|
|
19
|
-
var now = /* @__PURE__ */ new Date();
|
|
20
|
-
async function ensureDir(dirPath) {
|
|
21
|
-
try {
|
|
22
|
-
await mkdir(dirPath, { recursive: true });
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error(`Error ensuring directory: ${error.message}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
var optionsSchema = type({
|
|
28
|
-
"output?": "string",
|
|
29
|
-
runtime: allowedRuntimes,
|
|
30
|
-
tanstack: "boolean | string",
|
|
31
|
-
schemasOnly: "boolean",
|
|
32
|
-
"includeClient?": "boolean | 'true' | 'false'",
|
|
33
|
-
"successStatusCodes?": "string",
|
|
34
|
-
"errorStatusCodes?": "string"
|
|
35
|
-
});
|
|
36
|
-
async function generateClientFiles(input, options) {
|
|
37
|
-
const openApiDoc = await SwaggerParser.bundle(input);
|
|
38
|
-
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
39
|
-
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
40
|
-
const successStatusCodes = options.successStatusCodes ? options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) : void 0;
|
|
41
|
-
const errorStatusCodes = options.errorStatusCodes ? options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) : void 0;
|
|
42
|
-
const includeClient = options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
|
|
43
|
-
const generatorOptions = {
|
|
44
|
-
...ctx,
|
|
45
|
-
runtime: options.runtime,
|
|
46
|
-
schemasOnly: options.schemasOnly,
|
|
47
|
-
nameTransform: options.nameTransform,
|
|
48
|
-
includeClient: includeClient ?? true,
|
|
49
|
-
successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
50
|
-
errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES
|
|
51
|
-
};
|
|
52
|
-
const content = await prettify(generateFile(generatorOptions));
|
|
53
|
-
const outputPath = join(
|
|
54
|
-
cwd,
|
|
55
|
-
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`
|
|
56
|
-
);
|
|
57
|
-
console.log("Generating client...", outputPath);
|
|
58
|
-
await ensureDir(dirname(outputPath));
|
|
59
|
-
await writeFile(outputPath, content);
|
|
60
|
-
if (options.tanstack) {
|
|
61
|
-
const tanstackContent = await generateTanstackQueryFile({
|
|
62
|
-
...generatorOptions,
|
|
63
|
-
relativeApiClientPath: "./" + basename(outputPath)
|
|
64
|
-
});
|
|
65
|
-
const tanstackOutputPath = join(
|
|
66
|
-
dirname(outputPath),
|
|
67
|
-
typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`
|
|
68
|
-
);
|
|
69
|
-
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
70
|
-
await ensureDir(dirname(tanstackOutputPath));
|
|
71
|
-
await writeFile(tanstackOutputPath, tanstackContent);
|
|
72
|
-
}
|
|
73
|
-
console.log(`Done in ${(/* @__PURE__ */ new Date()).getTime() - now.getTime()}ms !`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export {
|
|
77
|
-
generateClientFiles
|
|
78
|
-
};
|