Skip to content

Commit

Permalink
feat: Handling imports of generated types (#148)
Browse files Browse the repository at this point in the history
* test: failing test case

* test: additional test case

* feat: new util function to extract import identifiers

* feat: more import utils, in their own file

* feat: add new helper generating instanceof

* feat: adding non-relative import handling of classes

* doc: adding documentation

* test: adding extend test case

* clean: remove moved tests after rebase

* test: union case test

* fix: remove unused const

* feat: allow any module import (relative & non-relative)

* test: add test to cover issue #140

* fix: same behavior as #140 with 3rd party imports

* feat: make arg optional

* fix: update tests after Jest update

* fix: typo in tests

* feat: enabling refs to extra files in post-generation validation

* revert switch to generic fixOptional

* fix: remove instanceof handling

* clean: remove wrongly commited file

* clean: remove unused isRelativeModuleImport

* doc: rework

* fix: missing makePosixPath after rebase

* test: adding failing tests

* fix: typo

* fix: complex array were not traversed

* fix: missing TS helpers in type extractor

* test: failing test case

* test: additional test case

* fix: same behavior as #140 with 3rd party imports

* feat: add createImport function

* feat: handle input / output mapping as source for generate

* feat: add zod import handling from config file

* doc: adding documentation for ZOD imports

* refacto test

* test: adding hybrid import

* test: adding new test

* fix: using relative to compare paths

* fix: multiple fixes after rebase

* fix: remove useless imports from output

* update documentation

* fix: build issues after rebase

---------

Co-authored-by: tvillaren <tvillaren@users.noreply.github.com>
  • Loading branch information
tvillaren and tvillaren committed Feb 6, 2024
1 parent a8ea597 commit 1a879b1
Show file tree
Hide file tree
Showing 15 changed files with 551 additions and 26 deletions.
56 changes: 54 additions & 2 deletions README.md
Expand Up @@ -370,7 +370,9 @@ To resume, you can use all the primitive types and some the following typescript
- `Array<>`
- `Promise<>`

This utils is design to work with one file only, and will reference types from the same file:
## Type references

This utility is designed to work with one file at a time (it will not split one file into several), and will handle references to types from the same file:

```ts
// source.ts
Expand All @@ -388,7 +390,9 @@ export const heroSchema = z.object({
});
```

It can also reference imported types from any modules but will use an `any` validation as placeholder, as we cannot now
### Non-Zod imports

`ts-to-zod` can reference imported types from other modules but will use an `any` validation as placeholder, as we cannot now
their schema.

```ts
Expand All @@ -415,6 +419,54 @@ export const heroSchema = z.object({
});
```

### Zod Imports

If an imported type is referenced in the `ts-to-zod.config.js` config (as input), this utility will automatically replace the import with the given output from the file, resolving the relative paths between both.

```ts

//ts-to-zod.config.js
/**
* ts-to-zod configuration.
*
* @type {import("./src/config").TsToZodConfig}
*/
module.exports = [
{
name: "villain",
input: "src/villain.ts",
output: "src/generated/villain.zod.ts",
getSchemaName: (id) => `z${id}`
},
{
name: "hero",
input: "src/example/heros.ts",
output: "src/example/heros.zod.ts",
},
];

// heros.ts (input)
import { Person } from "@3rdparty/person";
import { Villain } from "../villain";

export interface Hero {
name: string;
realPerson: Person;
nemesis: Villain;
}

// heros.zod.ts (output)
import { zVillain } from "../generated/villain.zod";

const personSchema = z.any();

export const heroSchema = z.object({
name: z.string(),
realPerson: personSchema,
nemesis: zVillain;
});
```

## Programmatic API

You need more than one file? Want even more power? No problem, just use the tool as a library.
Expand Down
4 changes: 3 additions & 1 deletion example/heros.ts
@@ -1,3 +1,5 @@
import { Person } from "./person";

export enum EnemyPower {
Flight = "flight",
Strength = "strength",
Expand All @@ -11,7 +13,7 @@ export namespace Skills {
};
}

export interface Enemy {
export interface Enemy extends Person {
name: string;
powers: EnemyPower[];
inPrison: boolean;
Expand Down
4 changes: 3 additions & 1 deletion example/heros.zod.ts
Expand Up @@ -2,13 +2,15 @@
import { z } from "zod";
import { EnemyPower, Villain, EvilPlan, EvilPlanDetails } from "./heros";

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

export const enemyPowerSchema = z.nativeEnum(EnemyPower);

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

export const enemySchema = z.object({
export const enemySchema = personSchema.extend({
name: z.string(),
powers: z.array(enemyPowerSchema),
inPrison: z.boolean(),
Expand Down
4 changes: 4 additions & 0 deletions example/person.ts
@@ -0,0 +1,4 @@
// Used as an import in hero.ts
export interface Person {
realName: string;
}
6 changes: 6 additions & 0 deletions example/person.zod.ts
@@ -0,0 +1,6 @@
// Generated by ts-to-zod
import { z } from "zod";

export const personSchema = z.object({
realName: z.string(),
});
44 changes: 39 additions & 5 deletions src/cli.ts
Expand Up @@ -7,7 +7,7 @@ import { join, parse, relative } from "path";
import prettier from "prettier";
import slash from "slash";
import ts from "typescript";
import { Config, TsToZodConfig } from "./config";
import { Config, TsToZodConfig, InputOutputMapping } from "./config";
import {
getSchemaNameSchema,
nameFilterSchema,
Expand Down Expand Up @@ -133,6 +133,8 @@ class TsToZod extends Command {

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

const ioMappings = getInputOutputMappings(config);

if (Array.isArray(fileConfig)) {
if (args.input || args.output) {
this.error(`INPUT and OUTPUT arguments are not compatible with --all`);
Expand All @@ -141,7 +143,7 @@ class TsToZod extends Command {
await Promise.all(
fileConfig.map(async (config) => {
this.log(`Generating "${config.name}"`);
const result = await this.generate(args, config, flags);
const result = await this.generate(args, config, flags, ioMappings);
if (result.success) {
this.log(` 🎉 Zod schemas generated!`);
} else {
Expand All @@ -156,7 +158,7 @@ class TsToZod extends Command {
this.error(error);
}
} else {
const result = await this.generate(args, fileConfig, flags);
const result = await this.generate(args, fileConfig, flags, ioMappings);
if (result.success) {
this.log(`🎉 Zod schemas generated!`);
} else {
Expand All @@ -177,7 +179,7 @@ class TsToZod extends Command {
? fileConfig.find((i) => i.input === slash(path))
: fileConfig;

const result = await this.generate(args, config, flags);
const result = await this.generate(args, config, flags, ioMappings);
if (result.success) {
this.log(`🎉 Zod schemas generated!`);
} else {
Expand All @@ -193,11 +195,13 @@ class TsToZod extends Command {
* @param args
* @param fileConfig
* @param Flags
* @param inputOutputMappings
*/
async generate(
args: { input?: string; output?: string },
fileConfig: Config | undefined,
Flags: Interfaces.InferredFlags<typeof TsToZod.flags>
Flags: Interfaces.InferredFlags<typeof TsToZod.flags>,
inputOutputMappings: InputOutputMapping[]
): Promise<{ success: true } | { success: false; error: string }> {
const input = args.input || fileConfig?.input;
const output = args.output || fileConfig?.output;
Expand All @@ -214,6 +218,17 @@ See more help with --help`,
const inputPath = join(process.cwd(), input);
const outputPath = join(process.cwd(), output || input);

const relativeIOMappings = inputOutputMappings.map((io) => {
const relativeInput = getImportPath(inputPath, io.input);
const relativeOutput = getImportPath(outputPath, io.output);

return {
input: relativeInput,
output: relativeOutput,
getSchemaName: io.getSchemaName,
};
});

// Check args/flags file extensions
const extErrors: { path: string; expectedExtensions: string[] }[] = [];
if (!hasExtensions(input, typescriptExtensions)) {
Expand Down Expand Up @@ -250,6 +265,7 @@ See more help with --help`,

const generateOptions: GenerateProps = {
sourceText,
inputOutputMappings: relativeIOMappings,
...fileConfig,
};
if (typeof Flags.keepComments === "boolean") {
Expand Down Expand Up @@ -443,4 +459,22 @@ function hasExtensions(path: string, extensions: string[]) {
return extensions.includes(ext);
}

function getInputOutputMappings(
config: TsToZodConfig | undefined
): InputOutputMapping[] {
if (!config) {
return [];
}

if (Array.isArray(config)) {
return config.map((c) => {
const { input, output, getSchemaName } = c;
return { input, output, getSchemaName };
});
}

const { input, output, getSchemaName } = config as Config;
return [{ input, output, getSchemaName }];
}

export = TsToZod;
5 changes: 5 additions & 0 deletions src/config.ts
Expand Up @@ -113,4 +113,9 @@ export type Configs = Array<
}
>;

export type InputOutputMapping = Pick<
Config,
"input" | "output" | "getSchemaName"
>;

export type TsToZodConfig = Config | Configs;
6 changes: 6 additions & 0 deletions src/config.zod.ts
Expand Up @@ -54,4 +54,10 @@ export const configsSchema = z.array(
)
);

export const inputOutputMappingSchema = configSchema.pick({
input: true,
output: true,
getSchemaName: true,
});

export const tsToZodConfigSchema = z.union([configSchema, configsSchema]);

0 comments on commit 1a879b1

Please sign in to comment.