From 3275f8d71d598d570c0458fffee6e27316561dd5 Mon Sep 17 00:00:00 2001
From: jamcalli <48490664+jamcalli@users.noreply.github.com>
Date: Mon, 13 Apr 2026 16:53:07 -0700
Subject: [PATCH 1/5] Add @fastify/tanstack renderer for TanStack Router SSR
---
examples/tanstack-vanilla-ts/package.json | 30 +++
.../tanstack-vanilla-ts/src/client/create.tsx | 19 ++
.../tanstack-vanilla-ts/src/client/index.html | 12 +
.../tanstack-vanilla-ts/src/client/routes.ts | 79 +++++++
.../src/client/tsconfig.json | 16 ++
.../tanstack-vanilla-ts/src/server.test.ts | 12 +
examples/tanstack-vanilla-ts/src/server.ts | 44 ++++
examples/tanstack-vanilla-ts/tsconfig.json | 27 +++
examples/tanstack-vanilla-ts/vite.config.js | 11 +
packages/fastify-tanstack/package.json | 69 ++++++
packages/fastify-tanstack/src/index.ts | 7 +
packages/fastify-tanstack/src/plugin/index.ts | 93 ++++++++
.../fastify-tanstack/src/plugin/preload.ts | 102 +++++++++
.../fastify-tanstack/src/plugin/virtual.ts | 94 ++++++++
packages/fastify-tanstack/src/rendering.ts | 179 +++++++++++++++
packages/fastify-tanstack/src/routing.ts | 143 ++++++++++++
packages/fastify-tanstack/src/server.ts | 20 ++
packages/fastify-tanstack/tsconfig.json | 21 ++
packages/fastify-tanstack/virtual/context.ts | 3 +
packages/fastify-tanstack/virtual/create.tsx | 8 +
packages/fastify-tanstack/virtual/index.ts | 10 +
packages/fastify-tanstack/virtual/mount.ts | 8 +
packages/fastify-tanstack/virtual/root.tsx | 3 +
packages/fastify-tanstack/virtual/routes.ts | 21 ++
packages/fastify-vite/src/index.test-d.ts | 4 +-
packages/fastify-vite/src/types/renderer.ts | 21 +-
pnpm-lock.yaml | 205 +++++++++++++++++-
27 files changed, 1230 insertions(+), 31 deletions(-)
create mode 100644 examples/tanstack-vanilla-ts/package.json
create mode 100644 examples/tanstack-vanilla-ts/src/client/create.tsx
create mode 100644 examples/tanstack-vanilla-ts/src/client/index.html
create mode 100644 examples/tanstack-vanilla-ts/src/client/routes.ts
create mode 100644 examples/tanstack-vanilla-ts/src/client/tsconfig.json
create mode 100644 examples/tanstack-vanilla-ts/src/server.test.ts
create mode 100644 examples/tanstack-vanilla-ts/src/server.ts
create mode 100644 examples/tanstack-vanilla-ts/tsconfig.json
create mode 100644 examples/tanstack-vanilla-ts/vite.config.js
create mode 100644 packages/fastify-tanstack/package.json
create mode 100644 packages/fastify-tanstack/src/index.ts
create mode 100644 packages/fastify-tanstack/src/plugin/index.ts
create mode 100644 packages/fastify-tanstack/src/plugin/preload.ts
create mode 100644 packages/fastify-tanstack/src/plugin/virtual.ts
create mode 100644 packages/fastify-tanstack/src/rendering.ts
create mode 100644 packages/fastify-tanstack/src/routing.ts
create mode 100644 packages/fastify-tanstack/src/server.ts
create mode 100644 packages/fastify-tanstack/tsconfig.json
create mode 100644 packages/fastify-tanstack/virtual/context.ts
create mode 100644 packages/fastify-tanstack/virtual/create.tsx
create mode 100644 packages/fastify-tanstack/virtual/index.ts
create mode 100644 packages/fastify-tanstack/virtual/mount.ts
create mode 100644 packages/fastify-tanstack/virtual/root.tsx
create mode 100644 packages/fastify-tanstack/virtual/routes.ts
diff --git a/examples/tanstack-vanilla-ts/package.json b/examples/tanstack-vanilla-ts/package.json
new file mode 100644
index 00000000..48e3aadd
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/package.json
@@ -0,0 +1,30 @@
+{
+ "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/cookie": "^11.0.2",
+ "@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..f4d32c56
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/client/create.tsx
@@ -0,0 +1,19 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routes.ts'
+
+interface User {
+ name: string
+}
+
+export interface RouterContext {
+ user: User | null
+}
+
+export function createAppRouter(req?: { user?: User | null }) {
+ return createRouter({
+ routeTree,
+ context: {
+ user: req?.user ?? null,
+ },
+ })
+}
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..2ce1f01d
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/client/routes.ts
@@ -0,0 +1,79 @@
+import { createElement, Fragment } from 'react'
+import {
+ createRootRouteWithContext,
+ createRoute,
+ Outlet,
+ Scripts,
+ redirect,
+ useLoaderData,
+} from '@tanstack/react-router'
+import type { RouterContext } from './create.tsx'
+
+const rootRoute = createRootRouteWithContext()({
+ component: function Root() {
+ return createElement(Fragment, null, createElement(Outlet), createElement(Scripts))
+ },
+})
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ beforeLoad: ({ context }) => {
+ if (!context.user) {
+ throw redirect({ to: '/login' })
+ }
+ },
+ loader: ({ context }) => {
+ return { user: context.user! }
+ },
+ component: function Index() {
+ const { user } = useLoaderData({ from: '/' })
+ return createElement(
+ 'div',
+ null,
+ createElement('h1', null, `Hello ${user.name} from TanStack Router SSR!`),
+ createElement(
+ 'form',
+ {
+ onSubmit: async (e: { preventDefault: () => void }) => {
+ e.preventDefault()
+ await fetch('/api/logout', { method: 'POST' })
+ window.location.href = '/login'
+ },
+ },
+ createElement('button', { type: 'submit' }, 'Logout'),
+ ),
+ )
+ },
+})
+
+const loginRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/login',
+ component: function Login() {
+ return createElement(
+ 'div',
+ null,
+ createElement('h1', null, 'Login'),
+ createElement(
+ 'form',
+ {
+ onSubmit: async (e: { preventDefault: () => void; target: any }) => {
+ e.preventDefault()
+ const form = new FormData(e.target)
+ await fetch('/api/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username: form.get('username') }),
+ })
+ window.location.href = '/'
+ },
+ },
+ createElement('input', { name: 'username', placeholder: 'Username', required: true }),
+ createElement('button', { type: 'submit' }, 'Sign in'),
+ ),
+ )
+ },
+})
+
+export const routeTree = rootRoute.addChildren([indexRoute, loginRoute])
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..995783ab
--- /dev/null
+++ b/examples/tanstack-vanilla-ts/src/server.ts
@@ -0,0 +1,44 @@
+import { resolve } from 'node:path'
+import Fastify from 'fastify'
+import FastifyVite from '@fastify/vite'
+import FastifyCookie from '@fastify/cookie'
+import * as renderer from '@fastify/tanstack'
+
+export async function main(dev?: boolean) {
+ const server = Fastify()
+
+ await server.register(FastifyCookie)
+
+ // Simulate auth: parse the "user" cookie on every request
+ server.addHook('onRequest', async (req) => {
+ const username = req.cookies.user
+ ;(req as any).user = username ? { name: username } : null
+ })
+
+ // Login endpoint sets the cookie
+ server.post('/api/login', async (req, reply) => {
+ const { username } = req.body as { username: string }
+ reply.setCookie('user', username, { path: '/', httpOnly: true })
+ return { ok: true }
+ })
+
+ // Logout endpoint clears the cookie
+ server.post('/api/logout', async (req, reply) => {
+ reply.clearCookie('user', { path: '/' })
+ return { ok: true }
+ })
+
+ 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..92ca49e6
--- /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', 'root.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..3a7d5622
--- /dev/null
+++ b/packages/fastify-tanstack/src/routing.ts
@@ -0,0 +1,143 @@
+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 } from '@fastify/vite'
+
+interface ClientEntries {
+ ssr?: unknown
+ [key: string]: unknown
+}
+
+interface TanStackRoute {
+ path: string
+ name?: string
+ configure?: (scope: FastifyInstance) => void | Promise
+}
+
+interface TanStackClient {
+ 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/root.tsx b/packages/fastify-tanstack/virtual/root.tsx
new file mode 100644
index 00000000..c33947a0
--- /dev/null
+++ b/packages/fastify-tanstack/virtual/root.tsx
@@ -0,0 +1,3 @@
+// Override point for wrapping the router in additional providers.
+// Not imported by the SSR entry chain - exists for user customization only.
+export {}
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..d2d9a734 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) => {
diff --git a/packages/fastify-vite/src/types/renderer.ts b/packages/fastify-vite/src/types/renderer.ts
index 7f7ecd99..edb8d32e 100644
--- a/packages/fastify-vite/src/types/renderer.ts
+++ b/packages/fastify-vite/src/types/renderer.ts
@@ -1,4 +1,5 @@
import type { FastifyInstance, RouteHandlerMethod, RouteOptions } from 'fastify'
+import type { RouteDefinition } from './route.ts'
/** Helper type to loosen strict object types by adding index signature */
export type Loosen = T & Record
@@ -37,22 +38,8 @@ export interface RendererFunctions {
source: string,
scope?: unknown,
config?: unknown,
- ): (ctx: Ctx) => Promise
- createRenderFunction(
- args: Loosen<{
- routes?: Array
- create?: (arg0: Record) => unknown
- createApp: unknown
- }>,
- ): Promise<
- (
- server: unknown,
- req: unknown,
- reply: unknown,
- ) =>
- | (Ctx | { element: string; hydration?: string })
- | Promise
- >
+ ): Promise<(ctx?: Ctx) => unknown>
+ createRenderFunction(client: Record): Promise<(...args: unknown[]) => unknown>
}
/** Renderer option interface for custom renderers */
@@ -71,7 +58,7 @@ export interface RendererOption<
client?: C
handler?: RouteHandlerMethod
errorHandler: NonNullable
- route?: RouteType
+ route?: RouteDefinition
}>,
scope: FastifyInstance,
config: unknown,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 682e718d..c91cbedb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -404,6 +404,49 @@ 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/cookie':
+ specifier: ^11.0.2
+ version: 11.0.2
+ '@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 +756,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':
@@ -2167,6 +2259,9 @@ packages:
'@fastify/ajv-compiler@4.0.5':
resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
+ '@fastify/cookie@11.0.2':
+ resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
+
'@fastify/deepmerge@3.2.1':
resolution: {integrity: sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==}
@@ -2918,6 +3013,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 +3182,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 +3692,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 +4532,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 +5559,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 +5862,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==}
@@ -6975,6 +7114,11 @@ snapshots:
ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.1.0
+ '@fastify/cookie@11.0.2':
+ dependencies:
+ cookie: 1.1.1
+ fastify-plugin: 5.1.0
+
'@fastify/deepmerge@3.2.1': {}
'@fastify/error@4.2.0': {}
@@ -7645,6 +7789,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 +7952,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 +7964,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 +7989,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 +8612,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie-es@3.1.1: {}
+
cookie@0.7.2: {}
cookie@1.1.1: {}
@@ -9426,6 +9595,8 @@ snapshots:
is-windows@1.0.2: {}
+ isbot@5.1.38: {}
+
isexe@2.0.0: {}
jiti@2.4.2: {}
@@ -9855,7 +10026,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 +10269,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 +10736,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 +11012,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: {}
From 164378f366240d73e6ae0d7122a71b855bae3fce Mon Sep 17 00:00:00 2001
From: jamcalli <48490664+jamcalli@users.noreply.github.com>
Date: Tue, 14 Apr 2026 15:11:38 -0700
Subject: [PATCH 2/5] Align RendererOption types with runtime signatures and
reuse shared type interfaces
---
packages/fastify-vite/src/index.test-d.ts | 10 +--
packages/fastify-vite/src/types/renderer.ts | 71 ++++++---------------
2 files changed, 24 insertions(+), 57 deletions(-)
diff --git a/packages/fastify-vite/src/index.test-d.ts b/packages/fastify-vite/src/index.test-d.ts
index d2d9a734..55b5f662 100644
--- a/packages/fastify-vite/src/index.test-d.ts
+++ b/packages/fastify-vite/src/index.test-d.ts
@@ -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/types/renderer.ts b/packages/fastify-vite/src/types/renderer.ts
index edb8d32e..3b9c7b16 100644
--- a/packages/fastify-vite/src/types/renderer.ts
+++ b/packages/fastify-vite/src/types/renderer.ts
@@ -1,72 +1,39 @@
import type { FastifyInstance, RouteHandlerMethod, RouteOptions } from 'fastify'
-import type { RouteDefinition } from './route.ts'
-
-/** 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,
- ): Promise<(ctx?: Ctx) => unknown>
- createRenderFunction(client: Record): Promise<(...args: unknown[]) => unknown>
+ ): Promise<(...args: unknown[]) => unknown>
+ createRenderFunction(
+ 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?: RouteDefinition
- }>,
- 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
}
From 66168e3cbfdd27a7d88d6a201d24dc0e2a19b989 Mon Sep 17 00:00:00 2001
From: jamcalli <48490664+jamcalli@users.noreply.github.com>
Date: Tue, 14 Apr 2026 15:21:16 -0700
Subject: [PATCH 3/5] Remove unused root.tsx virtual module from
@fastify/tanstack
---
packages/fastify-tanstack/src/plugin/virtual.ts | 2 +-
packages/fastify-tanstack/virtual/root.tsx | 3 ---
2 files changed, 1 insertion(+), 4 deletions(-)
delete mode 100644 packages/fastify-tanstack/virtual/root.tsx
diff --git a/packages/fastify-tanstack/src/plugin/virtual.ts b/packages/fastify-tanstack/src/plugin/virtual.ts
index 92ca49e6..90e89ed6 100644
--- a/packages/fastify-tanstack/src/plugin/virtual.ts
+++ b/packages/fastify-tanstack/src/plugin/virtual.ts
@@ -4,7 +4,7 @@ import { findExports } from 'mlly'
const virtualRoot = resolve(import.meta.dirname, '..', '..', 'virtual')
-const virtualModules = ['mount.ts', 'routes.ts', 'create.tsx', 'root.tsx', 'context.ts', 'index.ts']
+const virtualModules = ['mount.ts', 'routes.ts', 'create.tsx', 'context.ts', 'index.ts']
export const prefix = /^\/?\$app\//
diff --git a/packages/fastify-tanstack/virtual/root.tsx b/packages/fastify-tanstack/virtual/root.tsx
deleted file mode 100644
index c33947a0..00000000
--- a/packages/fastify-tanstack/virtual/root.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-// Override point for wrapping the router in additional providers.
-// Not imported by the SSR entry chain - exists for user customization only.
-export {}
From 4052e262883c00d30d139123cdfae47be66e21a0 Mon Sep 17 00:00:00 2001
From: jamcalli <48490664+jamcalli@users.noreply.github.com>
Date: Wed, 15 Apr 2026 17:01:42 -0700
Subject: [PATCH 4/5] Export ClientEntries and ClientModule types from
@fastify/vite
---
packages/fastify-tanstack/src/routing.ts | 9 ++-------
packages/fastify-vite/src/index.ts | 2 ++
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/packages/fastify-tanstack/src/routing.ts b/packages/fastify-tanstack/src/routing.ts
index 3a7d5622..808eaf2a 100644
--- a/packages/fastify-tanstack/src/routing.ts
+++ b/packages/fastify-tanstack/src/routing.ts
@@ -3,12 +3,7 @@ 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 } from '@fastify/vite'
-
-interface ClientEntries {
- ssr?: unknown
- [key: string]: unknown
-}
+import type { RuntimeConfig, RouteDefinition, ClientEntries, ClientModule } from '@fastify/vite'
interface TanStackRoute {
path: string
@@ -16,7 +11,7 @@ interface TanStackRoute {
configure?: (scope: FastifyInstance) => void | Promise
}
-interface TanStackClient {
+interface TanStackClient extends ClientModule {
createAppRouter:
| ((...args: unknown[]) => unknown)
| Promise<{ createAppRouter?: unknown; default?: unknown }>
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,
From 29c3f805e2141137e03e231f04f76b30e2c037c4 Mon Sep 17 00:00:00 2001
From: jamcalli <48490664+jamcalli@users.noreply.github.com>
Date: Thu, 16 Apr 2026 11:27:02 -0700
Subject: [PATCH 5/5] Simplify tanstack example and build @fastify/tanstack in
CI
---
.github/workflows/integration-tests.yml | 3 +
examples/tanstack-vanilla-ts/package.json | 1 -
.../tanstack-vanilla-ts/src/client/create.tsx | 13 +---
.../tanstack-vanilla-ts/src/client/routes.ts | 69 ++-----------------
examples/tanstack-vanilla-ts/src/server.ts | 22 ------
pnpm-lock.yaml | 11 ---
6 files changed, 8 insertions(+), 111 deletions(-)
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
index 48e3aadd..82974692 100644
--- a/examples/tanstack-vanilla-ts/package.json
+++ b/examples/tanstack-vanilla-ts/package.json
@@ -11,7 +11,6 @@
"test": "tsx --test"
},
"dependencies": {
- "@fastify/cookie": "^11.0.2",
"@fastify/tanstack": "workspace:^",
"@fastify/vite": "workspace:^",
"@tanstack/react-router": "^1.0.0",
diff --git a/examples/tanstack-vanilla-ts/src/client/create.tsx b/examples/tanstack-vanilla-ts/src/client/create.tsx
index f4d32c56..a040484a 100644
--- a/examples/tanstack-vanilla-ts/src/client/create.tsx
+++ b/examples/tanstack-vanilla-ts/src/client/create.tsx
@@ -1,19 +1,8 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routes.ts'
-interface User {
- name: string
-}
-
-export interface RouterContext {
- user: User | null
-}
-
-export function createAppRouter(req?: { user?: User | null }) {
+export function createAppRouter() {
return createRouter({
routeTree,
- context: {
- user: req?.user ?? null,
- },
})
}
diff --git a/examples/tanstack-vanilla-ts/src/client/routes.ts b/examples/tanstack-vanilla-ts/src/client/routes.ts
index 2ce1f01d..dda788a4 100644
--- a/examples/tanstack-vanilla-ts/src/client/routes.ts
+++ b/examples/tanstack-vanilla-ts/src/client/routes.ts
@@ -1,15 +1,7 @@
import { createElement, Fragment } from 'react'
-import {
- createRootRouteWithContext,
- createRoute,
- Outlet,
- Scripts,
- redirect,
- useLoaderData,
-} from '@tanstack/react-router'
-import type { RouterContext } from './create.tsx'
+import { createRootRoute, createRoute, Outlet, Scripts } from '@tanstack/react-router'
-const rootRoute = createRootRouteWithContext()({
+const rootRoute = createRootRoute({
component: function Root() {
return createElement(Fragment, null, createElement(Outlet), createElement(Scripts))
},
@@ -18,62 +10,9 @@ const rootRoute = createRootRouteWithContext()({
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
- beforeLoad: ({ context }) => {
- if (!context.user) {
- throw redirect({ to: '/login' })
- }
- },
- loader: ({ context }) => {
- return { user: context.user! }
- },
component: function Index() {
- const { user } = useLoaderData({ from: '/' })
- return createElement(
- 'div',
- null,
- createElement('h1', null, `Hello ${user.name} from TanStack Router SSR!`),
- createElement(
- 'form',
- {
- onSubmit: async (e: { preventDefault: () => void }) => {
- e.preventDefault()
- await fetch('/api/logout', { method: 'POST' })
- window.location.href = '/login'
- },
- },
- createElement('button', { type: 'submit' }, 'Logout'),
- ),
- )
- },
-})
-
-const loginRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: '/login',
- component: function Login() {
- return createElement(
- 'div',
- null,
- createElement('h1', null, 'Login'),
- createElement(
- 'form',
- {
- onSubmit: async (e: { preventDefault: () => void; target: any }) => {
- e.preventDefault()
- const form = new FormData(e.target)
- await fetch('/api/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username: form.get('username') }),
- })
- window.location.href = '/'
- },
- },
- createElement('input', { name: 'username', placeholder: 'Username', required: true }),
- createElement('button', { type: 'submit' }, 'Sign in'),
- ),
- )
+ return createElement('h1', null, 'Hello from TanStack Router SSR!')
},
})
-export const routeTree = rootRoute.addChildren([indexRoute, loginRoute])
+export const routeTree = rootRoute.addChildren([indexRoute])
diff --git a/examples/tanstack-vanilla-ts/src/server.ts b/examples/tanstack-vanilla-ts/src/server.ts
index 995783ab..284907a0 100644
--- a/examples/tanstack-vanilla-ts/src/server.ts
+++ b/examples/tanstack-vanilla-ts/src/server.ts
@@ -1,33 +1,11 @@
import { resolve } from 'node:path'
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'
-import FastifyCookie from '@fastify/cookie'
import * as renderer from '@fastify/tanstack'
export async function main(dev?: boolean) {
const server = Fastify()
- await server.register(FastifyCookie)
-
- // Simulate auth: parse the "user" cookie on every request
- server.addHook('onRequest', async (req) => {
- const username = req.cookies.user
- ;(req as any).user = username ? { name: username } : null
- })
-
- // Login endpoint sets the cookie
- server.post('/api/login', async (req, reply) => {
- const { username } = req.body as { username: string }
- reply.setCookie('user', username, { path: '/', httpOnly: true })
- return { ok: true }
- })
-
- // Logout endpoint clears the cookie
- server.post('/api/logout', async (req, reply) => {
- reply.clearCookie('user', { path: '/' })
- return { ok: true }
- })
-
await server.register(FastifyVite, {
root: resolve(import.meta.dirname, '..'),
dev: dev || process.argv.includes('--dev'),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c91cbedb..f18e5338 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -406,9 +406,6 @@ importers:
examples/tanstack-vanilla-ts:
dependencies:
- '@fastify/cookie':
- specifier: ^11.0.2
- version: 11.0.2
'@fastify/tanstack':
specifier: workspace:^
version: link:../../packages/fastify-tanstack
@@ -2259,9 +2256,6 @@ packages:
'@fastify/ajv-compiler@4.0.5':
resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
- '@fastify/cookie@11.0.2':
- resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
-
'@fastify/deepmerge@3.2.1':
resolution: {integrity: sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==}
@@ -7114,11 +7108,6 @@ snapshots:
ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.1.0
- '@fastify/cookie@11.0.2':
- dependencies:
- cookie: 1.1.1
- fastify-plugin: 5.1.0
-
'@fastify/deepmerge@3.2.1': {}
'@fastify/error@4.2.0': {}