fabien0102 / ts-to-zod

Generate zod schemas from typescript types/interfaces
MIT License
1.24k stars 69 forks source link

Generated imports fail when running in node and using `type: "module"` in package.json #259

Open t-animal opened 3 months ago

t-animal commented 3 months ago

Bug description

ts-to-zod does not include the filename in such a scenario, making it incompatible with "module"-packages running in node.

When running in node (i.e. not targeting a bundler/webbrowser), the typescript "module"-property must be set to "node16" or "nodenext". When using type:"module" in package.json, this means that imports have to include the ".js" suffix.

A minimal example would be:

//package.json
{
  "name": "test-modules",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "ts-to-zod": "^3.10.0",
    "typescript": "^5.5.4",
    "zod": "^3.23.8"
  }
}

//tsconfig.json
{
  "compilerOptions": {
    "lib": ["ES2020"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}

//ts-to-zod.config.cjs
module.exports = [
  { name: "b", input: "b.ts", output: "b.zod.ts" },
  { name: "a", input: "a.ts", output: "a.zod.ts" },
];

//a.ts
import { B } from "./b"; // This should be `import {B} from "./b.js";`

export type A = B | "a";

//b.ts
export type B = "b";

Running tsc gives the following error:

npx tsc
a.ts:3:19 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'?

1 import { B } from "./b"; // This should be `import {B} from "./b.js";`
                    ~~~~~

Found 1 error in a.ts:3

Our current solution is to fix the imports using sed in our build-script:

"build": "node ts-to-zod.config.cjs; npm run patch-zod-import; tsc",
"patch-zod-import": "sed -i 's/\\(^import [^\"]*\"\\.\\/[^\"]*\\).zod\";/\\1.zod.js\";/' src/schemas/*.zod.ts",

Input

Multiple files containing types, referencing each other. E.g:

//a.ts
import { B } from "./b.js";

export type A = B | "a";

//b.ts
export type B = "b";

Expected output

a.zod.ts should be:

// Generated by ts-to-zod
import { z } from "zod";

import { bSchema } from "./b.zod.js";

export const aSchema = z.union([bSchema, z.literal("a")]);

Actual output

// Generated by ts-to-zod
import { z } from "zod";

import { bSchema } from "./b.zod";

export const aSchema = z.union([bSchema, z.literal("a")]);

Versions

ydennisy commented 2 months ago

Hi @tvillaren do you have an ETA for a fix for this?

tvillaren commented 2 months ago

Hello,

Not really. I you have a solution in mind, please feel free to open a PR! 🙏 Thanks!

t-animal commented 2 months ago

Fwiw, I saw that detection for this setting is already in the codebase, somewhere. I can't work on this atm, though....

hongkongkiwi commented 2 weeks ago

Be great to get a simple fix for this, because it makes it actually totally unusuable in Deno in certain situations (when importing other files, it MUST have the extension, you can't just import it without). This causes issues when you have multiple files relying on another.

I did have a sed workaround, but that only works in certain situations.

hongkongkiwi commented 2 weeks ago

To elaborate, this will fail in the validation step in certain cases, specifically for me it's when using an enum as zod wants to infer back to the original enum, but cannot and so ts-to-zod fails in the validation step and never generates the code (so we can't use sed).

t-animal commented 2 weeks ago

For future reference: The code for detecting if the project has type: "module" is already here and I think the fix could be applied here, when actually writing the import to the output string or probably rather here, when creating the import node