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.
@@ -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-O7DZWQK4.js";
3
+ } from "./chunk-GD55PFNE.js";
4
4
  import {
5
5
  allowedRuntimes
6
- } from "./chunk-OVT6OLBK.js";
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 <name>",
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-DLE5RaXi.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-DLE5RaXi.js';
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
@@ -8,7 +8,7 @@ import {
8
8
  openApiSchemaToTs,
9
9
  tsFactory,
10
10
  unwrap
11
- } from "./chunk-OVT6OLBK.js";
11
+ } from "./chunk-4EZJSCLI.js";
12
12
  import "./chunk-KAEXXJ7X.js";
13
13
  export {
14
14
  createBoxFactory,
@@ -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-DLE5RaXi.js';
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;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-O7DZWQK4.js";
4
- import "./chunk-OVT6OLBK.js";
3
+ } from "./chunk-GD55PFNE.js";
4
+ import "./chunk-4EZJSCLI.js";
5
5
  import "./chunk-KAEXXJ7X.js";
6
6
  export {
7
7
  generateClientFiles
@@ -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": "1.5.1",
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 <name>",
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 { allowedRuntimes, generateFile } from "./generator.ts";
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
- const content = await prettify(
41
- generateFile({
42
- ...ctx,
43
- runtime: options.runtime,
44
- schemasOnly: options.schemasOnly,
45
- nameTransform: options.nameTransform,
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
- ...ctx,
93
+ ...generatorOptions,
60
94
  relativeApiClientPath: "./" + basename(outputPath),
61
95
  });
62
- const tanstackOutputPath = join(
63
- dirname(outputPath),
64
- typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`,
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
  }