Skip to content

Commit

Permalink
add some drafts
Browse files Browse the repository at this point in the history
  • Loading branch information
dvv committed Mar 30, 2023
1 parent 7bbed16 commit 96f36d3
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"async": "^3.2.0",
"case": "^1.6.3",
"chokidar": "^3.5.1",
"debug": "^4.3.4",
"fs-extra": "^9.1.0",
"inquirer": "^8.2.0",
"lodash": "^4.17.21",
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export type Config = {
*/
skipParseJSDoc?: boolean;

/**
* Generated file header
*
* @default "// Generated by ts-to-zod\nimport { z } from \"zod\";"
*/
headerText?: string;

/**
* Path of z.infer<> types file.
*/
Expand Down
1 change: 1 addition & 0 deletions src/config.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const configSchema = z.object({
getSchemaName: getSchemaNameSchema.optional(),
keepComments: z.boolean().optional().default(false),
skipParseJSDoc: z.boolean().optional().default(false),
headerText: z.string().default("// Generated by ts-to-zod\nimport { z } from \"zod\";"),
inferredTypes: z.string().optional(),
});

Expand Down
18 changes: 12 additions & 6 deletions src/core/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export interface GenerateProps {
*/
skipParseJSDoc?: boolean;

/**
* Generated file header
*
* @default "// Generated by ts-to-zod\nimport { z } from \"zod\";"
*/
headerText?: string;

/**
* Path of z.infer<> types file.
*/
Expand All @@ -66,6 +73,8 @@ export function generate({
getSchemaName = (id) => camel(id) + "Schema",
keepComments = false,
skipParseJSDoc = false,
headerText = `// Generated by ts-to-zod
import { z } from "z";`
}: GenerateProps) {
// Create a source file and deal with modules
const sourceFile = resolveModules(sourceText);
Expand Down Expand Up @@ -234,8 +243,7 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}`
const imports = Array.from(typeImports.values());
const getZodSchemasFile = (
typesImportPath: string
) => `// Generated by ts-to-zod
import { z } from "zod";
) => `${headerText}
${
imports.length
? `import { ${imports.join(", ")} } from "${typesImportPath}";\n`
Expand All @@ -258,8 +266,7 @@ ${Array.from(statements.values())
const getIntegrationTestFile = (
typesImportPath: string,
zodSchemasImportPath: string
) => `// Generated by ts-to-zod
import { z } from "zod";
) => `${headerText}
import * as spec from "${typesImportPath}";
import * as generated from "${zodSchemasImportPath}";
Expand Down Expand Up @@ -288,8 +295,7 @@ ${testCases.map(print).join("\n")}

const getInferredTypes = (
zodSchemasImportPath: string
) => `// Generated by ts-to-zod
import { z } from "zod";
) => `${headerText}
import * as generated from "${zodSchemasImportPath}";
Expand Down
133 changes: 124 additions & 9 deletions src/core/generateZodSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,13 @@ export function generateZodSchemaVariableStatement({
| ts.CallExpression
| ts.Identifier
| ts.PropertyAccessExpression
| ts.ArrowFunction
| undefined;
let dependencies: string[] = [];
let requiresImport = false;

if (ts.isInterfaceDeclaration(node)) {
let schemaExtensionClauses: string[] | undefined;
if (node.typeParameters) {
throw new Error("Interface with generics are not supported!");
}
if (node.heritageClauses) {
// Looping on heritageClauses browses the "extends" keywords
schemaExtensionClauses = node.heritageClauses.reduce(
Expand Down Expand Up @@ -111,9 +109,6 @@ export function generateZodSchemaVariableStatement({
}

if (ts.isTypeAliasDeclaration(node)) {
if (node.typeParameters) {
throw new Error("Type with generics are not supported!");
}
const jsDocTags = skipParseJSDoc ? {} : getJSDocTags(node, sourceFile);

schema = buildZodPrimitive({
Expand All @@ -133,6 +128,39 @@ export function generateZodSchemaVariableStatement({
requiresImport = true;
}

// process generic dependencies
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
if (schema !== undefined && node.typeParameters) {
const genericTypes = node
.typeParameters.map((p) => `${p.name.escapedText}`)
const genericDependencies = genericTypes.map((p) => getDependencyName(p))
dependencies = dependencies
.filter((dep) => !genericDependencies.includes(dep));
schema = f.createArrowFunction(
undefined,
genericTypes.map((dep) => f.createIdentifier(dep)),
genericTypes.map((dep) => f.createParameterDeclaration(
undefined,
undefined,
undefined,
f.createIdentifier(getDependencyName(dep)),
undefined,
f.createTypeReferenceNode(
f.createQualifiedName(
f.createIdentifier(zodImportValue),
f.createIdentifier(`ZodSchema<${dep}>`)
),
undefined
),
undefined
)),
undefined,
f.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
schema
);
}
}

return {
dependencies: uniq(dependencies),
statement: f.createVariableStatement(
Expand Down Expand Up @@ -205,7 +233,37 @@ function buildZodProperties({
return properties;
}

function buildZodPrimitive({
// decorate builder to allow for schema appending/overriding
function buildZodPrimitive(opts: {
z: string;
typeNode: ts.TypeNode;
isOptional: boolean;
isNullable?: boolean;
isPartial?: boolean;
isRequired?: boolean;
jsDocTags: JSDocTags;
sourceFile: ts.SourceFile;
dependencies: string[];
getDependencyName: (identifierName: string) => string;
skipParseJSDoc: boolean;
}) {
const schema = opts.jsDocTags.schema
delete opts.jsDocTags.schema
const generatedSchema = _buildZodPrimitive(opts);
// schema not specified? return generated one
if (!schema) return generatedSchema;
// schema starts with dot? append it
if (schema.startsWith(".")) {
return f.createPropertyAccessExpression(generatedSchema, f.createIdentifier(schema.slice(1)));
}
// otherwise use schema verbatim
return f.createPropertyAccessExpression(
f.createIdentifier(opts.z),
f.createIdentifier(schema)
);
}

function _buildZodPrimitive({
z,
typeNode,
isOptional,
Expand Down Expand Up @@ -493,6 +551,18 @@ function buildZodPrimitive({

const nodes = typeNode.types.filter(isNotNull);

// string-only enum? issue z.enum
if (typeNode.types.every((i) =>
ts.isLiteralTypeNode(i) && i.literal.kind === ts.SyntaxKind.StringLiteral
)) {
return buildZodSchema(
z,
"enum",
[f.createArrayLiteralExpression(nodes.map((i) => (i as any)["literal"]))],
zodProperties
);
}

// type A = | 'b' is a valid typescript definition
// Zod does not allow `z.union(['b']), so we have to return just the value
if (nodes.length === 1) {
Expand Down Expand Up @@ -530,10 +600,16 @@ function buildZodPrimitive({
});
}

// discriminator specified? use discriminatedUnion
return buildZodSchema(
z,
"union",
[f.createArrayLiteralExpression(values)],
jsDocTags.discriminator !== undefined
? "discriminatedUnion"
: "union",
jsDocTags.discriminator !== undefined
? [f.createStringLiteral(jsDocTags.discriminator),
f.createArrayLiteralExpression(values)]
: [f.createArrayLiteralExpression(values)],
zodProperties
);
}
Expand Down Expand Up @@ -726,6 +802,45 @@ function buildZodPrimitive({
);
}

/*
// TRPC procedures? how to iterate over interface methods?
if (ts.isFunctionTypeNode(typeNode)) {
let exp = f.createPropertyAccessExpression(f.createIdentifier("t.procedure"), f.createIdentifier("input"));
exp = f.createCallExpression(exp, undefined, typeNode.parameters.map((p) =>
buildZodPrimitive({
z,
typeNode:
p.type || f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
jsDocTags,
sourceFile,
dependencies,
getDependencyName,
isOptional: Boolean(p.questionToken),
skipParseJSDoc,
})
));
exp = f.createPropertyAccessExpression(exp, f.createIdentifier("output"));
exp = f.createCallExpression(exp, undefined, [
buildZodPrimitive({
z,
typeNode: typeNode.type,
jsDocTags,
sourceFile,
dependencies,
getDependencyName,
isOptional: false,
skipParseJSDoc,
}),
]);
exp = f.createPropertyAccessExpression(exp, f.createIdentifier("query"));
exp = f.createCallExpression(exp, undefined, [
// f.createIdentifier(`({ ctx, input }) => { throw new TRPCError({ code: "NOT_FOUND", cause: { ctx, input } }) }`)
f.createIdentifier(`({ ctx, input }) => { }`)
]);
return exp;
}
*/

if (ts.isIndexedAccessTypeNode(typeNode)) {
return buildSchemaReference({
node: typeNode,
Expand Down
16 changes: 16 additions & 0 deletions src/core/jsDocTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface JSDocTags {
maxLength?: TagWithError<number>;
format?: TagWithError<typeof formats[-1]>;
pattern?: string;
discriminator?: string;
schema?: string;
}

/**
Expand All @@ -46,6 +48,8 @@ function isJSDocTagKey(tagName: string): tagName is keyof JSDocTags {
"maxLength",
"format",
"pattern",
"discriminator",
"schema",
];
return (keys as string[]).includes(tagName);
}
Expand Down Expand Up @@ -142,6 +146,17 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
jsDocTags[tagName] = tag.comment;
}
break;
case "discriminator":
// TODO: ensure node is a union type
if (tag.comment) {
jsDocTags[tagName] = tag.comment;
}
break;
case "schema":
if (tag.comment) {
jsDocTags[tagName] = tag.comment;
}
break;
}
});
});
Expand Down Expand Up @@ -252,6 +267,7 @@ export function jsDocTagToZodProperties(
? [f.createFalse()]
: typeof jsDocTags.default === "number"
? [f.createNumericLiteral(jsDocTags.default)]
// TODO: catch native enum and do not quote it
: [f.createStringLiteral(jsDocTags.default)],
});
}
Expand Down
4 changes: 3 additions & 1 deletion src/utils/getImportPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ export function getImportPath(from: string, to: string) {
const relativePath = slash(relative(from, to).slice(1));
const { dir, name } = parse(relativePath);

return `${dir}/${name}`;
// import from the full path
// TODO: how to apply conditionally?
return `${dir}/${name}.ts`;
}
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2699,6 +2699,13 @@ debug@^4.2.0:
dependencies:
ms "2.1.2"

debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"

decimal.js@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
Expand Down

0 comments on commit 96f36d3

Please sign in to comment.