Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/import type #126

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions example/heros.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ export type SupermanName = z.infer<typeof generated.supermanNameSchema>;
export type SupermanInvinciblePower = z.infer<
typeof generated.supermanInvinciblePowerSchema
>;

export type EvilPlan = z.infer<typeof generated.evilPlanSchema>;

export type EvilPlanDetails = z.infer<typeof generated.evilPlanDetailsSchema>;
3 changes: 2 additions & 1 deletion example/heros.zod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Generated by ts-to-zod
import { z } from "zod";
import { EnemyPower, Villain, EvilPlan, EvilPlanDetails } from "./heros";
import type { Villain, EvilPlan, EvilPlanDetails } from "./heros";
import { EnemyPower } from "./heros";

export const enemyPowerSchema = z.nativeEnum(EnemyPower);

Expand Down
38 changes: 28 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,28 @@ import { eachSeries } from "async";
import { createConfig } from "./createConfig";
import chokidar from "chokidar";

type ConfigExt = ".js" | ".cjs";

// Try to load `ts-to-zod.config.js`
// We are doing this here to be able to infer the `flags` & `usage` in the cli help
const tsToZodConfigJs = "ts-to-zod.config.js";
const configPath = join(process.cwd(), tsToZodConfigJs);
const tsToZodConfig = "ts-to-zod.config";
const configPath = join(process.cwd(), tsToZodConfig);
let config: TsToZodConfig | undefined;
let haveMultiConfig = false;
const configKeys: string[] = [];

let configExt: ConfigExt | undefined;
try {
if (existsSync(configPath)) {
const rawConfig = require(slash(relative(__dirname, configPath)));
if (existsSync(`${configPath}.js`)) {
configExt = ".js";
} else if (existsSync(`${configPath}.cjs`)) {
configExt = ".cjs";
}

if (configExt) {
const rawConfig = require(slash(
relative(__dirname, `${configPath}${configExt}`)
));
config = tsToZodConfigSchema.parse(rawConfig);
if (Array.isArray(config)) {
haveMultiConfig = true;
Expand All @@ -41,7 +52,7 @@ try {
} catch (e) {
if (e instanceof Error) {
oclifError(
`"${tsToZodConfigJs}" invalid:
`"${tsToZodConfig}${configExt}" invalid:
${e.message}

Please fix the invalid configuration
Expand All @@ -67,6 +78,11 @@ class TsToZod extends Command {
static flags = {
version: flags.version({ char: "v" }),
help: flags.help({ char: "h" }),
esm: flags.boolean({
default: false,
description:
'Generate TypeScript import statements in ESM format (with ".js" extension)',
}),
keepComments: flags.boolean({
char: "k",
description: "Keep parameters comments",
Expand Down Expand Up @@ -123,7 +139,7 @@ class TsToZod extends Command {
return;
}

const fileConfig = await this.loadFileConfig(config, flags);
const fileConfig = await this.loadFileConfig(config, flags, configExt!);

if (Array.isArray(fileConfig)) {
if (args.input || args.output) {
Expand Down Expand Up @@ -278,7 +294,7 @@ See more help with --help`,
relativePath: "./source.integration.ts",
},
zodSchemas: {
sourceText: getZodSchemasFile("./source"),
sourceText: getZodSchemasFile("./source", sourceText),
relativePath: "./source.zod.ts",
},
skipParseJSDoc: Boolean(generateOptions.skipParseJSDoc),
Expand All @@ -297,7 +313,8 @@ See more help with --help`,
}

const zodSchemasFile = getZodSchemasFile(
getImportPath(outputPath, inputPath)
getImportPath(outputPath, inputPath) + (flags.esm ? ".js" : ""),
sourceText,
);

const prettierConfig = await prettier.resolveConfig(process.cwd());
Expand Down Expand Up @@ -354,7 +371,8 @@ See more help with --help`,
*/
async loadFileConfig(
config: TsToZodConfig | undefined,
flags: OutputFlags<typeof TsToZod.flags>
flags: OutputFlags<typeof TsToZod.flags>,
ext: ConfigExt
): Promise<TsToZodConfig | undefined> {
if (!config) {
return undefined;
Expand All @@ -366,7 +384,7 @@ See more help with --help`,
}>([
{
name: "mode",
message: `You have multiple configs available in "${tsToZodConfigJs}"\n What do you want?`,
message: `You have multiple configs available in "${tsToZodConfig}${ext}"\n What do you want?`,
type: "list",
choices: [
{
Expand Down
1 change: 1 addition & 0 deletions src/config.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const jSDocTagFilterSchema = z
export const configSchema = z.object({
input: z.string(),
output: z.string(),
esm: z.boolean().optional().default(false),
skipValidation: z.boolean().optional(),
nameFilter: nameFilterSchema.optional(),
jsDocTagFilter: jSDocTagFilterSchema.optional(),
Expand Down
75 changes: 75 additions & 0 deletions src/core/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
export const standardBuiltInObjects = [
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#fundamental_objects
"Object",
"Function",
"Boolean",
"Symbol",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#error_objects
"Error",
"AggregateError",
"EvalError",
"RangeError",
"ReferenceError",
"SyntaxError",
"TypeError",
"URIError",
// "InternalError",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#numbers_and_dates
"Number",
"BigInt",
"Math",
// "Date",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#indexed_collections
// "Array",
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"BigInt64Array",
"BigUint64Array",
"Float32Array",
"Float64Array",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#keyed_collections
"Map",
"Set",
"WeakMap",
"WeakSet",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#structured_data
"ArrayBuffer",
"SharedArrayBuffer",
"DataView",
"Atomics",
"JSON",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#managing_memory
"WeakRef",
"FinalizationRegistry",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#control_abstraction_objects
"Promise",
"GeneratorFunction",
"AsyncGeneratorFunction",
"Generator",
"AsyncGenerator",
"AsyncFunction",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#reflection
"Reflect",
"Proxy",
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#internationalization
// "Intl",
// "Intl.Collator",
// "Intl.DateTimeFormat",
// "Intl.DisplayNames",
// "Intl.ListFormat",
// "Intl.Locale",
// "Intl.NumberFormat",
// "Intl.PluralRules",
// "Intl.RelativeTimeFormat",
// "Intl.Segmenter",
];

export const standardBuiltInObjectVarNames = standardBuiltInObjects.map(
(n) => n[0].toLocaleLowerCase() + n.substring(1) + "Schema"
);
19 changes: 10 additions & 9 deletions src/core/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("generate", () => {
});

it("should generate the zod schemas", () => {
expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./hero", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";

Expand Down Expand Up @@ -95,7 +95,7 @@ describe("generate", () => {
});

it("should generate the zod schemas", () => {
expect(getZodSchemasFile("./superhero")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./superhero", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";
import { Superhero } from \\"./superhero\\";
Expand Down Expand Up @@ -160,7 +160,7 @@ describe("generate", () => {
}

export interface IHaveUnknownDependency {
dep: UnknwonDependency; // <- Missing dependency
dep: UnknownDependency; // <- Missing dependency
}
`;

Expand All @@ -169,10 +169,10 @@ describe("generate", () => {
});

it("should generate the zod schemas", () => {
expect(getZodSchemasFile("./villain")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./villain", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";
import { Villain, EvilPlan, EvilPlanDetails } from \\"./villain\\";
import type { Villain, EvilPlan, EvilPlanDetails } from \\"./villain\\";

export const villainSchema: z.ZodSchema<Villain> = z.lazy(() => z.object({
name: z.string(),
Expand Down Expand Up @@ -228,6 +228,7 @@ describe("generate", () => {
expect(errors).toMatchInlineSnapshot(`
Array [
"Some schemas can't be generated due to direct or indirect missing dependencies:
unknownDependencySchema
iHaveUnknownDependencySchema",
]
`);
Expand Down Expand Up @@ -256,7 +257,7 @@ describe("generate", () => {
});

it("should generate superman schema", () => {
expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./hero", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";

Expand Down Expand Up @@ -286,7 +287,7 @@ describe("generate", () => {
});

it("should generate superman schema", () => {
expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./hero", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";

Expand Down Expand Up @@ -334,7 +335,7 @@ describe("generate", () => {
jsDocTagFilter: (tags) => tags.map((tag) => tag.name).includes("zod"),
});

expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./source", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";

Expand Down Expand Up @@ -427,7 +428,7 @@ describe("generate", () => {
});

it("should generate the zod schemas", () => {
expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
expect(getZodSchemasFile("./hero", sourceText)).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";

Expand Down
66 changes: 58 additions & 8 deletions src/core/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { generateIntegrationTests } from "./generateIntegrationTests";
import { generateZodInferredType } from "./generateZodInferredType";
import { generateZodSchemaVariableStatement } from "./generateZodSchema";
import { transformRecursiveSchema } from "./transformRecursiveSchema";
import { standardBuiltInObjectVarNames } from "./const";

export interface GenerateProps {
/**
Expand Down Expand Up @@ -131,6 +132,43 @@ export function generate({
});
const zodSchemaNames = zodSchemas.map(({ varName }) => varName);

// Zod schemas with direct or indirect dependencies that are not in `zodSchemas`, won't be generated
const zodSchemasWithMissingDependencies = new Set<string>();
const standardBuiltInObjects = new Set<string>();
zodSchemas.forEach(
({ varName, dependencies, statement, typeName, requiresImport }) => {
dependencies
.filter((dep) => !zodSchemaNames.includes(dep))
.forEach((dep) => {
if (standardBuiltInObjectVarNames.includes(dep)) {
standardBuiltInObjects.add(dep);
} else {
zodSchemasWithMissingDependencies.add(dep);
zodSchemasWithMissingDependencies.add(varName);
}
});
}
);
zodSchemaNames.push(...standardBuiltInObjects);

zodSchemas.push(
...Array.from(standardBuiltInObjects).map((obj) => {
const typeName = obj[0].toUpperCase() + obj.substring(1, obj.length - 6);
return {
typeName,
varName: obj,
...generateZodSchemaVariableStatement({
typeName,
zodImportValue: "z",
sourceFile,
varName: obj,
getDependencyName: getSchemaName,
skipParseJSDoc,
}),
};
})
);

// Resolves statements order
// A schema can't be declared if all the referenced schemas used inside this one are not previously declared.
const statements = new Map<
Expand All @@ -139,9 +177,6 @@ export function generate({
>();
const typeImports: 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>();

let done = false;
// Loop until no more schemas can be generated and no more schemas with direct or indirect missing dependencies are found
while (
Expand Down Expand Up @@ -232,19 +267,34 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}`
const transformedSourceText = printerWithComments.printFile(sourceFile);

const imports = Array.from(typeImports.values());
const getZodSchemasFile = (
typesImportPath: string
) => `// Generated by ts-to-zod
const getZodSchemasFile = (typesImportPath: string, sourceText: string) => {
const typeImports = [];
const valueImports = [];

for (const type of imports) {
if (new RegExp(`(type|interface) ${type}(?!w)`).test(sourceText)) {
typeImports.push(type);
} else {
valueImports.push(type);
}
}

return `// Generated by ts-to-zod
import { z } from "zod";
${
imports.length
? `import { ${imports.join(", ")} } from "${typesImportPath}";\n`
typeImports.length
? `import type { ${typeImports.join(", ")} } from "${typesImportPath}";\n`
: ""
}${
valueImports.length
? `import { ${valueImports.join(", ")} } from "${typesImportPath}";\n`
: ""
}
${Array.from(statements.values())
.map((statement) => print(statement.value))
.join("\n\n")}
`;
};

const testCases = generateIntegrationTests(
Array.from(statements.values())
Expand Down
Loading