@schemashift/zod-v3-v4 0.3.0 → 0.8.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 +98 -0
- package/dist/index.cjs +463 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +463 -13
- package/dist/index.js.map +1 -1
- package/package.json +13 -2
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @schemashift/zod-v3-v4
|
|
2
|
+
|
|
3
|
+
Zod v3 to v4 upgrade transformer for SchemaShift. Handles 17+ breaking changes between Zod v3 and v4 with auto-transforms and runtime behavior warnings.
|
|
4
|
+
|
|
5
|
+
**Tier:** Individual
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @schemashift/zod-v3-v4
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createZodV3ToV4Handler } from '@schemashift/zod-v3-v4';
|
|
17
|
+
import { TransformEngine } from '@schemashift/core';
|
|
18
|
+
|
|
19
|
+
const engine = new TransformEngine();
|
|
20
|
+
engine.registerHandler('zod-v3', 'v4', createZodV3ToV4Handler());
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Breaking Changes Handled
|
|
24
|
+
|
|
25
|
+
### Auto-Transforms
|
|
26
|
+
|
|
27
|
+
These changes are automatically applied to your code:
|
|
28
|
+
|
|
29
|
+
| v3 Pattern | v4 Pattern | Notes |
|
|
30
|
+
|-----------|-----------|-------|
|
|
31
|
+
| `z.record(valueSchema)` | `z.record(z.string(), valueSchema)` | Explicit key type required |
|
|
32
|
+
| `schemaA.merge(schemaB)` | `schemaA.extend(schemaB.shape)` | `.merge()` deprecated |
|
|
33
|
+
| `z.nativeEnum(MyEnum)` | `z.enum(MyEnum)` | `nativeEnum` renamed |
|
|
34
|
+
| `.superRefine(fn)` | `.check(fn)` | Callback signature identical |
|
|
35
|
+
| `error.errors` | `error.issues` | Property renamed |
|
|
36
|
+
| `{ invalid_type_error: 'msg' }` | `{ error: 'msg' }` | Unified error param |
|
|
37
|
+
| `{ required_error: 'msg' }` | `{ error: 'msg' }` | Unified error param |
|
|
38
|
+
| Both error params | `{ error: (issue) => ... }` | Function-based error |
|
|
39
|
+
|
|
40
|
+
### Deprecation Warnings
|
|
41
|
+
|
|
42
|
+
These methods still work but are deprecated in v4:
|
|
43
|
+
|
|
44
|
+
| Method | Replacement | Warning |
|
|
45
|
+
|--------|-------------|---------|
|
|
46
|
+
| `.superRefine()` | `.check()` | Auto-renamed, warning emitted |
|
|
47
|
+
| `.strict()` | `z.strictObject()` | Structural change, review needed |
|
|
48
|
+
| `.passthrough()` | `z.looseObject()` | Structural change, review needed |
|
|
49
|
+
|
|
50
|
+
### Runtime Behavior Warnings
|
|
51
|
+
|
|
52
|
+
These changes do not cause compile-time errors but silently change runtime behavior:
|
|
53
|
+
|
|
54
|
+
| Pattern | Issue |
|
|
55
|
+
|---------|-------|
|
|
56
|
+
| `instanceof Error` on ZodError | `ZodError` no longer extends `Error` in v4 |
|
|
57
|
+
| `.refine()` followed by `.transform()` | `.transform()` now runs even when `.refine()` fails |
|
|
58
|
+
| `.default()` + `.optional()` | `.default()` always provides a value, making `.optional()` a no-op |
|
|
59
|
+
| `.pipe()` | Stricter type checking, may need explicit type annotations |
|
|
60
|
+
|
|
61
|
+
### Other Detected Changes
|
|
62
|
+
|
|
63
|
+
| Pattern | Warning |
|
|
64
|
+
|---------|---------|
|
|
65
|
+
| `.flatten()` | Removed in v4, use `.format()` instead |
|
|
66
|
+
| `z.function()` | Input/output parameter changes |
|
|
67
|
+
| `.uuid()` | Stricter RFC 4122 validation |
|
|
68
|
+
| `z.discriminatedUnion()` | Stricter discriminator requirements |
|
|
69
|
+
|
|
70
|
+
## Mappings Reference
|
|
71
|
+
|
|
72
|
+
### Behavior Changes Set
|
|
73
|
+
|
|
74
|
+
Methods with changed behavior in v4: `default`, `uuid`, `record`, `discriminatedUnion`, `function`, `pipe`.
|
|
75
|
+
|
|
76
|
+
### Deprecated Methods Set
|
|
77
|
+
|
|
78
|
+
Methods deprecated in v4: `merge`, `superRefine`, `strict`, `passthrough`.
|
|
79
|
+
|
|
80
|
+
### Auto-Transformable Methods
|
|
81
|
+
|
|
82
|
+
Methods that are automatically renamed: `superRefine` → `.check()`.
|
|
83
|
+
|
|
84
|
+
### Error Property Renames
|
|
85
|
+
|
|
86
|
+
| v3 | v4 |
|
|
87
|
+
|----|----|
|
|
88
|
+
| `.errors` | `.issues` |
|
|
89
|
+
| `.formErrors` | `.formErrors` (unchanged) |
|
|
90
|
+
| `.fieldErrors` | `.fieldErrors` (unchanged) |
|
|
91
|
+
|
|
92
|
+
### Error Parameter Changes
|
|
93
|
+
|
|
94
|
+
v3 accepted `invalid_type_error` and `required_error` as separate properties. v4 unifies these under a single `error` property (string or function).
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -48,8 +48,37 @@ var BEHAVIOR_CHANGES = /* @__PURE__ */ new Set([
|
|
|
48
48
|
// Requires key type
|
|
49
49
|
"discriminatedUnion",
|
|
50
50
|
// Stricter discriminator requirements
|
|
51
|
-
"function"
|
|
51
|
+
"function",
|
|
52
52
|
// Input/output parameter changes
|
|
53
|
+
"pipe",
|
|
54
|
+
// Stricter type checking in v4
|
|
55
|
+
"catch"
|
|
56
|
+
// .catch() on optional properties always returns catch value in v4
|
|
57
|
+
]);
|
|
58
|
+
var DEPRECATED_METHODS = /* @__PURE__ */ new Set([
|
|
59
|
+
"merge",
|
|
60
|
+
// Use .extend() instead
|
|
61
|
+
"superRefine",
|
|
62
|
+
// Use .check() instead
|
|
63
|
+
"strict",
|
|
64
|
+
// Use z.strictObject() instead
|
|
65
|
+
"passthrough"
|
|
66
|
+
// Use z.looseObject() instead
|
|
67
|
+
]);
|
|
68
|
+
var AUTO_TRANSFORMABLE = /* @__PURE__ */ new Set([
|
|
69
|
+
"superRefine",
|
|
70
|
+
// -> .check()
|
|
71
|
+
"flatten",
|
|
72
|
+
// error.flatten() -> z.flattenError(error)
|
|
73
|
+
"format"
|
|
74
|
+
// error.format() -> z.treeifyError(error)
|
|
75
|
+
]);
|
|
76
|
+
var UTILITY_TYPES = /* @__PURE__ */ new Set([
|
|
77
|
+
"ZodType",
|
|
78
|
+
"ZodSchema",
|
|
79
|
+
"ZodRawShape",
|
|
80
|
+
"ZodTypeAny",
|
|
81
|
+
"ZodFirstPartyTypeKind"
|
|
53
82
|
]);
|
|
54
83
|
|
|
55
84
|
// src/transformer.ts
|
|
@@ -63,8 +92,21 @@ var ZodV3ToV4Transformer = class {
|
|
|
63
92
|
const originalCode = sourceFile.getFullText();
|
|
64
93
|
try {
|
|
65
94
|
this.transformRecordCalls(sourceFile);
|
|
95
|
+
this.transformMerge(sourceFile);
|
|
96
|
+
this.transformNativeEnum(sourceFile);
|
|
97
|
+
this.transformSuperRefine(sourceFile);
|
|
66
98
|
this.transformErrorProperties(sourceFile);
|
|
99
|
+
this.transformErrorParams(sourceFile);
|
|
100
|
+
this.transformFlatten(sourceFile);
|
|
101
|
+
this.transformFormat(sourceFile);
|
|
102
|
+
this.transformDefAccess(sourceFile);
|
|
103
|
+
this.checkDeprecatedMethods(sourceFile);
|
|
67
104
|
this.checkBehaviorChanges(sourceFile);
|
|
105
|
+
this.checkInstanceofError(sourceFile);
|
|
106
|
+
this.checkTransformRefineOrder(sourceFile);
|
|
107
|
+
this.checkDefaultOptional(sourceFile);
|
|
108
|
+
this.checkCatchOptional(sourceFile);
|
|
109
|
+
this.checkUtilityTypeImports(sourceFile);
|
|
68
110
|
return {
|
|
69
111
|
success: this.errors.length === 0,
|
|
70
112
|
filePath,
|
|
@@ -113,21 +155,394 @@ var ZodV3ToV4Transformer = class {
|
|
|
113
155
|
});
|
|
114
156
|
}
|
|
115
157
|
/**
|
|
116
|
-
*
|
|
158
|
+
* Build a set of variable names that are likely ZodError instances.
|
|
159
|
+
* Used by transformErrorProperties, transformFlatten, and transformFormat.
|
|
160
|
+
*/
|
|
161
|
+
buildZodErrorVarSet(sourceFile) {
|
|
162
|
+
const zodErrorVars = /* @__PURE__ */ new Set();
|
|
163
|
+
sourceFile.forEachDescendant((node) => {
|
|
164
|
+
if (import_ts_morph.Node.isVariableDeclaration(node)) {
|
|
165
|
+
const typeNode = node.getTypeNode();
|
|
166
|
+
if (typeNode?.getText().includes("ZodError")) {
|
|
167
|
+
zodErrorVars.add(node.getName());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (import_ts_morph.Node.isCatchClause(node)) {
|
|
171
|
+
const param = node.getVariableDeclaration();
|
|
172
|
+
if (param) {
|
|
173
|
+
const block = node.getBlock();
|
|
174
|
+
const blockText = block.getText();
|
|
175
|
+
if (blockText.includes("instanceof ZodError") || blockText.includes("ZodError")) {
|
|
176
|
+
zodErrorVars.add(param.getName());
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (import_ts_morph.Node.isVariableDeclaration(node)) {
|
|
181
|
+
const initializer = node.getInitializer();
|
|
182
|
+
if (initializer?.getText().includes(".safeParse(")) {
|
|
183
|
+
zodErrorVars.add(`${node.getName()}.error`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (import_ts_morph.Node.isNewExpression(node)) {
|
|
187
|
+
if (node.getExpression().getText() === "ZodError") {
|
|
188
|
+
const parent = node.getParent();
|
|
189
|
+
if (parent && import_ts_morph.Node.isVariableDeclaration(parent)) {
|
|
190
|
+
zodErrorVars.add(parent.getName());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return zodErrorVars;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Transform error.errors to error.issues using AST-based detection
|
|
199
|
+
* instead of heuristic name matching.
|
|
117
200
|
*/
|
|
118
201
|
transformErrorProperties(sourceFile) {
|
|
202
|
+
const filePath = sourceFile.getFilePath();
|
|
203
|
+
const zodErrorVars = this.buildZodErrorVarSet(sourceFile);
|
|
119
204
|
sourceFile.forEachDescendant((node) => {
|
|
120
205
|
if (import_ts_morph.Node.isPropertyAccessExpression(node)) {
|
|
121
206
|
const name = node.getName();
|
|
122
207
|
if (name === "errors") {
|
|
123
208
|
const object = node.getExpression();
|
|
124
209
|
const objectText = object.getText();
|
|
125
|
-
|
|
210
|
+
const isZodError = zodErrorVars.has(objectText) || // Also check if accessing .error.errors (safeParse pattern)
|
|
211
|
+
objectText.endsWith(".error") && zodErrorVars.has(`${objectText.replace(/\.error$/, "")}.error`);
|
|
212
|
+
if (isZodError) {
|
|
126
213
|
const fullText = node.getText();
|
|
127
214
|
const newText = fullText.replace(/\.errors$/, ".issues");
|
|
128
215
|
node.replaceWithText(newText);
|
|
129
216
|
this.warnings.push(
|
|
130
|
-
`${
|
|
217
|
+
`${filePath}:${node.getStartLineNumber()}: .errors renamed to .issues (ZodError property change)`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Auto-transform error.flatten() to z.flattenError(error) in v4.
|
|
226
|
+
*/
|
|
227
|
+
transformFlatten(sourceFile) {
|
|
228
|
+
const filePath = sourceFile.getFilePath();
|
|
229
|
+
const zodErrorVars = this.buildZodErrorVarSet(sourceFile);
|
|
230
|
+
const nodesToTransform = [];
|
|
231
|
+
sourceFile.forEachDescendant((node) => {
|
|
232
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
233
|
+
const expression = node.getExpression();
|
|
234
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "flatten") {
|
|
235
|
+
const objectText = expression.getExpression().getText();
|
|
236
|
+
const isZodError = zodErrorVars.has(objectText) || objectText.endsWith(".error") && zodErrorVars.has(`${objectText.replace(/\.error$/, "")}.error`);
|
|
237
|
+
if (isZodError) {
|
|
238
|
+
nodesToTransform.push({
|
|
239
|
+
node,
|
|
240
|
+
objectText,
|
|
241
|
+
lineNumber: node.getStartLineNumber()
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
this.warnings.push(
|
|
245
|
+
`${filePath}:${node.getStartLineNumber()}: .flatten() is removed in v4. Use z.flattenError(error) or access error.issues directly. See: https://zod.dev/v4/changelog`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
252
|
+
for (const { node, objectText, lineNumber } of nodesToTransform) {
|
|
253
|
+
node.replaceWithText(`z.flattenError(${objectText})`);
|
|
254
|
+
this.warnings.push(
|
|
255
|
+
`${filePath}:${lineNumber}: .flatten() auto-transformed to z.flattenError(). Removed in v4 \u2014 now a standalone function.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Auto-transform error.format() to z.treeifyError(error) in v4.
|
|
261
|
+
*/
|
|
262
|
+
transformFormat(sourceFile) {
|
|
263
|
+
const filePath = sourceFile.getFilePath();
|
|
264
|
+
const zodErrorVars = this.buildZodErrorVarSet(sourceFile);
|
|
265
|
+
const nodesToTransform = [];
|
|
266
|
+
sourceFile.forEachDescendant((node) => {
|
|
267
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
268
|
+
const expression = node.getExpression();
|
|
269
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "format") {
|
|
270
|
+
const objectText = expression.getExpression().getText();
|
|
271
|
+
const isZodError = zodErrorVars.has(objectText) || objectText.endsWith(".error") && zodErrorVars.has(`${objectText.replace(/\.error$/, "")}.error`);
|
|
272
|
+
if (isZodError) {
|
|
273
|
+
nodesToTransform.push({
|
|
274
|
+
node,
|
|
275
|
+
objectText,
|
|
276
|
+
lineNumber: node.getStartLineNumber()
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
283
|
+
for (const { node, objectText, lineNumber } of nodesToTransform) {
|
|
284
|
+
node.replaceWithText(`z.treeifyError(${objectText})`);
|
|
285
|
+
this.warnings.push(
|
|
286
|
+
`${filePath}:${lineNumber}: .format() auto-transformed to z.treeifyError(). Removed in v4 \u2014 now a standalone function.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Auto-transform ._def property access to ._zod.def in v4.
|
|
292
|
+
*/
|
|
293
|
+
transformDefAccess(sourceFile) {
|
|
294
|
+
const filePath = sourceFile.getFilePath();
|
|
295
|
+
const nodesToTransform = [];
|
|
296
|
+
sourceFile.forEachDescendant((node) => {
|
|
297
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(node) && node.getName() === "_def") {
|
|
298
|
+
nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
302
|
+
for (const { node, lineNumber } of nodesToTransform) {
|
|
303
|
+
const objectText = node.getExpression().getText();
|
|
304
|
+
node.replaceWithText(`${objectText}._zod.def`);
|
|
305
|
+
this.warnings.push(
|
|
306
|
+
`${filePath}:${lineNumber}: ._def auto-transformed to ._zod.def. Internal API \u2014 structure may change between releases.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Transform .merge(otherSchema) to .extend(otherSchema.shape)
|
|
312
|
+
*/
|
|
313
|
+
transformMerge(sourceFile) {
|
|
314
|
+
const filePath = sourceFile.getFilePath();
|
|
315
|
+
const nodesToTransform = [];
|
|
316
|
+
sourceFile.forEachDescendant((node) => {
|
|
317
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
318
|
+
const expression = node.getExpression();
|
|
319
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "merge") {
|
|
320
|
+
nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
325
|
+
for (const { node, lineNumber } of nodesToTransform) {
|
|
326
|
+
const args = node.getArguments();
|
|
327
|
+
if (args.length === 1) {
|
|
328
|
+
const argText = args[0]?.getText();
|
|
329
|
+
if (argText) {
|
|
330
|
+
const expression = node.getExpression();
|
|
331
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
|
|
332
|
+
const objectText = expression.getExpression().getText();
|
|
333
|
+
node.replaceWithText(`${objectText}.extend(${argText}.shape)`);
|
|
334
|
+
this.warnings.push(
|
|
335
|
+
`${filePath}:${lineNumber}: .merge() renamed to .extend() with .shape access. Verify the merged schema exposes .shape.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Transform z.nativeEnum(X) to z.enum(X)
|
|
344
|
+
*/
|
|
345
|
+
transformNativeEnum(sourceFile) {
|
|
346
|
+
const filePath = sourceFile.getFilePath();
|
|
347
|
+
const fullText = sourceFile.getFullText();
|
|
348
|
+
const newText = fullText.replace(/\bz\.nativeEnum\(/g, "z.enum(");
|
|
349
|
+
if (newText !== fullText) {
|
|
350
|
+
sourceFile.replaceWithText(newText);
|
|
351
|
+
this.warnings.push(
|
|
352
|
+
`${filePath}: z.nativeEnum() renamed to z.enum() in v4. Verify enum values are compatible.`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Check for deprecated methods and add warnings
|
|
358
|
+
*/
|
|
359
|
+
checkDeprecatedMethods(sourceFile) {
|
|
360
|
+
const filePath = sourceFile.getFilePath();
|
|
361
|
+
sourceFile.forEachDescendant((node) => {
|
|
362
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
363
|
+
const expression = node.getExpression();
|
|
364
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
|
|
365
|
+
const name = expression.getName();
|
|
366
|
+
if (DEPRECATED_METHODS.has(name) && !AUTO_TRANSFORMABLE.has(name)) {
|
|
367
|
+
const lineNumber = node.getStartLineNumber();
|
|
368
|
+
switch (name) {
|
|
369
|
+
case "strict":
|
|
370
|
+
this.warnings.push(
|
|
371
|
+
`${filePath}:${lineNumber}: .strict() is deprecated in v4. Use z.strictObject() instead.`
|
|
372
|
+
);
|
|
373
|
+
break;
|
|
374
|
+
case "passthrough":
|
|
375
|
+
this.warnings.push(
|
|
376
|
+
`${filePath}:${lineNumber}: .passthrough() is deprecated in v4. Use z.looseObject() instead.`
|
|
377
|
+
);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Auto-transform .superRefine() to .check() — callback signature is identical.
|
|
387
|
+
*/
|
|
388
|
+
transformSuperRefine(sourceFile) {
|
|
389
|
+
const filePath = sourceFile.getFilePath();
|
|
390
|
+
const nodesToTransform = [];
|
|
391
|
+
sourceFile.forEachDescendant((node) => {
|
|
392
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(node) && node.getName() === "superRefine") {
|
|
393
|
+
nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
397
|
+
for (const { node, lineNumber } of nodesToTransform) {
|
|
398
|
+
const parent = node.getParent();
|
|
399
|
+
if (parent && import_ts_morph.Node.isCallExpression(parent)) {
|
|
400
|
+
const objectText = node.getExpression().getText();
|
|
401
|
+
const args = parent.getArguments();
|
|
402
|
+
let argsText = args.map((a) => a.getText()).join(", ");
|
|
403
|
+
if (args.length > 0) {
|
|
404
|
+
const callbackText = args[0]?.getText() ?? "";
|
|
405
|
+
const paramMatch = callbackText.match(
|
|
406
|
+
/^\s*\(?\s*\w+\s*,\s*(\w+)\s*\)?(?:\s*:\s*[^)]+)?\s*=>/
|
|
407
|
+
);
|
|
408
|
+
if (paramMatch?.[1]) {
|
|
409
|
+
const ctxName = paramMatch[1];
|
|
410
|
+
const addIssuePattern = new RegExp(`${ctxName}\\.addIssue\\(`, "g");
|
|
411
|
+
if (addIssuePattern.test(callbackText)) {
|
|
412
|
+
argsText = callbackText.replace(
|
|
413
|
+
new RegExp(`${ctxName}\\.addIssue\\(`, "g"),
|
|
414
|
+
`${ctxName}.issues.push(`
|
|
415
|
+
);
|
|
416
|
+
if (args.length > 1) {
|
|
417
|
+
const remaining = args.slice(1).map((a) => a.getText()).join(", ");
|
|
418
|
+
argsText = `${argsText}, ${remaining}`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
parent.replaceWithText(`${objectText}.check(${argsText})`);
|
|
424
|
+
this.warnings.push(
|
|
425
|
+
`${filePath}:${lineNumber}: .superRefine() auto-transformed to .check() (deprecated in v4).`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Auto-transform invalid_type_error/required_error params to unified `error` param.
|
|
432
|
+
*/
|
|
433
|
+
transformErrorParams(sourceFile) {
|
|
434
|
+
const filePath = sourceFile.getFilePath();
|
|
435
|
+
const nodesToTransform = [];
|
|
436
|
+
sourceFile.forEachDescendant((node) => {
|
|
437
|
+
if (import_ts_morph.Node.isObjectLiteralExpression(node)) {
|
|
438
|
+
const properties = node.getProperties();
|
|
439
|
+
const propNames = properties.filter((p) => import_ts_morph.Node.isPropertyAssignment(p)).map((p) => import_ts_morph.Node.isPropertyAssignment(p) ? p.getName() : "");
|
|
440
|
+
const hasInvalidType = propNames.includes("invalid_type_error");
|
|
441
|
+
const hasRequired = propNames.includes("required_error");
|
|
442
|
+
if (hasInvalidType || hasRequired) {
|
|
443
|
+
const parent = node.getParent();
|
|
444
|
+
if (parent && import_ts_morph.Node.isCallExpression(parent)) {
|
|
445
|
+
const expr = parent.getExpression();
|
|
446
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expr) && expr.getExpression().getText() === "z") {
|
|
447
|
+
nodesToTransform.push({ node, lineNumber: node.getStartLineNumber() });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
nodesToTransform.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
454
|
+
for (const { node, lineNumber } of nodesToTransform) {
|
|
455
|
+
const properties = node.getProperties();
|
|
456
|
+
let invalidTypeValue;
|
|
457
|
+
let requiredValue;
|
|
458
|
+
const otherProps = [];
|
|
459
|
+
for (const prop of properties) {
|
|
460
|
+
if (import_ts_morph.Node.isPropertyAssignment(prop)) {
|
|
461
|
+
const name = prop.getName();
|
|
462
|
+
const value = prop.getInitializer()?.getText() ?? "";
|
|
463
|
+
if (name === "invalid_type_error") {
|
|
464
|
+
invalidTypeValue = value;
|
|
465
|
+
} else if (name === "required_error") {
|
|
466
|
+
requiredValue = value;
|
|
467
|
+
} else {
|
|
468
|
+
otherProps.push(prop.getText());
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
let errorValue;
|
|
473
|
+
if (invalidTypeValue && requiredValue) {
|
|
474
|
+
errorValue = `error: (issue) => issue.input === undefined ? ${requiredValue} : ${invalidTypeValue}`;
|
|
475
|
+
} else if (requiredValue) {
|
|
476
|
+
errorValue = `error: ${requiredValue}`;
|
|
477
|
+
} else if (invalidTypeValue) {
|
|
478
|
+
errorValue = `error: ${invalidTypeValue}`;
|
|
479
|
+
} else {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const allProps = [errorValue, ...otherProps].join(", ");
|
|
483
|
+
node.replaceWithText(`{ ${allProps} }`);
|
|
484
|
+
this.warnings.push(
|
|
485
|
+
`${filePath}:${lineNumber}: invalid_type_error/required_error auto-transformed to unified error param.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Detect instanceof Error patterns in catch blocks that reference ZodError.
|
|
491
|
+
* In v4, ZodError no longer extends Error.
|
|
492
|
+
*/
|
|
493
|
+
checkInstanceofError(sourceFile) {
|
|
494
|
+
const filePath = sourceFile.getFilePath();
|
|
495
|
+
sourceFile.forEachDescendant((node) => {
|
|
496
|
+
if (import_ts_morph.Node.isCatchClause(node)) {
|
|
497
|
+
const block = node.getBlock();
|
|
498
|
+
const blockText = block.getText();
|
|
499
|
+
if (blockText.includes("ZodError") && blockText.includes("instanceof Error")) {
|
|
500
|
+
const lineNumber = node.getStartLineNumber();
|
|
501
|
+
this.warnings.push(
|
|
502
|
+
`${filePath}:${lineNumber}: ZodError no longer extends Error in v4. \`instanceof Error\` checks on ZodError will return false. Use \`instanceof ZodError\` or check for \`.issues\` property instead.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Detect .refine()/.superRefine() followed by .transform() in the same chain.
|
|
510
|
+
* In v4, .transform() runs even when .refine() fails.
|
|
511
|
+
*/
|
|
512
|
+
checkTransformRefineOrder(sourceFile) {
|
|
513
|
+
const filePath = sourceFile.getFilePath();
|
|
514
|
+
sourceFile.forEachDescendant((node) => {
|
|
515
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
516
|
+
const expression = node.getExpression();
|
|
517
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "transform") {
|
|
518
|
+
const chainText = node.getText();
|
|
519
|
+
if (chainText.includes(".refine(") || chainText.includes(".superRefine(") || chainText.includes(".check(")) {
|
|
520
|
+
const lineNumber = node.getStartLineNumber();
|
|
521
|
+
this.warnings.push(
|
|
522
|
+
`${filePath}:${lineNumber}: In v4, .transform() executes even if preceding .refine() fails. Consider using .pipe() to sequence validation before transforms.`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Detect .default() combined with .optional() — behavior changed silently in v4.
|
|
531
|
+
* In v4, .default() always provides a value, making .optional() a no-op.
|
|
532
|
+
*/
|
|
533
|
+
checkDefaultOptional(sourceFile) {
|
|
534
|
+
const filePath = sourceFile.getFilePath();
|
|
535
|
+
sourceFile.forEachDescendant((node) => {
|
|
536
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
537
|
+
const expression = node.getExpression();
|
|
538
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && (expression.getName() === "optional" || expression.getName() === "default")) {
|
|
539
|
+
const chainText = node.getText();
|
|
540
|
+
const hasDefault = chainText.includes(".default(");
|
|
541
|
+
const hasOptional = chainText.includes(".optional(");
|
|
542
|
+
if (hasDefault && hasOptional) {
|
|
543
|
+
const lineNumber = node.getStartLineNumber();
|
|
544
|
+
this.warnings.push(
|
|
545
|
+
`${filePath}:${lineNumber}: .default() + .optional() behavior changed in v4. .default() now always provides a value, making .optional() effectively a no-op. Review whether .optional() is still needed.`
|
|
131
546
|
);
|
|
132
547
|
}
|
|
133
548
|
}
|
|
@@ -167,20 +582,55 @@ var ZodV3ToV4Transformer = class {
|
|
|
167
582
|
`${filePath}:${lineNumber}: z.function() parameter handling has changed. Review input/output schema definitions.`
|
|
168
583
|
);
|
|
169
584
|
break;
|
|
585
|
+
case "pipe":
|
|
586
|
+
this.warnings.push(
|
|
587
|
+
`${filePath}:${lineNumber}: .pipe() has stricter type checking in v4. If you get type errors, add explicit type annotations to .transform() return types or cast through unknown as a last resort.`
|
|
588
|
+
);
|
|
589
|
+
break;
|
|
170
590
|
}
|
|
171
591
|
}
|
|
172
592
|
}
|
|
173
593
|
}
|
|
174
594
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Detect .catch() combined with .optional() — behavior changed in v4.
|
|
598
|
+
* In v4, .catch() on optional properties always returns the catch value.
|
|
599
|
+
*/
|
|
600
|
+
checkCatchOptional(sourceFile) {
|
|
601
|
+
const filePath = sourceFile.getFilePath();
|
|
602
|
+
sourceFile.forEachDescendant((node) => {
|
|
603
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
604
|
+
const expression = node.getExpression();
|
|
605
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && (expression.getName() === "optional" || expression.getName() === "catch")) {
|
|
606
|
+
const chainText = node.getText();
|
|
607
|
+
if (chainText.includes(".catch(") && chainText.includes(".optional(")) {
|
|
608
|
+
const lineNumber = node.getStartLineNumber();
|
|
609
|
+
this.warnings.push(
|
|
610
|
+
`${filePath}:${lineNumber}: .catch() + .optional() behavior changed in v4. .catch() on optional properties now always returns the catch value, even when the property is absent from input.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Detect imports of utility types that moved to 'zod/v4/core' in v4.
|
|
619
|
+
*/
|
|
620
|
+
checkUtilityTypeImports(sourceFile) {
|
|
621
|
+
const filePath = sourceFile.getFilePath();
|
|
622
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
623
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
624
|
+
if (moduleSpecifier !== "zod") continue;
|
|
625
|
+
const namedImports = importDecl.getNamedImports();
|
|
626
|
+
for (const namedImport of namedImports) {
|
|
627
|
+
const name = namedImport.getName();
|
|
628
|
+
if (UTILITY_TYPES.has(name)) {
|
|
629
|
+
const lineNumber = importDecl.getStartLineNumber();
|
|
630
|
+
this.warnings.push(
|
|
631
|
+
`${filePath}:${lineNumber}: '${name}' may need to be imported from 'zod/v4/core' in v4. Internal utility types have moved to a separate subpackage.`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
184
634
|
}
|
|
185
635
|
}
|
|
186
636
|
}
|