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 +20 -11
- package/dist/convert-name-to-top-level-api.js +44 -0
- package/dist/convert-zod-errors.js +8 -1
- package/dist/migrate.js +4 -3
- package/dist/zod-node.js +19 -8
- package/package.json +14 -17
package/dist/ast.js
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
import { SyntaxKind, ts, } from "ts-morph";
|
|
2
|
-
export function
|
|
3
|
-
const identifier =
|
|
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
|
|
9
|
+
return findRootNode(initializer);
|
|
10
10
|
}
|
|
11
|
-
return
|
|
11
|
+
return { type: "not found" };
|
|
12
12
|
}
|
|
13
13
|
if (mainDeclaration?.isKind(SyntaxKind.Parameter)) {
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
const identifier =
|
|
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
|
|
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
|
|
45
|
+
function findFirstIdentifier(node) {
|
|
37
46
|
const nestedExpression = node?.getExpression();
|
|
38
47
|
if (nestedExpression?.isKind(SyntaxKind.PropertyAccessExpression)) {
|
|
39
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|