From d095adf8b6f08c44b4b5b495b0179aafeb1e16f9 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Sat, 22 Aug 2026 23:04:30 +0100 Subject: [PATCH 1/2] feat(examples): add TanStack Start example --- .github/dependabot.yml | 5 + .github/workflows/validate.yml | 2 + .oxfmtrc.json | 3 +- examples/tanstack-start/.gitignore | 6 + examples/tanstack-start/README.md | 16 + examples/tanstack-start/package.json | 43 ++ examples/tanstack-start/playwright.config.ts | 32 + examples/tanstack-start/src/routeTree.gen.ts | 177 ++++++ examples/tanstack-start/src/router.tsx | 17 + examples/tanstack-start/src/routes/__root.tsx | 70 +++ .../tanstack-start/src/routes/file-upload.tsx | 90 +++ examples/tanstack-start/src/routes/index.tsx | 36 ++ examples/tanstack-start/src/routes/login.tsx | 111 ++++ .../src/routes/signup-async-schema.tsx | 163 +++++ examples/tanstack-start/src/routes/signup.tsx | 166 +++++ examples/tanstack-start/src/routes/todos.tsx | 192 ++++++ examples/tanstack-start/src/store.server.ts | 27 + examples/tanstack-start/src/styles.css | 27 + examples/tanstack-start/tests/index.test.ts | 191 ++++++ examples/tanstack-start/tsconfig.json | 23 + examples/tanstack-start/tsr.config.json | 4 + examples/tanstack-start/vite.config.ts | 8 + pnpm-lock.yaml | 594 ++++++++++++++++++ 23 files changed, 2002 insertions(+), 1 deletion(-) create mode 100644 examples/tanstack-start/.gitignore create mode 100644 examples/tanstack-start/README.md create mode 100644 examples/tanstack-start/package.json create mode 100644 examples/tanstack-start/playwright.config.ts create mode 100644 examples/tanstack-start/src/routeTree.gen.ts create mode 100644 examples/tanstack-start/src/router.tsx create mode 100644 examples/tanstack-start/src/routes/__root.tsx create mode 100644 examples/tanstack-start/src/routes/file-upload.tsx create mode 100644 examples/tanstack-start/src/routes/index.tsx create mode 100644 examples/tanstack-start/src/routes/login.tsx create mode 100644 examples/tanstack-start/src/routes/signup-async-schema.tsx create mode 100644 examples/tanstack-start/src/routes/signup.tsx create mode 100644 examples/tanstack-start/src/routes/todos.tsx create mode 100644 examples/tanstack-start/src/store.server.ts create mode 100644 examples/tanstack-start/src/styles.css create mode 100644 examples/tanstack-start/tests/index.test.ts create mode 100644 examples/tanstack-start/tsconfig.json create mode 100644 examples/tanstack-start/tsr.config.json create mode 100644 examples/tanstack-start/vite.config.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f041dab4d..f0fdaa45e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,6 +44,11 @@ updates: - '@react-router/dev' - '@react-router/node' - '@react-router/serve' + tanstack: + patterns: + - '@tanstack/react-router' + - '@tanstack/react-start' + - '@tanstack/router-cli' remix: patterns: - '@remix-run/cloudflare' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0d5745275..cbbbb1509 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -92,6 +92,7 @@ jobs: run: | pnpm --filter nextjs-example exec next typegen pnpm --filter react-router-example exec react-router typegen + pnpm --filter tanstack-start-example run generate-routes - name: Typecheck examples # Astro supports TypeScript 5 only and the legacy Remix example will be removed. env: @@ -109,6 +110,7 @@ jobs: examples/react-router \ examples/react-spa \ examples/shadcn-ui \ + examples/tanstack-start \ --pretty false epicstack: diff --git a/.oxfmtrc.json b/.oxfmtrc.json index e98c6b790..941b6b6e3 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -33,7 +33,8 @@ "!rollup.config.js", "/packages/**/*.js", "/packages/**/*.mjs", - "/guide/functions/" + "/guide/functions/", + "/examples/tanstack-start/src/routeTree.gen.ts" ], "overrides": [ { diff --git a/examples/tanstack-start/.gitignore b/examples/tanstack-start/.gitignore new file mode 100644 index 000000000..ed09b4214 --- /dev/null +++ b/examples/tanstack-start/.gitignore @@ -0,0 +1,6 @@ +node_modules +.output +.tanstack +dist +/playwright-report +/test-results diff --git a/examples/tanstack-start/README.md b/examples/tanstack-start/README.md new file mode 100644 index 000000000..3ce55cdb3 --- /dev/null +++ b/examples/tanstack-start/README.md @@ -0,0 +1,16 @@ +# TanStack Start Example + +This example demonstrates how to combine Conform with TanStack Start server +functions: + +- [Basic form with manual validation](./src/routes/login.tsx) +- [Async validation](./src/routes/signup.tsx) ([with an async schema](./src/routes/signup-async-schema.tsx)) +- [Dynamic form with data persistence](./src/routes/todos.tsx) +- [File uploads](./src/routes/file-upload.tsx) with `FormData` and native `File` values + +Each route passes Conform's `FormData` to a TanStack Start server function with +`useServerFn`. The server function performs trusted validation and the mutation, +then its Conform submission report is applied back to the form. Redirects are +handled by `useServerFn`. + +Try it out on [StackBlitz](https://stackblitz.com/github/edmundhung/conform/tree/main/examples/tanstack-start). diff --git a/examples/tanstack-start/package.json b/examples/tanstack-start/package.json new file mode 100644 index 000000000..dafc2a217 --- /dev/null +++ b/examples/tanstack-start/package.json @@ -0,0 +1,43 @@ +{ + "name": "tanstack-start-example", + "private": true, + "type": "module", + "imports": { + "#/*": "./src/*" + }, + "scripts": { + "build": "vite build", + "dev": "vite dev --port 3000", + "generate-routes": "tsr generate", + "preview": "vite preview", + "test": "playwright test", + "typecheck": "tsr generate && tsc" + }, + "dependencies": { + "@conform-to/react": "1.21.1", + "@conform-to/zod": "1.21.1", + "@tanstack/react-router": "1.170.31", + "@tanstack/react-start": "1.168.48", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "zod": "^4.4.3" + }, + "devDependencies": { + "@playwright/test": "1.62.1", + "@tanstack/router-cli": "1.167.32", + "@types/node": "^22", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.1.0", + "typescript": "^5.9.3", + "vite": "^8.2.1" + }, + "dependenciesMeta": { + "@conform-to/react": { + "injected": true + }, + "@conform-to/zod": { + "injected": true + } + } +} diff --git a/examples/tanstack-start/playwright.config.ts b/examples/tanstack-start/playwright.config.ts new file mode 100644 index 000000000..6f31f4fad --- /dev/null +++ b/examples/tanstack-start/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: 'tests', + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'github' : undefined, + use: { + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: devices['Desktop Chrome'], + }, + { + name: 'firefox', + use: devices['Desktop Firefox'], + }, + { + name: 'webkit', + use: devices['Desktop Safari'], + }, + ], + webServer: { + command: 'pnpm run build && pnpm run preview --port 5681', + port: 5681, + reuseExistingServer: !process.env.CI, + timeout: 30000, + }, +}); diff --git a/examples/tanstack-start/src/routeTree.gen.ts b/examples/tanstack-start/src/routeTree.gen.ts new file mode 100644 index 000000000..bf73853a4 --- /dev/null +++ b/examples/tanstack-start/src/routeTree.gen.ts @@ -0,0 +1,177 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FileUploadRouteImport } from './routes/file-upload' +import { Route as LoginRouteImport } from './routes/login' +import { Route as SignupRouteImport } from './routes/signup' +import { Route as SignupAsyncSchemaRouteImport } from './routes/signup-async-schema' +import { Route as TodosRouteImport } from './routes/todos' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FileUploadRoute = FileUploadRouteImport.update({ + id: '/file-upload', + path: '/file-upload', + getParentRoute: () => rootRouteImport, +} as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const SignupRoute = SignupRouteImport.update({ + id: '/signup', + path: '/signup', + getParentRoute: () => rootRouteImport, +} as any) +const SignupAsyncSchemaRoute = SignupAsyncSchemaRouteImport.update({ + id: '/signup-async-schema', + path: '/signup-async-schema', + getParentRoute: () => rootRouteImport, +} as any) +const TodosRoute = TodosRouteImport.update({ + id: '/todos', + path: '/todos', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/file-upload': typeof FileUploadRoute + '/login': typeof LoginRoute + '/signup': typeof SignupRoute + '/signup-async-schema': typeof SignupAsyncSchemaRoute + '/todos': typeof TodosRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/file-upload': typeof FileUploadRoute + '/login': typeof LoginRoute + '/signup': typeof SignupRoute + '/signup-async-schema': typeof SignupAsyncSchemaRoute + '/todos': typeof TodosRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/file-upload': typeof FileUploadRoute + '/login': typeof LoginRoute + '/signup': typeof SignupRoute + '/signup-async-schema': typeof SignupAsyncSchemaRoute + '/todos': typeof TodosRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/file-upload' + | '/login' + | '/signup' + | '/signup-async-schema' + | '/todos' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/file-upload' + | '/login' + | '/signup' + | '/signup-async-schema' + | '/todos' + id: + | '__root__' + | '/' + | '/file-upload' + | '/login' + | '/signup' + | '/signup-async-schema' + | '/todos' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FileUploadRoute: typeof FileUploadRoute + LoginRoute: typeof LoginRoute + SignupRoute: typeof SignupRoute + SignupAsyncSchemaRoute: typeof SignupAsyncSchemaRoute + TodosRoute: typeof TodosRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/file-upload': { + id: '/file-upload' + path: '/file-upload' + fullPath: '/file-upload' + preLoaderRoute: typeof FileUploadRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/signup': { + id: '/signup' + path: '/signup' + fullPath: '/signup' + preLoaderRoute: typeof SignupRouteImport + parentRoute: typeof rootRouteImport + } + '/signup-async-schema': { + id: '/signup-async-schema' + path: '/signup-async-schema' + fullPath: '/signup-async-schema' + preLoaderRoute: typeof SignupAsyncSchemaRouteImport + parentRoute: typeof rootRouteImport + } + '/todos': { + id: '/todos' + path: '/todos' + fullPath: '/todos' + preLoaderRoute: typeof TodosRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FileUploadRoute: FileUploadRoute, + LoginRoute: LoginRoute, + SignupRoute: SignupRoute, + SignupAsyncSchemaRoute: SignupAsyncSchemaRoute, + TodosRoute: TodosRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/examples/tanstack-start/src/router.tsx b/examples/tanstack-start/src/router.tsx new file mode 100644 index 000000000..34f36ed6b --- /dev/null +++ b/examples/tanstack-start/src/router.tsx @@ -0,0 +1,17 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export function getRouter() { + return createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadStaleTime: 0, + }); +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/examples/tanstack-start/src/routes/__root.tsx b/examples/tanstack-start/src/routes/__root.tsx new file mode 100644 index 000000000..e2f2b1f0f --- /dev/null +++ b/examples/tanstack-start/src/routes/__root.tsx @@ -0,0 +1,70 @@ +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router'; +import appCss from '../styles.css?url'; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { title: 'Conform / TanStack Start Example' }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + component: App, + shellComponent: RootDocument, +}); + +function App() { + return ( +
+

TanStack Start Example

+ +

This example demonstrates the following features:

+ +
    +
  • + Basic form with manual validation +
  • +
  • + Async validation ( + with async schema) +
  • +
  • + + Dynamic form with data persistence + +
  • +
  • + File upload +
  • +
+ +
+ + +
+ ); +} + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + + {children} + + + + ); +} diff --git a/examples/tanstack-start/src/routes/file-upload.tsx b/examples/tanstack-start/src/routes/file-upload.tsx new file mode 100644 index 000000000..15c87e194 --- /dev/null +++ b/examples/tanstack-start/src/routes/file-upload.tsx @@ -0,0 +1,90 @@ +import { parseSubmission, report, useForm } from '@conform-to/react/future'; +import { coerceFormValue } from '@conform-to/zod/v4/future'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { createServerFn, useServerFn } from '@tanstack/react-start'; +import { useState } from 'react'; +import { z } from 'zod'; + +const schema = coerceFormValue( + z.object({ + title: z.string().min(1, 'Title is required'), + file: z.file({ error: 'File is required' }), + }), +); + +const uploadFile = createServerFn({ method: 'POST' }) + .validator((formData: FormData) => parseSubmission(formData)) + .handler(async ({ data: submission }) => { + const result = schema.safeParse(submission.payload); + + if (!result.success) { + return report(submission, { + error: { issues: result.error.issues }, + }); + } + + throw redirect({ + to: '/', + search: { + value: JSON.stringify({ + title: result.data.title, + file: { + name: result.data.file.name, + size: result.data.file.size, + type: result.data.file.type, + }, + }), + }, + }); + }); + +export const Route = createFileRoute('/file-upload')({ component: FileUpload }); + +function FileUpload() { + const submit = useServerFn(uploadFile); + const [lastResult, setLastResult] = + useState>>(); + const { form, fields } = useForm(schema, { + lastResult, + async onSubmit(event, { formData }) { + event.preventDefault(); + const result = await submit({ data: formData }); + + if (result) { + setLastResult(result); + } + }, + shouldValidate: 'onBlur', + }); + + return ( +
+
+ + +
{fields.title.errors}
+
+
+ + +
{fields.file.errors}
+
+
+ +
+ ); +} diff --git a/examples/tanstack-start/src/routes/index.tsx b/examples/tanstack-start/src/routes/index.tsx new file mode 100644 index 000000000..111ac2fd5 --- /dev/null +++ b/examples/tanstack-start/src/routes/index.tsx @@ -0,0 +1,36 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/')({ + validateSearch: (search: Record) => ({ + value: typeof search.value === 'string' ? search.value : undefined, + }), + component: Home, +}); + +function parseJson(value: string | undefined): unknown { + if (!value) { + return null; + } + + try { + return JSON.parse(value); + } catch { + return null; + } +} + +function Home() { + const { value } = Route.useSearch(); + const submittedValue = parseJson(value); + + if (submittedValue === null) { + return null; + } + + return ( +
+ Submitted the following value: +
{JSON.stringify(submittedValue, null, 2)}
+
+ ); +} diff --git a/examples/tanstack-start/src/routes/login.tsx b/examples/tanstack-start/src/routes/login.tsx new file mode 100644 index 000000000..7629424d2 --- /dev/null +++ b/examples/tanstack-start/src/routes/login.tsx @@ -0,0 +1,111 @@ +import { + parseSubmission, + report, + type FormError, + useForm, +} from '@conform-to/react/future'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { createServerFn, useServerFn } from '@tanstack/react-start'; +import { useState } from 'react'; + +function validate(value: Record) { + const error: FormError = { + formErrors: null, + fieldErrors: {}, + }; + + if (!value.email) { + error.fieldErrors.email = 'Email is required'; + } else if (typeof value.email !== 'string' || !value.email.includes('@')) { + error.fieldErrors.email = 'Email is invalid'; + } + + if (!value.password) { + error.fieldErrors.password = 'Password is required'; + } + + return error.formErrors === null && + Object.keys(error.fieldErrors).length === 0 + ? null + : error; +} + +const login = createServerFn({ method: 'POST' }) + .validator((formData: FormData) => parseSubmission(formData)) + .handler(async ({ data: submission }) => { + const error = validate(submission.payload); + + if (error) { + return report(submission, { error }); + } + + throw redirect({ + to: '/', + search: { value: JSON.stringify(submission.payload) }, + }); + }); + +export const Route = createFileRoute('/login')({ component: Login }); + +function Login() { + const submit = useServerFn(login); + const [lastResult, setLastResult] = + useState>>(); + const { form, fields } = useForm({ + lastResult, + async onSubmit(event, { formData }) { + event.preventDefault(); + const result = await submit({ data: formData }); + + if (result) { + setLastResult(result); + } + }, + shouldValidate: 'onBlur', + onValidate({ payload }) { + return validate(payload); + }, + }); + + return ( +
+
+ + +
{fields.email.errors}
+
+
+ + +
{fields.password.errors}
+
+
+ + +
+
+ +
+ ); +} diff --git a/examples/tanstack-start/src/routes/signup-async-schema.tsx b/examples/tanstack-start/src/routes/signup-async-schema.tsx new file mode 100644 index 000000000..e443d85eb --- /dev/null +++ b/examples/tanstack-start/src/routes/signup-async-schema.tsx @@ -0,0 +1,163 @@ +import { + memoize, + parseSubmission, + report, + useForm, +} from '@conform-to/react/future'; +import { coerceFormValue } from '@conform-to/zod/v4/future'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { createServerFn, useServerFn } from '@tanstack/react-start'; +import { useMemo, useState } from 'react'; +import { z } from 'zod'; + +function createSignupSchema(checks: { + isUsernameUnique: (username: string) => Promise; +}) { + const isUsernameUnique = memoize(checks.isUsernameUnique); + + return coerceFormValue( + z + .object({ + username: z + .string({ error: 'Username is required' }) + .regex( + /^[a-zA-Z0-9]+$/, + 'Invalid username: only letters or numbers are allowed', + ) + .refine((username) => isUsernameUnique(username), { + message: 'Username is already used. How about "example"?', + }), + }) + .and( + z + .object({ + password: z.string({ error: 'Password is required' }), + confirmPassword: z.string({ + error: 'Confirm password is required', + }), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Password does not match', + path: ['confirmPassword'], + }), + ), + ); +} + +const signup = createServerFn({ method: 'POST' }) + .validator((formData: FormData) => parseSubmission(formData)) + .handler(async ({ data: submission }) => { + const schema = createSignupSchema({ + async isUsernameUnique(username) { + await new Promise((resolve) => { + setTimeout(resolve, Math.random() * 1000); + }); + + return username === 'example'; + }, + }); + const result = await schema.safeParseAsync(submission.payload); + + if (!result.success) { + return report(submission, { + error: { issues: result.error.issues }, + }); + } + + if (result.data.password !== 'secret') { + return report(submission, { + error: { + formErrors: ['Server error: Please try again later'], + }, + }); + } + + throw redirect({ + to: '/', + search: { value: JSON.stringify(result.data) }, + }); + }); + +export const Route = createFileRoute('/signup-async-schema')({ + component: Signup, +}); + +function Signup() { + const submit = useServerFn(signup); + const [lastResult, setLastResult] = + useState>>(); + const schema = useMemo( + () => + createSignupSchema({ + async isUsernameUnique(username) { + await new Promise((resolve) => { + setTimeout(resolve, Math.random() * 500); + }); + + return username === 'example'; + }, + }), + [], + ); + const { form, fields } = useForm(schema, { + lastResult, + async onSubmit(event, { formData }) { + event.preventDefault(); + const result = await submit({ data: formData }); + + if (result) { + setLastResult(result); + } + }, + shouldValidate: 'onBlur', + shouldRevalidate: 'onInput', + }); + + return ( +
+
{form.errors}
+
+ + +
{fields.username.errors}
+
+
+ + +
{fields.password.errors}
+
+
+ + +
+ {fields.confirmPassword.errors} +
+
+
+ +
+ ); +} diff --git a/examples/tanstack-start/src/routes/signup.tsx b/examples/tanstack-start/src/routes/signup.tsx new file mode 100644 index 000000000..1498c14e8 --- /dev/null +++ b/examples/tanstack-start/src/routes/signup.tsx @@ -0,0 +1,166 @@ +import { + memoize, + parseSubmission, + report, + useForm, +} from '@conform-to/react/future'; +import { coerceFormValue } from '@conform-to/zod/v4/future'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { createServerFn, useServerFn } from '@tanstack/react-start'; +import { useMemo, useState } from 'react'; +import { z } from 'zod'; + +const schema = coerceFormValue( + z + .object({ + username: z + .string({ error: 'Username is required' }) + .regex( + /^[a-zA-Z0-9]+$/, + 'Invalid username: only letters or numbers are allowed', + ), + }) + .and( + z + .object({ + password: z.string({ error: 'Password is required' }), + confirmPassword: z.string({ + error: 'Confirm password is required', + }), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Password does not match', + path: ['confirmPassword'], + }), + ), +); + +const signup = createServerFn({ method: 'POST' }) + .validator((formData: FormData) => parseSubmission(formData)) + .handler(async ({ data: submission }) => { + const result = schema.safeParse(submission.payload); + + if (!result.success) { + return report(submission, { + error: { issues: result.error.issues }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); + + if (result.data.username !== 'example') { + return report(submission, { + error: { + fieldErrors: { + username: ['Username is already used. How about "example"?'], + }, + }, + }); + } + + if (result.data.password !== 'secret') { + return report(submission, { + error: { + formErrors: ['Server error: Please try again later'], + }, + }); + } + + throw redirect({ + to: '/', + search: { value: JSON.stringify(result.data) }, + }); + }); + +export const Route = createFileRoute('/signup')({ component: Signup }); + +function Signup() { + const submit = useServerFn(signup); + const [lastResult, setLastResult] = + useState>>(); + const validateUsername = useMemo( + () => + memoize(async function isUnique(username: string) { + await new Promise((resolve) => { + setTimeout(resolve, Math.random() * 500); + }); + + return username === 'example' + ? null + : ['Username is already used. How about "example"?']; + }), + [], + ); + const { form, fields } = useForm(schema, { + lastResult, + async onSubmit(event, { formData }) { + event.preventDefault(); + const result = await submit({ data: formData }); + + if (result) { + setLastResult(result); + } + }, + shouldValidate: 'onBlur', + shouldRevalidate: 'onInput', + async onValidate({ payload, error }) { + if (typeof payload.username === 'string' && !error.fieldErrors.username) { + const messages = await validateUsername(payload.username); + + if (messages) { + error.fieldErrors.username = messages; + } + } + + return error; + }, + }); + + return ( +
+
{form.errors}
+
+ + +
{fields.username.errors}
+
+
+ + +
{fields.password.errors}
+
+
+ + +
+ {fields.confirmPassword.errors} +
+
+
+ +
+ ); +} diff --git a/examples/tanstack-start/src/routes/todos.tsx b/examples/tanstack-start/src/routes/todos.tsx new file mode 100644 index 000000000..ee3cbc6c1 --- /dev/null +++ b/examples/tanstack-start/src/routes/todos.tsx @@ -0,0 +1,192 @@ +import { + isDirty, + parseSubmission, + report, + useForm, + useFormData, +} from '@conform-to/react/future'; +import { coerceFormValue } from '@conform-to/zod/v4/future'; +import { createFileRoute } from '@tanstack/react-router'; +import { createServerFn, useServerFn } from '@tanstack/react-start'; +import { setResponseHeader } from '@tanstack/react-start/server'; +import { useState } from 'react'; +import { z } from 'zod'; +import { todoStore } from '#/store.server'; + +const taskSchema = z.object({ + content: z.string(), + completed: z.boolean().default(false), +}); + +const todosSchema = z.object({ + title: z.string(), + tasks: z.array(taskSchema).nonempty(), +}); + +const schema = coerceFormValue(todosSchema); + +const getTodos = createServerFn({ method: 'GET' }) + .validator((data: { id?: string }) => data) + .handler(({ data }) => { + setResponseHeader('Cache-Control', 'no-store'); + return todoStore.getValue(data.id); + }); + +const saveTodos = createServerFn({ method: 'POST' }) + .validator((formData: FormData) => parseSubmission(formData)) + .handler(async ({ data: submission }) => { + const id = submission.payload.id; + const storeId = typeof id === 'string' ? id : undefined; + const result = schema.safeParse(submission.payload); + + if (!result.success) { + return report(submission, { + error: { issues: result.error.issues }, + }); + } + + await todoStore.setValue(result.data, storeId); + + return report(submission, { + reset: true, + targetValue: result.data, + }); + }); + +export const Route = createFileRoute('/todos')({ + validateSearch: (search: Record) => ({ + id: typeof search.id === 'string' ? search.id : undefined, + }), + loaderDeps: ({ search }) => ({ id: search.id }), + loader: ({ deps }) => getTodos({ data: deps }), + component: Todos, +}); + +function Todos() { + const defaultValue = Route.useLoaderData(); + const { id } = Route.useSearch(); + const submit = useServerFn(saveTodos); + const [lastResult, setLastResult] = + useState>>(); + const { form, fields, intent } = useForm(schema, { + lastResult, + async onSubmit(event, { formData }) { + event.preventDefault(); + const result = await submit({ data: formData }); + + if (result) { + setLastResult(result); + } + }, + defaultValue, + shouldValidate: 'onBlur', + }); + const dirty = useFormData(form.id, (formData) => + isDirty(formData, { + defaultValue: form.defaultValue, + skipEntry(name) { + return name === 'id'; + }, + }), + ); + const tasks = fields.tasks.getFieldList(); + + return ( +
+ {id ? : null} +
+ + +
{fields.title.errors}
+
+
+
{fields.tasks.errors}
+ {tasks.map((task, index) => { + const taskFields = task.getFieldset(); + + return ( +
+
+ + +
+ {taskFields.content.errors} +
+
+
+ + +
+ + + +
+ ); + })} + +
+ + + +
+ ); +} diff --git a/examples/tanstack-start/src/store.server.ts b/examples/tanstack-start/src/store.server.ts new file mode 100644 index 000000000..22cd3a7c3 --- /dev/null +++ b/examples/tanstack-start/src/store.server.ts @@ -0,0 +1,27 @@ +import { setTimeout } from 'node:timers/promises'; + +function createInMemoryStore(stores: Record) { + return { + async getValue(id?: string) { + await setTimeout(Math.random() * 150); + return stores[id ?? ''] ?? null; + }, + async setValue(value: Type, id?: string) { + await setTimeout(Math.random() * 1000); + stores[id ?? ''] = value; + }, + }; +} + +type TodoList = { + title: string; + tasks: Array<{ content: string; completed: boolean }>; +}; + +const serverState = globalThis as typeof globalThis & { + conformTanStackStartTodos?: Record; +}; + +export const todoStore = createInMemoryStore( + (serverState.conformTanStackStartTodos ??= {}), +); diff --git a/examples/tanstack-start/src/styles.css b/examples/tanstack-start/src/styles.css new file mode 100644 index 000000000..7278873d3 --- /dev/null +++ b/examples/tanstack-start/src/styles.css @@ -0,0 +1,27 @@ +main { + max-width: 600px; + margin: 0 auto; +} + +hr { + margin: 30px 0; + color: gainsboro; +} + +label { + display: block; +} + +.form-error { + color: red; +} + +input.error { + outline: red; + color: red; + border: 1px solid red; +} + +input.error + div { + color: red; +} diff --git a/examples/tanstack-start/tests/index.test.ts b/examples/tanstack-start/tests/index.test.ts new file mode 100644 index 000000000..3ece20ef6 --- /dev/null +++ b/examples/tanstack-start/tests/index.test.ts @@ -0,0 +1,191 @@ +import { expect, test, type Page } from '@playwright/test'; + +test.describe('tanstack-start', () => { + test.describe('login', () => { + async function getForm(page: Page) { + await page.goto('/login'); + + return { + submitButton: page.getByRole('button', { name: 'Login' }), + submittedValue: () => page.locator('pre').innerText().then(JSON.parse), + email: page.getByLabel('Email'), + password: page.getByLabel('Password'), + remember: page.getByLabel('Remember me'), + }; + } + + test('submit', async ({ page }) => { + const form = await getForm(page); + + await form.submitButton.click(); + await expect(form.email).toHaveAccessibleDescription('Email is required'); + await expect(form.password).toHaveAccessibleDescription( + 'Password is required', + ); + + await form.email.fill('test@example.com'); + await form.email.blur(); + await expect(form.email).toHaveAccessibleDescription(''); + await form.remember.check(); + await form.password.fill('password123'); + await form.password.blur(); + await expect(form.password).toHaveAccessibleDescription(''); + await form.submitButton.click(); + + await expect.poll(form.submittedValue).toMatchObject({ + email: 'test@example.com', + password: 'password123', + remember: 'on', + }); + }); + }); + + for (const { name, path } of [ + { name: 'signup', path: '/signup' }, + { name: 'signup-async-schema', path: '/signup-async-schema' }, + ]) { + test.describe(name, () => { + async function getForm(page: Page) { + await page.goto(path); + + return { + container: page.locator('form'), + submitButton: page.getByRole('button', { name: 'Signup' }), + submittedValue: () => + page.locator('pre').innerText().then(JSON.parse), + username: page.getByLabel('Username'), + password: page.getByLabel('Password', { exact: true }), + confirmPassword: page.getByLabel('Confirm Password'), + }; + } + + test('submit', async ({ page, browserName }) => { + const form = await getForm(page); + + await form.submitButton.click(); + await expect(form.username).toHaveAccessibleDescription( + 'Username is required', + ); + await expect(form.password).toHaveAccessibleDescription( + 'Password is required', + ); + await expect(form.confirmPassword).toHaveAccessibleDescription( + 'Confirm password is required', + ); + + await form.username.fill('example'); + await expect(form.username).toHaveAccessibleDescription(''); + await form.password.fill('secret'); + await expect(form.password).toHaveAccessibleDescription(''); + await form.confirmPassword.fill('secret'); + await expect(form.confirmPassword).toHaveAccessibleDescription(''); + await form.submitButton.click(); + + if (browserName !== 'firefox') { + await expect.poll(form.submittedValue).toMatchObject({ + username: 'example', + password: 'secret', + confirmPassword: 'secret', + }); + } + }); + + if (name === 'signup') { + test('server validation', async ({ page }) => { + const form = await getForm(page); + + await form.username.fill('example'); + await form.password.fill('not-secret'); + await form.confirmPassword.fill('not-secret'); + await form.submitButton.click(); + + await expect(form.container.locator('.form-error')).toHaveText( + 'Server error: Please try again later', + ); + }); + } + }); + } + + test.describe('todos', () => { + let testId = 0; + + async function getForm(page: Page) { + await page.goto(`/todos?id=test-${++testId}-${Date.now()}`); + + return { + saveButton: page.getByRole('button', { name: 'Save' }), + addTaskButton: page.getByRole('button', { name: 'Add task' }), + title: page.getByLabel('Title'), + }; + } + + test('submit', async ({ page }) => { + const form = await getForm(page); + + await form.addTaskButton.click(); + await form.addTaskButton.click(); + await form.addTaskButton.click(); + await form.title.fill('My Todo List'); + await page.getByLabel('Task #1').fill('Task A'); + await page.getByLabel('Task #2').fill('Task B'); + await page.getByLabel('Task #3').fill('Task C'); + + await page + .locator('fieldset') + .nth(2) + .getByRole('button', { name: 'Move to top' }) + .click(); + await expect(page.getByLabel('Task #1')).toHaveValue('Task C'); + await expect(page.getByLabel('Task #2')).toHaveValue('Task A'); + await expect(page.getByLabel('Task #3')).toHaveValue('Task B'); + + await page + .locator('fieldset') + .nth(1) + .getByRole('button', { name: 'Clear' }) + .click(); + await page + .locator('fieldset') + .nth(1) + .getByRole('button', { name: 'Delete' }) + .click(); + + await form.saveButton.click(); + await expect(form.saveButton).toBeDisabled({ timeout: 10000 }); + await expect(form.title).toHaveValue('My Todo List'); + await expect(page.getByLabel('Task #1')).toHaveValue('Task C'); + await expect(page.getByLabel('Task #2')).toHaveValue('Task B'); + + await page.reload({ waitUntil: 'networkidle' }); + await expect(form.title).toHaveValue('My Todo List'); + await expect(page.getByLabel('Task #1')).toHaveValue('Task C'); + await expect(page.getByLabel('Task #2')).toHaveValue('Task B'); + }); + }); + + test.describe('file-upload', () => { + test('submit', async ({ page }) => { + await page.goto('/file-upload'); + + await page.getByLabel('Title').fill('Attachment & notes #1'); + await page.getByLabel('File').setInputFiles({ + name: 'example & #1.txt', + mimeType: 'text/plain', + buffer: Buffer.from('TanStack Start upload'), + }); + await page.getByRole('button', { name: 'Submit' }).click(); + + await expect + .poll(() => page.locator('pre').innerText().then(JSON.parse)) + .toEqual({ + title: 'Attachment & notes #1', + file: { + name: 'example & #1.txt', + size: 21, + type: 'text/plain', + }, + }); + }); + }); +}); diff --git a/examples/tanstack-start/tsconfig.json b/examples/tanstack-start/tsconfig.json new file mode 100644 index 000000000..f9a04b494 --- /dev/null +++ b/examples/tanstack-start/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "paths": { + "#/*": ["./src/*"] + }, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node", "vite/client"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + } +} diff --git a/examples/tanstack-start/tsr.config.json b/examples/tanstack-start/tsr.config.json new file mode 100644 index 000000000..cf6f8a63d --- /dev/null +++ b/examples/tanstack-start/tsr.config.json @@ -0,0 +1,4 @@ +{ + "semicolons": true, + "quoteStyle": "single" +} diff --git a/examples/tanstack-start/vite.config.ts b/examples/tanstack-start/vite.config.ts new file mode 100644 index 000000000..c16cd0ba3 --- /dev/null +++ b/examples/tanstack-start/vite.config.ts @@ -0,0 +1,8 @@ +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import viteReact from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + plugins: [tanstackStart(), viteReact()], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c773b2be..6ab17400e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -845,6 +845,60 @@ importers: specifier: ^8.2.1 version: 8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + examples/tanstack-start: + dependencies: + '@conform-to/react': + specifier: 1.21.1 + version: file:packages/conform-react(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@conform-to/zod': + specifier: 1.21.1 + version: file:packages/conform-zod(zod@4.4.3) + '@tanstack/react-router': + specifier: 1.170.31 + version: 1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-start': + specifier: 1.168.48 + version: 1.168.48(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + react: + specifier: ^19.2.7 + version: 19.2.7 + react-dom: + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) + zod: + specifier: ^4.4.3 + version: 4.4.3 + dependenciesMeta: + '@conform-to/react': + injected: true + '@conform-to/zod': + injected: true + devDependencies: + '@playwright/test': + specifier: 1.62.1 + version: 1.62.1 + '@tanstack/router-cli': + specifier: 1.167.32 + version: 1.167.32 + '@types/node': + specifier: ^22 + version: 22.20.1 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.1.0 + version: 6.1.0(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.2.1 + version: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + guide: dependencies: '@markdoc/markdoc': @@ -3928,6 +3982,22 @@ packages: '@octokit/types@12.4.0': resolution: {integrity: sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==} + '@oozcitak/dom@2.0.2': + resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==} + engines: {node: '>=20.0'} + + '@oozcitak/infra@2.0.2': + resolution: {integrity: sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==} + engines: {node: '>=20.0'} + + '@oozcitak/url@3.0.0': + resolution: {integrity: sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==} + engines: {node: '>=20.0'} + + '@oozcitak/util@10.0.0': + resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} + engines: {node: '>=20.0'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -5833,6 +5903,71 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/history@1.162.1': + resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==} + engines: {node: '>=20.19'} + + '@tanstack/react-router@1.170.31': + resolution: {integrity: sha512-jqITLcf9Y+es9Wm7fD7XfXFGKk1Fujm+okGqHtr+UhISnQuyLQ8d3frsJlIN0Np1doYoeb5weozSkv72+EZ5bw==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start-client@1.168.29': + resolution: {integrity: sha512-YhEtx3EwbsLUWXSdwclz/C/AlDqPNoNMoNTEHulYDruaMAKNbFeWsDgBbHv/aE6P2P3shAMFYAT1cVHZM1WcJQ==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start-rsc@0.1.47': + resolution: {integrity: sha512-Tr01FbuRRpHSwkLjyfg7OotXgrJ4ufDjGKuk9HpLe7a2ouEs1Iq/kC2RKY0alvCYX+cpYFVayoyxNZCeybCTNQ==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rspack/core': '>=2.0.0-0' + '@vitejs/plugin-rsc': '>=0.5.30' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + react-server-dom-rspack: '>=0.0.2' + peerDependenciesMeta: + '@rspack/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + react-server-dom-rspack: + optional: true + + '@tanstack/react-start-server@1.167.36': + resolution: {integrity: sha512-v22xUhNENr0u67AIDbfbJgmQwMGqcrwdqqNI44Nd605G4DGTA7W1KPbea8bBjPkHxf5I+FTKL8Nb56w2Sb/Gpw==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start@1.168.48': + resolution: {integrity: sha512-ANBtq/phgjPzfwgnB40kyREQGhrLONueJg9y+oFuGIuUVaTfqWjX5y820jsJ0FiZ94JU+WIV6GebVhphNhuYjg==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rsbuild/core': ^2.0.0 + '@vitejs/plugin-rsc': '*' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + vite: + optional: true + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-virtual@3.14.7': resolution: {integrity: sha512-11uSrj77IDijNBqizD4lY4y1laMyRrqMLSxjnWy5CvWkCjyRDW+gGmxYq0lwQKVas/sq7zyzYWXbL/BvBzR32g==} peerDependencies: @@ -5845,12 +5980,85 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@tanstack/router-cli@1.167.32': + resolution: {integrity: sha512-ctsnmv4KAWmuau382eSb7rvNplh0zcs+1pi6PX3c9NmiwqMKshkqpWstV6zN/TvTySCxRB5PmRMAdFWQRy34EQ==} + engines: {node: '>=20.19'} + hasBin: true + + '@tanstack/router-core@1.171.26': + resolution: {integrity: sha512-VymmPSs/93szHur/7PBygT6tRbmfcNO1xR46Wn/JVHeBqVduLPCuxBeJEgfVmVq8E4hSklPEh8i6qGXQd6WRqA==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.167.32': + resolution: {integrity: sha512-KKPWUMUda7JYil+uDQPrX4ecyoXP9J3DesdpavOWpGCavsiAd6cxH6yq18krLGa1j20qmPJc10LJSLOOFia5tQ==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.34': + resolution: {integrity: sha512-WsXLQU8tQmoTyoQRB1JI3lGg5wnwo6VB2R8D2RQbtCVlUi3bjreSNgZSJ7Ghw06ZoUyNMO+Csiqf679nNnzN+w==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.31 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.2': + resolution: {integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==} + engines: {node: '>=20.19'} + + '@tanstack/start-client-core@1.170.26': + resolution: {integrity: sha512-a8SmEQ4gIeWDHBSqy1ePFNwXu0kHTPCoMiEdE0eKRBITVsexfEX8yKvqxLi66cF6aGCmDIkndbth3yB7R344zQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-fn-stubs@1.162.0': + resolution: {integrity: sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-plugin-core@1.171.38': + resolution: {integrity: sha512-ldRjxosoTyWG0xP6CLo/DkiuENt668se7oZwAJuRMtRBGBDzhMVMwFnS86P2qgnpj/BQVFebVpXMVfzeH7tvDw==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rsbuild/core': ^2.0.0 + vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + vite: + optional: true + + '@tanstack/start-server-core@1.169.30': + resolution: {integrity: sha512-knlKOPsHyDbGKgP1pfUowtObZIKS9zRI6vl7dzbA8D/NJo6dMdDdqGcDf4PINqREW8hCHdkB4At0y1/Mk+L2SQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-storage-context@1.167.28': + resolution: {integrity: sha512-ZGl6bEXW5m77VVtWMt/naEBVoIeQkXYgF4UgiJM/3z3e1wbRzaoW7Dnc4HW+hoBPOOdfiVVNx31RT2ARnXWAiQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + '@tanstack/virtual-core@3.17.5': resolution: {integrity: sha512-AXfBC3sq6PuYSwyxYORqqgHCNjPGAvKJvZuBBJ1klhztWBB5cgqgwsq8+fNfaQJG7/K4xYBja9S90QFn2zmQAg==} '@tanstack/virtual-core@3.5.0': resolution: {integrity: sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==} + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + '@testing-library/dom@8.20.1': resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} engines: {node: '>=12'} @@ -6196,6 +6404,22 @@ packages: babel-plugin-react-compiler: optional: true + '@vitejs/plugin-react@6.1.0': + resolution: {integrity: sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + '@vitest/browser-playwright@4.1.10': resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==} peerDependencies: @@ -6612,6 +6836,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -7943,6 +8171,7 @@ packages: eslint@9.27.0: resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -8146,6 +8375,9 @@ packages: picomatch: optional: true + fetchdts@0.1.7: + resolution: {integrity: sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -8462,6 +8694,16 @@ packages: h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + h3@2.0.1-rc.20: + resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -11164,6 +11406,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.8.1: + resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -11243,6 +11488,16 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + seroval-plugins@1.6.2: + resolution: {integrity: sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.6.2: + resolution: {integrity: sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ==} + engines: {node: '>=10'} + serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -11405,6 +11660,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -11427,6 +11686,11 @@ packages: spdx-license-ids@3.0.16: resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + srvx@0.11.22: + resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} + engines: {node: '>=20.16.0'} + hasBin: true + ssri@10.0.5: resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -12012,6 +12276,39 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin@3.3.0: + resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + unstorage@1.17.5: resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} peerDependencies: @@ -12470,6 +12767,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -12600,6 +12900,10 @@ packages: xdm@2.1.0: resolution: {integrity: sha512-3LxxbxKcRogYY7cQSMy1tUuU1zKNK9YPqMT7/S0r7Cz2QpyF8O9yFySGD7caOZt+LWUOQioOIX+6ZzCoBCpcAA==} + xmlbuilder2@4.0.3: + resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} + engines: {node: '>=20.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -15462,6 +15766,23 @@ snapshots: dependencies: '@octokit/openapi-types': 19.1.0 + '@oozcitak/dom@2.0.2': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/url': 3.0.0 + '@oozcitak/util': 10.0.0 + + '@oozcitak/infra@2.0.2': + dependencies: + '@oozcitak/util': 10.0.0 + + '@oozcitak/url@3.0.0': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + + '@oozcitak/util@10.0.0': {} + '@oslojs/encoding@1.1.0': {} '@oxc-project/types@0.144.0': {} @@ -17270,6 +17591,97 @@ snapshots: tailwindcss: 4.3.3 vite: 8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + '@tanstack/history@1.162.1': {} + + '@tanstack/react-router@1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/history': 1.162.1 + '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.26 + isbot: 5.2.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@tanstack/react-start-client@1.168.29(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/react-router': 1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.26 + '@tanstack/start-client-core': 1.170.26 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@tanstack/react-start-rsc@0.1.47(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@tanstack/react-router': 1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.26 + '@tanstack/router-utils': 1.162.2 + '@tanstack/start-client-core': 1.170.26 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-plugin-core': 1.171.38(@tanstack/react-router@1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@tanstack/start-storage-context': 1.167.28 + pathe: 2.0.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - bun-types-no-globals + - crossws + - esbuild + - rolldown + - rollup + - supports-color + - unloader + - vite + - vite-plugin-solid + - webpack + + '@tanstack/react-start-server@1.167.36(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/react-router': 1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.26 + '@tanstack/start-server-core': 1.169.30 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - crossws + + '@tanstack/react-start@1.168.48(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@tanstack/react-router': 1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-start-client': 1.168.29(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-start-rsc': 0.1.47(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@tanstack/react-start-server': 1.167.36(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-utils': 1.162.2 + '@tanstack/start-client-core': 1.170.26 + '@tanstack/start-plugin-core': 1.171.38(@tanstack/react-router@1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@tanstack/start-server-core': 1.169.30 + pathe: 2.0.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - crossws + - esbuild + - react-server-dom-rspack + - rolldown + - rollup + - supports-color + - unloader + - vite-plugin-solid + - webpack + + '@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + '@tanstack/react-virtual@3.14.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@tanstack/virtual-core': 3.17.5 @@ -17282,10 +17694,142 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/router-cli@1.167.32': + dependencies: + '@tanstack/router-generator': 1.167.32 + chokidar: 5.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-core@1.171.26': + dependencies: + '@tanstack/history': 1.162.1 + cookie-es: 3.1.1 + seroval: 1.6.2 + seroval-plugins: 1.6.2(seroval@1.6.2) + + '@tanstack/router-generator@1.167.32': + dependencies: + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.26 + '@tanstack/router-utils': 1.162.2 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.9.5 + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.34(@tanstack/react-router@1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.26 + '@tanstack/router-generator': 1.167.32 + '@tanstack/router-utils': 1.162.2 + chokidar: 5.0.0 + unplugin: 3.3.0(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + zod: 4.4.3 + optionalDependencies: + '@tanstack/react-router': 1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - supports-color + - unloader + + '@tanstack/router-utils@1.162.2': + dependencies: + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + ansis: 4.3.1 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - supports-color + + '@tanstack/start-client-core@1.170.26': + dependencies: + '@tanstack/router-core': 1.171.26 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-storage-context': 1.167.28 + seroval: 1.6.2 + + '@tanstack/start-fn-stubs@1.162.0': {} + + '@tanstack/start-plugin-core@1.171.38(@tanstack/react-router@1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.29.7 + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.26 + '@tanstack/router-generator': 1.167.32 + '@tanstack/router-plugin': 1.168.34(@tanstack/react-router@1.170.31(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@tanstack/router-utils': 1.162.2 + '@tanstack/start-server-core': 1.169.30 + exsolve: 1.1.0 + lightningcss: 1.33.0 + pathe: 2.0.3 + picomatch: 4.0.5 + seroval: 1.6.2 + source-map: 0.7.6 + srvx: 0.11.22 + tinyglobby: 0.2.17 + ufo: 1.6.4 + vitefu: 1.1.3(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + xmlbuilder2: 4.0.3 + zod: 4.4.3 + optionalDependencies: + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - '@tanstack/react-router' + - bun-types-no-globals + - crossws + - esbuild + - rolldown + - rollup + - supports-color + - unloader + - vite-plugin-solid + - webpack + + '@tanstack/start-server-core@1.169.30': + dependencies: + '@tanstack/history': 1.162.1 + '@tanstack/router-core': 1.171.26 + '@tanstack/start-client-core': 1.170.26 + '@tanstack/start-storage-context': 1.167.28 + fetchdts: 0.1.7 + h3-v2: h3@2.0.1-rc.20 + seroval: 1.6.2 + transitivePeerDependencies: + - crossws + + '@tanstack/start-storage-context@1.167.28': + dependencies: + '@tanstack/router-core': 1.171.26 + + '@tanstack/store@0.9.3': {} + '@tanstack/virtual-core@3.17.5': {} '@tanstack/virtual-core@3.5.0': {} + '@tanstack/virtual-file-routes@1.162.0': {} + '@testing-library/dom@8.20.1': dependencies: '@babel/code-frame': 7.29.0 @@ -17751,6 +18295,11 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + '@vitejs/plugin-react@6.1.0(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + '@vitest/browser-playwright@4.1.10(playwright@1.62.1)(vite@8.2.1(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(vite@8.2.1(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) @@ -18575,6 +19124,8 @@ snapshots: ansi-styles@6.2.1: {} + ansis@4.3.1: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -20782,6 +21333,8 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fetchdts@0.1.7: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -21175,6 +21728,11 @@ snapshots: ufo: 1.6.4 uncrypto: 0.1.3 + h3@2.0.1-rc.20: + dependencies: + rou3: 0.8.1 + srvx: 0.11.22 + has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -24485,6 +25043,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + rou3@0.8.1: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -24593,6 +25153,12 @@ snapshots: transitivePeerDependencies: - supports-color + seroval-plugins@1.6.2(seroval@1.6.2): + dependencies: + seroval: 1.6.2 + + seroval@1.6.2: {} + serve-static@1.15.0: dependencies: encodeurl: 1.0.2 @@ -24879,6 +25445,8 @@ snapshots: source-map@0.7.4: {} + source-map@0.7.6: {} + sourcemap-codec@1.4.8: {} space-separated-tokens@1.1.5: {} @@ -24899,6 +25467,8 @@ snapshots: spdx-license-ids@3.0.16: {} + srvx@0.11.22: {} + ssri@10.0.5: dependencies: minipass: 7.0.4 @@ -25573,6 +26143,17 @@ snapshots: unpipe@1.0.0: {} + unplugin@3.3.0(esbuild@0.27.7)(rolldown@1.2.4)(rollup@4.62.2)(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + esbuild: 0.27.7 + rolldown: 1.2.4 + rollup: 4.62.2 + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + unstorage@1.17.5: dependencies: anymatch: 3.1.3 @@ -25801,6 +26382,10 @@ snapshots: optionalDependencies: vite: 7.3.5(@types/node@25.5.0)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + vitefu@1.1.3(vite@8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)): + optionalDependencies: + vite: 8.2.1(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + vitest-browser-react@1.0.1(@types/react-dom@18.3.7(@types/react@18.3.1))(@types/react@18.3.1)(@vitest/browser@4.1.10(vite@8.2.1(@types/node@25.5.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vitest@4.1.10): dependencies: '@vitest/browser': 4.1.10(vite@8.2.1(@types/node@25.5.0)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) @@ -25980,6 +26565,8 @@ snapshots: webidl-conversions@3.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -26170,6 +26757,13 @@ snapshots: transitivePeerDependencies: - supports-color + xmlbuilder2@4.0.3: + dependencies: + '@oozcitak/dom': 2.0.2 + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + js-yaml: 4.2.0 + xtend@4.0.2: {} xxhash-wasm@1.1.0: {} From aa5fa1fa6c9ba453a9715822bed06171769d3e16 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Sat, 22 Aug 2026 23:32:49 +0100 Subject: [PATCH 2/2] test(examples): cover raw JSON null search value --- examples/tanstack-start/src/routes/index.tsx | 11 ++++++++--- examples/tanstack-start/tests/index.test.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/tanstack-start/src/routes/index.tsx b/examples/tanstack-start/src/routes/index.tsx index 10398c694..ca98a0e1c 100644 --- a/examples/tanstack-start/src/routes/index.tsx +++ b/examples/tanstack-start/src/routes/index.tsx @@ -1,9 +1,14 @@ import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ - validateSearch: (search: Record) => ({ - value: typeof search.value === 'string' ? search.value : undefined, - }), + validateSearch: (search: Record) => { + const value = search.value; + + return { + value: + typeof value === 'string' || value === null ? String(value) : undefined, + }; + }, component: Home, }); diff --git a/examples/tanstack-start/tests/index.test.ts b/examples/tanstack-start/tests/index.test.ts index 4c98c8b61..fd1ddb03f 100644 --- a/examples/tanstack-start/tests/index.test.ts +++ b/examples/tanstack-start/tests/index.test.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from '@playwright/test'; test.describe('tanstack-start', () => { test('renders a submitted null value', async ({ page }) => { - await page.goto('/?value=%22null%22'); + await page.goto('/?value=null'); await expect(page.locator('pre')).toHaveText('null'); });