zod-v3-to-v4 1.9.0 → 1.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/README.md CHANGED
@@ -131,6 +131,8 @@ pnpm playground:interactive
131
131
  <tr>
132
132
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/yann-combarnous"><img src="https://avatars.githubusercontent.com/u/39089766?v=4?s=100" width="100px;" alt="yann-combarnous"/><br /><sub><b>yann-combarnous</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Ayann-combarnous" title="Bug reports">🐛</a></td>
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
+ <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
+ <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>
134
136
  </tr>
135
137
  </tbody>
136
138
  <tfoot>
@@ -1,4 +1,4 @@
1
- import { SyntaxKind } from "ts-morph";
1
+ import { SyntaxKind, } from "ts-morph";
2
2
  export const AstroZodModuleSpecifier = "astro/zod";
3
3
  // https://v6.docs.astro.build/en/guides/upgrade-to/v6/#deprecated-astroschema-and-z-from-astrocontent
4
4
  export const AstroDeprecatedZodModuleSpecifiers = [
@@ -36,3 +36,47 @@ export function collectZodReferences(importDeclarations) {
36
36
  .filter((node) => node !== namedNode);
37
37
  }));
38
38
  }
39
+ /**
40
+ * Collect references to variables that are derived from Zod schemas.
41
+ *
42
+ * For example, given:
43
+ *
44
+ * const baseSchema = z.string().refine(...);
45
+ * const constrainedSchema = baseSchema.refine(...);
46
+ *
47
+ * This function will return references to `baseSchema` (like the one used
48
+ * in `constrainedSchema`), so we can also migrate those.
49
+ */
50
+ export function collectDerivedZodSchemaReferences(zodReferences) {
51
+ const derivedReferences = [];
52
+ const visited = new Set();
53
+ function collectFromReferences(refs) {
54
+ for (const ref of refs) {
55
+ // Find the variable declaration that contains this Zod reference
56
+ const variableDeclaration = ref.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
57
+ if (!variableDeclaration) {
58
+ continue;
59
+ }
60
+ const variableName = variableDeclaration.getName();
61
+ if (visited.has(variableName)) {
62
+ continue;
63
+ }
64
+ visited.add(variableName);
65
+ // Get the identifier node for the variable name
66
+ const nameNode = variableDeclaration.getNameNode();
67
+ if (!nameNode.isKind(SyntaxKind.Identifier)) {
68
+ continue;
69
+ }
70
+ // Find all references to this variable (excluding the declaration itself)
71
+ const variableReferences = nameNode
72
+ .findReferencesAsNodes()
73
+ .filter((node) => node !== nameNode)
74
+ .filter((node) => node.isKind(SyntaxKind.Identifier));
75
+ derivedReferences.push(...variableReferences);
76
+ // Recursively collect references from these derived schemas
77
+ collectFromReferences(variableReferences);
78
+ }
79
+ }
80
+ collectFromReferences(zodReferences);
81
+ return derivedReferences;
82
+ }
@@ -123,7 +123,10 @@ export function convertZArrayPatternsToTopLevelApi(node, zodName) {
123
123
  getCallExpressionsToConvert(node, {
124
124
  oldName,
125
125
  names,
126
- }).forEach((callExpression) => {
126
+ })
127
+ // Start from the deepest, otherwise we can't get info from removed nodes
128
+ .reverse()
129
+ .forEach((callExpression) => {
127
130
  // Remove deprecated names from the chain
128
131
  callExpression
129
132
  .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
@@ -187,6 +187,13 @@ export function convertZodErrorAddIssueToDirectPushes(sourceFile, zodName) {
187
187
  : argument.getText();
188
188
  expression.replaceWithText(`${caller}.issues.push(${newArgument})`);
189
189
  });
190
+ sourceFile
191
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
192
+ .filter((expression) => isZodReference(zodName, ["ZodType", "ZodError"], expression))
193
+ .filter((expression) => expression.getName() === "errors")
194
+ .forEach((expression) => {
195
+ expression.getNameNode().replaceWithText("issues");
196
+ });
190
197
  }
191
198
  function convertZodIssueToV4(node) {
192
199
  if (node.isKind(SyntaxKind.ArrayLiteralExpression)) {
package/dist/index.js CHANGED
@@ -2,8 +2,10 @@
2
2
  import { cancel, confirm, intro, isCancel, log, outro, progress, text, } from "@clack/prompts";
3
3
  import { exec } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
+ import * as path from "node:path";
5
6
  import { promisify } from "node:util";
6
7
  import { Project } from "ts-morph";
8
+ import z from "zod";
7
9
  import { migrateZodV3ToV4 } from "./migrate.js";
8
10
  const execAsync = promisify(exec);
9
11
  intro(`đŸ—ī¸ Let's migrate Zod from v3 to v4`);
@@ -56,7 +58,26 @@ if (isCancel(tsConfigFilePath)) {
56
58
  await runMigration(tsConfigFilePath);
57
59
  async function runMigration(tsConfigFilePath) {
58
60
  const project = new Project({ tsConfigFilePath });
59
- const filesToProcess = project.getSourceFiles();
61
+ let filesToProcess = project.getSourceFiles();
62
+ const references = getProjectReferences(tsConfigFilePath);
63
+ // If we can only find refs, we are probably in a monorepo setup.
64
+ // Ask the user if they want to migrate all the references.
65
+ const isMonorepoRoot = filesToProcess.length === 0 && references.length > 0;
66
+ if (isMonorepoRoot) {
67
+ const shouldFollowRefs = await confirm({
68
+ message: `Found ${references.length} project reference(s). Migrate all referenced packages?`,
69
+ });
70
+ if (isCancel(shouldFollowRefs)) {
71
+ cancel("Migration cancelled.");
72
+ process.exit(0);
73
+ }
74
+ if (shouldFollowRefs) {
75
+ for (const refPath of references) {
76
+ project.addSourceFilesFromTsConfig(refPath);
77
+ }
78
+ filesToProcess = project.getSourceFiles();
79
+ }
80
+ }
60
81
  let processedFilesCount = 0;
61
82
  const progressBar = progress({ max: filesToProcess.length });
62
83
  progressBar.start("Processing files...");
@@ -85,6 +106,46 @@ async function runMigration(tsConfigFilePath) {
85
106
 
86
107
  â„šī¸ If the migration missed something or did something wrong, please report it at https://github.com/nicoespeon/zod-v3-to-v4/issues`);
87
108
  }
109
+ function getProjectReferences(tsConfigFilePath) {
110
+ // Use Zod to parse the tsconfig.json file for interesting fields.
111
+ const tsConfigWithRefs = z
112
+ .object({
113
+ references: z
114
+ .array(z.object({
115
+ path: z.string().nullable(),
116
+ }))
117
+ .default([]),
118
+ })
119
+ .passthrough();
120
+ try {
121
+ const content = fs.readFileSync(tsConfigFilePath, "utf-8");
122
+ const config = tsConfigWithRefs.parse(JSON.parse(content));
123
+ const tsConfigDir = path.dirname(tsConfigFilePath);
124
+ return config.references
125
+ .map((ref) => {
126
+ if (!ref.path)
127
+ return null;
128
+ const refDir = path.resolve(tsConfigDir, ref.path);
129
+ // Scenario: `ref.path` points directly to the tsconfig file
130
+ if (ref.path.endsWith(".json") && fs.existsSync(refDir)) {
131
+ return refDir;
132
+ }
133
+ // Otherwise, look for `tsconfig.json` in the referenced directory.
134
+ // We don't support other names yet. Workaround is to reference
135
+ // the tsconfig path or directly run the codemod with the path
136
+ // to the tsconfig you want to migrate.
137
+ const refTsConfig = path.join(refDir, "tsconfig.json");
138
+ if (fs.existsSync(refTsConfig)) {
139
+ return refTsConfig;
140
+ }
141
+ return null;
142
+ })
143
+ .filter((p) => p !== null);
144
+ }
145
+ catch (error) {
146
+ return [];
147
+ }
148
+ }
88
149
  function validateTsConfigPath(path) {
89
150
  if (!path.endsWith(".json")) {
90
151
  return {
package/dist/migrate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SyntaxKind } from "ts-morph";
2
2
  import { findRootExpression } from "./ast.js";
3
- import { AstroZodModuleSpecifiers, collectZodImportDeclarations, collectZodReferences, getZodName, } from "./collect-imports.js";
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
6
  import { convertDeprecatedErrorKeysToErrorFunction, convertErrorMapToErrorFunction, convertMessageKeyToError, convertZodErrorAddIssueToDirectPushes, convertZodErrorToTreeifyError, } from "./convert-zod-errors.js";
@@ -26,8 +26,9 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
26
26
  });
27
27
  // Collect references before modifying imports
28
28
  const zodReferences = collectZodReferences(importDeclarations);
29
+ const derivedReferences = collectDerivedZodSchemaReferences(zodReferences);
29
30
  replaceDeletedTypes(importDeclarations, zodReferences);
30
- zodReferences.forEach((node) => {
31
+ [...zodReferences, ...derivedReferences].forEach((node) => {
31
32
  if (node.wasForgotten()) {
32
33
  return;
33
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-v3-to-v4",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Migrate Zod from v3 to v4",
5
5
  "keywords": [
6
6
  "zod",
@@ -33,6 +33,7 @@
33
33
  "format-check": "prettier . --list-different",
34
34
  "playground": "pnpm playground:interactive playground/tsconfig.json",
35
35
  "playground:interactive": "node --experimental-strip-types --disable-warning=ExperimentalWarning src/index.ts",
36
+ "playground:monorepo": "pnpm playground:interactive playground-monorepo/tsconfig.json",
36
37
  "prepare": "husky",
37
38
  "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
38
39
  "test": "vitest run",