From f638921f345733752436af53cffa2f2bdaecf903 Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Sun, 23 May 2021 17:47:59 +0200 Subject: [PATCH] feat: Update to zod 3.0.2 --- package.json | 2 +- src/core/validateGeneratedTypes.test.ts | 52 ++++++++++ src/core/validateGeneratedTypes.ts | 6 +- src/utils/resolveDefaultProperties.test.ts | 109 +++++++++++++++++++++ src/utils/resolveDefaultProperties.ts | 54 ++++++++++ yarn.lock | 8 +- 6 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 src/utils/resolveDefaultProperties.test.ts create mode 100644 src/utils/resolveDefaultProperties.ts diff --git a/package.json b/package.json index cb5e7f3..884e7de 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "tslib": "^2.1.0", "tsutils": "^3.21.0", "typescript": "^4.2.3", - "zod": "^3.0.0-alpha.33" + "zod": "^3.0.2" }, "devDependencies": { "@types/async": "^3.2.6", diff --git a/src/core/validateGeneratedTypes.test.ts b/src/core/validateGeneratedTypes.test.ts index 6b28af6..b83dc1a 100644 --- a/src/core/validateGeneratedTypes.test.ts +++ b/src/core/validateGeneratedTypes.test.ts @@ -97,4 +97,56 @@ describe("validateGeneratedTypes", () => { ] `); }); + + it("should deal with optional value with default", () => { + const sourceTypes = { + sourceText: ` + export interface Citizen { + /** + * @default true + */ + isVillain?: boolean; + }; + `, + relativePath: "source.ts", + }; + + const zodSchemas = { + sourceText: `// Generated by ts-to-zod + import { z } from "zod"; + export const citizenSchema = z.object({ + isVillain: z.boolean().optional().default(true) + }); + `, + relativePath: "source.zod.ts", + }; + + const integrationTests = { + sourceText: `// Generated by ts-to-zod + import { z } from "zod"; + + import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}"; + import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}"; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function expectType(_: T) { + /* noop */ + } + + export type CitizenInferredType = z.infer; + + expectType({} as spec.Citizen); + expectType({} as CitizenInferredType); + `, + relativePath: "source.integration.ts", + }; + + const errors = validateGeneratedTypes({ + sourceTypes, + zodSchemas, + integrationTests, + }); + + expect(errors).toEqual([]); + }); }); diff --git a/src/core/validateGeneratedTypes.ts b/src/core/validateGeneratedTypes.ts index 025ee7e..53221ff 100644 --- a/src/core/validateGeneratedTypes.ts +++ b/src/core/validateGeneratedTypes.ts @@ -5,6 +5,7 @@ import { } from "@typescript/vfs"; import ts from "typescript"; import { join } from "path"; +import { resolveDefaultProperties } from "../utils/resolveDefaultProperties"; interface File { sourceText: string; relativePath: string; @@ -34,7 +35,10 @@ export function validateGeneratedTypes({ target: compilerOptions.target, }); const projectRoot = process.cwd(); - fsMap.set(getPath(sourceTypes), sourceTypes.sourceText); + fsMap.set( + getPath(sourceTypes), + resolveDefaultProperties(sourceTypes.sourceText) + ); fsMap.set(getPath(zodSchemas), zodSchemas.sourceText); fsMap.set(getPath(integrationTests), integrationTests.sourceText); diff --git a/src/utils/resolveDefaultProperties.test.ts b/src/utils/resolveDefaultProperties.test.ts new file mode 100644 index 0000000..156176e --- /dev/null +++ b/src/utils/resolveDefaultProperties.test.ts @@ -0,0 +1,109 @@ +import { resolveDefaultProperties } from "./resolveDefaultProperties"; + +describe("resolveDefaultProperties", () => { + it("should remove the question mark if @default is defined (interface)", () => { + const sourceText = ` + /** + * A citizen + */ + export interface Citizen { + name: string; + /** + * @default true + */ + isVillain?: boolean; + } + `; + expect(resolveDefaultProperties(sourceText)).toMatchInlineSnapshot(` + "/** + * A citizen + */ + export interface Citizen { + name: string; + /** + * @default true + */ + isVillain: boolean; + } + " + `); + }); + + it("should remove the question mark if @default is defined (type)", () => { + const sourceText = ` + /** + * A citizen + */ + export type Citizen = { + name: string; + /** + * @default true + */ + isVillain?: boolean; + }; + `; + expect(resolveDefaultProperties(sourceText)).toMatchInlineSnapshot(` + "/** + * A citizen + */ + export type Citizen = { + name: string; + /** + * @default true + */ + isVillain: boolean; + }; + " + `); + }); + + it("should remove `undefined` if @default is defined", () => { + const sourceText = ` + /** + * A citizen + */ + export interface Citizen { + name: string; + /** + * @default true + */ + isVillain: boolean | undefined; + } + `; + expect(resolveDefaultProperties(sourceText)).toMatchInlineSnapshot(` + "/** + * A citizen + */ + export interface Citizen { + name: string; + /** + * @default true + */ + isVillain: boolean; + } + " + `); + }); + + it("should do nothing if no @default", () => { + const sourceText = ` + /** + * A citizen + */ + export interface Citizen { + name: string; + isVillain?: boolean; + } + `; + expect(resolveDefaultProperties(sourceText)).toMatchInlineSnapshot(` + "/** + * A citizen + */ + export interface Citizen { + name: string; + isVillain?: boolean; + } + " + `); + }); +}); diff --git a/src/utils/resolveDefaultProperties.ts b/src/utils/resolveDefaultProperties.ts new file mode 100644 index 0000000..ae08b58 --- /dev/null +++ b/src/utils/resolveDefaultProperties.ts @@ -0,0 +1,54 @@ +import ts from "typescript"; +import { getJSDocTags } from "../core/jsDocTags"; + +/** + * Remove optional properties when `@default` jsdoc tag is defined. + * + * Indeed, `z.{type}().optional().default({value})` will be + * compile as a non-optional type. + */ +export function resolveDefaultProperties(sourceText: string) { + const sourceFile = ts.createSourceFile( + "index.ts", + sourceText, + ts.ScriptTarget.Latest + ); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + + const removeOptionalTransformer: ts.TransformerFactory = ( + context + ) => { + const visit: ts.Visitor = (node) => { + node = ts.visitEachChild(node, visit, context); + + if (ts.isPropertySignature(node)) { + const jsDocTags = getJSDocTags(node, sourceFile); + if (jsDocTags.default !== undefined) { + const type = node.type + ? ts.visitEachChild(node.type, omitUndefinedKeyword, context) + : undefined; + return ts.factory.createPropertySignature( + node.modifiers, + node.name, + undefined, // Remove `questionToken` + type + ); + } + } + return node; + }; + + return (node) => ts.visitNode(node, visit); + }; + + const outputFile = ts.transform(sourceFile, [removeOptionalTransformer]); + + return printer.printFile(outputFile.transformed[0]); +} + +function omitUndefinedKeyword(node: ts.Node) { + if (node.kind === ts.SyntaxKind.UndefinedKeyword) { + return undefined; + } + return node; +} diff --git a/yarn.lock b/yarn.lock index 1887eca..d298671 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5028,7 +5028,7 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== -zod@^3.0.0-alpha.33: - version "3.0.0-alpha.33" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.0.0-alpha.33.tgz#8562cc92e57b7df7e0f99f3376181782d7233cfb" - integrity sha512-zxh7bbKBd1gPiHhjFL1f4QNJm7fnw8IJir84pFhqVsHalSBFsxIhDTduV9MdpCTpyfOvRnFgOF0rqrdLeQTEyA== +zod@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.0.2.tgz#0d8f0adbc7569e1a3c67b2cc788f81a55dc8a403" + integrity sha512-a+9VrxBi5CWBFq2LO5aNgbAaIRzPpBLbH4qGjSFeKd/ClLAXZq1dNFLTe9N1VDUBKxqXgHVkMlyp5MtSJylJww==