typed-openapi 2.2.3 → 2.2.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "2.2.3",
4
+ "version": "2.2.6",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "exports": {
@@ -18,27 +18,27 @@
18
18
  "directory": "packages/typed-openapi"
19
19
  },
20
20
  "dependencies": {
21
- "@apidevtools/swagger-parser": "^10.1.1",
21
+ "@apidevtools/swagger-parser": "^12.1.0",
22
22
  "@sinclair/typebox-codegen": "^0.11.1",
23
- "arktype": "2.1.20",
24
- "cac": "^6.7.14",
25
- "openapi3-ts": "^4.4.0",
23
+ "arktype": "2.2.0",
24
+ "cac": "7.0.0",
25
+ "openapi3-ts": "4.5.0",
26
+ "oxfmt": "0.45.0",
26
27
  "pastable": "^2.2.1",
27
- "pathe": "^2.0.3",
28
- "prettier": "3.5.3",
29
- "ts-pattern": "^5.7.0"
28
+ "pathe": "2.0.3",
29
+ "ts-pattern": "^5.9.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@changesets/cli": "^2.29.4",
33
- "@tanstack/react-query": "5.85.0",
34
- "@types/node": "^22.15.17",
35
- "@types/prettier": "3.0.0",
36
- "msw": "2.10.5",
37
- "tstyche": "5.0.0",
38
- "tsup": "^8.4.0",
39
- "typescript": "^5.8.3",
40
- "vitest": "^3.1.3",
41
- "zod": "3.21.4"
32
+ "@changesets/cli": "^2.30.0",
33
+ "@tanstack/react-query": "5.99.0",
34
+ "@types/node": "^25.6.0",
35
+ "msw": "2.13.3",
36
+ "tstyche": "7.0.0",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "^6.0.2",
39
+ "vite": "7.3.2",
40
+ "vitest": "^4.1.4",
41
+ "zod": "4.3.6"
42
42
  },
43
43
  "files": [
44
44
  "src",
@@ -72,7 +72,7 @@
72
72
  "gen:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts --default-fetcher",
73
73
  "test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
74
74
  "test:runtime": "pnpm run gen:runtime && pnpm run test:runtime:run",
75
- "fmt": "prettier --write src",
75
+ "fmt": "oxfmt --write src tests",
76
76
  "typecheck": "tsc -b ./tsconfig.build.json"
77
77
  }
78
78
  }
package/src/cli.ts CHANGED
@@ -14,8 +14,12 @@ cli
14
14
  `Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
15
15
  { default: "none" },
16
16
  )
17
+ .option("--format", "Format generated files with oxfmt (defaults to false)", { default: false })
17
18
  .option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false })
18
19
  .option("--include-client", "Include API client types and implementation (defaults to true)", { default: true })
20
+ .option("--jsdoc", "Emit OpenAPI descriptions as JSDoc comments (defaults to false)", {
21
+ default: true,
22
+ })
19
23
  .option(
20
24
  "--success-status-codes <codes>",
21
25
  "Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)",
package/src/format.ts CHANGED
@@ -1,20 +1,75 @@
1
- import prettier, { type Options } from "prettier";
2
- import parserTypescript from "prettier/parser-typescript";
1
+ import { readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ type OxcFormatOptions = import("oxfmt").FormatConfig;
7
+
8
+ export type PrettifyOptions = OxcFormatOptions & {
9
+ enabled?: boolean;
10
+ filePath?: string;
11
+ };
12
+
13
+ const defaultFilePath = "generated.ts";
14
+ const require = createRequire(import.meta.url);
15
+
16
+ async function formatWithBundledOxfmt(filePath: string, input: string, options?: OxcFormatOptions): Promise<string> {
17
+ const packageJsonPath = require.resolve("oxfmt/package.json");
18
+ const packageRoot = dirname(packageJsonPath);
19
+ const indexSource = await readFile(join(packageRoot, "dist", "index.js"), "utf8");
20
+ const match = indexSource.match(/from ["']\.\/(apis-[^"']+\.js)["']/);
21
+ const internalFile = match?.[1];
22
+
23
+ if (!internalFile) {
24
+ throw new Error("Unable to locate oxfmt's bundled JS formatter");
25
+ }
26
+
27
+ const internalUrl = pathToFileURL(join(packageRoot, "dist", internalFile)).href;
28
+ const { r: formatFile } = await import(internalUrl);
29
+
30
+ return formatFile({
31
+ code: input,
32
+ options: {
33
+ printWidth: 120,
34
+ trailingComma: "all",
35
+ ...options,
36
+ filepath: filePath,
37
+ },
38
+ });
39
+ }
3
40
 
4
41
  /** @see https://github.dev/stephenh/ts-poet/blob/5ea0dbb3c9f1f4b0ee51a54abb2d758102eda4a2/src/Code.ts#L231 */
5
- function maybePretty(input: string, options?: Options | null) {
42
+ async function maybePretty(input: string, options?: PrettifyOptions | null) {
43
+ const { enabled, filePath = defaultFilePath, ...formatOptions } = options ?? {};
44
+
45
+ if (enabled === false) {
46
+ return input;
47
+ }
48
+
6
49
  try {
7
- return prettier.format(input, {
8
- parser: "typescript",
9
- plugins: [parserTypescript],
10
- ...options,
50
+ const { format } = await import("oxfmt");
51
+ const result = await format(filePath, input, {
52
+ printWidth: 120,
53
+ trailingComma: "all",
54
+ ...formatOptions,
11
55
  });
12
- } catch (err) {
13
- console.warn("Failed to format code");
14
- console.warn(err);
15
- return input; // assume it's invalid syntax and ignore
56
+
57
+ if (result.errors.length > 0) {
58
+ console.warn(`Failed to format code in ${filePath}`);
59
+ console.warn(result.errors);
60
+ return input;
61
+ }
62
+
63
+ return result.code;
64
+ } catch {
65
+ try {
66
+ return await formatWithBundledOxfmt(filePath, input, formatOptions);
67
+ } catch (err) {
68
+ console.warn(`Failed to format code in ${filePath}`);
69
+ console.warn(err);
70
+ return input; // assume it's invalid syntax and ignore
71
+ }
16
72
  }
17
73
  }
18
74
 
19
- export const prettify = (str: string, options?: Options | null) =>
20
- maybePretty(str, { printWidth: 120, trailingComma: "all", ...options });
75
+ export const prettify = (str: string, options?: PrettifyOptions | null) => maybePretty(str, options);
@@ -30,6 +30,7 @@ async function ensureDir(dirPath: string): Promise<void> {
30
30
  export const optionsSchema = type({
31
31
  "output?": "string",
32
32
  runtime: allowedRuntimes,
33
+ "format?": "boolean | 'true' | 'false'",
33
34
  tanstack: "boolean | string",
34
35
  "defaultFetcher?": type({
35
36
  "envApiBaseUrl?": "string",
@@ -39,14 +40,27 @@ export const optionsSchema = type({
39
40
  }),
40
41
  schemasOnly: "boolean",
41
42
  "includeClient?": "boolean | 'true' | 'false'",
43
+ "jsdoc?": "boolean | 'true' | 'false'",
42
44
  "successStatusCodes?": "string",
43
45
  "errorStatusCodes?": "string",
44
46
  });
45
47
 
46
- type GenerateClientFilesOptions = typeof optionsSchema.infer & {
48
+ export type GenerateClientFilesOptions = typeof optionsSchema.infer & {
47
49
  nameTransform?: NameTransformOptions;
48
50
  };
49
51
 
52
+ function parseBooleanOption(value: boolean | "true" | "false" | undefined) {
53
+ if (value === "false") {
54
+ return false;
55
+ }
56
+
57
+ if (value === "true") {
58
+ return true;
59
+ }
60
+
61
+ return value;
62
+ }
63
+
50
64
  export async function generateClientFiles(input: string, options: GenerateClientFilesOptions) {
51
65
  // TODO CLI option to save that file?
52
66
  const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
@@ -65,8 +79,9 @@ export async function generateClientFiles(input: string, options: GenerateClient
65
79
  : undefined;
66
80
 
67
81
  // Convert string boolean to actual boolean
68
- const includeClient =
69
- options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
82
+ const includeClient = parseBooleanOption(options.includeClient);
83
+ const jsdoc = parseBooleanOption(options.jsdoc) ?? true;
84
+ const shouldFormat = parseBooleanOption(options.format) ?? false;
70
85
 
71
86
  const generatorOptions: GeneratorOptions = {
72
87
  ...ctx,
@@ -74,25 +89,22 @@ export async function generateClientFiles(input: string, options: GenerateClient
74
89
  schemasOnly: options.schemasOnly,
75
90
  nameTransform: options.nameTransform,
76
91
  includeClient: includeClient ?? true,
92
+ jsdoc,
77
93
  successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
78
94
  errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
79
95
  };
80
96
 
81
- const content = await prettify(generateFile(generatorOptions));
82
97
  const outputPath = join(
83
98
  cwd,
84
99
  options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
85
100
  );
101
+ const content = await prettify(generateFile(generatorOptions), { enabled: shouldFormat, filePath: outputPath });
86
102
 
87
103
  console.log("Generating client...", outputPath);
88
104
  await ensureDir(dirname(outputPath));
89
105
  await writeFile(outputPath, content);
90
106
 
91
107
  if (options.tanstack) {
92
- const tanstackContent = await generateTanstackQueryFile({
93
- ...generatorOptions,
94
- relativeApiClientPath: "./" + basename(outputPath),
95
- });
96
108
  let tanstackOutputPath: string;
97
109
  if (typeof options.tanstack === "string" && isAbsolute(options.tanstack)) {
98
110
  tanstackOutputPath = options.tanstack;
@@ -102,6 +114,13 @@ export async function generateClientFiles(input: string, options: GenerateClient
102
114
  typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`,
103
115
  );
104
116
  }
117
+ const tanstackContent = await prettify(
118
+ await generateTanstackQueryFile({
119
+ ...generatorOptions,
120
+ relativeApiClientPath: "./" + basename(outputPath),
121
+ }),
122
+ { enabled: shouldFormat, filePath: tanstackOutputPath },
123
+ );
105
124
  console.log("Generating tanstack client...", tanstackOutputPath);
106
125
  await ensureDir(dirname(tanstackOutputPath));
107
126
  await writeFile(tanstackOutputPath, tanstackContent);
@@ -123,9 +142,13 @@ export async function generateClientFiles(input: string, options: GenerateClient
123
142
  typeof options.defaultFetcher === "string" ? options.defaultFetcher : `api.client.ts`,
124
143
  );
125
144
  }
145
+ const formattedDefaultFetcherContent = await prettify(defaultFetcherContent, {
146
+ enabled: shouldFormat,
147
+ filePath: defaultFetcherOutputPath,
148
+ });
126
149
  console.log("Generating default fetcher...", defaultFetcherOutputPath);
127
150
  await ensureDir(dirname(defaultFetcherOutputPath));
128
- await writeFile(defaultFetcherOutputPath, defaultFetcherContent);
151
+ await writeFile(defaultFetcherOutputPath, formattedDefaultFetcherContent);
129
152
  }
130
153
 
131
154
  console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
package/src/generator.ts CHANGED
@@ -28,6 +28,7 @@ export type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
28
28
  successStatusCodes?: readonly number[];
29
29
  errorStatusCodes?: readonly number[];
30
30
  includeClient?: boolean;
31
+ jsdoc?: boolean;
31
32
  };
32
33
  type GeneratorContext = Required<GeneratorOptions>;
33
34
 
@@ -77,6 +78,30 @@ const replacerByRuntime = {
77
78
  ),
78
79
  };
79
80
 
81
+ const shouldRenderDescriptionComments = (ctx: GeneratorContext) => ctx.jsdoc && ctx.runtime === "none";
82
+
83
+ const escapeCommentText = (text: string) => text.replace(/\*\//g, "*\\/");
84
+
85
+ const renderDescriptionComment = (description: string, indent = "") => {
86
+ const lines = description.trim().split(/\r?\n/);
87
+
88
+ return `${indent}/**\n${lines.map((line) => `${indent} * ${escapeCommentText(line)}`).join("\n")}\n${indent} */`;
89
+ };
90
+
91
+ const getSchemaDescription = (schema: Box<AnyBoxDef>["schema"]) => {
92
+ if (!schema || typeof schema !== "object") return undefined;
93
+ if ("description" in schema && typeof schema.description === "string") return schema.description;
94
+ return undefined;
95
+ };
96
+
97
+ const indentMultiline = (value: string, indent = " ") =>
98
+ value.includes("\n")
99
+ ? value
100
+ .split("\n")
101
+ .map((line, index) => (index === 0 ? line : `${indent}${line}`))
102
+ .join("\n")
103
+ : value;
104
+
80
105
  export const generateFile = (options: GeneratorOptions) => {
81
106
  const ctx = {
82
107
  ...options,
@@ -84,6 +109,7 @@ export const generateFile = (options: GeneratorOptions) => {
84
109
  successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
85
110
  errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
86
111
  includeClient: options.includeClient ?? true,
112
+ jsdoc: options.jsdoc ?? false,
87
113
  } as GeneratorContext;
88
114
 
89
115
  const schemaList = generateSchemaList(ctx);
@@ -128,7 +154,8 @@ export const generateFile = (options: GeneratorOptions) => {
128
154
  return file;
129
155
  };
130
156
 
131
- const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
157
+ const generateSchemaList = (ctx: GeneratorContext) => {
158
+ const { refs, runtime } = ctx;
132
159
  let file = `
133
160
  ${runtime === "none" ? "export namespace Schemas {" : ""}
134
161
  // <Schemas>
@@ -137,7 +164,13 @@ const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
137
164
  if (!infos?.name) return;
138
165
  if (infos.kind !== "schemas") return;
139
166
 
140
- file += `export type ${infos.normalized} = ${schema.value}\n`;
167
+ const description = shouldRenderDescriptionComments(ctx) ? getSchemaDescription(schema.schema) : undefined;
168
+ const schemaValue =
169
+ shouldRenderDescriptionComments(ctx) && !Box.isReference(schema)
170
+ ? boxToString(schema, ctx, { prefixRefsWithSchemas: false })
171
+ : schema.value;
172
+
173
+ file += `${description ? `${renderDescriptionComment(description)}\n` : ""}export type ${infos.normalized} = ${schemaValue}\n`;
141
174
  });
142
175
 
143
176
  return (
@@ -149,31 +182,108 @@ const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
149
182
  );
150
183
  };
151
184
 
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;
185
+ const boxToString = (
186
+ box: Box<AnyBoxDef>,
187
+ ctx: GeneratorContext,
188
+ options: { prefixRefsWithSchemas?: boolean } = {},
189
+ ): string => {
190
+ const prefixRefsWithSchemas = options.prefixRefsWithSchemas ?? true;
191
+
192
+ if (ctx.runtime !== "none") {
193
+ return box.value;
194
+ }
195
+
196
+ const renderValue = (value: any): string => {
197
+ if (typeof value === "string") {
198
+ return value;
199
+ }
200
+
201
+ if (Box.isBox(value)) {
202
+ return boxToString(value, ctx, options);
203
+ }
204
+
205
+ return value.value;
206
+ };
207
+
208
+ if (Box.isUnion(box)) {
209
+ return `(${box.params.types.map((type) => renderValue(type)).join(" | ")})`;
210
+ }
211
+
212
+ if (Box.isIntersection(box)) {
213
+ return `(${box.params.types.map((type) => renderValue(type)).join(" & ")})`;
214
+ }
215
+
216
+ if (Box.isArray(box)) {
217
+ return `Array<${renderValue(box.params.type)}>`;
218
+ }
219
+
220
+ if (Box.isOptional(box)) {
221
+ return `${renderValue(box.params.type)} | undefined`;
222
+ }
223
+
224
+ if (Box.isObject(box)) {
225
+ const renderedProps = Object.entries(box.params.props).map(([prop, type]) => {
226
+ const isOptional = typeof type !== "string" && Box.isBox(type) && Box.isOptional(type);
227
+ const renderedValue = indentMultiline(renderValue(type));
228
+ const description =
229
+ shouldRenderDescriptionComments(ctx) && typeof type !== "string" && Box.isBox(type)
230
+ ? getSchemaDescription(type.schema)
231
+ : undefined;
232
+
233
+ return {
234
+ description,
235
+ line: `${wrapWithQuotesIfNeeded(prop)}${isOptional ? "?" : ""}: ${renderedValue};`,
236
+ };
237
+ });
238
+
239
+ const shouldRenderMultiline =
240
+ shouldRenderDescriptionComments(ctx) &&
241
+ renderedProps.some(({ description, line }) => description || line.includes("\n"));
242
+
243
+ if (!shouldRenderMultiline) {
244
+ const propsString = renderedProps.map(({ line }) => line.slice(0, -1)).join(", ");
245
+ return `{ ${propsString} }`;
161
246
  }
162
247
 
163
- return parameters.value;
248
+ const propsString = renderedProps
249
+ .map(({ description, line }) => {
250
+ const comment = description ? `${renderDescriptionComment(description, " ")}\n` : "";
251
+ return `${comment} ${line}`;
252
+ })
253
+ .join("\n");
254
+
255
+ return `{
256
+ ${propsString}
257
+ }`;
258
+ }
259
+
260
+ if (Box.isReference(box)) {
261
+ if (!box.params.generics) {
262
+ return box.value === "null" ? box.value : prefixRefsWithSchemas ? `Schemas.${box.value}` : box.value;
263
+ }
264
+
265
+ return `${box.params.name}<${box.params.generics.map((type) => renderValue(type)).join(", ")}>`;
266
+ }
267
+
268
+ return box.value;
269
+ };
270
+
271
+ const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, AnyBox>, ctx: GeneratorContext) => {
272
+ if (parameters instanceof Box) {
273
+ return boxToString(parameters, ctx);
164
274
  }
165
275
 
166
276
  let str = "{";
167
277
  for (const [key, box] of Object.entries(parameters)) {
168
- str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value},\n`;
278
+ str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${indentMultiline(boxToString(box, ctx))},\n`;
169
279
  }
170
280
  return str + "}";
171
281
  };
172
282
 
173
- const responseHeadersObjectToString = (responseHeaders: Record<string, Box<BoxObject>>) => {
283
+ const responseHeadersObjectToString = (responseHeaders: Record<string, Box<BoxObject>>, ctx: GeneratorContext) => {
174
284
  let str = "{";
175
285
  for (const [key, responseHeader] of Object.entries(responseHeaders)) {
176
- str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${responseHeader.value},\n`;
286
+ str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${indentMultiline(boxToString(responseHeader, ctx))},\n`;
177
287
  }
178
288
  return str + "}";
179
289
  };
@@ -181,16 +291,7 @@ const responseHeadersObjectToString = (responseHeaders: Record<string, Box<BoxOb
181
291
  const generateResponsesObject = (responses: Record<string, AnyBox>, ctx: GeneratorContext) => {
182
292
  let str = "{";
183
293
  for (const [statusCode, responseType] of Object.entries(responses)) {
184
- const value =
185
- ctx.runtime === "none"
186
- ? responseType.recompute((box) => {
187
- if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
188
- box.value = `Schemas.${box.value}`;
189
- }
190
-
191
- return box;
192
- }).value
193
- : responseType.value;
294
+ const value = indentMultiline(boxToString(responseType, ctx));
194
295
  str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value},\n`;
195
296
  }
196
297
  return str + "}";
@@ -204,7 +305,9 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
204
305
  `;
205
306
  ctx.endpointList.map((endpoint) => {
206
307
  const parameters = endpoint.parameters ?? {};
207
- file += `export type ${endpoint.meta.alias} = {
308
+ const description = shouldRenderDescriptionComments(ctx) ? endpoint.operation.description : undefined;
309
+
310
+ file += `${description ? `${renderDescriptionComment(description)}\n` : ""}export type ${endpoint.meta.alias} = {
208
311
  method: "${endpoint.method.toUpperCase()}",
209
312
  path: "${endpoint.path}",
210
313
  requestFormat: "${endpoint.requestFormat}",
@@ -214,26 +317,12 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
214
317
  ${parameters.query ? `query: ${parameterObjectToString(parameters.query, ctx)},` : ""}
215
318
  ${parameters.path ? `path: ${parameterObjectToString(parameters.path, ctx)},` : ""}
216
319
  ${parameters.header ? `header: ${parameterObjectToString(parameters.header, ctx)},` : ""}
217
- ${
218
- parameters.body
219
- ? `body: ${parameterObjectToString(
220
- ctx.runtime === "none"
221
- ? parameters.body.recompute((box) => {
222
- if (Box.isReference(box) && !box.params.generics) {
223
- box.value = `Schemas.${box.value}`;
224
- }
225
- return box;
226
- })
227
- : parameters.body,
228
- ctx,
229
- )},`
230
- : ""
231
- }
320
+ ${parameters.body ? `body: ${parameterObjectToString(parameters.body, ctx)},` : ""}
232
321
  }`
233
322
  : "parameters: never,"
234
323
  }
235
324
  ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
236
- ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},` : ""}
325
+ ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},` : ""}
237
326
  }\n`;
238
327
  });
239
328
 
@@ -327,7 +416,7 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
327
416
 
328
417
  export interface Fetcher {
329
418
  decodePathParams?: (path: string, pathParams: Record<string, string>) => string
330
- encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
419
+ encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
331
420
  //
332
421
  fetch: (input: {
333
422
  method: Method;
@@ -1 +1 @@
1
- export { generateClientFiles } from "./generate-client-files.ts";
1
+ export { generateClientFiles, type GenerateClientFilesOptions } from "./generate-client-files.ts";
@@ -123,13 +123,9 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
123
123
 
124
124
  if (schemaType === "object" || schema.properties || schema.additionalProperties) {
125
125
  if (!schema.properties) {
126
- if (
127
- schema.additionalProperties &&
128
- !isReferenceObject(schema.additionalProperties) &&
129
- typeof schema.additionalProperties !== "boolean"
130
- ) {
126
+ if (schema.additionalProperties && typeof schema.additionalProperties !== "boolean") {
131
127
  const valueSchema = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta });
132
- return t.literal(`Record<string, ${valueSchema.value}>`);
128
+ return t.reference("Record", [t.string(), valueSchema]);
133
129
  }
134
130
 
135
131
  return t.literal("Record<string, unknown>");
@@ -151,9 +147,7 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
151
147
  });
152
148
  }
153
149
 
154
- additionalProperties = t.literal(
155
- `Record<string, ${additionalPropertiesType ? additionalPropertiesType.value : t.any().value}>`,
156
- );
150
+ additionalProperties = t.reference("Record", [t.string(), additionalPropertiesType ?? t.any()]);
157
151
  }
158
152
 
159
153
  const hasRequiredArray = schema.required && schema.required.length > 0;
@@ -180,7 +174,15 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
180
174
  return isPartial ? t.reference("Partial", [objectType]) : objectType;
181
175
  }
182
176
 
183
- if (!schemaType) return t.unknown();
177
+ if (!schemaType) {
178
+ const nullableKey = Object.keys(schema).filter((key) => !["nullable"].includes(key));
179
+
180
+ if (nullableKey.length === 0 && (schema as LibSchemaObject).nullable) {
181
+ return t.literal("null");
182
+ }
183
+
184
+ return t.unknown();
185
+ }
184
186
 
185
187
  throw new Error(`Unsupported schema type: ${schemaType}`);
186
188
  };
@@ -188,7 +190,7 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
188
190
  let output = getTs();
189
191
  if (!isReferenceObject(schema)) {
190
192
  // OpenAPI 3.1 does not have nullable, but OpenAPI 3.0 does
191
- if ((schema as LibSchemaObject).nullable) {
193
+ if ((schema as LibSchemaObject).nullable && output.value !== "null") {
192
194
  output = t.union([output, t.literal("null")]);
193
195
  }
194
196
  }
@@ -52,18 +52,35 @@ export const createRefResolver = (
52
52
  const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", ".");
53
53
  const map = get(doc, normalizedPath) ?? ({} as any);
54
54
 
55
+ const existingInfo = byRef.get(correctRef);
56
+ if (existingInfo) {
57
+ return map[split[split.length - 1]!] as T;
58
+ }
59
+
55
60
  // "#/components/schemas/Something.jsonld" -> "Something.jsonld"
56
61
  const name = split[split.length - 1]!;
57
- let normalized = normalizeString(name);
58
- if (nameTransform?.transformSchemaName) {
59
- normalized = nameTransform.transformSchemaName(normalized);
62
+ const kind = normalizedPath.split(".")[1] as RefInfo["kind"];
63
+ const baseNormalized = sanitizeName(
64
+ nameTransform?.transformSchemaName
65
+ ? nameTransform.transformSchemaName(normalizeString(name))
66
+ : normalizeString(name),
67
+ "schema",
68
+ );
69
+
70
+ let normalized = baseNormalized;
71
+ if (refByName.has(normalized) && refByName.get(normalized) !== correctRef) {
72
+ const kindSuffix = `${baseNormalized}_${kind}`;
73
+ normalized = kindSuffix;
74
+ let suffix = 2;
75
+ while (refByName.has(normalized) && refByName.get(normalized) !== correctRef) {
76
+ normalized = `${kindSuffix}_${suffix++}`;
77
+ }
60
78
  }
61
- normalized = sanitizeName(normalized, "schema");
62
79
 
63
80
  nameByRef.set(correctRef, normalized);
64
81
  refByName.set(normalized, correctRef);
65
82
 
66
- const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] as RefInfo["kind"] };
83
+ const infos = { ref: correctRef, name, normalized, kind };
67
84
  byRef.set(infos.ref, infos);
68
85
  byNormalized.set(infos.normalized, infos);
69
86
 
@@ -13,10 +13,10 @@ export function normalizeString(text: string) {
13
13
  .replace(/--+/g, "-"); // Replace multiple - with single -
14
14
  }
15
15
 
16
- const onlyWordRegex = /^\w+$/;
16
+ const barePropertyNameRegex = /^(?:[A-Za-z_]\w*|\d+)$/;
17
17
  export const wrapWithQuotesIfNeeded = (str: string) => {
18
18
  if (str[0] === '"' && str[str.length - 1] === '"') return str;
19
- if (onlyWordRegex.test(str)) {
19
+ if (barePropertyNameRegex.test(str)) {
20
20
  return str;
21
21
  }
22
22
 
@@ -1,5 +1,4 @@
1
1
  import { capitalize } from "pastable/server";
2
- import { prettify } from "./format.ts";
3
2
  import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
4
3
 
5
4
  type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
@@ -191,5 +190,5 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
191
190
  }
192
191
  `;
193
192
 
194
- return prettify(file);
193
+ return file;
195
194
  };