#!/usr/bin/env node /** * Convert Zod 3 chained format calls to the Zod 4 top-level API. * Idempotent: re-running on an already-converted file is a no-op. * * Before: z.string().email() * After: z.email() * * Mirrors https://zod.dev/v4/changelog and the react-doctor rule * `react-doctor/zod-v4-prefer-top-level-string-formats`. */ const fs = require("node:fs"); const path = require("node:path"); const SIMPLE_METHODS = [ "email", "url", "uuid", "cuid", "cuid2", "nanoid", "ulid", "emoji", "jwt", ]; const SIMPLE_REGEX = new RegExp( String.raw`\b(z|zod|schema)(\.string\(\))\.(${SIMPLE_METHODS.join("|")})\(`, "g", ); const ISO_REGEX = new RegExp( String.raw`\b(z|zod|schema)(\.string\(\))\.(${"datetime|date|time|ip|ipv4|ipv6|cidr|cidrv4|cidrv6"})\(`, "g", ); function patch(src) { let out = src; // email/url/uuid/etc — drop the .string() wrapper out = out.replace(SIMPLE_REGEX, (_, z, _str, method) => `${z}.${method}(`); // iso.* — wrap the leaf call in .iso.(...) out = out.replace(ISO_REGEX, (_, z, _str, method) => `${z}.iso.${method}(`); return out === src ? null : out; } function processFile(filePath) { const abs = path.resolve(filePath); if (!fs.existsSync(abs)) { console.error(`skip (not found): ${abs}`); return 0; } const original = fs.readFileSync(abs, "utf8"); if (!/from\s+["']zod["']/.test(original)) { return 0; } const patched = patch(original); if (patched) { fs.writeFileSync(abs, patched, "utf8"); console.log(`patched: ${abs}`); return 1; } return 0; } const files = process.argv.slice(2); if (files.length === 0) { console.error("usage: node scripts/fix-zod-format.js ..."); process.exit(2); } let n = 0; for (const f of files) { n += processFile(f); } console.log(`done — ${n} files patched`);