From 003d973c1edd40120158aa236641eea2cf8754c7 Mon Sep 17 00:00:00 2001 From: Zack Chapple Date: Tue, 6 Jan 2026 19:37:14 -0500 Subject: [PATCH] feat: add support for Zod v4 while maintaining v3 compatibility This change adds runtime detection to support both Zod v3 and v4 simultaneously, allowing users to upgrade to Zod v4 without breaking existing v3 users. Changes: - Added compatibility helpers that detect Zod version at runtime - Updated z.function() calls to use new v4 API when available - Falls back to v3 API for existing installations - Updated zod dependency to "^3.25.0 || ^4.0.0" in all packages Technical Details: - Detects Zod v4 by checking for '_zod' property on schemas - Uses z.function({ input: [], output: ... }) for v4 - Uses z.function().args(...).returns(...) for v3 - Minimum v3.25.0 required for compatibility layer support Affected packages: - @tanstack/router-generator - @tanstack/router-plugin - @tanstack/start-plugin-core Fixes #6138 Related to #4322, #4092 --- packages/router-generator/package.json | 2 +- packages/router-generator/src/config.ts | 25 ++++++++- packages/router-plugin/package.json | 2 +- packages/router-plugin/src/core/config.ts | 25 ++++++++- packages/start-plugin-core/package.json | 2 +- packages/start-plugin-core/src/schema.ts | 63 +++++++++++++++-------- 6 files changed, 93 insertions(+), 26 deletions(-) diff --git a/packages/router-generator/package.json b/packages/router-generator/package.json index 4d1704e54a5..b58185a4271 100644 --- a/packages/router-generator/package.json +++ b/packages/router-generator/package.json @@ -72,7 +72,7 @@ "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", - "zod": "^3.24.2" + "zod": "^3.25.0 || ^4.0.0" }, "devDependencies": { "@tanstack/react-router": "workspace:*" diff --git a/packages/router-generator/src/config.ts b/packages/router-generator/src/config.ts index 0d1ae473391..31ffbc9068d 100644 --- a/packages/router-generator/src/config.ts +++ b/packages/router-generator/src/config.ts @@ -4,6 +4,29 @@ import { z } from 'zod' import { virtualRootRouteSchema } from './filesystem/virtual/config' import type { GeneratorPlugin } from './plugin/types' +// Helper to create a function schema compatible with both Zod v3 and v4 +function createFunctionSchema() { + // Try Zod v4 syntax first + if (typeof (z as any).function === 'function') { + try { + // Check if this is Zod v4 by testing for the new API + const testSchema = z.string() + if ('_zod' in testSchema) { + // Zod v4: use new function API + return (z as any).function({ + input: [], + output: z.array(z.string()), + }) + } + } catch (e) { + // Fall through to v3 + } + } + + // Zod v3: use old function API + return (z as any).function().returns(z.array(z.string())) +} + export const baseConfigSchema = z.object({ target: z.enum(['react', 'solid', 'vue']).optional().default('react'), virtualRouteConfig: virtualRootRouteSchema.or(z.string()).optional(), @@ -40,7 +63,7 @@ export const configSchema = baseConfigSchema.extend({ routeTreeFileFooter: z .union([ z.array(z.string()).optional().default([]), - z.function().returns(z.array(z.string())), + createFunctionSchema(), ]) .optional(), autoCodeSplitting: z.boolean().optional(), diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json index 37be7ce691f..b6d05b783c5 100644 --- a/packages/router-plugin/package.json +++ b/packages/router-plugin/package.json @@ -117,7 +117,7 @@ "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", - "zod": "^3.24.2" + "zod": "^3.25.0 || ^4.0.0" }, "devDependencies": { "@types/babel__core": "^7.20.5", diff --git a/packages/router-plugin/src/core/config.ts b/packages/router-plugin/src/core/config.ts index f93166d3929..2dba87c01fa 100644 --- a/packages/router-plugin/src/core/config.ts +++ b/packages/router-plugin/src/core/config.ts @@ -10,6 +10,29 @@ import type { } from '@tanstack/router-core' import type { CodeSplitGroupings } from './constants' +// Helper to create a function schema compatible with both Zod v3 and v4 +function createGenericFunctionSchema(): any { + // Try Zod v4 syntax first + if (typeof (z as any).function === 'function') { + try { + // Check if this is Zod v4 by testing for the new API + const testSchema = z.string() + if ('_zod' in testSchema) { + // Zod v4: use new function API with any input/output + return (z as any).function({ + input: [z.any()], + output: z.any(), + }) + } + } catch (e) { + // Fall through to v3 + } + } + + // Zod v3: use old function API + return (z as any).function() +} + export const splitGroupingsSchema = z .array( z.array( @@ -73,7 +96,7 @@ export type CodeSplittingOptions = { } const codeSplittingOptionsSchema = z.object({ - splitBehavior: z.function().optional(), + splitBehavior: createGenericFunctionSchema().optional(), defaultBehavior: splitGroupingsSchema.optional(), deleteNodes: z.array(z.string()).optional(), addHmr: z.boolean().optional().default(true), diff --git a/packages/start-plugin-core/package.json b/packages/start-plugin-core/package.json index 5711e0419c6..e6a6798a65f 100644 --- a/packages/start-plugin-core/package.json +++ b/packages/start-plugin-core/package.json @@ -80,7 +80,7 @@ "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", - "zod": "^3.24.2" + "zod": "^3.25.0 || ^4.0.0" }, "devDependencies": { "@types/babel__code-frame": "^7.0.6", diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index 235796a1290..649b62e29bb 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -3,6 +3,33 @@ import { z } from 'zod' import { configSchema, getConfig } from '@tanstack/router-plugin' import type { TanStackStartVitePluginCoreOptions } from './types' +// Helper to create a function schema compatible with both Zod v3 and v4 +function createFunctionSchema( + args: any, + returns: any, +): any { + // Try Zod v4 syntax first + if (typeof (z as any).function === 'function') { + try { + // Check if this is Zod v4 by testing for the new API + const testSchema = z.string() + if ('_zod' in testSchema) { + // Zod v4: use new function API + return (z as any).function({ + input: Array.isArray(args) ? args : [args], + output: returns, + }) + } + } catch (e) { + // Fall through to v3 + } + } + + // Zod v3: use old function API + const argsArray = Array.isArray(args) ? args : [args] + return (z as any).function().args(...argsArray).returns(returns) +} + const tsrConfig = configSchema .omit({ autoCodeSplitting: true, target: true, verboseFileRoutes: true }) .partial() @@ -94,16 +121,13 @@ const pagePrerenderOptionsSchema = z.object({ crawlLinks: z.boolean().optional(), retryCount: z.number().optional(), retryDelay: z.number().optional(), - onSuccess: z - .function() - .args( - z.object({ - page: pageBaseSchema, - html: z.string(), - }), - ) - .returns(z.any()) - .optional(), + onSuccess: createFunctionSchema( + z.object({ + page: pageBaseSchema, + html: z.string(), + }), + z.any(), + ).optional(), headers: z.record(z.string(), z.string()).optional(), }) @@ -159,16 +183,13 @@ const tanstackStartOptionsSchema = z serverFns: z .object({ base: z.string().optional().default('/_serverFn'), - generateFunctionId: z - .function() - .args( - z.object({ - filename: z.string(), - functionName: z.string(), - }), - ) - .returns(z.string().optional()) - .optional(), + generateFunctionId: createFunctionSchema( + z.object({ + filename: z.string(), + functionName: z.string(), + }), + z.string().optional(), + ).optional(), }) .optional() .default({}), @@ -184,7 +205,7 @@ const tanstackStartOptionsSchema = z .object({ enabled: z.boolean().optional(), concurrency: z.number().optional(), - filter: z.function().args(pageSchema).returns(z.any()).optional(), + filter: createFunctionSchema(pageSchema, z.any()).optional(), failOnError: z.boolean().optional(), autoStaticPathsDiscovery: z.boolean().optional(), maxRedirects: z.number().min(0).optional(),