From ce2f9fa533a96561f33136127b16ad3638a532c0 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 17 Oct 2025 14:15:56 -0600 Subject: [PATCH 01/10] fix: refactor middleware --- .../server-functions/src/routeTree.gen.ts | 22 ++++ .../server-functions/src/routes/index.tsx | 5 + .../routes/middleware/unhandled-exception.tsx | 33 +++++ .../start-client-core/src/createServerFn.ts | 113 ++++++++++-------- packages/start-client-core/src/index.tsx | 1 - 5 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index b5692de0599..73ad1c262f2 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -27,6 +27,7 @@ import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index' import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index' import { Route as FactoryIndexRouteImport } from './routes/factory/index' import { Route as CookiesIndexRouteImport } from './routes/cookies/index' +import { Route as MiddlewareUnhandledExceptionRouteImport } from './routes/middleware/unhandled-exception' import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn' import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware' import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' @@ -123,6 +124,12 @@ const CookiesIndexRoute = CookiesIndexRouteImport.update({ path: '/cookies/', getParentRoute: () => rootRouteImport, } as any) +const MiddlewareUnhandledExceptionRoute = + MiddlewareUnhandledExceptionRouteImport.update({ + id: '/middleware/unhandled-exception', + path: '/middleware/unhandled-exception', + getParentRoute: () => rootRouteImport, + } as any) const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({ id: '/middleware/send-serverFn', path: '/middleware/send-serverFn', @@ -170,6 +177,7 @@ export interface FileRoutesByFullPath { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies': typeof CookiesIndexRoute '/factory': typeof FactoryIndexRoute '/formdata-redirect': typeof FormdataRedirectIndexRoute @@ -195,6 +203,7 @@ export interface FileRoutesByTo { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies': typeof CookiesIndexRoute '/factory': typeof FactoryIndexRoute '/formdata-redirect': typeof FormdataRedirectIndexRoute @@ -221,6 +230,7 @@ export interface FileRoutesById { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies/': typeof CookiesIndexRoute '/factory/': typeof FactoryIndexRoute '/formdata-redirect/': typeof FormdataRedirectIndexRoute @@ -248,6 +258,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/unhandled-exception' | '/cookies' | '/factory' | '/formdata-redirect' @@ -273,6 +284,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/unhandled-exception' | '/cookies' | '/factory' | '/formdata-redirect' @@ -298,6 +310,7 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' + | '/middleware/unhandled-exception' | '/cookies/' | '/factory/' | '/formdata-redirect/' @@ -324,6 +337,7 @@ export interface RootRouteChildren { MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute + MiddlewareUnhandledExceptionRoute: typeof MiddlewareUnhandledExceptionRoute CookiesIndexRoute: typeof CookiesIndexRoute FactoryIndexRoute: typeof FactoryIndexRoute FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute @@ -460,6 +474,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CookiesIndexRouteImport parentRoute: typeof rootRouteImport } + '/middleware/unhandled-exception': { + id: '/middleware/unhandled-exception' + path: '/middleware/unhandled-exception' + fullPath: '/middleware/unhandled-exception' + preLoaderRoute: typeof MiddlewareUnhandledExceptionRouteImport + parentRoute: typeof rootRouteImport + } '/middleware/send-serverFn': { id: '/middleware/send-serverFn' path: '/middleware/send-serverFn' @@ -516,6 +537,7 @@ const rootRouteChildren: RootRouteChildren = { MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute, MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute, + MiddlewareUnhandledExceptionRoute: MiddlewareUnhandledExceptionRoute, CookiesIndexRoute: CookiesIndexRoute, FactoryIndexRoute: FactoryIndexRoute, FormdataRedirectIndexRoute: FormdataRedirectIndexRoute, diff --git a/e2e/react-start/server-functions/src/routes/index.tsx b/e2e/react-start/server-functions/src/routes/index.tsx index 596b855c099..ca1394015af 100644 --- a/e2e/react-start/server-functions/src/routes/index.tsx +++ b/e2e/react-start/server-functions/src/routes/index.tsx @@ -88,6 +88,11 @@ function Home() {
  • Server Functions Factory E2E tests
  • +
  • + + Server Functions Middleware Unhandled Exception E2E tests + +
  • ) diff --git a/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx b/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx new file mode 100644 index 00000000000..f6d3bb60179 --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx @@ -0,0 +1,33 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createMiddleware, createServerFn } from '@tanstack/react-start' + +export const authMiddleware = createMiddleware({ type: 'function' }).server( + async ({ next, context }) => { + throw new Error('Unauthorized') + }, +) + +const personServerFn = createServerFn({ method: 'GET' }) + .middleware([authMiddleware]) + .inputValidator((d: string) => d) + .handler(({ data: name }) => { + return { name, randomNumber: Math.floor(Math.random() * 100) } + }) + +export const Route = createFileRoute('/middleware/unhandled-exception')({ + loader: async () => { + return { + person: await personServerFn({ data: 'John Doe' }), + } + }, + component: RouteComponent, +}) + +function RouteComponent() { + const { person } = Route.useLoaderData() + return ( +
    + {person.name} - {person.randomNumber} +
    + ) +} diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index bc95a6b5b9a..a120dafa67a 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -1,4 +1,3 @@ -import { isNotFound, isRedirect } from '@tanstack/router-core' import { mergeHeaders } from '@tanstack/router-core/ssr/client' import { TSS_SERVER_FUNCTION_FACTORY } from './constants' @@ -112,17 +111,17 @@ export const createServerFn: CreateServerFn = (options, __opts) => { return Object.assign( async (opts?: CompiledFetcherFnOptions) => { // Start by executing the client-side middleware chain - return executeMiddleware(resolvedMiddleware, 'client', { + const result = await executeMiddleware(resolvedMiddleware, 'client', { ...extractedFn, ...newOptions, data: opts?.data as any, headers: opts?.headers, signal: opts?.signal, context: {}, - }).then((d) => { - if (d.error) throw d.error - return d.result }) + + if (result.error) throw result.error + return result.result }, { // This copies over the URL, function ID @@ -180,7 +179,7 @@ export async function executeMiddleware( ...middlewares, ]) - const next: NextFn = async (ctx) => { + const callNextMiddleware: NextFn = async (ctx) => { // Get the next middleware const nextMiddleware = flattenedMiddlewares.shift() @@ -212,27 +211,70 @@ export async function executeMiddleware( middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined } - if (middlewareFn) { - // Execute the middleware - return applyMiddleware(middlewareFn, ctx, async (newCtx) => { - return next(newCtx).catch((error: any) => { - if (isRedirect(error) || isNotFound(error)) { + // Execute the middleware + try { + if (middlewareFn) { + const userNext = async ( + userCtx: ServerFnMiddlewareResult | undefined = {} as any, + ) => { + // Return the next middleware + const nextCtx = { + ...ctx, + ...userCtx, + context: { + ...ctx.context, + ...userCtx.context, + }, + sendContext: { + ...ctx.sendContext, + ...(userCtx.sendContext ?? {}), + }, + headers: mergeHeaders(ctx.headers, userCtx.headers), + result: + userCtx.result !== undefined + ? userCtx.result + : userCtx instanceof Response + ? userCtx + : (ctx as any).result, + error: userCtx.error ?? (ctx as any).error, + } + + try { + return await callNextMiddleware(nextCtx) + } catch (error: any) { return { - ...newCtx, + ...nextCtx, error, } } + } - throw error - }) - }) - } + // Execute the middleware + const result = await middlewareFn({ + ...ctx, + next: userNext as any, + } as any) + + if (!(result as any)) { + throw new Error( + 'User middleware returned undefined. You must call next() or return a result in your middlewares.', + ) + } + + return result + } - return next(ctx) + return callNextMiddleware(ctx) + } catch (error: any) { + return { + ...ctx, + error, + } + } } // Start the middleware chain - return next({ + return callNextMiddleware({ ...opts, headers: opts.headers || {}, sendContext: opts.sendContext || {}, @@ -628,41 +670,6 @@ export type MiddlewareFn = ( }, ) => Promise -export const applyMiddleware = async ( - middlewareFn: MiddlewareFn, - ctx: ServerFnMiddlewareOptions, - nextFn: NextFn, -) => { - return middlewareFn({ - ...ctx, - next: (async ( - userCtx: ServerFnMiddlewareResult | undefined = {} as any, - ) => { - // Return the next middleware - return nextFn({ - ...ctx, - ...userCtx, - context: { - ...ctx.context, - ...userCtx.context, - }, - sendContext: { - ...ctx.sendContext, - ...(userCtx.sendContext ?? {}), - }, - headers: mergeHeaders(ctx.headers, userCtx.headers), - result: - userCtx.result !== undefined - ? userCtx.result - : userCtx instanceof Response - ? userCtx - : (ctx as any).result, - error: userCtx.error ?? (ctx as any).error, - }) - }) as any, - } as any) -} - export function execValidator( validator: AnyValidator, input: unknown, diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx index 70a4692842a..9ba2eeaa71f 100644 --- a/packages/start-client-core/src/index.tsx +++ b/packages/start-client-core/src/index.tsx @@ -74,7 +74,6 @@ export type { RequiredFetcher, } from './createServerFn' export { - applyMiddleware, execValidator, flattenMiddlewares, executeMiddleware, From ed548799d38abe1a73c9ed3f9005833cec06fe35 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 17 Oct 2025 14:52:25 -0600 Subject: [PATCH 02/10] fix: catch errors from validators --- .../start-client-core/src/createServerFn.ts | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index a120dafa67a..0fecefcd50f 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -188,31 +188,33 @@ export async function executeMiddleware( return ctx } - if ( - 'inputValidator' in nextMiddleware.options && - nextMiddleware.options.inputValidator && - env === 'server' - ) { - // Execute the middleware's input function - ctx.data = await execValidator( - nextMiddleware.options.inputValidator, - ctx.data, - ) - } + // Execute the middleware + try { + if ( + 'inputValidator' in nextMiddleware.options && + nextMiddleware.options.inputValidator && + env === 'server' + ) { + // Execute the middleware's input function + ctx.data = await execValidator( + nextMiddleware.options.inputValidator, + ctx.data, + ) + } - let middlewareFn: MiddlewareFn | undefined = undefined - if (env === 'client') { - if ('client' in nextMiddleware.options) { - middlewareFn = nextMiddleware.options.client as MiddlewareFn | undefined + let middlewareFn: MiddlewareFn | undefined = undefined + if (env === 'client') { + if ('client' in nextMiddleware.options) { + middlewareFn = nextMiddleware.options.client as + | MiddlewareFn + | undefined + } + } + // env === 'server' + else if ('server' in nextMiddleware.options) { + middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined } - } - // env === 'server' - else if ('server' in nextMiddleware.options) { - middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined - } - // Execute the middleware - try { if (middlewareFn) { const userNext = async ( userCtx: ServerFnMiddlewareResult | undefined = {} as any, From c7eddee32cba07565624c5f3b18f89e7a6af7efb Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 27 Oct 2025 10:59:43 -0700 Subject: [PATCH 03/10] checkpoint --- .../src/routes/submit-post-formdata.tsx | 2 +- .../src/client-rpc/serverFnFetcher.ts | 47 ++-- packages/start-client-core/src/constants.ts | 1 + .../start-client-core/src/createServerFn.ts | 44 +++- packages/start-client-core/src/index.tsx | 1 + .../src/createStartHandler.ts | 7 +- .../src/server-functions-handler.ts | 234 ++++++++++-------- 7 files changed, 204 insertions(+), 132 deletions(-) diff --git a/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx b/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx index 826ec255d38..5f9165adf74 100644 --- a/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx +++ b/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx @@ -33,7 +33,7 @@ function SubmitPostFormDataFn() {

    Submit POST FormData Fn Call

    - It should return navigate and return{' '} + It should navigate to a raw response of {''}
                 Hello, {testValues.name}!
    diff --git a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    index 5511bdc5173..b1b781d7d00 100644
    --- a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    +++ b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    @@ -17,6 +17,16 @@ import type { Plugin as SerovalPlugin } from 'seroval'
     
     let serovalPlugins: Array> | null = null
     
    +// caller =>
    +//   serverFnFetcher =>
    +//     client =>
    +//       server =>
    +//         fn =>
    +//       seroval =>
    +//     client middleware =>
    +//   serverFnFetcher =>
    +// caller
    +
     export async function serverFnFetcher(
       url: string,
       args: Array,
    @@ -37,7 +47,8 @@ export async function serverFnFetcher(
     
         // Arrange the headers
         const headers = new Headers({
    -      'x-tsr-redirect': 'manual',
    +      'x-tsr-serverFn': 'true',
    +      'x-tsr-createServerFn': 'true',
           ...(first.headers instanceof Headers
             ? Object.fromEntries(first.headers.entries())
             : first.headers),
    @@ -65,12 +76,6 @@ export async function serverFnFetcher(
           }
         }
     
    -    if (url.includes('?')) {
    -      url += `&createServerFn`
    -    } else {
    -      url += `?createServerFn`
    -    }
    -
         let body = undefined
         if (first.method === 'POST') {
           const fetchBody = await getFetchBody(first)
    @@ -97,6 +102,7 @@ export async function serverFnFetcher(
         handler(url, {
           method: 'POST',
           headers: {
    +        'x-tsr-serverFn': 'true',
             Accept: 'application/json',
             'Content-Type': 'application/json',
           },
    @@ -165,7 +171,7 @@ async function getFetchBody(
     async function getResponse(fn: () => Promise) {
       const response = await (async () => {
         try {
    -      return await fn()
    +      return await fn() // client => server => fn => server => client
         } catch (error) {
           if (error instanceof Response) {
             return error
    @@ -178,22 +184,16 @@ async function getResponse(fn: () => Promise) {
       if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') {
         return response
       }
    +
       const contentType = response.headers.get('content-type')
       invariant(contentType, 'expected content-type header to be set')
       const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)
    -  // If the response is not ok, throw an error
    -  if (!response.ok) {
    -    if (serializedByStart && contentType.includes('application/json')) {
    -      const jsonPayload = await response.json()
    -      const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
    -      throw result
    -    }
    -
    -    throw new Error(await response.text())
    -  }
     
    +  // If the response is serialized by the start server, we need to process it
    +  // differently than a normal response.
       if (serializedByStart) {
         let result
    +    // If it's a stream from the start serializer, process it as such
         if (contentType.includes('application/x-ndjson')) {
           const refs = new Map()
           result = await processServerFnResponse({
    @@ -206,17 +206,22 @@ async function getResponse(fn: () => Promise) {
             },
           })
         }
    +    // If it's a JSON response, it can be simpler
         if (contentType.includes('application/json')) {
           const jsonPayload = await response.json()
           result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
         }
    +
         invariant(result, 'expected result to be resolved')
         if (result instanceof Error) {
           throw result
         }
    +
         return result
       }
     
    +  // If it wasn't processed by the start serializer, check
    +  // if it's JSON
       if (contentType.includes('application/json')) {
         const jsonPayload = await response.json()
         const redirect = parseRedirect(jsonPayload)
    @@ -229,6 +234,12 @@ async function getResponse(fn: () => Promise) {
         return jsonPayload
       }
     
    +  // Othwerwise, if it's not OK, throw the content
    +  if (!response.ok) {
    +    throw new Error(await response.text())
    +  }
    +
    +  // Or return the response itself
       return response
     }
     
    diff --git a/packages/start-client-core/src/constants.ts b/packages/start-client-core/src/constants.ts
    index 1e541af3231..4e0777068d2 100644
    --- a/packages/start-client-core/src/constants.ts
    +++ b/packages/start-client-core/src/constants.ts
    @@ -6,4 +6,5 @@ export const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(
     
     export const X_TSS_SERIALIZED = 'x-tss-serialized'
     export const X_TSS_RAW_RESPONSE = 'x-tss-raw'
    +export const X_TSS_CONTEXT = 'x-tss-context'
     export {}
    diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts
    index 0fecefcd50f..b472d88d795 100644
    --- a/packages/start-client-core/src/createServerFn.ts
    +++ b/packages/start-client-core/src/createServerFn.ts
    @@ -1,9 +1,9 @@
     import { mergeHeaders } from '@tanstack/router-core/ssr/client'
     
    +import { isRedirect, parseRedirect } from '@tanstack/router-core'
     import { TSS_SERVER_FUNCTION_FACTORY } from './constants'
     import { getStartOptions } from './getStartOptions'
     import { getStartContextServerOnly } from './getStartContextServerOnly'
    -import type { TSS_SERVER_FUNCTION } from './constants'
     import type {
       AnyValidator,
       Constrain,
    @@ -15,6 +15,7 @@ import type {
       ValidateSerializableInput,
       Validator,
     } from '@tanstack/router-core'
    +import type { TSS_SERVER_FUNCTION } from './constants'
     import type {
       AnyFunctionMiddleware,
       AnyRequestMiddleware,
    @@ -120,6 +121,11 @@ export const createServerFn: CreateServerFn = (options, __opts) => {
                 context: {},
               })
     
    +          const redirect = parseRedirect(result.error)
    +          if (redirect) {
    +            throw redirect
    +          }
    +
               if (result.error) throw result.error
               return result.result
             },
    @@ -143,14 +149,18 @@ export const createServerFn: CreateServerFn = (options, __opts) => {
                   request: startContext.request,
                 }
     
    -            return executeMiddleware(resolvedMiddleware, 'server', ctx).then(
    -              (d) => ({
    -                // Only send the result and sendContext back to the client
    -                result: d.result,
    -                error: d.error,
    -                context: d.sendContext,
    -              }),
    -            )
    +            const result = await executeMiddleware(
    +              resolvedMiddleware,
    +              'server',
    +              ctx,
    +            ).then((d) => ({
    +              // Only send the result and sendContext back to the client
    +              result: d.result,
    +              error: d.error,
    +              context: d.sendContext,
    +            }))
    +
    +            return result
               },
             },
           ) as any
    @@ -257,6 +267,22 @@ export async function executeMiddleware(
               next: userNext as any,
             } as any)
     
    +        // If result is NOT a ctx object, we need to return it as
    +        // the { result }
    +        if (isRedirect(result)) {
    +          return {
    +            ...ctx,
    +            error: result,
    +          }
    +        }
    +
    +        if (result instanceof Response) {
    +          return {
    +            ...ctx,
    +            result,
    +          }
    +        }
    +
             if (!(result as any)) {
               throw new Error(
                 'User middleware returned undefined. You must call next() or return a result in your middlewares.',
    diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx
    index 9ba2eeaa71f..10a84f299bd 100644
    --- a/packages/start-client-core/src/index.tsx
    +++ b/packages/start-client-core/src/index.tsx
    @@ -84,6 +84,7 @@ export {
       TSS_SERVER_FUNCTION,
       X_TSS_SERIALIZED,
       X_TSS_RAW_RESPONSE,
    +  X_TSS_CONTEXT,
     } from './constants'
     
     export type * from './serverRoute'
    diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts
    index d0fd84e8f43..ff7bd0f4456 100644
    --- a/packages/start-server-core/src/createStartHandler.ts
    +++ b/packages/start-server-core/src/createStartHandler.ts
    @@ -268,16 +268,17 @@ export function createStartHandler(
           [...middlewares, requestHandlerMiddleware],
           {
             request,
    -
             context: requestOpts?.context || {},
           },
         )
     
         const response: Response = ctx.response
     
    +    console.log('response', response)
    +
         if (isRedirect(response)) {
           if (isResolvedRedirect(response)) {
    -        if (request.headers.get('x-tsr-redirect') === 'manual') {
    +        if (request.headers.get('x-tsr-createServerFn') === 'true') {
               return json(
                 {
                   ...response.options,
    @@ -318,7 +319,7 @@ export function createStartHandler(
           const router = await getRouter()
           const redirect = router.resolveRedirect(response)
     
    -      if (request.headers.get('x-tsr-redirect') === 'manual') {
    +      if (request.headers.get('x-tsr-createServerFn') === 'true') {
             return json(
               {
                 ...response.options,
    diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts
    index ec9042ec473..221ca5af004 100644
    --- a/packages/start-server-core/src/server-functions-handler.ts
    +++ b/packages/start-server-core/src/server-functions-handler.ts
    @@ -1,10 +1,11 @@
    -import { isNotFound } from '@tanstack/router-core'
    +import { isNotFound, isPlainObject } from '@tanstack/router-core'
     import invariant from 'tiny-invariant'
     import {
       TSS_FORMDATA_CONTEXT,
       X_TSS_RAW_RESPONSE,
       X_TSS_SERIALIZED,
       getDefaultSerovalPlugins,
    +  json,
     } from '@tanstack/start-client-core'
     import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
     import { getResponse } from './request-response'
    @@ -41,7 +42,9 @@ export const handleServerAction = async ({
         createServerFn?: boolean
       }
     
    -  const isCreateServerFn = 'createServerFn' in search
    +  const isServerFn = request.headers.get('x-tsr-serverFn') === 'true'
    +  const isCreateServerFn =
    +    request.headers.get('x-tsr-createServerFn') === 'true'
     
       if (typeof serverFnId !== 'string') {
         throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
    @@ -56,6 +59,9 @@ export const handleServerAction = async ({
       ]
     
       const contentType = request.headers.get('Content-Type')
    +  const isFormData = formDataContentTypes.some(
    +    (type) => contentType && contentType.includes(type),
    +  )
       const serovalPlugins = getDefaultSerovalPlugins()
     
       function parsePayload(payload: any) {
    @@ -65,13 +71,9 @@ export const handleServerAction = async ({
     
       const response = await (async () => {
         try {
    -      let result = await (async () => {
    +      let res = await (async () => {
             // FormData
    -        if (
    -          formDataContentTypes.some(
    -            (type) => contentType && contentType.includes(type),
    -          )
    -        ) {
    +        if (isFormData) {
               // We don't support GET requests with FormData payloads... that seems impossible
               invariant(
                 method.toLowerCase() !== 'get',
    @@ -135,25 +137,55 @@ export const handleServerAction = async ({
             return await action(...jsonPayload)
           })()
     
    -      // Any time we get a Response back, we should just
    -      // return it immediately.
    -      if (result.result instanceof Response) {
    -        result.result.headers.set(X_TSS_RAW_RESPONSE, 'true')
    -        return result.result
    -      }
    +      const isCtxResult =
    +        isPlainObject(res) &&
    +        'context' in res &&
    +        ('result' in res || 'error' in res)
    +
    +      console.log(
    +        {
    +          isServerFn,
    +          isCreateServerFn,
    +          isFormData,
    +          isCtxResult,
    +        },
    +        res,
    +      )
     
    -      // If this is a non createServerFn request, we need to
    -      // pull out the result from the result object
    -      if (!isCreateServerFn) {
    -        result = result.result
    +      function unwrapResultOrError(result: any) {
    +        if (
    +          isPlainObject(result) &&
    +          ('result' in result || 'error' in result)
    +        ) {
    +          console.log('tanner')
    +          return result.result || result.error
    +        }
    +        return result
    +      }
     
    -        // The result might again be a response,
    -        // and if it is, return it.
    -        if (result instanceof Response) {
    -          return result
    +      // This was not called by the serverFnFetcher, so it's likely a no-JS POST request)
    +      if (isCtxResult) {
    +        const unwrapped = unwrapResultOrError(res)
    +        if (unwrapped instanceof Response) {
    +          res = unwrapped
    +        } else {
    +          res = json(unwrapped)
             }
           }
     
    +      if (isNotFound(res)) {
    +        res = isNotFoundResponse(res)
    +      }
    +
    +      if (!isServerFn) {
    +        return res
    +      }
    +
    +      if (res instanceof Response) {
    +        res.headers.set(X_TSS_RAW_RESPONSE, 'true')
    +        return res
    +      }
    +
           // TODO: RSCs Where are we getting this package?
           // if (isValidElement(result)) {
           //   const { renderToPipeableStream } = await import(
    @@ -173,91 +205,91 @@ export const handleServerAction = async ({
           //   return new Response(null, { status: 200 })
           // }
     
    -      if (isNotFound(result)) {
    -        return isNotFoundResponse(result)
    -      }
    -
    -      const response = getResponse()
    -      let nonStreamingBody: any = undefined
    -
    -      if (result !== undefined) {
    -        // first run without the stream in case `result` does not need streaming
    -        let done = false as boolean
    -        const callbacks: {
    -          onParse: (value: any) => void
    -          onDone: () => void
    -          onError: (error: any) => void
    -        } = {
    -          onParse: (value) => {
    -            nonStreamingBody = value
    -          },
    -          onDone: () => {
    -            done = true
    -          },
    -          onError: (error) => {
    -            throw error
    -          },
    -        }
    -        toCrossJSONStream(result, {
    -          refs: new Map(),
    -          plugins: serovalPlugins,
    -          onParse(value) {
    -            callbacks.onParse(value)
    -          },
    -          onDone() {
    -            callbacks.onDone()
    -          },
    -          onError: (error) => {
    -            callbacks.onError(error)
    -          },
    -        })
    -        if (done) {
    -          return new Response(
    -            nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
    -            {
    -              status: response?.status,
    -              statusText: response?.statusText,
    -              headers: {
    -                'Content-Type': 'application/json',
    -                [X_TSS_SERIALIZED]: 'true',
    +      return serializeResult(res)
    +
    +      function serializeResult(res: unknown): Response {
    +        let nonStreamingBody: any = undefined
    +
    +        const alsResponse = getResponse()
    +        if (res !== undefined) {
    +          // first run without the stream in case `result` does not need streaming
    +          let done = false as boolean
    +          const callbacks: {
    +            onParse: (value: any) => void
    +            onDone: () => void
    +            onError: (error: any) => void
    +          } = {
    +            onParse: (value) => {
    +              nonStreamingBody = value
    +            },
    +            onDone: () => {
    +              done = true
    +            },
    +            onError: (error) => {
    +              throw error
    +            },
    +          }
    +          toCrossJSONStream(res, {
    +            refs: new Map(),
    +            plugins: serovalPlugins,
    +            onParse(value) {
    +              callbacks.onParse(value)
    +            },
    +            onDone() {
    +              callbacks.onDone()
    +            },
    +            onError: (error) => {
    +              callbacks.onError(error)
    +            },
    +          })
    +          if (done) {
    +            return new Response(
    +              nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
    +              {
    +                status: alsResponse?.status,
    +                statusText: alsResponse?.statusText,
    +                headers: {
    +                  'Content-Type': 'application/json',
    +                  [X_TSS_SERIALIZED]: 'true',
    +                },
                   },
    +            )
    +          }
    +
    +          // not done yet, we need to stream
    +          const stream = new ReadableStream({
    +            start(controller) {
    +              callbacks.onParse = (value) =>
    +                controller.enqueue(JSON.stringify(value) + '\n')
    +              callbacks.onDone = () => {
    +                try {
    +                  controller.close()
    +                } catch (error) {
    +                  controller.error(error)
    +                }
    +              }
    +              callbacks.onError = (error) => controller.error(error)
    +              // stream the initial body
    +              if (nonStreamingBody !== undefined) {
    +                callbacks.onParse(nonStreamingBody)
    +              }
                 },
    -          )
    +          })
    +          return new Response(stream, {
    +            status: alsResponse?.status,
    +            statusText: alsResponse?.statusText,
    +            headers: {
    +              'Content-Type': 'application/x-ndjson',
    +              [X_TSS_SERIALIZED]: 'true',
    +            },
    +          })
             }
     
    -        // not done yet, we need to stream
    -        const stream = new ReadableStream({
    -          start(controller) {
    -            callbacks.onParse = (value) =>
    -              controller.enqueue(JSON.stringify(value) + '\n')
    -            callbacks.onDone = () => {
    -              try {
    -                controller.close()
    -              } catch (error) {
    -                controller.error(error)
    -              }
    -            }
    -            callbacks.onError = (error) => controller.error(error)
    -            // stream the initial body
    -            if (nonStreamingBody !== undefined) {
    -              callbacks.onParse(nonStreamingBody)
    -            }
    -          },
    -        })
    -        return new Response(stream, {
    -          status: response?.status,
    -          statusText: response?.statusText,
    -          headers: {
    -            'Content-Type': 'application/x-ndjson',
    -            [X_TSS_SERIALIZED]: 'true',
    -          },
    +        return new Response(undefined, {
    +          status: alsResponse?.status,
    +          statusText: alsResponse?.statusText,
             })
           }
    -
    -      return new Response(undefined, {
    -        status: response?.status,
    -        statusText: response?.statusText,
    -      })
         } catch (error: any) {
           if (error instanceof Response) {
             return error
    
    From 61586767c1ee71a5766b5127dc342eaf84edc809 Mon Sep 17 00:00:00 2001
    From: Tanner Linsley 
    Date: Thu, 6 Nov 2025 21:01:46 -0700
    Subject: [PATCH 04/10] Building a preact adapter for tanstack router
    
    ---
     .../server-functions/count-effect.txt         |   1 +
     .../server-functions/src/routes/__root.tsx    |   4 +
     .../server-functions/src/routes/status.tsx    |  43 +-
     .../tests/server-functions.spec.ts            |   2 +-
     .../server-functions/count-effect.txt         |   1 +
     .../preact/authenticated-routes/index.html    |  13 +
     .../preact/authenticated-routes/package.json  |  28 +
     .../preact/authenticated-routes/src/auth.tsx  |  65 ++
     .../preact/authenticated-routes/src/main.tsx  |  43 +
     .../preact/authenticated-routes/src/posts.ts  |  24 +
     .../src/routes/__root.tsx                     |  18 +
     .../src/routes/_auth.dashboard.tsx            |  19 +
     .../src/routes/_auth.invoices.$invoiceId.tsx  |  31 +
     .../src/routes/_auth.invoices.index.tsx       |   6 +
     .../src/routes/_auth.invoices.tsx             |  44 +
     .../authenticated-routes/src/routes/_auth.tsx |  71 ++
     .../authenticated-routes/src/routes/index.tsx |  47 +
     .../authenticated-routes/src/routes/login.tsx |  95 ++
     .../authenticated-routes/src/styles.css       |  14 +
     .../preact/authenticated-routes/src/utils.ts  |   4 +
     .../preact/authenticated-routes/tsconfig.json |  11 +
     .../authenticated-routes/vite.config.js       |  15 +
     examples/preact/basic-file-based/index.html   |  13 +
     examples/preact/basic-file-based/package.json |  28 +
     .../basic-file-based/postcss.config.mjs       |   7 +
     examples/preact/basic-file-based/src/main.tsx |  26 +
     .../preact/basic-file-based/src/posts.tsx     |  33 +
     .../basic-file-based/src/routes/__root.tsx    |  71 ++
     .../src/routes/_pathlessLayout.tsx            |  18 +
     .../routes/_pathlessLayout/_nested-layout.tsx |  36 +
     .../_nested-layout/route-a.tsx                |  11 +
     .../_nested-layout/route-b.tsx                |  11 +
     .../basic-file-based/src/routes/anchor.tsx    | 208 ++++
     .../basic-file-based/src/routes/index.tsx     |  14 +
     .../src/routes/posts.$postId.tsx              |  29 +
     .../src/routes/posts.index.tsx                |  10 +
     .../src/routes/posts.route.tsx                |  40 +
     .../preact/basic-file-based/src/styles.css    |  14 +
     .../basic-file-based/tailwind.config.mjs      |   9 +
     .../preact/basic-file-based/tsconfig.json     |  11 +
     .../preact/basic-file-based/vite.config.js    |  15 +
     examples/preact/basic-preact-query/index.html |  13 +
     .../preact/basic-preact-query/package.json    |  28 +
     .../preact/basic-preact-query/src/main.tsx    | 279 ++++++
     .../basic-preact-query/src/posts.lazy.tsx     |  40 +
     .../preact/basic-preact-query/src/posts.ts    |  45 +
     .../preact/basic-preact-query/src/styles.css  |  14 +
     .../preact/basic-preact-query/tsconfig.json   |  11 +
     .../preact/basic-preact-query/vite.config.js  |   8 +
     examples/preact/basic/.gitignore              |  10 +
     examples/preact/basic/.vscode/settings.json   |  11 +
     examples/preact/basic/README.md               |   6 +
     examples/preact/basic/index.html              |  12 +
     examples/preact/basic/package.json            |  25 +
     examples/preact/basic/postcss.config.mjs      |   6 +
     examples/preact/basic/server.js               |  47 +
     examples/preact/basic/src/entry-client.tsx    |   8 +
     examples/preact/basic/src/entry-server.tsx    |  84 ++
     examples/preact/basic/src/main.tsx            | 230 +++++
     examples/preact/basic/src/posts.lazy.tsx      |  35 +
     examples/preact/basic/src/posts.ts            |  32 +
     examples/preact/basic/src/preact.d.ts         |   6 +
     examples/preact/basic/src/router.tsx          | 105 ++
     examples/preact/basic/src/styles.css          |  13 +
     examples/preact/basic/tailwind.config.mjs     |   4 +
     examples/preact/basic/tsconfig.json           |  13 +
     examples/preact/basic/vite.config.js          |   7 +
     examples/preact/scroll-restoration/index.html |  13 +
     .../preact/scroll-restoration/package.json    |  26 +
     .../preact/scroll-restoration/src/main.tsx    | 211 ++++
     .../preact/scroll-restoration/src/styles.css  |  14 +
     .../preact/scroll-restoration/tsconfig.json   |  11 +
     .../preact/scroll-restoration/vite.config.js  |   8 +
     examples/react/basic/src/main.tsx             |   2 +-
     .../vanilla/authenticated-routes/index.html   |  33 +
     .../vanilla/authenticated-routes/package.json |  19 +
     .../vanilla/authenticated-routes/src/main.ts  | 234 +++++
     .../authenticated-routes/tsconfig.json        |  20 +
     .../authenticated-routes/vite.config.ts       |  14 +
     examples/vanilla/basic/index.html             |  69 ++
     examples/vanilla/basic/package.json           |  20 +
     examples/vanilla/basic/src/main.ts            | 153 +++
     examples/vanilla/basic/src/posts.ts           |  32 +
     examples/vanilla/basic/tsconfig.json          |  19 +
     examples/vanilla/basic/vite.config.js         |  15 +
     examples/vanilla/jsx-router/index.html        |  83 ++
     examples/vanilla/jsx-router/package.json      |  19 +
     examples/vanilla/jsx-router/src/main.ts       | 176 ++++
     examples/vanilla/jsx-router/src/renderer.ts   | 263 +++++
     examples/vanilla/jsx-router/tsconfig.json     |  22 +
     examples/vanilla/jsx-router/vite.config.ts    |  14 +
     .../vanilla/scroll-restoration/index.html     |  27 +
     .../vanilla/scroll-restoration/package.json   |  19 +
     .../vanilla/scroll-restoration/src/main.ts    | 137 +++
     .../vanilla/scroll-restoration/tsconfig.json  |  20 +
     .../vanilla/scroll-restoration/vite.config.ts |  14 +
     packages/preact-router/README.md              |  31 +
     packages/preact-router/build-no-check.js      |   5 +
     packages/preact-router/eslint.config.ts       |  24 +
     packages/preact-router/package.json           | 117 +++
     packages/preact-router/src/Asset.tsx          | 164 +++
     packages/preact-router/src/CatchBoundary.tsx  | 121 +++
     packages/preact-router/src/ClientOnly.tsx     |  69 ++
     packages/preact-router/src/HeadContent.tsx    | 210 ++++
     packages/preact-router/src/Match.tsx          | 360 +++++++
     packages/preact-router/src/Matches.tsx        | 266 +++++
     packages/preact-router/src/RouterProvider.tsx |  81 ++
     packages/preact-router/src/SafeFragment.tsx   |   6 +
     packages/preact-router/src/ScriptOnce.tsx     |  18 +
     packages/preact-router/src/Scripts.tsx        |  66 ++
     .../preact-router/src/ScrollRestoration.tsx   |  69 ++
     packages/preact-router/src/Transitioner.tsx   | 128 +++
     packages/preact-router/src/awaited.tsx        |  50 +
     packages/preact-router/src/fileRoute.ts       | 278 ++++++
     packages/preact-router/src/history.ts         |   9 +
     packages/preact-router/src/index.tsx          | 356 +++++++
     .../preact-router/src/lazyRouteComponent.tsx  |  83 ++
     packages/preact-router/src/link.tsx           | 579 +++++++++++
     packages/preact-router/src/matchContext.tsx   |   8 +
     packages/preact-router/src/not-found.tsx      |  42 +
     packages/preact-router/src/preact-store.ts    |  66 ++
     .../preact-router/src/renderRouteNotFound.tsx |  28 +
     packages/preact-router/src/route.tsx          | 711 +++++++++++++
     packages/preact-router/src/router.ts          | 116 +++
     packages/preact-router/src/routerContext.tsx  |  25 +
     .../preact-router/src/scroll-restoration.tsx  |  43 +
     .../preact-router/src/ssr/RouterClient.tsx    |  22 +
     .../preact-router/src/ssr/RouterServer.tsx    |   8 +
     packages/preact-router/src/ssr/client.ts      |   2 +
     .../src/ssr/defaultRenderHandler.tsx          |  12 +
     .../src/ssr/defaultStreamHandler.tsx          |  13 +
     .../src/ssr/renderRouterToStream.tsx          |  67 ++
     .../src/ssr/renderRouterToString.tsx          |  34 +
     packages/preact-router/src/ssr/serializer.ts  |   7 +
     packages/preact-router/src/ssr/server.ts      |   6 +
     .../preact-router/src/structuralSharing.ts    |  47 +
     packages/preact-router/src/typePrimitives.ts  |  84 ++
     packages/preact-router/src/useBlocker.tsx     | 311 ++++++
     packages/preact-router/src/useCanGoBack.ts    |   5 +
     packages/preact-router/src/useLoaderData.tsx  |  80 ++
     packages/preact-router/src/useLoaderDeps.tsx  |  58 ++
     packages/preact-router/src/useLocation.tsx    |  41 +
     packages/preact-router/src/useMatch.tsx       | 120 +++
     packages/preact-router/src/useNavigate.tsx    |  55 +
     packages/preact-router/src/useParams.tsx      |  95 ++
     packages/preact-router/src/useRouteContext.ts |  30 +
     packages/preact-router/src/useRouter.tsx      |  15 +
     packages/preact-router/src/useRouterState.tsx |  59 ++
     packages/preact-router/src/useSearch.tsx      |  93 ++
     packages/preact-router/src/utils.ts           | 114 +++
     packages/preact-router/tsconfig.json          |   9 +
     packages/preact-router/tsconfig.legacy.json   |   4 +
     packages/preact-router/vite.config.ts         |  25 +
     .../start-client-core/src/createServerFn.ts   |  25 +
     .../src/createStartHandler.ts                 |  39 +-
     .../start-server-core/src/request-response.ts |  59 ++
     .../src/server-functions-handler.ts           |  48 +-
     .../RENDERER_ABSTRACTION_PROPOSAL.md          | 251 +++++
     .../examples/renderer/renderer.ts             | 394 ++++++++
     .../examples/router-jsx-example.html          | 261 +++++
     .../examples/vanilla-dom-example.ts           | 130 +++
     .../examples/vanilla-jsx-example.ts           | 150 +++
     packages/vanilla-router/package.json          |  77 ++
     packages/vanilla-router/src/error-handling.ts |  71 ++
     packages/vanilla-router/src/fileRoute.ts      | 267 +++++
     packages/vanilla-router/src/index.ts          | 101 ++
     packages/vanilla-router/src/navigation.ts     |  73 ++
     packages/vanilla-router/src/route-data.ts     | 116 +++
     packages/vanilla-router/src/route.ts          | 421 ++++++++
     packages/vanilla-router/src/router.ts         |  86 ++
     .../vanilla-router/src/scroll-restoration.ts  | 123 +++
     packages/vanilla-router/src/types.ts          |   8 +
     packages/vanilla-router/src/utils.ts          |  90 ++
     packages/vanilla-router/src/vanilla-router.ts | 246 +++++
     packages/vanilla-router/tests/router.test.ts  | 156 +++
     packages/vanilla-router/tsconfig.json         |   7 +
     packages/vanilla-router/vite.config.ts        |  38 +
     pnpm-lock.yaml                                | 936 +++++++++++++++++-
     178 files changed, 13636 insertions(+), 84 deletions(-)
     create mode 100644 e2e/react-start/server-functions/count-effect.txt
     create mode 100644 e2e/solid-start/server-functions/count-effect.txt
     create mode 100644 examples/preact/authenticated-routes/index.html
     create mode 100644 examples/preact/authenticated-routes/package.json
     create mode 100644 examples/preact/authenticated-routes/src/auth.tsx
     create mode 100644 examples/preact/authenticated-routes/src/main.tsx
     create mode 100644 examples/preact/authenticated-routes/src/posts.ts
     create mode 100644 examples/preact/authenticated-routes/src/routes/__root.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/_auth.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/index.tsx
     create mode 100644 examples/preact/authenticated-routes/src/routes/login.tsx
     create mode 100644 examples/preact/authenticated-routes/src/styles.css
     create mode 100644 examples/preact/authenticated-routes/src/utils.ts
     create mode 100644 examples/preact/authenticated-routes/tsconfig.json
     create mode 100644 examples/preact/authenticated-routes/vite.config.js
     create mode 100644 examples/preact/basic-file-based/index.html
     create mode 100644 examples/preact/basic-file-based/package.json
     create mode 100644 examples/preact/basic-file-based/postcss.config.mjs
     create mode 100644 examples/preact/basic-file-based/src/main.tsx
     create mode 100644 examples/preact/basic-file-based/src/posts.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/__root.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/anchor.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/index.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/posts.$postId.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/posts.index.tsx
     create mode 100644 examples/preact/basic-file-based/src/routes/posts.route.tsx
     create mode 100644 examples/preact/basic-file-based/src/styles.css
     create mode 100644 examples/preact/basic-file-based/tailwind.config.mjs
     create mode 100644 examples/preact/basic-file-based/tsconfig.json
     create mode 100644 examples/preact/basic-file-based/vite.config.js
     create mode 100644 examples/preact/basic-preact-query/index.html
     create mode 100644 examples/preact/basic-preact-query/package.json
     create mode 100644 examples/preact/basic-preact-query/src/main.tsx
     create mode 100644 examples/preact/basic-preact-query/src/posts.lazy.tsx
     create mode 100644 examples/preact/basic-preact-query/src/posts.ts
     create mode 100644 examples/preact/basic-preact-query/src/styles.css
     create mode 100644 examples/preact/basic-preact-query/tsconfig.json
     create mode 100644 examples/preact/basic-preact-query/vite.config.js
     create mode 100644 examples/preact/basic/.gitignore
     create mode 100644 examples/preact/basic/.vscode/settings.json
     create mode 100644 examples/preact/basic/README.md
     create mode 100644 examples/preact/basic/index.html
     create mode 100644 examples/preact/basic/package.json
     create mode 100644 examples/preact/basic/postcss.config.mjs
     create mode 100644 examples/preact/basic/server.js
     create mode 100644 examples/preact/basic/src/entry-client.tsx
     create mode 100644 examples/preact/basic/src/entry-server.tsx
     create mode 100644 examples/preact/basic/src/main.tsx
     create mode 100644 examples/preact/basic/src/posts.lazy.tsx
     create mode 100644 examples/preact/basic/src/posts.ts
     create mode 100644 examples/preact/basic/src/preact.d.ts
     create mode 100644 examples/preact/basic/src/router.tsx
     create mode 100644 examples/preact/basic/src/styles.css
     create mode 100644 examples/preact/basic/tailwind.config.mjs
     create mode 100644 examples/preact/basic/tsconfig.json
     create mode 100644 examples/preact/basic/vite.config.js
     create mode 100644 examples/preact/scroll-restoration/index.html
     create mode 100644 examples/preact/scroll-restoration/package.json
     create mode 100644 examples/preact/scroll-restoration/src/main.tsx
     create mode 100644 examples/preact/scroll-restoration/src/styles.css
     create mode 100644 examples/preact/scroll-restoration/tsconfig.json
     create mode 100644 examples/preact/scroll-restoration/vite.config.js
     create mode 100644 examples/vanilla/authenticated-routes/index.html
     create mode 100644 examples/vanilla/authenticated-routes/package.json
     create mode 100644 examples/vanilla/authenticated-routes/src/main.ts
     create mode 100644 examples/vanilla/authenticated-routes/tsconfig.json
     create mode 100644 examples/vanilla/authenticated-routes/vite.config.ts
     create mode 100644 examples/vanilla/basic/index.html
     create mode 100644 examples/vanilla/basic/package.json
     create mode 100644 examples/vanilla/basic/src/main.ts
     create mode 100644 examples/vanilla/basic/src/posts.ts
     create mode 100644 examples/vanilla/basic/tsconfig.json
     create mode 100644 examples/vanilla/basic/vite.config.js
     create mode 100644 examples/vanilla/jsx-router/index.html
     create mode 100644 examples/vanilla/jsx-router/package.json
     create mode 100644 examples/vanilla/jsx-router/src/main.ts
     create mode 100644 examples/vanilla/jsx-router/src/renderer.ts
     create mode 100644 examples/vanilla/jsx-router/tsconfig.json
     create mode 100644 examples/vanilla/jsx-router/vite.config.ts
     create mode 100644 examples/vanilla/scroll-restoration/index.html
     create mode 100644 examples/vanilla/scroll-restoration/package.json
     create mode 100644 examples/vanilla/scroll-restoration/src/main.ts
     create mode 100644 examples/vanilla/scroll-restoration/tsconfig.json
     create mode 100644 examples/vanilla/scroll-restoration/vite.config.ts
     create mode 100644 packages/preact-router/README.md
     create mode 100644 packages/preact-router/build-no-check.js
     create mode 100644 packages/preact-router/eslint.config.ts
     create mode 100644 packages/preact-router/package.json
     create mode 100644 packages/preact-router/src/Asset.tsx
     create mode 100644 packages/preact-router/src/CatchBoundary.tsx
     create mode 100644 packages/preact-router/src/ClientOnly.tsx
     create mode 100644 packages/preact-router/src/HeadContent.tsx
     create mode 100644 packages/preact-router/src/Match.tsx
     create mode 100644 packages/preact-router/src/Matches.tsx
     create mode 100644 packages/preact-router/src/RouterProvider.tsx
     create mode 100644 packages/preact-router/src/SafeFragment.tsx
     create mode 100644 packages/preact-router/src/ScriptOnce.tsx
     create mode 100644 packages/preact-router/src/Scripts.tsx
     create mode 100644 packages/preact-router/src/ScrollRestoration.tsx
     create mode 100644 packages/preact-router/src/Transitioner.tsx
     create mode 100644 packages/preact-router/src/awaited.tsx
     create mode 100644 packages/preact-router/src/fileRoute.ts
     create mode 100644 packages/preact-router/src/history.ts
     create mode 100644 packages/preact-router/src/index.tsx
     create mode 100644 packages/preact-router/src/lazyRouteComponent.tsx
     create mode 100644 packages/preact-router/src/link.tsx
     create mode 100644 packages/preact-router/src/matchContext.tsx
     create mode 100644 packages/preact-router/src/not-found.tsx
     create mode 100644 packages/preact-router/src/preact-store.ts
     create mode 100644 packages/preact-router/src/renderRouteNotFound.tsx
     create mode 100644 packages/preact-router/src/route.tsx
     create mode 100644 packages/preact-router/src/router.ts
     create mode 100644 packages/preact-router/src/routerContext.tsx
     create mode 100644 packages/preact-router/src/scroll-restoration.tsx
     create mode 100644 packages/preact-router/src/ssr/RouterClient.tsx
     create mode 100644 packages/preact-router/src/ssr/RouterServer.tsx
     create mode 100644 packages/preact-router/src/ssr/client.ts
     create mode 100644 packages/preact-router/src/ssr/defaultRenderHandler.tsx
     create mode 100644 packages/preact-router/src/ssr/defaultStreamHandler.tsx
     create mode 100644 packages/preact-router/src/ssr/renderRouterToStream.tsx
     create mode 100644 packages/preact-router/src/ssr/renderRouterToString.tsx
     create mode 100644 packages/preact-router/src/ssr/serializer.ts
     create mode 100644 packages/preact-router/src/ssr/server.ts
     create mode 100644 packages/preact-router/src/structuralSharing.ts
     create mode 100644 packages/preact-router/src/typePrimitives.ts
     create mode 100644 packages/preact-router/src/useBlocker.tsx
     create mode 100644 packages/preact-router/src/useCanGoBack.ts
     create mode 100644 packages/preact-router/src/useLoaderData.tsx
     create mode 100644 packages/preact-router/src/useLoaderDeps.tsx
     create mode 100644 packages/preact-router/src/useLocation.tsx
     create mode 100644 packages/preact-router/src/useMatch.tsx
     create mode 100644 packages/preact-router/src/useNavigate.tsx
     create mode 100644 packages/preact-router/src/useParams.tsx
     create mode 100644 packages/preact-router/src/useRouteContext.ts
     create mode 100644 packages/preact-router/src/useRouter.tsx
     create mode 100644 packages/preact-router/src/useRouterState.tsx
     create mode 100644 packages/preact-router/src/useSearch.tsx
     create mode 100644 packages/preact-router/src/utils.ts
     create mode 100644 packages/preact-router/tsconfig.json
     create mode 100644 packages/preact-router/tsconfig.legacy.json
     create mode 100644 packages/preact-router/vite.config.ts
     create mode 100644 packages/vanilla-router/RENDERER_ABSTRACTION_PROPOSAL.md
     create mode 100644 packages/vanilla-router/examples/renderer/renderer.ts
     create mode 100644 packages/vanilla-router/examples/router-jsx-example.html
     create mode 100644 packages/vanilla-router/examples/vanilla-dom-example.ts
     create mode 100644 packages/vanilla-router/examples/vanilla-jsx-example.ts
     create mode 100644 packages/vanilla-router/package.json
     create mode 100644 packages/vanilla-router/src/error-handling.ts
     create mode 100644 packages/vanilla-router/src/fileRoute.ts
     create mode 100644 packages/vanilla-router/src/index.ts
     create mode 100644 packages/vanilla-router/src/navigation.ts
     create mode 100644 packages/vanilla-router/src/route-data.ts
     create mode 100644 packages/vanilla-router/src/route.ts
     create mode 100644 packages/vanilla-router/src/router.ts
     create mode 100644 packages/vanilla-router/src/scroll-restoration.ts
     create mode 100644 packages/vanilla-router/src/types.ts
     create mode 100644 packages/vanilla-router/src/utils.ts
     create mode 100644 packages/vanilla-router/src/vanilla-router.ts
     create mode 100644 packages/vanilla-router/tests/router.test.ts
     create mode 100644 packages/vanilla-router/tsconfig.json
     create mode 100644 packages/vanilla-router/vite.config.ts
    
    diff --git a/e2e/react-start/server-functions/count-effect.txt b/e2e/react-start/server-functions/count-effect.txt
    new file mode 100644
    index 00000000000..d8263ee9860
    --- /dev/null
    +++ b/e2e/react-start/server-functions/count-effect.txt
    @@ -0,0 +1 @@
    +2
    \ No newline at end of file
    diff --git a/e2e/react-start/server-functions/src/routes/__root.tsx b/e2e/react-start/server-functions/src/routes/__root.tsx
    index 5dec9273376..af7afa5ca87 100644
    --- a/e2e/react-start/server-functions/src/routes/__root.tsx
    +++ b/e2e/react-start/server-functions/src/routes/__root.tsx
    @@ -8,6 +8,7 @@ import {
       createRootRouteWithContext,
     } from '@tanstack/react-router'
     
    +import { z } from 'zod'
     import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
     import { NotFound } from '~/components/NotFound'
     import appCss from '~/styles/app.css?url'
    @@ -34,6 +35,9 @@ export const Route = createRootRouteWithContext<{ foo: { bar: string } }>()({
       },
       notFoundComponent: () => ,
       component: RootComponent,
    +  validateSearch: z.object({
    +    status: z.string().optional(),
    +  }),
     })
     
     function RootComponent() {
    diff --git a/e2e/react-start/server-functions/src/routes/status.tsx b/e2e/react-start/server-functions/src/routes/status.tsx
    index b8dcdee2e65..dbe42941f3d 100644
    --- a/e2e/react-start/server-functions/src/routes/status.tsx
    +++ b/e2e/react-start/server-functions/src/routes/status.tsx
    @@ -2,11 +2,18 @@ import { createFileRoute } from '@tanstack/react-router'
     import { createServerFn, useServerFn } from '@tanstack/react-start'
     import { setResponseStatus } from '@tanstack/react-start/server'
     
    -const helloFn = createServerFn().handler(() => {
    +const simpleSetFn = createServerFn().handler(() => {
       setResponseStatus(225, `hello`)
    -  return {
    -    hello: 'world',
    -  }
    +  return null
    +})
    +
    +const requestSetFn = createServerFn().handler(() => {
    +  return new Response(undefined, { status: 226, statusText: `status-226` })
    +})
    +
    +const bothSetFn = createServerFn().handler(() => {
    +  setResponseStatus(225, `status-225`)
    +  return new Response(undefined, { status: 226, statusText: `status-226` })
     })
     
     export const Route = createFileRoute('/status')({
    @@ -14,14 +21,36 @@ export const Route = createFileRoute('/status')({
     })
     
     function StatusComponent() {
    -  const hello = useServerFn(helloFn)
    +  const simpleSet = useServerFn(simpleSetFn)
    +  const requestSet = useServerFn(requestSetFn)
    +  const bothSet = useServerFn(bothSetFn)
     
       return (
         
    + + diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts index 2edc524065d..0fa968fc41b 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -35,7 +35,7 @@ test('invoking a server function with custom response status code', async ({ resolve() }) }) - await page.getByTestId('invoke-server-fn').click() + await page.getByTestId('invoke-server-fn-simple-set').click() await requestPromise }) diff --git a/e2e/solid-start/server-functions/count-effect.txt b/e2e/solid-start/server-functions/count-effect.txt new file mode 100644 index 00000000000..56a6051ca2b --- /dev/null +++ b/e2e/solid-start/server-functions/count-effect.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/examples/preact/authenticated-routes/index.html b/examples/preact/authenticated-routes/index.html new file mode 100644 index 00000000000..433d9e052af --- /dev/null +++ b/examples/preact/authenticated-routes/index.html @@ -0,0 +1,13 @@ + + + + + + TanStack Router - Preact Authenticated Routes + + +
    + + + + diff --git a/examples/preact/authenticated-routes/package.json b/examples/preact/authenticated-routes/package.json new file mode 100644 index 00000000000..ebc5552e393 --- /dev/null +++ b/examples/preact/authenticated-routes/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-preact-example-authenticated-routes", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/preact-router-devtools": "workspace:*", + "@tanstack/router-plugin": "workspace:*", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} + diff --git a/examples/preact/authenticated-routes/src/auth.tsx b/examples/preact/authenticated-routes/src/auth.tsx new file mode 100644 index 00000000000..b8bb61dc37e --- /dev/null +++ b/examples/preact/authenticated-routes/src/auth.tsx @@ -0,0 +1,65 @@ +import { createContext } from 'preact' +import { useContext, useState, useEffect, useCallback } from 'preact/hooks' + +import { sleep } from './utils' + +export interface AuthContext { + isAuthenticated: boolean + login: (username: string) => Promise + logout: () => Promise + user: string | null +} + +const AuthContext = createContext(null) + +const key = 'tanstack.auth.user' + +function getStoredUser() { + return localStorage.getItem(key) +} + +function setStoredUser(user: string | null) { + if (user) { + localStorage.setItem(key, user) + } else { + localStorage.removeItem(key) + } +} + +export function AuthProvider({ children }: { children: preact.ComponentChildren }) { + const [user, setUser] = useState(getStoredUser()) + const isAuthenticated = !!user + + const logout = useCallback(async () => { + await sleep(250) + + setStoredUser(null) + setUser(null) + }, []) + + const login = useCallback(async (username: string) => { + await sleep(500) + + setStoredUser(username) + setUser(username) + }, []) + + useEffect(() => { + setUser(getStoredUser()) + }, []) + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} + diff --git a/examples/preact/authenticated-routes/src/main.tsx b/examples/preact/authenticated-routes/src/main.tsx new file mode 100644 index 00000000000..d91c21a76ad --- /dev/null +++ b/examples/preact/authenticated-routes/src/main.tsx @@ -0,0 +1,43 @@ +import { render } from 'preact' +import { RouterProvider, createRouter } from '@tanstack/preact-router' + +import { routeTree } from './routeTree.gen' +import { AuthProvider, useAuth } from './auth' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, + context: { + auth: undefined!, // This will be set after we wrap the app in an AuthProvider + }, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +function InnerApp() { + const auth = useAuth() + return +} + +function App() { + return ( + + + + ) +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} + diff --git a/examples/preact/authenticated-routes/src/posts.ts b/examples/preact/authenticated-routes/src/posts.ts new file mode 100644 index 00000000000..1c519123341 --- /dev/null +++ b/examples/preact/authenticated-routes/src/posts.ts @@ -0,0 +1,24 @@ +import axios from 'redaxios' + +export type InvoiceType = { + id: number + title: string + body: string +} + +export const fetchInvoices = async () => { + console.info('Fetching invoices...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchInvoiceById = async (id: number) => { + console.info(`Fetching invoice with id ${id}...`) + await new Promise((r) => setTimeout(r, 500)) + return axios + .get(`https://jsonplaceholder.typicode.com/posts/${id}`) + .then((r) => r.data) +} + diff --git a/examples/preact/authenticated-routes/src/routes/__root.tsx b/examples/preact/authenticated-routes/src/routes/__root.tsx new file mode 100644 index 00000000000..7d0f34a6940 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/__root.tsx @@ -0,0 +1,18 @@ +import { Outlet, createRootRouteWithContext } from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' + +import type { AuthContext } from '../auth' + +interface MyRouterContext { + auth: AuthContext +} + +export const Route = createRootRouteWithContext()({ + component: () => ( + <> + + + + ), +}) + diff --git a/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx b/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx new file mode 100644 index 00000000000..8acde0cf0f6 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/preact-router' + +import { useAuth } from '../auth' + +export const Route = createFileRoute('/_auth/dashboard')({ + component: DashboardPage, +}) + +function DashboardPage() { + const auth = useAuth() + + return ( +
    +

    Hi {auth.user}!

    +

    You are currently on the dashboard route.

    +
    + ) +} + diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx new file mode 100644 index 00000000000..f5732318d81 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx @@ -0,0 +1,31 @@ +import { createFileRoute } from '@tanstack/preact-router' + +import { fetchInvoiceById } from '../posts' + +export const Route = createFileRoute('/_auth/invoices/$invoiceId')({ + loader: async ({ params: { invoiceId } }) => { + return { + invoice: await fetchInvoiceById(parseInt(invoiceId)), + } + }, + component: InvoicePage, +}) + +function InvoicePage() { + const { invoice } = Route.useLoaderData() + + return ( +
    +

    + Invoice No. #{invoice.id.toString().padStart(2, '0')} +

    +

    + Invoice title: {invoice.title} +

    +

    + Invoice body: {invoice.body} +

    +
    + ) +} + diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx new file mode 100644 index 00000000000..d955e1c043a --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/preact-router' + +export const Route = createFileRoute('/_auth/invoices/')({ + component: () =>
    Select an invoice to view it!
    , +}) + diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx new file mode 100644 index 00000000000..609c3a3bce7 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx @@ -0,0 +1,44 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet } from '@tanstack/preact-router' + +import { fetchInvoices } from '../posts' + +export const Route = createFileRoute('/_auth/invoices')({ + loader: async () => ({ + invoices: await fetchInvoices(), + }), + component: InvoicesRoute, +}) + +function InvoicesRoute() { + const { invoices } = Route.useLoaderData() + + return ( +
    +
    +

    Choose an invoice from the list below.

    +
      + {invoices.map((invoice) => ( +
    1. + + + #{invoice.id.toString().padStart(2, '0')} + {' '} + - {invoice.title.slice(0, 16)}... + +
    2. + ))} +
    +
    +
    + +
    +
    + ) +} + diff --git a/examples/preact/authenticated-routes/src/routes/_auth.tsx b/examples/preact/authenticated-routes/src/routes/_auth.tsx new file mode 100644 index 00000000000..a7a2b085071 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.tsx @@ -0,0 +1,71 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet, redirect, useRouter } from '@tanstack/preact-router' + +import { useAuth } from '../auth' + +export const Route = createFileRoute('/_auth')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ + to: '/login', + search: { + redirect: location.href, + }, + }) + } + }, + component: AuthLayout, +}) + +function AuthLayout() { + const router = useRouter() + const navigate = Route.useNavigate() + const auth = useAuth() + + const handleLogout = () => { + if (window.confirm('Are you sure you want to logout?')) { + auth.logout().then(() => { + router.invalidate().finally(() => { + navigate({ to: '/' }) + }) + }) + } + } + + return ( +
    +

    Authenticated Route

    +

    This route's content is only visible to authenticated users.

    +
      +
    • + + Dashboard + +
    • +
    • + + Invoices + +
    • +
    • + +
    • +
    +
    + +
    + ) +} + diff --git a/examples/preact/authenticated-routes/src/routes/index.tsx b/examples/preact/authenticated-routes/src/routes/index.tsx new file mode 100644 index 00000000000..bd8a6bb4537 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/index.tsx @@ -0,0 +1,47 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link } from '@tanstack/preact-router' + +export const Route = createFileRoute('/')({ + component: HomeComponent, +}) + +function HomeComponent() { + return ( +
    +

    Welcome!

    +

    + IMPORTANT!!! This is just an + example of how to use authenticated routes with TanStack Router. +
    + This is NOT an example how you'd write a production-level authentication + system. +
    + You'll need to take the concepts and patterns used in this example and + adapt then to work with your authentication flow/system for your app. +

    +

    + You are currently on the index route of the{' '} + authenticated-routes example. +

    +

    You can try going through these options.

    +
      +
    1. + + Go to the public login page. + +
    2. +
    3. + + Go to the auth-only dashboard page. + +
    4. +
    5. + + Go to the auth-only invoices page. + +
    6. +
    +
    + ) +} + diff --git a/examples/preact/authenticated-routes/src/routes/login.tsx b/examples/preact/authenticated-routes/src/routes/login.tsx new file mode 100644 index 00000000000..0a15d793cb8 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/login.tsx @@ -0,0 +1,95 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { useState } from 'preact/hooks' +import { redirect, useRouter, useRouterState } from '@tanstack/preact-router' +import { z } from 'zod' + +import { useAuth } from '../auth' +import { sleep } from '../utils' + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion +const fallback = '/dashboard' as const + +export const Route = createFileRoute('/login')({ + validateSearch: z.object({ + redirect: z.string().optional().catch(''), + }), + beforeLoad: ({ context, search }) => { + if (context.auth.isAuthenticated) { + throw redirect({ to: search.redirect || fallback }) + } + }, + component: LoginComponent, +}) + +function LoginComponent() { + const auth = useAuth() + const router = useRouter() + const isLoading = useRouterState({ select: (s) => s.isLoading }) + const navigate = Route.useNavigate() + const [isSubmitting, setIsSubmitting] = useState(false) + + const search = Route.useSearch() + + const onFormSubmit = async (evt: Event) => { + setIsSubmitting(true) + try { + evt.preventDefault() + const form = evt.target as HTMLFormElement + const data = new FormData(form) + const fieldValue = data.get('username') + + if (!fieldValue) return + const username = fieldValue.toString() + await auth.login(username) + + await router.invalidate() + + // This is just a hack being used to wait for the auth state to update + // in a real app, you'd want to use a more robust solution + await sleep(1) + + await navigate({ to: search.redirect || fallback }) + } catch (error) { + console.error('Error logging in: ', error) + } finally { + setIsSubmitting(false) + } + } + + const isLoggingIn = isLoading || isSubmitting + + return ( +
    +

    Login page

    + {search.redirect ? ( +

    You need to login to access this page.

    + ) : ( +

    Login to see all the cool content in here.

    + )} +
    +
    +
    + + +
    + +
    +
    +
    + ) +} + diff --git a/examples/preact/authenticated-routes/src/styles.css b/examples/preact/authenticated-routes/src/styles.css new file mode 100644 index 00000000000..e07de1f8541 --- /dev/null +++ b/examples/preact/authenticated-routes/src/styles.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} + diff --git a/examples/preact/authenticated-routes/src/utils.ts b/examples/preact/authenticated-routes/src/utils.ts new file mode 100644 index 00000000000..5a1d7cb6b91 --- /dev/null +++ b/examples/preact/authenticated-routes/src/utils.ts @@ -0,0 +1,4 @@ +export async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + diff --git a/examples/preact/authenticated-routes/tsconfig.json b/examples/preact/authenticated-routes/tsconfig.json new file mode 100644 index 00000000000..af80d030c77 --- /dev/null +++ b/examples/preact/authenticated-routes/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} + diff --git a/examples/preact/authenticated-routes/vite.config.js b/examples/preact/authenticated-routes/vite.config.js new file mode 100644 index 00000000000..1a28c9b7f18 --- /dev/null +++ b/examples/preact/authenticated-routes/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'preact', + autoCodeSplitting: true, + }), + preact(), + ], +}) + diff --git a/examples/preact/basic-file-based/index.html b/examples/preact/basic-file-based/index.html new file mode 100644 index 00000000000..7e0378f9ade --- /dev/null +++ b/examples/preact/basic-file-based/index.html @@ -0,0 +1,13 @@ + + + + + + TanStack Router - Preact File-Based + + +
    + + + + diff --git a/examples/preact/basic-file-based/package.json b/examples/preact/basic-file-based/package.json new file mode 100644 index 00000000000..543f628d44b --- /dev/null +++ b/examples/preact/basic-file-based/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-preact-example-basic-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/preact-router-devtools": "workspace:*", + "@tanstack/router-plugin": "workspace:*", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} + diff --git a/examples/preact/basic-file-based/postcss.config.mjs b/examples/preact/basic-file-based/postcss.config.mjs new file mode 100644 index 00000000000..b4a6220e2db --- /dev/null +++ b/examples/preact/basic-file-based/postcss.config.mjs @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/examples/preact/basic-file-based/src/main.tsx b/examples/preact/basic-file-based/src/main.tsx new file mode 100644 index 00000000000..15da18cdd9b --- /dev/null +++ b/examples/preact/basic-file-based/src/main.tsx @@ -0,0 +1,26 @@ +import { render } from 'preact' +import { RouterProvider, createRouter } from '@tanstack/preact-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} + diff --git a/examples/preact/basic-file-based/src/posts.tsx b/examples/preact/basic-file-based/src/posts.tsx new file mode 100644 index 00000000000..8f24a7b332d --- /dev/null +++ b/examples/preact/basic-file-based/src/posts.tsx @@ -0,0 +1,33 @@ +import { notFound } from '@tanstack/preact-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + diff --git a/examples/preact/basic-file-based/src/routes/__root.tsx b/examples/preact/basic-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..d8c2f672f3e --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/__root.tsx @@ -0,0 +1,71 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet } from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' + +export const Route = createFileRoute('/__root')({ + component: RootComponent, + notFoundComponent: () => { + return ( +
    +

    This is the notFoundComponent configured on root route

    + Start Over +
    + ) + }, +}) + +function RootComponent() { + return ( + <> +
    + + Home + {' '} + + Posts + {' '} + + Pathless Layout + {' '} + + Anchor + {' '} + + This Route Does Not Exist + +
    +
    + + {/* Start rendering router matches */} + + + ) +} + diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx new file mode 100644 index 00000000000..49427f0a963 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Outlet } from '@tanstack/preact-router' + +export const Route = createFileRoute('/_pathlessLayout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
    +
    I'm a pathless layout
    +
    + +
    +
    + ) +} + diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx new file mode 100644 index 00000000000..562b9277b0c --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx @@ -0,0 +1,36 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet } from '@tanstack/preact-router' + +export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
    +
    I'm a nested pathless layout
    +
    + + Go to route A + + + Go to route B + +
    +
    + +
    +
    + ) +} + diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx new file mode 100644 index 00000000000..90f0bd15ba5 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/preact-router' +export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')( + { + component: LayoutAComponent, + }, +) + +function LayoutAComponent() { + return
    I'm layout A!
    +} + diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx new file mode 100644 index 00000000000..6f55fe35601 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/preact-router' +export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')( + { + component: LayoutBComponent, + }, +) + +function LayoutBComponent() { + return
    I'm layout B!
    +} + diff --git a/examples/preact/basic-file-based/src/routes/anchor.tsx b/examples/preact/basic-file-based/src/routes/anchor.tsx new file mode 100644 index 00000000000..7fdb51fac20 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/anchor.tsx @@ -0,0 +1,208 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { useLayoutEffect, useRef, useState } from 'preact/hooks' +import { Link, useLocation, useNavigate } from '@tanstack/preact-router' + +export const Route = createFileRoute('/anchor')({ + component: AnchorComponent, +}) + +const anchors: Array<{ + id: string + title: string + hashScrollIntoView?: boolean | ScrollIntoViewOptions +}> = [ + { + id: 'default-anchor', + title: 'Default Anchor', + }, + { + id: 'false-anchor', + title: 'No Scroll Into View', + hashScrollIntoView: false, + }, + { + id: 'smooth-scroll', + title: 'Smooth Scroll', + hashScrollIntoView: { behavior: 'smooth' }, + }, +] as const + +function AnchorSection({ id, title }: { id: string; title: string }) { + const [hasShown, setHasShown] = useState(false) + const elementRef = useRef(null) + + useLayoutEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (!hasShown && entry.isIntersecting) { + setHasShown(true) + } + }, + { threshold: 0.01 }, + ) + + const currentRef = elementRef.current + if (currentRef) { + observer.observe(currentRef) + } + + return () => { + if (currentRef) { + observer.unobserve(currentRef) + } + } + }, [hasShown]) + + return ( +
    +

    + {title} + {hasShown ? ' (shown)' : ''} +

    +
    + ) +} + +function AnchorComponent() { + const navigate = useNavigate() + const location = useLocation() + const [withScroll, setWithScroll] = useState(true) + + return ( +
    + +
    +
    { + event.preventDefault() + event.stopPropagation() + const formData = new FormData(event.target as HTMLFormElement) + + const toHash = formData.get('hash') as string + + if (!toHash) { + return + } + + const hashScrollIntoView = withScroll + ? ({ + behavior: formData.get('scrollBehavior') as ScrollBehavior, + block: formData.get('scrollBlock') as ScrollLogicalPosition, + inline: formData.get('scrollInline') as ScrollLogicalPosition, + } satisfies ScrollIntoViewOptions) + : false + + navigate({ hash: toHash, hashScrollIntoView }) + }} + > +

    Scroll with navigate

    +
    + +
    + +
    +
    + {withScroll ? ( + <> +
    + +
    + +
    + +
    + +
    + +
    + + ) : null} +
    + +
    +
    + + {anchors.map((anchor) => ( + + ))} +
    +
    + ) +} + diff --git a/examples/preact/basic-file-based/src/routes/index.tsx b/examples/preact/basic-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..aec1512aef4 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/preact-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
    +

    Welcome Home!

    +
    + ) +} + diff --git a/examples/preact/basic-file-based/src/routes/posts.$postId.tsx b/examples/preact/basic-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..7e7c0742c01 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { ErrorComponent } from '@tanstack/preact-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

    Post not found

    + }, + component: PostComponent, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
    +

    {post.title}

    +
    {post.body}
    +
    + ) +} + diff --git a/examples/preact/basic-file-based/src/routes/posts.index.tsx b/examples/preact/basic-file-based/src/routes/posts.index.tsx new file mode 100644 index 00000000000..fadfe708a0f --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/posts.index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/preact-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
    Select a post.
    +} + diff --git a/examples/preact/basic-file-based/src/routes/posts.route.tsx b/examples/preact/basic-file-based/src/routes/posts.route.tsx new file mode 100644 index 00000000000..ef152a16b29 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/posts.route.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet } from '@tanstack/preact-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const posts = Route.useLoaderData() + + return ( +
    +
      + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
    • + +
      {post.title.substring(0, 20)}
      + +
    • + ) + }, + )} +
    +
    + +
    + ) +} + diff --git a/examples/preact/basic-file-based/src/styles.css b/examples/preact/basic-file-based/src/styles.css new file mode 100644 index 00000000000..e07de1f8541 --- /dev/null +++ b/examples/preact/basic-file-based/src/styles.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} + diff --git a/examples/preact/basic-file-based/tailwind.config.mjs b/examples/preact/basic-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..6bdcac8282a --- /dev/null +++ b/examples/preact/basic-file-based/tailwind.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/examples/preact/basic-file-based/tsconfig.json b/examples/preact/basic-file-based/tsconfig.json new file mode 100644 index 00000000000..af80d030c77 --- /dev/null +++ b/examples/preact/basic-file-based/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} + diff --git a/examples/preact/basic-file-based/vite.config.js b/examples/preact/basic-file-based/vite.config.js new file mode 100644 index 00000000000..1a28c9b7f18 --- /dev/null +++ b/examples/preact/basic-file-based/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'preact', + autoCodeSplitting: true, + }), + preact(), + ], +}) + diff --git a/examples/preact/basic-preact-query/index.html b/examples/preact/basic-preact-query/index.html new file mode 100644 index 00000000000..9dd2d55e6e1 --- /dev/null +++ b/examples/preact/basic-preact-query/index.html @@ -0,0 +1,13 @@ + + + + + + TanStack Router - Preact Query + + +
    + + + + diff --git a/examples/preact/basic-preact-query/package.json b/examples/preact/basic-preact-query/package.json new file mode 100644 index 00000000000..9a75249e2b8 --- /dev/null +++ b/examples/preact/basic-preact-query/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-preact-example-preact-query", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/preact-router-devtools": "workspace:*", + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} + diff --git a/examples/preact/basic-preact-query/src/main.tsx b/examples/preact/basic-preact-query/src/main.tsx new file mode 100644 index 00000000000..4202fe25d59 --- /dev/null +++ b/examples/preact/basic-preact-query/src/main.tsx @@ -0,0 +1,279 @@ +import { render } from 'preact' +import { useEffect } from 'preact/hooks' +import { + ErrorComponent, + Link, + Outlet, + RouterProvider, + createRootRouteWithContext, + createRoute, + createRouter, + useRouter, +} from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { + QueryClient, + QueryClientProvider, + useQueryErrorResetBoundary, + useSuspenseQuery, +} from '@tanstack/react-query' +import { NotFoundError, postQueryOptions, postsQueryOptions } from './posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' +import './styles.css' + +const rootRoute = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: RootComponent, + notFoundComponent: () => { + return ( +
    +

    This is the notFoundComponent configured on root route

    + Start Over +
    + ) + }, +}) + +function RootComponent() { + return ( + <> +
    + + Home + {' '} + + Posts + {' '} + + Pathless Layout + {' '} + + This Route Does Not Exist + +
    +
    + + + + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexRouteComponent, +}) + +function IndexRouteComponent() { + return ( +
    +

    Welcome Home!

    +
    + ) +} + +const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: ({ context: { queryClient } }) => + queryClient.ensureQueryData(postsQueryOptions), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexRouteComponent, +}) + +function PostsIndexRouteComponent() { + return
    Select a post.
    +} + +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ context: { queryClient }, params: { postId } }) => + queryClient.ensureQueryData(postQueryOptions(postId)), + component: PostRouteComponent, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + const router = useRouter() + if (error instanceof NotFoundError) { + return
    {error.message}
    + } + const queryErrorResetBoundary = useQueryErrorResetBoundary() + + useEffect(() => { + queryErrorResetBoundary.reset() + }, [queryErrorResetBoundary]) + + return ( +
    + + +
    + ) +} + +function PostRouteComponent() { + const { postId } = postRoute.useParams() + const postQuery = useSuspenseQuery(postQueryOptions(postId)) + const post = postQuery.data + + return ( +
    +

    {post.title}

    +
    {post.body}
    +
    + ) +} + +const pathlessLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_pathlessLayout', + component: PathlessLayoutComponent, +}) + +function PathlessLayoutComponent() { + return ( +
    +
    I'm a pathless layout
    +
    + +
    +
    + ) +} + +const nestedPathlessLayoutRoute = createRoute({ + getParentRoute: () => pathlessLayoutRoute, + id: '_nestedPathlessLayout', + component: Layout2Component, +}) + +function Layout2Component() { + return ( +
    +
    I'm a nested pathless layout
    +
    + + Go to route A + + + Go to route B + +
    +
    + +
    +
    + ) +} + +const pathlessLayoutARoute = createRoute({ + getParentRoute: () => nestedPathlessLayoutRoute, + path: '/route-a', + component: PathlessLayoutAComponent, +}) + +function PathlessLayoutAComponent() { + return
    I'm layout A!
    +} + +const pathlessLayoutBRoute = createRoute({ + getParentRoute: () => nestedPathlessLayoutRoute, + path: '/route-b', + component: PathlessLayoutBComponent, +}) + +function PathlessLayoutBComponent() { + return
    I'm layout B!
    +} + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + pathlessLayoutRoute.addChildren([ + nestedPathlessLayoutRoute.addChildren([ + pathlessLayoutARoute, + pathlessLayoutBRoute, + ]), + ]), + indexRoute, +]) + +const queryClient = new QueryClient() + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + // Since we're using React Query, we don't want loader calls to ever be stale + // This will ensure that the loader is always called when the route is preloaded or visited + defaultPreloadStaleTime: 0, + scrollRestoration: true, + context: { + queryClient, + }, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render( + + + , + rootElement, + ) +} + diff --git a/examples/preact/basic-preact-query/src/posts.lazy.tsx b/examples/preact/basic-preact-query/src/posts.lazy.tsx new file mode 100644 index 00000000000..5e99f6d3d58 --- /dev/null +++ b/examples/preact/basic-preact-query/src/posts.lazy.tsx @@ -0,0 +1,40 @@ +import { Link, Outlet, createLazyRoute } from '@tanstack/preact-router' +import { useSuspenseQuery } from '@tanstack/react-query' +import { postsQueryOptions } from './posts' + +export const Route = createLazyRoute('/posts')({ + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const postsQuery = useSuspenseQuery(postsQueryOptions) + + const posts = postsQuery.data + + return ( +
    +
      + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
    • + +
      {post.title.substring(0, 20)}
      + +
    • + ) + }, + )} +
    + +
    + ) +} + diff --git a/examples/preact/basic-preact-query/src/posts.ts b/examples/preact/basic-preact-query/src/posts.ts new file mode 100644 index 00000000000..fde3365fe87 --- /dev/null +++ b/examples/preact/basic-preact-query/src/posts.ts @@ -0,0 +1,45 @@ +import axios from 'redaxios' +import { queryOptions } from '@tanstack/react-query' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} + +export const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', { postId }], + queryFn: () => fetchPost(postId), + }) + +export const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: () => fetchPosts(), +}) + diff --git a/examples/preact/basic-preact-query/src/styles.css b/examples/preact/basic-preact-query/src/styles.css new file mode 100644 index 00000000000..e07de1f8541 --- /dev/null +++ b/examples/preact/basic-preact-query/src/styles.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} + diff --git a/examples/preact/basic-preact-query/tsconfig.json b/examples/preact/basic-preact-query/tsconfig.json new file mode 100644 index 00000000000..af80d030c77 --- /dev/null +++ b/examples/preact/basic-preact-query/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} + diff --git a/examples/preact/basic-preact-query/vite.config.js b/examples/preact/basic-preact-query/vite.config.js new file mode 100644 index 00000000000..41b3a05cafd --- /dev/null +++ b/examples/preact/basic-preact-query/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +}) + diff --git a/examples/preact/basic/.gitignore b/examples/preact/basic/.gitignore new file mode 100644 index 00000000000..8354e4d50d5 --- /dev/null +++ b/examples/preact/basic/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/examples/preact/basic/.vscode/settings.json b/examples/preact/basic/.vscode/settings.json new file mode 100644 index 00000000000..00b5278e580 --- /dev/null +++ b/examples/preact/basic/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/examples/preact/basic/README.md b/examples/preact/basic/README.md new file mode 100644 index 00000000000..115199d292c --- /dev/null +++ b/examples/preact/basic/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm start` or `yarn start` diff --git a/examples/preact/basic/index.html b/examples/preact/basic/index.html new file mode 100644 index 00000000000..4dcb5c0570b --- /dev/null +++ b/examples/preact/basic/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
    + + + diff --git a/examples/preact/basic/package.json b/examples/preact/basic/package.json new file mode 100644 index 00000000000..afbb1505e0a --- /dev/null +++ b/examples/preact/basic/package.json @@ -0,0 +1,25 @@ +{ + "name": "tanstack-router-preact-example-basic", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "autoprefixer": "^10.4.20", + "express": "^4.21.2", + "postcss": "^8.5.1", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/preact/basic/postcss.config.mjs b/examples/preact/basic/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/examples/preact/basic/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/preact/basic/server.js b/examples/preact/basic/server.js new file mode 100644 index 00000000000..3e60fc01876 --- /dev/null +++ b/examples/preact/basic/server.js @@ -0,0 +1,47 @@ +import express from 'express' +import { createServer as createViteServer } from 'vite' + +const app = express() + +const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: 'custom', +}) + +app.use(vite.middlewares) + +app.use('*', async (req, res) => { + try { + const url = req.originalUrl + + let viteHead = await vite.transformIndexHtml( + url, + `
    `, + ) + + viteHead = viteHead.substring( + viteHead.indexOf('') + 6, + viteHead.indexOf(''), + ) + + const entry = await vite.ssrLoadModule('/src/entry-server.tsx') + + // Test streaming by checking query param + const useStreaming = req.query.stream === 'true' + + console.info('Rendering:', url, useStreaming ? '(streaming)' : '(string)') + await entry.render({ req, res, head: viteHead, useStreaming }) + } catch (e) { + vite.ssrFixStacktrace(e) + console.error(e.stack) + res.status(500).end(e.stack) + } +}) + +const port = 3000 +app.listen(port, () => { + console.info(`Server running at http://localhost:${port}`) + console.info(`Test SSR: http://localhost:${port}/`) + console.info(`Test Streaming: http://localhost:${port}/?stream=true`) +}) + diff --git a/examples/preact/basic/src/entry-client.tsx b/examples/preact/basic/src/entry-client.tsx new file mode 100644 index 00000000000..a46d66f3eec --- /dev/null +++ b/examples/preact/basic/src/entry-client.tsx @@ -0,0 +1,8 @@ +import { render } from 'preact' +import { RouterClient } from '@tanstack/preact-router/ssr/client' +import { createRouterInstance } from './router' + +const router = createRouterInstance() + +render(, document) + diff --git a/examples/preact/basic/src/entry-server.tsx b/examples/preact/basic/src/entry-server.tsx new file mode 100644 index 00000000000..9c0fad8d5be --- /dev/null +++ b/examples/preact/basic/src/entry-server.tsx @@ -0,0 +1,84 @@ +import { pipeline } from 'node:stream/promises' +import { + RouterServer, + createRequestHandler, + renderRouterToString, + renderRouterToStream, +} from '@tanstack/preact-router/ssr/server' +import { createRouterInstance } from './router' +import type express from 'express' + +export async function render({ + req, + res, + head, + useStreaming = false, +}: { + head: string + req: express.Request + res: express.Response + useStreaming?: boolean +}) { + // Convert the express request to a fetch request + const url = new URL(req.originalUrl || req.url, 'http://localhost:3000').href + + const request = new Request(url, { + method: req.method, + headers: (() => { + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') { + headers.set(key, value) + } else if (Array.isArray(value)) { + value.forEach((v) => headers.append(key, String(v))) + } + } + return headers + })(), + }) + + // Create a request handler + const handler = createRequestHandler({ + request, + createRouter: () => { + const router = createRouterInstance() + + // Update each router instance with the head info from vite + router.update({ + context: { + ...router.options.context, + head: head, + }, + }) + return router + }, + }) + + // Use either streaming or string rendering + const response = await handler(({ responseHeaders, router }) => + useStreaming + ? renderRouterToStream({ + request, + responseHeaders, + router, + children: , + }) + : renderRouterToString({ + responseHeaders, + router, + children: , + }), + ) + + // Convert the fetch response back to an express response + res.statusMessage = response.statusText + res.status(response.status) + + response.headers.forEach((value, name) => { + res.setHeader(name, value) + }) + + // Stream the response body + return pipeline(response.body as any, res) +} + diff --git a/examples/preact/basic/src/main.tsx b/examples/preact/basic/src/main.tsx new file mode 100644 index 00000000000..e335054ac39 --- /dev/null +++ b/examples/preact/basic/src/main.tsx @@ -0,0 +1,230 @@ +import { render } from 'preact' +import { + ErrorComponent, + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/preact-router' +import { NotFoundError, fetchPost, fetchPosts } from './posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' +import './styles.css' + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
    +

    This is the notFoundComponent configured on root route

    + Start Over +
    + ) + }, +}) + +function RootComponent() { + return ( + <> +
    + + Home + {' '} + + Posts + {' '} + + Pathless Layout + {' '} + + This Route Does Not Exist + +
    + + + ) +} +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
    +

    Welcome Home!

    +
    + ) +} + +export const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
    Select a post.
    +} + +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ params }) => fetchPost(params.postId), + component: PostComponent, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + if (error instanceof NotFoundError) { + return
    {error.message}
    + } + + return +} + +function PostComponent() { + const post = postRoute.useLoaderData() + + return ( +
    +

    {post.title}

    +
    +
    {post.body}
    +
    + ) +} + +const pathlessLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_pathlessLayout', + component: PathlessLayoutComponent, +}) + +function PathlessLayoutComponent() { + return ( +
    +
    I'm a pathless layout
    +
    + +
    +
    + ) +} + +const nestedPathlessLayout2Route = createRoute({ + getParentRoute: () => pathlessLayoutRoute, + id: '_nestedPathlessLayout', + component: PathlessLayout2Component, +}) + +function PathlessLayout2Component() { + return ( +
    +
    I'm a nested pathless layout
    +
    + + Go to Route A + + + Go to Route B + +
    +
    + +
    +
    + ) +} + +const pathlessLayoutARoute = createRoute({ + getParentRoute: () => nestedPathlessLayout2Route, + path: '/route-a', + component: PathlessLayoutAComponent, +}) + +function PathlessLayoutAComponent() { + return
    I'm route A!
    +} + +const pathlessLayoutBRoute = createRoute({ + getParentRoute: () => nestedPathlessLayout2Route, + path: '/route-b', + component: PathlessLayoutBComponent, +}) + +function PathlessLayoutBComponent() { + return
    I'm route B!
    +} + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + pathlessLayoutRoute.addChildren([ + nestedPathlessLayout2Route.addChildren([ + pathlessLayoutARoute, + pathlessLayoutBRoute, + ]), + ]), + indexRoute, +]) + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} diff --git a/examples/preact/basic/src/posts.lazy.tsx b/examples/preact/basic/src/posts.lazy.tsx new file mode 100644 index 00000000000..1e4a4daf6fa --- /dev/null +++ b/examples/preact/basic/src/posts.lazy.tsx @@ -0,0 +1,35 @@ +import { Link, Outlet, createLazyRoute } from '@tanstack/preact-router' + +export const Route = createLazyRoute('/posts')({ + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const posts = Route.useLoaderData() + + return ( +
    +
      + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
    • + +
      {post.title.substring(0, 20)}
      + +
    • + ) + }, + )} +
    + +
    + ) +} diff --git a/examples/preact/basic/src/posts.ts b/examples/preact/basic/src/posts.ts new file mode 100644 index 00000000000..54d62e57886 --- /dev/null +++ b/examples/preact/basic/src/posts.ts @@ -0,0 +1,32 @@ +import axios from 'redaxios' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} diff --git a/examples/preact/basic/src/preact.d.ts b/examples/preact/basic/src/preact.d.ts new file mode 100644 index 00000000000..fc03c2efc84 --- /dev/null +++ b/examples/preact/basic/src/preact.d.ts @@ -0,0 +1,6 @@ +/// + +declare namespace JSX { + type Element = preact.JSX.Element + type IntrinsicElements = preact.JSX.IntrinsicElements +} diff --git a/examples/preact/basic/src/router.tsx b/examples/preact/basic/src/router.tsx new file mode 100644 index 00000000000..6d2b6f2f89b --- /dev/null +++ b/examples/preact/basic/src/router.tsx @@ -0,0 +1,105 @@ +import { createRouter, createRootRoute, createRoute, Outlet } from '@tanstack/preact-router' +import { NotFoundError, fetchPost, fetchPosts } from './posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
    +

    This is the notFoundComponent configured on root route

    + Start Over +
    + ) + }, +}) + +function RootComponent() { + return ( + <> +
    + Home + Posts +
    + + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
    +

    Welcome Home!

    +
    + ) +} + +export const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
    Select a post.
    +} + +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ params }) => fetchPost(params.postId), + component: PostComponent, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + if (error instanceof NotFoundError) { + return
    {error.message}
    + } + return
    Error: {String(error)}
    +} + +function PostComponent() { + const post = postRoute.useLoaderData() + return ( +
    +

    {post.title}

    +
    +
    {post.body}
    +
    + ) +} + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +export function createRouterInstance() { + return createRouter({ + routeTree, + context: { + head: '', + }, + defaultPreload: 'intent', + scrollRestoration: true, + }) +} + +declare module '@tanstack/preact-router' { + interface Register { + router: ReturnType + } +} diff --git a/examples/preact/basic/src/styles.css b/examples/preact/basic/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/examples/preact/basic/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/examples/preact/basic/tailwind.config.mjs b/examples/preact/basic/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/examples/preact/basic/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/examples/preact/basic/tsconfig.json b/examples/preact/basic/tsconfig.json new file mode 100644 index 00000000000..a4510fc5fc0 --- /dev/null +++ b/examples/preact/basic/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} diff --git a/examples/preact/basic/vite.config.js b/examples/preact/basic/vite.config.js new file mode 100644 index 00000000000..29b326faf09 --- /dev/null +++ b/examples/preact/basic/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +}) diff --git a/examples/preact/scroll-restoration/index.html b/examples/preact/scroll-restoration/index.html new file mode 100644 index 00000000000..f8a2f4d113d --- /dev/null +++ b/examples/preact/scroll-restoration/index.html @@ -0,0 +1,13 @@ + + + + + + TanStack Router - Preact Scroll Restoration + + +
    + + + + diff --git a/examples/preact/scroll-restoration/package.json b/examples/preact/scroll-restoration/package.json new file mode 100644 index 00000000000..4cb786e72e1 --- /dev/null +++ b/examples/preact/scroll-restoration/package.json @@ -0,0 +1,26 @@ +{ + "name": "tanstack-router-preact-example-scroll-restoration", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/react-virtual": "^3.13.0", + "@tanstack/preact-router-devtools": "workspace:*", + "preact": "^10.24.3", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} + diff --git a/examples/preact/scroll-restoration/src/main.tsx b/examples/preact/scroll-restoration/src/main.tsx new file mode 100644 index 00000000000..6ceca01c33c --- /dev/null +++ b/examples/preact/scroll-restoration/src/main.tsx @@ -0,0 +1,211 @@ +import { render } from 'preact' +import { useRef } from 'preact/hooks' +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, + useElementScrollRestoration, +} from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' +import { useVirtualizer } from '@tanstack/react-virtual' +import './styles.css' + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> +
    + + Home + {' '} + + About + + + About (No Reset) + + + By-Element + +
    + + + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
    +

    Welcome Home!

    +
    + {Array.from({ length: 50 }).map((_, i) => ( +
    + Home Item {i + 1} +
    + ))} +
    +
    + ) +} + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: AboutComponent, +}) + +function AboutComponent() { + return ( +
    +
    Hello from About!
    +
    + {Array.from({ length: 50 }).map((_, i) => ( +
    + About Item {i + 1} +
    + ))} +
    +
    + ) +} + +const byElementRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/by-element', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: ByElementComponent, +}) + +function ByElementComponent() { + // We need a unique ID for manual scroll restoration on a specific element + // It should be as unique as possible for this element across your app + const scrollRestorationId = 'myVirtualizedContent' + + // We use that ID to get the scroll entry for this element + const scrollEntry = useElementScrollRestoration({ + id: scrollRestorationId, + }) + + // Let's use TanStack Virtual to virtualize some content! + const virtualizerParentRef = useRef(null) + const virtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => virtualizerParentRef.current, + estimateSize: () => 100, + // We pass the scrollY from the scroll restoration entry to the virtualizer + // as the initial offset + initialOffset: scrollEntry?.scrollY, + }) + + return ( +
    +
    Hello from By-Element!
    +
    +
    + {Array.from({ length: 50 }).map((_, i) => ( +
    + About Item {i + 1} +
    + ))} +
    +
    + {Array.from({ length: 2 }).map((_, i) => ( +
    +
    + {Array.from({ length: 50 }).map((_, i) => ( +
    + About Item {i + 1} +
    + ))} +
    +
    + ))} +
    +
    Virtualized
    +
    +
    + {virtualizer.getVirtualItems().map((item) => ( +
    +
    + Virtualized Item {item.index + 1} +
    +
    + ))} +
    +
    +
    +
    +
    +
    + ) +} + +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + byElementRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, +}) + +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} + diff --git a/examples/preact/scroll-restoration/src/styles.css b/examples/preact/scroll-restoration/src/styles.css new file mode 100644 index 00000000000..e07de1f8541 --- /dev/null +++ b/examples/preact/scroll-restoration/src/styles.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} + diff --git a/examples/preact/scroll-restoration/tsconfig.json b/examples/preact/scroll-restoration/tsconfig.json new file mode 100644 index 00000000000..af80d030c77 --- /dev/null +++ b/examples/preact/scroll-restoration/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} + diff --git a/examples/preact/scroll-restoration/vite.config.js b/examples/preact/scroll-restoration/vite.config.js new file mode 100644 index 00000000000..41b3a05cafd --- /dev/null +++ b/examples/preact/scroll-restoration/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +}) + diff --git a/examples/react/basic/src/main.tsx b/examples/react/basic/src/main.tsx index ca6c41b7096..41abd842c56 100644 --- a/examples/react/basic/src/main.tsx +++ b/examples/react/basic/src/main.tsx @@ -8,7 +8,7 @@ import { createRoute, createRouter, } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' + import { NotFoundError, fetchPost, fetchPosts } from './posts' import type { ErrorComponentProps } from '@tanstack/react-router' import './styles.css' diff --git a/examples/vanilla/authenticated-routes/index.html b/examples/vanilla/authenticated-routes/index.html new file mode 100644 index 00000000000..fbd362ac2df --- /dev/null +++ b/examples/vanilla/authenticated-routes/index.html @@ -0,0 +1,33 @@ + + + + + + TanStack Router - Vanilla Authenticated Routes + + + +
    + + + + diff --git a/examples/vanilla/authenticated-routes/package.json b/examples/vanilla/authenticated-routes/package.json new file mode 100644 index 00000000000..fb05b6a4aa6 --- /dev/null +++ b/examples/vanilla/authenticated-routes/package.json @@ -0,0 +1,19 @@ +{ + "name": "vanilla-authenticated-routes-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3005", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^7.1.7" + } +} + diff --git a/examples/vanilla/authenticated-routes/src/main.ts b/examples/vanilla/authenticated-routes/src/main.ts new file mode 100644 index 00000000000..74683432eae --- /dev/null +++ b/examples/vanilla/authenticated-routes/src/main.ts @@ -0,0 +1,234 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + getMatchesHtml, + vanillaRouter, + redirect, +} from '@tanstack/vanilla-router' + +// Simple auth state management +let currentUser: { username: string } | null = null + +function login(username: string, password: string): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (username === 'admin' && password === 'password') { + currentUser = { username } + resolve() + } else { + reject(new Error('Invalid credentials')) + } + }, 500) + }) +} + +function logout() { + currentUser = null +} + +function isAuthenticated(): boolean { + return currentUser !== null +} + +function getCurrentUser() { + return currentUser +} + +// Root component +const RootComponent = (router: ReturnType) => { + const currentPath = router.state.location.pathname + const user = getCurrentUser() + + return ` + +
    + ${outlet()} +
    + ` +} + +const rootRoute = createRootRoute({ + component: RootComponent, + context: () => ({ + auth: { + isAuthenticated: isAuthenticated(), + user: getCurrentUser(), + }, + }), +}) + +// Index component (public) +const IndexComponent = (router: ReturnType) => { + const user = getCurrentUser() + return ` +
    +

    Welcome Home!

    +

    This is a public page. Anyone can access it.

    + ${user ? ` +

    You are logged in as ${user.username}.

    + Go to Dashboard + ` : ` +

    You are not logged in.

    + Login + `} +
    + ` +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +// Login component (public, but redirects if already authenticated) +const LoginComponent = (router: ReturnType) => { + return ` +
    +

    Login

    +
    + + + + + +
    + +

    + Demo credentials:
    + Username: admin
    + Password: password +

    +
    + ` +} + +const loginRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/login', + beforeLoad: ({ context }) => { + // Redirect to dashboard if already authenticated + if (context.auth.isAuthenticated) { + throw redirect({ to: '/dashboard' }) + } + }, + component: LoginComponent, +}) + +// Dashboard component (protected) +const DashboardComponent = (router: ReturnType) => { + const user = getCurrentUser() + return ` +
    +

    Dashboard

    +

    Welcome, ${user?.username}!

    +

    This is a protected route. Only authenticated users can access it.

    +

    If you try to access this page while logged out, you'll be redirected to the login page.

    + +
    + ` +} + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + beforeLoad: ({ context }) => { + // Redirect to login if not authenticated + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } + }, + component: DashboardComponent, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + indexRoute, + loginRoute, + dashboardRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + context: { + auth: { + isAuthenticated: false, + user: null, + }, + }, +}) + +// Render function +const rootElement = document.getElementById('app') +if (!rootElement) throw new Error('App element not found') + +function render() { + if (!rootElement) return + + // Update router context with current auth state + router.options.context = { + auth: { + isAuthenticated: isAuthenticated(), + user: getCurrentUser(), + }, + } + + const htmlParts = getMatchesHtml(router, router.state.matches) + rootElement.innerHTML = htmlParts.join('') + + // Update active links + const currentPath = router.state.location.pathname + const links = rootElement.querySelectorAll('nav a') + links.forEach((link) => { + const href = link.getAttribute('href') + if (href === currentPath) { + link.classList.add('active') + } else { + link.classList.remove('active') + } + }) + + // Setup login form handler + const loginForm = rootElement.querySelector('#login-form') as HTMLFormElement + if (loginForm) { + loginForm.onsubmit = async (e) => { + e.preventDefault() + const formData = new FormData(loginForm) + const username = formData.get('username') as string + const password = formData.get('password') as string + const errorDiv = rootElement.querySelector('#login-error') as HTMLDivElement + + try { + await login(username, password) + errorDiv.style.display = 'none' + // Navigate to dashboard after successful login + await router.navigate({ to: '/dashboard' }) + } catch (error) { + errorDiv.style.display = 'block' + errorDiv.className = 'error' + errorDiv.textContent = error instanceof Error ? error.message : 'Login failed' + } + } + } +} + +// Setup logout function +;(window as any).logout = async () => { + logout() + await router.navigate({ to: '/' }) +} + +// Setup router +vanillaRouter(router, render).catch(console.error) + diff --git a/examples/vanilla/authenticated-routes/tsconfig.json b/examples/vanilla/authenticated-routes/tsconfig.json new file mode 100644 index 00000000000..74ca91605f6 --- /dev/null +++ b/examples/vanilla/authenticated-routes/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} + diff --git a/examples/vanilla/authenticated-routes/vite.config.ts b/examples/vanilla/authenticated-routes/vite.config.ts new file mode 100644 index 00000000000..9744cb90d9a --- /dev/null +++ b/examples/vanilla/authenticated-routes/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve(__dirname, '../../../packages/vanilla-router/src'), + }, + }, +}) + diff --git a/examples/vanilla/basic/index.html b/examples/vanilla/basic/index.html new file mode 100644 index 00000000000..6508cc3b1fd --- /dev/null +++ b/examples/vanilla/basic/index.html @@ -0,0 +1,69 @@ + + + + + + TanStack Router - Vanilla JS + + + +
    + + + + diff --git a/examples/vanilla/basic/package.json b/examples/vanilla/basic/package.json new file mode 100644 index 00000000000..8cf0c6ae760 --- /dev/null +++ b/examples/vanilla/basic/package.json @@ -0,0 +1,20 @@ +{ + "name": "tanstack-router-vanilla-example-basic", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3001", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*", + "redaxios": "^0.5.1" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} + diff --git a/examples/vanilla/basic/src/main.ts b/examples/vanilla/basic/src/main.ts new file mode 100644 index 00000000000..b578e9d5239 --- /dev/null +++ b/examples/vanilla/basic/src/main.ts @@ -0,0 +1,153 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + getMatchesHtml, + vanillaRouter, +} from '@tanstack/vanilla-router' +import { NotFoundError, fetchPost, fetchPosts } from './posts' + +// Root component +const RootComponent = (router: ReturnType) => { + return ` +
    + Home + Posts +
    + ${outlet()} + ` +} + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => (router: ReturnType) => { + return ` +
    +

    This is the notFoundComponent configured on root route

    + Start Over +
    + ` + }, +}) + +// Index component +const IndexComponent = (router: ReturnType) => { + return ` +
    +

    Welcome Home!

    +
    + ` +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +// Posts layout route with component +export const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), + component: (router: ReturnType) => { + // Access data via route getters - pass router explicitly + const posts = postsLayoutRoute.getLoaderData(router) as Array<{ id: string; title: string }> | undefined + + if (!posts) { + return `
    Loading posts...
    ` + } + + return ` +
    + + ${outlet()} +
    + ` + }, +}) + +// Posts index component +const PostsIndexComponent = (router: ReturnType) => { + return `
    Select a post.
    ` +} + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexComponent, +}) + +// Post route +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: ({ error }) => (router: ReturnType) => { + if (error instanceof NotFoundError) { + return `
    ${error.message}
    ` + } + return `
    Error: ${String(error)}
    ` + }, + loader: ({ params }) => fetchPost(params.postId), + component: (router: ReturnType) => { + // Access data via route getters - pass router explicitly + const post = postRoute.getLoaderData(router) as { title: string; body: string } | undefined + + if (!post) { + return `
    Loading...
    ` + } + + return ` +
    +

    ${post.title}

    +
    +
    ${post.body}
    +
    + ` + }, +}) + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +// Create router +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, +}) + +// Register router for type safety +declare module '@tanstack/vanilla-router' { + interface Register { + router: typeof router + } +} + +// Initialize router +const rootElement = document.getElementById('app')! + +// Render function - direct DOM manipulation +function render() { + rootElement.innerHTML = '' + + const state = router.state + + // Use getMatchesHtml utility to get nested HTML strings (outlet replacement is handled internally) + const htmlParts = getMatchesHtml(router, state.matches) + rootElement.innerHTML = htmlParts.join('') +} + +// Setup router with state subscription and link handlers +await vanillaRouter(router, render) + diff --git a/examples/vanilla/basic/src/posts.ts b/examples/vanilla/basic/src/posts.ts new file mode 100644 index 00000000000..b97cda82415 --- /dev/null +++ b/examples/vanilla/basic/src/posts.ts @@ -0,0 +1,32 @@ +import axios from 'redaxios' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} + diff --git a/examples/vanilla/basic/tsconfig.json b/examples/vanilla/basic/tsconfig.json new file mode 100644 index 00000000000..1b0362ce9bf --- /dev/null +++ b/examples/vanilla/basic/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/vanilla/basic/vite.config.js b/examples/vanilla/basic/vite.config.js new file mode 100644 index 00000000000..cbbe8482318 --- /dev/null +++ b/examples/vanilla/basic/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve(__dirname, '../../../packages/vanilla-router/src'), + }, + }, +}) + diff --git a/examples/vanilla/jsx-router/index.html b/examples/vanilla/jsx-router/index.html new file mode 100644 index 00000000000..94efbd9a278 --- /dev/null +++ b/examples/vanilla/jsx-router/index.html @@ -0,0 +1,83 @@ + + + + + + TanStack Router - Vanilla JSX + + + +
    + + + + diff --git a/examples/vanilla/jsx-router/package.json b/examples/vanilla/jsx-router/package.json new file mode 100644 index 00000000000..7ddb702c0e6 --- /dev/null +++ b/examples/vanilla/jsx-router/package.json @@ -0,0 +1,19 @@ +{ + "name": "vanilla-jsx-router-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^7.1.7" + } +} + diff --git a/examples/vanilla/jsx-router/src/main.ts b/examples/vanilla/jsx-router/src/main.ts new file mode 100644 index 00000000000..7b336010be0 --- /dev/null +++ b/examples/vanilla/jsx-router/src/main.ts @@ -0,0 +1,176 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + vanillaRouter, +} from '@tanstack/vanilla-router' +import { VanillaRenderer, jsx } from './renderer' + +// Mock data fetching +async function fetchPosts() { + await new Promise(resolve => setTimeout(resolve, 500)) + return [ + { id: '1', title: 'Getting Started with Vanilla Router' }, + { id: '2', title: 'JSX Rendering Made Simple' }, + { id: '3', title: 'Building Modern Web Apps' }, + ] +} + +async function fetchPost(id: string) { + await new Promise(resolve => setTimeout(resolve, 300)) + const posts: Record = { + '1': { title: 'Getting Started with Vanilla Router', body: 'Vanilla Router is a powerful routing solution for vanilla JavaScript applications. It provides type-safe routing, nested routes, and excellent developer experience.' }, + '2': { title: 'JSX Rendering Made Simple', body: 'Combine the power of JSX with vanilla JavaScript. The JSX renderer makes it easy to build component-based UIs without a framework.' }, + '3': { title: 'Building Modern Web Apps', body: 'Modern web applications require modern tools. Vanilla Router and JSX renderer provide a lightweight alternative to heavy frameworks.' }, + } + if (!posts[id]) throw new Error('Post not found') + return posts[id] +} + +// Root component using JSX +const RootComponent = (router: ReturnType) => { + return () => { + return jsx('div', {}, + jsx('nav', {}, + jsx('a', { href: buildHref(router, { to: '/' }) }, 'Home'), + jsx('a', { href: buildHref(router, { to: '/posts' }) }, 'Posts') + ), + jsx('main', {}, + outlet() + ) + ) + } +} + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +// Index component +const IndexComponent = (router: ReturnType) => { + return () => { + return jsx('div', {}, + jsx('h1', {}, 'Welcome Home!'), + jsx('p', {}, 'This is the home page using Vanilla Router with JSX rendering.') + ) + } +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +// Posts layout route +const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), + component: (router: ReturnType) => { + return () => { + const posts = postsLayoutRoute.getLoaderData(router) + + if (!posts) { + return jsx('div', { className: 'loading' }, 'Loading posts...') + } + + return jsx('div', {}, + jsx('h1', {}, 'Posts'), + jsx('ul', { className: 'post-list' }, + ...posts.map(post => + jsx('li', { className: 'post-item' }, + jsx('a', { href: buildHref(router, { to: '/posts/$postId', params: { postId: post.id } }) }, post.title) + ) + ) + ), + outlet() + ) + } + }, +}) + +// Posts index +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: (router: ReturnType) => { + return () => { + return jsx('div', {}, + jsx('p', {}, 'Select a post to view details.') + ) + } + }, +}) + +// Post detail route +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + loader: ({ params }: { params: { postId: string } }) => fetchPost(params.postId), + component: (router: ReturnType) => { + return () => { + const post = postRoute.getLoaderData(router) + + if (!post) { + return jsx('div', { className: 'loading' }, 'Loading...') + } + + return jsx('div', { className: 'post-detail' }, + jsx('h1', { className: 'post-title' }, post.title), + jsx('div', { className: 'post-body' }, post.body) + ) + } + }, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', +}) + +// Render function using VanillaRenderer +const renderer = new VanillaRenderer() +const rootElement = document.getElementById('app') +if (!rootElement) throw new Error('App element not found') + +function render() { + if (!rootElement) return + const state = router.state + + // Convert router matches to render contexts + const contexts = state.matches.map((match) => { + const route = router.routesById[match.routeId] + const matchState = router.getMatch(match.id) + + return { + component: route.options.component, + errorComponent: route.options.errorComponent, + pendingComponent: route.options.pendingComponent, + error: matchState?.error, + isPending: matchState?._displayPending, + data: { + loaderData: matchState?.loaderData, + params: matchState?.params, + search: matchState?.search, + routeId: match.routeId, + }, + } + }) + + // Render using JSX renderer with router + const html = renderer.render(contexts, router) + rootElement.innerHTML = html +} + +// Setup router +vanillaRouter(router, render).catch(console.error) + diff --git a/examples/vanilla/jsx-router/src/renderer.ts b/examples/vanilla/jsx-router/src/renderer.ts new file mode 100644 index 00000000000..d49265d8351 --- /dev/null +++ b/examples/vanilla/jsx-router/src/renderer.ts @@ -0,0 +1,263 @@ +// Copy of the renderer from packages/vanilla-router/examples/renderer/renderer.ts +// This allows the example to work independently + +import type { VanillaRouteComponent, VanillaErrorRouteComponent } from '@tanstack/vanilla-router' + +interface ComponentInstance { + cleanup?: () => void + getHtml: () => string +} + +export interface RenderContext { + component: VanillaRouteComponent | undefined + pendingComponent?: VanillaRouteComponent + errorComponent?: VanillaErrorRouteComponent + error?: Error + isPending?: boolean + data?: any +} + +export class VanillaRenderer { + private cleanupFunctions: Map void> = new Map() + private componentInstances: Map = new Map() + private currentContexts: Array = [] + private currentIndex: number = -1 + private childHtmlCache: Map = new Map() + + render(contexts: Array, router?: any): string { + this.cleanupFunctions.forEach((cleanup) => { + try { + cleanup() + } catch (error) { + console.error('Error during component cleanup:', error) + } + }) + this.cleanupFunctions.clear() + this.componentInstances.clear() + this.childHtmlCache.clear() + + if (contexts.length === 0) { + return '' + } + + this.currentContexts = contexts + + if (router) { + ;(globalThis as any).__tanstackRouter = router + } + + try { + return this.renderContexts(contexts, 0, router) + } catch (error) { + return this.renderError(error as Error, contexts[0]?.errorComponent, contexts[0]?.data, router) + } finally { + if (router) { + delete (globalThis as any).__tanstackRouter + } + } + } + + private renderContexts(contexts: Array, index: number, router?: any): string { + if (index >= contexts.length) { + return '' + } + + const context = contexts[index] + if (!context) { + return '' + } + + const previousIndex = this.currentIndex + this.currentIndex = index + + try { + if (context.isPending && context.pendingComponent) { + const pendingInstance = this.createComponentInstance(context.pendingComponent, index, router) + return pendingInstance.getHtml() + } + + if (context.error && context.errorComponent) { + const errorFactory = context.errorComponent({ error: context.error }) + const errorInstance = this.createComponentInstance( + router ? errorFactory(router) : errorFactory, + index, + router + ) + return errorInstance.getHtml() + } + + const Component = context.component + if (!Component) { + return this.renderContexts(contexts, index + 1, router) + } + + const instance = this.createComponentInstance(Component, index, router) + this.componentInstances.set(String(index), instance) + + if (instance.cleanup) { + this.cleanupFunctions.set(String(index), instance.cleanup) + } + + const childHtml = this.renderContexts(contexts, index + 1, router) + this.childHtmlCache.set(index, childHtml) + + const getContextData = () => this.currentContexts[index]?.data + const getChildHtmlFn = () => this.childHtmlCache.get(index) || '' + const renderContext = { data: getContextData, childHtml: getChildHtmlFn } + ;(globalThis as any).__vanillaRendererContext = renderContext + + try { + let html = instance.getHtml() + const OUTLET_MARKER = '__TANSTACK_ROUTER_OUTLET__' + if (html.includes(OUTLET_MARKER)) { + html = html.replace(OUTLET_MARKER, childHtml) + } + return html + } finally { + delete (globalThis as any).__vanillaRendererContext + } + } finally { + this.currentIndex = previousIndex + } + } + + private createComponentInstance(component: VanillaRouteComponent, index: number, router?: any): ComponentInstance { + const getContext = () => this.currentContexts[index]?.data + const getChildHtml = () => this.childHtmlCache.get(index) || '' + + const context = { + data: getContext, + childHtml: getChildHtml, + } + + ;(globalThis as any).__vanillaRendererContext = context + + const routerInstance = router || (globalThis as any).__tanstackRouter + const result = routerInstance ? component(routerInstance) : component() + + delete (globalThis as any).__vanillaRendererContext + + if (Array.isArray(result)) { + const [cleanup, getHtml] = result + return { cleanup, getHtml } + } else { + return { getHtml: result } + } + } + + private renderError(error: Error, errorComponent?: VanillaErrorRouteComponent, data?: any, router?: any): string { + if (errorComponent) { + const context = { data: () => data, childHtml: () => '' } + ;(globalThis as any).__vanillaRendererContext = context + + const routerInstance = router || (globalThis as any).__tanstackRouter + const errorFactory = errorComponent({ error }) + const instance = this.createComponentInstance(routerInstance ? errorFactory(routerInstance) : errorFactory, 0, routerInstance) + delete (globalThis as any).__vanillaRendererContext + + return instance.getHtml() + } else { + return `
    Error: ${error.message || String(error)}
    ` + } + } + + destroy() { + this.cleanupFunctions.forEach((cleanup) => { + try { + cleanup() + } catch (error) { + console.error('Error during component cleanup:', error) + } + }) + this.cleanupFunctions.clear() + this.componentInstances.clear() + this.childHtmlCache.clear() + this.currentContexts = [] + } +} + +function escapeHtml(text: string): string { + if (typeof document === 'undefined') { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + const div = document.createElement('div') + div.textContent = String(text) + return div.innerHTML +} + +function renderChild(child: any): string { + if (child === null || child === undefined || child === false) return '' + if (typeof child === 'string' && child.trim().startsWith('<')) { + return child + } + if (typeof child === 'string' || typeof child === 'number') return escapeHtml(String(child)) + if (Array.isArray(child)) return child.map(renderChild).join('') + if (typeof child === 'object' && child.$$typeof === Symbol.for('react.element')) { + return jsx(child.type, child.props) + } + return String(child) +} + +export function jsx(type: any, props: any, ...children: any[]): string { + const normalizedChildren = children.filter(child => child !== undefined && child !== null) + + if (type === Symbol.for('react.fragment') || type === null) { + return normalizedChildren.map(child => renderChild(child)).join('') + } + + if (typeof type === 'function') { + const componentResult = type(props || {}) + if (typeof componentResult === 'function') { + return componentResult(normalizedChildren.map(child => renderChild(child)).join('')) + } + return renderChild(componentResult) + } + + const tagName = String(type).toLowerCase() + const attrs = props ? Object.entries(props) + .filter(([key]) => key !== 'children') + .map(([key, value]) => { + if (key === 'className') { + return `class="${escapeHtml(String(value))}"` + } + const attrName = key.replace(/([A-Z])/g, '-$1').toLowerCase() + if (value === true) return attrName + if (value === false || value === null || value === undefined) return '' + return `${attrName}="${escapeHtml(String(value))}"` + }) + .filter(Boolean) + .join(' ') : '' + + const childrenHtml = normalizedChildren.length > 0 + ? normalizedChildren.map(child => { + if (child && typeof child === 'object' && child.__html) { + return child.__html + } + return renderChild(child) + }).join('') + : '' + + const selfClosingTags = ['input', 'img', 'br', 'hr', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'] + if (selfClosingTags.includes(tagName)) { + return `<${tagName}${attrs ? ' ' + attrs : ''} />` + } + + return `<${tagName}${attrs ? ' ' + attrs : ''}>${childrenHtml}` +} + +export function jsxs(type: any, props: any, ...children: any[]): string { + return jsx(type, props, ...children) +} + +export function Fragment({ children = [] }: { children?: any[] }): string { + return (Array.isArray(children) ? children : [children]) + .filter(child => child !== undefined && child !== null) + .map(child => renderChild(child)) + .join('') +} + diff --git a/examples/vanilla/jsx-router/tsconfig.json b/examples/vanilla/jsx-router/tsconfig.json new file mode 100644 index 00000000000..927890b40f9 --- /dev/null +++ b/examples/vanilla/jsx-router/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "jsxFactory": "jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} + diff --git a/examples/vanilla/jsx-router/vite.config.ts b/examples/vanilla/jsx-router/vite.config.ts new file mode 100644 index 00000000000..9744cb90d9a --- /dev/null +++ b/examples/vanilla/jsx-router/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve(__dirname, '../../../packages/vanilla-router/src'), + }, + }, +}) + diff --git a/examples/vanilla/scroll-restoration/index.html b/examples/vanilla/scroll-restoration/index.html new file mode 100644 index 00000000000..31a429e8539 --- /dev/null +++ b/examples/vanilla/scroll-restoration/index.html @@ -0,0 +1,27 @@ + + + + + + TanStack Router - Vanilla Scroll Restoration + + + +
    + + + + diff --git a/examples/vanilla/scroll-restoration/package.json b/examples/vanilla/scroll-restoration/package.json new file mode 100644 index 00000000000..2483c1e7da2 --- /dev/null +++ b/examples/vanilla/scroll-restoration/package.json @@ -0,0 +1,19 @@ +{ + "name": "vanilla-scroll-restoration-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3004", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^7.1.7" + } +} + diff --git a/examples/vanilla/scroll-restoration/src/main.ts b/examples/vanilla/scroll-restoration/src/main.ts new file mode 100644 index 00000000000..1e9fea6ca35 --- /dev/null +++ b/examples/vanilla/scroll-restoration/src/main.ts @@ -0,0 +1,137 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + getMatchesHtml, + vanillaRouter, +} from '@tanstack/vanilla-router' + +// Root component +const RootComponent = (router: ReturnType) => { + const currentPath = router.state.location.pathname + return ` + +
    + ${outlet()} +
    + ` +} + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +// Index component with scrollable content +const IndexComponent = (router: ReturnType) => { + return ` +
    + Scroll Restoration Demo: Scroll down on this page, then navigate to another page and come back. + Your scroll position should be restored automatically! +
    +

    Welcome Home!

    +
    + ${Array.from({ length: 50 }, (_, i) => ` +
    Home Item ${i + 1}
    + `).join('')} +
    + ` +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: IndexComponent, +}) + +// About component with scrollable content +const AboutComponent = (router: ReturnType) => { + return ` +
    + Scroll Restoration Demo: Scroll down on this page, navigate away, and come back. + Your scroll position should be restored! +
    +

    About Page

    +
    + ${Array.from({ length: 50 }, (_, i) => ` +
    About Item ${i + 1}
    + `).join('')} +
    + ` +} + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: AboutComponent, +}) + +// Scrollable container component (demonstrates element-level scroll restoration) +const ScrollableComponent = (router: ReturnType) => { + return ` +
    + Element-Level Scroll Restoration: Scroll within the container below, navigate away, and come back. + The scroll position within the container should be restored! +
    +

    Scrollable Container Demo

    +
    + ${Array.from({ length: 50 }, (_, i) => ` +
    Scrollable Item ${i + 1}
    + `).join('')} +
    + ` +} + +const scrollableRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/scrollable', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: ScrollableComponent, +}) + +// Create router with scroll restoration enabled +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + scrollableRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, // Enable scroll restoration +}) + +// Render function +const rootElement = document.getElementById('app') +if (!rootElement) throw new Error('App element not found') + +function render() { + if (!rootElement) return + const htmlParts = getMatchesHtml(router, router.state.matches) + rootElement.innerHTML = htmlParts.join('') + + // Update active links + const currentPath = router.state.location.pathname + const links = rootElement.querySelectorAll('nav a') + links.forEach((link) => { + const href = link.getAttribute('href') + if (href === currentPath) { + link.classList.add('active') + } else { + link.classList.remove('active') + } + }) +} + +// Setup router +vanillaRouter(router, render).catch(console.error) + diff --git a/examples/vanilla/scroll-restoration/tsconfig.json b/examples/vanilla/scroll-restoration/tsconfig.json new file mode 100644 index 00000000000..74ca91605f6 --- /dev/null +++ b/examples/vanilla/scroll-restoration/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} + diff --git a/examples/vanilla/scroll-restoration/vite.config.ts b/examples/vanilla/scroll-restoration/vite.config.ts new file mode 100644 index 00000000000..9744cb90d9a --- /dev/null +++ b/examples/vanilla/scroll-restoration/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve(__dirname, '../../../packages/vanilla-router/src'), + }, + }, +}) + diff --git a/packages/preact-router/README.md b/packages/preact-router/README.md new file mode 100644 index 00000000000..d83bf5fdf43 --- /dev/null +++ b/packages/preact-router/README.md @@ -0,0 +1,31 @@ + + +# TanStack React Router + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/preact-router/build-no-check.js b/packages/preact-router/build-no-check.js new file mode 100644 index 00000000000..0b0e0c4a42a --- /dev/null +++ b/packages/preact-router/build-no-check.js @@ -0,0 +1,5 @@ +import { build } from 'vite' +build({ mode: 'production', build: { emptyOutDir: true }, logLevel: 'warn' }).catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/packages/preact-router/eslint.config.ts b/packages/preact-router/eslint.config.ts new file mode 100644 index 00000000000..a2f17156678 --- /dev/null +++ b/packages/preact-router/eslint.config.ts @@ -0,0 +1,24 @@ +import pluginReact from '@eslint-react/eslint-plugin' +// @ts-expect-error +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + '@eslint-react': pluginReact, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, +] diff --git a/packages/preact-router/package.json b/packages/preact-router/package.json new file mode 100644 index 00000000000..f7399658098 --- /dev/null +++ b/packages/preact-router/package.json @@ -0,0 +1,117 @@ +{ + "name": "@tanstack/preact-router", + "version": "1.133.13", + "description": "Modern and scalable routing for Preact applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/preact-router" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "preact", + "location", + "router", + "routing", + "async", + "async router", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts59": "tsc -p tsconfig.legacy.json", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", + "test:perf": "vitest bench", + "test:perf:dev": "pnpm run test:perf --watch --hideSkippedTests", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "pnpm run build:lib", + "build:lib": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./ssr/server": { + "import": { + "types": "./dist/esm/ssr/server.d.ts", + "default": "./dist/esm/ssr/server.js" + }, + "require": { + "types": "./dist/cjs/ssr/server.d.cts", + "default": "./dist/cjs/ssr/server.cjs" + } + }, + "./ssr/client": { + "import": { + "types": "./dist/esm/ssr/client.d.ts", + "default": "./dist/esm/ssr/client.js" + }, + "require": { + "types": "./dist/cjs/ssr/client.d.cts", + "default": "./dist/cjs/ssr/client.cjs" + } + }, + "./package.json": "./package.json", + "./llms": { + "import": { + "types": "./dist/llms/index.d.ts", + "default": "./dist/llms/index.js" + } + } + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/history": "workspace:*", + "@tanstack/react-store": "^0.7.0", + "@tanstack/router-core": "workspace:*", + "@tanstack/store": "^0.7.0", + "isbot": "^5.1.22", + "preact-render-to-string": "^6.3.1", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "devDependencies": { + "@preact/preset-vite": "^2.9.3", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/preact": "^3.2.4", + "combinate": "^1.1.11", + "preact": "^10.24.3", + "vibe-rules": "^0.2.57", + "zod": "^3.24.2" + }, + "peerDependencies": { + "preact": ">=10.0.0" + } +} diff --git a/packages/preact-router/src/Asset.tsx b/packages/preact-router/src/Asset.tsx new file mode 100644 index 00000000000..ae50f5aa4a9 --- /dev/null +++ b/packages/preact-router/src/Asset.tsx @@ -0,0 +1,164 @@ +import type { ComponentChildren } from 'preact' +import { useEffect } from 'preact/hooks' +import { useRouter } from './useRouter' +import type { RouterManagedTag } from '@tanstack/router-core' + +interface ScriptAttrs { + [key: string]: string | boolean | undefined + src?: string +} + +export function Asset({ + tag, + attrs, + children, + nonce, +}: RouterManagedTag & { nonce?: string }): any { + switch (tag) { + case 'title': + return ( + + {children} + + ) + case 'meta': + return + case 'link': + return + case 'style': + return ( + + + +
    + + + + diff --git a/packages/vanilla-router/examples/vanilla-dom-example.ts b/packages/vanilla-router/examples/vanilla-dom-example.ts new file mode 100644 index 00000000000..a6e43288374 --- /dev/null +++ b/packages/vanilla-router/examples/vanilla-dom-example.ts @@ -0,0 +1,130 @@ +/** + * Example: Using headless router with direct DOM manipulation + */ + +import { createRouter, createRoute } from '@tanstack/vanilla-router' +import { buildHref, outlet, getMatchesHtml, setupLinkHandlers, setRouter, getRouter } from '@tanstack/vanilla-router' + +// Define routes - components are simple functions that return HTML strings +// Note: Routes can reference each other, but for simple paths you can use strings +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => { + return '

    Home Page

    Welcome to the home page!

    ' + }, +}) + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: () => { + return '

    About

    This is the about page.

    ' + }, +}) + +const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '/posts/$postId', + component: () => { + // Use route getter to access params + const params = postRoute.getParams() + return `

    Post ${params.postId}

    This is post ${params.postId}

    ` + }, +}) + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/posts', + component: () => { + const posts = [ + { id: 1, title: 'Post 1' }, + { id: 2, title: 'Post 2' }, + ] + // Use buildHref utility for type-safe hrefs with params (like navigate() and ) + const router = getRouter() + return ` +

    Posts

    + + ` + }, +}) + +const rootRoute = createRoute({ + getParentRoute: () => rootRoute, + id: 'root', + component: () => { + // Use outlet() function to mark where child routes should render + const router = getRouter() + return ` +
    + +
    ${outlet()}
    +
    + ` + }, +}) + +// Create router +const routeTree = rootRoute.addChildren([indexRoute, aboutRoute, postsRoute.addChildren([postRoute])]) + +const router = createRouter({ + routeTree, +}) + +// Initialize router +const rootElement = document.getElementById('app')! +setRouter(router) // Set global router context for route getters + +// Render function - direct DOM manipulation +function renderToDOM(router: typeof router, rootElement: HTMLElement) { + rootElement.innerHTML = '' + + const state = router.state + + // Check for not found + if (state.matches.some(m => m.status === 'notFound' || m.globalNotFound)) { + rootElement.innerHTML = '
    404 - Not Found
    ' + return + } + + // Use getMatchesHtml utility to get nested HTML strings (outlet replacement is handled internally) + const htmlParts = getMatchesHtml(router, state.matches) + rootElement.innerHTML = htmlParts.join('') +} + +// Subscribe to router events and render on changes +async function init() { + // Load initial matches if needed + if (router.state.matches.length === 0) { + try { + await router.load() + } catch (error) { + console.error('Error loading router:', error) + } + } + + // Initial render + renderToDOM(router, rootElement) + + // Subscribe to router state changes + router.subscribe('onResolved', () => { + renderToDOM(router, rootElement) + }) + + router.subscribe('onLoad', () => { + renderToDOM(router, rootElement) + }) +} + +init() + +// Setup link handlers (returns cleanup function) +const cleanupLinkHandlers = setupLinkHandlers(router, rootElement) + diff --git a/packages/vanilla-router/examples/vanilla-jsx-example.ts b/packages/vanilla-router/examples/vanilla-jsx-example.ts new file mode 100644 index 00000000000..525cd2764c1 --- /dev/null +++ b/packages/vanilla-router/examples/vanilla-jsx-example.ts @@ -0,0 +1,150 @@ +/** + * Example: Using headless router with component renderer (JSX) + * + * NOTE: This example is outdated - HeadlessRouter was refactored into utilities. + * The renderer is now located in examples/renderer/ for example use only. + */ + +import { createRouter, createRoute } from '../../src' +import { VanillaRenderer, type RenderContext } from './renderer/renderer' +import { jsx } from './renderer/jsx-runtime' + +// Define routes with JSX components +const rootRoute = createRoute({ + getParentRoute: () => rootRoute, + id: 'root', + component: () => { + return () => { + return jsx('div', {}, + jsx('nav', {}, + jsx('a', { href: '/' }, 'Home'), + jsx('a', { href: '/about' }, 'About'), + jsx('a', { href: '/posts' }, 'Posts') + ), + jsx('main', { id: 'outlet' }) + ) + } + }, +}) + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => { + return () => { + return jsx('div', {}, + jsx('h1', {}, 'Home Page'), + jsx('p', {}, 'Welcome to the home page!') + ) + } + }, +}) + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: () => { + return () => { + return jsx('div', {}, + jsx('h1', {}, 'About'), + jsx('p', {}, 'This is the about page.') + ) + } + }, +}) + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/posts', + component: () => { + return () => { + const posts = [ + { id: 1, title: 'Post 1' }, + { id: 2, title: 'Post 2' }, + ] + return jsx('div', {}, + jsx('h1', {}, 'Posts'), + jsx('ul', {}, + ...posts.map((post) => + jsx('li', {}, + jsx('a', { href: `/posts/${post.id}` }, post.title) + ) + ) + ) + ) + } + }, +}) + +const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '/posts/$postId', + component: () => { + return () => { + // Use route getter to access params + const params = postRoute.getParams() + return jsx('div', {}, + jsx('h1', {}, `Post ${params.postId}`), + jsx('p', {}, `This is post ${params.postId}`) + ) + } + }, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + postsRoute.addChildren([postRoute]), +]) + +const router = createRouter({ + routeTree, +}) + +// Render function - using component renderer +const renderer = new VanillaRenderer() + +function renderWithComponentRenderer( + state: RouterRenderState, + rootElement: HTMLElement, +) { + if (state.isNotFound) { + rootElement.innerHTML = '
    404 - Not Found
    ' + return + } + + if (state.globalError) { + rootElement.innerHTML = `
    Error: ${state.globalError.message}
    ` + return + } + + // Convert router state to render contexts + const contexts: RenderContext[] = state.matches.map((match) => ({ + component: match.component, + pendingComponent: match.pendingComponent, + errorComponent: match.errorComponent, + error: match.error, + isPending: match.isPending, + data: { + loaderData: match.loaderData, + params: match.params, + search: match.search, + routeId: match.routeId, + }, + })) + + // Use component renderer + const html = renderer.render(contexts) + rootElement.innerHTML = html +} + +// Initialize headless router +const rootElement = document.getElementById('app')! +const headlessRouter = new HeadlessRouter(router, (state) => { + renderWithComponentRenderer(state, rootElement) +}) + +// Setup link handlers +headlessRouter.setupLinkHandlers(rootElement) + diff --git a/packages/vanilla-router/package.json b/packages/vanilla-router/package.json new file mode 100644 index 00000000000..4cdd6af3ae1 --- /dev/null +++ b/packages/vanilla-router/package.json @@ -0,0 +1,77 @@ +{ + "name": "@tanstack/vanilla-router", + "version": "1.133.13", + "description": "Modern and scalable routing for vanilla JavaScript applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/vanilla-router" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "vanilla", + "javascript", + "location", + "router", + "routing", + "async", + "async router", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint", + "test:types": "tsc -p tsconfig.json --noEmit", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "pnpm run build:lib", + "build:lib": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/history": "workspace:*", + "@tanstack/router-core": "workspace:*", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "devDependencies": { + "combinate": "^1.1.11", + "typescript": "^5.7.2", + "vibe-rules": "^0.2.57", + "vite-plugin-dts": "^4.3.0", + "vitest": "^2.1.8", + "zod": "^3.24.2" + } +} + diff --git a/packages/vanilla-router/src/error-handling.ts b/packages/vanilla-router/src/error-handling.ts new file mode 100644 index 00000000000..d2237119507 --- /dev/null +++ b/packages/vanilla-router/src/error-handling.ts @@ -0,0 +1,71 @@ +import type { AnyRouter, AnyRouteMatch } from '@tanstack/router-core' +import { isNotFound, isRedirect } from '@tanstack/router-core' +import type { VanillaErrorRouteComponent, VanillaNotFoundRouteComponent } from './types' + +/** + * Check if an error is a NotFoundError + * + * @param error - The error to check + * @returns True if the error is a NotFoundError + */ +export function checkIsNotFound(error: unknown): error is import('@tanstack/router-core').NotFoundError { + return isNotFound(error) +} + +/** + * Check if an error is a Redirect + * + * @param error - The error to check + * @returns True if the error is a Redirect + */ +export function checkIsRedirect(error: unknown): error is import('@tanstack/router-core').Redirect { + return isRedirect(error) +} + +/** + * Get the error component for a match + * This checks the route's errorComponent option and falls back to router's defaultErrorComponent + * + * @param router - The router instance + * @param match - The match to get error component for + * @returns The error component factory function, or undefined if none configured + */ +export function getErrorComponent( + router: TRouter, + match: AnyRouteMatch, +): VanillaErrorRouteComponent | undefined { + const route = router.routesById[match.routeId] + if (!route) return undefined + + const errorComponent = route.options.errorComponent === false + ? undefined + : route.options.errorComponent ?? router.options.defaultErrorComponent + + return errorComponent +} + +/** + * Get the not found component for a match + * This checks the route's notFoundComponent option and falls back to router's defaultNotFoundComponent + * + * @param router - The router instance + * @param match - The match to get not found component for + * @returns The not found component factory function, or undefined if none configured + */ +export function getNotFoundComponent( + router: TRouter, + match: AnyRouteMatch, +): VanillaNotFoundRouteComponent | undefined { + const route = router.routesById[match.routeId] + if (!route) return undefined + + const notFoundComponent = route.options.notFoundComponent === false + ? undefined + : route.options.notFoundComponent ?? router.options.defaultNotFoundComponent + + return notFoundComponent +} + +// Re-export isNotFound and isRedirect for convenience +export { isNotFound, isRedirect } from '@tanstack/router-core' + diff --git a/packages/vanilla-router/src/fileRoute.ts b/packages/vanilla-router/src/fileRoute.ts new file mode 100644 index 00000000000..560f52c6d80 --- /dev/null +++ b/packages/vanilla-router/src/fileRoute.ts @@ -0,0 +1,267 @@ +import warning from 'tiny-warning' +import { createRoute } from './route' +import type { + AnyContext, + AnyRoute, + AnyRouter, + Constrain, + ConstrainLiteral, + FileBaseRouteOptions, + FileRoutesByPath, + LazyRouteOptions, + Register, + RegisteredRouter, + ResolveParams, + Route, + RouteById, + RouteConstraints, + RouteIds, + RouteLoaderFn, + UpdatableRouteOptions, +} from '@tanstack/router-core' +import type { VanillaRouteComponent } from './types' + +/** + * Create a file route for vanilla JS + * This is adapted from React/Preact fileRoute but without hooks + * Instead, route getters are used (e.g., route.getLoaderData(router)) + * + * @param path - The file path (e.g., '/posts/$postId') + * @returns A function that creates a route with the given options + */ +export function createFileRoute< + TFilePath extends keyof FileRoutesByPath, + TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'], + TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'], + TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'], + TFullPath extends + RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'], +>( + path?: TFilePath, +): FileRoute['createRoute'] { + if (typeof path === 'object') { + return new FileRoute(path, { + silent: true, + }).createRoute(path) as any + } + return new FileRoute(path, { + silent: true, + }).createRoute +} + +/** + * FileRoute class for vanilla JS + * Provides route creation without hooks - use route getters instead + * + * @deprecated It's no longer recommended to use the `FileRoute` class directly. + * Instead, use `createFileRoute('/path/to/file')(options)` to create a file route. + */ +export class FileRoute< + TFilePath extends keyof FileRoutesByPath, + TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'], + TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'], + TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'], + TFullPath extends + RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'], +> { + silent?: boolean + + constructor( + public path?: TFilePath, + _opts?: { silent: boolean }, + ) { + this.silent = _opts?.silent + } + + createRoute = < + TRegister = Register, + TSearchValidator = undefined, + TParams = ResolveParams, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, + TLoaderDeps extends Record = {}, + TLoaderFn = undefined, + TChildren = unknown, + TSSR = unknown, + const TMiddlewares = unknown, + THandlers = undefined, + >( + options?: FileBaseRouteOptions< + TRegister, + TParentRoute, + TId, + TPath, + TSearchValidator, + TParams, + TLoaderDeps, + TLoaderFn, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + AnyContext, + TSSR, + TMiddlewares, + THandlers + > & + UpdatableRouteOptions< + TParentRoute, + TId, + TFullPath, + TParams, + TSearchValidator, + TLoaderFn, + TLoaderDeps, + AnyContext, + TRouteContextFn, + TBeforeLoadFn + >, + ): ReturnType => { + warning( + this.silent, + 'FileRoute is deprecated and will be removed in the next major version. Use the createFileRoute(path)(options) function instead.', + ) + const route = createRoute(options as any) + ;(route as any).isRoot = false + return route as any + } +} + +/** + * FileRouteLoader for vanilla JS + * Note: In vanilla JS, loaders should be defined directly in route options + * This is provided for compatibility but is deprecated + * + * @deprecated It's recommended not to split loaders into separate files. + * Instead, place the loader function in the main route file, inside the + * `createFileRoute('/path/to/file')(options)` options. + */ +export function FileRouteLoader< + TFilePath extends keyof FileRoutesByPath, + TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'], +>( + _path: TFilePath, +): ( + loaderFn: Constrain< + TLoaderFn, + RouteLoaderFn< + Register, + TRoute['parentRoute'], + TRoute['types']['id'], + TRoute['types']['params'], + TRoute['types']['loaderDeps'], + TRoute['types']['routerContext'], + TRoute['types']['routeContextFn'], + TRoute['types']['beforeLoadFn'] + > + >, +) => TLoaderFn { + warning( + false, + `FileRouteLoader is deprecated and will be removed in the next major version. Please place the loader function in the main route file, inside the \`createFileRoute('/path/to/file')(options)\` options`, + ) + return (loaderFn) => loaderFn as any +} + +/** + * Create a lazy route for vanilla JS + * Note: Lazy routes in vanilla JS don't use hooks - use route getters instead + */ +export function createLazyRoute< + TRouter extends AnyRouter = RegisteredRouter, + TId extends string = string, + TRoute extends AnyRoute = RouteById, +>(id: ConstrainLiteral>) { + return (opts: LazyRouteOptions) => { + return new LazyRoute({ + id: id, + ...opts, + }) + } +} + +/** + * Create a lazy file route for vanilla JS + */ +export function createLazyFileRoute< + TFilePath extends keyof FileRoutesByPath, + TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'], +>(id: TFilePath): (opts: LazyRouteOptions) => LazyRoute { + if (typeof id === 'object') { + return new LazyRoute(id) as any + } + + return (opts: LazyRouteOptions) => new LazyRoute({ id, ...opts }) +} + +/** + * LazyRoute class for vanilla JS + * Note: In vanilla JS, route getters should be used instead of hooks + */ +export class LazyRoute { + options: { + id: string + } & LazyRouteOptions + + constructor( + opts: { + id: string + } & LazyRouteOptions, + ) { + this.options = opts + } + + /** + * Get match data for this lazy route + * Use route.getMatch(router) instead of this method + */ + getMatch(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find((m) => m.routeId === this.options.id) + if (!match) return undefined + return router.getMatch(match.id) + } + + /** + * Get loader data for this lazy route + * Use route.getLoaderData(router) instead of this method + */ + getLoaderData(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find((m) => m.routeId === this.options.id) + if (!match) return undefined + const matchState = router.getMatch(match.id) + return matchState?.loaderData + } + + /** + * Get params for this lazy route + * Use route.getParams(router) instead of this method + */ + getParams(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find((m) => m.routeId === this.options.id) + if (!match) return {} + const matchState = router.getMatch(match.id) + return matchState?._strictParams ?? matchState?.params ?? {} + } + + /** + * Get search params for this lazy route + * Use route.getSearch(router) instead of this method + */ + getSearch(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find((m) => m.routeId === this.options.id) + if (!match) return {} + const matchState = router.getMatch(match.id) + return matchState?._strictSearch ?? matchState?.search ?? {} + } + + /** + * Get route context for this lazy route + * Use route.getRouteContext(router) instead of this method + */ + getRouteContext(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find((m) => m.routeId === this.options.id) + if (!match) return {} + const matchState = router.getMatch(match.id) + return matchState?.context ?? {} + } +} + diff --git a/packages/vanilla-router/src/index.ts b/packages/vanilla-router/src/index.ts new file mode 100644 index 00000000000..dd1d2cedb1d --- /dev/null +++ b/packages/vanilla-router/src/index.ts @@ -0,0 +1,101 @@ +export { createRouter } from './router' +export type { Router } from './router' + +export { + createRoute, + createRootRoute, + Route, + RootRoute, +} from './route' +export type { + VanillaRouteComponent, + VanillaErrorRouteComponent, + VanillaNotFoundRouteComponent, +} from './types' + +export { + buildHref, + getMatchesHtml, + outlet, + setupLinkHandlers, + vanillaRouter, +} from './vanilla-router' + +// Router state utilities +export { + subscribeRouterState, + getRouterState, + getLocation, + getMatches, +} from './utils' + +// Navigation utilities +export { + navigate, + canGoBack, + goBack, + goForward, + go, +} from './navigation' + +// Route data utilities +export { + getMatchData, + getParams, + getSearch, + getLoaderData, + getRouteContext, + getLoaderDeps, +} from './route-data' + +// Error handling utilities +export { + checkIsNotFound, + checkIsRedirect, + getErrorComponent, + getNotFoundComponent, + isNotFound, + isRedirect, +} from './error-handling' + +// Scroll restoration utilities +export { + setupScrollRestorationUtil as setupScrollRestoration, + getScrollPosition, + saveScrollPosition, +} from './scroll-restoration' + +// File route utilities +export { + createFileRoute, + FileRoute, + FileRouteLoader, + createLazyRoute, + createLazyFileRoute, + LazyRoute, +} from './fileRoute' + +// Re-export core types and utilities +export type { + AnyRouter, + AnyRoute, + RouterState, + NavigateOptions, + ParsedLocation, +} from '@tanstack/router-core' + +export { + redirect, + notFound, +} from '@tanstack/router-core' + +export { + createBrowserHistory, + createHashHistory, + createMemoryHistory, +} from '@tanstack/history' + +export type { + RouterHistory, + HistoryLocation, +} from '@tanstack/history' diff --git a/packages/vanilla-router/src/navigation.ts b/packages/vanilla-router/src/navigation.ts new file mode 100644 index 00000000000..43a9aa74457 --- /dev/null +++ b/packages/vanilla-router/src/navigation.ts @@ -0,0 +1,73 @@ +import type { AnyRouter, NavigateOptions } from '@tanstack/router-core' + +/** + * Navigate programmatically + * This is a vanilla JS equivalent of useNavigate hook + * + * @param router - The router instance + * @param options - Navigation options (to, params, search, hash, replace, etc.) + * @returns Promise that resolves when navigation completes + */ +export function navigate( + router: TRouter, + options: NavigateOptions, +): Promise { + return router.navigate(options) +} + +/** + * Check if the router can go back in history + * This is a vanilla JS equivalent of useCanGoBack hook + * + * @param router - The router instance + * @returns True if can go back, false otherwise + */ +export function canGoBack( + router: TRouter, +): boolean { + return router.history.canGoBack() +} + +/** + * Go back in history + * Equivalent to browser back button + * + * @param router - The router instance + * @param options - Optional navigation options (e.g., ignoreBlocker) + */ +export function goBack( + router: TRouter, + options?: { ignoreBlocker?: boolean }, +): void { + router.history.back(options) +} + +/** + * Go forward in history + * Equivalent to browser forward button + * + * @param router - The router instance + * @param options - Optional navigation options (e.g., ignoreBlocker) + */ +export function goForward( + router: TRouter, + options?: { ignoreBlocker?: boolean }, +): void { + router.history.forward(options) +} + +/** + * Go to a specific index in history + * + * @param router - The router instance + * @param index - The index to navigate to (negative goes back, positive goes forward) + * @param options - Optional navigation options (e.g., ignoreBlocker) + */ +export function go( + router: TRouter, + index: number, + options?: { ignoreBlocker?: boolean }, +): void { + router.history.go(index, options) +} + diff --git a/packages/vanilla-router/src/route-data.ts b/packages/vanilla-router/src/route-data.ts new file mode 100644 index 00000000000..d8310efc4cb --- /dev/null +++ b/packages/vanilla-router/src/route-data.ts @@ -0,0 +1,116 @@ +import type { AnyRouter } from '@tanstack/router-core' + +/** + * Get match data for a specific route by routeId + * This is a vanilla JS equivalent of useMatch hook + * + * @param router - The router instance + * @param routeId - The route ID to get match data for + * @returns Match data for the route, or undefined if not found + */ +export function getMatchData( + router: TRouter, + routeId: string, +): TRouter['state']['matches'][0] | undefined { + return router.state.matches.find((match) => match.routeId === routeId) +} + +/** + * Get params for a specific route by routeId + * This is a vanilla JS equivalent of useParams hook + * + * @param router - The router instance + * @param routeId - The route ID to get params for + * @param strict - Whether to use strict params (default: true) + * @returns Params for the route, or undefined if route not found + */ +export function getParams( + router: TRouter, + routeId: string, + strict: boolean = true, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + if (!matchState) return undefined + + return strict ? matchState._strictParams : matchState.params +} + +/** + * Get search params for a specific route by routeId + * This is a vanilla JS equivalent of useSearch hook + * + * @param router - The router instance + * @param routeId - The route ID to get search params for + * @returns Search params for the route, or undefined if route not found + */ +export function getSearch( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.search +} + +/** + * Get loader data for a specific route by routeId + * This is a vanilla JS equivalent of useLoaderData hook + * + * @param router - The router instance + * @param routeId - The route ID to get loader data for + * @returns Loader data for the route, or undefined if route not found or loader hasn't run + */ +export function getLoaderData( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.loaderData +} + +/** + * Get route context for a specific route by routeId + * This is a vanilla JS equivalent of useRouteContext hook + * + * @param router - The router instance + * @param routeId - The route ID to get context for + * @returns Route context for the route, or undefined if route not found + */ +export function getRouteContext( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.context +} + +/** + * Get loader dependencies for a specific route by routeId + * This is a vanilla JS equivalent of useLoaderDeps hook + * + * @param router - The router instance + * @param routeId - The route ID to get loader deps for + * @returns Loader dependencies for the route, or undefined if route not found + */ +export function getLoaderDeps( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.loaderDeps +} + diff --git a/packages/vanilla-router/src/route.ts b/packages/vanilla-router/src/route.ts new file mode 100644 index 00000000000..30f59406fc0 --- /dev/null +++ b/packages/vanilla-router/src/route.ts @@ -0,0 +1,421 @@ +import { + BaseRootRoute, + BaseRoute, + notFound, +} from '@tanstack/router-core' +import type { + AnyContext, + AnyRoute, + AnyRouter, + NotFoundError, + Register, + ResolveFullPath, + ResolveId, + ResolveParams, + RootRouteOptions, + RouteConstraints, + RouteOptions, +} from '@tanstack/router-core' + +// Router context for route getters - deprecated, router should be passed as parameter +declare global { + var __tanstackRouter: import('@tanstack/router-core').AnyRouter | undefined +} + +declare module '@tanstack/router-core' { + export interface UpdatableRouteOptionsExtensions { + component?: VanillaRouteComponent + errorComponent?: false | null | undefined | VanillaErrorRouteComponent + notFoundComponent?: VanillaNotFoundRouteComponent + pendingComponent?: VanillaRouteComponent + } + + export interface RootRouteOptionsExtensions { + shellComponent?: VanillaRouteComponent + } +} + +export class Route< + in out TRegister = unknown, + in out TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute, + in out TPath extends RouteConstraints['TPath'] = '/', + in out TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath< + TParentRoute, + TPath + >, + in out TCustomId extends RouteConstraints['TCustomId'] = string, + in out TId extends RouteConstraints['TId'] = ResolveId< + TParentRoute, + TCustomId, + TPath + >, + in out TSearchValidator = undefined, + in out TParams = ResolveParams, + in out TRouterContext = AnyContext, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, + in out TLoaderDeps extends Record = {}, + in out TLoaderFn = undefined, + in out TChildren = unknown, + in out TFileRouteTypes = unknown, + in out TSSR = unknown, + in out TServerMiddlewares = unknown, + in out THandlers = undefined, + > + extends BaseRoute< + TRegister, + TParentRoute, + TPath, + TFullPath, + TCustomId, + TId, + TSearchValidator, + TParams, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TFileRouteTypes, + TSSR, + TServerMiddlewares, + THandlers + > { + constructor( + options?: RouteOptions< + TRegister, + TParentRoute, + TId, + TCustomId, + TFullPath, + TPath, + TSearchValidator, + TParams, + TLoaderDeps, + TLoaderFn, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TSSR, + TServerMiddlewares, + THandlers + >, + ) { + super(options) + } + + notFound = (opts?: NotFoundError) => { + return notFound({ routeId: this.id as string, ...opts }) + } + + /** + * Get the loader data for this route + * @param router - The router instance (required) + */ + getLoaderData = (router: import('@tanstack/router-core').AnyRouter): TLoaderFn extends undefined ? undefined : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined as any + const matchState = router.getMatch(match.id) + return matchState?.loaderData as any + } + + /** + * Get the params for this route + * @param router - The router instance (required) + */ + getParams = (router: import('@tanstack/router-core').AnyRouter): TParams => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as TParams + const matchState = router.getMatch(match.id) + return (matchState?._strictParams ?? matchState?.params ?? {}) as TParams + } + + /** + * Get the search params for this route + * @param router - The router instance (required) + */ + getSearch = (router: import('@tanstack/router-core').AnyRouter): TSearchValidator extends undefined ? Record : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return (matchState?._strictSearch ?? matchState?.search ?? {}) as any + } + + /** + * Get the route context for this route + * @param router - The router instance (required) + */ + getRouteContext = (router: import('@tanstack/router-core').AnyRouter): TRouteContextFn extends AnyContext ? AnyContext : TRouteContextFn => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return matchState?.context as any + } + + /** + * Get the loader dependencies for this route + * @param router - The router instance (required) + */ + getLoaderDeps = (router: import('@tanstack/router-core').AnyRouter): TLoaderDeps => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as TLoaderDeps + const matchState = router.getMatch(match.id) + return matchState?.loaderDeps ?? ({} as TLoaderDeps) + } + + /** + * Get the full match data for this route + * @param router - The router instance (required) + */ + getMatch = (router: import('@tanstack/router-core').AnyRouter) => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined + return router.getMatch(match.id) + } +} + +export function createRoute< + TRegister = unknown, + TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute, + TPath extends RouteConstraints['TPath'] = '/', + TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath< + TParentRoute, + TPath + >, + TCustomId extends RouteConstraints['TCustomId'] = string, + TId extends RouteConstraints['TId'] = ResolveId< + TParentRoute, + TCustomId, + TPath + >, + TSearchValidator = undefined, + TParams = ResolveParams, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, + TLoaderDeps extends Record = {}, + TLoaderFn = undefined, + TChildren = unknown, + TSSR = unknown, + const TServerMiddlewares = unknown, +>( + options: RouteOptions< + TRegister, + TParentRoute, + TId, + TCustomId, + TFullPath, + TPath, + TSearchValidator, + TParams, + TLoaderDeps, + TLoaderFn, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + TSSR, + TServerMiddlewares + >, +): Route< + TRegister, + TParentRoute, + TPath, + TFullPath, + TCustomId, + TId, + TSearchValidator, + TParams, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TSSR, + TServerMiddlewares +> { + return new Route< + TRegister, + TParentRoute, + TPath, + TFullPath, + TCustomId, + TId, + TSearchValidator, + TParams, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TSSR, + TServerMiddlewares + >(options as any) +} + +export class RootRoute< + in out TRegister = unknown, + in out TSearchValidator = undefined, + in out TRouterContext = {}, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, + in out TLoaderDeps extends Record = {}, + in out TLoaderFn = undefined, + in out TChildren = unknown, + in out TFileRouteTypes = unknown, + in out TSSR = unknown, + in out TServerMiddlewares = unknown, + in out THandlers = undefined, + > + extends BaseRootRoute< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TFileRouteTypes, + TSSR, + TServerMiddlewares, + THandlers + > { + constructor( + options?: RootRouteOptions< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TSSR, + TServerMiddlewares, + THandlers + >, + ) { + super(options) + } + + /** + * Get the loader data for this route + * @param router - The router instance (required) + */ + getLoaderData = (router: import('@tanstack/router-core').AnyRouter): TLoaderFn extends undefined ? undefined : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined as any + const matchState = router.getMatch(match.id) + return matchState?.loaderData as any + } + + /** + * Get the params for this route + * @param router - The router instance (required) + */ + getParams = (router: import('@tanstack/router-core').AnyRouter): Record => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as Record + const matchState = router.getMatch(match.id) + return (matchState?._strictParams ?? matchState?.params ?? {}) as Record + } + + /** + * Get the search params for this route + * @param router - The router instance (required) + */ + getSearch = (router: import('@tanstack/router-core').AnyRouter): TSearchValidator extends undefined ? Record : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return (matchState?._strictSearch ?? matchState?.search ?? {}) as any + } + + /** + * Get the route context for this route + * @param router - The router instance (required) + */ + getRouteContext = (router: import('@tanstack/router-core').AnyRouter): TRouteContextFn extends AnyContext ? AnyContext : TRouteContextFn => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return matchState?.context as any + } + + /** + * Get the loader dependencies for this route + * @param router - The router instance (required) + */ + getLoaderDeps = (router: import('@tanstack/router-core').AnyRouter): TLoaderDeps => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as TLoaderDeps + const matchState = router.getMatch(match.id) + return matchState?.loaderDeps ?? ({} as TLoaderDeps) + } + + /** + * Get the full match data for this route + * @param router - The router instance (required) + */ + getMatch = (router: import('@tanstack/router-core').AnyRouter) => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined + return router.getMatch(match.id) + } +} + +export function createRootRoute< + TRegister = Register, + TSearchValidator = undefined, + TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, + TLoaderDeps extends Record = {}, + TLoaderFn = undefined, + TSSR = unknown, + const TServerMiddlewares = unknown, + THandlers = undefined, +>( + options?: RootRouteOptions< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TSSR, + TServerMiddlewares, + THandlers + >, +): RootRoute< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + unknown, + unknown, + TSSR, + TServerMiddlewares, + THandlers +> { + return new RootRoute< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + unknown, + unknown, + TSSR, + TServerMiddlewares, + THandlers + >(options as any) +} + diff --git a/packages/vanilla-router/src/router.ts b/packages/vanilla-router/src/router.ts new file mode 100644 index 00000000000..e78498a1ae5 --- /dev/null +++ b/packages/vanilla-router/src/router.ts @@ -0,0 +1,86 @@ +import { RouterCore } from '@tanstack/router-core' +import type { RouterHistory } from '@tanstack/history' +import type { + AnyRoute, + CreateRouterFn, + RouterConstructorOptions, + RouterState, + TrailingSlashOption, +} from '@tanstack/router-core' + +import type { + VanillaErrorRouteComponent, + VanillaNotFoundRouteComponent, + VanillaRouteComponent, +} from './types' + +declare module '@tanstack/router-core' { + export interface RouterOptionsExtensions { + /** + * The default `component` a route should use if no component is provided. + * + * @default Outlet + */ + defaultComponent?: VanillaRouteComponent + /** + * The default `errorComponent` a route should use if no error component is provided. + */ + defaultErrorComponent?: VanillaErrorRouteComponent + /** + * The default `pendingComponent` a route should use if no pending component is provided. + */ + defaultPendingComponent?: VanillaRouteComponent + /** + * The default `notFoundComponent` a route should use if no notFound component is provided. + */ + defaultNotFoundComponent?: VanillaNotFoundRouteComponent + } +} + +export const createRouter: CreateRouterFn = (options) => { + return new Router(options) +} + +export class Router< + in out TRouteTree extends AnyRoute, + in out TTrailingSlashOption extends TrailingSlashOption = 'never', + in out TDefaultStructuralSharingOption extends boolean = false, + in out TRouterHistory extends RouterHistory = RouterHistory, + in out TDehydrated extends Record = Record, +> extends RouterCore< + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated +> { + constructor( + options: RouterConstructorOptions< + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated + >, + ) { + super(options) + } + + /** + * Subscribe to router state changes + * This is the recommended way to react to router state updates in vanilla JS + * Similar to React Router's useRouterState hook + * + * @param callback - Function called whenever router state changes + * @returns Unsubscribe function + */ + subscribeState( + callback: (state: RouterState) => void, + ): () => void { + // Subscribe directly to the router's store (same as React Router does internally) + return this.__store.subscribe(() => { + callback(this.state) + }) + } +} + diff --git a/packages/vanilla-router/src/scroll-restoration.ts b/packages/vanilla-router/src/scroll-restoration.ts new file mode 100644 index 00000000000..5bc310c0ac2 --- /dev/null +++ b/packages/vanilla-router/src/scroll-restoration.ts @@ -0,0 +1,123 @@ +import type { AnyRouter, ParsedLocation, ScrollRestorationEntry } from '@tanstack/router-core' +import { + defaultGetScrollRestorationKey, + getCssSelector, + scrollRestorationCache, + setupScrollRestoration, +} from '@tanstack/router-core' + +/** + * Setup scroll restoration for the router + * This is typically called automatically when scrollRestoration: true is set in router options + * But can be called manually if needed + * + * @param router - The router instance + * @param force - Force setup even if scrollRestoration is false in options + */ +export function setupScrollRestorationUtil( + router: TRouter, + force?: boolean, +): void { + setupScrollRestoration(router, force) +} + +/** + * Get scroll position for a specific element or window + * This is a vanilla JS equivalent of useElementScrollRestoration hook + * + * @param router - The router instance + * @param options - Options for getting scroll position + * @param options.id - Unique ID for the element (must match data-scroll-restoration-id attribute) + * @param options.getElement - Function that returns the element to get scroll position for + * @param options.getKey - Optional function to get the cache key (defaults to location.href) + * @returns Scroll restoration entry with scrollX and scrollY, or undefined if not found + */ +export function getScrollPosition( + router: TRouter, + options: { + id?: string + getElement?: () => Window | Element | undefined | null + getKey?: (location: ParsedLocation) => string + }, +): ScrollRestorationEntry | undefined { + if (!scrollRestorationCache) return undefined + + const getKey = options.getKey || defaultGetScrollRestorationKey + const restoreKey = getKey(router.latestLocation) + const byKey = scrollRestorationCache.state[restoreKey] + + if (!byKey) return undefined + + let elementSelector = '' + + if (options.id) { + elementSelector = `[data-scroll-restoration-id="${options.id}"]` + } else { + const element = options.getElement?.() + if (!element) { + return undefined + } + elementSelector = + element instanceof Window ? 'window' : getCssSelector(element) + } + + return byKey[elementSelector] +} + +/** + * Save scroll position for a specific element or window + * This is typically handled automatically by setupScrollRestoration + * But can be called manually if needed + * + * @param router - The router instance + * @param options - Options for saving scroll position + * @param options.id - Unique ID for the element (must match data-scroll-restoration-id attribute) + * @param options.getElement - Function that returns the element to save scroll position for + * @param options.getKey - Optional function to get the cache key (defaults to location.href) + */ +export function saveScrollPosition( + router: TRouter, + options: { + id?: string + getElement?: () => Window | Element | undefined | null + getKey?: (location: ParsedLocation) => string + }, +): void { + if (!scrollRestorationCache) return + + const getKey = options.getKey || defaultGetScrollRestorationKey + const restoreKey = getKey(router.latestLocation) + + let elementSelector = '' + + if (options.id) { + elementSelector = `[data-scroll-restoration-id="${options.id}"]` + } else { + const element = options.getElement?.() + if (!element) { + return + } + elementSelector = + element instanceof Window ? 'window' : getCssSelector(element) + } + + scrollRestorationCache.set((state) => { + const keyEntry = (state[restoreKey] ||= {} as any) + + const elementEntry = (keyEntry[elementSelector] ||= {} as ScrollRestorationEntry) + + if (elementSelector === 'window') { + elementEntry.scrollX = window.scrollX || 0 + elementEntry.scrollY = window.scrollY || 0 + } else if (elementSelector) { + const element = document.querySelector(elementSelector) + if (element) { + elementEntry.scrollX = element.scrollLeft || 0 + elementEntry.scrollY = element.scrollTop || 0 + } + } + + return state + }) +} + diff --git a/packages/vanilla-router/src/types.ts b/packages/vanilla-router/src/types.ts new file mode 100644 index 00000000000..bdcc0f4d1bb --- /dev/null +++ b/packages/vanilla-router/src/types.ts @@ -0,0 +1,8 @@ +// Vanilla component types +// Components receive router as parameter and return a render function with no parameters +export type VanillaComponent = (router: import('@tanstack/router-core').AnyRouter) => (() => string) | [() => void, () => string] + +export type VanillaRouteComponent = VanillaComponent +export type VanillaErrorRouteComponent = (props: { error: Error }) => VanillaComponent +export type VanillaNotFoundRouteComponent = (props: { data?: any }) => VanillaComponent + diff --git a/packages/vanilla-router/src/utils.ts b/packages/vanilla-router/src/utils.ts new file mode 100644 index 00000000000..d06e1135943 --- /dev/null +++ b/packages/vanilla-router/src/utils.ts @@ -0,0 +1,90 @@ +import type { AnyRouter, RouterState } from '@tanstack/router-core' +import { replaceEqualDeep } from '@tanstack/router-core' + +/** + * Subscribe to router state changes with optional selector + * This is a vanilla JS equivalent of useRouterState hook + * + * @param router - The router instance + * @param callback - Function called whenever router state changes (or selected state changes) + * @param selector - Optional function to select a portion of the router state + * @param structuralSharing - Whether to use structural sharing (deep equality check) for the selected value + * @returns Unsubscribe function + */ +export function subscribeRouterState( + router: TRouter, + callback: (state: RouterState) => void, + selector?: (state: RouterState) => any, + structuralSharing?: boolean, +): () => void { + let previousResult: any = undefined + + const unsubscribe = router.subscribeState((state) => { + if (selector) { + const newSlice = selector(state) + + if (structuralSharing ?? router.options.defaultStructuralSharing) { + const sharedSlice = replaceEqualDeep(previousResult, newSlice) + previousResult = sharedSlice + + // Always call callback - replaceEqualDeep handles equality checking + callback(sharedSlice) + } else { + callback(newSlice) + } + } else { + callback(state) + } + }) + + return unsubscribe +} + +/** + * Get current router state (synchronous) + * This is a vanilla JS equivalent of useRouterState hook without subscription + * + * @param router - The router instance + * @param selector - Optional function to select a portion of the router state + * @returns Current router state or selected portion + */ +export function getRouterState( + router: TRouter, + selector?: (state: RouterState) => any, +): any { + const state = router.state + return selector ? selector(state) : state +} + +/** + * Get current location from router state + * This is a vanilla JS equivalent of useLocation hook + * + * @param router - The router instance + * @param selector - Optional function to select a portion of the location + * @returns Current location or selected portion + */ +export function getLocation( + router: TRouter, + selector?: (location: RouterState['location']) => any, +): any { + const location = router.state.location + return selector ? selector(location) : location +} + +/** + * Get current matches from router state + * This is a vanilla JS equivalent of useMatches hook + * + * @param router - The router instance + * @param selector - Optional function to select/transform matches + * @returns Current matches or selected/transformed matches + */ +export function getMatches( + router: TRouter, + selector?: (matches: RouterState['matches']) => any, +): any { + const matches = router.state.matches + return selector ? selector(matches) : matches +} + diff --git a/packages/vanilla-router/src/vanilla-router.ts b/packages/vanilla-router/src/vanilla-router.ts new file mode 100644 index 00000000000..218a739da86 --- /dev/null +++ b/packages/vanilla-router/src/vanilla-router.ts @@ -0,0 +1,246 @@ +import type { AnyRouter, AnyRoute } from '@tanstack/router-core' +import { isNotFound } from '@tanstack/router-core' +import { setupScrollRestorationUtil } from './scroll-restoration' + +/** + * Internal outlet marker - used by outlet() function + */ +const OUTLET_MARKER = '__TANSTACK_ROUTER_OUTLET__' + +/** + * Function to mark where child routes should be rendered + * Returns a special marker that will be replaced with child content + */ +export function outlet(): string { + return OUTLET_MARKER +} + +/** + * Process router matches and return nested HTML strings + * Handles outlet replacement for nested routes automatically + * Returns an array of HTML strings in render order + * + * @param router - The router instance + * @param matches - Array of matches from router.state.matches + * @returns Array of HTML strings with nested routes properly composed + */ +export function getMatchesHtml( + router: AnyRouter, + matches: AnyRouter['state']['matches'], +): string[] { + const htmlParts: string[] = [] + + matches.forEach((match, index) => { + // Get HTML for this match + const componentHtml = getMatchHtml(router, match) + + // For nested routes, replace outlet marker in parent + if (index > 0 && htmlParts.length > 0) { + const lastHtml = htmlParts[htmlParts.length - 1] + if (lastHtml.includes(OUTLET_MARKER)) { + htmlParts[htmlParts.length - 1] = lastHtml.replace( + OUTLET_MARKER, + componentHtml, + ) + } else { + htmlParts.push(componentHtml) + } + } else { + htmlParts.push(componentHtml) + } + }) + + // Clean up any remaining outlet markers (e.g., when root route has outlet but no children) + return htmlParts.map(html => html.replace(OUTLET_MARKER, '')) +} + +/** + * Get HTML for a single match (handles error, pending, not found, and component states) + */ +function getMatchHtml(router: AnyRouter, match: AnyRouter['state']['matches'][0]): string { + const route: AnyRoute = router.routesById[match.routeId] + const matchState = router.getMatch(match.id) + + try { + // Check for not found status first (like React/Preact adapters) + if (match.status === 'notFound') { + const notFoundComponent = route.options.notFoundComponent === false + ? undefined + : route.options.notFoundComponent ?? router.options.defaultNotFoundComponent + + if (notFoundComponent && matchState?.error && isNotFound(matchState.error)) { + const notFoundFactory = notFoundComponent({ data: matchState.error }) + const notFoundHtml = notFoundFactory(router) + return typeof notFoundHtml === 'string' ? notFoundHtml : notFoundHtml() + } + + // Fallback if no notFoundComponent configured + return '
    Not Found
    ' + } + + // Get components from route options + const errorComponent = route.options.errorComponent === false + ? undefined + : route.options.errorComponent ?? router.options.defaultErrorComponent + const pendingComponent = route.options.pendingComponent ?? router.options.defaultPendingComponent + const component = route.options.component ?? router.options.defaultComponent + + // Check for error state + if (matchState?.error && errorComponent) { + const errorFactory = errorComponent({ error: matchState.error }) + const errorHtml = errorFactory(router) + return typeof errorHtml === 'string' ? errorHtml : errorHtml() + } + + // Check for pending state + if (matchState?._displayPending && pendingComponent) { + const pendingHtml = pendingComponent(router) + return typeof pendingHtml === 'string' ? pendingHtml : pendingHtml() + } + + // Render component + if (component) { + const componentHtml = component(router) + return typeof componentHtml === 'string' ? componentHtml : componentHtml() + } + + return '' + } catch (error) { + // If component throws, return empty string + console.error('Error rendering component:', error) + return '' + } +} + +/** + * Build a type-safe href for navigation + * Useful for direct DOM manipulation where you don't have Link components + * Behaves like navigate() and APIs - accepts a 'to' path string + */ +export function buildHref( + router: AnyRouter, + options: { + to?: string + params?: Record + search?: Record + hash?: string + }, +): string { + const location = router.buildLocation({ + to: options.to, + params: options.params, + search: options.search, + hash: options.hash, + }) + return location.href +} + +/** + * Enable automatic link handling for the entire document + * Returns a cleanup function to remove event listeners + */ +export function setupLinkHandlers(router: AnyRouter): () => void { + // Use event delegation for link clicks on the entire document + const linkClickHandler = (e: Event) => { + const target = e.target as HTMLElement + const link = target.closest('a[href]') as HTMLAnchorElement + if (!link) return + + const href = link.getAttribute('href') + if (!href) return + + // Skip external links and links with target + if (href.startsWith('http://') || href.startsWith('https://')) return + if (link.target && link.target !== '_self') return + + // Skip if modifier keys are pressed + if ( + (e as MouseEvent).metaKey || + (e as MouseEvent).ctrlKey || + (e as MouseEvent).altKey || + (e as MouseEvent).shiftKey + ) + return + + e.preventDefault() + e.stopPropagation() + + const replace = link.hasAttribute('data-replace') + const resetScroll = link.hasAttribute('data-reset-scroll') + const hashScroll = link.getAttribute('data-hash-scroll') + + router + .navigate({ + to: href, + replace, + resetScroll: resetScroll !== null, + hashScrollIntoView: + hashScroll === 'true' + ? true + : hashScroll === 'false' + ? false + : undefined, + }) + .then(() => { + // Router state change will trigger render via subscription + }) + .catch((err) => { + console.error('Navigation error:', err) + }) + } + + document.addEventListener('click', linkClickHandler, true) + + // Return cleanup function + return () => { + document.removeEventListener('click', linkClickHandler, true) + } +} + +/** + * Setup router with automatic state subscription and link handling + * This is a convenience function that combines subscribeState and setupLinkHandlers + * Also handles initial loading and rendering + * + * @param router - The router instance + * @param renderCallback - Function called whenever router state changes to render the UI + * @returns Cleanup function that unsubscribes from state changes and removes link handlers + */ +export async function vanillaRouter( + router: AnyRouter, + renderCallback: () => void, +): Promise<() => void> { + // Load initial matches if needed + if (router.state.matches.length === 0) { + try { + await router.load() + } catch (error) { + console.error('Error loading router:', error) + } + } + + // Setup scroll restoration if enabled + if (router.options.scrollRestoration) { + setupScrollRestorationUtil(router) + } + + // Initial render + renderCallback() + + // Subscribe to router state changes + // Use subscribeState if available (vanilla Router), otherwise fall back to __store + const unsubscribeState = + typeof (router as any).subscribeState === 'function' + ? (router as any).subscribeState(renderCallback) + : router.__store.subscribe(renderCallback) + + // Setup link handlers on document + const cleanupLinkHandlers = setupLinkHandlers(router) + + // Return combined cleanup function + return () => { + unsubscribeState() + cleanupLinkHandlers() + } +} + diff --git a/packages/vanilla-router/tests/router.test.ts b/packages/vanilla-router/tests/router.test.ts new file mode 100644 index 00000000000..7c3e3302cbe --- /dev/null +++ b/packages/vanilla-router/tests/router.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { createRouter, createRootRoute, createRoute } from '../src' +import { getMatchesHtml, buildHref, outlet } from '../src' + +describe('Vanilla Router', () => { + it('should create a router', () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
    Root
    ` + }, + }) + + const router = createRouter({ + routeTree: rootRoute, + }) + + expect(router).toBeDefined() + expect(router.state).toBeDefined() + }) + + it('should render root component', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
    Root Component
    ` + }, + }) + + const router = createRouter({ + routeTree: rootRoute, + }) + + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + expect(htmlParts.join('')).toContain('Root Component') + }) + + it('should render nested routes', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
    Root ${outlet()}
    ` + }, + }) + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: (router) => { + return `
    Index
    ` + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), + }) + + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + const html = htmlParts.join('') + expect(html).toContain('Root') + expect(html).toContain('Index') + }) + + it('should build hrefs', () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
    Root
    ` + }, + }) + + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + }) + + const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([ + postsRoute.addChildren([postRoute]), + ]), + }) + + const href = buildHref(router, { + to: '/posts/$postId', + params: { postId: '123' }, + }) + + expect(href).toBe('/posts/123') + }) + + it('should use route getters', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
    Root
    ` + }, + }) + + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => { + return [{ id: '1', title: 'Post 1' }] + }, + component: (router) => { + const posts = postsRoute.getLoaderData(router) + return `
    Posts: ${JSON.stringify(posts)}
    ` + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([postsRoute]), + }) + + await router.navigate({ to: '/posts' }) + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + const html = htmlParts.join('') + expect(html).toContain('Posts:') + expect(html).toContain('Post 1') + }) + + it('should handle route params', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
    Root
    ` + }, + }) + + const postRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts/$postId', + component: (router) => { + const params = postRoute.getParams(router) + return `
    Post ID: ${params.postId}
    ` + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([postRoute]), + }) + + await router.navigate({ to: '/posts/123' }) + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + const html = htmlParts.join('') + expect(html).toContain('Post ID: 123') + }) +}) + diff --git a/packages/vanilla-router/tsconfig.json b/packages/vanilla-router/tsconfig.json new file mode 100644 index 00000000000..65fd562dd68 --- /dev/null +++ b/packages/vanilla-router/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/vanilla-router/vite.config.ts b/packages/vanilla-router/vite.config.ts new file mode 100644 index 00000000000..bc658c2ad2c --- /dev/null +++ b/packages/vanilla-router/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'vitest/config' +import dts from 'vite-plugin-dts' +import packageJson from './package.json' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const config = defineConfig({ + plugins: [ + dts({ + entryRoot: 'src', + tsconfigPath: path.join(__dirname, 'tsconfig.json'), + outDir: 'dist', + }), + ], + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + typecheck: { enabled: true }, + }, + build: { + lib: { + entry: ['./src/index.ts'], + formats: ['es', 'cjs'], + fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'), + }, + rollupOptions: { + // Bundle router-core and history instead of externalizing them + external: [], + }, + }, +}) + +export default config + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76349f4615c..40f5cf2a3e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6844,6 +6844,55 @@ importers: specifier: ^7.1.7 version: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + packages/preact-router: + dependencies: + '@tanstack/history': + specifier: workspace:* + version: link:../history + '@tanstack/react-store': + specifier: ^0.7.0 + version: 0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/store': + specifier: ^0.7.0 + version: 0.7.0 + isbot: + specifier: ^5.1.22 + version: 5.1.28 + preact-render-to-string: + specifier: ^6.3.1 + version: 6.6.3(preact@10.27.2) + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + tiny-warning: + specifier: ^1.0.3 + version: 1.0.3 + devDependencies: + '@preact/preset-vite': + specifier: ^2.9.3 + version: 2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/preact': + specifier: ^3.2.4 + version: 3.2.4(preact@10.27.2) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + preact: + specifier: ^10.24.3 + version: 10.27.2 + vibe-rules: + specifier: ^0.2.57 + version: 0.2.57 + zod: + specifier: ^3.24.2 + version: 3.25.57 + packages/react-router: dependencies: '@tanstack/history': @@ -7671,6 +7720,40 @@ importers: specifier: 1.0.0-beta.15 version: 1.0.0-beta.15(typescript@5.9.2) + packages/vanilla-router: + dependencies: + '@tanstack/history': + specifier: workspace:* + version: link:../history + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + tiny-warning: + specifier: ^1.0.3 + version: 1.0.3 + devDependencies: + combinate: + specifier: ^1.1.11 + version: 1.1.11 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vibe-rules: + specifier: ^0.2.57 + version: 0.2.57 + vite-plugin-dts: + specifier: 4.0.3 + version: 4.0.3(@types/node@22.10.2)(rollup@4.52.2)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.0)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + zod: + specifier: ^3.24.2 + version: 3.25.57 + packages/virtual-file-routes: {} packages/zod-adapter: @@ -7900,6 +7983,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.25.9': resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} engines: {node: '>=6.9.0'} @@ -7924,6 +8013,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.27.1': resolution: {integrity: sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==} engines: {node: '>=6.9.0'} @@ -9744,6 +9839,29 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@preact/preset-vite@2.10.2': + resolution: {integrity: sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==} + peerDependencies: + '@babel/core': 7.x + vite: ^7.1.7 + + '@prefresh/babel-plugin@0.5.2': + resolution: {integrity: sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==} + + '@prefresh/core@1.5.8': + resolution: {integrity: sha512-T7HMpakS1iPVCFZvfDLMGyrWAcO3toUN9/RkJUqqoRr/vNhQrZgHjidfhq3awDzAQtw1emDWH8dsOeu0DWqtgA==} + peerDependencies: + preact: ^10.0.0 || ^11.0.0-0 + + '@prefresh/utils@1.2.1': + resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} + + '@prefresh/vite@2.4.11': + resolution: {integrity: sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==} + peerDependencies: + preact: ^10.4.0 || ^11.0.0-0 + vite: ^7.1.7 + '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -10542,6 +10660,10 @@ packages: rollup: optional: true + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -11431,10 +11553,20 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + '@testing-library/jest-dom@6.6.3': resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/preact@3.2.4': + resolution: {integrity: sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==} + engines: {node: '>= 12'} + peerDependencies: + preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0' + '@testing-library/react@16.2.0': resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} engines: {node: '>=18'} @@ -11924,9 +12056,23 @@ packages: webdriverio: optional: true + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^7.1.7 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.0.6': resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==} peerDependencies: @@ -11949,18 +12095,30 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.0.6': resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==} '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.0.6': resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==} @@ -11972,6 +12130,9 @@ packages: peerDependencies: vitest: 3.0.6 + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.0.6': resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==} @@ -12290,6 +12451,9 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -12300,6 +12464,10 @@ packages: arktype@2.1.7: resolution: {integrity: sha512-RpczU+Ny4g4BqeVu9v2o288A5p8DQ8w8kJuFcD3okCT+oHP8C9YDTkj2kJG2Vz3XQAN2O9Aw06RNoJV0UZ8m6A==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -12337,6 +12505,10 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + axios@1.9.0: resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} @@ -12355,6 +12527,11 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} + babel-plugin-transform-hook-names@1.0.2: + resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} + peerDependencies: + '@babel/core': ^7.12.10 + babel-preset-solid@1.9.3: resolution: {integrity: sha512-jvlx5wDp8s+bEF9sGFw/84SInXOA51ttkUEroQziKMbxplXThVKt83qB6bDTa1HuLNatdU9FHpFOiQWs1tLQIg==} peerDependencies: @@ -12462,10 +12639,22 @@ packages: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + call-bound@1.0.3: resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} @@ -12940,6 +13129,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -12958,6 +13151,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -12966,6 +13163,10 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -13179,6 +13380,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -13590,6 +13794,10 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -13661,6 +13869,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -13673,6 +13884,10 @@ packages: resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -13787,6 +14002,10 @@ packages: handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -13795,10 +14014,17 @@ packages: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -13986,6 +14212,10 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} @@ -14026,20 +14256,44 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -14081,6 +14335,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -14091,6 +14349,10 @@ packages: is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -14120,6 +14382,18 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -14128,6 +14402,14 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} @@ -14136,6 +14418,14 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -14155,6 +14445,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.1.28: resolution: {integrity: sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==} engines: {node: '>=18'} @@ -14276,9 +14569,6 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -14893,6 +15183,9 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} @@ -14958,6 +15251,18 @@ packages: resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -15198,6 +15503,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -15243,6 +15552,14 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.6.3: + resolution: {integrity: sha512-7oHG7jzjriqsFPkSPiPnzrQ0GcxFm6wOkYWNdStK5Ks9YlWSQQXKGBRAX4nKDdqX7HAQuRvI4pZNZMycK4WwDw==} + peerDependencies: + preact: '>=10 || >= 11.0.0-0' + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -15511,6 +15828,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + relateurl@0.2.7: resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} engines: {node: '>= 0.10'} @@ -15634,6 +15955,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -15722,6 +16047,14 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} @@ -15774,6 +16107,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-code-frame@1.3.0: + resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} + simple-git@3.28.0: resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} @@ -15869,6 +16205,10 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stack-trace@1.0.0-pre2: + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} + engines: {node: '>=16'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -15892,6 +16232,10 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -16099,6 +16443,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -16563,6 +16911,11 @@ packages: resolution: {integrity: sha512-CVGXHyKRvDeC3S6SywxTcNGuckmSjwB+2q/v8eDSmwDBTlz0ziRqm49eI5ELLy4djKq6DdCSYvV4EGcwzsHRog==} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -16593,6 +16946,11 @@ packages: '@testing-library/jest-dom': optional: true + vite-prerender-plugin@0.5.12: + resolution: {integrity: sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==} + peerDependencies: + vite: ^7.1.7 + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -16649,6 +17007,31 @@ packages: vite: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': 22.10.2 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -16820,6 +17203,18 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -17138,7 +17533,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -17165,19 +17560,19 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -17210,7 +17605,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 '@babel/helper-plugin-utils@7.27.1': {} @@ -17219,14 +17614,14 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.27.7 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -17313,6 +17708,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 @@ -17343,9 +17745,20 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) '@babel/helper-plugin-utils': 7.27.1 @@ -17372,8 +17785,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.7 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@babel/traverse@7.27.7': dependencies: @@ -18675,8 +19088,8 @@ snapshots: '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -18684,8 +19097,8 @@ snapshots: '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.0': {} @@ -19117,6 +19530,42 @@ snapshots: '@poppinss/exception@1.2.2': {} + '@preact/preset-vite@2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.4) + '@prefresh/vite': 2.4.11(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@rollup/pluginutils': 4.2.1 + babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.4) + debug: 4.4.3 + picocolors: 1.1.1 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-prerender-plugin: 0.5.12(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + transitivePeerDependencies: + - preact + - supports-color + + '@prefresh/babel-plugin@0.5.2': {} + + '@prefresh/core@1.5.8(preact@10.27.2)': + dependencies: + preact: 10.27.2 + + '@prefresh/utils@1.2.1': {} + + '@prefresh/vite@2.4.11(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.4 + '@prefresh/babel-plugin': 0.5.2 + '@prefresh/core': 1.5.8(preact@10.27.2) + '@prefresh/utils': 1.2.1 + '@rollup/pluginutils': 4.2.1 + preact: 10.27.2 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: prisma: 5.22.0 @@ -19941,6 +20390,11 @@ snapshots: optionalDependencies: rollup: 4.52.2 + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + '@rollup/pluginutils@5.1.4(rollup@4.52.2)': dependencies: '@types/estree': 1.0.8 @@ -20867,6 +21321,17 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.26.7 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.1 @@ -20877,6 +21342,11 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 + '@testing-library/preact@3.2.4(preact@10.27.2)': + dependencies: + '@testing-library/dom': 8.20.1 + preact: 10.27.2 + '@testing-library/react@16.2.0(@testing-library/dom@10.4.1)(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.7 @@ -21455,6 +21925,13 @@ snapshots: - utf-8-validate - vite + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -21463,6 +21940,15 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + msw: 2.7.0(@types/node@22.10.2)(typescript@5.9.2) + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/mocker@3.0.6(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.0.6 @@ -21481,6 +21967,10 @@ snapshots: msw: 2.7.0(@types/node@22.10.2)(typescript@5.9.2) vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.0.6': dependencies: tinyrainbow: 2.0.0 @@ -21489,18 +21979,33 @@ snapshots: dependencies: tinyrainbow: 2.0.0 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 strip-literal: 3.0.0 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.0.6': dependencies: tinyspy: 3.0.2 @@ -21520,6 +22025,12 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.0)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@3.0.6': dependencies: '@vitest/pretty-format': 3.0.6 @@ -21905,6 +22416,10 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -21916,6 +22431,11 @@ snapshots: '@ark/schema': 0.44.2 '@ark/util': 0.44.2 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.3 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} array-ify@1.0.0: {} @@ -21960,6 +22480,10 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + axios@1.9.0: dependencies: follow-redirects: 1.15.9(debug@4.4.3) @@ -21984,7 +22508,7 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 html-entities: 2.3.3 parse5: 7.3.0 validate-html-nesting: 1.2.2 @@ -21994,7 +22518,7 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 html-entities: 2.3.3 parse5: 7.3.0 validate-html-nesting: 1.2.2 @@ -22004,7 +22528,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 html-entities: 2.3.3 parse5: 7.3.0 validate-html-nesting: 1.2.2 @@ -22015,6 +22539,10 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.10 + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + babel-preset-solid@1.9.3(@babel/core@7.27.4): dependencies: '@babel/core': 7.27.4 @@ -22166,11 +22694,28 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + call-bound@1.0.3: dependencies: call-bind-apply-helpers: 1.0.1 get-intrinsic: 1.2.7 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsite@1.0.0: optional: true @@ -22190,7 +22735,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 + loupe: 3.2.1 pathval: 2.0.0 chalk@3.0.0: @@ -22605,6 +23150,27 @@ snapshots: deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.7 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -22620,10 +23186,22 @@ snapshots: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@2.0.0: {} define-lazy-prop@3.0.0: {} + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + defu@6.1.4: {} delayed-stream@1.0.0: {} @@ -22730,7 +23308,7 @@ snapshots: dunder-proto@1.0.1: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 @@ -22802,6 +23380,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.7 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.6.0: {} es-module-lexer@1.7.0: {} @@ -23481,6 +24071,10 @@ snapshots: optionalDependencies: debug: 4.4.3 + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 @@ -23523,7 +24117,7 @@ snapshots: fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 fs-extra@7.0.1: @@ -23542,6 +24136,8 @@ snapshots: function-bind@1.1.2: {} + functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -23559,6 +24155,19 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port-please@3.2.0: {} @@ -23680,12 +24289,22 @@ snapshots: handle-thing@2.0.1: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-own-prop@2.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -23885,6 +24504,12 @@ snapshots: inline-style-parser@0.2.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + interpret@3.1.1: {} ioredis@5.8.0: @@ -23929,18 +24554,45 @@ snapshots: iron-webcrypto@1.2.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-arrayish@0.2.1: {} is-arrayish@0.3.4: {} + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -23973,12 +24625,19 @@ snapshots: is-interactive@1.0.0: {} + is-map@2.0.3: {} + is-module@1.0.0: {} is-network-error@1.1.0: {} is-node-process@1.2.0: {} + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-obj@2.0.0: {} @@ -23999,16 +24658,47 @@ snapshots: dependencies: '@types/estree': 1.0.7 + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.3 + is-stream@2.0.1: {} is-stream@3.0.0: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.3 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 is-unicode-supported@0.1.0: {} + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-what@4.1.16: {} is-wsl@2.2.0: @@ -24025,6 +24715,8 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isbot@5.1.28: {} isexe@2.0.0: {} @@ -24161,12 +24853,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -24837,6 +25523,11 @@ snapshots: node-gyp-build@4.8.4: {} + node-html-parser@6.1.13: + dependencies: + css-select: 5.1.0 + he: 1.2.0 + node-machine-id@1.1.12: {} node-mock-http@1.0.3: {} @@ -24931,6 +25622,22 @@ snapshots: object-inspect@1.13.3: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + obuf@1.1.2: {} ofetch@1.4.1: @@ -25166,6 +25873,8 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + postcss-import@15.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -25181,7 +25890,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 - yaml: 2.7.0 + yaml: 2.8.1 optionalDependencies: postcss: 8.5.6 @@ -25209,6 +25918,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@6.6.3(preact@10.27.2): + dependencies: + preact: 10.27.2 + + preact@10.27.2: {} + prelude-ls@1.2.1: {} prettier@3.4.2: {} @@ -25527,6 +26242,15 @@ snapshots: regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + relateurl@0.2.7: {} renderkid@3.0.0: @@ -25658,6 +26382,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -25780,6 +26510,22 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + setprototypeof@1.1.0: {} setprototypeof@1.2.0: {} @@ -25829,16 +26575,16 @@ snapshots: side-channel-map@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.3 side-channel-weakmap@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.3 side-channel-map: 1.0.1 @@ -25856,6 +26602,10 @@ snapshots: signal-exit@4.1.0: {} + simple-code-frame@1.3.0: + dependencies: + kolorist: 1.8.0 + simple-git@3.28.0: dependencies: '@kwsites/file-exists': 1.1.1 @@ -25962,6 +26712,8 @@ snapshots: stable-hash-x@0.2.0: {} + stack-trace@1.0.0-pre2: {} + stackback@0.0.2: {} stackframe@1.3.4: {} @@ -25979,6 +26731,11 @@ snapshots: std-env@3.9.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + stoppable@1.1.0: {} streamx@2.22.0: @@ -26044,7 +26801,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -26214,6 +26971,8 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -26324,8 +27083,8 @@ snapshots: tsx@4.20.3: dependencies: - esbuild: 0.25.4 - get-tsconfig: 4.10.0 + esbuild: 0.25.10 + get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -26618,6 +27377,27 @@ snapshots: import-meta-resolve: 4.1.0 zod: 3.25.57 + vite-node@2.1.9(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -26698,6 +27478,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite-prerender-plugin@0.5.12(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): + dependencies: + kolorist: 1.8.0 + magic-string: 0.30.19 + node-html-parser: 6.1.13 + simple-code-frame: 1.3.0 + source-map: 0.7.6 + stack-trace: 1.0.0-pre2 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: debug: 4.4.0 @@ -26752,6 +27542,47 @@ snapshots: optionalDependencies: vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vitest@2.1.9(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.0)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-node: 2.1.9(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.10.2 + '@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.52.0)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/ui': 3.0.6(vitest@3.2.4) + jsdom: 27.0.0(postcss@8.5.6) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.0)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 @@ -27076,6 +27907,31 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 From 60871de695a76a05ef907db409da74409c6d45ea Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 6 Nov 2025 21:12:02 -0700 Subject: [PATCH 05/10] chore: remove unrelated changes from middleware refactor and tests - Revert changes to e2e test files (server-functions) - Revert changes to packages/start-client-core and packages/start-server-core - Revert changes to examples/react/basic - Keep only preact-router, vanilla-router, and their examples --- .../server-functions/count-effect.txt | 1 - .../server-functions/src/routeTree.gen.ts | 22 -- .../server-functions/src/routes/__root.tsx | 4 - .../server-functions/src/routes/index.tsx | 5 - .../routes/middleware/unhandled-exception.tsx | 33 --- .../server-functions/src/routes/status.tsx | 43 +--- .../src/routes/submit-post-formdata.tsx | 2 +- .../tests/server-functions.spec.ts | 2 +- .../server-functions/count-effect.txt | 1 - examples/react/basic/src/main.tsx | 2 +- .../src/client-rpc/serverFnFetcher.ts | 47 ++-- packages/start-client-core/src/constants.ts | 1 - .../start-client-core/src/createServerFn.ts | 224 ++++++----------- packages/start-client-core/src/index.tsx | 2 +- .../src/createStartHandler.ts | 42 +--- .../start-server-core/src/request-response.ts | 59 ----- .../src/server-functions-handler.ts | 236 ++++++++---------- 17 files changed, 225 insertions(+), 501 deletions(-) delete mode 100644 e2e/react-start/server-functions/count-effect.txt delete mode 100644 e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx delete mode 100644 e2e/solid-start/server-functions/count-effect.txt diff --git a/e2e/react-start/server-functions/count-effect.txt b/e2e/react-start/server-functions/count-effect.txt deleted file mode 100644 index d8263ee9860..00000000000 --- a/e2e/react-start/server-functions/count-effect.txt +++ /dev/null @@ -1 +0,0 @@ -2 \ No newline at end of file diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index 73ad1c262f2..b5692de0599 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -27,7 +27,6 @@ import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index' import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index' import { Route as FactoryIndexRouteImport } from './routes/factory/index' import { Route as CookiesIndexRouteImport } from './routes/cookies/index' -import { Route as MiddlewareUnhandledExceptionRouteImport } from './routes/middleware/unhandled-exception' import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn' import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware' import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' @@ -124,12 +123,6 @@ const CookiesIndexRoute = CookiesIndexRouteImport.update({ path: '/cookies/', getParentRoute: () => rootRouteImport, } as any) -const MiddlewareUnhandledExceptionRoute = - MiddlewareUnhandledExceptionRouteImport.update({ - id: '/middleware/unhandled-exception', - path: '/middleware/unhandled-exception', - getParentRoute: () => rootRouteImport, - } as any) const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({ id: '/middleware/send-serverFn', path: '/middleware/send-serverFn', @@ -177,7 +170,6 @@ export interface FileRoutesByFullPath { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute - '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies': typeof CookiesIndexRoute '/factory': typeof FactoryIndexRoute '/formdata-redirect': typeof FormdataRedirectIndexRoute @@ -203,7 +195,6 @@ export interface FileRoutesByTo { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute - '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies': typeof CookiesIndexRoute '/factory': typeof FactoryIndexRoute '/formdata-redirect': typeof FormdataRedirectIndexRoute @@ -230,7 +221,6 @@ export interface FileRoutesById { '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute - '/middleware/unhandled-exception': typeof MiddlewareUnhandledExceptionRoute '/cookies/': typeof CookiesIndexRoute '/factory/': typeof FactoryIndexRoute '/formdata-redirect/': typeof FormdataRedirectIndexRoute @@ -258,7 +248,6 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' - | '/middleware/unhandled-exception' | '/cookies' | '/factory' | '/formdata-redirect' @@ -284,7 +273,6 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' - | '/middleware/unhandled-exception' | '/cookies' | '/factory' | '/formdata-redirect' @@ -310,7 +298,6 @@ export interface FileRouteTypes { | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' - | '/middleware/unhandled-exception' | '/cookies/' | '/factory/' | '/formdata-redirect/' @@ -337,7 +324,6 @@ export interface RootRouteChildren { MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute - MiddlewareUnhandledExceptionRoute: typeof MiddlewareUnhandledExceptionRoute CookiesIndexRoute: typeof CookiesIndexRoute FactoryIndexRoute: typeof FactoryIndexRoute FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute @@ -474,13 +460,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CookiesIndexRouteImport parentRoute: typeof rootRouteImport } - '/middleware/unhandled-exception': { - id: '/middleware/unhandled-exception' - path: '/middleware/unhandled-exception' - fullPath: '/middleware/unhandled-exception' - preLoaderRoute: typeof MiddlewareUnhandledExceptionRouteImport - parentRoute: typeof rootRouteImport - } '/middleware/send-serverFn': { id: '/middleware/send-serverFn' path: '/middleware/send-serverFn' @@ -537,7 +516,6 @@ const rootRouteChildren: RootRouteChildren = { MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute, MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute, - MiddlewareUnhandledExceptionRoute: MiddlewareUnhandledExceptionRoute, CookiesIndexRoute: CookiesIndexRoute, FactoryIndexRoute: FactoryIndexRoute, FormdataRedirectIndexRoute: FormdataRedirectIndexRoute, diff --git a/e2e/react-start/server-functions/src/routes/__root.tsx b/e2e/react-start/server-functions/src/routes/__root.tsx index af7afa5ca87..5dec9273376 100644 --- a/e2e/react-start/server-functions/src/routes/__root.tsx +++ b/e2e/react-start/server-functions/src/routes/__root.tsx @@ -8,7 +8,6 @@ import { createRootRouteWithContext, } from '@tanstack/react-router' -import { z } from 'zod' import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' @@ -35,9 +34,6 @@ export const Route = createRootRouteWithContext<{ foo: { bar: string } }>()({ }, notFoundComponent: () => , component: RootComponent, - validateSearch: z.object({ - status: z.string().optional(), - }), }) function RootComponent() { diff --git a/e2e/react-start/server-functions/src/routes/index.tsx b/e2e/react-start/server-functions/src/routes/index.tsx index ca1394015af..596b855c099 100644 --- a/e2e/react-start/server-functions/src/routes/index.tsx +++ b/e2e/react-start/server-functions/src/routes/index.tsx @@ -88,11 +88,6 @@ function Home() {
  • Server Functions Factory E2E tests
  • -
  • - - Server Functions Middleware Unhandled Exception E2E tests - -
  • ) diff --git a/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx b/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx deleted file mode 100644 index f6d3bb60179..00000000000 --- a/e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { createMiddleware, createServerFn } from '@tanstack/react-start' - -export const authMiddleware = createMiddleware({ type: 'function' }).server( - async ({ next, context }) => { - throw new Error('Unauthorized') - }, -) - -const personServerFn = createServerFn({ method: 'GET' }) - .middleware([authMiddleware]) - .inputValidator((d: string) => d) - .handler(({ data: name }) => { - return { name, randomNumber: Math.floor(Math.random() * 100) } - }) - -export const Route = createFileRoute('/middleware/unhandled-exception')({ - loader: async () => { - return { - person: await personServerFn({ data: 'John Doe' }), - } - }, - component: RouteComponent, -}) - -function RouteComponent() { - const { person } = Route.useLoaderData() - return ( -
    - {person.name} - {person.randomNumber} -
    - ) -} diff --git a/e2e/react-start/server-functions/src/routes/status.tsx b/e2e/react-start/server-functions/src/routes/status.tsx index dbe42941f3d..b8dcdee2e65 100644 --- a/e2e/react-start/server-functions/src/routes/status.tsx +++ b/e2e/react-start/server-functions/src/routes/status.tsx @@ -2,18 +2,11 @@ import { createFileRoute } from '@tanstack/react-router' import { createServerFn, useServerFn } from '@tanstack/react-start' import { setResponseStatus } from '@tanstack/react-start/server' -const simpleSetFn = createServerFn().handler(() => { +const helloFn = createServerFn().handler(() => { setResponseStatus(225, `hello`) - return null -}) - -const requestSetFn = createServerFn().handler(() => { - return new Response(undefined, { status: 226, statusText: `status-226` }) -}) - -const bothSetFn = createServerFn().handler(() => { - setResponseStatus(225, `status-225`) - return new Response(undefined, { status: 226, statusText: `status-226` }) + return { + hello: 'world', + } }) export const Route = createFileRoute('/status')({ @@ -21,36 +14,14 @@ export const Route = createFileRoute('/status')({ }) function StatusComponent() { - const simpleSet = useServerFn(simpleSetFn) - const requestSet = useServerFn(requestSetFn) - const bothSet = useServerFn(bothSetFn) + const hello = useServerFn(helloFn) return (
    - - diff --git a/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx b/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx index 5f9165adf74..826ec255d38 100644 --- a/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx +++ b/e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx @@ -33,7 +33,7 @@ function SubmitPostFormDataFn() {

    Submit POST FormData Fn Call

    - It should navigate to a raw response of {''} + It should return navigate and return{' '}
                 Hello, {testValues.name}!
    diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts
    index 0fa968fc41b..2edc524065d 100644
    --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts
    +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts
    @@ -35,7 +35,7 @@ test('invoking a server function with custom response status code', async ({
           resolve()
         })
       })
    -  await page.getByTestId('invoke-server-fn-simple-set').click()
    +  await page.getByTestId('invoke-server-fn').click()
       await requestPromise
     })
     
    diff --git a/e2e/solid-start/server-functions/count-effect.txt b/e2e/solid-start/server-functions/count-effect.txt
    deleted file mode 100644
    index 56a6051ca2b..00000000000
    --- a/e2e/solid-start/server-functions/count-effect.txt
    +++ /dev/null
    @@ -1 +0,0 @@
    -1
    \ No newline at end of file
    diff --git a/examples/react/basic/src/main.tsx b/examples/react/basic/src/main.tsx
    index 41abd842c56..ca6c41b7096 100644
    --- a/examples/react/basic/src/main.tsx
    +++ b/examples/react/basic/src/main.tsx
    @@ -8,7 +8,7 @@ import {
       createRoute,
       createRouter,
     } from '@tanstack/react-router'
    -
    +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
     import { NotFoundError, fetchPost, fetchPosts } from './posts'
     import type { ErrorComponentProps } from '@tanstack/react-router'
     import './styles.css'
    diff --git a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    index b1b781d7d00..5511bdc5173 100644
    --- a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    +++ b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts
    @@ -17,16 +17,6 @@ import type { Plugin as SerovalPlugin } from 'seroval'
     
     let serovalPlugins: Array> | null = null
     
    -// caller =>
    -//   serverFnFetcher =>
    -//     client =>
    -//       server =>
    -//         fn =>
    -//       seroval =>
    -//     client middleware =>
    -//   serverFnFetcher =>
    -// caller
    -
     export async function serverFnFetcher(
       url: string,
       args: Array,
    @@ -47,8 +37,7 @@ export async function serverFnFetcher(
     
         // Arrange the headers
         const headers = new Headers({
    -      'x-tsr-serverFn': 'true',
    -      'x-tsr-createServerFn': 'true',
    +      'x-tsr-redirect': 'manual',
           ...(first.headers instanceof Headers
             ? Object.fromEntries(first.headers.entries())
             : first.headers),
    @@ -76,6 +65,12 @@ export async function serverFnFetcher(
           }
         }
     
    +    if (url.includes('?')) {
    +      url += `&createServerFn`
    +    } else {
    +      url += `?createServerFn`
    +    }
    +
         let body = undefined
         if (first.method === 'POST') {
           const fetchBody = await getFetchBody(first)
    @@ -102,7 +97,6 @@ export async function serverFnFetcher(
         handler(url, {
           method: 'POST',
           headers: {
    -        'x-tsr-serverFn': 'true',
             Accept: 'application/json',
             'Content-Type': 'application/json',
           },
    @@ -171,7 +165,7 @@ async function getFetchBody(
     async function getResponse(fn: () => Promise) {
       const response = await (async () => {
         try {
    -      return await fn() // client => server => fn => server => client
    +      return await fn()
         } catch (error) {
           if (error instanceof Response) {
             return error
    @@ -184,16 +178,22 @@ async function getResponse(fn: () => Promise) {
       if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') {
         return response
       }
    -
       const contentType = response.headers.get('content-type')
       invariant(contentType, 'expected content-type header to be set')
       const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)
    +  // If the response is not ok, throw an error
    +  if (!response.ok) {
    +    if (serializedByStart && contentType.includes('application/json')) {
    +      const jsonPayload = await response.json()
    +      const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
    +      throw result
    +    }
    +
    +    throw new Error(await response.text())
    +  }
     
    -  // If the response is serialized by the start server, we need to process it
    -  // differently than a normal response.
       if (serializedByStart) {
         let result
    -    // If it's a stream from the start serializer, process it as such
         if (contentType.includes('application/x-ndjson')) {
           const refs = new Map()
           result = await processServerFnResponse({
    @@ -206,22 +206,17 @@ async function getResponse(fn: () => Promise) {
             },
           })
         }
    -    // If it's a JSON response, it can be simpler
         if (contentType.includes('application/json')) {
           const jsonPayload = await response.json()
           result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
         }
    -
         invariant(result, 'expected result to be resolved')
         if (result instanceof Error) {
           throw result
         }
    -
         return result
       }
     
    -  // If it wasn't processed by the start serializer, check
    -  // if it's JSON
       if (contentType.includes('application/json')) {
         const jsonPayload = await response.json()
         const redirect = parseRedirect(jsonPayload)
    @@ -234,12 +229,6 @@ async function getResponse(fn: () => Promise) {
         return jsonPayload
       }
     
    -  // Othwerwise, if it's not OK, throw the content
    -  if (!response.ok) {
    -    throw new Error(await response.text())
    -  }
    -
    -  // Or return the response itself
       return response
     }
     
    diff --git a/packages/start-client-core/src/constants.ts b/packages/start-client-core/src/constants.ts
    index 4e0777068d2..1e541af3231 100644
    --- a/packages/start-client-core/src/constants.ts
    +++ b/packages/start-client-core/src/constants.ts
    @@ -6,5 +6,4 @@ export const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(
     
     export const X_TSS_SERIALIZED = 'x-tss-serialized'
     export const X_TSS_RAW_RESPONSE = 'x-tss-raw'
    -export const X_TSS_CONTEXT = 'x-tss-context'
     export {}
    diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts
    index 6b7bf72711e..bc95a6b5b9a 100644
    --- a/packages/start-client-core/src/createServerFn.ts
    +++ b/packages/start-client-core/src/createServerFn.ts
    @@ -1,9 +1,10 @@
    +import { isNotFound, isRedirect } from '@tanstack/router-core'
     import { mergeHeaders } from '@tanstack/router-core/ssr/client'
     
    -import { isRedirect, parseRedirect } from '@tanstack/router-core'
     import { TSS_SERVER_FUNCTION_FACTORY } from './constants'
     import { getStartOptions } from './getStartOptions'
     import { getStartContextServerOnly } from './getStartContextServerOnly'
    +import type { TSS_SERVER_FUNCTION } from './constants'
     import type {
       AnyValidator,
       Constrain,
    @@ -15,7 +16,6 @@ import type {
       ValidateSerializableInput,
       Validator,
     } from '@tanstack/router-core'
    -import type { TSS_SERVER_FUNCTION } from './constants'
     import type {
       AnyFunctionMiddleware,
       AnyRequestMiddleware,
    @@ -112,22 +112,17 @@ export const createServerFn: CreateServerFn = (options, __opts) => {
           return Object.assign(
             async (opts?: CompiledFetcherFnOptions) => {
               // Start by executing the client-side middleware chain
    -          const result = await executeMiddleware(resolvedMiddleware, 'client', {
    +          return executeMiddleware(resolvedMiddleware, 'client', {
                 ...extractedFn,
                 ...newOptions,
                 data: opts?.data as any,
                 headers: opts?.headers,
                 signal: opts?.signal,
                 context: {},
    +          }).then((d) => {
    +            if (d.error) throw d.error
    +            return d.result
               })
    -
    -          const redirect = parseRedirect(result.error)
    -          if (redirect) {
    -            throw redirect
    -          }
    -
    -          if (result.error) throw result.error
    -          return result.result
             },
             {
               // This copies over the URL, function ID
    @@ -149,18 +144,14 @@ export const createServerFn: CreateServerFn = (options, __opts) => {
                   request: startContext.request,
                 }
     
    -            const result = await executeMiddleware(
    -              resolvedMiddleware,
    -              'server',
    -              ctx,
    -            ).then((d) => ({
    -              // Only send the result and sendContext back to the client
    -              result: d.result,
    -              error: d.error,
    -              context: d.sendContext,
    -            }))
    -
    -            return result
    +            return executeMiddleware(resolvedMiddleware, 'server', ctx).then(
    +              (d) => ({
    +                // Only send the result and sendContext back to the client
    +                result: d.result,
    +                error: d.error,
    +                context: d.sendContext,
    +              }),
    +            )
               },
             },
           ) as any
    @@ -189,7 +180,7 @@ export async function executeMiddleware(
         ...middlewares,
       ])
     
    -  const callNextMiddleware: NextFn = async (ctx) => {
    +  const next: NextFn = async (ctx) => {
         // Get the next middleware
         const nextMiddleware = flattenedMiddlewares.shift()
     
    @@ -198,136 +189,50 @@ export async function executeMiddleware(
           return ctx
         }
     
    -    // Execute the middleware
    -    try {
    -      if (
    -        'inputValidator' in nextMiddleware.options &&
    -        nextMiddleware.options.inputValidator &&
    -        env === 'server'
    -      ) {
    -        // Execute the middleware's input function
    -        ctx.data = await execValidator(
    -          nextMiddleware.options.inputValidator,
    -          ctx.data,
    -        )
    -      }
    +    if (
    +      'inputValidator' in nextMiddleware.options &&
    +      nextMiddleware.options.inputValidator &&
    +      env === 'server'
    +    ) {
    +      // Execute the middleware's input function
    +      ctx.data = await execValidator(
    +        nextMiddleware.options.inputValidator,
    +        ctx.data,
    +      )
    +    }
     
    -      let middlewareFn: MiddlewareFn | undefined = undefined
    -      if (env === 'client') {
    -        if ('client' in nextMiddleware.options) {
    -          middlewareFn = nextMiddleware.options.client as
    -            | MiddlewareFn
    -            | undefined
    -        }
    +    let middlewareFn: MiddlewareFn | undefined = undefined
    +    if (env === 'client') {
    +      if ('client' in nextMiddleware.options) {
    +        middlewareFn = nextMiddleware.options.client as MiddlewareFn | undefined
           }
    -      // env === 'server'
    -      else if ('server' in nextMiddleware.options) {
    -        middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
    -      }
    -
    -      if (middlewareFn) {
    -        const userNext = async (
    -          userCtx: ServerFnMiddlewareResult | undefined = {} as any,
    -        ) => {
    -          // Return the next middleware
    -          const nextCtx = {
    -            ...ctx,
    -            ...userCtx,
    -            context: {
    -              ...ctx.context,
    -              ...userCtx.context,
    -            },
    -            sendContext: {
    -              ...ctx.sendContext,
    -              ...(userCtx.sendContext ?? {}),
    -            },
    -            headers: mergeHeaders(ctx.headers, userCtx.headers),
    -            result:
    -              userCtx.result !== undefined
    -                ? userCtx.result
    -                : userCtx instanceof Response
    -                  ? userCtx
    -                  : (ctx as any).result,
    -            error: userCtx.error ?? (ctx as any).error,
    -          }
    +    }
    +    // env === 'server'
    +    else if ('server' in nextMiddleware.options) {
    +      middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
    +    }
     
    -          try {
    -            return await callNextMiddleware(nextCtx)
    -          } catch (error: any) {
    +    if (middlewareFn) {
    +      // Execute the middleware
    +      return applyMiddleware(middlewareFn, ctx, async (newCtx) => {
    +        return next(newCtx).catch((error: any) => {
    +          if (isRedirect(error) || isNotFound(error)) {
                 return {
    -              ...nextCtx,
    +              ...newCtx,
                   error,
                 }
               }
    -        }
    -
    -        // SNAPSHOT BEFORE - only on server
    -        let h3Before: any = undefined
    -        if (env === 'server') {
    -          const { snapshotH3State } = await import(
    -            '@tanstack/start-server-core'
    -          )
    -          h3Before = snapshotH3State()
    -        }
    -
    -        // Execute the middleware
    -        const result = await middlewareFn({
    -          ...ctx,
    -          next: userNext as any,
    -        } as any)
    -
    -        // RECONCILE AFTER - if returned Response and on server
    -        if (result instanceof Response && env === 'server' && h3Before) {
    -          const { snapshotH3State, reconcileResponseWithH3Changes } =
    -            await import('@tanstack/start-server-core')
    -          const h3After = snapshotH3State()
    -          const reconciledResponse = reconcileResponseWithH3Changes(
    -            result,
    -            h3Before,
    -            h3After,
    -          )
    -          return {
    -            ...ctx,
    -            result: reconciledResponse,
    -          }
    -        }
    -
    -        // If result is NOT a ctx object, we need to return it as
    -        // the { result }
    -        if (isRedirect(result)) {
    -          return {
    -            ...ctx,
    -            error: result,
    -          }
    -        }
    -
    -        if (result instanceof Response) {
    -          return {
    -            ...ctx,
    -            result,
    -          }
    -        }
    -
    -        if (!(result as any)) {
    -          throw new Error(
    -            'User middleware returned undefined. You must call next() or return a result in your middlewares.',
    -          )
    -        }
    -
    -        return result
    -      }
     
    -      return callNextMiddleware(ctx)
    -    } catch (error: any) {
    -      return {
    -        ...ctx,
    -        error,
    -      }
    +          throw error
    +        })
    +      })
         }
    +
    +    return next(ctx)
       }
     
       // Start the middleware chain
    -  return callNextMiddleware({
    +  return next({
         ...opts,
         headers: opts.headers || {},
         sendContext: opts.sendContext || {},
    @@ -723,6 +628,41 @@ export type MiddlewareFn = (
       },
     ) => Promise
     
    +export const applyMiddleware = async (
    +  middlewareFn: MiddlewareFn,
    +  ctx: ServerFnMiddlewareOptions,
    +  nextFn: NextFn,
    +) => {
    +  return middlewareFn({
    +    ...ctx,
    +    next: (async (
    +      userCtx: ServerFnMiddlewareResult | undefined = {} as any,
    +    ) => {
    +      // Return the next middleware
    +      return nextFn({
    +        ...ctx,
    +        ...userCtx,
    +        context: {
    +          ...ctx.context,
    +          ...userCtx.context,
    +        },
    +        sendContext: {
    +          ...ctx.sendContext,
    +          ...(userCtx.sendContext ?? {}),
    +        },
    +        headers: mergeHeaders(ctx.headers, userCtx.headers),
    +        result:
    +          userCtx.result !== undefined
    +            ? userCtx.result
    +            : userCtx instanceof Response
    +              ? userCtx
    +              : (ctx as any).result,
    +        error: userCtx.error ?? (ctx as any).error,
    +      })
    +    }) as any,
    +  } as any)
    +}
    +
     export function execValidator(
       validator: AnyValidator,
       input: unknown,
    diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx
    index 10a84f299bd..70a4692842a 100644
    --- a/packages/start-client-core/src/index.tsx
    +++ b/packages/start-client-core/src/index.tsx
    @@ -74,6 +74,7 @@ export type {
       RequiredFetcher,
     } from './createServerFn'
     export {
    +  applyMiddleware,
       execValidator,
       flattenMiddlewares,
       executeMiddleware,
    @@ -84,7 +85,6 @@ export {
       TSS_SERVER_FUNCTION,
       X_TSS_SERIALIZED,
       X_TSS_RAW_RESPONSE,
    -  X_TSS_CONTEXT,
     } from './constants'
     
     export type * from './serverRoute'
    diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts
    index bad53560413..d0fd84e8f43 100644
    --- a/packages/start-server-core/src/createStartHandler.ts
    +++ b/packages/start-server-core/src/createStartHandler.ts
    @@ -14,11 +14,7 @@ import {
       getOrigin,
     } from '@tanstack/router-core/ssr/server'
     import { runWithStartContext } from '@tanstack/start-storage-context'
    -import {
    -  requestHandler,
    -  snapshotH3State,
    -  reconcileResponseWithH3Changes,
    -} from './request-response'
    +import { requestHandler } from './request-response'
     import { getStartManifest } from './router-manifest'
     import { handleServerAction } from './server-functions-handler'
     
    @@ -32,7 +28,6 @@ import type {
       StartEntry,
     } from '@tanstack/start-client-core'
     import type { RequestHandler } from './request-handler'
    -import type { H3StateSnapshot } from './request-response'
     import type {
       AnyRoute,
       AnyRouter,
    @@ -273,6 +268,7 @@ export function createStartHandler(
           [...middlewares, requestHandlerMiddleware],
           {
             request,
    +
             context: requestOpts?.context || {},
           },
         )
    @@ -281,7 +277,7 @@ export function createStartHandler(
     
         if (isRedirect(response)) {
           if (isResolvedRedirect(response)) {
    -        if (request.headers.get('x-tsr-createServerFn') === 'true') {
    +        if (request.headers.get('x-tsr-redirect') === 'manual') {
               return json(
                 {
                   ...response.options,
    @@ -322,7 +318,7 @@ export function createStartHandler(
           const router = await getRouter()
           const redirect = router.resolveRedirect(response)
     
    -      if (request.headers.get('x-tsr-createServerFn') === 'true') {
    +      if (request.headers.get('x-tsr-redirect') === 'manual') {
             return json(
               {
                 ...response.options,
    @@ -479,9 +475,6 @@ function executeMiddleware(middlewares: TODO, ctx: TODO) {
         const middleware = middlewares[index]
         if (!middleware) return ctx
     
    -    // Snapshot before middleware
    -    const h3Before = snapshotH3State()
    -
         let result
         try {
           result = await middleware({
    @@ -505,41 +498,24 @@ function executeMiddleware(middlewares: TODO, ctx: TODO) {
           })
         } catch (err: TODO) {
           if (isSpecialResponse(err)) {
    -        // Reconcile thrown Response
    -        const h3After = snapshotH3State()
    -        const reconciled =
    -          err instanceof Response
    -            ? reconcileResponseWithH3Changes(err, h3Before, h3After)
    -            : err
             result = {
    -          response: reconciled,
    +          response: err,
             }
           } else {
             throw err
           }
         }
     
    -    // Reconcile result if it's a Response
    -    const reconciledResult = handleCtxResult(result, h3Before)
    -    return Object.assign(ctx, reconciledResult)
    +    // Merge the middleware result into the context, just in case it
    +    // returns a partial context
    +    return Object.assign(ctx, handleCtxResult(result))
       }
     
       return handleCtxResult(next(ctx))
     }
     
    -function handleCtxResult(result: TODO, h3Before?: H3StateSnapshot) {
    +function handleCtxResult(result: TODO) {
       if (isSpecialResponse(result)) {
    -    if (result instanceof Response && h3Before) {
    -      const h3After = snapshotH3State()
    -      const reconciled = reconcileResponseWithH3Changes(
    -        result,
    -        h3Before,
    -        h3After,
    -      )
    -      return {
    -        response: reconciled,
    -      }
    -    }
         return {
           response: result,
         }
    diff --git a/packages/start-server-core/src/request-response.ts b/packages/start-server-core/src/request-response.ts
    index 1b2eb6f770c..cf0e426db9c 100644
    --- a/packages/start-server-core/src/request-response.ts
    +++ b/packages/start-server-core/src/request-response.ts
    @@ -203,65 +203,6 @@ export function setResponseStatus(code?: number, text?: string): void {
       }
     }
     
    -export function getResponseStatusText(): string | undefined {
    -  return getH3Event().res.statusText
    -}
    -
    -export interface H3StateSnapshot {
    -  status: number
    -  statusText: string | undefined
    -  headers: Headers
    -}
    -
    -/**
    - * Capture current h3 event state for reconciliation
    - */
    -export function snapshotH3State(): H3StateSnapshot {
    -  const event = getH3Event()
    -  return {
    -    status: event.res.status || 200,
    -    statusText: event.res.statusText,
    -    headers: new Headers(event.res.headers),
    -  }
    -}
    -
    -/**
    - * Reconcile a Response with h3 changes detected via snapshot diff.
    - * This enables "last-call wins" semantics where imperative API calls
    - * made after a Response is returned will modify the final Response.
    - */
    -export function reconcileResponseWithH3Changes(
    -  response: Response,
    -  before: H3StateSnapshot,
    -  after: H3StateSnapshot,
    -): Response {
    -  // Start with Response headers
    -  const finalHeaders = new Headers(response.headers)
    -
    -  // Apply h3 header additions/modifications
    -  after.headers.forEach((value, key) => {
    -    const beforeValue = before.headers.get(key)
    -    const afterValue = after.headers.get(key)
    -    if (beforeValue !== afterValue) {
    -      finalHeaders.set(key, value)
    -    }
    -  })
    -
    -  // Apply h3 header deletions
    -  before.headers.forEach((_, key) => {
    -    if (!after.headers.has(key)) {
    -      finalHeaders.delete(key)
    -    }
    -  })
    -
    -  // Clone Response with reconciled values
    -  return new Response(response.body, {
    -    status: after.status,
    -    statusText: after.statusText,
    -    headers: finalHeaders,
    -  })
    -}
    -
     /**
      * Parse the request to get HTTP Cookie header string and return an object of all cookie name-value pairs.
      * @returns Object of cookie name-value pairs
    diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts
    index bfc67672790..ec9042ec473 100644
    --- a/packages/start-server-core/src/server-functions-handler.ts
    +++ b/packages/start-server-core/src/server-functions-handler.ts
    @@ -1,18 +1,13 @@
    -import { isNotFound, isPlainObject } from '@tanstack/router-core'
    +import { isNotFound } from '@tanstack/router-core'
     import invariant from 'tiny-invariant'
     import {
       TSS_FORMDATA_CONTEXT,
       X_TSS_RAW_RESPONSE,
       X_TSS_SERIALIZED,
       getDefaultSerovalPlugins,
    -  json,
     } from '@tanstack/start-client-core'
     import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
    -import {
    -  getResponse,
    -  getResponseStatus,
    -  getResponseStatusText,
    -} from './request-response'
    +import { getResponse } from './request-response'
     import { getServerFnById } from './getServerFnById'
     
     let regex: RegExp | undefined = undefined
    @@ -46,9 +41,7 @@ export const handleServerAction = async ({
         createServerFn?: boolean
       }
     
    -  const isServerFn = request.headers.get('x-tsr-serverFn') === 'true'
    -  const isCreateServerFn =
    -    request.headers.get('x-tsr-createServerFn') === 'true'
    +  const isCreateServerFn = 'createServerFn' in search
     
       if (typeof serverFnId !== 'string') {
         throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
    @@ -63,9 +56,6 @@ export const handleServerAction = async ({
       ]
     
       const contentType = request.headers.get('Content-Type')
    -  const isFormData = formDataContentTypes.some(
    -    (type) => contentType && contentType.includes(type),
    -  )
       const serovalPlugins = getDefaultSerovalPlugins()
     
       function parsePayload(payload: any) {
    @@ -75,9 +65,13 @@ export const handleServerAction = async ({
     
       const response = await (async () => {
         try {
    -      let res = await (async () => {
    +      let result = await (async () => {
             // FormData
    -        if (isFormData) {
    +        if (
    +          formDataContentTypes.some(
    +            (type) => contentType && contentType.includes(type),
    +          )
    +        ) {
               // We don't support GET requests with FormData payloads... that seems impossible
               invariant(
                 method.toLowerCase() !== 'get',
    @@ -141,45 +135,23 @@ export const handleServerAction = async ({
             return await action(...jsonPayload)
           })()
     
    -      const isCtxResult =
    -        isPlainObject(res) &&
    -        'context' in res &&
    -        ('result' in res || 'error' in res)
    -
    -      function unwrapResultOrError(result: any) {
    -        if (
    -          isPlainObject(result) &&
    -          ('result' in result || 'error' in result)
    -        ) {
    -          return result.result || result.error
    -        }
    -        return result
    +      // Any time we get a Response back, we should just
    +      // return it immediately.
    +      if (result.result instanceof Response) {
    +        result.result.headers.set(X_TSS_RAW_RESPONSE, 'true')
    +        return result.result
           }
     
    -      // This was not called by the serverFnFetcher, so it's likely a no-JS POST request)
    -      if (isCtxResult) {
    -        const unwrapped = unwrapResultOrError(res)
    -        if (unwrapped instanceof Response) {
    -          res = unwrapped
    -        } else {
    -          // Create Response with h3 state
    -          res = new Response(JSON.stringify(unwrapped), {
    -            status: getResponseStatus(),
    -            statusText: getResponseStatusText(),
    -            headers: {
    -              'Content-Type': 'application/json',
    -            },
    -          })
    -        }
    -      }
    -
    -      if (isNotFound(res)) {
    -        res = isNotFoundResponse(res)
    -      }
    +      // If this is a non createServerFn request, we need to
    +      // pull out the result from the result object
    +      if (!isCreateServerFn) {
    +        result = result.result
     
    -      if (res instanceof Response) {
    -        res.headers.set(X_TSS_RAW_RESPONSE, 'true')
    -        return res
    +        // The result might again be a response,
    +        // and if it is, return it.
    +        if (result instanceof Response) {
    +          return result
    +        }
           }
     
           // TODO: RSCs Where are we getting this package?
    @@ -201,90 +173,91 @@ export const handleServerAction = async ({
           //   return new Response(null, { status: 200 })
           // }
     
    -      return serializeResult(res)
    -
    -      function serializeResult(res: unknown): Response {
    -        let nonStreamingBody: any = undefined
    -
    -        if (res !== undefined) {
    -          // first run without the stream in case `result` does not need streaming
    -          let done = false as boolean
    -          const callbacks: {
    -            onParse: (value: any) => void
    -            onDone: () => void
    -            onError: (error: any) => void
    -          } = {
    -            onParse: (value) => {
    -              nonStreamingBody = value
    -            },
    -            onDone: () => {
    -              done = true
    -            },
    -            onError: (error) => {
    -              throw error
    -            },
    -          }
    -          toCrossJSONStream(res, {
    -            refs: new Map(),
    -            plugins: serovalPlugins,
    -            onParse(value) {
    -              callbacks.onParse(value)
    -            },
    -            onDone() {
    -              callbacks.onDone()
    -            },
    -            onError: (error) => {
    -              callbacks.onError(error)
    -            },
    -          })
    -          if (done) {
    -            return new Response(
    -              nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
    -              {
    -                status: getResponseStatus(),
    -                statusText: getResponseStatusText(),
    -                headers: {
    -                  'Content-Type': 'application/json',
    -                  [X_TSS_SERIALIZED]: 'true',
    -                },
    -              },
    -            )
    -          }
    +      if (isNotFound(result)) {
    +        return isNotFoundResponse(result)
    +      }
     
    -          // not done yet, we need to stream
    -          const stream = new ReadableStream({
    -            start(controller) {
    -              callbacks.onParse = (value) =>
    -                controller.enqueue(JSON.stringify(value) + '\n')
    -              callbacks.onDone = () => {
    -                try {
    -                  controller.close()
    -                } catch (error) {
    -                  controller.error(error)
    -                }
    -              }
    -              callbacks.onError = (error) => controller.error(error)
    -              // stream the initial body
    -              if (nonStreamingBody !== undefined) {
    -                callbacks.onParse(nonStreamingBody)
    -              }
    -            },
    -          })
    -          return new Response(stream, {
    -            status: getResponseStatus(),
    -            statusText: getResponseStatusText(),
    -            headers: {
    -              'Content-Type': 'application/x-ndjson',
    -              [X_TSS_SERIALIZED]: 'true',
    +      const response = getResponse()
    +      let nonStreamingBody: any = undefined
    +
    +      if (result !== undefined) {
    +        // first run without the stream in case `result` does not need streaming
    +        let done = false as boolean
    +        const callbacks: {
    +          onParse: (value: any) => void
    +          onDone: () => void
    +          onError: (error: any) => void
    +        } = {
    +          onParse: (value) => {
    +            nonStreamingBody = value
    +          },
    +          onDone: () => {
    +            done = true
    +          },
    +          onError: (error) => {
    +            throw error
    +          },
    +        }
    +        toCrossJSONStream(result, {
    +          refs: new Map(),
    +          plugins: serovalPlugins,
    +          onParse(value) {
    +            callbacks.onParse(value)
    +          },
    +          onDone() {
    +            callbacks.onDone()
    +          },
    +          onError: (error) => {
    +            callbacks.onError(error)
    +          },
    +        })
    +        if (done) {
    +          return new Response(
    +            nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
    +            {
    +              status: response?.status,
    +              statusText: response?.statusText,
    +              headers: {
    +                'Content-Type': 'application/json',
    +                [X_TSS_SERIALIZED]: 'true',
    +              },
                 },
    -          })
    +          )
             }
     
    -        return new Response(undefined, {
    -          status: getResponseStatus(),
    -          statusText: getResponseStatusText(),
    +        // not done yet, we need to stream
    +        const stream = new ReadableStream({
    +          start(controller) {
    +            callbacks.onParse = (value) =>
    +              controller.enqueue(JSON.stringify(value) + '\n')
    +            callbacks.onDone = () => {
    +              try {
    +                controller.close()
    +              } catch (error) {
    +                controller.error(error)
    +              }
    +            }
    +            callbacks.onError = (error) => controller.error(error)
    +            // stream the initial body
    +            if (nonStreamingBody !== undefined) {
    +              callbacks.onParse(nonStreamingBody)
    +            }
    +          },
    +        })
    +        return new Response(stream, {
    +          status: response?.status,
    +          statusText: response?.statusText,
    +          headers: {
    +            'Content-Type': 'application/x-ndjson',
    +            [X_TSS_SERIALIZED]: 'true',
    +          },
             })
           }
    +
    +      return new Response(undefined, {
    +        status: response?.status,
    +        statusText: response?.statusText,
    +      })
         } catch (error: any) {
           if (error instanceof Response) {
             return error
    @@ -320,9 +293,10 @@ export const handleServerAction = async ({
               }),
             ),
           )
    +      const response = getResponse()
           return new Response(serializedError, {
    -        status: getResponseStatus() || 500,
    -        statusText: getResponseStatusText(),
    +        status: response?.status ?? 500,
    +        statusText: response?.statusText,
             headers: {
               'Content-Type': 'application/json',
               [X_TSS_SERIALIZED]: 'true',
    
    From e79b6a26f3af91408172a4440362743c706bdf6d Mon Sep 17 00:00:00 2001
    From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
    Date: Fri, 7 Nov 2025 04:18:14 +0000
    Subject: [PATCH 06/10] ci: apply automated fixes
    
    ---
     .../preact/authenticated-routes/index.html    |   1 -
     .../preact/authenticated-routes/package.json  |   1 -
     .../preact/authenticated-routes/src/auth.tsx  |   7 +-
     .../preact/authenticated-routes/src/main.tsx  |   1 -
     .../preact/authenticated-routes/src/posts.ts  |   1 -
     .../src/routes/__root.tsx                     |   1 -
     .../src/routes/_auth.dashboard.tsx            |   1 -
     .../src/routes/_auth.invoices.$invoiceId.tsx  |   1 -
     .../src/routes/_auth.invoices.index.tsx       |   1 -
     .../src/routes/_auth.invoices.tsx             |   1 -
     .../authenticated-routes/src/routes/_auth.tsx |   1 -
     .../authenticated-routes/src/routes/index.tsx |   1 -
     .../authenticated-routes/src/routes/login.tsx |   1 -
     .../authenticated-routes/src/styles.css       |   1 -
     .../preact/authenticated-routes/src/utils.ts  |   1 -
     .../preact/authenticated-routes/tsconfig.json |   1 -
     .../authenticated-routes/vite.config.js       |   1 -
     examples/preact/basic-file-based/index.html   |   1 -
     examples/preact/basic-file-based/package.json |   1 -
     .../basic-file-based/postcss.config.mjs       |   1 -
     examples/preact/basic-file-based/src/main.tsx |   1 -
     .../preact/basic-file-based/src/posts.tsx     |   1 -
     .../basic-file-based/src/routes/__root.tsx    |   1 -
     .../src/routes/_pathlessLayout.tsx            |   1 -
     .../routes/_pathlessLayout/_nested-layout.tsx |   1 -
     .../_nested-layout/route-a.tsx                |   1 -
     .../_nested-layout/route-b.tsx                |   1 -
     .../basic-file-based/src/routes/anchor.tsx    |   5 +-
     .../basic-file-based/src/routes/index.tsx     |   1 -
     .../src/routes/posts.$postId.tsx              |   1 -
     .../src/routes/posts.index.tsx                |   1 -
     .../src/routes/posts.route.tsx                |   1 -
     .../preact/basic-file-based/src/styles.css    |   1 -
     .../basic-file-based/tailwind.config.mjs      |   1 -
     .../preact/basic-file-based/tsconfig.json     |   1 -
     .../preact/basic-file-based/vite.config.js    |   1 -
     examples/preact/basic-preact-query/index.html |   1 -
     .../preact/basic-preact-query/package.json    |   1 -
     .../preact/basic-preact-query/src/main.tsx    |   1 -
     .../basic-preact-query/src/posts.lazy.tsx     |   1 -
     .../preact/basic-preact-query/src/posts.ts    |   1 -
     .../preact/basic-preact-query/src/styles.css  |   1 -
     .../preact/basic-preact-query/tsconfig.json   |   1 -
     .../preact/basic-preact-query/vite.config.js  |   1 -
     examples/preact/basic/server.js               |   3 +-
     examples/preact/basic/src/entry-client.tsx    |   1 -
     examples/preact/basic/src/entry-server.tsx    |   1 -
     examples/preact/basic/src/router.tsx          |   7 +-
     examples/preact/scroll-restoration/index.html |   1 -
     .../preact/scroll-restoration/package.json    |   1 -
     .../preact/scroll-restoration/src/main.tsx    |   1 -
     .../preact/scroll-restoration/src/styles.css  |   1 -
     .../preact/scroll-restoration/tsconfig.json   |   1 -
     .../preact/scroll-restoration/vite.config.js  |   1 -
     .../vanilla/authenticated-routes/index.html   | 116 +++-
     .../vanilla/authenticated-routes/package.json |   1 -
     .../vanilla/authenticated-routes/src/main.ts  |  30 +-
     .../authenticated-routes/tsconfig.json        |   1 -
     .../authenticated-routes/vite.config.ts       |   6 +-
     examples/vanilla/basic/index.html             |   6 +-
     examples/vanilla/basic/package.json           |   1 -
     examples/vanilla/basic/src/main.ts            |  33 +-
     examples/vanilla/basic/src/posts.ts           |   1 -
     examples/vanilla/basic/vite.config.js         |   6 +-
     examples/vanilla/jsx-router/index.html        |   6 +-
     examples/vanilla/jsx-router/package.json      |   1 -
     examples/vanilla/jsx-router/src/main.ts       |  96 ++--
     examples/vanilla/jsx-router/src/renderer.ts   | 156 ++++--
     examples/vanilla/jsx-router/tsconfig.json     |   1 -
     examples/vanilla/jsx-router/vite.config.ts    |   6 +-
     .../vanilla/scroll-restoration/index.html     |  83 ++-
     .../vanilla/scroll-restoration/package.json   |   1 -
     .../vanilla/scroll-restoration/src/main.ts    |  24 +-
     .../vanilla/scroll-restoration/tsconfig.json  |   1 -
     .../vanilla/scroll-restoration/vite.config.ts |   6 +-
     labeler-config.yml                            |   6 +
     packages/preact-router/build-no-check.js      |   6 +-
     packages/preact-router/src/Asset.tsx          |  13 +-
     packages/preact-router/src/ClientOnly.tsx     |   5 +-
     packages/preact-router/src/HeadContent.tsx    |   9 +-
     packages/preact-router/src/Match.tsx          |  14 +-
     packages/preact-router/src/SafeFragment.tsx   |   9 +-
     .../preact-router/src/lazyRouteComponent.tsx  |   9 +-
     packages/preact-router/src/link.tsx           |   3 +-
     packages/preact-router/src/matchContext.tsx   |   4 +-
     packages/preact-router/src/preact-store.ts    |   7 +-
     .../preact-router/src/renderRouteNotFound.tsx |   9 +-
     packages/preact-router/src/route.tsx          |   2 +-
     packages/preact-router/src/router.ts          |   5 +-
     .../src/ssr/renderRouterToStream.tsx          |   4 +-
     packages/preact-router/src/useBlocker.tsx     |  17 +-
     packages/preact-router/src/useMatch.tsx       |   9 +-
     packages/preact-router/src/useNavigate.tsx    |   9 +-
     packages/preact-router/src/useRouterState.tsx |  34 +-
     packages/preact-router/src/utils.ts           |  10 +-
     .../examples/renderer/renderer.ts             | 183 ++++---
     .../examples/router-jsx-example.html          | 507 ++++++++++--------
     .../examples/vanilla-dom-example.ts           |  18 +-
     .../examples/vanilla-jsx-example.ts           |  49 +-
     packages/vanilla-router/package.json          |   1 -
     packages/vanilla-router/src/error-handling.ts |  37 +-
     packages/vanilla-router/src/fileRoute.ts      |  27 +-
     packages/vanilla-router/src/index.ts          |  25 +-
     packages/vanilla-router/src/navigation.ts     |  15 +-
     packages/vanilla-router/src/route-data.ts     |  25 +-
     packages/vanilla-router/src/route.ts          | 188 ++++---
     packages/vanilla-router/src/router.ts         |   3 +-
     .../vanilla-router/src/scroll-restoration.ts  |  16 +-
     packages/vanilla-router/src/types.ts          |  13 +-
     packages/vanilla-router/src/utils.ts          |  13 +-
     packages/vanilla-router/src/vanilla-router.ts |  48 +-
     packages/vanilla-router/tests/router.test.ts  |   5 +-
     packages/vanilla-router/vite.config.ts        |   1 -
     113 files changed, 1203 insertions(+), 809 deletions(-)
    
    diff --git a/examples/preact/authenticated-routes/index.html b/examples/preact/authenticated-routes/index.html
    index 433d9e052af..5c7fd8a77e9 100644
    --- a/examples/preact/authenticated-routes/index.html
    +++ b/examples/preact/authenticated-routes/index.html
    @@ -10,4 +10,3 @@
         
       
     
    -
    diff --git a/examples/preact/authenticated-routes/package.json b/examples/preact/authenticated-routes/package.json
    index ebc5552e393..54a467d4caa 100644
    --- a/examples/preact/authenticated-routes/package.json
    +++ b/examples/preact/authenticated-routes/package.json
    @@ -25,4 +25,3 @@
         "vite": "^7.1.7"
       }
     }
    -
    diff --git a/examples/preact/authenticated-routes/src/auth.tsx b/examples/preact/authenticated-routes/src/auth.tsx
    index b8bb61dc37e..478e4ebbf98 100644
    --- a/examples/preact/authenticated-routes/src/auth.tsx
    +++ b/examples/preact/authenticated-routes/src/auth.tsx
    @@ -26,7 +26,11 @@ function setStoredUser(user: string | null) {
       }
     }
     
    -export function AuthProvider({ children }: { children: preact.ComponentChildren }) {
    +export function AuthProvider({
    +  children,
    +}: {
    +  children: preact.ComponentChildren
    +}) {
       const [user, setUser] = useState(getStoredUser())
       const isAuthenticated = !!user
     
    @@ -62,4 +66,3 @@ export function useAuth() {
       }
       return context
     }
    -
    diff --git a/examples/preact/authenticated-routes/src/main.tsx b/examples/preact/authenticated-routes/src/main.tsx
    index d91c21a76ad..26f8f235e9c 100644
    --- a/examples/preact/authenticated-routes/src/main.tsx
    +++ b/examples/preact/authenticated-routes/src/main.tsx
    @@ -40,4 +40,3 @@ const rootElement = document.getElementById('app')!
     if (!rootElement.innerHTML) {
       render(, rootElement)
     }
    -
    diff --git a/examples/preact/authenticated-routes/src/posts.ts b/examples/preact/authenticated-routes/src/posts.ts
    index 1c519123341..3c74418441c 100644
    --- a/examples/preact/authenticated-routes/src/posts.ts
    +++ b/examples/preact/authenticated-routes/src/posts.ts
    @@ -21,4 +21,3 @@ export const fetchInvoiceById = async (id: number) => {
         .get(`https://jsonplaceholder.typicode.com/posts/${id}`)
         .then((r) => r.data)
     }
    -
    diff --git a/examples/preact/authenticated-routes/src/routes/__root.tsx b/examples/preact/authenticated-routes/src/routes/__root.tsx
    index 7d0f34a6940..6222423d6cb 100644
    --- a/examples/preact/authenticated-routes/src/routes/__root.tsx
    +++ b/examples/preact/authenticated-routes/src/routes/__root.tsx
    @@ -15,4 +15,3 @@ export const Route = createRootRouteWithContext()({
         
       ),
     })
    -
    diff --git a/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx b/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx
    index 8acde0cf0f6..1aec8e70f49 100644
    --- a/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx
    +++ b/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx
    @@ -16,4 +16,3 @@ function DashboardPage() {
         
       )
     }
    -
    diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx
    index f5732318d81..3fb0458ee4a 100644
    --- a/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx
    +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx
    @@ -28,4 +28,3 @@ function InvoicePage() {
         
       )
     }
    -
    diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx
    index d955e1c043a..ca17732f950 100644
    --- a/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx
    +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx
    @@ -3,4 +3,3 @@ import { createFileRoute } from '@tanstack/preact-router'
     export const Route = createFileRoute('/_auth/invoices/')({
       component: () => 
    Select an invoice to view it!
    , }) - diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx index 609c3a3bce7..032f2d41a5a 100644 --- a/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx @@ -41,4 +41,3 @@ function InvoicesRoute() {
    ) } - diff --git a/examples/preact/authenticated-routes/src/routes/_auth.tsx b/examples/preact/authenticated-routes/src/routes/_auth.tsx index a7a2b085071..8b949380afa 100644 --- a/examples/preact/authenticated-routes/src/routes/_auth.tsx +++ b/examples/preact/authenticated-routes/src/routes/_auth.tsx @@ -68,4 +68,3 @@ function AuthLayout() {
    ) } - diff --git a/examples/preact/authenticated-routes/src/routes/index.tsx b/examples/preact/authenticated-routes/src/routes/index.tsx index bd8a6bb4537..bd4578caa23 100644 --- a/examples/preact/authenticated-routes/src/routes/index.tsx +++ b/examples/preact/authenticated-routes/src/routes/index.tsx @@ -44,4 +44,3 @@ function HomeComponent() {
    ) } - diff --git a/examples/preact/authenticated-routes/src/routes/login.tsx b/examples/preact/authenticated-routes/src/routes/login.tsx index 0a15d793cb8..0694775e4b8 100644 --- a/examples/preact/authenticated-routes/src/routes/login.tsx +++ b/examples/preact/authenticated-routes/src/routes/login.tsx @@ -92,4 +92,3 @@ function LoginComponent() {
    ) } - diff --git a/examples/preact/authenticated-routes/src/styles.css b/examples/preact/authenticated-routes/src/styles.css index e07de1f8541..0b8e317099c 100644 --- a/examples/preact/authenticated-routes/src/styles.css +++ b/examples/preact/authenticated-routes/src/styles.css @@ -11,4 +11,3 @@ html { body { @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; } - diff --git a/examples/preact/authenticated-routes/src/utils.ts b/examples/preact/authenticated-routes/src/utils.ts index 5a1d7cb6b91..4f3faad1343 100644 --- a/examples/preact/authenticated-routes/src/utils.ts +++ b/examples/preact/authenticated-routes/src/utils.ts @@ -1,4 +1,3 @@ export async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } - diff --git a/examples/preact/authenticated-routes/tsconfig.json b/examples/preact/authenticated-routes/tsconfig.json index af80d030c77..3b34947304d 100644 --- a/examples/preact/authenticated-routes/tsconfig.json +++ b/examples/preact/authenticated-routes/tsconfig.json @@ -8,4 +8,3 @@ "skipLibCheck": true } } - diff --git a/examples/preact/authenticated-routes/vite.config.js b/examples/preact/authenticated-routes/vite.config.js index 1a28c9b7f18..b239d60f4f7 100644 --- a/examples/preact/authenticated-routes/vite.config.js +++ b/examples/preact/authenticated-routes/vite.config.js @@ -12,4 +12,3 @@ export default defineConfig({ preact(), ], }) - diff --git a/examples/preact/basic-file-based/index.html b/examples/preact/basic-file-based/index.html index 7e0378f9ade..f9d481eb748 100644 --- a/examples/preact/basic-file-based/index.html +++ b/examples/preact/basic-file-based/index.html @@ -10,4 +10,3 @@ - diff --git a/examples/preact/basic-file-based/package.json b/examples/preact/basic-file-based/package.json index 543f628d44b..fb0590ff52a 100644 --- a/examples/preact/basic-file-based/package.json +++ b/examples/preact/basic-file-based/package.json @@ -25,4 +25,3 @@ "vite": "^7.1.7" } } - diff --git a/examples/preact/basic-file-based/postcss.config.mjs b/examples/preact/basic-file-based/postcss.config.mjs index b4a6220e2db..2e7af2b7f1a 100644 --- a/examples/preact/basic-file-based/postcss.config.mjs +++ b/examples/preact/basic-file-based/postcss.config.mjs @@ -4,4 +4,3 @@ export default { autoprefixer: {}, }, } - diff --git a/examples/preact/basic-file-based/src/main.tsx b/examples/preact/basic-file-based/src/main.tsx index 15da18cdd9b..cddc50db860 100644 --- a/examples/preact/basic-file-based/src/main.tsx +++ b/examples/preact/basic-file-based/src/main.tsx @@ -23,4 +23,3 @@ const rootElement = document.getElementById('app')! if (!rootElement.innerHTML) { render(, rootElement) } - diff --git a/examples/preact/basic-file-based/src/posts.tsx b/examples/preact/basic-file-based/src/posts.tsx index 8f24a7b332d..e07ecd42326 100644 --- a/examples/preact/basic-file-based/src/posts.tsx +++ b/examples/preact/basic-file-based/src/posts.tsx @@ -30,4 +30,3 @@ export const fetchPosts = async () => { .get>('https://jsonplaceholder.typicode.com/posts') .then((r) => r.data.slice(0, 10)) } - diff --git a/examples/preact/basic-file-based/src/routes/__root.tsx b/examples/preact/basic-file-based/src/routes/__root.tsx index d8c2f672f3e..ad391da9684 100644 --- a/examples/preact/basic-file-based/src/routes/__root.tsx +++ b/examples/preact/basic-file-based/src/routes/__root.tsx @@ -68,4 +68,3 @@ function RootComponent() { ) } - diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx index 49427f0a963..a96fbbda14a 100644 --- a/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx @@ -15,4 +15,3 @@ function LayoutComponent() {
    ) } - diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx index 562b9277b0c..b7110984deb 100644 --- a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx @@ -33,4 +33,3 @@ function LayoutComponent() { ) } - diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx index 90f0bd15ba5..957fa5f89bd 100644 --- a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx @@ -8,4 +8,3 @@ export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')( function LayoutAComponent() { return
    I'm layout A!
    } - diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx index 6f55fe35601..0adb241a1da 100644 --- a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx @@ -8,4 +8,3 @@ export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')( function LayoutBComponent() { return
    I'm layout B!
    } - diff --git a/examples/preact/basic-file-based/src/routes/anchor.tsx b/examples/preact/basic-file-based/src/routes/anchor.tsx index 7fdb51fac20..1c7b9419e2f 100644 --- a/examples/preact/basic-file-based/src/routes/anchor.tsx +++ b/examples/preact/basic-file-based/src/routes/anchor.tsx @@ -134,7 +134,9 @@ function AnchorComponent() {