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/dist/chunk-MAZKDIN3.js +61 -0
- package/dist/{chunk-RVHSVZW7.js → chunk-S3NICEHD.js} +132 -55
- package/dist/{chunk-GIUNFA7X.js → chunk-WTE26U5C.js} +30 -9
- package/dist/cli.js +6 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -2
- package/dist/node.export.d.ts +6 -4
- package/dist/node.export.js +3 -3
- package/dist/pretty.export.d.ts +7 -2
- package/dist/pretty.export.js +1 -1
- package/dist/{types-BOJSTQwz.d.ts → types-DjwHsNyZ.d.ts} +3 -3
- package/package.json +19 -19
- package/src/cli.ts +4 -0
- package/src/format.ts +68 -13
- package/src/generate-client-files.ts +32 -9
- package/src/generator.ts +132 -43
- package/src/node.export.ts +1 -1
- package/src/openapi-schema-to-ts.ts +13 -11
- package/src/ref-resolver.ts +22 -5
- package/src/string-utils.ts +2 -2
- package/src/tanstack-query.generator.ts +1 -2
- package/src/ts-factory.ts +8 -3
- package/dist/chunk-KAEXXJ7X.js +0 -21
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.2.
|
|
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": "^
|
|
21
|
+
"@apidevtools/swagger-parser": "^12.1.0",
|
|
22
22
|
"@sinclair/typebox-codegen": "^0.11.1",
|
|
23
|
-
"arktype": "2.
|
|
24
|
-
"cac": "
|
|
25
|
-
"openapi3-ts": "
|
|
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": "
|
|
28
|
-
"
|
|
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.
|
|
33
|
-
"@tanstack/react-query": "5.
|
|
34
|
-
"@types/node": "^
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"vitest": "^
|
|
41
|
-
"zod": "3.
|
|
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": "
|
|
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
|
|
2
|
-
import
|
|
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?:
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
50
|
+
const { format } = await import("oxfmt");
|
|
51
|
+
const result = await format(filePath, input, {
|
|
52
|
+
printWidth: 120,
|
|
53
|
+
trailingComma: "all",
|
|
54
|
+
...formatOptions,
|
|
11
55
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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?:
|
|
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
|
-
|
|
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,
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
419
|
+
encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
|
|
331
420
|
//
|
|
332
421
|
fetch: (input: {
|
|
333
422
|
method: Method;
|
package/src/node.export.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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)
|
|
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
|
}
|
package/src/ref-resolver.ts
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
83
|
+
const infos = { ref: correctRef, name, normalized, kind };
|
|
67
84
|
byRef.set(infos.ref, infos);
|
|
68
85
|
byNormalized.set(infos.normalized, infos);
|
|
69
86
|
|
package/src/string-utils.ts
CHANGED
|
@@ -13,10 +13,10 @@ export function normalizeString(text: string) {
|
|
|
13
13
|
.replace(/--+/g, "-"); // Replace multiple - with single -
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
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 (
|
|
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
|
|
193
|
+
return file;
|
|
195
194
|
};
|