Skip to content

Commit 56b4374

Browse files
authored
fix(Imports): support for named imports in generated files (#287)
* fix: update generated integration file * fix: add support for named imports * test: add one more * chore: disable codecov
1 parent 476180f commit 56b4374

6 files changed

+216
-30
lines changed

example/heros.zod.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Generated by ts-to-zod
22
import { z } from "zod";
3-
import { EnemyPower, Villain, EvilPlan, EvilPlanDetails } from "./heros";
3+
import {
4+
type Villain,
5+
type EvilPlan,
6+
type EvilPlanDetails,
7+
EnemyPower,
8+
} from "./heros";
49

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

src/core/generate.test.ts

+128
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,62 @@ describe("generate", () => {
738738
});
739739
});
740740

741+
describe("named import", () => {
742+
const sourceText = `
743+
import { Villain as Nemesis } from "@project/villain-module";
744+
745+
export interface Superman {
746+
nemesis: Nemesis;
747+
id: number
748+
}
749+
`;
750+
751+
const { getZodSchemasFile, getIntegrationTestFile, errors } = generate({
752+
sourceText,
753+
});
754+
755+
it("should generate the zod schemas", () => {
756+
expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
757+
"// Generated by ts-to-zod
758+
import { z } from "zod";
759+
760+
const nemesisSchema = z.any();
761+
762+
export const supermanSchema = z.object({
763+
nemesis: nemesisSchema,
764+
id: z.number()
765+
});
766+
"
767+
`);
768+
});
769+
770+
it("should generate the integration tests", () => {
771+
expect(getIntegrationTestFile("./hero", "hero.zod"))
772+
.toMatchInlineSnapshot(`
773+
"// Generated by ts-to-zod
774+
import { z } from "zod";
775+
776+
import * as spec from "./hero";
777+
import * as generated from "hero.zod";
778+
779+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
780+
function expectType<T>(_: T) {
781+
/* noop */
782+
}
783+
784+
export type supermanSchemaInferredType = z.infer<typeof generated.supermanSchema>;
785+
786+
expectType<spec.Superman>({} as supermanSchemaInferredType)
787+
expectType<supermanSchemaInferredType>({} as spec.Superman)
788+
"
789+
`);
790+
});
791+
792+
it("should not have any errors", () => {
793+
expect(errors.length).toBe(0);
794+
});
795+
});
796+
741797
describe("multiple imports", () => {
742798
const sourceText = `
743799
import { Name } from "nameModule";
@@ -1115,6 +1171,78 @@ describe("generate", () => {
11151171
});
11161172
});
11171173

1174+
describe("one named import in one statement", () => {
1175+
const input = "./hero";
1176+
const output = "./hero.zod";
1177+
const inputOutputMappings = [{ input, output }];
1178+
1179+
const sourceText = `
1180+
import { Hero as Superman } from "${input}"
1181+
1182+
export interface Person {
1183+
id: number
1184+
hero: Superman
1185+
}
1186+
`;
1187+
1188+
const { getZodSchemasFile } = generate({
1189+
sourceText,
1190+
inputOutputMappings,
1191+
});
1192+
1193+
it("should generate the zod schemas with right import", () => {
1194+
expect(getZodSchemasFile(input)).toMatchInlineSnapshot(`
1195+
"// Generated by ts-to-zod
1196+
import { z } from "zod";
1197+
1198+
import { heroSchema as supermanSchema } from "${output}";
1199+
1200+
export const personSchema = z.object({
1201+
id: z.number(),
1202+
hero: supermanSchema
1203+
});
1204+
"
1205+
`);
1206+
});
1207+
});
1208+
1209+
describe("mixed named imports in one statement", () => {
1210+
const input = "./hero";
1211+
const output = "./hero.zod";
1212+
const inputOutputMappings = [{ input, output }];
1213+
1214+
const sourceText = `
1215+
import { Hero as Superman, Villain } from "${input}"
1216+
1217+
export interface Person {
1218+
id: number
1219+
hero: Superman
1220+
nemesis: Villain
1221+
}
1222+
`;
1223+
1224+
const { getZodSchemasFile } = generate({
1225+
sourceText,
1226+
inputOutputMappings,
1227+
});
1228+
1229+
it("should generate the zod schemas with right import", () => {
1230+
expect(getZodSchemasFile(input)).toMatchInlineSnapshot(`
1231+
"// Generated by ts-to-zod
1232+
import { z } from "zod";
1233+
1234+
import { heroSchema as supermanSchema, villainSchema } from "${output}";
1235+
1236+
export const personSchema = z.object({
1237+
id: z.number(),
1238+
hero: supermanSchema,
1239+
nemesis: villainSchema
1240+
});
1241+
"
1242+
`);
1243+
});
1244+
});
1245+
11181246
describe("one import in one statement, alternate getSchemaName for mapping", () => {
11191247
const input = "./hero";
11201248
const output = "./hero.zod";

src/core/generate.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
import {
2020
getImportIdentifiers,
2121
createImportNode,
22+
ImportIdentifier,
23+
getSingleImportIdentierForNode,
2224
} from "../utils/importHandling";
2325

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

130132
if (ts.isImportDeclaration(node) && node.importClause) {
131133
const identifiers = getImportIdentifiers(node);
132-
identifiers.forEach((i) => typeNameMapping.set(i, node));
134+
identifiers.forEach(({ name }) => typeNameMapping.set(name, node));
133135

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

147-
const identifiers = getImportIdentifiers(node);
148-
identifiers.forEach((i) =>
149-
importedZodNamesAvailable.set(i, schemaMethod(i))
149+
identifiers.forEach(({ name }) =>
150+
importedZodNamesAvailable.set(name, schemaMethod(name))
150151
);
151152

152153
const zodImportNode = createImportNode(
153-
identifiers.map(schemaMethod),
154+
identifiers.map(({ name, original }) => {
155+
return {
156+
name: schemaMethod(name),
157+
original: original ? schemaMethod(original) : undefined,
158+
};
159+
}),
154160
eligibleMapping.output
155161
);
156162
zodImportNodes.push(zodImportNode);
157163
}
158164
// Not a Zod import, handling it as 3rd party import later on
159165
else {
160-
identifiers.forEach((i) => externalImportNamesAvailable.add(i));
166+
identifiers.forEach(({ name }) =>
167+
externalImportNamesAvailable.add(name)
168+
);
161169
}
162170
}
163171
};
@@ -189,7 +197,7 @@ export function generate({
189197
const importedZodSchemas = new Set<string>();
190198

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

194202
/**
195203
* We browse all the extracted type references from the source file
@@ -208,10 +216,15 @@ export function generate({
208216
// If the reference is part of a qualified name, we need to import it from the same file
209217
if (typeRef.partOfQualifiedName) {
210218
const identifiers = importsToKeep.get(node);
219+
const importIdentifier = getSingleImportIdentierForNode(
220+
node,
221+
typeRef.name
222+
);
223+
if (!importIdentifier) return;
211224
if (identifiers) {
212-
identifiers.push(typeRef.name);
225+
identifiers.push(importIdentifier);
213226
} else {
214-
importsToKeep.set(node, [typeRef.name]);
227+
importsToKeep.set(node, [importIdentifier]);
215228
}
216229
return;
217230
}
@@ -379,7 +392,7 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}`
379392

380393
const zodImportToOutput = zodImportNodes.filter((node) => {
381394
const nodeIdentifiers = getImportIdentifiers(node);
382-
return nodeIdentifiers.some((i) => importedZodSchemas.has(i));
395+
return nodeIdentifiers.some(({ name }) => importedZodSchemas.has(name));
383396
});
384397

385398
const originalImportsToOutput = Array.from(importsToKeep.keys()).map((node) =>

src/core/validateGeneratedTypes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function validateGeneratedTypes({
6565
) {
6666
if (node.importClause) {
6767
const identifiers = getImportIdentifiers(node);
68-
identifiers.forEach((i) => importsToHandleAsAny.add(i));
68+
identifiers.forEach(({ name }) => importsToHandleAsAny.add(name));
6969
}
7070
}
7171
};

src/utils/importHandling.test.ts

+24-10
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe("getImportIdentifiers", () => {
1717
`;
1818

1919
expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
20-
"MyGlobal",
20+
{ name: "MyGlobal" },
2121
]);
2222
});
2323

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

2929
expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
30-
"MyGlobal",
30+
{ name: "MyGlobal" },
3131
]);
3232
});
3333

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

3939
expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
40-
"MyGlobal",
40+
{ name: "MyGlobal" },
4141
]);
4242
});
4343

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

4949
expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
50-
"MyGlobal",
50+
{ name: "MyGlobal", original: "AA" },
5151
]);
5252
});
5353

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

5959
expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
60-
"MyGlobal",
61-
"MyGlobal2",
60+
{ name: "MyGlobal" },
61+
{ name: "MyGlobal2" },
6262
]);
6363
});
6464

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

7070
expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
71-
"MyGlobal",
72-
"MyGlobal2",
71+
{ name: "MyGlobal" },
72+
{ name: "MyGlobal2" },
7373
]);
7474
});
7575
});
@@ -85,7 +85,7 @@ describe("createImportNode", () => {
8585
}
8686

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

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

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

102101
// Yes, this is valid
103102
const expected = 'import {} from "./testPath";';
103+
const result = createImportNode([], path);
104+
105+
expect(printNode(result)).toEqual(expected);
106+
});
107+
108+
it("should create an ImportDeclaration with alias", () => {
109+
const identifiers = [
110+
{ name: "Test1", original: "T1" },
111+
{ name: "Test2" },
112+
{ name: "Test3", original: "T3" },
113+
];
114+
const path = "./testPath";
115+
116+
const expected =
117+
'import { T1 as Test1, Test2, T3 as Test3 } from "./testPath";';
104118

105119
const result = createImportNode(identifiers, path);
106120

0 commit comments

Comments
 (0)