Unverified 提交 f0431145 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Migrate to Zod v4 (#2415)

## Summary - Upgraded zod package from ^3.25.76 to ^4.3.6 - Updated z.record() calls to use two arguments (key schema, value schema) as required by Zod v4 - Changed ZodError.errors to ZodError.issues (API rename in v4) - Fixed type inference issues with complex generics in createStreamClient ## Test plan - [x] TypeScript type-checking passes (`npm run ts`) - [x] Linting passes (`npm run lint`) - [x] Unit tests pass (661/661 tests) - [x] E2E tests pass (195/213 tests - 12 failures are pre-existing flaky tests unrelated to Zod) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2415"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because this upgrades a core validation library and adjusts IPC schema/handler typings; behavioral changes in Zod parsing/record typing could surface at runtime if any edge cases differ. > > **Overview** > Upgrades `zod` from v3 to v4 (and bumps `zod-to-json-schema`), updating the lockfile and keeping a nested v3 `zod` for `@modelcontextprotocol/sdk`. > > Updates IPC validation code to match Zod v4: switches `ZodError.errors` to `ZodError.issues`, updates all `z.record(...)` usages to the new `z.record(keySchema, valueSchema)` signature, and applies a few TypeScript workarounds (notably in `createStreamClient` key handling and `registerTypedHandlers` casting) to avoid inference breaks under v4. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2e1becc1b56c343ee969e86de2146e36d255e7ae. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Migrated the codebase to Zod v4. Updated record schemas and error handling, plus small type fixes in IPC; no runtime changes expected. - **Dependencies** - Upgraded zod to ^4.3.6. - Bumped zod-to-json-schema to 3.25.1. - **Refactors** - Replaced z.record(value) with z.record(key, value) across schemas. - Switched ZodError.errors to ZodError.issues in handlers. - Improved createStreamClient key typing (string | number) and used safer Record<string, unknown> casts. - Added an explicit type assertion in registerTypedHandlers for handler registration. <sup>Written for commit 15413b80916c6816f7c24a59815663f502ac9e3d. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatarclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
上级 669ac72f
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"update-electron-app": "^3.1.2", "update-electron-app": "^3.1.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^3.25.76" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.11.1", "@electron-forge/cli": "^7.11.1",
...@@ -4328,6 +4328,15 @@ ...@@ -4328,6 +4328,15 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@monaco-editor/loader": { "node_modules/@monaco-editor/loader": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
...@@ -11685,17 +11694,6 @@ ...@@ -11685,17 +11694,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
...@@ -23667,21 +23665,21 @@ ...@@ -23667,21 +23665,21 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.76", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
}, },
"node_modules/zod-to-json-schema": { "node_modules/zod-to-json-schema": {
"version": "3.24.6", "version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"zod": "^3.24.1" "zod": "^3.25 || ^4"
} }
}, },
"node_modules/zwitch": { "node_modules/zwitch": {
......
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"update-electron-app": "^3.1.2", "update-electron-app": "^3.1.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^3.25.76" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.11.1", "@electron-forge/cli": "^7.11.1",
......
...@@ -268,7 +268,10 @@ export function createStreamClient< ...@@ -268,7 +268,10 @@ export function createStreamClient<
const getIpcRenderer = () => (window as any).electron?.ipcRenderer; const getIpcRenderer = () => (window as any).electron?.ipcRenderer;
type Input = z.infer<TInput>; type Input = z.infer<TInput>;
type KeyValue = Input[TKey]; // Use string | number for KeyValue to support common key types while
// maintaining better type safety than unknown. TypeScript cannot infer
// the exact key type from TInput[TKey] due to Zod v4 type system limitations.
type KeyValue = string | number;
const streams = new Map< const streams = new Map<
KeyValue, KeyValue,
...@@ -290,7 +293,9 @@ export function createStreamClient< ...@@ -290,7 +293,9 @@ export function createStreamClient<
ipcRenderer.on(contract.events.chunk.channel, (data: unknown) => { ipcRenderer.on(contract.events.chunk.channel, (data: unknown) => {
const parsed = contract.events.chunk.payload.safeParse(data); const parsed = contract.events.chunk.payload.safeParse(data);
if (parsed.success) { if (parsed.success) {
const key = (parsed.data as any)[contract.keyField]; const key = (parsed.data as Record<string, unknown>)[
contract.keyField
] as KeyValue;
streams.get(key)?.onChunk(parsed.data); streams.get(key)?.onChunk(parsed.data);
} }
}); });
...@@ -298,7 +303,9 @@ export function createStreamClient< ...@@ -298,7 +303,9 @@ export function createStreamClient<
ipcRenderer.on(contract.events.end.channel, (data: unknown) => { ipcRenderer.on(contract.events.end.channel, (data: unknown) => {
const parsed = contract.events.end.payload.safeParse(data); const parsed = contract.events.end.payload.safeParse(data);
if (parsed.success) { if (parsed.success) {
const key = (parsed.data as any)[contract.keyField]; const key = (parsed.data as Record<string, unknown>)[
contract.keyField
] as KeyValue;
streams.get(key)?.onEnd(parsed.data); streams.get(key)?.onEnd(parsed.data);
streams.delete(key); streams.delete(key);
} }
...@@ -307,7 +314,9 @@ export function createStreamClient< ...@@ -307,7 +314,9 @@ export function createStreamClient<
ipcRenderer.on(contract.events.error.channel, (data: unknown) => { ipcRenderer.on(contract.events.error.channel, (data: unknown) => {
const parsed = contract.events.error.payload.safeParse(data); const parsed = contract.events.error.payload.safeParse(data);
if (parsed.success) { if (parsed.success) {
const key = (parsed.data as any)[contract.keyField]; const key = (parsed.data as Record<string, unknown>)[
contract.keyField
] as KeyValue;
streams.get(key)?.onError(parsed.data); streams.get(key)?.onError(parsed.data);
streams.delete(key); streams.delete(key);
} }
...@@ -333,13 +342,17 @@ export function createStreamClient< ...@@ -333,13 +342,17 @@ export function createStreamClient<
const ipcRenderer = getIpcRenderer(); const ipcRenderer = getIpcRenderer();
if (!ipcRenderer) { if (!ipcRenderer) {
callbacks.onError({ callbacks.onError({
[contract.keyField]: input[contract.keyField as keyof Input], [contract.keyField]: (input as Record<string, unknown>)[
contract.keyField
],
error: "IPC renderer not available", error: "IPC renderer not available",
} as any); } as any);
return; return;
} }
const key = input[contract.keyField as keyof Input] as KeyValue; const key = (input as Record<string, unknown>)[
contract.keyField
] as KeyValue;
streams.set(key, callbacks); streams.set(key, callbacks);
ipcRenderer.invoke(contract.channel, input).catch((err: Error) => { ipcRenderer.invoke(contract.channel, input).catch((err: Error) => {
......
...@@ -31,7 +31,7 @@ export function createTypedHandler< ...@@ -31,7 +31,7 @@ export function createTypedHandler<
// Runtime validation of input // Runtime validation of input
const parsed = contract.input.safeParse(rawInput); const parsed = contract.input.safeParse(rawInput);
if (!parsed.success) { if (!parsed.success) {
const errorMessage = parsed.error.errors const errorMessage = parsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`) .map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; "); .join("; ");
throw new Error(`[${contract.channel}] Invalid input: ${errorMessage}`); throw new Error(`[${contract.channel}] Invalid input: ${errorMessage}`);
...@@ -43,7 +43,7 @@ export function createTypedHandler< ...@@ -43,7 +43,7 @@ export function createTypedHandler<
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
const outputParsed = contract.output.safeParse(result); const outputParsed = contract.output.safeParse(result);
if (!outputParsed.success) { if (!outputParsed.success) {
const errorMessage = outputParsed.error.errors const errorMessage = outputParsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`) .map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; "); .join("; ");
console.error( console.error(
...@@ -88,7 +88,7 @@ export function createLoggedTypedHandler(logger: { ...@@ -88,7 +88,7 @@ export function createLoggedTypedHandler(logger: {
// Runtime validation of input // Runtime validation of input
const parsed = contract.input.safeParse(rawInput); const parsed = contract.input.safeParse(rawInput);
if (!parsed.success) { if (!parsed.success) {
const errorMessage = parsed.error.errors const errorMessage = parsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`) .map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; "); .join("; ");
const error = new Error( const error = new Error(
...@@ -106,7 +106,7 @@ export function createLoggedTypedHandler(logger: { ...@@ -106,7 +106,7 @@ export function createLoggedTypedHandler(logger: {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
const outputParsed = contract.output.safeParse(result); const outputParsed = contract.output.safeParse(result);
if (!outputParsed.success) { if (!outputParsed.success) {
const errorMessage = outputParsed.error.errors const errorMessage = outputParsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`) .map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; "); .join("; ");
console.error( console.error(
......
...@@ -93,7 +93,7 @@ export const languageModelContracts = { ...@@ -93,7 +93,7 @@ export const languageModelContracts = {
getModelsByProviders: defineContract({ getModelsByProviders: defineContract({
channel: "get-language-models-by-providers", channel: "get-language-models-by-providers",
input: z.void(), input: z.void(),
output: z.record(z.array(LanguageModelSchema)), output: z.record(z.string(), z.array(LanguageModelSchema)),
}), }),
createCustomProvider: defineContract({ createCustomProvider: defineContract({
......
...@@ -19,8 +19,8 @@ export const McpServerSchema = z.object({ ...@@ -19,8 +19,8 @@ export const McpServerSchema = z.object({
transport: McpTransportEnum, transport: McpTransportEnum,
command: z.string().nullable(), command: z.string().nullable(),
args: z.array(z.string()).nullable(), args: z.array(z.string()).nullable(),
envJson: z.record(z.string()).nullable(), envJson: z.record(z.string(), z.string()).nullable(),
headersJson: z.record(z.string()).nullable(), headersJson: z.record(z.string(), z.string()).nullable(),
url: z.string().nullable(), url: z.string().nullable(),
enabled: z.boolean(), enabled: z.boolean(),
createdAt: z.date(), createdAt: z.date(),
...@@ -38,11 +38,11 @@ export const CreateMcpServerSchema = z.object({ ...@@ -38,11 +38,11 @@ export const CreateMcpServerSchema = z.object({
.nullable() .nullable()
.optional(), .optional(),
envJson: z envJson: z
.union([z.record(z.string()), z.string()]) .union([z.record(z.string(), z.string()), z.string()])
.nullable() .nullable()
.optional(), .optional(),
headersJson: z headersJson: z
.union([z.record(z.string()), z.string()]) .union([z.record(z.string(), z.string()), z.string()])
.nullable() .nullable()
.optional(), .optional(),
url: z.string().nullable().optional(), url: z.string().nullable().optional(),
...@@ -58,8 +58,10 @@ export const McpServerUpdateSchema = z.object({ ...@@ -58,8 +58,10 @@ export const McpServerUpdateSchema = z.object({
command: z.string().optional(), command: z.string().optional(),
args: z.string().optional(), args: z.string().optional(),
cwd: z.string().optional(), cwd: z.string().optional(),
envJson: z.union([z.record(z.string()), z.string()]).optional(), envJson: z.union([z.record(z.string(), z.string()), z.string()]).optional(),
headersJson: z.union([z.record(z.string()), z.string()]).optional(), headersJson: z
.union([z.record(z.string(), z.string()), z.string()])
.optional(),
url: z.string().optional(), url: z.string().optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
}); });
......
...@@ -117,7 +117,7 @@ export const miscContracts = { ...@@ -117,7 +117,7 @@ export const miscContracts = {
getEnvVars: defineContract({ getEnvVars: defineContract({
channel: "get-env-vars", channel: "get-env-vars",
input: z.void(), input: z.void(),
output: z.record(z.string().optional()), output: z.record(z.string(), z.string().optional()),
}), }),
// App-specific env vars // App-specific env vars
......
...@@ -81,7 +81,7 @@ export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>; ...@@ -81,7 +81,7 @@ export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>;
export const TelemetryEventPayloadSchema = z.object({ export const TelemetryEventPayloadSchema = z.object({
eventName: z.string(), eventName: z.string(),
properties: z.record(z.any()).optional(), properties: z.record(z.string(), z.any()).optional(),
}); });
export type TelemetryEventPayload = z.infer<typeof TelemetryEventPayloadSchema>; export type TelemetryEventPayload = z.infer<typeof TelemetryEventPayloadSchema>;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论