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.
@@ -150,7 +150,9 @@ var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => {
150
150
  meta
151
151
  });
152
152
  }
153
- additionalProperties = t.object({ [t.string().value]: additionalPropertiesType });
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) return parameters.value;
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-MCVYB63W.js";
3
+ } from "./chunk-Q6LQYDKL.js";
4
4
  import {
5
5
  allowedRuntimes
6
- } from "./chunk-E6A7N4ND.js";
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
@@ -8,7 +8,7 @@ import {
8
8
  openApiSchemaToTs,
9
9
  tsFactory,
10
10
  unwrap
11
- } from "./chunk-E6A7N4ND.js";
11
+ } from "./chunk-KF4JBLDM.js";
12
12
  import "./chunk-KAEXXJ7X.js";
13
13
  export {
14
14
  createBoxFactory,
@@ -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;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-MCVYB63W.js";
4
- import "./chunk-E6A7N4ND.js";
3
+ } from "./chunk-Q6LQYDKL.js";
4
+ import "./chunk-KF4JBLDM.js";
5
5
  import "./chunk-KAEXXJ7X.js";
6
6
  export {
7
7
  generateClientFiles
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "2.0.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
- "generate:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts",
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 generate:runtime && pnpm run test:runtime: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
- const tanstackOutputPath = join(
89
- dirname(outputPath),
90
- typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`,
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) return parameters.value;
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
- .with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
491
- .with(
492
- "arktype",
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.object({ [t.string().value]: additionalPropertiesType! });
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;
@@ -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
- };