Skip to content

Commit 913198c

Browse files
committed
first ts-to-zod integration prototype
1 parent 02402c0 commit 913198c

File tree

11 files changed

+194
-54
lines changed

11 files changed

+194
-54
lines changed

cli/src/commands/GenerateCommand.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export class GenerateCommand extends Command {
233233
const prettierConfig = await prettier.resolveConfig(process.cwd());
234234

235235
const writeFile = async (file: string, data: string) => {
236+
data = "/* eslint-disable */\n" + data;
236237
await fsExtra.outputFile(
237238
path.join(process.cwd(), config.outputDir, file),
238239
prettier.format(data, { parser: "babel-ts", ...prettierConfig })

plugins/typescript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"lodash": "^4.17.21",
3333
"openapi3-ts": "^2.0.1",
3434
"pluralize": "^8.0.0",
35+
"ts-to-zod": "^3.6.1",
3536
"tslib": "^2.3.1",
3637
"tsutils": "^3.21.0",
3738
"typescript": "4.8.2"

plugins/typescript/src/core/createNamespaceImport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { factory as f } from "typescript";
77
* @param filename path of the module
88
* @returns ts.Node of the import declaration
99
*/
10-
export const createNamespaceImport = (namespace: string, filename: string) =>
10+
export const createNamespaceImport = (namespace: string, filename: string, isTypeOnly = true) =>
1111
f.createImportDeclaration(
1212
undefined,
1313
f.createImportClause(
14-
true,
14+
isTypeOnly,
1515
undefined,
1616
f.createNamespaceImport(f.createIdentifier(namespace))
1717
),

plugins/typescript/src/core/createOperationFetcherFnNodes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { OperationObject } from "openapi3-ts";
22
import ts, { factory as f } from "typescript";
3+
import { createZodValidatorResponse } from "../utils/zodHelper";
34
import { camelizedPathParams } from "./camelizedPathParams";
45

56
/**
@@ -20,6 +21,7 @@ export const createOperationFetcherFnNodes = ({
2021
url,
2122
verb,
2223
name,
24+
printNodes,
2325
}: {
2426
dataType: ts.TypeNode;
2527
errorType: ts.TypeNode;
@@ -33,6 +35,7 @@ export const createOperationFetcherFnNodes = ({
3335
url: string;
3436
verb: string;
3537
name: string;
38+
printNodes: (nodes: ts.Node[]) => string;
3639
}) => {
3740
const nodes: ts.Node[] = [];
3841
if (operation.description) {
@@ -119,6 +122,7 @@ export const createOperationFetcherFnNodes = ({
119122
f.createIdentifier("signal")
120123
),
121124
]),
125+
...createZodValidatorResponse(dataType, printNodes),
122126
],
123127
false
124128
),

plugins/typescript/src/generators/generateFetchers.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
import ts, { factory as f } from "typescript";
21
import * as c from "case";
32
import { get } from "lodash";
3+
import ts, { factory as f } from "typescript";
44

5-
import { ConfigBase, Context } from "./types";
65
import { PathItemObject } from "openapi3-ts";
6+
import { ConfigBase, Context } from "./types";
77

8-
import { getUsedImports } from "../core/getUsedImports";
9-
import { createWatermark } from "../core/createWatermark";
8+
import { createNamedImport } from "../core/createNamedImport";
109
import { createOperationFetcherFnNodes } from "../core/createOperationFetcherFnNodes";
11-
import { isVerb } from "../core/isVerb";
12-
import { isOperationObject } from "../core/isOperationObject";
10+
import { createWatermark } from "../core/createWatermark";
1311
import { getOperationTypes } from "../core/getOperationTypes";
14-
import { createNamedImport } from "../core/createNamedImport";
12+
import { getUsedImports } from "../core/getUsedImports";
13+
import { isOperationObject } from "../core/isOperationObject";
14+
import { isVerb } from "../core/isVerb";
1515

16+
import { createNamespaceImport } from "../core/createNamespaceImport";
1617
import { getFetcher } from "../templates/fetcher";
1718
import { getUtils } from "../templates/utils";
18-
import { createNamespaceImport } from "../core/createNamespaceImport";
19+
import { createZodNamespaceImport } from "../utils/zodHelper";
1920

2021
export type Config = ConfigBase & {
2122
/**
@@ -33,6 +34,12 @@ export type Config = ConfigBase & {
3334
* This will mark the header as optional in the component API
3435
*/
3536
injectedHeaders?: string[];
37+
38+
zodFiles?: {
39+
schemas: string;
40+
inferredTypes: string;
41+
integrationTests: string;
42+
}
3643
};
3744

3845
export const generateFetchers = async (context: Context, config: Config) => {
@@ -172,6 +179,7 @@ export const generateFetchers = async (context: Context, config: Config) => {
172179
url: route,
173180
verb,
174181
name: operationId,
182+
printNodes,
175183
})
176184
);
177185
});
@@ -230,6 +238,7 @@ export const generateFetchers = async (context: Context, config: Config) => {
230238
createWatermark(context.openAPIDocument.info),
231239
createNamespaceImport("Fetcher", `./${fetcherFilename}`),
232240
createNamedImport(fetcherImports, `./${fetcherFilename}`),
241+
...createZodNamespaceImport(config.zodFiles?.schemas),
233242
...usedImportsNodes,
234243
...nodes,
235244
])

plugins/typescript/src/generators/generateReactQueryComponents.ts

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { createNamedImport } from "../core/createNamedImport";
1616
import { getFetcher } from "../templates/fetcher";
1717
import { getContext } from "../templates/context";
1818
import { getUtils } from "../templates/utils";
19+
import { createZodNamespaceImport } from "../utils/zodHelper";
1920
import { createNamespaceImport } from "../core/createNamespaceImport";
2021
import { camelizedPathParams } from "../core/camelizedPathParams";
2122

@@ -35,6 +36,12 @@ export type Config = ConfigBase & {
3536
* This will mark the header as optional in the component API
3637
*/
3738
injectedHeaders?: string[];
39+
40+
zodFiles?: {
41+
schemas: string;
42+
inferredTypes: string;
43+
integrationTests: string;
44+
}
3845
};
3946

4047
export const generateReactQueryComponents = async (
@@ -58,9 +65,9 @@ export const generateReactQueryComponents = async (
5865
return (
5966
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile) +
6067
(ts.isJSDoc(node) ||
61-
(ts.isImportDeclaration(node) &&
62-
nodes[i + 1] &&
63-
ts.isImportDeclaration(nodes[i + 1]))
68+
(ts.isImportDeclaration(node) &&
69+
nodes[i + 1] &&
70+
ts.isImportDeclaration(nodes[i + 1]))
6471
? ""
6572
: "\n")
6673
);
@@ -195,28 +202,29 @@ export const generateReactQueryComponents = async (
195202
url: route,
196203
verb,
197204
name: operationFetcherFnName,
205+
printNodes,
198206
}),
199207
...(component === "useQuery"
200208
? createQueryHook({
201-
operationFetcherFnName,
202-
operation,
203-
dataType,
204-
errorType,
205-
variablesType,
206-
contextHookName,
207-
name: `use${c.pascal(operationId)}`,
208-
operationId,
209-
url: route,
210-
})
209+
operationFetcherFnName,
210+
operation,
211+
dataType,
212+
errorType,
213+
variablesType,
214+
contextHookName,
215+
name: `use${c.pascal(operationId)}`,
216+
operationId,
217+
url: route,
218+
})
211219
: createMutationHook({
212-
operationFetcherFnName,
213-
operation,
214-
dataType,
215-
errorType,
216-
variablesType,
217-
contextHookName,
218-
name: `use${c.pascal(operationId)}`,
219-
}))
220+
operationFetcherFnName,
221+
operation,
222+
dataType,
223+
errorType,
224+
variablesType,
225+
contextHookName,
226+
name: `use${c.pascal(operationId)}`,
227+
}))
220228
);
221229
});
222230
}
@@ -232,25 +240,25 @@ export const generateReactQueryComponents = async (
232240
keyManagerItems.length > 0
233241
? f.createUnionTypeNode(keyManagerItems)
234242
: f.createTypeLiteralNode([
235-
f.createPropertySignature(
236-
undefined,
237-
f.createIdentifier("path"),
238-
undefined,
239-
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
240-
),
241-
f.createPropertySignature(
242-
undefined,
243-
f.createIdentifier("operationId"),
244-
undefined,
245-
f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)
246-
),
247-
f.createPropertySignature(
248-
undefined,
249-
f.createIdentifier("variables"),
250-
undefined,
251-
f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
252-
),
253-
])
243+
f.createPropertySignature(
244+
undefined,
245+
f.createIdentifier("path"),
246+
undefined,
247+
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
248+
),
249+
f.createPropertySignature(
250+
undefined,
251+
f.createIdentifier("operationId"),
252+
undefined,
253+
f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)
254+
),
255+
f.createPropertySignature(
256+
undefined,
257+
f.createIdentifier("variables"),
258+
undefined,
259+
f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
260+
),
261+
])
254262
);
255263

256264
const { nodes: usedImportsNodes, keys: usedImportsKeys } = getUsedImports(
@@ -276,6 +284,7 @@ export const generateReactQueryComponents = async (
276284
),
277285
createNamespaceImport("Fetcher", `./${fetcherFilename}`),
278286
createNamedImport(fetcherFn, `./${fetcherFilename}`),
287+
...createZodNamespaceImport(config.zodFiles?.schemas),
279288
...usedImportsNodes,
280289
...nodes,
281290
queryKeyManager,

plugins/typescript/src/generators/generateReactQueryFunctions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getFetcher } from "../templates/fetcher";
1919
import { getContext } from "../templates/context";
2020
import { getUtils } from "../templates/utils";
2121
import { createNamespaceImport } from "../core/createNamespaceImport";
22+
import { createZodNamespaceImport } from "../utils/zodHelper";
2223
import { camelizedPathParams } from "../core/camelizedPathParams";
2324

2425
export type Config = ConfigBase & {
@@ -37,6 +38,12 @@ export type Config = ConfigBase & {
3738
* This will mark the header as optional in the component API
3839
*/
3940
injectedHeaders?: string[];
41+
42+
zodFiles?: {
43+
schemas: string;
44+
inferredTypes: string;
45+
integrationTests: string;
46+
}
4047
};
4148

4249
export const generateReactQueryFunctions = async (
@@ -199,6 +206,7 @@ export const generateReactQueryFunctions = async (
199206
url: route,
200207
verb,
201208
name: operationFetcherFnName,
209+
printNodes,
202210
}),
203211
...createOperationQueryFnNodes({
204212
operationFetcherFnName,
@@ -275,6 +283,7 @@ export const generateReactQueryFunctions = async (
275283
),
276284
createNamespaceImport("Fetcher", `./${fetcherFilename}`),
277285
createNamedImport(fetcherFn, `./${fetcherFilename}`),
286+
...createZodNamespaceImport(config.zodFiles?.schemas),
278287
...usedImportsNodes,
279288
...nodes,
280289
queryKeyManager,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as c from "case";
2+
import { GenerateProps, generate } from "ts-to-zod";
3+
import { ConfigBase, Context } from "../generators/types";
4+
5+
export type Config = ConfigBase & {
6+
/**
7+
* Generated files paths from `generateSchemaTypes`
8+
*/
9+
schemasFiles: {
10+
requestBodies: string;
11+
schemas: string;
12+
parameters: string;
13+
responses: string;
14+
};
15+
generateProps?: GenerateProps;
16+
writeInferredTypes?: boolean
17+
writeIntegrationTests?: boolean
18+
};
19+
20+
export const generateZod = async (context: Context, config: Config) => {
21+
const result = generate({
22+
sourceText: await context.readFile(config.schemasFiles.schemas + ".ts"),
23+
...config.generateProps,
24+
})
25+
26+
const filenamePrefix =
27+
c.snake(config.filenamePrefix ?? context.openAPIDocument.info.title) + "-";
28+
29+
const formatFilename = config.filenameCase ? c[config.filenameCase] : c.camel;
30+
31+
const zodFiles = {
32+
schemas: formatFilename(filenamePrefix + "-zod-schemas"),
33+
inferredTypes: formatFilename(filenamePrefix + "-zod-inferred-types"),
34+
integrationTests: formatFilename(filenamePrefix + "-zod-integration-tests"),
35+
}
36+
37+
if (result.errors.length === 0) {
38+
await context.writeFile(zodFiles.schemas + ".ts", result.getZodSchemasFile("./" + config.schemasFiles.schemas));
39+
40+
if (config.writeInferredTypes !== false) {
41+
await context.writeFile(zodFiles.inferredTypes + ".ts", result.getInferredTypes("./" + zodFiles.schemas));
42+
43+
if (config.writeIntegrationTests !== false) {
44+
await context.writeFile(zodFiles.integrationTests + ".ts", result.getIntegrationTestFile("./" + zodFiles.inferredTypes, "./" + zodFiles.schemas));
45+
}
46+
}
47+
} else {
48+
console.log(`⚠️ Zod Generate finished with errors!`, result.errors);
49+
}
50+
51+
return zodFiles
52+
}

plugins/typescript/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { generateSchemaTypes } from "./generators/generateSchemaTypes";
33
export { generateReactQueryComponents } from "./generators/generateReactQueryComponents";
44
export { generateReactQueryFunctions } from "./generators/generateReactQueryFunctions";
55
export { generateFetchers } from "./generators/generateFetchers";
6+
export { generateZod } from "./generators/generateZod";
67

78
// Utils
89
export { renameComponent } from "./utils/renameComponent";

0 commit comments

Comments
 (0)