diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 68a913e4..485afb7e 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -43,5 +43,8 @@ jobs:
- name: Build @fastify/vite
run: pnpm --filter @fastify/vite build
+ - name: Build @fastify/tanstack
+ run: pnpm --filter @fastify/tanstack build
+
- name: Run integration tests
run: pnpm test:examples
diff --git a/examples/tanstack-vanilla-ts/package.json b/examples/tanstack-vanilla-ts/package.json
new file mode 100644
index 00000000..82974692
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@fastify-vite/example-tanstack-vanilla-ts",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "tsx src/server.ts --dev",
+ "start": "NODE_ENV=production node build/server.js",
+ "build": "pnpm build:server && pnpm build:client",
+ "build:client": "vite build --app",
+ "build:server": "tsc",
+ "test": "tsx --test"
+ },
+ "dependencies": {
+ "@fastify/tanstack": "workspace:^",
+ "@fastify/vite": "workspace:^",
+ "@tanstack/react-router": "^1.0.0",
+ "fastify": "catalog:",
+ "react": "catalog:react",
+ "react-dom": "catalog:react"
+ },
+ "devDependencies": {
+ "@types/node": "^24.12.2",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.2",
+ "tsx": "catalog:",
+ "typescript": "catalog:",
+ "vite": "catalog:"
+ }
+}
diff --git a/examples/tanstack-vanilla-ts/src/client/create.tsx b/examples/tanstack-vanilla-ts/src/client/create.tsx
new file mode 100644
index 00000000..a040484a
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/client/create.tsx
@@ -0,0 +1,8 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routes.ts'
+
+export function createAppRouter() {
+ return createRouter({
+ routeTree,
+ })
+}
diff --git a/examples/tanstack-vanilla-ts/src/client/index.html b/examples/tanstack-vanilla-ts/src/client/index.html
new file mode 100644
index 00000000..e289a82c
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/client/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/tanstack-vanilla-ts/src/client/routes.ts b/examples/tanstack-vanilla-ts/src/client/routes.ts
new file mode 100644
index 00000000..dda788a4
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/client/routes.ts
@@ -0,0 +1,18 @@
+import { createElement, Fragment } from 'react'
+import { createRootRoute, createRoute, Outlet, Scripts } from '@tanstack/react-router'
+
+const rootRoute = createRootRoute({
+ component: function Root() {
+ return createElement(Fragment, null, createElement(Outlet), createElement(Scripts))
+ },
+})
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: function Index() {
+ return createElement('h1', null, 'Hello from TanStack Router SSR!')
+ },
+})
+
+export const routeTree = rootRoute.addChildren([indexRoute])
diff --git a/examples/tanstack-vanilla-ts/src/client/tsconfig.json b/examples/tanstack-vanilla-ts/src/client/tsconfig.json
new file mode 100644
index 00000000..b076d185
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/client/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "allowJs": false,
+ "jsx": "react-jsx",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "noEmit": true,
+ "target": "ES2023",
+ "types": ["vite/client"]
+ },
+ "include": ["**/*"],
+ "exclude": []
+}
diff --git a/examples/tanstack-vanilla-ts/src/server.test.ts b/examples/tanstack-vanilla-ts/src/server.test.ts
new file mode 100644
index 00000000..fb2000f7
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/server.test.ts
@@ -0,0 +1,12 @@
+import test from 'node:test'
+import { resolve } from 'node:path'
+import { makeBuildTest, makeIndexTest } from '../../test-factories.mjs'
+import { main } from './server.ts'
+
+const viteConfigLocation = resolve(import.meta.dirname, '..')
+
+test('tanstack-vanilla-ts', async (t) => {
+ await t.test('build production bundle', makeBuildTest({ cwd: viteConfigLocation }))
+ await t.test('render index page in production', makeIndexTest({ main, dev: false }))
+ await t.test('render index page in development', makeIndexTest({ main, dev: true }))
+})
diff --git a/examples/tanstack-vanilla-ts/src/server.ts b/examples/tanstack-vanilla-ts/src/server.ts
new file mode 100644
index 00000000..284907a0
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/server.ts
@@ -0,0 +1,22 @@
+import { resolve } from 'node:path'
+import Fastify from 'fastify'
+import FastifyVite from '@fastify/vite'
+import * as renderer from '@fastify/tanstack'
+
+export async function main(dev?: boolean) {
+ const server = Fastify()
+
+ await server.register(FastifyVite, {
+ root: resolve(import.meta.dirname, '..'),
+ dev: dev || process.argv.includes('--dev'),
+ renderer,
+ })
+
+ await server.vite.ready()
+ return server
+}
+
+if (process.argv[1] === import.meta.filename) {
+ const server = await main()
+ await server.listen({ port: 3000 })
+}
diff --git a/examples/tanstack-vanilla-ts/tsconfig.json b/examples/tanstack-vanilla-ts/tsconfig.json
new file mode 100644
index 00000000..c431b0ca
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "allowSyntheticDefaultImports": true,
+ "checkJs": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "incremental": true,
+ "isolatedModules": true,
+ "lib": ["ESNext"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "build",
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "removeComments": true,
+ "resolveJsonModule": true,
+ "rootDir": "src",
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "ESNext",
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src"],
+ "exclude": ["src/client/**/*", "src/**/*.test.ts"]
+}
diff --git a/examples/tanstack-vanilla-ts/vite.config.js b/examples/tanstack-vanilla-ts/vite.config.js
new file mode 100644
index 00000000..80c7c038
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/vite.config.js
@@ -0,0 +1,11 @@
+import { resolve } from 'node:path'
+import viteFastifyTanstack from '@fastify/tanstack/plugin'
+
+export default {
+ root: resolve(import.meta.dirname, 'src', 'client'),
+ plugins: [viteFastifyTanstack()],
+ build: {
+ emptyOutDir: true,
+ outDir: resolve(import.meta.dirname, 'build'),
+ },
+}
diff --git a/packages/fastify-tanstack/package.json b/packages/fastify-tanstack/package.json
new file mode 100644
index 00000000..8ec150a4
--- /dev/null
+++ b/packages/fastify-tanstack/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@fastify/tanstack",
+ "version": "0.0.1",
+ "description": "@fastify/vite renderer for TanStack Router SSR",
+ "keywords": [
+ "fastify",
+ "ssr",
+ "tanstack",
+ "tanstack-router",
+ "vite"
+ ],
+ "homepage": "https://github.com/fastify/fastify-vite",
+ "bugs": {
+ "url": "https://github.com/fastify/fastify-vite/issues"
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/fastify/fastify-vite.git"
+ },
+ "files": [
+ "dist/**/*",
+ "!dist/**/*.test.*",
+ "virtual/**/*"
+ ],
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ },
+ "./plugin": {
+ "types": "./dist/plugin/index.d.ts",
+ "import": "./dist/plugin/index.js"
+ },
+ "./server": {
+ "types": "./dist/server.d.ts",
+ "import": "./dist/server.js"
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "build": "tsc",
+ "prepublishOnly": "pnpm build"
+ },
+ "dependencies": {
+ "@fastify/vite": "workspace:^",
+ "@tanstack/history": "^1.0.0",
+ "@tanstack/react-router": "^1.0.0",
+ "@tanstack/router-core": "^1.0.0",
+ "mlly": "^1.7.4",
+ "react": "catalog:react",
+ "react-dom": "catalog:react",
+ "youch": "^3.3.4"
+ },
+ "devDependencies": {
+ "@types/node": "^24.12.2",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.2",
+ "fastify": "catalog:",
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "vitest": "catalog:"
+ }
+}
diff --git a/packages/fastify-tanstack/src/index.ts b/packages/fastify-tanstack/src/index.ts
new file mode 100644
index 00000000..152bf56d
--- /dev/null
+++ b/packages/fastify-tanstack/src/index.ts
@@ -0,0 +1,7 @@
+export { prepareServer } from './server.ts'
+
+export { prepareClient, createRouteHandler, createErrorHandler, createRoute } from './routing.ts'
+
+export { createRenderFunction, createHtmlFunction } from './rendering.ts'
+
+export const clientModule = '$app/index.ts'
diff --git a/packages/fastify-tanstack/src/plugin/index.ts b/packages/fastify-tanstack/src/plugin/index.ts
new file mode 100644
index 00000000..446a4430
--- /dev/null
+++ b/packages/fastify-tanstack/src/plugin/index.ts
@@ -0,0 +1,93 @@
+import viteFastify from '@fastify/vite/plugin'
+import type { Plugin, ResolvedConfig } from 'vite'
+import {
+ prefix,
+ resolveId,
+ loadSource,
+ loadVirtualModule,
+ createPlaceholderExports,
+} from './virtual.ts'
+import { closeBundle as closeBundleImpl } from './preload.ts'
+
+interface PluginContext {
+ root: string
+ resolvedConfig: ResolvedConfig | null
+}
+
+export default function viteFastifyTanstackPlugin(): Plugin[] {
+ let resolvedBundle: Record | null = null
+
+ const context: PluginContext = {
+ root: '',
+ resolvedConfig: null,
+ }
+ return [
+ viteFastify({
+ clientModule: '$app/index.ts',
+ }),
+ {
+ name: 'vite-plugin-tanstack-fastify',
+ config: configHook,
+ configResolved: configResolved.bind(context),
+ resolveId: resolveId.bind(context),
+ async load(id) {
+ if (id.includes('?server') && !this.environment.config.build?.ssr) {
+ const source = loadSource(id)
+ return createPlaceholderExports(source)
+ }
+ if (id.includes('?client') && this.environment.config.build?.ssr) {
+ const source = loadSource(id)
+ return createPlaceholderExports(source)
+ }
+ if (prefix.test(id)) {
+ const [, virtual] = id.split(prefix)
+ if (virtual) {
+ return loadVirtualModule(virtual)
+ }
+ }
+ },
+ transformIndexHtml: {
+ order: 'post',
+ handler(_html, { bundle }) {
+ if (bundle) {
+ resolvedBundle = bundle
+ }
+ },
+ },
+ closeBundle() {
+ closeBundleImpl.call(this, resolvedBundle)
+ },
+ },
+ ]
+}
+
+function configResolved(this: PluginContext, config: ResolvedConfig) {
+ this.resolvedConfig = config
+ this.root = config.root
+}
+
+function configHook(config: Record, { command }: { command: string }) {
+ if (command === 'build') {
+ if (!config.build) {
+ config.build = {}
+ }
+ if (!config.build.rollupOptions) {
+ config.build.rollupOptions = {}
+ }
+ config.build.rollupOptions.onwarn = onwarn
+ }
+}
+
+function onwarn(
+ warning: { code?: string; message?: string },
+ rollupWarn: (warning: unknown) => void,
+) {
+ if (
+ !(
+ warning.code == 'PLUGIN_WARNING' &&
+ warning.message?.includes?.('dynamic import will not move module into another chunk')
+ )
+ ) {
+ rollupWarn(warning)
+ }
+}
diff --git a/packages/fastify-tanstack/src/plugin/preload.ts b/packages/fastify-tanstack/src/plugin/preload.ts
new file mode 100644
index 00000000..b9ab195d
--- /dev/null
+++ b/packages/fastify-tanstack/src/plugin/preload.ts
@@ -0,0 +1,102 @@
+import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
+import { join, isAbsolute, parse as parsePath } from 'node:path'
+
+const imageFileRE = /\.((png)|(jpg)|(svg)|(webp)|(gif))$/
+
+interface BundleChunk {
+ type: 'chunk'
+ isDynamicEntry?: boolean
+ name: string
+ fileName: string
+ imports?: string[]
+ moduleIds: string[]
+ modules: Record
+ viteMetadata?: {
+ importedCss?: string[]
+ }
+}
+
+function isDynamicChunk(meta: unknown): meta is BundleChunk {
+ return (
+ typeof meta === 'object' &&
+ meta !== null &&
+ (meta as Record).type === 'chunk' &&
+ !!(meta as Record).isDynamicEntry
+ )
+}
+
+export async function closeBundle(
+ this: {
+ environment: {
+ name: string
+ config: {
+ build: { assetsInlineLimit: number | Function; outDir: string }
+ root: string
+ base: string
+ }
+ }
+ },
+ resolvedBundle: Record | null,
+) {
+ if (this.environment.name !== 'client') {
+ return
+ }
+ const { assetsInlineLimit } = this.environment.config.build
+ const { root, base } = this.environment.config
+ let distDir: string
+ if (isAbsolute(this.environment.config.build.outDir)) {
+ distDir = this.environment.config.build.outDir
+ } else {
+ distDir = join(root, this.environment.config.build.outDir)
+ }
+
+ const indexHtml = readFileSync(join(distDir, 'index.html'), 'utf8')
+
+ const routeChunks = Object.values(resolvedBundle ?? {}).filter(isDynamicChunk)
+
+ if (routeChunks.length === 0) {
+ return
+ }
+
+ const limit = typeof assetsInlineLimit === 'number' ? assetsInlineLimit : 0
+
+ for (const chunk of routeChunks) {
+ const jsImports = chunk.imports ?? []
+ const cssImports = chunk.viteMetadata?.importedCss ?? []
+ const images = chunk.moduleIds.filter((img) => {
+ return chunk.modules[img].originalLength > limit && imageFileRE.test(img)
+ })
+
+ let imagePreloads = '\n'
+ for (let image of images) {
+ image = image.slice(root.length + 1)
+ imagePreloads += ` \n`
+ }
+ let cssPreloads = ''
+ for (const css of cssImports) {
+ cssPreloads += ` \n`
+ }
+ let jsPreloads = ''
+ for (const js of jsImports) {
+ jsPreloads += ` \n`
+ }
+
+ const pageHtml = appendHead(indexHtml, imagePreloads, cssPreloads, jsPreloads)
+ const htmlPath = `html/${chunk.name}.html`
+ writeHtml(htmlPath, pageHtml, distDir)
+ }
+}
+
+function appendHead(html: string, ...tags: string[]) {
+ const content = tags.join('\n ')
+ return html.replace(/]*)>/i, `\n ${content}`)
+}
+
+function writeHtml(htmlPath: string, pageHtml: string, distDir: string) {
+ const { dir, base } = parsePath(htmlPath)
+ const htmlDir = join(distDir, dir)
+ if (!existsSync(htmlDir)) {
+ mkdirSync(htmlDir, { recursive: true })
+ }
+ writeFileSync(join(htmlDir, base), pageHtml)
+}
diff --git a/packages/fastify-tanstack/src/plugin/virtual.ts b/packages/fastify-tanstack/src/plugin/virtual.ts
new file mode 100644
index 00000000..90e89ed6
--- /dev/null
+++ b/packages/fastify-tanstack/src/plugin/virtual.ts
@@ -0,0 +1,94 @@
+import { readFileSync, existsSync } from 'node:fs'
+import { resolve } from 'node:path'
+import { findExports } from 'mlly'
+
+const virtualRoot = resolve(import.meta.dirname, '..', '..', 'virtual')
+
+const virtualModules = ['mount.ts', 'routes.ts', 'create.tsx', 'context.ts', 'index.ts']
+
+export const prefix = /^\/?\$app\//
+
+export async function resolveId(this: { root: string }, id: string) {
+ if (process.platform === 'win32' && /^\.\.\/[C-Z]:/.test(id)) {
+ return id.substring(3)
+ }
+
+ if (prefix.test(id)) {
+ const [, virtual] = id.split(prefix)
+ if (virtual) {
+ const override = loadVirtualModuleOverride(this.root, virtual)
+ if (override) {
+ return override
+ }
+ return id
+ }
+ }
+}
+
+export function loadVirtualModule(virtual: string): { code: string; map: null } | undefined {
+ if (!matchesVirtualModule(virtual)) {
+ return
+ }
+ const codePath = resolve(virtualRoot, virtual)
+ return {
+ code: readFileSync(codePath, 'utf8'),
+ map: null,
+ }
+}
+
+function matchesVirtualModule(virtual: string): boolean {
+ if (!virtual) {
+ return false
+ }
+ for (const entry of virtualModules) {
+ if (virtual.startsWith(entry)) {
+ return true
+ }
+ }
+ return false
+}
+
+function loadVirtualModuleOverride(viteProjectRoot: string, virtual: string): string | undefined {
+ if (!matchesVirtualModule(virtual)) {
+ return
+ }
+ const overridePath = resolve(viteProjectRoot, virtual)
+ if (existsSync(overridePath)) {
+ return overridePath
+ }
+ const base = overridePath.replace(/\.[^.]+$/, '')
+ for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
+ const candidate = base + ext
+ if (candidate !== overridePath && existsSync(candidate)) {
+ return candidate
+ }
+ }
+}
+
+export function loadSource(id: string): string {
+ const filePath = id.replace(/\?client$/, '').replace(/\?server$/, '')
+ return readFileSync(filePath, 'utf8')
+}
+
+export function createPlaceholderExports(source: string): string {
+ let pExports = ''
+ for (const exp of findExports(source)) {
+ switch (exp.type) {
+ case 'named':
+ for (const name of exp.names) {
+ pExports += `export const ${name} = {}\n`
+ }
+ break
+ case 'default':
+ pExports += `export default {}\n`
+ break
+ case 'declaration':
+ pExports += `export const ${exp.name} = {}\n`
+ break
+ case 'star':
+ pExports += `export ${exp.code}\n`
+ break
+ }
+ }
+ return pExports
+}
diff --git a/packages/fastify-tanstack/src/rendering.ts b/packages/fastify-tanstack/src/rendering.ts
new file mode 100644
index 00000000..1e4757ea
--- /dev/null
+++ b/packages/fastify-tanstack/src/rendering.ts
@@ -0,0 +1,179 @@
+import { PassThrough } from 'node:stream'
+import { createElement } from 'react'
+import { createRequire } from 'node:module'
+
+// Bun's react-dom/server shim only activates through require(), not ESM import.
+// Without this, renderToPipeableStream silently uses the wrong implementation.
+const _require = createRequire(import.meta.url)
+const { renderToPipeableStream } = _require(
+ 'react-dom/server.node',
+) as typeof import('react-dom/server')
+import { createMemoryHistory } from '@tanstack/history'
+import { RouterProvider } from '@tanstack/react-router'
+import type { AnyRouter, AnyRedirect } from '@tanstack/router-core'
+import {
+ attachRouterServerSsrUtils,
+ transformPipeableStreamWithRouter,
+} from '@tanstack/router-core/ssr/server'
+import { createHtmlTemplateFunction } from '@fastify/vite/utils'
+import type { RuntimeConfig } from '@fastify/vite'
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
+
+interface RenderResult {
+ pipeable: NodeJS.ReadableStream | null
+ router: AnyRouter
+ redirect?: AnyRedirect
+}
+
+export async function createRenderFunction(client: Record) {
+ const createAppRouter = client.createAppRouter as (req?: FastifyRequest) => AnyRouter
+ // Registered as reply.render()
+ return async function (this: FastifyReply): Promise {
+ const url = this.request.url
+ const router = createAppRouter(this.request)
+
+ attachRouterServerSsrUtils({ router, manifest: undefined })
+
+ const history = createMemoryHistory({ initialEntries: [url] })
+ router.update({ history })
+
+ await router.load()
+
+ const pendingRedirect = router.stores.redirect.get()
+ if (pendingRedirect) {
+ return { pipeable: null, router, redirect: pendingRedirect }
+ }
+
+ await router.serverSsr?.dehydrate()
+
+ const app = createElement(RouterProvider, { router })
+
+ const pipeable = await new Promise((resolve, reject) => {
+ try {
+ const stream = renderToPipeableStream(app, {
+ onShellReady() {
+ resolve(stream as unknown as NodeJS.ReadableStream)
+ },
+ onShellError: reject,
+ onError(error: unknown) {
+ console.error('SSR streaming error:', error)
+ },
+ })
+ } catch (error) {
+ reject(error)
+ }
+ })
+
+ return { pipeable, router }
+ }
+}
+
+type HtmlFunction = (this: FastifyReply) => FastifyReply | Promise
+
+// The return value of this function gets registered as reply.html()
+export async function createHtmlFunction(
+ source: string,
+ _: FastifyInstance,
+ config: RuntimeConfig,
+): Promise {
+ if (config.spa) {
+ const template = createHtmlTemplateFunction(source)
+ return function (this: FastifyReply) {
+ this.type('text/html')
+ this.send(template({ element: '' }))
+ return this
+ }
+ }
+
+ const { beforeHtml, afterHtml } = splitHtmlTemplate(source)
+
+ return async function (this: FastifyReply) {
+ const {
+ pipeable,
+ router,
+ redirect: pendingRedirect,
+ } = (await (this as any).render()) as RenderResult
+
+ if (pendingRedirect) {
+ const resolved = router.resolveRedirect(pendingRedirect)
+ const href = resolved.headers.get('Location') ?? '/'
+ this.code(resolved.status)
+ this.redirect(href)
+ return this
+ }
+
+ const statusCode = router.stores.statusCode.get()
+ this.code(statusCode)
+ this.type('text/html')
+
+ if (!pipeable) {
+ this.send(beforeHtml + afterHtml)
+ return this
+ }
+
+ const assembled = assembleHtmlStream(beforeHtml, afterHtml, pipeable)
+ const transformed = transformPipeableStreamWithRouter(router, assembled)
+
+ if (config.dev) {
+ const checker = new PassThrough()
+ let sawBarrier = false
+ checker.on('data', (chunk: Buffer) => {
+ if (!sawBarrier && chunk.toString().includes('$tsr-stream-barrier')) {
+ sawBarrier = true
+ }
+ })
+ checker.on('end', () => {
+ if (!sawBarrier) {
+ console.warn(
+ '[@fastify/tanstack] not found in rendered output. ' +
+ 'Dehydration will fail. Ensure root route renders .',
+ )
+ }
+ })
+ transformed.pipe(checker)
+ }
+
+ this.send(transformed)
+ return this
+ }
+}
+
+function splitHtmlTemplate(source: string) {
+ const marker = ''
+ const idx = source.indexOf(marker)
+ if (idx !== -1) {
+ return {
+ beforeHtml: source.slice(0, idx),
+ afterHtml: source.slice(idx + marker.length),
+ }
+ }
+ const bodyIdx = source.indexOf('')
+ if (bodyIdx !== -1) {
+ return {
+ beforeHtml: source.slice(0, bodyIdx),
+ afterHtml: source.slice(bodyIdx),
+ }
+ }
+ return { beforeHtml: source, afterHtml: '' }
+}
+
+function assembleHtmlStream(
+ beforeHtml: string,
+ afterHtml: string,
+ pipeable: NodeJS.ReadableStream,
+) {
+ const stream = new PassThrough()
+
+ const reactOut = new PassThrough()
+ reactOut.on('data', (chunk: Buffer) => stream.write(chunk))
+ reactOut.on('end', () => {
+ stream.write(afterHtml)
+ stream.end()
+ })
+ reactOut.on('error', (err: Error) => stream.destroy(err))
+
+ stream.write(beforeHtml)
+ ;(pipeable as any).pipe(reactOut)
+
+ return stream
+}
diff --git a/packages/fastify-tanstack/src/routing.ts b/packages/fastify-tanstack/src/routing.ts
new file mode 100644
index 00000000..808eaf2a
--- /dev/null
+++ b/packages/fastify-tanstack/src/routing.ts
@@ -0,0 +1,138 @@
+import { readFileSync } from 'node:fs'
+import { join, isAbsolute } from 'node:path'
+import { createHtmlFunction } from './rendering.ts'
+import Youch from 'youch'
+import type { FastifyInstance, FastifyReply, FastifyRequest, RouteOptions } from 'fastify'
+import type { RuntimeConfig, RouteDefinition, ClientEntries, ClientModule } from '@fastify/vite'
+
+interface TanStackRoute {
+ path: string
+ name?: string
+ configure?: (scope: FastifyInstance) => void | Promise
+}
+
+interface TanStackClient extends ClientModule {
+ createAppRouter:
+ | ((...args: unknown[]) => unknown)
+ | Promise<{ createAppRouter?: unknown; default?: unknown }>
+ getRoutes?:
+ | ((...args: unknown[]) => TanStackRoute[])
+ | Promise<{ getRoutes?: unknown; default?: unknown }>
+ routes?: TanStackRoute[]
+}
+
+export async function prepareClient(entries: ClientEntries, _: FastifyInstance) {
+ const client = entries.ssr as TanStackClient | undefined
+ if (!client) return null
+
+ if (typeof client.createAppRouter !== 'function') {
+ if (client.createAppRouter instanceof Promise) {
+ const mod = (await client.createAppRouter) as Record
+ client.createAppRouter = (mod.createAppRouter ??
+ mod.default) as TanStackClient['createAppRouter']
+ }
+ }
+ if (typeof client.getRoutes !== 'function') {
+ if (client.getRoutes instanceof Promise) {
+ const mod = (await client.getRoutes) as Record
+ client.getRoutes = (mod.getRoutes ?? mod.default) as TanStackClient['getRoutes']
+ }
+ }
+
+ if (typeof client.getRoutes === 'function') {
+ const routes = client.getRoutes()
+ // Without a catch-all, unmatched URLs hit Fastify's default 404
+ // instead of TanStack Router's notFoundComponent
+ const hasCatchAll = routes.some((r) => r.path === '/$' || r.path === '*' || r.path === '/*')
+ if (!hasCatchAll) {
+ routes.push({ path: '/*' })
+ }
+ return { ...client, routes }
+ }
+
+ return client
+}
+
+export function createRouteHandler() {
+ return (_: FastifyRequest, reply: FastifyReply) => (reply as any).html()
+}
+
+export function createErrorHandler(_: unknown, __: FastifyInstance, config: RuntimeConfig) {
+ return async (error: Error, req: FastifyRequest, reply: FastifyReply) => {
+ req.log.error(error)
+ if (config.dev) {
+ const youch = new Youch(error, req.raw)
+ reply.code(500)
+ reply.type('text/html')
+ reply.send(await youch.toHTML())
+ return reply
+ }
+ reply.code(500)
+ reply.send('')
+ return reply
+ }
+}
+
+export async function createRoute(
+ {
+ handler,
+ errorHandler,
+ route,
+ }: {
+ handler?: RouteOptions['handler']
+ errorHandler?: RouteOptions['errorHandler']
+ route?: RouteDefinition
+ },
+ scope: FastifyInstance,
+ config: RuntimeConfig,
+) {
+ if (!route?.path || !handler) return
+ if (route.configure) {
+ await route.configure(scope)
+ }
+
+ let routePath = route.path.replace(/\$\$/, '*').replace(/\$([a-zA-Z_]\w*)/g, ':$1')
+ if (routePath !== '/') {
+ routePath = routePath.replace(/\/$/, '')
+ }
+
+ let routeHandler = handler
+ if (!config.dev && route.path !== '/*') {
+ const htmlPath = resolveRouteHtmlPath(route, config)
+ if (htmlPath) {
+ const htmlSource = readFileSync(htmlPath, 'utf8')
+ const htmlFunction = await createHtmlFunction(htmlSource, scope, config)
+ routeHandler = (_: FastifyRequest, reply: FastifyReply) => htmlFunction.call(reply)
+ }
+ }
+
+ scope.route({
+ url: routePath,
+ method: 'GET',
+ errorHandler,
+ handler: routeHandler,
+ })
+}
+
+function resolveRouteHtmlPath(route: RouteDefinition, config: RuntimeConfig): string | null {
+ let distDir = config.viteConfig.build.outDir
+ if (!isAbsolute(distDir)) {
+ distDir = join(config.viteConfig.root, distDir)
+ }
+ const candidates: string[] = []
+ if (route.name) {
+ candidates.push(join(distDir, 'html', `${String(route.name)}.html`))
+ }
+ const derived = (route.path ?? '').replace(/^\//, '') || 'index'
+ candidates.push(join(distDir, 'html', `${derived}.html`))
+
+ for (const candidate of candidates) {
+ try {
+ readFileSync(candidate, 'utf8')
+ return candidate
+ } catch {
+ // not found, try next
+ }
+ }
+ return null
+}
diff --git a/packages/fastify-tanstack/src/server.ts b/packages/fastify-tanstack/src/server.ts
new file mode 100644
index 00000000..c699aeb7
--- /dev/null
+++ b/packages/fastify-tanstack/src/server.ts
@@ -0,0 +1,20 @@
+import { Server as TlsServer } from 'node:tls'
+import type { FastifyInstance } from 'fastify'
+
+export function prepareServer(server: FastifyInstance) {
+ let url: string
+ server.decorate('serverURL', { getter: () => url })
+ server.addHook('onListen', () => {
+ const { port, address, family } = server.server.address() as {
+ port: number
+ address: string
+ family: string
+ }
+ const protocol = server.server instanceof TlsServer ? 'https' : 'http'
+ if (family === 'IPv6') {
+ url = `${protocol}://[${address}]:${port}`
+ } else {
+ url = `${protocol}://${address}:${port}`
+ }
+ })
+}
diff --git a/packages/fastify-tanstack/tsconfig.json b/packages/fastify-tanstack/tsconfig.json
new file mode 100644
index 00000000..65d347a3
--- /dev/null
+++ b/packages/fastify-tanstack/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": false,
+ "jsx": "react-jsx",
+ "lib": ["ES2020"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "noImplicitAny": true,
+ "outDir": "dist",
+ "rewriteRelativeImportExtensions": true,
+ "rootDir": "src",
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "target": "esnext"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**/*.test.*"]
+}
diff --git a/packages/fastify-tanstack/virtual/context.ts b/packages/fastify-tanstack/virtual/context.ts
new file mode 100644
index 00000000..5d26fd60
--- /dev/null
+++ b/packages/fastify-tanstack/virtual/context.ts
@@ -0,0 +1,3 @@
+export default function getRouterContext() {
+ return {}
+}
diff --git a/packages/fastify-tanstack/virtual/create.tsx b/packages/fastify-tanstack/virtual/create.tsx
new file mode 100644
index 00000000..766e0ff8
--- /dev/null
+++ b/packages/fastify-tanstack/virtual/create.tsx
@@ -0,0 +1,8 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from '$app/routes.ts'
+
+export function createAppRouter() {
+ return createRouter({
+ routeTree,
+ })
+}
diff --git a/packages/fastify-tanstack/virtual/index.ts b/packages/fastify-tanstack/virtual/index.ts
new file mode 100644
index 00000000..fd0ce99d
--- /dev/null
+++ b/packages/fastify-tanstack/virtual/index.ts
@@ -0,0 +1,10 @@
+import { createAppRouter } from '$app/create.tsx'
+
+export { createAppRouter }
+
+export function getRoutes() {
+ const router = createAppRouter()
+ return Object.entries(router.routesByPath).map(([_path, route]) => ({
+ path: route.fullPath,
+ }))
+}
diff --git a/packages/fastify-tanstack/virtual/mount.ts b/packages/fastify-tanstack/virtual/mount.ts
new file mode 100644
index 00000000..bda4e79d
--- /dev/null
+++ b/packages/fastify-tanstack/virtual/mount.ts
@@ -0,0 +1,8 @@
+import { createElement } from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { RouterClient } from '@tanstack/react-router/ssr/client'
+import { createAppRouter } from '$app/create.tsx'
+
+const router = createAppRouter()
+
+hydrateRoot(document.getElementById('root')!, createElement(RouterClient, { router }))
diff --git a/packages/fastify-tanstack/virtual/routes.ts b/packages/fastify-tanstack/virtual/routes.ts
new file mode 100644
index 00000000..f034013c
--- /dev/null
+++ b/packages/fastify-tanstack/virtual/routes.ts
@@ -0,0 +1,21 @@
+import { createElement, Fragment } from 'react'
+import { createRootRoute, createRoute, Outlet, Scripts } from '@tanstack/react-router'
+
+const rootRoute = createRootRoute({
+ component: function Root() {
+ return createElement(Fragment, null, createElement(Outlet), createElement(Scripts))
+ },
+})
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: function Index() {
+ return createElement('div', null, 'Hello from TanStack Router SSR')
+ },
+})
+
+const routeTree = rootRoute.addChildren([indexRoute])
+
+export { routeTree }
+export default routeTree
diff --git a/packages/fastify-vite/src/index.test-d.ts b/packages/fastify-vite/src/index.test-d.ts
index e5678abb..55b5f662 100644
--- a/packages/fastify-vite/src/index.test-d.ts
+++ b/packages/fastify-vite/src/index.test-d.ts
@@ -12,8 +12,8 @@ const options = {
const resolvedRoutes = await routesPromise
return { context, routes: resolvedRoutes, ...others }
},
- createHtmlFunction(source) {
- return ({ routes, context, body }) => Promise.resolve()
+ async createHtmlFunction(source) {
+ return (ctx) => Promise.resolve()
},
createRenderFunction({ create, routes, createApp }) {
return Promise.resolve((req) => {
@@ -21,17 +21,17 @@ const options = {
})
},
renderer: {
- createErrorHandler(client, scope, config) {
+ createErrorHandler(args, scope, config) {
return (error: Error, req: any, reply: any) => {}
},
- createRoute({ client }, scope, config) {},
- createRouteHandler(client, scope, config) {
+ createRoute(args, scope, config) {},
+ createRouteHandler(args, scope, config) {
return (req, res) => {
return Promise.resolve()
}
},
- prepareClient(clientModule, scope, config) {
- return Promise.resolve(clientModule)
+ prepareClient(entries, scope, config) {
+ return Promise.resolve(entries.ssr ?? null)
},
},
} satisfies FastifyViteOptions
diff --git a/packages/fastify-vite/src/index.ts b/packages/fastify-vite/src/index.ts
index ce1ceec1..1401f214 100644
--- a/packages/fastify-vite/src/index.ts
+++ b/packages/fastify-vite/src/index.ts
@@ -22,6 +22,8 @@ import type { RouteDefinition } from './types/route.ts'
// Re-export types for consumers
export type {
+ ClientEntries,
+ ClientModule,
DevRuntimeConfig,
FastifyViteOptions,
ProdRuntimeConfig,
diff --git a/packages/fastify-vite/src/types/renderer.ts b/packages/fastify-vite/src/types/renderer.ts
index 7f7ecd99..3b9c7b16 100644
--- a/packages/fastify-vite/src/types/renderer.ts
+++ b/packages/fastify-vite/src/types/renderer.ts
@@ -1,85 +1,39 @@
import type { FastifyInstance, RouteHandlerMethod, RouteOptions } from 'fastify'
-
-/** Helper type to loosen strict object types by adding index signature */
-export type Loosen = T & Record
-
-/** Route properties available in renderer context */
-export type RouteType = Partial<{
- server: unknown
- req: unknown
- reply: unknown
- head: unknown
- state: unknown
- data: Record
- firstRender: boolean
- layout: unknown
- getMeta: unknown
- getData: unknown
- onEnter: unknown
- streaming: unknown
- clientOnly: unknown
- serverOnly: unknown
-}>
-
-/** Renderer context passed to html/render functions */
-export type Ctx = Loosen<{
- routes: Array
- context: unknown
- body: unknown
- stream: unknown
- data: unknown
-}>
+import type { ClientEntries, ClientModule } from './client.ts'
+import type { ClientRouteArgs, CreateRouteArgs } from './route.ts'
/** Renderer function definitions */
export interface RendererFunctions {
createHtmlTemplateFunction(source: string): unknown
createHtmlFunction(
source: string,
- scope?: unknown,
+ scope?: FastifyInstance,
config?: unknown,
- ): (ctx: Ctx) => Promise
+ ): Promise<(...args: unknown[]) => unknown>
createRenderFunction(
- args: Loosen<{
- routes?: Array
- create?: (arg0: Record) => unknown
- createApp: unknown
- }>,
- ): Promise<
- (
- server: unknown,
- req: unknown,
- reply: unknown,
- ) =>
- | (Ctx | { element: string; hydration?: string })
- | Promise
- >
+ client: ClientModule,
+ scope?: FastifyInstance,
+ config?: unknown,
+ ): Promise<(...args: unknown[]) => unknown>
}
/** Renderer option interface for custom renderers */
-export interface RendererOption<
- CM = string | Record | unknown,
- C = unknown,
-> extends RendererFunctions {
- clientModule: CM
+export interface RendererOption extends RendererFunctions {
+ clientModule: string
createErrorHandler(
- client: C,
+ args: ClientRouteArgs,
scope: FastifyInstance,
config?: unknown,
): NonNullable | Promise>
- createRoute(
- args: Loosen<{
- client?: C
- handler?: RouteHandlerMethod
- errorHandler: NonNullable
- route?: RouteType
- }>,
- scope: FastifyInstance,
- config: unknown,
- ): void | Promise
+ createRoute(args: CreateRouteArgs, scope: FastifyInstance, config: unknown): void | Promise
createRouteHandler(
- client: C,
+ args: ClientRouteArgs,
scope: FastifyInstance,
config?: unknown,
): RouteHandlerMethod | Promise
- prepareClient(clientModule: CM, scope?: FastifyInstance, config?: unknown): Promise
+ prepareClient(
+ entries: ClientEntries,
+ scope?: FastifyInstance,
+ config?: unknown,
+ ): Promise
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 682e718d..f18e5338 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -404,6 +404,46 @@ importers:
specifier: 'catalog:'
version: 8.0.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.4.2)(tsx@4.21.0)(yaml@2.7.1)
+ examples/tanstack-vanilla-ts:
+ dependencies:
+ '@fastify/tanstack':
+ specifier: workspace:^
+ version: link:../../packages/fastify-tanstack
+ '@fastify/vite':
+ specifier: workspace:^
+ version: link:../../packages/fastify-vite
+ '@tanstack/react-router':
+ specifier: ^1.0.0
+ version: 1.168.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ fastify:
+ specifier: 'catalog:'
+ version: 5.8.4
+ react:
+ specifier: catalog:react
+ version: 19.2.4
+ react-dom:
+ specifier: catalog:react
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@types/node':
+ specifier: ^24.12.2
+ version: 24.12.2
+ '@types/react':
+ specifier: ^19.1.2
+ version: 19.1.2
+ '@types/react-dom':
+ specifier: ^19.1.2
+ version: 19.1.2(@types/react@19.1.2)
+ tsx:
+ specifier: 'catalog:'
+ version: 4.21.0
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.2
+ vite:
+ specifier: 'catalog:'
+ version: 8.0.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.4.2)(tsx@4.21.0)(yaml@2.7.1)
+
examples/vue-hydration:
dependencies:
'@fastify/one-line-logger':
@@ -713,6 +753,55 @@ importers:
specifier: ^3.3.4
version: 3.3.4
+ packages/fastify-tanstack:
+ dependencies:
+ '@fastify/vite':
+ specifier: workspace:^
+ version: link:../fastify-vite
+ '@tanstack/history':
+ specifier: ^1.0.0
+ version: 1.161.6
+ '@tanstack/react-router':
+ specifier: ^1.0.0
+ version: 1.168.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/router-core':
+ specifier: ^1.0.0
+ version: 1.168.14
+ mlly:
+ specifier: ^1.7.4
+ version: 1.8.0
+ react:
+ specifier: catalog:react
+ version: 19.2.4
+ react-dom:
+ specifier: catalog:react
+ version: 19.2.4(react@19.2.4)
+ youch:
+ specifier: ^3.3.4
+ version: 3.3.4
+ devDependencies:
+ '@types/node':
+ specifier: ^24.12.2
+ version: 24.12.2
+ '@types/react':
+ specifier: ^19.1.2
+ version: 19.1.2
+ '@types/react-dom':
+ specifier: ^19.1.2
+ version: 19.1.2(@types/react@19.1.2)
+ fastify:
+ specifier: 'catalog:'
+ version: 5.8.4
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.2
+ vite:
+ specifier: 'catalog:'
+ version: 8.0.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.4.2)(tsx@4.21.0)(yaml@2.7.1)
+ vitest:
+ specifier: 'catalog:'
+ version: 4.1.4(@types/node@24.12.2)(jsdom@23.2.0)(vite@8.0.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.4.2)(tsx@4.21.0)(yaml@2.7.1))
+
packages/fastify-vite:
dependencies:
'@fastify/deepmerge':
@@ -2918,6 +3007,31 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6
+ '@tanstack/history@1.161.6':
+ resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/react-router@1.168.19':
+ resolution: {integrity: sha512-0NCuwMPRlEpffDIF7OTSe3g4d8U93WsHxMi15YLJxjmNbng2of50wx+8UnT8IxKLbSdpFHSEDNTi4qnNyWn/Kw==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ react: '>=18.0.0 || >=19.0.0'
+ react-dom: '>=18.0.0 || >=19.0.0'
+
+ '@tanstack/react-store@0.9.3':
+ resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tanstack/router-core@1.168.14':
+ resolution: {integrity: sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA==}
+ engines: {node: '>=20.19'}
+ hasBin: true
+
+ '@tanstack/store@0.9.3':
+ resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
+
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -3062,9 +3176,6 @@ packages:
'@types/node@22.14.0':
resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==}
- '@types/node@22.19.17':
- resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
-
'@types/node@24.12.2':
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
@@ -3575,6 +3686,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-es@3.1.1:
+ resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
+
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
@@ -4412,6 +4526,10 @@ packages:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
+ isbot@5.1.38:
+ resolution: {integrity: sha512-Cus2702JamTNMEY4zTP+TShgq/3qzjvGcBC4XMOV45BLaxD4iUFENkqu7ZhFeSzwNsCSZLjnGlihDQznnpnEEA==}
+ engines: {node: '>=18'}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -5435,6 +5553,16 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ seroval-plugins@1.5.2:
+ resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ seroval: ^1.0
+
+ seroval@1.5.2:
+ resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==}
+ engines: {node: '>=10'}
+
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
@@ -5728,6 +5856,11 @@ packages:
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -7645,6 +7778,33 @@ snapshots:
tailwindcss: 4.1.2
vite: 8.0.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.4.2)(tsx@4.21.0)(yaml@2.7.1)
+ '@tanstack/history@1.161.6': {}
+
+ '@tanstack/react-router@1.168.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@tanstack/history': 1.161.6
+ '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/router-core': 1.168.14
+ isbot: 5.1.38
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@tanstack/store': 0.9.3
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ use-sync-external-store: 1.6.0(react@19.2.4)
+
+ '@tanstack/router-core@1.168.14':
+ dependencies:
+ '@tanstack/history': 1.161.6
+ cookie-es: 3.1.1
+ seroval: 1.5.2
+ seroval-plugins: 1.5.2(seroval@1.5.2)
+
+ '@tanstack/store@0.9.3': {}
+
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -7781,7 +7941,7 @@ snapshots:
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
- '@types/node': 22.19.17
+ '@types/node': 24.12.2
'@types/geojson@7946.0.16': {}
@@ -7793,11 +7953,11 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
- '@types/node': 22.19.17
+ '@types/node': 24.12.2
'@types/klaw@3.0.7':
dependencies:
- '@types/node': 22.19.17
+ '@types/node': 24.12.2
'@types/linkify-it@5.0.0': {}
@@ -7818,10 +7978,6 @@ snapshots:
dependencies:
undici-types: 6.21.0
- '@types/node@22.19.17':
- dependencies:
- undici-types: 6.21.0
-
'@types/node@24.12.2':
dependencies:
undici-types: 7.16.0
@@ -8445,6 +8601,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie-es@3.1.1: {}
+
cookie@0.7.2: {}
cookie@1.1.1: {}
@@ -9426,6 +9584,8 @@ snapshots:
is-windows@1.0.2: {}
+ isbot@5.1.38: {}
+
isexe@2.0.0: {}
jiti@2.4.2: {}
@@ -9855,7 +10015,7 @@ snapshots:
mlly@1.8.0:
dependencies:
- acorn: 8.15.0
+ acorn: 8.16.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.1
@@ -10098,7 +10258,7 @@ snapshots:
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
- mlly: 1.7.4
+ mlly: 1.8.0
pathe: 2.0.3
points-on-curve@0.2.0: {}
@@ -10565,6 +10725,12 @@ snapshots:
semver@7.7.4: {}
+ seroval-plugins@1.5.2(seroval@1.5.2):
+ dependencies:
+ seroval: 1.5.2
+
+ seroval@1.5.2: {}
+
set-cookie-parser@2.7.2: {}
setprototypeof@1.2.0: {}
@@ -10835,6 +11001,10 @@ snapshots:
requires-port: 1.0.0
optional: true
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
util-deprecate@1.0.2: {}
uuid@11.1.0: {}