typed-openapi 1.3.1 → 1.4.0
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-6PTWWQX6.js +51 -0
- package/dist/{chunk-YLPQQ24J.js → chunk-DU37V6HC.js} +35 -14
- package/dist/cli.js +14 -36
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/node.export.d.ts +14 -0
- package/dist/node.export.js +10 -0
- package/package.json +4 -2
- package/src/cli.ts +12 -41
- package/src/generate-client-files.ts +51 -0
- package/src/generator.ts +93 -76
- package/src/index.ts +1 -1
- package/src/map-openapi-endpoints.ts +10 -7
- package/src/node.export.ts +2 -0
- package/src/openapi-schema-to-ts.ts +6 -1
- package/src/ref-resolver.ts +1 -1
- package/src/string-utils.ts +1 -2
- package/src/tanstack-query.generator.ts +13 -7
- package/src/types.ts +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allowedRuntimes,
|
|
3
|
+
generateFile,
|
|
4
|
+
generateTanstackQueryFile,
|
|
5
|
+
mapOpenApiEndpoints,
|
|
6
|
+
prettify
|
|
7
|
+
} from "./chunk-DU37V6HC.js";
|
|
8
|
+
|
|
9
|
+
// src/generate-client-files.ts
|
|
10
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
11
|
+
import { basename, join, dirname } from "pathe";
|
|
12
|
+
import { type } from "arktype";
|
|
13
|
+
import { writeFile } from "fs/promises";
|
|
14
|
+
var cwd = process.cwd();
|
|
15
|
+
var now = /* @__PURE__ */ new Date();
|
|
16
|
+
var optionsSchema = type({
|
|
17
|
+
"output?": "string",
|
|
18
|
+
runtime: allowedRuntimes,
|
|
19
|
+
tanstack: "boolean | string",
|
|
20
|
+
schemasOnly: "boolean"
|
|
21
|
+
});
|
|
22
|
+
async function generateClientFiles(input, options) {
|
|
23
|
+
const openApiDoc = await SwaggerParser.bundle(input);
|
|
24
|
+
const ctx = mapOpenApiEndpoints(openApiDoc);
|
|
25
|
+
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
26
|
+
const content = await prettify(generateFile({
|
|
27
|
+
...ctx,
|
|
28
|
+
runtime: options.runtime,
|
|
29
|
+
schemasOnly: options.schemasOnly
|
|
30
|
+
}));
|
|
31
|
+
const outputPath = join(
|
|
32
|
+
cwd,
|
|
33
|
+
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`
|
|
34
|
+
);
|
|
35
|
+
console.log("Generating client...", outputPath);
|
|
36
|
+
await writeFile(outputPath, content);
|
|
37
|
+
if (options.tanstack) {
|
|
38
|
+
const tanstackContent = await generateTanstackQueryFile({
|
|
39
|
+
...ctx,
|
|
40
|
+
relativeApiClientPath: "./" + basename(outputPath)
|
|
41
|
+
});
|
|
42
|
+
const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
43
|
+
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
44
|
+
await writeFile(tanstackOutputPath, tanstackContent);
|
|
45
|
+
}
|
|
46
|
+
console.log(`Done in ${(/* @__PURE__ */ new Date()).getTime() - now.getTime()}ms !`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
generateClientFiles
|
|
51
|
+
};
|
|
@@ -276,14 +276,20 @@ var methods = ["get", "put", "post", "delete", "options", "head", "patch", "trac
|
|
|
276
276
|
var methodsRegex = new RegExp(`(?:${methods.join("|")})_`);
|
|
277
277
|
var endpointExport = new RegExp(`export (?:type|const) (?:${methodsRegex.source})`);
|
|
278
278
|
var replacerByRuntime = {
|
|
279
|
-
yup: (line) => line.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(
|
|
280
|
-
|
|
279
|
+
yup: (line) => line.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(
|
|
280
|
+
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(y\.object)(\()/).source, "g"),
|
|
281
|
+
"$1$2("
|
|
282
|
+
),
|
|
283
|
+
zod: (line) => line.replace(/z\.infer<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(
|
|
284
|
+
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(z\.object)(\()/).source, "g"),
|
|
285
|
+
"$1$2("
|
|
286
|
+
)
|
|
281
287
|
};
|
|
282
288
|
var generateFile = (options) => {
|
|
283
289
|
const ctx = { ...options, runtime: options.runtime ?? "none" };
|
|
284
290
|
const schemaList = generateSchemaList(ctx);
|
|
285
|
-
const endpointSchemaList = generateEndpointSchemaList(ctx);
|
|
286
|
-
const apiClient = generateApiClient(ctx);
|
|
291
|
+
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
|
|
292
|
+
const apiClient = options.schemasOnly ? "" : generateApiClient(ctx);
|
|
287
293
|
const transform = ctx.runtime === "none" ? (file2) => file2 : (file2) => {
|
|
288
294
|
const model = Codegen.TypeScriptToModel.Generate(file2);
|
|
289
295
|
const transformer = runtimeValidationGenerator[ctx.runtime];
|
|
@@ -327,7 +333,7 @@ var parameterObjectToString = (parameters) => {
|
|
|
327
333
|
if (parameters instanceof Box) return parameters.value;
|
|
328
334
|
let str = "{";
|
|
329
335
|
for (const [key, box] of Object.entries(parameters)) {
|
|
330
|
-
str += `${wrapWithQuotesIfNeeded(key)}: ${box.value},
|
|
336
|
+
str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value},
|
|
331
337
|
`;
|
|
332
338
|
}
|
|
333
339
|
return str + "}";
|
|
@@ -490,7 +496,17 @@ export class ApiClient {
|
|
|
490
496
|
>(
|
|
491
497
|
method: TMethod,
|
|
492
498
|
path: TPath,
|
|
493
|
-
...params: MaybeOptionalArg<${match(ctx.runtime).with(
|
|
499
|
+
...params: MaybeOptionalArg<${match(ctx.runtime).with(
|
|
500
|
+
"zod",
|
|
501
|
+
"yup",
|
|
502
|
+
() => inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`)
|
|
503
|
+
).with(
|
|
504
|
+
"arktype",
|
|
505
|
+
"io-ts",
|
|
506
|
+
"typebox",
|
|
507
|
+
"valibot",
|
|
508
|
+
() => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`
|
|
509
|
+
).otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
494
510
|
: Promise<Omit<Response, "json"> & {
|
|
495
511
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
|
|
496
512
|
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
|
@@ -754,12 +770,15 @@ var mapOpenApiEndpoints = (doc) => {
|
|
|
754
770
|
},
|
|
755
771
|
{ query: {}, path: {}, header: {} }
|
|
756
772
|
);
|
|
757
|
-
const params = Object.entries(paramObjects).reduce(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
773
|
+
const params = Object.entries(paramObjects).reduce(
|
|
774
|
+
(acc, [key, value]) => {
|
|
775
|
+
if (Object.keys(value).length) {
|
|
776
|
+
acc[key] = value;
|
|
777
|
+
}
|
|
778
|
+
return acc;
|
|
779
|
+
},
|
|
780
|
+
{}
|
|
781
|
+
);
|
|
763
782
|
if (operation.requestBody) {
|
|
764
783
|
endpoint.meta.hasParameters = true;
|
|
765
784
|
const requestBody = refs.unwrap(operation.requestBody ?? {});
|
|
@@ -907,7 +926,8 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
907
926
|
export class TanstackQueryApiClient {
|
|
908
927
|
constructor(public client: ApiClient) { }
|
|
909
928
|
|
|
910
|
-
${Array.from(endpointMethods).map(
|
|
929
|
+
${Array.from(endpointMethods).map(
|
|
930
|
+
(method) => `
|
|
911
931
|
// <ApiClient.${method}>
|
|
912
932
|
${method}<Path extends keyof ${capitalize4(method)}Endpoints, TEndpoint extends ${capitalize4(method)}Endpoints[Path]>(
|
|
913
933
|
path: Path,
|
|
@@ -945,7 +965,8 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
945
965
|
return query
|
|
946
966
|
}
|
|
947
967
|
// </ApiClient.${method}>
|
|
948
|
-
`
|
|
968
|
+
`
|
|
969
|
+
).join("\n")}
|
|
949
970
|
|
|
950
971
|
// <ApiClient.request>
|
|
951
972
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -1,50 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./chunk-YLPQQ24J.js";
|
|
2
|
+
generateClientFiles
|
|
3
|
+
} from "./chunk-6PTWWQX6.js";
|
|
4
|
+
import {
|
|
5
|
+
allowedRuntimes
|
|
6
|
+
} from "./chunk-DU37V6HC.js";
|
|
8
7
|
|
|
9
8
|
// src/cli.ts
|
|
10
|
-
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
11
9
|
import { cac } from "cac";
|
|
12
|
-
import { basename, join } from "pathe";
|
|
13
|
-
import { type } from "arktype";
|
|
14
|
-
import { writeFile } from "fs/promises";
|
|
15
10
|
import { readFileSync } from "fs";
|
|
16
|
-
import { dirname } from "path";
|
|
17
11
|
var { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
18
|
-
var cwd = process.cwd();
|
|
19
12
|
var cli = cac(name);
|
|
20
|
-
var now = /* @__PURE__ */ new Date();
|
|
21
|
-
var optionsSchema = type({ "output?": "string", runtime: allowedRuntimes, tanstack: "boolean | string" });
|
|
22
13
|
cli.command("<input>", "Generate").option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)").option(
|
|
23
14
|
"-r, --runtime <name>",
|
|
24
15
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
25
16
|
{ default: "none" }
|
|
26
|
-
).option(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
36
|
-
console.log("Generating client...", outputPath);
|
|
37
|
-
await writeFile(outputPath, content);
|
|
38
|
-
if (options.tanstack) {
|
|
39
|
-
const tanstackContent = await generateTanstackQueryFile({
|
|
40
|
-
...ctx,
|
|
41
|
-
relativeApiClientPath: "./" + basename(outputPath)
|
|
42
|
-
});
|
|
43
|
-
const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
44
|
-
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
45
|
-
await writeFile(tanstackOutputPath, tanstackContent);
|
|
46
|
-
}
|
|
47
|
-
console.log(`Done in ${(/* @__PURE__ */ new Date()).getTime() - now.getTime()}ms !`);
|
|
17
|
+
).option(
|
|
18
|
+
"--schemas-only",
|
|
19
|
+
"Only generate schemas, skipping client generation (defaults to false)",
|
|
20
|
+
{ default: false }
|
|
21
|
+
).option(
|
|
22
|
+
"--tanstack [name]",
|
|
23
|
+
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file"
|
|
24
|
+
).action(async (input, _options) => {
|
|
25
|
+
return generateClientFiles(input, _options);
|
|
48
26
|
});
|
|
49
27
|
cli.help();
|
|
50
28
|
cli.version(version);
|
package/dist/index.d.ts
CHANGED
|
@@ -236,6 +236,7 @@ type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
236
236
|
|
|
237
237
|
type GeneratorOptions$1 = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
238
238
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
239
|
+
schemasOnly?: boolean;
|
|
239
240
|
};
|
|
240
241
|
declare const allowedRuntimes: arktype_internal_methods_string_ts.StringType<"none" | "arktype" | "io-ts" | "typebox" | "valibot" | "yup" | "zod", {}>;
|
|
241
242
|
type OutputRuntime = typeof allowedRuntimes.infer;
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Options } from 'prettier';
|
|
2
|
+
import * as arktype_internal_methods_object_ts from 'arktype/internal/methods/object.ts';
|
|
3
|
+
|
|
4
|
+
declare const prettify: (str: string, options?: Options | null) => string | Promise<string>;
|
|
5
|
+
|
|
6
|
+
declare const optionsSchema: arktype_internal_methods_object_ts.ObjectType<{
|
|
7
|
+
runtime: "none" | "arktype" | "io-ts" | "typebox" | "valibot" | "yup" | "zod";
|
|
8
|
+
tanstack: string | boolean;
|
|
9
|
+
schemasOnly: boolean;
|
|
10
|
+
output?: string;
|
|
11
|
+
}, {}>;
|
|
12
|
+
declare function generateClientFiles(input: string, options: typeof optionsSchema.infer): Promise<void>;
|
|
13
|
+
|
|
14
|
+
export { generateClientFiles, prettify };
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./dist/index.js"
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./node": "./dist/node.export.js"
|
|
9
10
|
},
|
|
10
11
|
"bin": {
|
|
11
12
|
"typed-openapi": "bin.js"
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"dev": "tsup --watch",
|
|
63
64
|
"build": "tsup",
|
|
64
65
|
"test": "vitest",
|
|
66
|
+
"fmt": "prettier --write src",
|
|
65
67
|
"typecheck": "tsc -b ./tsconfig.build.json"
|
|
66
68
|
}
|
|
67
69
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
2
1
|
import { cac } from "cac";
|
|
3
|
-
import type { OpenAPIObject } from "openapi3-ts/oas31";
|
|
4
|
-
import { basename, join } from "pathe";
|
|
5
|
-
import { type } from "arktype";
|
|
6
2
|
|
|
7
|
-
import { writeFile } from "fs/promises";
|
|
8
|
-
import { allowedRuntimes, generateFile } from "./generator.ts";
|
|
9
|
-
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
10
|
-
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
11
3
|
import { readFileSync } from "fs";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
4
|
+
import { generateClientFiles } from "./generate-client-files.ts";
|
|
5
|
+
import { allowedRuntimes } from "./generator.ts";
|
|
14
6
|
|
|
15
7
|
const { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
16
|
-
const cwd = process.cwd();
|
|
17
8
|
const cli = cac(name);
|
|
18
|
-
const now = new Date();
|
|
19
|
-
|
|
20
|
-
const optionsSchema = type({ "output?": "string", runtime: allowedRuntimes, tanstack: "boolean | string" });
|
|
21
9
|
|
|
22
10
|
cli
|
|
23
11
|
.command("<input>", "Generate")
|
|
@@ -27,34 +15,17 @@ cli
|
|
|
27
15
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
28
16
|
{ default: "none" },
|
|
29
17
|
)
|
|
30
|
-
.option(
|
|
18
|
+
.option(
|
|
19
|
+
"--schemas-only",
|
|
20
|
+
"Only generate schemas, skipping client generation (defaults to false)",
|
|
21
|
+
{ default: false },
|
|
22
|
+
)
|
|
23
|
+
.option(
|
|
24
|
+
"--tanstack [name]",
|
|
25
|
+
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file",
|
|
26
|
+
)
|
|
31
27
|
.action(async (input, _options) => {
|
|
32
|
-
|
|
33
|
-
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
|
|
34
|
-
|
|
35
|
-
const ctx = mapOpenApiEndpoints(openApiDoc);
|
|
36
|
-
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
37
|
-
|
|
38
|
-
const content = await prettify(generateFile({ ...ctx, runtime: options.runtime }));
|
|
39
|
-
const outputPath = join(
|
|
40
|
-
cwd,
|
|
41
|
-
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
console.log("Generating client...", outputPath);
|
|
45
|
-
await writeFile(outputPath, content);
|
|
46
|
-
|
|
47
|
-
if (options.tanstack) {
|
|
48
|
-
const tanstackContent = await generateTanstackQueryFile({
|
|
49
|
-
...ctx,
|
|
50
|
-
relativeApiClientPath: './' + basename(outputPath),
|
|
51
|
-
});
|
|
52
|
-
const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
53
|
-
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
54
|
-
await writeFile(tanstackOutputPath, tanstackContent);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
|
|
28
|
+
return generateClientFiles(input, _options);
|
|
58
29
|
});
|
|
59
30
|
|
|
60
31
|
cli.help();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
2
|
+
import type { OpenAPIObject } from "openapi3-ts/oas31";
|
|
3
|
+
import { basename, join, dirname } from "pathe";
|
|
4
|
+
import { type } from "arktype";
|
|
5
|
+
import { writeFile } from "fs/promises";
|
|
6
|
+
import { allowedRuntimes, generateFile } from "./generator.ts";
|
|
7
|
+
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
8
|
+
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
9
|
+
import { prettify } from "./format.ts";
|
|
10
|
+
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const now = new Date();
|
|
13
|
+
|
|
14
|
+
export const optionsSchema = type({
|
|
15
|
+
"output?": "string",
|
|
16
|
+
runtime: allowedRuntimes,
|
|
17
|
+
tanstack: "boolean | string",
|
|
18
|
+
schemasOnly: "boolean",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export async function generateClientFiles(input: string, options: typeof optionsSchema.infer) {
|
|
22
|
+
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
|
|
23
|
+
|
|
24
|
+
const ctx = mapOpenApiEndpoints(openApiDoc);
|
|
25
|
+
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
26
|
+
|
|
27
|
+
const content = await prettify(generateFile({
|
|
28
|
+
...ctx,
|
|
29
|
+
runtime: options.runtime,
|
|
30
|
+
schemasOnly: options.schemasOnly,
|
|
31
|
+
}));
|
|
32
|
+
const outputPath = join(
|
|
33
|
+
cwd,
|
|
34
|
+
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
console.log("Generating client...", outputPath);
|
|
38
|
+
await writeFile(outputPath, content);
|
|
39
|
+
|
|
40
|
+
if (options.tanstack) {
|
|
41
|
+
const tanstackContent = await generateTanstackQueryFile({
|
|
42
|
+
...ctx,
|
|
43
|
+
relativeApiClientPath: './' + basename(outputPath),
|
|
44
|
+
});
|
|
45
|
+
const tanstackOutputPath = join(dirname(outputPath), typeof options.tanstack === "string" ? options.tanstack : `tanstack.client.ts`);
|
|
46
|
+
console.log("Generating tanstack client...", tanstackOutputPath);
|
|
47
|
+
await writeFile(tanstackOutputPath, tanstackContent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`Done in ${new Date().getTime() - now.getTime()}ms !`);
|
|
51
|
+
}
|
package/src/generator.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
|
9
9
|
|
|
10
10
|
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
11
11
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
12
|
+
schemasOnly?: boolean;
|
|
12
13
|
};
|
|
13
14
|
type GeneratorContext = Required<GeneratorOptions>;
|
|
14
15
|
|
|
@@ -45,54 +46,60 @@ const replacerByRuntime = {
|
|
|
45
46
|
yup: (line: string) =>
|
|
46
47
|
line
|
|
47
48
|
.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1")
|
|
48
|
-
.replace(
|
|
49
|
+
.replace(
|
|
50
|
+
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(y\.object)(\()/).source, "g"),
|
|
51
|
+
"$1$2(",
|
|
52
|
+
),
|
|
49
53
|
zod: (line: string) =>
|
|
50
54
|
line
|
|
51
55
|
.replace(/z\.infer<\s*?typeof (.*?)\s*?>/g, "typeof $1")
|
|
52
|
-
.replace(
|
|
56
|
+
.replace(
|
|
57
|
+
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(z\.object)(\()/).source, "g"),
|
|
58
|
+
"$1$2(",
|
|
59
|
+
),
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
export const generateFile = (options: GeneratorOptions) => {
|
|
56
63
|
const ctx = { ...options, runtime: options.runtime ?? "none" } as GeneratorContext;
|
|
57
64
|
|
|
58
65
|
const schemaList = generateSchemaList(ctx);
|
|
59
|
-
const endpointSchemaList = generateEndpointSchemaList(ctx);
|
|
60
|
-
const apiClient = generateApiClient(ctx);
|
|
66
|
+
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
|
|
67
|
+
const apiClient = options.schemasOnly ? "" : generateApiClient(ctx);
|
|
61
68
|
|
|
62
69
|
const transform =
|
|
63
70
|
ctx.runtime === "none"
|
|
64
71
|
? (file: string) => file
|
|
65
72
|
: (file: string) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
const model = Codegen.TypeScriptToModel.Generate(file);
|
|
74
|
+
const transformer = runtimeValidationGenerator[ctx.runtime as Exclude<typeof ctx.runtime, "none">];
|
|
75
|
+
// tmp fix for typebox, there's currently a "// todo" only with Codegen.ModelToTypeBox.Generate
|
|
76
|
+
// https://github.com/sinclairzx81/typebox-codegen/blob/44d44d55932371b69f349331b1c8a60f5d760d9e/src/model/model-to-typebox.ts#L31
|
|
77
|
+
const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file) : transformer(model);
|
|
78
|
+
|
|
79
|
+
let converted = "";
|
|
80
|
+
const match = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/);
|
|
81
|
+
const content = match?.[2];
|
|
82
|
+
|
|
83
|
+
if (content && ctx.runtime in replacerByRuntime) {
|
|
84
|
+
const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START"));
|
|
85
|
+
converted =
|
|
86
|
+
before +
|
|
87
|
+
replacerByRuntime[ctx.runtime as keyof typeof replacerByRuntime](
|
|
88
|
+
content.slice(content.indexOf("export")),
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
converted = generated;
|
|
92
|
+
}
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
return converted;
|
|
95
|
+
};
|
|
89
96
|
|
|
90
97
|
const file = `
|
|
91
98
|
${transform(schemaList + endpointSchemaList)}
|
|
92
99
|
${apiClient}
|
|
93
100
|
`;
|
|
94
101
|
|
|
95
|
-
return
|
|
102
|
+
return file;
|
|
96
103
|
};
|
|
97
104
|
|
|
98
105
|
const generateSchemaList = ({ refs, runtime }: GeneratorContext) => {
|
|
@@ -121,7 +128,7 @@ const parameterObjectToString = (parameters: Box<AnyBoxDef> | Record<string, Any
|
|
|
121
128
|
|
|
122
129
|
let str = "{";
|
|
123
130
|
for (const [key, box] of Object.entries(parameters)) {
|
|
124
|
-
str += `${wrapWithQuotesIfNeeded(key)}: ${box.value},\n`;
|
|
131
|
+
str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value},\n`;
|
|
125
132
|
}
|
|
126
133
|
return str + "}";
|
|
127
134
|
};
|
|
@@ -137,36 +144,39 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
137
144
|
method: "${endpoint.method.toUpperCase()}",
|
|
138
145
|
path: "${endpoint.path}",
|
|
139
146
|
requestFormat: "${endpoint.requestFormat}",
|
|
140
|
-
${
|
|
141
|
-
|
|
147
|
+
${
|
|
148
|
+
endpoint.meta.hasParameters
|
|
149
|
+
? `parameters: {
|
|
142
150
|
${parameters.query ? `query: ${parameterObjectToString(parameters.query)},` : ""}
|
|
143
151
|
${parameters.path ? `path: ${parameterObjectToString(parameters.path)},` : ""}
|
|
144
152
|
${parameters.header ? `header: ${parameterObjectToString(parameters.header)},` : ""}
|
|
145
|
-
${
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
${
|
|
154
|
+
parameters.body
|
|
155
|
+
? `body: ${parameterObjectToString(
|
|
156
|
+
ctx.runtime === "none"
|
|
157
|
+
? parameters.body.recompute((box) => {
|
|
158
|
+
if (Box.isReference(box) && !box.params.generics) {
|
|
159
|
+
box.value = `Schemas.${box.value}`;
|
|
160
|
+
}
|
|
161
|
+
return box;
|
|
162
|
+
})
|
|
163
|
+
: parameters.body,
|
|
164
|
+
)},`
|
|
165
|
+
: ""
|
|
157
166
|
}
|
|
158
167
|
}`
|
|
159
|
-
|
|
168
|
+
: "parameters: never,"
|
|
160
169
|
}
|
|
161
|
-
response: ${
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
response: ${
|
|
171
|
+
ctx.runtime === "none"
|
|
172
|
+
? endpoint.response.recompute((box) => {
|
|
173
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
174
|
+
box.value = `Schemas.${box.value}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return box;
|
|
178
|
+
}).value
|
|
179
|
+
: endpoint.response.value
|
|
170
180
|
},
|
|
171
181
|
}\n`;
|
|
172
182
|
});
|
|
@@ -189,14 +199,14 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
189
199
|
// <EndpointByMethod>
|
|
190
200
|
export ${ctx.runtime === "none" ? "type" : "const"} EndpointByMethod = {
|
|
191
201
|
${Object.entries(byMethods)
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
.map(([method, list]) => {
|
|
203
|
+
return `${method}: {
|
|
194
204
|
${list.map(
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
|
|
206
|
+
)}
|
|
197
207
|
}`;
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
})
|
|
209
|
+
.join(",\n")}
|
|
200
210
|
}
|
|
201
211
|
${ctx.runtime === "none" ? "" : "export type EndpointByMethod = typeof EndpointByMethod;"}
|
|
202
212
|
// </EndpointByMethod>
|
|
@@ -263,7 +273,6 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
|
|
|
263
273
|
// </ApiClientTypes>
|
|
264
274
|
`;
|
|
265
275
|
|
|
266
|
-
|
|
267
276
|
const apiClient = `
|
|
268
277
|
// <ApiClient>
|
|
269
278
|
export class ApiClient {
|
|
@@ -285,22 +294,22 @@ export class ApiClient {
|
|
|
285
294
|
}
|
|
286
295
|
|
|
287
296
|
${Object.entries(byMethods)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
297
|
+
.map(([method, endpointByMethod]) => {
|
|
298
|
+
const capitalizedMethod = capitalize(method);
|
|
299
|
+
const infer = inferByRuntime[ctx.runtime];
|
|
291
300
|
|
|
292
|
-
|
|
293
|
-
|
|
301
|
+
return endpointByMethod.length
|
|
302
|
+
? `// <ApiClient.${method}>
|
|
294
303
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
295
304
|
path: Path,
|
|
296
305
|
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
306
|
+
.with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
|
|
307
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
|
|
308
|
+
.otherwise(() => `TEndpoint["parameters"]`)}>
|
|
300
309
|
): Promise<${match(ctx.runtime)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
.with("zod", "yup", () => infer(`TEndpoint["response"]`))
|
|
311
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
|
|
312
|
+
.otherwise(() => `TEndpoint["response"]`)}> {
|
|
304
313
|
return this.fetcher("${method}", this.baseUrl + path, params[0])
|
|
305
314
|
.then(response => this.parseResponse(response))${match(ctx.runtime)
|
|
306
315
|
.with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
|
|
@@ -309,9 +318,9 @@ export class ApiClient {
|
|
|
309
318
|
}
|
|
310
319
|
// </ApiClient.${method}>
|
|
311
320
|
`
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
: "";
|
|
322
|
+
})
|
|
323
|
+
.join("\n")}
|
|
315
324
|
|
|
316
325
|
// <ApiClient.request>
|
|
317
326
|
/**
|
|
@@ -325,9 +334,17 @@ export class ApiClient {
|
|
|
325
334
|
method: TMethod,
|
|
326
335
|
path: TPath,
|
|
327
336
|
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
337
|
+
.with("zod", "yup", () =>
|
|
338
|
+
inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`),
|
|
339
|
+
)
|
|
340
|
+
.with(
|
|
341
|
+
"arktype",
|
|
342
|
+
"io-ts",
|
|
343
|
+
"typebox",
|
|
344
|
+
"valibot",
|
|
345
|
+
() => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
|
|
346
|
+
)
|
|
347
|
+
.otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
331
348
|
: Promise<Omit<Response, "json"> & {
|
|
332
349
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
|
|
333
350
|
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./box-factory.ts";
|
|
2
2
|
export { generateFile, type OutputRuntime } from "./generator.ts";
|
|
3
|
-
export * from "./tanstack-query.generator.ts"
|
|
3
|
+
export * from "./tanstack-query.generator.ts";
|
|
4
4
|
export * from "./map-openapi-endpoints.ts";
|
|
5
5
|
export * from "./openapi-schema-to-ts.ts";
|
|
6
6
|
export * from "./ref-resolver.ts";
|
|
@@ -64,13 +64,16 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
64
64
|
);
|
|
65
65
|
|
|
66
66
|
// Filter out empty objects
|
|
67
|
-
const params = Object.entries(paramObjects).reduce(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
const params = Object.entries(paramObjects).reduce(
|
|
68
|
+
(acc, [key, value]) => {
|
|
69
|
+
if (Object.keys(value).length) {
|
|
70
|
+
// @ts-expect-error
|
|
71
|
+
acc[key] = value;
|
|
72
|
+
}
|
|
73
|
+
return acc;
|
|
74
|
+
},
|
|
75
|
+
{} as { query?: Record<string, Box>; path?: Record<string, Box>; header?: Record<string, Box>; body?: Box },
|
|
76
|
+
);
|
|
74
77
|
|
|
75
78
|
// Body
|
|
76
79
|
if (operation.requestBody) {
|
|
@@ -100,7 +100,12 @@ export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: Openapi
|
|
|
100
100
|
|
|
101
101
|
if (schemaType === "object" || schema.properties || schema.additionalProperties) {
|
|
102
102
|
if (!schema.properties) {
|
|
103
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
schema.additionalProperties &&
|
|
105
|
+
!isReferenceObject(schema.additionalProperties) &&
|
|
106
|
+
typeof schema.additionalProperties !== "boolean" &&
|
|
107
|
+
schema.additionalProperties.type
|
|
108
|
+
) {
|
|
104
109
|
const valueSchema = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta });
|
|
105
110
|
return t.literal(`Record<string, ${valueSchema.value}>`);
|
|
106
111
|
}
|
package/src/ref-resolver.ts
CHANGED
|
@@ -119,7 +119,7 @@ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) =
|
|
|
119
119
|
};
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
export interface RefResolver extends ReturnType<typeof createRefResolver> {
|
|
122
|
+
export interface RefResolver extends ReturnType<typeof createRefResolver> {}
|
|
123
123
|
|
|
124
124
|
const setSchemaDependencies = (schema: LibSchemaObject, deps: Set<string>) => {
|
|
125
125
|
const visit = (schema: LibSchemaObject | ReferenceObject): void => {
|
package/src/string-utils.ts
CHANGED
|
@@ -35,9 +35,8 @@ const prefixStringStartingWithNumberIfNeeded = (str: string) => {
|
|
|
35
35
|
const pathParamWithBracketsRegex = /({\w+})/g;
|
|
36
36
|
const wordPrecededByNonWordCharacter = /[^\w\-]+/g;
|
|
37
37
|
|
|
38
|
-
|
|
39
38
|
/** @example turns `/media-objects/{id}` into `MediaObjectsId` */
|
|
40
39
|
export const pathToVariableName = (path: string) =>
|
|
41
|
-
capitalize(kebabToCamel(
|
|
40
|
+
capitalize(kebabToCamel(path).replaceAll("/", "_")) // /media-objects/{id} -> MediaObjects{id}
|
|
42
41
|
.replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))) // {id} -> Id
|
|
43
42
|
.replace(wordPrecededByNonWordCharacter, "_"); // "/robots.txt" -> "/robots_txt"
|
|
@@ -2,13 +2,13 @@ import { capitalize } from "pastable/server";
|
|
|
2
2
|
import { prettify } from "./format.ts";
|
|
3
3
|
import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
4
4
|
|
|
5
|
-
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints
|
|
5
|
+
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
|
|
6
6
|
type GeneratorContext = Required<GeneratorOptions>;
|
|
7
7
|
|
|
8
8
|
export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => {
|
|
9
|
-
|
|
9
|
+
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const file = `
|
|
12
12
|
import { queryOptions } from "@tanstack/react-query"
|
|
13
13
|
import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
|
|
14
14
|
|
|
@@ -44,7 +44,9 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
// <EndpointByMethod.Shorthands>
|
|
47
|
-
${Array.from(endpointMethods)
|
|
47
|
+
${Array.from(endpointMethods)
|
|
48
|
+
.map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"];`)
|
|
49
|
+
.join("\n")}
|
|
48
50
|
// </EndpointByMethod.Shorthands>
|
|
49
51
|
|
|
50
52
|
// <ApiClientTypes>
|
|
@@ -67,7 +69,9 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
67
69
|
export class TanstackQueryApiClient {
|
|
68
70
|
constructor(public client: ApiClient) { }
|
|
69
71
|
|
|
70
|
-
${Array.from(endpointMethods)
|
|
72
|
+
${Array.from(endpointMethods)
|
|
73
|
+
.map(
|
|
74
|
+
(method) => `
|
|
71
75
|
// <ApiClient.${method}>
|
|
72
76
|
${method}<Path extends keyof ${capitalize(method)}Endpoints, TEndpoint extends ${capitalize(method)}Endpoints[Path]>(
|
|
73
77
|
path: Path,
|
|
@@ -105,7 +109,9 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
105
109
|
return query
|
|
106
110
|
}
|
|
107
111
|
// </ApiClient.${method}>
|
|
108
|
-
|
|
112
|
+
`,
|
|
113
|
+
)
|
|
114
|
+
.join("\n")}
|
|
109
115
|
|
|
110
116
|
// <ApiClient.request>
|
|
111
117
|
/**
|
|
@@ -139,5 +145,5 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
139
145
|
}
|
|
140
146
|
`;
|
|
141
147
|
|
|
142
|
-
|
|
148
|
+
return prettify(file);
|
|
143
149
|
};
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { SchemaObject as SchemaObject3 } from "openapi3-ts/oas30";
|
|
|
4
4
|
import type { RefResolver } from "./ref-resolver.ts";
|
|
5
5
|
import { Box } from "./box.ts";
|
|
6
6
|
|
|
7
|
-
export type LibSchemaObject = SchemaObject & SchemaObject3
|
|
7
|
+
export type LibSchemaObject = SchemaObject & SchemaObject3;
|
|
8
8
|
|
|
9
9
|
export type BoxDefinition = {
|
|
10
10
|
type: string;
|