Skip to content

Commit

Permalink
feat: Support namespace (#44)
Browse files Browse the repository at this point in the history
* Resolve module as first step

Known issue, this doesn’t work with all type references

* Deal with module

* Add a namespace in the example and fix generation
  • Loading branch information
fabien0102 committed Sep 20, 2021
1 parent 7e9cd82 commit 3255083
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 13 deletions.
9 changes: 6 additions & 3 deletions example/heros.ts
Expand Up @@ -4,9 +4,12 @@ export enum EnemyPower {
Speed = "speed",
}

export type SpeedEnemy = {
power: EnemyPower.Speed;
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Skills {
export type SpeedEnemy = {
power: EnemyPower.Speed;
};
}

export interface Enemy {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion example/heros.zod.ts
Expand Up @@ -4,7 +4,7 @@ import { EnemyPower, Villain } from "./heros";

export const enemyPowerSchema = z.nativeEnum(EnemyPower);

export const speedEnemySchema = z.object({
export const skillsSpeedEnemySchema = z.object({
power: z.literal(EnemyPower.Speed),
});

Expand Down
3 changes: 2 additions & 1 deletion src/cli.ts
Expand Up @@ -241,6 +241,7 @@ See more help with --help`,

const {
errors,
transformedSourceText,
getZodSchemasFile,
getIntegrationTestFile,
hasCircularDependencies,
Expand All @@ -261,7 +262,7 @@ See more help with --help`,
if (flags.all) validatorSpinner.indent = 1;
const generationErrors = await worker.validateGeneratedTypesInWorker({
sourceTypes: {
sourceText,
sourceText: transformedSourceText,
relativePath: "./source.ts",
},
integrationTests: {
Expand Down
82 changes: 80 additions & 2 deletions src/core/generate.test.ts
Expand Up @@ -231,7 +231,7 @@ describe("generate", () => {
keepComments: true,
});

it("should only generate superman schema", () => {
it("should generate superman schema", () => {
expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";
Expand All @@ -248,7 +248,7 @@ describe("generate", () => {
});

describe("with non-exported types", () => {
it("should generate tests only for exported schemas", () => {
it("should generate tests for exported schemas", () => {
const sourceText = `
export type Name = "superman" | "clark kent" | "kal-l";
Expand Down Expand Up @@ -294,4 +294,82 @@ describe("generate", () => {
`);
});
});

describe("with namespace", () => {
const sourceText = `
export namespace Metropolis {
export type Name = "superman" | "clark kent" | "kal-l";
// Note that the Superman is declared after
export type BadassSuperman = Omit<Superman, "underKryptonite">;
export interface Superman {
name: Name;
age: number;
underKryptonite?: boolean;
/**
* @format email
**/
email: string;
}
const fly = () => console.log("I can fly!");
}
`;

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

it("should generate the zod schemas", () => {
expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";
export const metropolisNameSchema = z.union([z.literal(\\"superman\\"), z.literal(\\"clark kent\\"), z.literal(\\"kal-l\\")]);
export const metropolisSupermanSchema = z.object({
name: metropolisNameSchema,
age: z.number(),
underKryptonite: z.boolean().optional(),
email: z.string().email()
});
export const metropolisBadassSupermanSchema = metropolisSupermanSchema.omit({ \\"underKryptonite\\": true });
"
`);
});

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 metropolisNameSchemaInferredType = z.infer<typeof generated.metropolisNameSchema>;
export type metropolisSupermanSchemaInferredType = z.infer<typeof generated.metropolisSupermanSchema>;
export type metropolisBadassSupermanSchemaInferredType = z.infer<typeof generated.metropolisBadassSupermanSchema>;
expectType<spec.MetropolisName>({} as metropolisNameSchemaInferredType)
expectType<metropolisNameSchemaInferredType>({} as spec.MetropolisName)
expectType<spec.MetropolisSuperman>({} as metropolisSupermanSchemaInferredType)
expectType<metropolisSupermanSchemaInferredType>({} as spec.MetropolisSuperman)
expectType<spec.MetropolisBadassSuperman>({} as metropolisBadassSupermanSchemaInferredType)
expectType<metropolisBadassSupermanSchemaInferredType>({} as spec.MetropolisBadassSuperman)
"
`);
});
it("should not have any errors", () => {
expect(errors).toEqual([]);
});
});
});
21 changes: 15 additions & 6 deletions src/core/generate.ts
@@ -1,5 +1,6 @@
import { camel } from "case";
import ts from "typescript";
import { resolveModules } from "../utils/resolveModules";
import { generateIntegrationTests } from "./generateIntegrationTests";
import { generateZodInferredType } from "./generateZodInferredType";
import { generateZodSchemaVariableStatement } from "./generateZodSchema";
Expand Down Expand Up @@ -45,12 +46,8 @@ export function generate({
getSchemaName = (id) => camel(id) + "Schema",
keepComments = false,
}: GenerateProps) {
// Create a source file
const sourceFile = ts.createSourceFile(
"index.ts",
sourceText,
ts.ScriptTarget.Latest
);
// Create a source file and deal with modules
const sourceFile = resolveModules(sourceText);

// Extract the nodes (interface declarations & type aliases)
const nodes: Array<
Expand Down Expand Up @@ -142,9 +139,16 @@ ${missingStatements.map(({ varName }) => `${varName}`).join("\n")}`
newLine: ts.NewLineKind.LineFeed,
removeComments: !keepComments,
});

const printerWithComments = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
});

const print = (node: ts.Node) =>
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);

const transformedSourceText = printerWithComments.printFile(sourceFile);

const imports = Array.from(typeImports.values());
const getZodSchemasFile = (
typesImportPath: string
Expand Down Expand Up @@ -200,6 +204,11 @@ ${testCases.map(print).join("\n")}
`;

return {
/**
* Source text with pre-process applied.
*/
transformedSourceText,

/**
* Get the content of the zod schemas file.
*
Expand Down
103 changes: 103 additions & 0 deletions src/utils/resolveModules.test.ts
@@ -0,0 +1,103 @@
import ts from "typescript";
import { resolveModules } from "./resolveModules";

describe("resolveModules", () => {
it("should prefix interface", () => {
const sourceText = `export namespace Metropolis {
export interface Superman {
name: string;
hasPower: boolean;
}
}`;

expect(print(resolveModules(sourceText))).toMatchInlineSnapshot(`
"export interface MetropolisSuperman {
name: string;
hasPower: boolean;
}
"
`);
});

it("should prefix type", () => {
const sourceText = `export namespace Metropolis {
export type Name = "superman" | "clark kent" | "kal-l";
}`;

expect(print(resolveModules(sourceText))).toMatchInlineSnapshot(`
"export type MetropolisName = \\"superman\\" | \\"clark kent\\" | \\"kal-l\\";
"
`);
});

it("should prefix enum", () => {
const sourceText = `export namespace Metropolis {
export enum Superhero {
Superman = "superman",
ClarkKent = "clark_kent",
};
}`;

expect(print(resolveModules(sourceText))).toMatchInlineSnapshot(`
"export enum MetropolisSuperhero {
Superman = \\"superman\\",
ClarkKent = \\"clark_kent\\"
}
;
"
`);
});

it("should prefix every type references", () => {
const sourceText = `
export type Weakness = "krytonite" | "lois"
export namespace Metropolis {
export type Name = string;
export type BadassSuperman = Omit<Superman, "underKryptonite">;
export interface Superman {
fullName: Name;
name: { first: Name; last: Name };
hasPower: boolean;
weakness: Weakness;
}
export type SupermanBis = {
fullName: Name;
name: { first: Name; last: Name };
hasPower: boolean;
weakness: Weakness;
}
}`;

expect(print(resolveModules(sourceText))).toMatchInlineSnapshot(`
"export type Weakness = \\"krytonite\\" | \\"lois\\";
export type MetropolisName = string;
export type MetropolisBadassSuperman = Omit<MetropolisSuperman, \\"underKryptonite\\">;
export interface MetropolisSuperman {
fullName: MetropolisName;
name: {
first: MetropolisName;
last: MetropolisName;
};
hasPower: boolean;
weakness: Weakness;
}
export type MetropolisSupermanBis = {
fullName: MetropolisName;
name: {
first: MetropolisName;
last: MetropolisName;
};
hasPower: boolean;
weakness: Weakness;
};
"
`);
});
});

const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const print = (sourceFile: ts.SourceFile) => printer.printFile(sourceFile);

0 comments on commit 3255083

Please sign in to comment.