zod-v3-to-v4 1.10.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -132,6 +132,9 @@ pnpm playground:interactive
132
132
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/yann-combarnous"><img src="https://avatars.githubusercontent.com/u/39089766?v=4?s=100" width="100px;" alt="yann-combarnous"/><br /><sub><b>yann-combarnous</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Ayann-combarnous" title="Bug reports">🐛</a></td>
133
133
  <td align="center" valign="top" width="14.28%"><a href="http://adam.doussan.info"><img src="https://avatars.githubusercontent.com/u/26745150?v=4?s=100" width="100px;" alt="acdoussan"/><br /><sub><b>acdoussan</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Aacdoussan" title="Bug reports">🐛</a></td>
134
134
  <td align="center" valign="top" width="14.28%"><a href="http://matsjfunke.com"><img src="https://avatars.githubusercontent.com/u/125814808?v=4?s=100" width="100px;" alt="Mats Julius Funke"/><br /><sub><b>Mats Julius Funke</b></sub></a><br /><a href="#ideas-matsjfunke" title="Ideas, Planning, & Feedback">🤔</a></td>
135
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/popfendi"><img src="https://avatars.githubusercontent.com/u/84185235?v=4?s=100" width="100px;" alt="popfendi"/><br /><sub><b>popfendi</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Apopfendi" title="Bug reports">🐛</a></td>
136
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/luis-azevedo-visma"><img src="https://avatars.githubusercontent.com/u/149402066?v=4?s=100" width="100px;" alt="Luís Azevedo"/><br /><sub><b>Luís Azevedo</b></sub></a><br /><a href="#ideas-luis-azevedo-visma" title="Ideas, Planning, & Feedback">🤔</a></td>
137
+ <td align="center" valign="top" width="14.28%"><a href="https://florian-lefebvre.dev"><img src="https://avatars.githubusercontent.com/u/69633530?v=4?s=100" width="100px;" alt="Florian Lefebvre"/><br /><sub><b>Florian Lefebvre</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Aflorian-lefebvre" title="Bug reports">🐛</a></td>
135
138
  </tr>
136
139
  </tbody>
137
140
  <tfoot>
@@ -1,4 +1,4 @@
1
- import { SyntaxKind } from "ts-morph";
1
+ import { SyntaxKind, } from "ts-morph";
2
2
  export const AstroZodModuleSpecifier = "astro/zod";
3
3
  // https://v6.docs.astro.build/en/guides/upgrade-to/v6/#deprecated-astroschema-and-z-from-astrocontent
4
4
  export const AstroDeprecatedZodModuleSpecifiers = [
@@ -36,3 +36,47 @@ export function collectZodReferences(importDeclarations) {
36
36
  .filter((node) => node !== namedNode);
37
37
  }));
38
38
  }
39
+ /**
40
+ * Collect references to variables that are derived from Zod schemas.
41
+ *
42
+ * For example, given:
43
+ *
44
+ * const baseSchema = z.string().refine(...);
45
+ * const constrainedSchema = baseSchema.refine(...);
46
+ *
47
+ * This function will return references to `baseSchema` (like the one used
48
+ * in `constrainedSchema`), so we can also migrate those.
49
+ */
50
+ export function collectDerivedZodSchemaReferences(zodReferences) {
51
+ const derivedReferences = [];
52
+ const visited = new Set();
53
+ function collectFromReferences(refs) {
54
+ for (const ref of refs) {
55
+ // Find the variable declaration that contains this Zod reference
56
+ const variableDeclaration = ref.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
57
+ if (!variableDeclaration) {
58
+ continue;
59
+ }
60
+ const variableName = variableDeclaration.getName();
61
+ if (visited.has(variableName)) {
62
+ continue;
63
+ }
64
+ visited.add(variableName);
65
+ // Get the identifier node for the variable name
66
+ const nameNode = variableDeclaration.getNameNode();
67
+ if (!nameNode.isKind(SyntaxKind.Identifier)) {
68
+ continue;
69
+ }
70
+ // Find all references to this variable (excluding the declaration itself)
71
+ const variableReferences = nameNode
72
+ .findReferencesAsNodes()
73
+ .filter((node) => node !== nameNode)
74
+ .filter((node) => node.isKind(SyntaxKind.Identifier));
75
+ derivedReferences.push(...variableReferences);
76
+ // Recursively collect references from these derived schemas
77
+ collectFromReferences(variableReferences);
78
+ }
79
+ }
80
+ collectFromReferences(zodReferences);
81
+ return derivedReferences;
82
+ }
@@ -85,6 +85,8 @@ export function convertZCoercePatternsToTopLevelApi(node, zodName) {
85
85
  });
86
86
  }
87
87
  export function convertZObjectPatternsToTopLevelApi(node, zodName) {
88
+ // First, convert z.object() methods like .strict() or .passthrough()
89
+ convertObjectMethodOnSchemaReference(node, zodName);
88
90
  convertNameToTopLevelApi(node, {
89
91
  zodName,
90
92
  oldName: "object",
@@ -98,6 +100,56 @@ export function convertZObjectPatternsToTopLevelApi(node, zodName) {
98
100
  });
99
101
  convertZObjectMergeToExtend(node);
100
102
  }
103
+ /**
104
+ * @example
105
+ * UserSchema.pick({ id: true }).strict()
106
+ * // becomes
107
+ * z.strictObject(UserSchema.pick({ id: true }).shape)
108
+ */
109
+ function convertObjectMethodOnSchemaReference(node, zodName) {
110
+ const methodMappings = {
111
+ strict: "strictObject",
112
+ passthrough: "looseObject",
113
+ strip: "object",
114
+ nonstrict: "object",
115
+ };
116
+ const methodNames = Object.keys(methodMappings);
117
+ node
118
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
119
+ // Start from the deepest to handle nested cases
120
+ .reverse()
121
+ .filter((e) => methodNames.includes(e.getName()))
122
+ .forEach((propertyAccess) => {
123
+ const methodName = propertyAccess.getName();
124
+ const parent = propertyAccess.getFirstAncestorByKind(SyntaxKind.CallExpression);
125
+ if (!parent || parent.getExpression() !== propertyAccess) {
126
+ return;
127
+ }
128
+ const baseExpression = propertyAccess.getExpression();
129
+ const baseText = baseExpression.getText();
130
+ const hasZObjectInChain = baseExpression
131
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
132
+ .some((e) => e.getName() === "object" && e.getExpression().getText() === zodName);
133
+ if (hasZObjectInChain) {
134
+ return;
135
+ }
136
+ if (baseExpression.isKind(SyntaxKind.CallExpression) &&
137
+ baseExpression
138
+ .getExpression()
139
+ .isKind(SyntaxKind.PropertyAccessExpression)) {
140
+ const propAccess = baseExpression.getExpression();
141
+ if (propAccess.isKind(SyntaxKind.PropertyAccessExpression) &&
142
+ propAccess.getName() === "object" &&
143
+ propAccess.getExpression().getText() === zodName) {
144
+ return;
145
+ }
146
+ }
147
+ // This is a schema reference with .strict(), etc.
148
+ // Transform it to `z.strictObject(schemaRef.shape)`
149
+ const newName = methodMappings[methodName];
150
+ parent.replaceWithText(`${zodName}.${newName}(${baseText}.shape)`);
151
+ });
152
+ }
101
153
  function convertZObjectMergeToExtend(node) {
102
154
  node
103
155
  .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
@@ -123,7 +175,10 @@ export function convertZArrayPatternsToTopLevelApi(node, zodName) {
123
175
  getCallExpressionsToConvert(node, {
124
176
  oldName,
125
177
  names,
126
- }).forEach((callExpression) => {
178
+ })
179
+ // Start from the deepest, otherwise we can't get info from removed nodes
180
+ .reverse()
181
+ .forEach((callExpression) => {
127
182
  // Remove deprecated names from the chain
128
183
  callExpression
129
184
  .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
@@ -1,5 +1,41 @@
1
1
  import { SyntaxKind, } from "ts-morph";
2
2
  import { getDirectDescendantsOfKind, isZodReference, } from "./zod-node.js";
3
+ export function convertSetErrorMapToConfig(sourceFile, zodName) {
4
+ sourceFile
5
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
6
+ .filter((expression) => {
7
+ const callee = expression.getExpression();
8
+ if (!callee.isKind(SyntaxKind.PropertyAccessExpression)) {
9
+ return false;
10
+ }
11
+ const objectName = callee.getExpression().getText();
12
+ const methodName = callee.getName();
13
+ return objectName === zodName && methodName === "setErrorMap";
14
+ })
15
+ .forEach((expression) => {
16
+ const argument = expression.getArguments()[0];
17
+ if (!argument) {
18
+ return;
19
+ }
20
+ expression.replaceWithText(`${zodName}.config({ customError: ${argument.getText()} })`);
21
+ });
22
+ }
23
+ export function convertZodErrorMapType(sourceFile, zodName) {
24
+ sourceFile
25
+ .getDescendantsOfKind(SyntaxKind.TypeReference)
26
+ .filter((typeRef) => {
27
+ const typeName = typeRef.getTypeName();
28
+ if (!typeName.isKind(SyntaxKind.QualifiedName)) {
29
+ return false;
30
+ }
31
+ const left = typeName.getLeft().getText();
32
+ const right = typeName.getRight().getText();
33
+ return left === zodName && right === "ZodErrorMap";
34
+ })
35
+ .forEach((typeRef) => {
36
+ typeRef.replaceWithText(`${zodName}.core.$ZodErrorMap`);
37
+ });
38
+ }
3
39
  export function convertErrorMapToErrorFunction(node) {
4
40
  // Find all errorMap properties
5
41
  getDirectDescendantsOfKind(node, SyntaxKind.Identifier)
package/dist/index.js CHANGED
@@ -2,8 +2,10 @@
2
2
  import { cancel, confirm, intro, isCancel, log, outro, progress, text, } from "@clack/prompts";
3
3
  import { exec } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
+ import * as path from "node:path";
5
6
  import { promisify } from "node:util";
6
7
  import { Project } from "ts-morph";
8
+ import { z } from "zod";
7
9
  import { migrateZodV3ToV4 } from "./migrate.js";
8
10
  const execAsync = promisify(exec);
9
11
  intro(`🏗️ Let's migrate Zod from v3 to v4`);
@@ -56,7 +58,26 @@ if (isCancel(tsConfigFilePath)) {
56
58
  await runMigration(tsConfigFilePath);
57
59
  async function runMigration(tsConfigFilePath) {
58
60
  const project = new Project({ tsConfigFilePath });
59
- const filesToProcess = project.getSourceFiles();
61
+ let filesToProcess = project.getSourceFiles();
62
+ const references = getProjectReferences(tsConfigFilePath);
63
+ // If we can only find refs, we are probably in a monorepo setup.
64
+ // Ask the user if they want to migrate all the references.
65
+ const isMonorepoRoot = filesToProcess.length === 0 && references.length > 0;
66
+ if (isMonorepoRoot) {
67
+ const shouldFollowRefs = await confirm({
68
+ message: `Found ${references.length} project reference(s). Migrate all referenced packages?`,
69
+ });
70
+ if (isCancel(shouldFollowRefs)) {
71
+ cancel("Migration cancelled.");
72
+ process.exit(0);
73
+ }
74
+ if (shouldFollowRefs) {
75
+ for (const refPath of references) {
76
+ project.addSourceFilesFromTsConfig(refPath);
77
+ }
78
+ filesToProcess = project.getSourceFiles();
79
+ }
80
+ }
60
81
  let processedFilesCount = 0;
61
82
  const progressBar = progress({ max: filesToProcess.length });
62
83
  progressBar.start("Processing files...");
@@ -85,6 +106,46 @@ async function runMigration(tsConfigFilePath) {
85
106
 
86
107
  ℹ️ If the migration missed something or did something wrong, please report it at https://github.com/nicoespeon/zod-v3-to-v4/issues`);
87
108
  }
109
+ function getProjectReferences(tsConfigFilePath) {
110
+ // Use Zod to parse the tsconfig.json file for interesting fields.
111
+ const tsConfigWithRefs = z
112
+ .object({
113
+ references: z
114
+ .array(z.object({
115
+ path: z.string().nullable(),
116
+ }))
117
+ .default([]),
118
+ })
119
+ .passthrough();
120
+ try {
121
+ const content = fs.readFileSync(tsConfigFilePath, "utf-8");
122
+ const config = tsConfigWithRefs.parse(JSON.parse(content));
123
+ const tsConfigDir = path.dirname(tsConfigFilePath);
124
+ return config.references
125
+ .map((ref) => {
126
+ if (!ref.path)
127
+ return null;
128
+ const refDir = path.resolve(tsConfigDir, ref.path);
129
+ // Scenario: `ref.path` points directly to the tsconfig file
130
+ if (ref.path.endsWith(".json") && fs.existsSync(refDir)) {
131
+ return refDir;
132
+ }
133
+ // Otherwise, look for `tsconfig.json` in the referenced directory.
134
+ // We don't support other names yet. Workaround is to reference
135
+ // the tsconfig path or directly run the codemod with the path
136
+ // to the tsconfig you want to migrate.
137
+ const refTsConfig = path.join(refDir, "tsconfig.json");
138
+ if (fs.existsSync(refTsConfig)) {
139
+ return refTsConfig;
140
+ }
141
+ return null;
142
+ })
143
+ .filter((p) => p !== null);
144
+ }
145
+ catch (error) {
146
+ return [];
147
+ }
148
+ }
88
149
  function validateTsConfigPath(path) {
89
150
  if (!path.endsWith(".json")) {
90
151
  return {
package/dist/migrate.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { SyntaxKind } from "ts-morph";
2
2
  import { findRootExpression } from "./ast.js";
3
- import { AstroZodModuleSpecifiers, collectZodImportDeclarations, collectZodReferences, getZodName, } from "./collect-imports.js";
3
+ import { AstroZodModuleSpecifiers, collectDerivedZodSchemaReferences, collectZodImportDeclarations, collectZodReferences, getZodName, } from "./collect-imports.js";
4
4
  import { convertAstroDeprecatedZodImports } from "./convert-astro-imports.js";
5
5
  import { convertZArrayPatternsToTopLevelApi, convertZCoercePatternsToTopLevelApi, convertZFunctionPatternsToTopLevelApi, convertZNumberPatternsToZInt, convertZObjectPatternsToTopLevelApi, convertZRecordPatternsToTopLevelApi, convertZStringPatternsToTopLevelApi, } from "./convert-name-to-top-level-api.js";
6
- import { convertDeprecatedErrorKeysToErrorFunction, convertErrorMapToErrorFunction, convertMessageKeyToError, convertZodErrorAddIssueToDirectPushes, convertZodErrorToTreeifyError, } from "./convert-zod-errors.js";
6
+ import { convertDeprecatedErrorKeysToErrorFunction, convertErrorMapToErrorFunction, convertMessageKeyToError, convertSetErrorMapToConfig, convertZodErrorAddIssueToDirectPushes, convertZodErrorMapType, convertZodErrorToTreeifyError, } from "./convert-zod-errors.js";
7
7
  import { replaceDeletedTypes } from "./replace-deleted-types.js";
8
8
  import { isZodNode } from "./zod-node.js";
9
9
  export function migrateZodV3ToV4(sourceFile, options = {}) {
@@ -26,8 +26,9 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
26
26
  });
27
27
  // Collect references before modifying imports
28
28
  const zodReferences = collectZodReferences(importDeclarations);
29
+ const derivedReferences = collectDerivedZodSchemaReferences(zodReferences);
29
30
  replaceDeletedTypes(importDeclarations, zodReferences);
30
- zodReferences.forEach((node) => {
31
+ [...zodReferences, ...derivedReferences].forEach((node) => {
31
32
  if (node.wasForgotten()) {
32
33
  return;
33
34
  }
@@ -55,6 +56,8 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
55
56
  });
56
57
  convertZodErrorToTreeifyError(sourceFile, zodName);
57
58
  convertZodErrorAddIssueToDirectPushes(sourceFile, zodName);
59
+ convertSetErrorMapToConfig(sourceFile, zodName);
60
+ convertZodErrorMapType(sourceFile, zodName);
58
61
  renameZSchemaEnumToLowercase(sourceFile, zodName);
59
62
  convertAstroDeprecatedZodImports(importDeclarations);
60
63
  return sourceFile.getFullText();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-v3-to-v4",
3
- "version": "1.10.0",
3
+ "version": "1.12.1",
4
4
  "description": "Migrate Zod from v3 to v4",
5
5
  "keywords": [
6
6
  "zod",
@@ -33,6 +33,7 @@
33
33
  "format-check": "prettier . --list-different",
34
34
  "playground": "pnpm playground:interactive playground/tsconfig.json",
35
35
  "playground:interactive": "node --experimental-strip-types --disable-warning=ExperimentalWarning src/index.ts",
36
+ "playground:monorepo": "pnpm playground:interactive playground-monorepo/tsconfig.json",
36
37
  "prepare": "husky",
37
38
  "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
38
39
  "test": "vitest run",
@@ -43,22 +44,22 @@
43
44
  "*": "prettier --ignore-unknown --write"
44
45
  },
45
46
  "dependencies": {
46
- "@clack/prompts": "1.0.0-alpha.1",
47
- "ts-morph": "26.0.0"
47
+ "@clack/prompts": "1.0.0-alpha.9",
48
+ "ts-morph": "26.0.0",
49
+ "zod": "3.25.12"
48
50
  },
49
51
  "devDependencies": {
50
- "@types/node": "20.9.0",
52
+ "@types/node": "25.0.3",
51
53
  "husky": "9.1.7",
52
- "lint-staged": "16.0.0",
54
+ "lint-staged": "16.2.7",
53
55
  "prettier": "3.5.3",
54
56
  "prettier-plugin-curly": "0.3.1",
55
57
  "prettier-plugin-organize-imports": "4.3.0",
56
58
  "prettier-plugin-packagejson": "2.5.10",
57
- "prettier-plugin-sh": "0.15.0",
59
+ "prettier-plugin-sh": "0.17.4",
58
60
  "rimraf": "6.0.1",
59
- "typescript": "5.8.3",
60
- "vitest": "3.1.4",
61
- "zod": "3.25.12"
61
+ "typescript": "5.9.3",
62
+ "vitest": "3.1.4"
62
63
  },
63
64
  "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
64
65
  }