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 @@
"tw-animate-css": "^1.2.5",
"update-electron-app": "^3.1.2",
"uuid": "^11.1.0",
"zod": "^3.25.76"
"zod": "^4.3.6"
},
"devDependencies": {
"@electron-forge/cli": "^7.11.1",
......@@ -4328,6 +4328,15 @@
"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": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
......@@ -11685,17 +11694,6 @@
"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": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
......@@ -23667,21 +23665,21 @@
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
"version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
"zod": "^3.25 || ^4"
}
},
"node_modules/zwitch": {
......
......@@ -135,7 +135,7 @@
"tw-animate-css": "^1.2.5",
"update-electron-app": "^3.1.2",
"uuid": "^11.1.0",
"zod": "^3.25.76"
"zod": "^4.3.6"
},
"devDependencies": {
"@electron-forge/cli": "^7.11.1",
......
......@@ -268,7 +268,10 @@ export function createStreamClient<
const getIpcRenderer = () => (window as any).electron?.ipcRenderer;
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<
KeyValue,
......@@ -290,7 +293,9 @@ export function createStreamClient<
ipcRenderer.on(contract.events.chunk.channel, (data: unknown) => {
const parsed = contract.events.chunk.payload.safeParse(data);
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);
}
});
......@@ -298,7 +303,9 @@ export function createStreamClient<
ipcRenderer.on(contract.events.end.channel, (data: unknown) => {
const parsed = contract.events.end.payload.safeParse(data);
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.delete(key);
}
......@@ -307,7 +314,9 @@ export function createStreamClient<
ipcRenderer.on(contract.events.error.channel, (data: unknown) => {
const parsed = contract.events.error.payload.safeParse(data);
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.delete(key);
}
......@@ -333,13 +342,17 @@ export function createStreamClient<
const ipcRenderer = getIpcRenderer();
if (!ipcRenderer) {
callbacks.onError({
[contract.keyField]: input[contract.keyField as keyof Input],
[contract.keyField]: (input as Record<string, unknown>)[
contract.keyField
],
error: "IPC renderer not available",
} as any);
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);
ipcRenderer.invoke(contract.channel, input).catch((err: Error) => {
......
......@@ -31,7 +31,7 @@ export function createTypedHandler<
// Runtime validation of input
const parsed = contract.input.safeParse(rawInput);
if (!parsed.success) {
const errorMessage = parsed.error.errors
const errorMessage = parsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; ");
throw new Error(`[${contract.channel}] Invalid input: ${errorMessage}`);
......@@ -43,7 +43,7 @@ export function createTypedHandler<
if (process.env.NODE_ENV === "development") {
const outputParsed = contract.output.safeParse(result);
if (!outputParsed.success) {
const errorMessage = outputParsed.error.errors
const errorMessage = outputParsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; ");
console.error(
......@@ -88,7 +88,7 @@ export function createLoggedTypedHandler(logger: {
// Runtime validation of input
const parsed = contract.input.safeParse(rawInput);
if (!parsed.success) {
const errorMessage = parsed.error.errors
const errorMessage = parsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; ");
const error = new Error(
......@@ -106,7 +106,7 @@ export function createLoggedTypedHandler(logger: {
if (process.env.NODE_ENV === "development") {
const outputParsed = contract.output.safeParse(result);
if (!outputParsed.success) {
const errorMessage = outputParsed.error.errors
const errorMessage = outputParsed.error.issues
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join("; ");
console.error(
......
......@@ -93,7 +93,7 @@ export const languageModelContracts = {
getModelsByProviders: defineContract({
channel: "get-language-models-by-providers",
input: z.void(),
output: z.record(z.array(LanguageModelSchema)),
output: z.record(z.string(), z.array(LanguageModelSchema)),
}),
createCustomProvider: defineContract({
......
......@@ -19,8 +19,8 @@ export const McpServerSchema = z.object({
transport: McpTransportEnum,
command: z.string().nullable(),
args: z.array(z.string()).nullable(),
envJson: z.record(z.string()).nullable(),
headersJson: z.record(z.string()).nullable(),
envJson: z.record(z.string(), z.string()).nullable(),
headersJson: z.record(z.string(), z.string()).nullable(),
url: z.string().nullable(),
enabled: z.boolean(),
createdAt: z.date(),
......@@ -38,11 +38,11 @@ export const CreateMcpServerSchema = z.object({
.nullable()
.optional(),
envJson: z
.union([z.record(z.string()), z.string()])
.union([z.record(z.string(), z.string()), z.string()])
.nullable()
.optional(),
headersJson: z
.union([z.record(z.string()), z.string()])
.union([z.record(z.string(), z.string()), z.string()])
.nullable()
.optional(),
url: z.string().nullable().optional(),
......@@ -58,8 +58,10 @@ export const McpServerUpdateSchema = z.object({
command: z.string().optional(),
args: z.string().optional(),
cwd: z.string().optional(),
envJson: z.union([z.record(z.string()), z.string()]).optional(),
headersJson: 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()), z.string()])
.optional(),
url: z.string().optional(),
enabled: z.boolean().optional(),
});
......
......@@ -117,7 +117,7 @@ export const miscContracts = {
getEnvVars: defineContract({
channel: "get-env-vars",
input: z.void(),
output: z.record(z.string().optional()),
output: z.record(z.string(), z.string().optional()),
}),
// App-specific env vars
......
......@@ -81,7 +81,7 @@ export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>;
export const TelemetryEventPayloadSchema = z.object({
eventName: z.string(),
properties: z.record(z.any()).optional(),
properties: z.record(z.string(), z.any()).optional(),
});
export type TelemetryEventPayload = z.infer<typeof TelemetryEventPayloadSchema>;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论