zod-v3-to-v4 1.11.0 → 1.13.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/README.md +3 -0
- package/dist/collect-imports.js +35 -15
- package/dist/convert-name-to-top-level-api.js +52 -0
- package/dist/convert-zod-errors.js +36 -0
- package/dist/index.js +1 -1
- package/dist/migrate.js +5 -3
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -133,6 +133,9 @@ pnpm playground:interactive
|
|
|
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
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>
|
|
138
|
+
<td align="center" valign="top" width="14.28%"><a href="http://www.robertcooper.me"><img src="https://avatars.githubusercontent.com/u/16786990?v=4?s=100" width="100px;" alt="Robert Cooper"/><br /><sub><b>Robert Cooper</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Arobertcoopercode" title="Bug reports">🐛</a></td>
|
|
136
139
|
</tr>
|
|
137
140
|
</tbody>
|
|
138
141
|
<tfoot>
|
package/dist/collect-imports.js
CHANGED
|
@@ -25,16 +25,38 @@ export function getZodImport(importDeclaration) {
|
|
|
25
25
|
?.getNamedImports()
|
|
26
26
|
.find((namedImport) => namedImport.getName() === "z");
|
|
27
27
|
}
|
|
28
|
-
export function collectZodReferences(importDeclarations) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
export function collectZodReferences(sourceFile, importDeclarations) {
|
|
29
|
+
const identifierMap = buildIdentifierMap(sourceFile);
|
|
30
|
+
return {
|
|
31
|
+
zodReferences: importDeclarations.flatMap((importDeclaration) => importDeclaration.getNamedImports().flatMap((namedImport) => {
|
|
32
|
+
const namedNode = namedImport.getAliasNode() ?? namedImport.getNameNode();
|
|
33
|
+
if (!namedNode.isKind(SyntaxKind.Identifier)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const name = namedNode.getText();
|
|
37
|
+
const allReferences = identifierMap.get(name) ?? [];
|
|
38
|
+
return allReferences.filter((node) => node !== namedNode);
|
|
39
|
+
})),
|
|
40
|
+
identifierMap,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a map of identifier name -> all nodes with that name.
|
|
45
|
+
* This allows O(1) lookups instead of expensive findReferencesAsNodes() calls.
|
|
46
|
+
*/
|
|
47
|
+
function buildIdentifierMap(sourceFile) {
|
|
48
|
+
const identifierMap = new Map();
|
|
49
|
+
for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
50
|
+
const name = identifier.getText();
|
|
51
|
+
const existing = identifierMap.get(name);
|
|
52
|
+
if (existing) {
|
|
53
|
+
existing.push(identifier);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
identifierMap.set(name, [identifier]);
|
|
33
57
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.filter((node) => node !== namedNode);
|
|
37
|
-
}));
|
|
58
|
+
}
|
|
59
|
+
return identifierMap;
|
|
38
60
|
}
|
|
39
61
|
/**
|
|
40
62
|
* Collect references to variables that are derived from Zod schemas.
|
|
@@ -47,7 +69,7 @@ export function collectZodReferences(importDeclarations) {
|
|
|
47
69
|
* This function will return references to `baseSchema` (like the one used
|
|
48
70
|
* in `constrainedSchema`), so we can also migrate those.
|
|
49
71
|
*/
|
|
50
|
-
export function collectDerivedZodSchemaReferences(zodReferences) {
|
|
72
|
+
export function collectDerivedZodSchemaReferences(zodReferences, identifierMap) {
|
|
51
73
|
const derivedReferences = [];
|
|
52
74
|
const visited = new Set();
|
|
53
75
|
function collectFromReferences(refs) {
|
|
@@ -67,11 +89,9 @@ export function collectDerivedZodSchemaReferences(zodReferences) {
|
|
|
67
89
|
if (!nameNode.isKind(SyntaxKind.Identifier)) {
|
|
68
90
|
continue;
|
|
69
91
|
}
|
|
70
|
-
// Find all references to this variable
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
.filter((node) => node !== nameNode)
|
|
74
|
-
.filter((node) => node.isKind(SyntaxKind.Identifier));
|
|
92
|
+
// Find all references to this variable
|
|
93
|
+
const allReferences = identifierMap.get(variableName) ?? [];
|
|
94
|
+
const variableReferences = allReferences.filter((node) => node !== nameNode);
|
|
75
95
|
derivedReferences.push(...variableReferences);
|
|
76
96
|
// Recursively collect references from these derived schemas
|
|
77
97
|
collectFromReferences(variableReferences);
|
|
@@ -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)
|
|
@@ -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
|
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { Project } from "ts-morph";
|
|
8
|
-
import z from "zod";
|
|
8
|
+
import { z } from "zod";
|
|
9
9
|
import { migrateZodV3ToV4 } from "./migrate.js";
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
intro(`🏗️ Let's migrate Zod from v3 to v4`);
|
package/dist/migrate.js
CHANGED
|
@@ -3,7 +3,7 @@ import { findRootExpression } from "./ast.js";
|
|
|
3
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 = {}) {
|
|
@@ -25,8 +25,8 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
|
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
// Collect references before modifying imports
|
|
28
|
-
const zodReferences = collectZodReferences(importDeclarations);
|
|
29
|
-
const derivedReferences = collectDerivedZodSchemaReferences(zodReferences);
|
|
28
|
+
const { zodReferences, identifierMap } = collectZodReferences(sourceFile, importDeclarations);
|
|
29
|
+
const derivedReferences = collectDerivedZodSchemaReferences(zodReferences, identifierMap);
|
|
30
30
|
replaceDeletedTypes(importDeclarations, zodReferences);
|
|
31
31
|
[...zodReferences, ...derivedReferences].forEach((node) => {
|
|
32
32
|
if (node.wasForgotten()) {
|
|
@@ -56,6 +56,8 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
|
|
|
56
56
|
});
|
|
57
57
|
convertZodErrorToTreeifyError(sourceFile, zodName);
|
|
58
58
|
convertZodErrorAddIssueToDirectPushes(sourceFile, zodName);
|
|
59
|
+
convertSetErrorMapToConfig(sourceFile, zodName);
|
|
60
|
+
convertZodErrorMapType(sourceFile, zodName);
|
|
59
61
|
renameZSchemaEnumToLowercase(sourceFile, zodName);
|
|
60
62
|
convertAstroDeprecatedZodImports(importDeclarations);
|
|
61
63
|
return sourceFile.getFullText();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zod-v3-to-v4",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Migrate Zod from v3 to v4",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zod",
|
|
@@ -44,22 +44,22 @@
|
|
|
44
44
|
"*": "prettier --ignore-unknown --write"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@clack/prompts": "1.0.0-alpha.
|
|
48
|
-
"ts-morph": "26.0.0"
|
|
47
|
+
"@clack/prompts": "1.0.0-alpha.9",
|
|
48
|
+
"ts-morph": "26.0.0",
|
|
49
|
+
"zod": "3.25.12"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@types/node": "
|
|
52
|
+
"@types/node": "25.0.3",
|
|
52
53
|
"husky": "9.1.7",
|
|
53
|
-
"lint-staged": "16.
|
|
54
|
+
"lint-staged": "16.2.7",
|
|
54
55
|
"prettier": "3.5.3",
|
|
55
56
|
"prettier-plugin-curly": "0.3.1",
|
|
56
57
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
57
58
|
"prettier-plugin-packagejson": "2.5.10",
|
|
58
|
-
"prettier-plugin-sh": "0.
|
|
59
|
+
"prettier-plugin-sh": "0.17.4",
|
|
59
60
|
"rimraf": "6.0.1",
|
|
60
|
-
"typescript": "5.
|
|
61
|
-
"vitest": "3.1.4"
|
|
62
|
-
"zod": "3.25.12"
|
|
61
|
+
"typescript": "5.9.3",
|
|
62
|
+
"vitest": "3.1.4"
|
|
63
63
|
},
|
|
64
64
|
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
|
65
65
|
}
|