Skip to content

Commit

Permalink
fix(Imports): support for named imports in generated files (#287)
Browse files Browse the repository at this point in the history
* fix: update generated integration file

* fix: add support for named imports

* test: add one more

* chore: disable codecov
  • Loading branch information
tvillaren authored Nov 24, 2024
1 parent 476180f commit 56b4374
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 30 deletions.
7 changes: 6 additions & 1 deletion example/heros.zod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Generated by ts-to-zod
import { z } from "zod";
import { EnemyPower, Villain, EvilPlan, EvilPlanDetails } from "./heros";
import {
type Villain,
type EvilPlan,
type EvilPlanDetails,
EnemyPower,
} from "./heros";

import { personSchema } from "./person.zod";

Expand Down
128 changes: 128 additions & 0 deletions src/core/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,62 @@ describe("generate", () => {
});
});

describe("named import", () => {
const sourceText = `
import { Villain as Nemesis } from "@project/villain-module";
export interface Superman {
nemesis: Nemesis;
id: number
}
`;

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

it("should generate the zod schemas", () => {
expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from "zod";
const nemesisSchema = z.any();
export const supermanSchema = z.object({
nemesis: nemesisSchema,
id: z.number()
});
"
`);
});

it("should generate the integration tests", () => {
expect(getIntegrationTestFile("./hero", "hero.zod"))
.toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from "zod";
import * as spec from "./hero";
import * as generated from "hero.zod";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function expectType<T>(_: T) {
/* noop */
}
export type supermanSchemaInferredType = z.infer<typeof generated.supermanSchema>;
expectType<spec.Superman>({} as supermanSchemaInferredType)
expectType<supermanSchemaInferredType>({} as spec.Superman)
"
`);
});

it("should not have any errors", () => {
expect(errors.length).toBe(0);
});
});

describe("multiple imports", () => {
const sourceText = `
import { Name } from "nameModule";
Expand Down Expand Up @@ -1115,6 +1171,78 @@ describe("generate", () => {
});
});

describe("one named import in one statement", () => {
const input = "./hero";
const output = "./hero.zod";
const inputOutputMappings = [{ input, output }];

const sourceText = `
import { Hero as Superman } from "${input}"
export interface Person {
id: number
hero: Superman
}
`;

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

it("should generate the zod schemas with right import", () => {
expect(getZodSchemasFile(input)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from "zod";
import { heroSchema as supermanSchema } from "${output}";
export const personSchema = z.object({
id: z.number(),
hero: supermanSchema
});
"
`);
});
});

describe("mixed named imports in one statement", () => {
const input = "./hero";
const output = "./hero.zod";
const inputOutputMappings = [{ input, output }];

const sourceText = `
import { Hero as Superman, Villain } from "${input}"
export interface Person {
id: number
hero: Superman
nemesis: Villain
}
`;

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

it("should generate the zod schemas with right import", () => {
expect(getZodSchemasFile(input)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from "zod";
import { heroSchema as supermanSchema, villainSchema } from "${output}";
export const personSchema = z.object({
id: z.number(),
hero: supermanSchema,
nemesis: villainSchema
});
"
`);
});
});

describe("one import in one statement, alternate getSchemaName for mapping", () => {
const input = "./hero";
const output = "./hero.zod";
Expand Down
33 changes: 23 additions & 10 deletions src/core/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
import {
getImportIdentifiers,
createImportNode,
ImportIdentifier,
getSingleImportIdentierForNode,
} from "../utils/importHandling";

import { generateIntegrationTests } from "./generateIntegrationTests";
Expand Down Expand Up @@ -129,7 +131,7 @@ export function generate({

if (ts.isImportDeclaration(node) && node.importClause) {
const identifiers = getImportIdentifiers(node);
identifiers.forEach((i) => typeNameMapping.set(i, node));
identifiers.forEach(({ name }) => typeNameMapping.set(name, node));

// Check if we're importing from a mapped file
const eligibleMapping = inputOutputMappings.find(
Expand All @@ -144,20 +146,26 @@ export function generate({
const schemaMethod =
eligibleMapping.getSchemaName || DEFAULT_GET_SCHEMA;

const identifiers = getImportIdentifiers(node);
identifiers.forEach((i) =>
importedZodNamesAvailable.set(i, schemaMethod(i))
identifiers.forEach(({ name }) =>
importedZodNamesAvailable.set(name, schemaMethod(name))
);

const zodImportNode = createImportNode(
identifiers.map(schemaMethod),
identifiers.map(({ name, original }) => {
return {
name: schemaMethod(name),
original: original ? schemaMethod(original) : undefined,
};
}),
eligibleMapping.output
);
zodImportNodes.push(zodImportNode);
}
// Not a Zod import, handling it as 3rd party import later on
else {
identifiers.forEach((i) => externalImportNamesAvailable.add(i));
identifiers.forEach(({ name }) =>
externalImportNamesAvailable.add(name)
);
}
}
};
Expand Down Expand Up @@ -189,7 +197,7 @@ export function generate({
const importedZodSchemas = new Set<string>();

// All original import to keep in the target
const importsToKeep = new Map<ts.ImportDeclaration, string[]>();
const importsToKeep = new Map<ts.ImportDeclaration, ImportIdentifier[]>();

/**
* We browse all the extracted type references from the source file
Expand All @@ -208,10 +216,15 @@ export function generate({
// If the reference is part of a qualified name, we need to import it from the same file
if (typeRef.partOfQualifiedName) {
const identifiers = importsToKeep.get(node);
const importIdentifier = getSingleImportIdentierForNode(
node,
typeRef.name
);
if (!importIdentifier) return;
if (identifiers) {
identifiers.push(typeRef.name);
identifiers.push(importIdentifier);
} else {
importsToKeep.set(node, [typeRef.name]);
importsToKeep.set(node, [importIdentifier]);
}
return;
}
Expand Down Expand Up @@ -379,7 +392,7 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}`

const zodImportToOutput = zodImportNodes.filter((node) => {
const nodeIdentifiers = getImportIdentifiers(node);
return nodeIdentifiers.some((i) => importedZodSchemas.has(i));
return nodeIdentifiers.some(({ name }) => importedZodSchemas.has(name));
});

const originalImportsToOutput = Array.from(importsToKeep.keys()).map((node) =>
Expand Down
2 changes: 1 addition & 1 deletion src/core/validateGeneratedTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function validateGeneratedTypes({
) {
if (node.importClause) {
const identifiers = getImportIdentifiers(node);
identifiers.forEach((i) => importsToHandleAsAny.add(i));
identifiers.forEach(({ name }) => importsToHandleAsAny.add(name));
}
}
};
Expand Down
34 changes: 24 additions & 10 deletions src/utils/importHandling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("getImportIdentifiers", () => {
`;

expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
"MyGlobal",
{ name: "MyGlobal" },
]);
});

Expand All @@ -27,7 +27,7 @@ describe("getImportIdentifiers", () => {
`;

expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
"MyGlobal",
{ name: "MyGlobal" },
]);
});

Expand All @@ -37,7 +37,7 @@ describe("getImportIdentifiers", () => {
`;

expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
"MyGlobal",
{ name: "MyGlobal" },
]);
});

Expand All @@ -47,7 +47,7 @@ describe("getImportIdentifiers", () => {
`;

expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
"MyGlobal",
{ name: "MyGlobal", original: "AA" },
]);
});

Expand All @@ -57,8 +57,8 @@ describe("getImportIdentifiers", () => {
`;

expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
"MyGlobal",
"MyGlobal2",
{ name: "MyGlobal" },
{ name: "MyGlobal2" },
]);
});

Expand All @@ -68,8 +68,8 @@ describe("getImportIdentifiers", () => {
`;

expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
"MyGlobal",
"MyGlobal2",
{ name: "MyGlobal" },
{ name: "MyGlobal2" },
]);
});
});
Expand All @@ -85,7 +85,7 @@ describe("createImportNode", () => {
}

it("should create an ImportDeclaration node correctly", () => {
const identifiers = ["Test1", "Test2"];
const identifiers = [{ name: "Test1" }, { name: "Test2" }];
const path = "./testPath";

const expected = 'import { Test1, Test2 } from "./testPath";';
Expand All @@ -96,11 +96,25 @@ describe("createImportNode", () => {
});

it("should handle empty identifiers array", () => {
const identifiers: string[] = [];
const path = "./testPath";

// Yes, this is valid
const expected = 'import {} from "./testPath";';
const result = createImportNode([], path);

expect(printNode(result)).toEqual(expected);
});

it("should create an ImportDeclaration with alias", () => {
const identifiers = [
{ name: "Test1", original: "T1" },
{ name: "Test2" },
{ name: "Test3", original: "T3" },
];
const path = "./testPath";

const expected =
'import { T1 as Test1, Test2, T3 as Test3 } from "./testPath";';

const result = createImportNode(identifiers, path);

Expand Down
Loading

0 comments on commit 56b4374

Please sign in to comment.