Skip to content

Commit

Permalink
feat: add type notation to type imports (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
sleewoo authored Aug 29, 2024
1 parent 38c795f commit 941169d
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 38 deletions.
37 changes: 36 additions & 1 deletion src/core/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ describe("generate", () => {
expect(getZodSchemasFile("./villain")).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from "zod";
import { Villain, EvilPlan, EvilPlanDetails } from "./villain";
import { type Villain, type EvilPlan, type EvilPlanDetails } from "./villain";
export const villainSchema: z.ZodSchema<Villain> = z.lazy(() => z.object({
name: z.string(),
Expand Down Expand Up @@ -961,6 +961,41 @@ describe("generate", () => {
`);
});
});

describe("with mixed imports", () => {
const input = "./person";

const sourceText = `
import { PersonEnum } from "${input}"
export interface Hero {
id: number
hero: PersonEnum.Hero
parent: Hero
}
`;

const { getZodSchemasFile } = generate({
sourceText,
});

it("should add type notation to non-enum imports", () => {
expect(getZodSchemasFile(input)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from "zod";
import { type Hero } from "${input}";
import { PersonEnum } from "${input}";
export const heroSchema: z.ZodSchema<Hero> = z.lazy(() => z.object({
id: z.number(),
hero: z.literal(PersonEnum.Hero),
parent: heroSchema
}));
"
`);
});
});
});

describe("with input/output mappings to manage imports", () => {
Expand Down
75 changes: 41 additions & 34 deletions src/core/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export function generate({
varName,
zodImportValue: "z",
}),
requiresImport: false,
enumImport: false,
typeName: importName,
varName,
};
Expand All @@ -282,8 +282,9 @@ export function generate({
{ typeName: string; value: ts.VariableStatement }
>();

// Keep track of types which need to be imported from the source file
// Keep track of types/enums which need to be imported from the source file
const sourceTypeImports: Set<string> = new Set();
const sourceEnumImports: Set<string> = new Set();

// Zod schemas with direct or indirect dependencies that are not in `zodSchemas`, won't be generated
const zodSchemasWithMissingDependencies = new Set<string>();
Expand All @@ -302,40 +303,38 @@ export function generate({
!statements.has(varName) &&
!zodSchemasWithMissingDependencies.has(varName)
)
.forEach(
({ varName, dependencies, statement, typeName, requiresImport }) => {
const isCircular = dependencies.includes(varName);
const notGeneratedDependencies = dependencies
.filter((dep) => dep !== varName)
.filter((dep) => !statements.has(dep))
.filter((dep) => !importedZodSchemas.has(dep));
if (notGeneratedDependencies.length === 0) {
done = false;
if (isCircular) {
sourceTypeImports.add(typeName);
statements.set(varName, {
value: transformRecursiveSchema("z", statement, typeName),
typeName,
});
} else {
if (requiresImport) {
sourceTypeImports.add(typeName);
}
statements.set(varName, { value: statement, typeName });
.forEach(({ varName, dependencies, statement, typeName, enumImport }) => {
const isCircular = dependencies.includes(varName);
const notGeneratedDependencies = dependencies
.filter((dep) => dep !== varName)
.filter((dep) => !statements.has(dep))
.filter((dep) => !importedZodSchemas.has(dep));
if (notGeneratedDependencies.length === 0) {
done = false;
if (isCircular) {
sourceTypeImports.add(typeName);
statements.set(varName, {
value: transformRecursiveSchema("z", statement, typeName),
typeName,
});
} else {
if (enumImport) {
sourceEnumImports.add(typeName);
}
} else if (
// Check if every dependency is (in `zodSchemas` and not in `zodSchemasWithMissingDependencies`)
!notGeneratedDependencies.every(
(dep) =>
zodSchemaNames.includes(dep) &&
!zodSchemasWithMissingDependencies.has(dep)
)
) {
done = false;
zodSchemasWithMissingDependencies.add(varName);
statements.set(varName, { value: statement, typeName });
}
} else if (
// Check if every dependency is (in `zodSchemas` and not in `zodSchemasWithMissingDependencies`)
!notGeneratedDependencies.every(
(dep) =>
zodSchemaNames.includes(dep) &&
!zodSchemasWithMissingDependencies.has(dep)
)
) {
done = false;
zodSchemasWithMissingDependencies.add(varName);
}
);
});
}

// Generate remaining schemas, which have circular dependencies with loop of length > 1 like: A->B—>C->A
Expand Down Expand Up @@ -390,7 +389,15 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}`
)
);

const sourceTypeImportsValues = Array.from(sourceTypeImports.values());
const sourceTypeImportsValues = [
...sourceTypeImports.values(),
...sourceEnumImports.values(),
].map((name) => {
return sourceEnumImports.has(name)
? name // enum import, no type notation added
: `type ${name}`;
});

const getZodSchemasFile = (
typesImportPath: string
) => `// Generated by ts-to-zod
Expand Down
6 changes: 3 additions & 3 deletions src/core/generateZodSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function generateZodSchemaVariableStatement({
| ts.PropertyAccessExpression
| undefined;
let dependencies: string[] = [];
let requiresImport = false;
let enumImport = false;

if (ts.isInterfaceDeclaration(node)) {
let schemaExtensionClauses: SchemaExtensionClause[] | undefined;
Expand Down Expand Up @@ -177,7 +177,7 @@ export function generateZodSchemaVariableStatement({

if (ts.isEnumDeclaration(node)) {
schema = buildZodSchema(zodImportValue, "nativeEnum", [node.name]);
requiresImport = true;
enumImport = true;
}

return {
Expand All @@ -196,7 +196,7 @@ export function generateZodSchemaVariableStatement({
ts.NodeFlags.Const
)
),
requiresImport,
enumImport,
};
}

Expand Down

0 comments on commit 941169d

Please sign in to comment.