@schemashift/zod-v3-v4 0.9.0 → 0.11.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/index.cjs +453 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -4
- package/dist/index.d.ts +100 -4
- package/dist/index.js +450 -6
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.cjs
CHANGED
|
@@ -23,10 +23,174 @@ __export(index_exports, {
|
|
|
23
23
|
BEHAVIOR_CHANGES: () => BEHAVIOR_CHANGES,
|
|
24
24
|
V3_TO_V4_PATTERNS: () => V3_TO_V4_PATTERNS,
|
|
25
25
|
ZodV3ToV4Transformer: () => ZodV3ToV4Transformer,
|
|
26
|
-
createZodV3ToV4Handler: () => createZodV3ToV4Handler
|
|
26
|
+
createZodV3ToV4Handler: () => createZodV3ToV4Handler,
|
|
27
|
+
detectBehavioralPatterns: () => detectBehavioralPatterns,
|
|
28
|
+
generateBehaviorTests: () => generateBehaviorTests
|
|
27
29
|
});
|
|
28
30
|
module.exports = __toCommonJS(index_exports);
|
|
29
31
|
|
|
32
|
+
// src/behavior-test-generator.ts
|
|
33
|
+
var V4_BEHAVIORAL_PATTERNS = [
|
|
34
|
+
{
|
|
35
|
+
name: "default-optional",
|
|
36
|
+
description: ".default() + .optional() behaves differently in v4",
|
|
37
|
+
regex: /\.default\([^)]*\)[\s\S]{0,20}\.optional\(\)|\.optional\(\)[\s\S]{0,20}\.default\([^)]*\)/,
|
|
38
|
+
testTemplate: [
|
|
39
|
+
" it('verifies .default() + .optional() behavior (v4 change)', () => {",
|
|
40
|
+
" // v3: undefined input returns undefined (.optional() wins)",
|
|
41
|
+
" // v4: undefined input returns default value (.default() always applies)",
|
|
42
|
+
" const schema = z.string().default('fallback').optional();",
|
|
43
|
+
" const result = schema.parse(undefined);",
|
|
44
|
+
" expect(result).toBe('fallback');",
|
|
45
|
+
" });"
|
|
46
|
+
].join("\n")
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "error-instanceof",
|
|
50
|
+
description: "ZodError no longer extends Error in v4",
|
|
51
|
+
regex: /instanceof\s+Error|instanceof\s+ZodError/,
|
|
52
|
+
testTemplate: [
|
|
53
|
+
" it('verifies ZodError instanceof check (v4 change)', () => {",
|
|
54
|
+
" // v3: ZodError extends Error",
|
|
55
|
+
" // v4: ZodError does NOT extend Error",
|
|
56
|
+
" try {",
|
|
57
|
+
" z.string().parse(123);",
|
|
58
|
+
" } catch (e) {",
|
|
59
|
+
" expect(e).toBeDefined();",
|
|
60
|
+
" }",
|
|
61
|
+
" });"
|
|
62
|
+
].join("\n")
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "transform-after-refine",
|
|
66
|
+
description: ".transform() after .refine() may change execution order in v4",
|
|
67
|
+
regex: /\.refine\([^)]*\)[\s\S]{0,30}\.transform\(/,
|
|
68
|
+
testTemplate: [
|
|
69
|
+
" it('verifies .refine() then .transform() ordering (v4 change)', () => {",
|
|
70
|
+
" const schema = z.string()",
|
|
71
|
+
" .refine((val) => val.length > 0, 'Must not be empty')",
|
|
72
|
+
" .transform((val) => val.toUpperCase());",
|
|
73
|
+
" const result = schema.safeParse('hello');",
|
|
74
|
+
" expect(result.success).toBe(true);",
|
|
75
|
+
" if (result.success) {",
|
|
76
|
+
" expect(result.data).toBe('HELLO');",
|
|
77
|
+
" }",
|
|
78
|
+
" });"
|
|
79
|
+
].join("\n")
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "catch-optional",
|
|
83
|
+
description: ".catch() + .optional() interaction changed in v4",
|
|
84
|
+
regex: /\.catch\([^)]*\)[\s\S]{0,20}\.optional\(\)|\.optional\(\)[\s\S]{0,20}\.catch\([^)]*\)/,
|
|
85
|
+
testTemplate: [
|
|
86
|
+
" it('verifies .catch() + .optional() behavior (v4 change)', () => {",
|
|
87
|
+
" const schema = z.string().catch('default').optional();",
|
|
88
|
+
" const result = schema.parse(undefined);",
|
|
89
|
+
" expect(result).toBeDefined();",
|
|
90
|
+
" });"
|
|
91
|
+
].join("\n")
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "error-message-format",
|
|
95
|
+
description: 'Error messages changed from "Required" to descriptive messages',
|
|
96
|
+
regex: /['"]Required['"]/,
|
|
97
|
+
testTemplate: [
|
|
98
|
+
" it('verifies error message format (v4 change)', () => {",
|
|
99
|
+
' // v3: "Required" for missing fields',
|
|
100
|
+
' // v4: "Invalid input: expected string, received undefined"',
|
|
101
|
+
" const schema = z.object({ name: z.string() });",
|
|
102
|
+
" const result = schema.safeParse({});",
|
|
103
|
+
" expect(result.success).toBe(false);",
|
|
104
|
+
" if (!result.success) {",
|
|
105
|
+
" const msg = result.error.issues[0]?.message ?? '';",
|
|
106
|
+
" expect(msg.length).toBeGreaterThan(0);",
|
|
107
|
+
" }",
|
|
108
|
+
" });"
|
|
109
|
+
].join("\n")
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "error-flatten",
|
|
113
|
+
description: "error.flatten() moved to z.flattenError(error) in v4",
|
|
114
|
+
regex: /\.flatten\(\)/,
|
|
115
|
+
testTemplate: [
|
|
116
|
+
" it('verifies error flattening (v4 change)', () => {",
|
|
117
|
+
" // v3: error.flatten()",
|
|
118
|
+
" // v4: z.flattenError(error)",
|
|
119
|
+
" const schema = z.object({ name: z.string() });",
|
|
120
|
+
" const result = schema.safeParse({});",
|
|
121
|
+
" if (!result.success) {",
|
|
122
|
+
" expect(result.error).toBeDefined();",
|
|
123
|
+
" }",
|
|
124
|
+
" });"
|
|
125
|
+
].join("\n")
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
function detectBehavioralPatterns(sourceCode, filePath) {
|
|
129
|
+
const results = [];
|
|
130
|
+
const lines = sourceCode.split("\n");
|
|
131
|
+
for (const pattern of V4_BEHAVIORAL_PATTERNS) {
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
const line = lines[i] ?? "";
|
|
134
|
+
if (pattern.regex.test(line)) {
|
|
135
|
+
results.push({
|
|
136
|
+
pattern,
|
|
137
|
+
filePath,
|
|
138
|
+
lineNumber: i + 1,
|
|
139
|
+
matchedText: line.trim()
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (pattern.regex.test(sourceCode) && !results.some((r) => r.pattern.name === pattern.name && r.filePath === filePath)) {
|
|
144
|
+
const match = pattern.regex.exec(sourceCode);
|
|
145
|
+
if (match) {
|
|
146
|
+
const lineNumber = sourceCode.slice(0, match.index).split("\n").length;
|
|
147
|
+
results.push({
|
|
148
|
+
pattern,
|
|
149
|
+
filePath,
|
|
150
|
+
lineNumber,
|
|
151
|
+
matchedText: match[0].trim().slice(0, 80)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
function generateBehaviorTests(files) {
|
|
159
|
+
const allPatterns = [];
|
|
160
|
+
const testsByFile = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const { filePath, sourceCode } of files) {
|
|
162
|
+
const detected = detectBehavioralPatterns(sourceCode, filePath);
|
|
163
|
+
allPatterns.push(...detected);
|
|
164
|
+
for (const d of detected) {
|
|
165
|
+
const existing = testsByFile.get(filePath) ?? /* @__PURE__ */ new Set();
|
|
166
|
+
existing.add(d.pattern.name);
|
|
167
|
+
testsByFile.set(filePath, existing);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const tests = [];
|
|
171
|
+
for (const [filePath, patternNames] of testsByFile) {
|
|
172
|
+
const patterns = [...patternNames];
|
|
173
|
+
const testBlocks = patterns.map((name) => V4_BEHAVIORAL_PATTERNS.find((p) => p.name === name)?.testTemplate ?? "").filter(Boolean);
|
|
174
|
+
if (testBlocks.length === 0) continue;
|
|
175
|
+
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
176
|
+
const testCode = [
|
|
177
|
+
"import { describe, expect, it } from 'vitest';",
|
|
178
|
+
"import { z } from 'zod';",
|
|
179
|
+
"",
|
|
180
|
+
"/**",
|
|
181
|
+
" * Behavioral regression tests for Zod v3 to v4 migration",
|
|
182
|
+
` * Source: ${shortPath}`,
|
|
183
|
+
" * Generated by SchemaShift",
|
|
184
|
+
" */",
|
|
185
|
+
`describe('v4 behavioral changes: ${shortPath}', () => {`,
|
|
186
|
+
...testBlocks.map((b) => b),
|
|
187
|
+
"});"
|
|
188
|
+
].join("\n");
|
|
189
|
+
tests.push({ filePath, testCode, patterns });
|
|
190
|
+
}
|
|
191
|
+
return { tests, totalPatterns: allPatterns.length };
|
|
192
|
+
}
|
|
193
|
+
|
|
30
194
|
// src/transformer.ts
|
|
31
195
|
var import_ts_morph = require("ts-morph");
|
|
32
196
|
|
|
@@ -41,19 +205,23 @@ var V3_TO_V4_PATTERNS = {
|
|
|
41
205
|
};
|
|
42
206
|
var BEHAVIOR_CHANGES = /* @__PURE__ */ new Set([
|
|
43
207
|
"default",
|
|
44
|
-
// Type inference changed
|
|
208
|
+
// Type inference changed; now matches output type, use .prefault() for v3 behavior
|
|
45
209
|
"uuid",
|
|
46
|
-
// Stricter validation
|
|
210
|
+
// Stricter RFC 9562/4122 validation; use z.guid() for lenient matching
|
|
47
211
|
"record",
|
|
48
212
|
// Requires key type
|
|
49
213
|
"discriminatedUnion",
|
|
50
214
|
// Stricter discriminator requirements
|
|
51
215
|
"function",
|
|
52
|
-
//
|
|
216
|
+
// Complete overhaul — .args()/.returns() removed
|
|
53
217
|
"pipe",
|
|
54
218
|
// Stricter type checking in v4
|
|
55
|
-
"catch"
|
|
219
|
+
"catch",
|
|
56
220
|
// .catch() on optional properties always returns catch value in v4
|
|
221
|
+
"nonempty",
|
|
222
|
+
// Type inference changed: string[] instead of [string, ...string[]]
|
|
223
|
+
"coerce"
|
|
224
|
+
// Input type changed to unknown for all z.coerce.* schemas
|
|
57
225
|
]);
|
|
58
226
|
var DEPRECATED_METHODS = /* @__PURE__ */ new Set([
|
|
59
227
|
"merge",
|
|
@@ -62,8 +230,26 @@ var DEPRECATED_METHODS = /* @__PURE__ */ new Set([
|
|
|
62
230
|
// Use .check() instead
|
|
63
231
|
"strict",
|
|
64
232
|
// Use z.strictObject() instead
|
|
65
|
-
"passthrough"
|
|
233
|
+
"passthrough",
|
|
66
234
|
// Use z.looseObject() instead
|
|
235
|
+
"deepPartial",
|
|
236
|
+
// Removed entirely — was deprecated anti-pattern
|
|
237
|
+
"strip",
|
|
238
|
+
// Deprecated — default behavior in v4
|
|
239
|
+
"nonstrict"
|
|
240
|
+
// Removed entirely
|
|
241
|
+
]);
|
|
242
|
+
var DEPRECATED_FACTORIES = /* @__PURE__ */ new Set([
|
|
243
|
+
"promise",
|
|
244
|
+
// z.promise() deprecated — await values before parsing
|
|
245
|
+
"ostring",
|
|
246
|
+
// z.ostring() removed (optional shorthand)
|
|
247
|
+
"onumber",
|
|
248
|
+
// z.onumber() removed
|
|
249
|
+
"oboolean",
|
|
250
|
+
// z.oboolean() removed
|
|
251
|
+
"preprocess"
|
|
252
|
+
// z.preprocess() removed — use z.pipe() instead
|
|
67
253
|
]);
|
|
68
254
|
var AUTO_TRANSFORMABLE = /* @__PURE__ */ new Set([
|
|
69
255
|
"superRefine",
|
|
@@ -80,6 +266,22 @@ var UTILITY_TYPES = /* @__PURE__ */ new Set([
|
|
|
80
266
|
"ZodTypeAny",
|
|
81
267
|
"ZodFirstPartyTypeKind"
|
|
82
268
|
]);
|
|
269
|
+
var STRING_FORMAT_TO_TOPLEVEL = /* @__PURE__ */ new Map([
|
|
270
|
+
["email", "z.email()"],
|
|
271
|
+
["url", "z.url()"],
|
|
272
|
+
["uuid", "z.uuid()"],
|
|
273
|
+
["emoji", "z.emoji()"],
|
|
274
|
+
["nanoid", "z.nanoid()"],
|
|
275
|
+
["cuid", "z.cuid()"],
|
|
276
|
+
["cuid2", "z.cuid2()"],
|
|
277
|
+
["ulid", "z.ulid()"],
|
|
278
|
+
["ipv4", "z.ipv4()"],
|
|
279
|
+
["ipv6", "z.ipv6()"],
|
|
280
|
+
["cidrv4", "z.cidrv4()"],
|
|
281
|
+
["cidrv6", "z.cidrv6()"],
|
|
282
|
+
["base64", "z.base64()"],
|
|
283
|
+
["base64url", "z.base64url()"]
|
|
284
|
+
]);
|
|
83
285
|
|
|
84
286
|
// src/transformer.ts
|
|
85
287
|
var ZodV3ToV4Transformer = class {
|
|
@@ -100,13 +302,23 @@ var ZodV3ToV4Transformer = class {
|
|
|
100
302
|
this.transformFlatten(sourceFile);
|
|
101
303
|
this.transformFormat(sourceFile);
|
|
102
304
|
this.transformDefAccess(sourceFile);
|
|
305
|
+
this.transformFunctionValidation(sourceFile);
|
|
306
|
+
this.transformOptionalShorthands(sourceFile);
|
|
103
307
|
this.checkDeprecatedMethods(sourceFile);
|
|
308
|
+
this.checkDeprecatedFactories(sourceFile);
|
|
309
|
+
this.checkStringFormatMethods(sourceFile);
|
|
104
310
|
this.checkBehaviorChanges(sourceFile);
|
|
105
311
|
this.checkInstanceofError(sourceFile);
|
|
106
312
|
this.checkTransformRefineOrder(sourceFile);
|
|
107
313
|
this.checkDefaultOptional(sourceFile);
|
|
108
314
|
this.checkCatchOptional(sourceFile);
|
|
109
315
|
this.checkUtilityTypeImports(sourceFile);
|
|
316
|
+
this.checkNonemptyTypeChange(sourceFile);
|
|
317
|
+
this.checkCoerceInputType(sourceFile);
|
|
318
|
+
this.checkDeepPartialRemoval(sourceFile);
|
|
319
|
+
this.checkStripDeprecation(sourceFile);
|
|
320
|
+
this.checkErrorMapPrecedence(sourceFile);
|
|
321
|
+
this.checkAddIssueMethods(sourceFile);
|
|
110
322
|
return {
|
|
111
323
|
success: this.errors.length === 0,
|
|
112
324
|
filePath,
|
|
@@ -634,6 +846,238 @@ var ZodV3ToV4Transformer = class {
|
|
|
634
846
|
}
|
|
635
847
|
}
|
|
636
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* Warn about string format methods that moved to top-level functions in v4.
|
|
851
|
+
* e.g., z.string().email() → z.email() (instance methods are deprecated but still work)
|
|
852
|
+
*/
|
|
853
|
+
checkStringFormatMethods(sourceFile) {
|
|
854
|
+
const filePath = sourceFile.getFilePath();
|
|
855
|
+
sourceFile.forEachDescendant((node) => {
|
|
856
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
857
|
+
const expression = node.getExpression();
|
|
858
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
|
|
859
|
+
const name = expression.getName();
|
|
860
|
+
if (STRING_FORMAT_TO_TOPLEVEL.has(name)) {
|
|
861
|
+
const chainText = node.getText();
|
|
862
|
+
if (chainText.includes("z.string()") || chainText.includes(".string()")) {
|
|
863
|
+
const topLevel = STRING_FORMAT_TO_TOPLEVEL.get(name);
|
|
864
|
+
const lineNumber = node.getStartLineNumber();
|
|
865
|
+
this.warnings.push(
|
|
866
|
+
`${filePath}:${lineNumber}: .${name}() is deprecated as an instance method in v4. Use ${topLevel} as a top-level function instead. The instance method still works but may be removed in future versions.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Check for deprecated factory functions (z.promise(), z.ostring(), etc.)
|
|
876
|
+
*/
|
|
877
|
+
checkDeprecatedFactories(sourceFile) {
|
|
878
|
+
const filePath = sourceFile.getFilePath();
|
|
879
|
+
sourceFile.forEachDescendant((node) => {
|
|
880
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
881
|
+
const expression = node.getExpression();
|
|
882
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
|
|
883
|
+
const name = expression.getName();
|
|
884
|
+
const object = expression.getExpression();
|
|
885
|
+
if (object.getText() === "z" && DEPRECATED_FACTORIES.has(name)) {
|
|
886
|
+
const lineNumber = node.getStartLineNumber();
|
|
887
|
+
switch (name) {
|
|
888
|
+
case "promise":
|
|
889
|
+
this.warnings.push(
|
|
890
|
+
`${filePath}:${lineNumber}: z.promise() is deprecated in v4. Await values before parsing instead of wrapping in z.promise().`
|
|
891
|
+
);
|
|
892
|
+
break;
|
|
893
|
+
case "ostring":
|
|
894
|
+
this.warnings.push(
|
|
895
|
+
`${filePath}:${lineNumber}: z.ostring() is removed in v4. Use z.string().optional() instead.`
|
|
896
|
+
);
|
|
897
|
+
break;
|
|
898
|
+
case "onumber":
|
|
899
|
+
this.warnings.push(
|
|
900
|
+
`${filePath}:${lineNumber}: z.onumber() is removed in v4. Use z.number().optional() instead.`
|
|
901
|
+
);
|
|
902
|
+
break;
|
|
903
|
+
case "oboolean":
|
|
904
|
+
this.warnings.push(
|
|
905
|
+
`${filePath}:${lineNumber}: z.oboolean() is removed in v4. Use z.boolean().optional() instead.`
|
|
906
|
+
);
|
|
907
|
+
break;
|
|
908
|
+
case "preprocess":
|
|
909
|
+
this.warnings.push(
|
|
910
|
+
`${filePath}:${lineNumber}: z.preprocess() is removed in v4. Use z.pipe(z.unknown(), z.transform(fn), targetSchema) instead.`
|
|
911
|
+
);
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Transform z.function().args().returns() to z.function({ input, output }) in v4.
|
|
921
|
+
*/
|
|
922
|
+
transformFunctionValidation(sourceFile) {
|
|
923
|
+
const filePath = sourceFile.getFilePath();
|
|
924
|
+
sourceFile.forEachDescendant((node) => {
|
|
925
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
926
|
+
const expression = node.getExpression();
|
|
927
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
|
|
928
|
+
const name = expression.getName();
|
|
929
|
+
if (name === "args" || name === "returns") {
|
|
930
|
+
const chainText = node.getText();
|
|
931
|
+
if (chainText.includes("z.function()") && (chainText.includes(".args(") || chainText.includes(".returns("))) {
|
|
932
|
+
const lineNumber = node.getStartLineNumber();
|
|
933
|
+
this.warnings.push(
|
|
934
|
+
`${filePath}:${lineNumber}: z.function().args().returns() is removed in v4. Use z.function({ input: z.tuple([...]), output: schema }) instead. The result is no longer a Zod schema \u2014 use .implement() to wrap functions.`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Transform z.ostring(), z.onumber(), z.oboolean() to explicit .optional() calls.
|
|
944
|
+
*/
|
|
945
|
+
transformOptionalShorthands(sourceFile) {
|
|
946
|
+
const filePath = sourceFile.getFilePath();
|
|
947
|
+
const fullText = sourceFile.getFullText();
|
|
948
|
+
const replacements = [
|
|
949
|
+
{ pattern: /\bz\.ostring\(\)/g, replacement: "z.string().optional()", name: "z.ostring()" },
|
|
950
|
+
{ pattern: /\bz\.onumber\(\)/g, replacement: "z.number().optional()", name: "z.onumber()" },
|
|
951
|
+
{
|
|
952
|
+
pattern: /\bz\.oboolean\(\)/g,
|
|
953
|
+
replacement: "z.boolean().optional()",
|
|
954
|
+
name: "z.oboolean()"
|
|
955
|
+
}
|
|
956
|
+
];
|
|
957
|
+
let newText = fullText;
|
|
958
|
+
for (const { pattern, replacement, name } of replacements) {
|
|
959
|
+
if (pattern.test(newText)) {
|
|
960
|
+
newText = newText.replace(pattern, replacement);
|
|
961
|
+
this.warnings.push(
|
|
962
|
+
`${filePath}: ${name} auto-transformed to ${replacement}. Removed in v4.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (newText !== fullText) {
|
|
967
|
+
sourceFile.replaceWithText(newText);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Check for .nonempty() which has a type inference change in v4.
|
|
972
|
+
* v3: [string, ...string[]] (tuple with rest)
|
|
973
|
+
* v4: string[] (matches .min(1))
|
|
974
|
+
*/
|
|
975
|
+
checkNonemptyTypeChange(sourceFile) {
|
|
976
|
+
const filePath = sourceFile.getFilePath();
|
|
977
|
+
sourceFile.forEachDescendant((node) => {
|
|
978
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
979
|
+
const expression = node.getExpression();
|
|
980
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "nonempty") {
|
|
981
|
+
const lineNumber = node.getStartLineNumber();
|
|
982
|
+
this.warnings.push(
|
|
983
|
+
`${filePath}:${lineNumber}: .nonempty() type inference changed in v4. v3 inferred [T, ...T[]] (tuple with rest), v4 infers T[] (same as .min(1)). If you rely on the tuple type, use z.tuple([z.string()], z.string()) instead.`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Check for z.coerce.* usage — input type changed to unknown in v4.
|
|
991
|
+
*/
|
|
992
|
+
checkCoerceInputType(sourceFile) {
|
|
993
|
+
const filePath = sourceFile.getFilePath();
|
|
994
|
+
const fullText = sourceFile.getFullText();
|
|
995
|
+
if (fullText.includes("z.coerce.")) {
|
|
996
|
+
sourceFile.forEachDescendant((node) => {
|
|
997
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(node)) {
|
|
998
|
+
const text = node.getText();
|
|
999
|
+
if (text.startsWith("z.coerce.")) {
|
|
1000
|
+
const lineNumber = node.getStartLineNumber();
|
|
1001
|
+
this.warnings.push(
|
|
1002
|
+
`${filePath}:${lineNumber}: z.coerce.* input type changed to 'unknown' in v4. Previously, z.coerce.string() had input type string. This enables more accurate type inference but may affect type guards.`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Check for .deepPartial() which was removed entirely in v4.
|
|
1011
|
+
*/
|
|
1012
|
+
checkDeepPartialRemoval(sourceFile) {
|
|
1013
|
+
const filePath = sourceFile.getFilePath();
|
|
1014
|
+
sourceFile.forEachDescendant((node) => {
|
|
1015
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
1016
|
+
const expression = node.getExpression();
|
|
1017
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "deepPartial") {
|
|
1018
|
+
const lineNumber = node.getStartLineNumber();
|
|
1019
|
+
this.warnings.push(
|
|
1020
|
+
`${filePath}:${lineNumber}: .deepPartial() is removed in v4. This was a long-deprecated anti-pattern. Use recursive partial types or restructure schemas to avoid deep partial requirements.`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Check for .strip() which is deprecated in v4 (it was the default behavior).
|
|
1028
|
+
*/
|
|
1029
|
+
checkStripDeprecation(sourceFile) {
|
|
1030
|
+
const filePath = sourceFile.getFilePath();
|
|
1031
|
+
sourceFile.forEachDescendant((node) => {
|
|
1032
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
1033
|
+
const expression = node.getExpression();
|
|
1034
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression) && expression.getName() === "strip") {
|
|
1035
|
+
const lineNumber = node.getStartLineNumber();
|
|
1036
|
+
this.warnings.push(
|
|
1037
|
+
`${filePath}:${lineNumber}: .strip() is deprecated in v4 (it was the default behavior). Remove .strip() \u2014 z.object() strips unknown keys by default. To convert a strict object back to stripping, use z.object(schema.shape).`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Check for errorMap usage patterns where precedence changed in v4.
|
|
1045
|
+
* In v4, schema-level error maps take precedence over parse-time error maps (inverted from v3).
|
|
1046
|
+
*/
|
|
1047
|
+
checkErrorMapPrecedence(sourceFile) {
|
|
1048
|
+
const filePath = sourceFile.getFilePath();
|
|
1049
|
+
const fullText = sourceFile.getFullText();
|
|
1050
|
+
const hasSchemaErrorMap = fullText.includes("errorMap:") || fullText.includes("error:");
|
|
1051
|
+
const hasParseErrorMap = fullText.includes(".parse(") && fullText.includes("errorMap") || fullText.includes(".safeParse(") && fullText.includes("errorMap");
|
|
1052
|
+
if (hasSchemaErrorMap && hasParseErrorMap) {
|
|
1053
|
+
this.warnings.push(
|
|
1054
|
+
`${filePath}: Error map precedence changed in v4. Schema-level error maps now take precedence over parse-time error maps (inverted from v3). Review error map usage to ensure correct error messages.`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Check for ctx.addIssue() / ctx.addIssues() which are deprecated in v4.
|
|
1060
|
+
*/
|
|
1061
|
+
checkAddIssueMethods(sourceFile) {
|
|
1062
|
+
const filePath = sourceFile.getFilePath();
|
|
1063
|
+
const fullText = sourceFile.getFullText();
|
|
1064
|
+
if (fullText.includes(".addIssue(") || fullText.includes(".addIssues(")) {
|
|
1065
|
+
sourceFile.forEachDescendant((node) => {
|
|
1066
|
+
if (import_ts_morph.Node.isCallExpression(node)) {
|
|
1067
|
+
const expression = node.getExpression();
|
|
1068
|
+
if (import_ts_morph.Node.isPropertyAccessExpression(expression)) {
|
|
1069
|
+
const name = expression.getName();
|
|
1070
|
+
if (name === "addIssue" || name === "addIssues") {
|
|
1071
|
+
const lineNumber = node.getStartLineNumber();
|
|
1072
|
+
this.warnings.push(
|
|
1073
|
+
`${filePath}:${lineNumber}: .${name}() is deprecated in v4. Directly manipulate the issues array instead: ctx.issues.push({...}). Note: .superRefine() callbacks using addIssue should migrate to .check() with issues.push().`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
637
1081
|
};
|
|
638
1082
|
|
|
639
1083
|
// src/handler.ts
|
|
@@ -650,6 +1094,8 @@ function createZodV3ToV4Handler() {
|
|
|
650
1094
|
BEHAVIOR_CHANGES,
|
|
651
1095
|
V3_TO_V4_PATTERNS,
|
|
652
1096
|
ZodV3ToV4Transformer,
|
|
653
|
-
createZodV3ToV4Handler
|
|
1097
|
+
createZodV3ToV4Handler,
|
|
1098
|
+
detectBehavioralPatterns,
|
|
1099
|
+
generateBehaviorTests
|
|
654
1100
|
});
|
|
655
1101
|
//# sourceMappingURL=index.cjs.map
|