From 0c9a0612eb8f87eeb1a8e57328956b14c3df2700 Mon Sep 17 00:00:00 2001 From: Laura Koye Date: Fri, 27 Mar 2026 09:24:34 -0500 Subject: [PATCH 1/3] Upgrade zod from v3 to v4 - Replace .merge() with .extend() (deprecated in v4) - Replace .strict() with z.strictObject() (deprecated in v4) - Add explicit key schema to z.record() (single-arg no longer supported) - Make z.undefined() fields optional to preserve v3 behavior --- lib/src/http/variables.ts | 68 +++++++++++++------------------ lib/src/outputs/android.ts | 10 +++-- lib/src/outputs/iosStrings.ts | 10 +++-- lib/src/outputs/iosStringsDict.ts | 10 +++-- lib/src/outputs/json.ts | 32 +++++++++------ lib/src/outputs/jsonICU.ts | 10 +++-- lib/src/services/projectConfig.ts | 8 ++-- package.json | 2 +- yarn.lock | 8 ++-- 9 files changed, 83 insertions(+), 75 deletions(-) diff --git a/lib/src/http/variables.ts b/lib/src/http/variables.ts index ce9c2d4..8bb304f 100644 --- a/lib/src/http/variables.ts +++ b/lib/src/http/variables.ts @@ -9,49 +9,39 @@ const ZBaseVariable = z.object({ name: z.string(), }); -const ZVariableNumber = ZBaseVariable.merge( - z.object({ - type: z.literal("number"), - data: z.object({ - example: z.union([z.number(), z.string()]), - fallback: z.union([z.number(), z.string()]).optional(), - }), - }) -); +const ZVariableNumber = ZBaseVariable.extend({ + type: z.literal("number"), + data: z.object({ + example: z.union([z.number(), z.string()]), + fallback: z.union([z.number(), z.string()]).optional(), + }), +}); -const ZVariableString = ZBaseVariable.merge( - z.object({ - type: z.literal("string"), - data: z.object({ - example: z.string(), - fallback: z.string().optional(), - }), - }) -); +const ZVariableString = ZBaseVariable.extend({ + type: z.literal("string"), + data: z.object({ + example: z.string(), + fallback: z.string().optional(), + }), +}); -const ZVariableHyperlink = ZBaseVariable.merge( - z.object({ - type: z.literal("hyperlink"), - data: z.object({ - text: z.string(), - url: z.string(), - }), - }) -); +const ZVariableHyperlink = ZBaseVariable.extend({ + type: z.literal("hyperlink"), + data: z.object({ + text: z.string(), + url: z.string(), + }), +}); -const ZVariableList = ZBaseVariable.merge( - z.object({ - type: z.literal("list"), - data: z.array(z.string()), - }) -); +const ZVariableList = ZBaseVariable.extend({ + type: z.literal("list"), + data: z.array(z.string()), +}); -const ZVariableMap = ZBaseVariable.merge( - z.object({ - type: z.literal("map"), - data: z.record(z.string()), - }) -); +const ZVariableMap = ZBaseVariable.extend({ + type: z.literal("map"), + data: z.record(z.string(), z.string()), +}); const ZVariable = z.discriminatedUnion("type", [ ZVariableString, diff --git a/lib/src/outputs/android.ts b/lib/src/outputs/android.ts index f1acd7a..6213e7b 100644 --- a/lib/src/outputs/android.ts +++ b/lib/src/outputs/android.ts @@ -1,7 +1,9 @@ import { z } from "zod"; import { ZBaseOutputFilters } from "./shared"; -export const ZAndroidOutput = ZBaseOutputFilters.extend({ - format: z.literal("android"), - framework: z.undefined(), -}).strict(); +export const ZAndroidOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("android"), + framework: z.undefined().optional(), + }).shape +); diff --git a/lib/src/outputs/iosStrings.ts b/lib/src/outputs/iosStrings.ts index 1dd64b4..3050537 100644 --- a/lib/src/outputs/iosStrings.ts +++ b/lib/src/outputs/iosStrings.ts @@ -1,7 +1,9 @@ import { z } from "zod"; import { ZBaseOutputFilters } from "./shared"; -export const ZIOSStringsOutput = ZBaseOutputFilters.extend({ - format: z.literal("ios-strings"), - framework: z.undefined(), -}).strict(); +export const ZIOSStringsOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("ios-strings"), + framework: z.undefined().optional(), + }).shape +); diff --git a/lib/src/outputs/iosStringsDict.ts b/lib/src/outputs/iosStringsDict.ts index 0249382..8a100d5 100644 --- a/lib/src/outputs/iosStringsDict.ts +++ b/lib/src/outputs/iosStringsDict.ts @@ -1,7 +1,9 @@ import { z } from "zod"; import { ZBaseOutputFilters } from "./shared"; -export const ZIOSStringsDictOutput = ZBaseOutputFilters.extend({ - format: z.literal("ios-stringsdict"), - framework: z.undefined(), -}).strict(); +export const ZIOSStringsDictOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("ios-stringsdict"), + framework: z.undefined().optional(), + }).shape +); diff --git a/lib/src/outputs/json.ts b/lib/src/outputs/json.ts index 3bc061c..4aef730 100644 --- a/lib/src/outputs/json.ts +++ b/lib/src/outputs/json.ts @@ -1,20 +1,28 @@ import { z } from "zod"; import { ZBaseOutputFilters } from "./shared"; -const ZBaseJSONOutput = ZBaseOutputFilters.extend({ - format: z.literal("json"), - framework: z.undefined(), -}).strict(); +const ZBaseJSONOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("json"), + framework: z.undefined().optional(), + }).shape +); -const Zi18NextJSONOutput = ZBaseJSONOutput.extend({ - framework: z.literal("i18next"), - type: z.literal("module").or(z.literal("commonjs")).optional(), -}).strict(); +const Zi18NextJSONOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("json"), + framework: z.literal("i18next"), + type: z.literal("module").or(z.literal("commonjs")).optional(), + }).shape +); -const ZVueI18nJSONOutput = ZBaseJSONOutput.extend({ - framework: z.literal("vue-i18n"), - type: z.literal("module").or(z.literal("commonjs")).optional(), -}).strict(); +const ZVueI18nJSONOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("json"), + framework: z.literal("vue-i18n"), + type: z.literal("module").or(z.literal("commonjs")).optional(), + }).shape +); export const ZJSONOutput = z.discriminatedUnion("framework", [ ZBaseJSONOutput, diff --git a/lib/src/outputs/jsonICU.ts b/lib/src/outputs/jsonICU.ts index 5b36bf2..0cb87c5 100644 --- a/lib/src/outputs/jsonICU.ts +++ b/lib/src/outputs/jsonICU.ts @@ -1,7 +1,9 @@ import { z } from "zod"; import { ZBaseOutputFilters } from "./shared"; -export const ZJSONICUOutput = ZBaseOutputFilters.extend({ - format: z.literal("json_icu"), - framework: z.undefined(), -}).strict(); +export const ZJSONICUOutput = z.strictObject( + ZBaseOutputFilters.extend({ + format: z.literal("json_icu"), + framework: z.undefined().optional(), + }).shape +); diff --git a/lib/src/services/projectConfig.ts b/lib/src/services/projectConfig.ts index 2b10549..dd2f16f 100644 --- a/lib/src/services/projectConfig.ts +++ b/lib/src/services/projectConfig.ts @@ -7,9 +7,11 @@ import { ZBaseOutputFilters } from "../outputs/shared"; import { ZOutput } from "../outputs"; import DittoError, { ErrorType } from "../utils/DittoError"; -const ZProjectConfigYAML = ZBaseOutputFilters.extend({ - outputs: z.array(ZOutput), -}).strict(); +const ZProjectConfigYAML = z.strictObject( + ZBaseOutputFilters.extend({ + outputs: z.array(ZOutput), + }).shape +); export type ProjectConfigYAML = z.infer; diff --git a/package.json b/package.json index 6dae159..cc78c94 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "js-yaml": "^4.1.0", "memfs": "^4.7.7", "ora": "^5.0.0", - "zod": "^3.24.2" + "zod": "^4.0.0" }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx,css,json}": "prettier --write" diff --git a/yarn.lock b/yarn.lock index 7094618..8a607e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4695,7 +4695,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@^3.24.2: - version "3.24.2" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3" - integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ== +zod@^4.0.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== From 0740ca7ca0f3ba9ffd72ea29fcfabb94163b21e4 Mon Sep 17 00:00:00 2001 From: Marla Hoggard Date: Tue, 31 Mar 2026 09:30:29 -0400 Subject: [PATCH 2/3] Format errors and package bump --- lib/src/formatters/shared/base.ts | 2 +- lib/src/services/apiToken/promptForApiToken.ts | 2 +- lib/src/services/projectConfig.ts | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/formatters/shared/base.ts b/lib/src/formatters/shared/base.ts index 5ad483a..ed97f00 100644 --- a/lib/src/formatters/shared/base.ts +++ b/lib/src/formatters/shared/base.ts @@ -130,7 +130,7 @@ export default class BaseFormatter { public async format(): Promise { const data = await this.fetchAPIData(); - const files = await this.transformAPIData(data); + const files = this.transformAPIData(data); await this.writeFiles(files); } diff --git a/lib/src/services/apiToken/promptForApiToken.ts b/lib/src/services/apiToken/promptForApiToken.ts index 91da9b6..6144acd 100644 --- a/lib/src/services/apiToken/promptForApiToken.ts +++ b/lib/src/services/apiToken/promptForApiToken.ts @@ -14,11 +14,11 @@ export const validate = async (token: string) => { * @returns The collected token */ export default async function promptForApiToken() { + // @ts-expect-error - Enquirer types are not updated for the validate function const response = await prompt<{ token: string }>({ type: "input", name: "token", message: "What is your API key?", - // @ts-expect-error - Enquirer types are not updated for the validate function validate, }); diff --git a/lib/src/services/projectConfig.ts b/lib/src/services/projectConfig.ts index dd2f16f..04694f3 100644 --- a/lib/src/services/projectConfig.ts +++ b/lib/src/services/projectConfig.ts @@ -65,7 +65,7 @@ function readProjectConfigData( throw new DittoError({ type: ErrorType.ConfigParseError, data: { - formattedError: JSON.stringify(parsedYAML.error.flatten(), null, 2), + formattedError: z.prettifyError(parsedYAML.error), messagePrefix: "There is an error in your project config file.", }, }); diff --git a/package.json b/package.json index cc78c94..f25d01b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dittowords/cli", - "version": "5.5.0", + "version": "5.5.1", "description": "Command Line Interface for Ditto (dittowords.com).", "license": "MIT", "main": "bin/ditto.js", From 88a6bcfb9fafdd79692005518256d7bef2149b2f Mon Sep 17 00:00:00 2001 From: Marla Hoggard Date: Tue, 31 Mar 2026 09:41:25 -0400 Subject: [PATCH 3/3] Move framework undefined to base type, remove from all the others --- lib/src/outputs/android.ts | 1 - lib/src/outputs/iosStrings.ts | 1 - lib/src/outputs/iosStringsDict.ts | 1 - lib/src/outputs/json.ts | 1 - lib/src/outputs/jsonICU.ts | 1 - lib/src/outputs/shared.ts | 1 + 6 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/outputs/android.ts b/lib/src/outputs/android.ts index 6213e7b..c46f2aa 100644 --- a/lib/src/outputs/android.ts +++ b/lib/src/outputs/android.ts @@ -4,6 +4,5 @@ import { ZBaseOutputFilters } from "./shared"; export const ZAndroidOutput = z.strictObject( ZBaseOutputFilters.extend({ format: z.literal("android"), - framework: z.undefined().optional(), }).shape ); diff --git a/lib/src/outputs/iosStrings.ts b/lib/src/outputs/iosStrings.ts index 3050537..673df81 100644 --- a/lib/src/outputs/iosStrings.ts +++ b/lib/src/outputs/iosStrings.ts @@ -4,6 +4,5 @@ import { ZBaseOutputFilters } from "./shared"; export const ZIOSStringsOutput = z.strictObject( ZBaseOutputFilters.extend({ format: z.literal("ios-strings"), - framework: z.undefined().optional(), }).shape ); diff --git a/lib/src/outputs/iosStringsDict.ts b/lib/src/outputs/iosStringsDict.ts index 8a100d5..d7141fd 100644 --- a/lib/src/outputs/iosStringsDict.ts +++ b/lib/src/outputs/iosStringsDict.ts @@ -4,6 +4,5 @@ import { ZBaseOutputFilters } from "./shared"; export const ZIOSStringsDictOutput = z.strictObject( ZBaseOutputFilters.extend({ format: z.literal("ios-stringsdict"), - framework: z.undefined().optional(), }).shape ); diff --git a/lib/src/outputs/json.ts b/lib/src/outputs/json.ts index 4aef730..746dfe2 100644 --- a/lib/src/outputs/json.ts +++ b/lib/src/outputs/json.ts @@ -4,7 +4,6 @@ import { ZBaseOutputFilters } from "./shared"; const ZBaseJSONOutput = z.strictObject( ZBaseOutputFilters.extend({ format: z.literal("json"), - framework: z.undefined().optional(), }).shape ); diff --git a/lib/src/outputs/jsonICU.ts b/lib/src/outputs/jsonICU.ts index 0cb87c5..d8ca45d 100644 --- a/lib/src/outputs/jsonICU.ts +++ b/lib/src/outputs/jsonICU.ts @@ -4,6 +4,5 @@ import { ZBaseOutputFilters } from "./shared"; export const ZJSONICUOutput = z.strictObject( ZBaseOutputFilters.extend({ format: z.literal("json_icu"), - framework: z.undefined().optional(), }).shape ); diff --git a/lib/src/outputs/shared.ts b/lib/src/outputs/shared.ts index 9cec7b8..4084214 100644 --- a/lib/src/outputs/shared.ts +++ b/lib/src/outputs/shared.ts @@ -6,6 +6,7 @@ import { ZTagsFilter, ZTextStatus } from "../http/types"; * They are all optional by default unless otherwise specified in the output config. */ export const ZBaseOutputFilters = z.object({ + framework: z.undefined().optional(), projects: z.array(z.object({ id: z.string() })).optional(), components: z .object({