zod-v3-to-v4 1.4.0 → 1.5.0

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/dist/ast.js CHANGED
@@ -1,30 +1,39 @@
1
1
  import { SyntaxKind, ts, } from "ts-morph";
2
- export function findRootQualifiedName(node) {
3
- const identifier = findRootIdentifier(node);
2
+ export function findRootNode(node) {
3
+ const identifier = findFirstIdentifier(node);
4
4
  const [mainDeclaration] = identifier?.getSymbol()?.getDeclarations() ?? [];
5
5
  if (mainDeclaration?.isKind(SyntaxKind.VariableDeclaration)) {
6
6
  const initializer = mainDeclaration.getInitializer();
7
7
  if (initializer?.isKind(SyntaxKind.CallExpression) ||
8
8
  initializer?.isKind(SyntaxKind.PropertyAccessExpression)) {
9
- return findRootQualifiedName(initializer);
9
+ return findRootNode(initializer);
10
10
  }
11
- return undefined;
11
+ return { type: "not found" };
12
12
  }
13
13
  if (mainDeclaration?.isKind(SyntaxKind.Parameter)) {
14
- return mainDeclaration
14
+ const qualifiedName = mainDeclaration
15
15
  ?.getFirstChildByKind(ts.SyntaxKind.TypeReference)
16
16
  ?.getFirstChildByKind(ts.SyntaxKind.QualifiedName);
17
+ return qualifiedName
18
+ ? { type: "qualified name", value: qualifiedName }
19
+ : { type: "not found" };
17
20
  }
18
- return undefined;
21
+ if (mainDeclaration?.isKind(SyntaxKind.ImportSpecifier)) {
22
+ const importDeclaration = mainDeclaration.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
23
+ return importDeclaration
24
+ ? { type: "import declaration", value: importDeclaration }
25
+ : { type: "not found" };
26
+ }
27
+ return { type: "not found" };
19
28
  }
20
- export function findRootNode(node) {
21
- const identifier = findRootIdentifier(node);
29
+ export function findRootExpression(node) {
30
+ const identifier = findFirstIdentifier(node);
22
31
  const [mainDeclaration] = identifier?.getSymbol()?.getDeclarations() ?? [];
23
32
  if (mainDeclaration?.isKind(SyntaxKind.VariableDeclaration)) {
24
33
  const initializer = mainDeclaration.getInitializer();
25
34
  if (initializer?.isKind(SyntaxKind.CallExpression) ||
26
35
  initializer?.isKind(SyntaxKind.PropertyAccessExpression)) {
27
- return findRootNode(initializer);
36
+ return findRootExpression(initializer);
28
37
  }
29
38
  return undefined;
30
39
  }
@@ -33,10 +42,10 @@ export function findRootNode(node) {
33
42
  }
34
43
  return undefined;
35
44
  }
36
- function findRootIdentifier(node) {
45
+ function findFirstIdentifier(node) {
37
46
  const nestedExpression = node?.getExpression();
38
47
  if (nestedExpression?.isKind(SyntaxKind.PropertyAccessExpression)) {
39
- return findRootIdentifier(nestedExpression);
48
+ return findFirstIdentifier(nestedExpression);
40
49
  }
41
50
  if (nestedExpression?.isKind(SyntaxKind.Identifier)) {
42
51
  return nestedExpression;
@@ -40,6 +40,50 @@ export function convertZStringPatternsToTopLevelApi(node, zodName) {
40
40
  renames: [{ name: "cidrv4" }, { name: "cidrv6" }],
41
41
  });
42
42
  }
43
+ export function convertZCoercePatternsToTopLevelApi(node, zodName) {
44
+ const names = ["bigint", "boolean", "date", "number", "string"];
45
+ node
46
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
47
+ // Start from the deepest, otherwise we can't get info from removed nodes
48
+ .reverse()
49
+ .filter((e) => {
50
+ const expression = e.getExpression();
51
+ if (!expression.isKind(SyntaxKind.PropertyAccessExpression)) {
52
+ return false;
53
+ }
54
+ const [firstArgument, ...otherArgs] = e.getArguments();
55
+ if (otherArgs.length > 0 ||
56
+ !firstArgument?.isKind(SyntaxKind.ObjectLiteralExpression)) {
57
+ return false;
58
+ }
59
+ const propertiesNames = firstArgument
60
+ .getProperties()
61
+ .map((p) => ("getName" in p ? p.getName() : ""));
62
+ return (names.includes(expression.getName()) &&
63
+ propertiesNames.includes("coerce"));
64
+ })
65
+ .forEach((e) => {
66
+ const expression = e.getExpression();
67
+ if (!expression.isKind(SyntaxKind.PropertyAccessExpression)) {
68
+ return;
69
+ }
70
+ const [firstArgument, ...otherArgs] = e.getArguments();
71
+ if (otherArgs.length > 0 ||
72
+ !firstArgument?.isKind(SyntaxKind.ObjectLiteralExpression)) {
73
+ return;
74
+ }
75
+ // Delete `coerce` property
76
+ firstArgument
77
+ .getProperties()
78
+ .filter((p) => "getName" in p && p.getName() === "coerce")
79
+ .forEach((p) => p.remove());
80
+ // If only `{}` is left, remove it
81
+ if (firstArgument.getProperties().length === 0) {
82
+ e.removeArgument(firstArgument);
83
+ }
84
+ expression.replaceWithText(`${zodName}.coerce.${expression.getName()}`);
85
+ });
86
+ }
43
87
  export function convertZObjectPatternsToTopLevelApi(node, zodName) {
44
88
  convertNameToTopLevelApi(node, {
45
89
  zodName,
@@ -141,13 +141,20 @@ export function convertZodErrorToTreeifyError(sourceFile, zodName) {
141
141
  .forEach((expression) => {
142
142
  const caller = expression.getFirstChild()?.getFirstChild()?.getText() ?? "";
143
143
  expression.replaceWithText(`${zodName}.treeifyError(${caller})`);
144
+ // Get rid of `.formErrors` or `.fieldErrors` on `z.treeifyError()`
145
+ const parentObject = expression.getParentIfKind(SyntaxKind.PropertyAccessExpression);
146
+ if (parentObject &&
147
+ ["formErrors", "fieldErrors"].includes(parentObject.getName())) {
148
+ parentObject.replaceWithText(expression.getText());
149
+ }
144
150
  });
145
151
  sourceFile
146
152
  .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
147
153
  .filter((expression) => isZodReference(zodName, ["ZodJSONSchema", "ZodError"], expression))
148
154
  .filter((expression) => {
149
155
  const looksLikeZodErrorFormErrors = expression.getName() === "formErrors";
150
- return looksLikeZodErrorFormErrors;
156
+ const looksLikeZodErrorFieldErrors = expression.getName() === "fieldErrors";
157
+ return looksLikeZodErrorFormErrors || looksLikeZodErrorFieldErrors;
151
158
  })
152
159
  .forEach((expression) => {
153
160
  const caller = expression.getFirstChild()?.getText() ?? "";
package/dist/migrate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SyntaxKind } from "ts-morph";
2
- import { findRootNode } from "./ast.js";
2
+ import { findRootExpression } from "./ast.js";
3
3
  import { collectZodImportDeclarations, collectZodReferences, getZodName, } from "./collect-imports.js";
4
- import { convertZArrayPatternsToTopLevelApi, convertZFunctionPatternsToTopLevelApi, convertZNumberPatternsToZInt, convertZObjectPatternsToTopLevelApi, convertZRecordPatternsToTopLevelApi, convertZStringPatternsToTopLevelApi, } from "./convert-name-to-top-level-api.js";
4
+ import { convertZArrayPatternsToTopLevelApi, convertZCoercePatternsToTopLevelApi, convertZFunctionPatternsToTopLevelApi, convertZNumberPatternsToZInt, convertZObjectPatternsToTopLevelApi, convertZRecordPatternsToTopLevelApi, convertZStringPatternsToTopLevelApi, } from "./convert-name-to-top-level-api.js";
5
5
  import { convertDeprecatedErrorKeysToErrorFunction, convertErrorMapToErrorFunction, convertMessageKeyToError, convertZodErrorAddIssueToDirectPushes, convertZodErrorToTreeifyError, } from "./convert-zod-errors.js";
6
6
  import { isZodNode } from "./zod-node.js";
7
7
  export function migrateZodV3ToV4(sourceFile, options = {}) {
@@ -31,6 +31,7 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
31
31
  convertDeprecatedErrorKeysToErrorFunction(parentStatement);
32
32
  convertZNumberPatternsToZInt(parentStatement, zodName);
33
33
  convertZStringPatternsToTopLevelApi(parentStatement, zodName);
34
+ convertZCoercePatternsToTopLevelApi(parentStatement, zodName);
34
35
  convertZObjectPatternsToTopLevelApi(parentStatement, zodName);
35
36
  convertZArrayPatternsToTopLevelApi(parentStatement, zodName);
36
37
  convertZFunctionPatternsToTopLevelApi(parentStatement);
@@ -67,7 +68,7 @@ function renameZSchemaEnumToLowercase(sourceFile, zodName) {
67
68
  .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
68
69
  .filter((node) => node.getName() === "Enum")
69
70
  .filter((node) => {
70
- const rootNode = findRootNode(node);
71
+ const rootNode = findRootExpression(node);
71
72
  const expression = rootNode?.getExpression();
72
73
  const isFromZodEnum = expression?.isKind(SyntaxKind.PropertyAccessExpression) &&
73
74
  expression.getName() === "enum" &&
package/dist/zod-node.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SyntaxKind, } from "ts-morph";
2
- import { findRootQualifiedName } from "./ast.js";
2
+ import { findRootNode } from "./ast.js";
3
3
  export function isZodNode(node) {
4
4
  return (node.isKind(SyntaxKind.CallExpression) ||
5
5
  node.isKind(SyntaxKind.PropertyAccessExpression) ||
@@ -31,11 +31,22 @@ export function getDirectDescendantsOfKind(node, kind) {
31
31
  * // => true if expression is `z.ZodJSONSchema` or `z.ZodError`
32
32
  */
33
33
  export function isZodReference(zodName, expectedTypes, expression) {
34
- const qualifiedName = findRootQualifiedName(expression);
35
- const left = qualifiedName?.getLeft();
36
- const belongsToZod = left?.isKind(SyntaxKind.Identifier) && left.getText() === zodName;
37
- const right = qualifiedName?.getRight();
38
- const isExpectedType = right?.isKind(SyntaxKind.Identifier) &&
39
- expectedTypes.includes(right.getText());
40
- return belongsToZod && isExpectedType;
34
+ const result = findRootNode(expression);
35
+ if (result.type === "qualified name") {
36
+ const left = result.value.getLeft();
37
+ const belongsToZod = left?.isKind(SyntaxKind.Identifier) && left.getText() === zodName;
38
+ const right = result.value.getRight();
39
+ const isExpectedType = right?.isKind(SyntaxKind.Identifier) &&
40
+ expectedTypes.includes(right.getText());
41
+ return belongsToZod && isExpectedType;
42
+ }
43
+ if (result.type === "import declaration") {
44
+ const hasZodImport = result.value
45
+ .getImportClause()
46
+ ?.getNamedImports()
47
+ ?.some((i) => i.getName() === zodName);
48
+ const isFromZod = ["zod", "zod/v4"].includes(result.value.getModuleSpecifierValue());
49
+ return hasZodImport && isFromZod;
50
+ }
51
+ return false;
41
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-v3-to-v4",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Migrate Zod from v3 to v4",
5
5
  "keywords": [
6
6
  "zod",
@@ -25,20 +25,6 @@
25
25
  "README.md",
26
26
  "dist/*.js"
27
27
  ],
28
- "scripts": {
29
- "prebuild": "rimraf dist",
30
- "build": "tsc --project tsconfig.build.json",
31
- "postbuild": "chmod +x dist/index.js",
32
- "format": "prettier . --write",
33
- "format-check": "prettier . --list-different",
34
- "playground": "pnpm playground:interactive playground/tsconfig.json",
35
- "playground:interactive": "node --experimental-strip-types --disable-warning=ExperimentalWarning src/index.ts",
36
- "prepare": "husky",
37
- "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
38
- "test": "vitest run",
39
- "test:watch": "vitest watch",
40
- "typecheck": "tsc --noEmit"
41
- },
42
28
  "lint-staged": {
43
29
  "*": "prettier --ignore-unknown --write"
44
30
  },
@@ -59,5 +45,16 @@
59
45
  "vitest": "3.1.4",
60
46
  "zod": "3.25.12"
61
47
  },
62
- "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
63
- }
48
+ "scripts": {
49
+ "prebuild": "rimraf dist",
50
+ "build": "tsc --project tsconfig.build.json",
51
+ "postbuild": "chmod +x dist/index.js",
52
+ "format": "prettier . --write",
53
+ "format-check": "prettier . --list-different",
54
+ "playground": "pnpm playground:interactive playground/tsconfig.json",
55
+ "playground:interactive": "node --experimental-strip-types --disable-warning=ExperimentalWarning src/index.ts",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest watch",
58
+ "typecheck": "tsc --noEmit"
59
+ }
60
+ }