()
diff --git a/e2e/solid-router/basic-file-based/src/routes/(another-group)/onlyrouteinside.tsx b/e2e/solid-router/basic-file-based/src/routes/(another-group)/onlyrouteinside.tsx
new file mode 100644
index 00000000000..5dfc82fc334
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(another-group)/onlyrouteinside.tsx
@@ -0,0 +1,27 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+const routeApi = getRouteApi('/(another-group)/onlyrouteinside')
+
+export const Route = createFileRoute('/(another-group)/onlyrouteinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: () => {
+ const searchViaHook = useSearch({
+ from: '/(another-group)/onlyrouteinside',
+ })
+ const searchViaRouteHook = Route.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+ return (
+ <>
+ {searchViaHook().hello}
+
+ {searchViaRouteHook().hello}
+
+
+ {searchViaRouteApi().hello}
+
+ >
+ )
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.insidelayout.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.insidelayout.tsx
new file mode 100644
index 00000000000..dd06459a357
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.insidelayout.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+const routeApi = getRouteApi('/(group)/_layout/insidelayout')
+
+export const Route = createFileRoute('/(group)/_layout/insidelayout')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: () => {
+ const searchViaHook = useSearch({ from: '/(group)/_layout/insidelayout' })
+ const searchViaRouteHook = Route.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+ return (
+ <>
+ {searchViaHook().hello}
+
+ {searchViaRouteHook().hello}
+
+
+ {searchViaRouteApi().hello}
+
+ >
+ )
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.tsx
new file mode 100644
index 00000000000..660cfb70e67
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.tsx
@@ -0,0 +1,10 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/(group)/_layout')({
+ component: () => (
+ <>
+ /(group)/_layout!
+
+ >
+ ),
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/inside.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/inside.tsx
new file mode 100644
index 00000000000..76ed0e8a07c
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(group)/inside.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+const routeApi = getRouteApi('/(group)/inside')
+
+export const Route = createFileRoute('/(group)/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: () => {
+ const searchViaHook = useSearch({ from: '/(group)/inside' })
+ const searchViaRouteHook = Route.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+ return (
+ <>
+ {searchViaHook().hello}
+
+ {searchViaRouteHook().hello}
+
+
+ {searchViaRouteApi().hello}
+
+ >
+ )
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.lazy.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.lazy.tsx
new file mode 100644
index 00000000000..a68febeaddf
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.lazy.tsx
@@ -0,0 +1,26 @@
+import {
+ createLazyFileRoute,
+ getRouteApi,
+ useSearch,
+} from '@tanstack/solid-router'
+
+const routeApi = getRouteApi('/(group)/lazyinside')
+
+export const Route = createLazyFileRoute('/(group)/lazyinside')({
+ component: () => {
+ const searchViaHook = useSearch({ from: '/(group)/lazyinside' })
+ const searchViaRouteHook = Route.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+ return (
+ <>
+ {searchViaHook().hello}
+
+ {searchViaRouteHook().hello}
+
+
+ {searchViaRouteApi().hello}
+
+ >
+ )
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.tsx
new file mode 100644
index 00000000000..dcaf7b07270
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/lazyinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/subfolder/inside.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/subfolder/inside.tsx
new file mode 100644
index 00000000000..9e963d170c6
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/(group)/subfolder/inside.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+const routeApi = getRouteApi('/(group)/subfolder/inside')
+
+export const Route = createFileRoute('/(group)/subfolder/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: () => {
+ const searchViaHook = useSearch({ from: '/(group)/subfolder/inside' })
+ const searchViaRouteHook = Route.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+ return (
+ <>
+ {searchViaHook().hello}
+
+ {searchViaRouteHook().hello}
+
+
+ {searchViaRouteApi().hello}
+
+ >
+ )
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/__root.tsx b/e2e/solid-router/basic-file-based/src/routes/__root.tsx
new file mode 100644
index 00000000000..8dca5f31041
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/__root.tsx
@@ -0,0 +1,136 @@
+import {
+ Link,
+ Outlet,
+ createRootRoute,
+ useCanGoBack,
+ useRouter,
+} from '@tanstack/solid-router'
+// // import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ const router = useRouter()
+ const canGoBack = useCanGoBack()
+
+ return (
+ <>
+
+ router.history.back()}
+ class={!canGoBack() ? 'line-through' : undefined}
+ >
+ Back
+ {' '}
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ Only Route Inside Group
+ {' '}
+
+ Inside Group
+ {' '}
+
+ Inside Subfolder Inside Group
+ {' '}
+
+ Inside Group Inside Layout
+ {' '}
+
+ Lazy Inside Group
+ {' '}
+
+ redirect
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+ {/* Start rendering router matches */}
+ {/* {/* */}
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout.tsx
new file mode 100644
index 00000000000..d43b4ef5f5e
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 00000000000..7a5a3623a03
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 00000000000..b69951b2465
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 00000000000..30dbcce90fa
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/anchor.tsx b/e2e/solid-router/basic-file-based/src/routes/anchor.tsx
new file mode 100644
index 00000000000..77c0e5a6de4
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/anchor.tsx
@@ -0,0 +1,226 @@
+import {
+ Link,
+ createFileRoute,
+ useLocation,
+ useNavigate,
+} from '@tanstack/solid-router'
+import {
+ createEffect,
+ createRenderEffect,
+ createSignal,
+ onCleanup,
+} from 'solid-js'
+
+export const Route = createFileRoute('/anchor')({
+ component: AnchorComponent,
+})
+
+const anchors: Array<{
+ id: string
+ title: string
+ hashScrollIntoView?: boolean | ScrollIntoViewOptions
+}> = [
+ {
+ id: 'default-anchor',
+ title: 'Default Anchor',
+ },
+ {
+ id: 'false-anchor',
+ title: 'No Scroll Into View',
+ hashScrollIntoView: false,
+ },
+ {
+ id: 'smooth-scroll',
+ title: 'Smooth Scroll',
+ hashScrollIntoView: { behavior: 'smooth' },
+ },
+] as const
+
+function AnchorSection({ id, title }: { id: string; title: string }) {
+ const [hasShown, setHasShown] = createSignal(false)
+ let elementRef: HTMLHeadingElement | null = null
+
+ createEffect(() => {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!hasShown() && entry.isIntersecting) {
+ setHasShown(true)
+ }
+ },
+ { threshold: 0.01 },
+ )
+
+ const currentRef = elementRef
+ if (currentRef) {
+ observer.observe(currentRef)
+ }
+
+ onCleanup(() => {
+ if (currentRef) {
+ observer.unobserve(currentRef)
+ }
+ })
+ })
+
+ return (
+
+
+ {`${title} ${hasShown() ? '(shown)' : ''}`}
+
+
+ )
+}
+
+function AnchorComponent() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const [withScroll, setWithScroll] = createSignal(true)
+
+ return (
+
+
+
+ {anchors.map((anchor) => (
+
+
+ {anchor.title}
+
+
+ ))}
+
+
+
+
+
+ {anchors.map((anchor) => (
+
+ ))}
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/editing-a.tsx b/e2e/solid-router/basic-file-based/src/routes/editing-a.tsx
new file mode 100644
index 00000000000..e8931690250
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/editing-a.tsx
@@ -0,0 +1,45 @@
+import { createFileRoute, useBlocker } from '@tanstack/solid-router'
+import { createSignal } from 'solid-js'
+
+export const Route = createFileRoute('/editing-a')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const navigate = Route.useNavigate()
+ const [input, setInput] = createSignal('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: ({ next }) => {
+ if (next.fullPath === '/editing-b' && input().length > 0) {
+ return true
+ }
+ return false
+ },
+ withResolver: true,
+ })
+
+ return (
+
+
Editing A
+
+ Enter your name:
+ setInput(e.target.value)}
+ />
+
+ {
+ navigate({ to: '/editing-b' })
+ }}
+ >
+ Go to next step
+
+ {blocker().status === 'blocked' && (
+ blocker().proceed?.()}>Proceed
+ )}
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/editing-b.tsx b/e2e/solid-router/basic-file-based/src/routes/editing-b.tsx
new file mode 100644
index 00000000000..3dc2d1655f3
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/editing-b.tsx
@@ -0,0 +1,41 @@
+import { createFileRoute, useBlocker } from '@tanstack/solid-router'
+import { createEffect, createSignal, createMemo } from 'solid-js'
+
+export const Route = createFileRoute('/editing-b')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const navigate = Route.useNavigate()
+ const [input, setInput] = createSignal('')
+
+ const blocker = createMemo(() =>
+ useBlocker({
+ condition: input(),
+ }),
+ )
+
+ return (
+
+
Editing B
+
+ Enter your name:
+ setInput(e.target.value)}
+ />
+
+ {
+ navigate({ to: '/editing-a' })
+ }}
+ >
+ Go back
+
+ {blocker()().status === 'blocked' && (
+ blocker()().proceed?.()}>Proceed
+ )}
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/index.tsx b/e2e/solid-router/basic-file-based/src/routes/index.tsx
new file mode 100644
index 00000000000..bdfb4c76768
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/basic-file-based/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..a48895f3391
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/posts.$postId.tsx
@@ -0,0 +1,29 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { fetchPost } from '../posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ errorComponent: PostErrorComponent,
+ notFoundComponent: () => {
+ return Post not found
+ },
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ {post().title}
+
+
{post().body}
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/posts.index.tsx b/e2e/solid-router/basic-file-based/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..c7d8cfe19c7
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/posts.tsx b/e2e/solid-router/basic-file-based/src/routes/posts.tsx
new file mode 100644
index 00000000000..fd42972ee9a
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/posts.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { fetchPosts } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/posts_.$postId.edit.tsx b/e2e/solid-router/basic-file-based/src/routes/posts_.$postId.edit.tsx
new file mode 100644
index 00000000000..f38f369b7a5
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/posts_.$postId.edit.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, getRouteApi, useParams } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts_/$postId/edit')({
+ component: PostEditPage,
+})
+
+const api = getRouteApi('/posts_/$postId/edit')
+
+function PostEditPage() {
+ const paramsViaApi = api.useParams()
+ const paramsViaHook = useParams({ from: '/posts_/$postId/edit' })
+ const paramsViaRouteHook = Route.useParams()
+
+ return (
+ <>
+ {paramsViaHook().postId}
+
+ {paramsViaRouteHook().postId}
+
+ {paramsViaApi().postId}
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target.tsx
new file mode 100644
index 00000000000..525dd9da254
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute, retainSearchParams } from '@tanstack/solid-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/redirect/$target')({
+ params: {
+ parse: (p) =>
+ z
+ .object({
+ target: z.union([z.literal('internal'), z.literal('external')]),
+ })
+ .parse(p),
+ },
+ validateSearch: z.object({
+ reloadDocument: z.boolean().optional(),
+ preload: z.literal(false).optional(),
+ externalHost: z.string().optional(),
+ }),
+ search: {
+ middlewares: [retainSearchParams(['externalHost'])],
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target/index.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/index.tsx
new file mode 100644
index 00000000000..a44008e3c6b
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/index.tsx
@@ -0,0 +1,65 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/')({
+ component: () => {
+ const preload = Route.useSearch({ select: (s) => s.preload })
+ return (
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-beforeLoad (reloadDocument=true)
+
+
+
+
+ via-loader
+
+
+
+
+ via-loader (reloadDocument=true)
+
+
+
+ )
+ },
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx
new file mode 100644
index 00000000000..c88cc079864
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({
+ beforeLoad: ({
+ params: { target },
+ search: { reloadDocument, externalHost },
+ }) => {
+ switch (target) {
+ case 'internal':
+ throw redirect({ to: '/posts', reloadDocument })
+ case 'external':
+ const href = externalHost ?? 'http://example.com'
+ throw redirect({ href })
+ }
+ },
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx
new file mode 100644
index 00000000000..5c059717c5c
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/via-loader')({
+ loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({
+ reloadDocument,
+ externalHost,
+ }),
+ loader: ({ params: { target }, deps: { externalHost, reloadDocument } }) => {
+ switch (target) {
+ case 'internal':
+ throw redirect({ to: '/posts', reloadDocument })
+ case 'external':
+ const href = externalHost ?? 'http://example.com'
+ throw redirect({ href })
+ }
+ },
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/index.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/index.tsx
new file mode 100644
index 00000000000..043f305e574
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/index.tsx
@@ -0,0 +1,28 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/')({
+ component: () => (
+
+
+ internal
+ {' '}
+
+ external
+
+
+ ),
+})
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/preload/first.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/first.tsx
new file mode 100644
index 00000000000..68752e5a238
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/first.tsx
@@ -0,0 +1,23 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/preload/first')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+ go to second
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/preload/second.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/second.tsx
new file mode 100644
index 00000000000..0ecd74ef80c
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/second.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/preload/second')({
+ loader: async () => {
+ await new Promise((r) => setTimeout(r, 1000))
+ throw redirect({ from: Route.fullPath, to: '../third' })
+ },
+ component: RouteComponent,
+ pendingComponent: () => second pending
,
+})
+
+function RouteComponent() {
+ return Hello "/redirect/preload/second"!
+}
diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/preload/third.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/third.tsx
new file mode 100644
index 00000000000..e831276d29a
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/third.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/preload/third')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/redirect/preload/third"!
+}
diff --git a/e2e/solid-router/basic-file-based/src/styles.css b/e2e/solid-router/basic-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic-file-based/tailwind.config.mjs b/e2e/solid-router/basic-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts
new file mode 100644
index 00000000000..fd2627c9378
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts
@@ -0,0 +1,255 @@
+import { expect, test } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test("useBlocker doesn't block navigation if condition is not met", async ({
+ page,
+}) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+})
+
+test('useBlocker does block navigation if condition is met', async ({
+ page,
+}) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible()
+})
+
+test('Proceeding through blocked navigation works', async ({ page }) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByRole('button', { name: 'Proceed' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+})
+
+test("legacy useBlocker doesn't block navigation if condition is not met", async ({
+ page,
+}) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+})
+
+test('legacy useBlocker does block navigation if condition is met', async ({
+ page,
+}) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible()
+})
+
+test('legacy Proceeding through blocked navigation works', async ({ page }) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByRole('button', { name: 'Proceed' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+})
+
+test('useCanGoBack correctly disables back button', async ({ page }) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goForward()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+test('useCanGoBack correctly disables back button, using router.history and window.history', async ({
+ page,
+}) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+const testCases = [
+ {
+ description: 'Navigating to a route inside a route group',
+ testId: 'link-to-route-inside-group',
+ },
+ {
+ description:
+ 'Navigating to a route inside a subfolder inside a route group ',
+ testId: 'link-to-route-inside-group-inside-subfolder',
+ },
+ {
+ description: 'Navigating to a route inside a route group inside a layout',
+ testId: 'link-to-route-inside-group-inside-layout',
+ },
+ {
+ description: 'Navigating to a lazy route inside a route group',
+ testId: 'link-to-lazy-route-inside-group',
+ },
+
+ {
+ description: 'Navigating to the only route inside a route group ',
+ testId: 'link-to-only-route-inside-group',
+ },
+]
+
+testCases.forEach(({ description, testId }) => {
+ test(description, async ({ page }) => {
+ await page.getByTestId(testId).click()
+ await expect(page.getByTestId('search-via-hook')).toContainText('world')
+ await expect(page.getByTestId('search-via-route-hook')).toContainText(
+ 'world',
+ )
+ await expect(page.getByTestId('search-via-route-api')).toContainText(
+ 'world',
+ )
+ })
+})
+
+test('navigating to an unnested route', async ({ page }) => {
+ const postId = 'hello-world'
+ page.goto(`/posts/${postId}/edit`)
+ await expect(page.getByTestId('params-via-hook')).toContainText(postId)
+ await expect(page.getByTestId('params-via-route-hook')).toContainText(postId)
+ await expect(page.getByTestId('params-via-route-api')).toContainText(postId)
+})
+
+async function getRenderCount(page: Page) {
+ const renderCount = parseInt(
+ await page.getByTestId('render-count').innerText(),
+ )
+ return renderCount
+}
+async function structuralSharingTest(page: Page, enabled: boolean) {
+ page.goto(`/structural-sharing/${enabled}/?foo=f1&bar=b1`)
+ await expect(page.getByTestId('enabled')).toHaveText(JSON.stringify(enabled))
+
+ async function checkSearch({ foo, bar }: { foo: string; bar: string }) {
+ expect(page.url().endsWith(`?foo=${foo}&bar=${bar}`)).toBe(true)
+ const expectedSearch = JSON.stringify({ values: [foo, bar] })
+ await expect(page.getByTestId('search-via-hook')).toHaveText(expectedSearch)
+ await expect(page.getByTestId('search-via-route-hook')).toHaveText(
+ expectedSearch,
+ )
+ await expect(page.getByTestId('search-via-route-api-hook')).toHaveText(
+ expectedSearch,
+ )
+ }
+
+ await checkSearch({ bar: 'b1', foo: 'f1' })
+ await page.getByTestId('link').click()
+ await checkSearch({ bar: 'b2', foo: 'f2' })
+}
diff --git a/e2e/solid-router/basic-file-based/tests/redirect.spec.ts b/e2e/solid-router/basic-file-based/tests/redirect.spec.ts
new file mode 100644
index 00000000000..7026b047019
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/tests/redirect.spec.ts
@@ -0,0 +1,137 @@
+import { expect, test } from '@playwright/test'
+import combinateImport from 'combinate'
+import { derivePort, localDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../package.json' with { type: 'json' }
+import { Server } from 'node:http'
+import queryString from 'node:querystring'
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+const PORT = derivePort(packageJson.name)
+const EXTERNAL_HOST_PORT = derivePort(`${packageJson.name}-external`)
+
+test.describe('redirects', () => {
+ let server: Server
+ test.beforeAll(async () => {
+ server = await localDummyServer(EXTERNAL_HOST_PORT)
+ })
+ test.afterAll(async () => {
+ server.close()
+ })
+
+ const internalNavigationTestMatrix = combinate({
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ preload: [false, true] as const,
+ })
+
+ internalNavigationTestMatrix.forEach(
+ ({ thrower, reloadDocument, preload }) => {
+ test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+ page,
+ }) => {
+ await page.waitForLoadState('networkidle')
+ await page.goto(
+ `/redirect/internal${preload === false ? '?preload=false' : ''}`,
+ )
+ const link = page.getByTestId(
+ `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+ )
+
+ await page.waitForLoadState('networkidle')
+ let requestHappened = false
+
+ const requestPromise = new Promise((resolve) => {
+ page.on('request', (request) => {
+ if (
+ request.url() === 'https://jsonplaceholder.typicode.com/posts'
+ ) {
+ requestHappened = true
+ resolve()
+ }
+ })
+ })
+ await link.focus()
+
+ const expectRequestHappened = preload && !reloadDocument
+ const timeoutPromise = new Promise((resolve) =>
+ setTimeout(resolve, expectRequestHappened ? 5000 : 500),
+ )
+ await Promise.race([requestPromise, timeoutPromise])
+ expect(requestHappened).toBe(expectRequestHappened)
+ await link.click()
+ let fullPageLoad = false
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ const url = `http://localhost:${PORT}/posts`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ })
+ },
+ )
+
+ const internalDirectVisitTestMatrix = combinate({
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ internalDirectVisitTestMatrix.forEach(({ thrower, reloadDocument }) => {
+ test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ await page.waitForLoadState('networkidle')
+
+ await page.goto(`/redirect/internal/via-${thrower}`)
+
+ const url = `http://localhost:${PORT}/posts`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ })
+ })
+
+ const externalTestMatrix = combinate({
+ scenario: ['navigate', 'direct_visit'] as const,
+ thrower: ['beforeLoad', 'loader'] as const,
+ })
+
+ externalTestMatrix.forEach(({ scenario, thrower }) => {
+ test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({
+ page,
+ }) => {
+ await page.waitForLoadState('networkidle')
+
+ let q = queryString.stringify({
+ externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`,
+ })
+ if (scenario === 'navigate') {
+ await page.goto(`/redirect/external?${q}`)
+ await page.getByTestId(`via-${thrower}`).click()
+ } else {
+ await page.goto(`/redirect/external/via-${thrower}?${q}`)
+ }
+
+ const url = `http://localhost:${EXTERNAL_HOST_PORT}/`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ })
+ })
+
+ test('regression test for #3097', async ({ page }) => {
+ await page.goto(`/redirect/preload/first`)
+ const link = page.getByTestId(`link`)
+ await link.focus()
+ await link.click()
+ await page.waitForURL('/redirect/preload/third')
+ await expect(page.getByTestId(`third`)).toBeInViewport()
+ })
+})
diff --git a/e2e/solid-router/basic-file-based/tests/scroll-into-view.spec.ts b/e2e/solid-router/basic-file-based/tests/scroll-into-view.spec.ts
new file mode 100644
index 00000000000..d1af86adfb2
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/tests/scroll-into-view.spec.ts
@@ -0,0 +1,213 @@
+import { expect, test } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+const anchors = {
+ defaultAnchor: 'default-anchor',
+ noScrollIntoView: 'false-anchor',
+ smoothScroll: 'smooth-scroll',
+} as const
+
+const formTestIds = {
+ targetAnchor: 'hash-select',
+ scrollIntoView: 'with-scroll',
+ behaviorSelect: 'behavior-select',
+ blockSelect: 'block-select',
+ inlineSelect: 'inline-select',
+ navigateButton: 'navigate-button',
+}
+
+const shownSuffix = '(shown)'
+
+const activeClass = 'font-bold active'
+
+function getAnchorTarget(page: Page, anchor: string) {
+ return page.getByTestId(`heading-${anchor}`)
+}
+
+function getAnchorLink(page: Page, anchor: string) {
+ return page.getByTestId(`link-${anchor}`)
+}
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/anchor')
+})
+
+// Testing the `Link` component with the `hashScrollIntoView` prop
+test('Navigating via anchor `Link` with default hash scrolling behavior', async ({
+ page,
+}) => {
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+ shownSuffix,
+ )
+
+ await getAnchorLink(page, anchors.defaultAnchor).click()
+
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).toBeVisible()
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorLink(page, anchors.defaultAnchor)).toHaveClass(
+ activeClass,
+ )
+})
+
+test('Navigating via anchor `Link` with hash scrolling disabled', async ({
+ page,
+}) => {
+ const initialScrollPosition = await page.evaluate(() => window.scrollY)
+
+ await expect(
+ getAnchorTarget(page, anchors.noScrollIntoView),
+ ).not.toContainText(shownSuffix)
+
+ await getAnchorLink(page, anchors.noScrollIntoView).click()
+
+ // The active anchor should have updated
+ await expect(getAnchorLink(page, anchors.noScrollIntoView)).toHaveClass(
+ activeClass,
+ )
+
+ // The anchor should not have been visible, because the scroll should not have been activated
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+ shownSuffix,
+ )
+
+ // Expect the same scroll position as before
+ expect(await page.evaluate(() => window.scrollY)).toBe(initialScrollPosition)
+})
+
+test('Navigating via anchor `Link` with smooth hash scrolling behavior', async ({
+ page,
+}) => {
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+ shownSuffix,
+ )
+
+ await getAnchorLink(page, anchors.smoothScroll).click()
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).toBeVisible()
+
+ // Smooth scrolling should activate the IntersectionObserver on all headings, making them all render "(shown)"
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText(
+ shownSuffix,
+ )
+ await expect(getAnchorTarget(page, anchors.noScrollIntoView)).toContainText(
+ shownSuffix,
+ )
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+ activeClass,
+ )
+})
+
+// Testing the `useNavigate` hook with the `hashScrollIntoView` option
+test('Navigating via `useNavigate` with instant scroll behavior', async ({
+ page,
+}) => {
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+ shownSuffix,
+ )
+
+ // Scroll to the last anchor instantly, should not activate Intersection Observers for the other anchors
+ await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll')
+ await page.getByTestId(formTestIds.scrollIntoView).check()
+ await page.getByTestId(formTestIds.behaviorSelect).selectOption('instant')
+ await page.getByTestId(formTestIds.blockSelect).selectOption('start')
+ await page.getByTestId(formTestIds.inlineSelect).selectOption('nearest')
+ await page.getByTestId(formTestIds.navigateButton).click()
+
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+ shownSuffix,
+ )
+
+ await expect(
+ getAnchorTarget(page, anchors.noScrollIntoView),
+ ).not.toContainText(shownSuffix)
+
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorLink(page, anchors.smoothScroll)).toBeVisible()
+
+ await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+ activeClass,
+ )
+})
+
+test('Navigating via `useNavigate` with scrollIntoView disabled', async ({
+ page,
+}) => {
+ const initialScrollPosition = await page.evaluate(() => window.scrollY)
+
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+ shownSuffix,
+ )
+
+ await expect(
+ getAnchorTarget(page, anchors.noScrollIntoView),
+ ).not.toContainText(shownSuffix)
+
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+ shownSuffix,
+ )
+
+ // Navigate to the last anchor, but with scrollIntoView disabled should not activate Intersection Observers for any anchors
+ await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll')
+ await page.getByTestId(formTestIds.scrollIntoView).uncheck()
+ await page.getByTestId(formTestIds.navigateButton).click()
+
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText(
+ shownSuffix,
+ )
+
+ await expect(
+ getAnchorTarget(page, anchors.noScrollIntoView),
+ ).not.toContainText(shownSuffix)
+
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+ activeClass,
+ )
+
+ // Expect the same scroll position as before
+ expect(await page.evaluate(() => window.scrollY)).toBe(initialScrollPosition)
+})
+
+test('Navigating via `useNavigate` with smooth scroll behavior', async ({
+ page,
+}) => {
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText(
+ shownSuffix,
+ )
+
+ // Scroll to the last anchor smoothly, should activate Intersection Observers for the other anchors, making them all render "(shown)"
+ await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll')
+ await page.getByTestId(formTestIds.scrollIntoView).check()
+ await page.getByTestId(formTestIds.behaviorSelect).selectOption('smooth')
+ await page.getByTestId(formTestIds.blockSelect).selectOption('start')
+ await page.getByTestId(formTestIds.inlineSelect).selectOption('nearest')
+ await page.getByTestId(formTestIds.navigateButton).click()
+
+ await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorTarget(page, anchors.noScrollIntoView)).toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText(
+ shownSuffix,
+ )
+
+ await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass(
+ activeClass,
+ )
+})
diff --git a/e2e/solid-router/basic-file-based/tsconfig.json b/e2e/solid-router/basic-file-based/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic-file-based/vite.config.js b/e2e/solid-router/basic-file-based/vite.config.js
new file mode 100644
index 00000000000..0d2f08b695a
--- /dev/null
+++ b/e2e/solid-router/basic-file-based/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [TanStackRouterVite({ target: 'solid' }), solid()],
+})
diff --git a/e2e/solid-router/basic-scroll-restoration/.gitignore b/e2e/solid-router/basic-scroll-restoration/.gitignore
new file mode 100644
index 00000000000..8354e4d50d5
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
\ No newline at end of file
diff --git a/e2e/solid-router/basic-scroll-restoration/index.html b/e2e/solid-router/basic-scroll-restoration/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/basic-scroll-restoration/package.json b/e2e/solid-router/basic-scroll-restoration/package.json
new file mode 100644
index 00000000000..95387e1a701
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "tanstack-router-e2e-solid-basic-scroll-restoration",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-virtual": "^3.13.0",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/playwright.config.ts b/e2e/solid-router/basic-scroll-restoration/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/basic-scroll-restoration/postcss.config.mjs b/e2e/solid-router/basic-scroll-restoration/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/src/main.tsx b/e2e/solid-router/basic-scroll-restoration/src/main.tsx
new file mode 100644
index 00000000000..f1a9dae0f9d
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/src/main.tsx
@@ -0,0 +1,229 @@
+import { render } from 'solid-js/web'
+import {
+ Link,
+ Outlet,
+ RouterProvider,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ useElementScrollRestoration,
+} from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+import { createVirtualizer } from '@tanstack/solid-virtual'
+import './styles.css'
+import { createRenderEffect } from 'solid-js'
+
+const rootRoute = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ About
+
+
+ About (No Reset)
+
+
+ By-Element
+
+
+
+ {/* */}
+ >
+ )
+}
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: () => new Promise((r) => setTimeout(r, 500)),
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ createRenderEffect(() => {
+ window.invokeOrders.push('index-useLayoutEffect')
+ }, [])
+
+ return (
+
+
+ Welcome Home!
+
+
+
+ {Array.from({ length: 50 }).map((_, i) => (
+
+ Home Item {i + 1}
+
+ ))}
+
+
+ )
+}
+
+const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ loader: () => new Promise((r) => setTimeout(r, 500)),
+ component: AboutComponent,
+})
+
+function AboutComponent() {
+ createRenderEffect(() => {
+ window.invokeOrders.push('about-useLayoutEffect')
+ }, [])
+ return (
+
+
Hello from About!
+
+ {Array.from({ length: 50 }).map((_, i) => (
+
+ About Item {i + 1}
+
+ ))}
+
+
+ )
+}
+
+const byElementRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/by-element',
+ loader: () => new Promise((r) => setTimeout(r, 500)),
+ component: ByElementComponent,
+})
+
+function ByElementComponent() {
+ // We need a unique ID for manual scroll restoration on a specific element
+ // It should be as unique as possible for this element across your app
+ const scrollRestorationId = 'myVirtualizedContent'
+
+ // We use that ID to get the scroll entry for this element
+ const scrollEntry = useElementScrollRestoration({
+ id: scrollRestorationId,
+ })
+
+ // Let's use TanStack Virtual to virtualize some content!
+ let virtualizerParentRef: any = null
+ const virtualizer = createVirtualizer({
+ count: 10000,
+ getScrollElement: () => virtualizerParentRef?.current,
+ estimateSize: () => 100,
+ // We pass the scrollY from the scroll restoration entry to the virtualizer
+ // as the initial offset
+ initialOffset: scrollEntry?.scrollY,
+ })
+
+ return (
+
+
Hello from By-Element!
+
+
+
+ First Regular List Item
+
+
+ {Array.from({ length: 50 }).map((_, i) => (
+
+ Regular List Item {i + 1}
+
+ ))}
+
+
+ {Array.from({ length: 2 }).map((_, i) => (
+
+
+ {Array.from({ length: 50 }).map((_, i) => (
+
+ About Item {i + 1}
+
+ ))}
+
+
+ ))}
+
+
Virtualized
+
+
+ {virtualizer.getVirtualItems().map((item) => (
+
+
+ Virtualized Item {item.index + 1}
+
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+const routeTree = rootRoute.addChildren([
+ indexRoute,
+ aboutRoute,
+ byElementRoute,
+])
+
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ scrollRestoration: true,
+ getScrollRestorationKey: (location) => location.pathname,
+})
+
+declare global {
+ interface Window {
+ invokeOrders: Array
+ }
+}
+window.invokeOrders = []
+router.subscribe('onBeforeRouteMount', (event) => {
+ window.invokeOrders.push(event.type)
+})
+
+router.subscribe('onResolved', (event) => {
+ window.invokeOrders.push(event.type)
+})
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(() => , rootElement)
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/src/styles.css b/e2e/solid-router/basic-scroll-restoration/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/tailwind.config.mjs b/e2e/solid-router/basic-scroll-restoration/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/tests/app.spec.ts b/e2e/solid-router/basic-scroll-restoration/tests/app.spec.ts
new file mode 100644
index 00000000000..d23c1fb7ce2
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/tests/app.spec.ts
@@ -0,0 +1,82 @@
+import { expect, test } from '@playwright/test'
+
+test('restore scroll positions by page, home pages top message should not display on navigating back', async ({
+ page,
+}) => {
+ // Step 1: Navigate to the home page
+ await page.goto('/')
+
+ await expect(page.locator('#greeting')).toContainText('Welcome Home!')
+ await expect(page.locator('#top-message')).toBeInViewport()
+
+ // Step 2: Scroll to a position that hides the top
+ const targetScrollPosition = 1000
+ await page.evaluate(
+ (scrollPos: number) => window.scrollTo(0, scrollPos),
+ targetScrollPosition,
+ )
+
+ // Verify initial scroll position
+ const scrollPosition = await page.evaluate(() => window.scrollY)
+ expect(scrollPosition).toBe(targetScrollPosition)
+
+ await expect(page.locator('#top-message')).not.toBeInViewport()
+
+ // Step 3: Navigate to the about page
+ await page.getByRole('link', { name: 'About', exact: true }).click()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ // Step 4: Go back to the home page and immediately check the message
+ await page.goBack()
+
+ // Wait for the home page to have rendered
+ await page.waitForSelector('#greeting')
+ await page.waitForTimeout(1000)
+ await expect(page.locator('#top-message')).not.toBeInViewport()
+
+ // Confirm the scroll position was restored correctly
+ const restoredScrollPosition = await page.evaluate(() => window.scrollY)
+ expect(restoredScrollPosition).toBe(targetScrollPosition)
+})
+
+test('restore scroll positions by element, first regular list item should not display on navigating back', async ({
+ page,
+}) => {
+ // Step 1: Navigate to the by-element page
+ await page.goto('/by-element')
+
+ // Step 2: Scroll to a position that hides the first list item in regular list
+ const targetScrollPosition = 1000
+ await page.waitForSelector('#RegularList')
+ await expect(page.locator('#first-regular-list-item')).toBeInViewport()
+
+ await page.evaluate(
+ (scrollPos: number) =>
+ document.querySelector('#RegularList')!.scrollTo(0, scrollPos),
+ targetScrollPosition,
+ )
+
+ // Verify initial scroll position
+ const scrollPosition = await page.evaluate(
+ () => document.querySelector('#RegularList')!.scrollTop,
+ )
+ expect(scrollPosition).toBe(targetScrollPosition)
+
+ await expect(page.locator('#first-regular-list-item')).not.toBeInViewport()
+
+ // Step 3: Navigate to the about page
+ await page.getByRole('link', { name: 'About', exact: true }).click()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ // Step 4: Go back to the by-element page and immediately check the message
+ await page.goBack()
+
+ // TODO: For some reason, this only works in headed mode.
+ // When someone can explain that to me, I'll fix this test.
+
+ // Confirm the scroll position was restored correctly
+ // const restoredScrollPosition = await page.evaluate(
+ // () => document.querySelector('#RegularList')!.scrollTop,
+ // )
+ // expect(restoredScrollPosition).toBe(targetScrollPosition)
+})
diff --git a/e2e/solid-router/basic-scroll-restoration/tests/router-events.spec.ts b/e2e/solid-router/basic-scroll-restoration/tests/router-events.spec.ts
new file mode 100644
index 00000000000..23cbd154514
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/tests/router-events.spec.ts
@@ -0,0 +1,44 @@
+import { expect, test } from '@playwright/test'
+
+test('after a navigation, should have emitted "onBeforeRouteMount","onResolved" and useRenderEffect setup in the correct order', async ({
+ page,
+}) => {
+ // Navigate to the Home page
+ await page.goto('/')
+ await expect(page.locator('#greeting')).toContainText('Welcome Home!')
+
+ let orders = await page.evaluate(() => window.invokeOrders)
+
+ expectItemOrder(orders, 'onBeforeRouteMount', 'onResolved')
+ expectItemOrder(orders, 'onBeforeRouteMount', 'index-useLayoutEffect')
+
+ // Clear the invokeOrders array
+ orders = await page.evaluate(() => {
+ window.invokeOrders = []
+ return window.invokeOrders
+ })
+
+ // Navigate to the About page
+ await page.getByRole('link', { name: 'About', exact: true }).click()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ orders = await page.evaluate(() => window.invokeOrders)
+
+ expectItemOrder(orders, 'onBeforeRouteMount', 'onResolved')
+ expectItemOrder(orders, 'onBeforeRouteMount', 'about-useLayoutEffect')
+})
+
+function expectItemOrder(
+ array: Array,
+ firstItem: TItem,
+ secondItem: TItem,
+) {
+ const firstIndex = array.findIndex((item) => item === firstItem)
+ const secondIndex = array.findIndex((item) => item === secondItem)
+
+ if (firstIndex === -1 || secondIndex === -1) {
+ throw new Error('One or both items were not found in the array ' + array)
+ }
+
+ expect(firstIndex).toBeLessThan(secondIndex)
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/tsconfig.json b/e2e/solid-router/basic-scroll-restoration/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic-scroll-restoration/vite.config.js b/e2e/solid-router/basic-scroll-restoration/vite.config.js
new file mode 100644
index 00000000000..05041cc6d83
--- /dev/null
+++ b/e2e/solid-router/basic-scroll-restoration/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/e2e/solid-router/basic-solid-query-file-based/.gitignore b/e2e/solid-router/basic-solid-query-file-based/.gitignore
new file mode 100644
index 00000000000..a6ea47e5085
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-router/basic-solid-query-file-based/index.html b/e2e/solid-router/basic-solid-query-file-based/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/basic-solid-query-file-based/package.json b/e2e/solid-router/basic-solid-query-file-based/package.json
new file mode 100644
index 00000000000..93c299cecb6
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "tanstack-router-e2e-solid-basic-solid-query-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-query": "^5.66.0",
+ "@tanstack/solid-query-devtools": "^5.66.0",
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/router-plugin": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/playwright.config.ts b/e2e/solid-router/basic-solid-query-file-based/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/basic-solid-query-file-based/postcss.config.mjs b/e2e/solid-router/basic-solid-query-file-based/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/main.tsx b/e2e/solid-router/basic-solid-query-file-based/src/main.tsx
new file mode 100644
index 00000000000..7f1d2165f16
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/main.tsx
@@ -0,0 +1,40 @@
+import { render } from 'solid-js/web'
+import { RouterProvider, createRouter } from '@tanstack/solid-router'
+import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+export const queryClient = new QueryClient()
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ context: {
+ queryClient,
+ },
+ scrollRestoration: true,
+ defaultPreload: 'intent',
+ // Since we're using React Query, we don't want loader calls to ever be stale
+ // This will ensure that the loader is always called when the route is preloaded or visited
+ defaultPreloadStaleTime: 0,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(
+ () => (
+
+
+
+ ),
+ rootElement,
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/postQueryOptions.tsx b/e2e/solid-router/basic-solid-query-file-based/src/postQueryOptions.tsx
new file mode 100644
index 00000000000..fe118d7bdea
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/postQueryOptions.tsx
@@ -0,0 +1,8 @@
+import { queryOptions } from '@tanstack/solid-query'
+import { fetchPost } from './posts'
+
+export const postQueryOptions = (postId: string) =>
+ queryOptions({
+ queryKey: ['posts', { postId }],
+ queryFn: () => fetchPost(postId),
+ })
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/posts.tsx b/e2e/solid-router/basic-solid-query-file-based/src/posts.tsx
new file mode 100644
index 00000000000..d551659b92e
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/posts.tsx
@@ -0,0 +1,33 @@
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export class PostNotFoundError extends Error {}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw new PostNotFoundError(`Post with id "${postId}" not found!`)
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/postsQueryOptions.tsx b/e2e/solid-router/basic-solid-query-file-based/src/postsQueryOptions.tsx
new file mode 100644
index 00000000000..ce59b8a0bb4
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/postsQueryOptions.tsx
@@ -0,0 +1,7 @@
+import { queryOptions } from '@tanstack/solid-query'
+import { fetchPosts } from './posts'
+
+export const postsQueryOptions = queryOptions({
+ queryKey: ['posts'],
+ queryFn: () => fetchPosts(),
+})
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-solid-query-file-based/src/routeTree.gen.ts
new file mode 100644
index 00000000000..586c996b9bf
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routeTree.gen.ts
@@ -0,0 +1,243 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as PostsImport } from './routes/posts'
+import { Route as LayoutImport } from './routes/_layout'
+import { Route as IndexImport } from './routes/index'
+import { Route as PostsIndexImport } from './routes/posts.index'
+import { Route as PostsPostIdImport } from './routes/posts.$postId'
+import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2'
+import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a'
+
+// Create/Update Routes
+
+const PostsRoute = PostsImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LayoutRoute = LayoutImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const PostsIndexRoute = PostsIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any)
+
+const PostsPostIdRoute = PostsPostIdImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any)
+
+const LayoutLayout2Route = LayoutLayout2Import.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any)
+
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutImport
+ parentRoute: typeof rootRoute
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsImport
+ parentRoute: typeof rootRoute
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2Import
+ parentRoute: typeof LayoutImport
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdImport
+ parentRoute: typeof PostsImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexImport
+ parentRoute: typeof PostsImport
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutAImport
+ parentRoute: typeof LayoutLayout2Import
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBImport
+ parentRoute: typeof LayoutLayout2Import
+ }
+ }
+}
+
+// Create and export the route tree
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '': typeof LayoutLayout2RouteWithChildren
+ '/posts': typeof PostsRouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '': typeof LayoutLayout2RouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts': typeof PostsIndexRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/_layout': typeof LayoutRouteWithChildren
+ '/posts': typeof PostsRouteWithChildren
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | ''
+ | '/posts'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/layout-a'
+ | '/layout-b'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b'
+ id:
+ | '__root__'
+ | '/'
+ | '/_layout'
+ | '/posts'
+ | '/_layout/_layout-2'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ LayoutRoute: typeof LayoutRouteWithChildren
+ PostsRoute: typeof PostsRouteWithChildren
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ LayoutRoute: LayoutRouteWithChildren,
+ PostsRoute: PostsRouteWithChildren,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/__root.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/__root.tsx
new file mode 100644
index 00000000000..f52862bfbf7
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/__root.tsx
@@ -0,0 +1,69 @@
+import {
+ Link,
+ Outlet,
+ createRootRouteWithContext,
+} from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
+import type { QueryClient } from '@tanstack/solid-query'
+
+export const Route = createRootRouteWithContext<{
+ queryClient: QueryClient
+}>()({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+
+ {/* */}
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout.tsx
new file mode 100644
index 00000000000..d43b4ef5f5e
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 00000000000..7a5a3623a03
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 00000000000..b69951b2465
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 00000000000..30dbcce90fa
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/index.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/index.tsx
new file mode 100644
index 00000000000..bdfb4c76768
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..ee729333659
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.$postId.tsx
@@ -0,0 +1,58 @@
+import {
+ ErrorComponent,
+ createFileRoute,
+ useRouter,
+} from '@tanstack/solid-router'
+import { createQuery } from '@tanstack/solid-query'
+import { PostNotFoundError } from '../posts'
+import { postQueryOptions } from '../postQueryOptions'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+import { createEffect, createMemo } from 'solid-js'
+import { queryClient } from '../main'
+
+export function PostErrorComponent({ error, reset }: ErrorComponentProps) {
+ const router = useRouter()
+ if (error instanceof PostNotFoundError) {
+ return {error.message}
+ }
+
+ createEffect(() => {
+ reset()
+ queryClient.resetQueries()
+ })
+
+ return (
+
+ {
+ router.invalidate()
+ }}
+ >
+ retry
+
+
+
+ )
+}
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: ({ context: { queryClient }, params: { postId } }) => {
+ return queryClient.ensureQueryData(postQueryOptions(postId))
+ },
+ errorComponent: PostErrorComponent,
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const params = Route.useParams()
+ const post = createMemo(() =>
+ createQuery(() => postQueryOptions(params().postId)),
+ )
+
+ return (
+
+
{post()?.data?.title}
+
{post()?.data?.body}
+
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.index.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..33d0386c195
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.tsx
new file mode 100644
index 00000000000..8ab5195a348
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.tsx
@@ -0,0 +1,48 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createQuery } from '@tanstack/solid-query'
+import { postsQueryOptions } from '../postsQueryOptions'
+import { createMemo } from 'solid-js'
+
+export const Route = createFileRoute('/posts')({
+ loader: ({ context: { queryClient } }) =>
+ queryClient.ensureQueryData(postsQueryOptions),
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const postsQuery = createQuery(() => postsQueryOptions)
+ const posts = createMemo(() => {
+ if (postsQuery.data) {
+ return postsQuery.data
+ } else {
+ return []
+ }
+ })
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/src/styles.css b/e2e/solid-router/basic-solid-query-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/tailwind.config.mjs b/e2e/solid-router/basic-solid-query-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/tests/app.spec.ts b/e2e/solid-router/basic-solid-query-file-based/tests/app.spec.ts
new file mode 100644
index 00000000000..3e5a69ccaae
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/tests/app.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/solid-router/basic-solid-query-file-based/tsconfig.json b/e2e/solid-router/basic-solid-query-file-based/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic-solid-query-file-based/vite.config.js b/e2e/solid-router/basic-solid-query-file-based/vite.config.js
new file mode 100644
index 00000000000..0d2f08b695a
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query-file-based/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [TanStackRouterVite({ target: 'solid' }), solid()],
+})
diff --git a/e2e/solid-router/basic-solid-query/.gitignore b/e2e/solid-router/basic-solid-query/.gitignore
new file mode 100644
index 00000000000..a6ea47e5085
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-router/basic-solid-query/index.html b/e2e/solid-router/basic-solid-query/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/basic-solid-query/package.json b/e2e/solid-router/basic-solid-query/package.json
new file mode 100644
index 00000000000..b98a7681fe0
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "tanstack-router-e2e-solid-solid-query",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-query": "^5.66.0",
+ "@tanstack/solid-query-devtools": "^5.66.0",
+ "@tanstack/solid-router": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/basic-solid-query/playwright.config.ts b/e2e/solid-router/basic-solid-query/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/basic-solid-query/postcss.config.mjs b/e2e/solid-router/basic-solid-query/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/basic-solid-query/src/main.tsx b/e2e/solid-router/basic-solid-query/src/main.tsx
new file mode 100644
index 00000000000..c7bbbabcdf0
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/src/main.tsx
@@ -0,0 +1,275 @@
+import { render } from 'solid-js/web'
+import {
+ ErrorComponent,
+ Link,
+ Outlet,
+ RouterProvider,
+ createRootRouteWithContext,
+ createRoute,
+ createRouter,
+ useRouter,
+} from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
+import {
+ QueryClient,
+ QueryClientProvider,
+ createQuery,
+} from '@tanstack/solid-query'
+import { NotFoundError, postQueryOptions, postsQueryOptions } from './posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+import './styles.css'
+import { createEffect, createMemo } from 'solid-js'
+
+const rootRoute = createRootRouteWithContext<{
+ queryClient: QueryClient
+}>()({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+
+ {/* */}
+ >
+ )
+}
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexRouteComponent,
+})
+
+function IndexRouteComponent() {
+ return (
+
+
Welcome Home!
+
+ )
+}
+
+const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'posts',
+ loader: ({ context: { queryClient } }) =>
+ queryClient.ensureQueryData(postsQueryOptions),
+}).lazy(() => import('./posts.lazy').then((d) => d.Route))
+
+const postsIndexRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '/',
+ component: PostsIndexRouteComponent,
+})
+
+function PostsIndexRouteComponent() {
+ return Select a post.
+}
+
+function PostErrorComponent({ error, reset }: ErrorComponentProps) {
+ const router = useRouter()
+ if (error instanceof NotFoundError) {
+ return {error.message}
+ }
+ createEffect(() => {
+ reset()
+ queryClient.resetQueries()
+ })
+
+ return (
+
+ {
+ router.invalidate()
+ }}
+ >
+ retry
+
+
+
+ )
+}
+
+const postRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '$postId',
+ errorComponent: PostErrorComponent,
+ loader: ({ context: { queryClient }, params: { postId } }) =>
+ queryClient.ensureQueryData(postQueryOptions(postId)),
+ component: PostRouteComponent,
+})
+
+function PostRouteComponent() {
+ const params = postRoute.useParams()
+ const postQuery = createQuery(() => postQueryOptions(params().postId))
+ const post = createMemo(() => postQuery.data)
+
+ return (
+
+
{post()?.title}
+
{post()?.body}
+
+ )
+}
+
+const layoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_layout',
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
+
+const layout2Route = createRoute({
+ getParentRoute: () => layoutRoute,
+ id: '_layout-2',
+ component: Layout2Component,
+})
+
+function Layout2Component() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
+
+const layoutARoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-a',
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
+
+const layoutBRoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-b',
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
+
+const routeTree = rootRoute.addChildren([
+ postsRoute.addChildren([postRoute, postsIndexRoute]),
+ layoutRoute.addChildren([
+ layout2Route.addChildren([layoutARoute, layoutBRoute]),
+ ]),
+ indexRoute,
+])
+
+const queryClient = new QueryClient()
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: 'intent',
+ // Since we're using React Query, we don't want loader calls to ever be stale
+ // This will ensure that the loader is always called when the route is preloaded or visited
+ defaultPreloadStaleTime: 0,
+ context: {
+ queryClient,
+ },
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(
+ () => (
+
+
+
+ ),
+ rootElement,
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query/src/posts.lazy.tsx b/e2e/solid-router/basic-solid-query/src/posts.lazy.tsx
new file mode 100644
index 00000000000..ce21b15cc69
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/src/posts.lazy.tsx
@@ -0,0 +1,49 @@
+import { Link, Outlet, createLazyRoute } from '@tanstack/solid-router'
+import { createQuery } from '@tanstack/solid-query'
+import { postsQueryOptions } from './posts'
+import { createMemo, Suspense } from 'solid-js'
+
+export const Route = createLazyRoute('/posts')({
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const postsQuery = createQuery(() => postsQueryOptions)
+
+ const posts = createMemo(() => {
+ if (postsQuery.data) {
+ return postsQuery.data
+ } else {
+ return []
+ }
+ })
+
+ return (
+
+
+
+ {[
+ ...posts(),
+ { id: 'i-do-not-exist', title: 'Non-existent Post' },
+ ].map((post) => {
+ return (
+
+
+ {post.title.substring(0, 20)}
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-solid-query/src/posts.ts b/e2e/solid-router/basic-solid-query/src/posts.ts
new file mode 100644
index 00000000000..cc07134ab2d
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/src/posts.ts
@@ -0,0 +1,44 @@
+import axios from 'redaxios'
+import { queryOptions } from '@tanstack/solid-query'
+
+export class NotFoundError extends Error {}
+
+type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
+
+const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!post) {
+ throw new NotFoundError(`Post with id "${postId}" not found!`)
+ }
+
+ return post
+}
+
+export const postQueryOptions = (postId: string) =>
+ queryOptions({
+ queryKey: ['posts', { postId }],
+ queryFn: () => fetchPost(postId),
+ })
+
+export const postsQueryOptions = queryOptions({
+ queryKey: ['posts'],
+ queryFn: () => fetchPosts(),
+})
diff --git a/e2e/solid-router/basic-solid-query/src/styles.css b/e2e/solid-router/basic-solid-query/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic-solid-query/tailwind.config.mjs b/e2e/solid-router/basic-solid-query/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic-solid-query/tests/app.spec.ts b/e2e/solid-router/basic-solid-query/tests/app.spec.ts
new file mode 100644
index 00000000000..3e5a69ccaae
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/tests/app.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/solid-router/basic-solid-query/tsconfig.json b/e2e/solid-router/basic-solid-query/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic-solid-query/vite.config.js b/e2e/solid-router/basic-solid-query/vite.config.js
new file mode 100644
index 00000000000..05041cc6d83
--- /dev/null
+++ b/e2e/solid-router/basic-solid-query/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/.gitignore b/e2e/solid-router/basic-virtual-file-based/.gitignore
new file mode 100644
index 00000000000..a6ea47e5085
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-router/basic-virtual-file-based/index.html b/e2e/solid-router/basic-virtual-file-based/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/basic-virtual-file-based/package.json b/e2e/solid-router/basic-virtual-file-based/package.json
new file mode 100644
index 00000000000..c97d54dbf14
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "tanstack-router-e2e-solid-basic-virtual-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/router-plugin": "workspace:^",
+ "@tanstack/virtual-file-routes": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/playwright.config.ts b/e2e/solid-router/basic-virtual-file-based/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/postcss.config.mjs b/e2e/solid-router/basic-virtual-file-based/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/routes.ts b/e2e/solid-router/basic-virtual-file-based/routes.ts
new file mode 100644
index 00000000000..6c2c144ec5a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/routes.ts
@@ -0,0 +1,22 @@
+import {
+ index,
+ layout,
+ physical,
+ rootRoute,
+ route,
+} from '@tanstack/virtual-file-routes'
+
+export const routes = rootRoute('root.tsx', [
+ index('home.tsx'),
+ route('/posts', 'posts/posts.tsx', [
+ index('posts/posts-home.tsx'),
+ route('$postId', 'posts/posts-detail.tsx'),
+ ]),
+ layout('first', 'layout/first-layout.tsx', [
+ layout('second', 'layout/second-layout.tsx', [
+ route('/layout-a', 'a.tsx'),
+ route('/layout-b', 'b.tsx'),
+ ]),
+ ]),
+ physical('/classic', 'file-based-subtree'),
+])
diff --git a/e2e/solid-router/basic-virtual-file-based/src/main.tsx b/e2e/solid-router/basic-virtual-file-based/src/main.tsx
new file mode 100644
index 00000000000..8128384193c
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/main.tsx
@@ -0,0 +1,25 @@
+import { render } from 'solid-js/web'
+import { RouterProvider, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultStaleTime: 5000,
+ scrollRestoration: true,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(() => , rootElement)
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/posts.tsx b/e2e/solid-router/basic-virtual-file-based/src/posts.tsx
new file mode 100644
index 00000000000..a42cb8d24e7
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/posts.tsx
@@ -0,0 +1,32 @@
+import { notFound } from '@tanstack/solid-router'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-virtual-file-based/src/routeTree.gen.ts
new file mode 100644
index 00000000000..fc2a2b44e11
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routeTree.gen.ts
@@ -0,0 +1,345 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/root'
+import { Route as postsPostsImport } from './routes/posts/posts'
+import { Route as layoutFirstLayoutImport } from './routes/layout/first-layout'
+import { Route as homeImport } from './routes/home'
+import { Route as postsPostsDetailImport } from './routes/posts/posts-detail'
+import { Route as layoutSecondLayoutImport } from './routes/layout/second-layout'
+import { Route as postsPostsHomeImport } from './routes/posts/posts-home'
+import { Route as ClassicHelloRouteImport } from './routes/file-based-subtree/hello/route'
+import { Route as ClassicHelloIndexImport } from './routes/file-based-subtree/hello/index'
+import { Route as ClassicHelloWorldImport } from './routes/file-based-subtree/hello/world'
+import { Route as ClassicHelloUniverseImport } from './routes/file-based-subtree/hello/universe'
+import { Route as bImport } from './routes/b'
+import { Route as aImport } from './routes/a'
+
+// Create/Update Routes
+
+const postsPostsRoute = postsPostsImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const layoutFirstLayoutRoute = layoutFirstLayoutImport.update({
+ id: '/_first',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const homeRoute = homeImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const postsPostsDetailRoute = postsPostsDetailImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => postsPostsRoute,
+} as any)
+
+const layoutSecondLayoutRoute = layoutSecondLayoutImport.update({
+ id: '/_second',
+ getParentRoute: () => layoutFirstLayoutRoute,
+} as any)
+
+const postsPostsHomeRoute = postsPostsHomeImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => postsPostsRoute,
+} as any)
+
+const ClassicHelloRouteRoute = ClassicHelloRouteImport.update({
+ id: '/classic/hello',
+ path: '/classic/hello',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ClassicHelloIndexRoute = ClassicHelloIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => ClassicHelloRouteRoute,
+} as any)
+
+const ClassicHelloWorldRoute = ClassicHelloWorldImport.update({
+ id: '/world',
+ path: '/world',
+ getParentRoute: () => ClassicHelloRouteRoute,
+} as any)
+
+const ClassicHelloUniverseRoute = ClassicHelloUniverseImport.update({
+ id: '/universe',
+ path: '/universe',
+ getParentRoute: () => ClassicHelloRouteRoute,
+} as any)
+
+const bRoute = bImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => layoutSecondLayoutRoute,
+} as any)
+
+const aRoute = aImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => layoutSecondLayoutRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof homeImport
+ parentRoute: typeof rootRoute
+ }
+ '/_first': {
+ id: '/_first'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof layoutFirstLayoutImport
+ parentRoute: typeof rootRoute
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof postsPostsImport
+ parentRoute: typeof rootRoute
+ }
+ '/classic/hello': {
+ id: '/classic/hello'
+ path: '/classic/hello'
+ fullPath: '/classic/hello'
+ preLoaderRoute: typeof ClassicHelloRouteImport
+ parentRoute: typeof rootRoute
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof postsPostsHomeImport
+ parentRoute: typeof postsPostsImport
+ }
+ '/_first/_second': {
+ id: '/_first/_second'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof layoutSecondLayoutImport
+ parentRoute: typeof layoutFirstLayoutImport
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof postsPostsDetailImport
+ parentRoute: typeof postsPostsImport
+ }
+ '/_first/_second/layout-a': {
+ id: '/_first/_second/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof aImport
+ parentRoute: typeof layoutSecondLayoutImport
+ }
+ '/_first/_second/layout-b': {
+ id: '/_first/_second/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof bImport
+ parentRoute: typeof layoutSecondLayoutImport
+ }
+ '/classic/hello/universe': {
+ id: '/classic/hello/universe'
+ path: '/universe'
+ fullPath: '/classic/hello/universe'
+ preLoaderRoute: typeof ClassicHelloUniverseImport
+ parentRoute: typeof ClassicHelloRouteImport
+ }
+ '/classic/hello/world': {
+ id: '/classic/hello/world'
+ path: '/world'
+ fullPath: '/classic/hello/world'
+ preLoaderRoute: typeof ClassicHelloWorldImport
+ parentRoute: typeof ClassicHelloRouteImport
+ }
+ '/classic/hello/': {
+ id: '/classic/hello/'
+ path: '/'
+ fullPath: '/classic/hello/'
+ preLoaderRoute: typeof ClassicHelloIndexImport
+ parentRoute: typeof ClassicHelloRouteImport
+ }
+ }
+}
+
+// Create and export the route tree
+
+interface layoutSecondLayoutRouteChildren {
+ aRoute: typeof aRoute
+ bRoute: typeof bRoute
+}
+
+const layoutSecondLayoutRouteChildren: layoutSecondLayoutRouteChildren = {
+ aRoute: aRoute,
+ bRoute: bRoute,
+}
+
+const layoutSecondLayoutRouteWithChildren =
+ layoutSecondLayoutRoute._addFileChildren(layoutSecondLayoutRouteChildren)
+
+interface layoutFirstLayoutRouteChildren {
+ layoutSecondLayoutRoute: typeof layoutSecondLayoutRouteWithChildren
+}
+
+const layoutFirstLayoutRouteChildren: layoutFirstLayoutRouteChildren = {
+ layoutSecondLayoutRoute: layoutSecondLayoutRouteWithChildren,
+}
+
+const layoutFirstLayoutRouteWithChildren =
+ layoutFirstLayoutRoute._addFileChildren(layoutFirstLayoutRouteChildren)
+
+interface postsPostsRouteChildren {
+ postsPostsHomeRoute: typeof postsPostsHomeRoute
+ postsPostsDetailRoute: typeof postsPostsDetailRoute
+}
+
+const postsPostsRouteChildren: postsPostsRouteChildren = {
+ postsPostsHomeRoute: postsPostsHomeRoute,
+ postsPostsDetailRoute: postsPostsDetailRoute,
+}
+
+const postsPostsRouteWithChildren = postsPostsRoute._addFileChildren(
+ postsPostsRouteChildren,
+)
+
+interface ClassicHelloRouteRouteChildren {
+ ClassicHelloUniverseRoute: typeof ClassicHelloUniverseRoute
+ ClassicHelloWorldRoute: typeof ClassicHelloWorldRoute
+ ClassicHelloIndexRoute: typeof ClassicHelloIndexRoute
+}
+
+const ClassicHelloRouteRouteChildren: ClassicHelloRouteRouteChildren = {
+ ClassicHelloUniverseRoute: ClassicHelloUniverseRoute,
+ ClassicHelloWorldRoute: ClassicHelloWorldRoute,
+ ClassicHelloIndexRoute: ClassicHelloIndexRoute,
+}
+
+const ClassicHelloRouteRouteWithChildren =
+ ClassicHelloRouteRoute._addFileChildren(ClassicHelloRouteRouteChildren)
+
+export interface FileRoutesByFullPath {
+ '/': typeof homeRoute
+ '': typeof layoutSecondLayoutRouteWithChildren
+ '/posts': typeof postsPostsRouteWithChildren
+ '/classic/hello': typeof ClassicHelloRouteRouteWithChildren
+ '/posts/': typeof postsPostsHomeRoute
+ '/posts/$postId': typeof postsPostsDetailRoute
+ '/layout-a': typeof aRoute
+ '/layout-b': typeof bRoute
+ '/classic/hello/universe': typeof ClassicHelloUniverseRoute
+ '/classic/hello/world': typeof ClassicHelloWorldRoute
+ '/classic/hello/': typeof ClassicHelloIndexRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof homeRoute
+ '': typeof layoutSecondLayoutRouteWithChildren
+ '/posts': typeof postsPostsHomeRoute
+ '/posts/$postId': typeof postsPostsDetailRoute
+ '/layout-a': typeof aRoute
+ '/layout-b': typeof bRoute
+ '/classic/hello/universe': typeof ClassicHelloUniverseRoute
+ '/classic/hello/world': typeof ClassicHelloWorldRoute
+ '/classic/hello': typeof ClassicHelloIndexRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof homeRoute
+ '/_first': typeof layoutFirstLayoutRouteWithChildren
+ '/posts': typeof postsPostsRouteWithChildren
+ '/classic/hello': typeof ClassicHelloRouteRouteWithChildren
+ '/posts/': typeof postsPostsHomeRoute
+ '/_first/_second': typeof layoutSecondLayoutRouteWithChildren
+ '/posts/$postId': typeof postsPostsDetailRoute
+ '/_first/_second/layout-a': typeof aRoute
+ '/_first/_second/layout-b': typeof bRoute
+ '/classic/hello/universe': typeof ClassicHelloUniverseRoute
+ '/classic/hello/world': typeof ClassicHelloWorldRoute
+ '/classic/hello/': typeof ClassicHelloIndexRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | ''
+ | '/posts'
+ | '/classic/hello'
+ | '/posts/'
+ | '/posts/$postId'
+ | '/layout-a'
+ | '/layout-b'
+ | '/classic/hello/universe'
+ | '/classic/hello/world'
+ | '/classic/hello/'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | ''
+ | '/posts'
+ | '/posts/$postId'
+ | '/layout-a'
+ | '/layout-b'
+ | '/classic/hello/universe'
+ | '/classic/hello/world'
+ | '/classic/hello'
+ id:
+ | '__root__'
+ | '/'
+ | '/_first'
+ | '/posts'
+ | '/classic/hello'
+ | '/posts/'
+ | '/_first/_second'
+ | '/posts/$postId'
+ | '/_first/_second/layout-a'
+ | '/_first/_second/layout-b'
+ | '/classic/hello/universe'
+ | '/classic/hello/world'
+ | '/classic/hello/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ homeRoute: typeof homeRoute
+ layoutFirstLayoutRoute: typeof layoutFirstLayoutRouteWithChildren
+ postsPostsRoute: typeof postsPostsRouteWithChildren
+ ClassicHelloRouteRoute: typeof ClassicHelloRouteRouteWithChildren
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ homeRoute: homeRoute,
+ layoutFirstLayoutRoute: layoutFirstLayoutRouteWithChildren,
+ postsPostsRoute: postsPostsRouteWithChildren,
+ ClassicHelloRouteRoute: ClassicHelloRouteRouteWithChildren,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/a.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/a.tsx
new file mode 100644
index 00000000000..055cba1e6f6
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/b.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/b.tsx
new file mode 100644
index 00000000000..c5bb8051af9
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx
new file mode 100644
index 00000000000..f7ff5379165
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/')({
+ component: () => This is the index
,
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx
new file mode 100644
index 00000000000..f4f30d84256
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx
@@ -0,0 +1,27 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello')({
+ component: () => (
+
+ Hello!
+ {' '}
+
+ say hello to the universe
+ {' '}
+
+ say hello to the world
+
+
+
+ ),
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx
new file mode 100644
index 00000000000..2a6bf16c377
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/universe')({
+ component: () => Hello /classic/hello/universe!
,
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx
new file mode 100644
index 00000000000..03edc7f484a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/world')({
+ component: () => Hello /classic/hello/world!
,
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx
new file mode 100644
index 00000000000..bdfb4c76768
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx
new file mode 100644
index 00000000000..5c77421bb29
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx
new file mode 100644
index 00000000000..9ab147de5e9
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx
new file mode 100644
index 00000000000..990f473ae8a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx
@@ -0,0 +1,27 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { fetchPost } from '../../posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ errorComponent: PostErrorComponent as any,
+ notFoundComponent: () => {
+ return Post not found
+ },
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post().title}
+
{post().body}
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx
new file mode 100644
index 00000000000..33d0386c195
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts.tsx
new file mode 100644
index 00000000000..9ae6dfc747d
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { fetchPosts } from '../../posts'
+
+export const Route = createFileRoute('/posts')({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/root.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/root.tsx
new file mode 100644
index 00000000000..95685a9cd3b
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/root.tsx
@@ -0,0 +1,69 @@
+import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ Subtree
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+ {/* Start rendering router matches */}
+ {/* */}
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/src/styles.css b/e2e/solid-router/basic-virtual-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/tailwind.config.mjs b/e2e/solid-router/basic-virtual-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/tests/app.spec.ts b/e2e/solid-router/basic-virtual-file-based/tests/app.spec.ts
new file mode 100644
index 00000000000..3e5a69ccaae
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/tests/app.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/solid-router/basic-virtual-file-based/tsconfig.json b/e2e/solid-router/basic-virtual-file-based/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic-virtual-file-based/vite.config.ts b/e2e/solid-router/basic-virtual-file-based/vite.config.ts
new file mode 100644
index 00000000000..2b456a8eaa1
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-file-based/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+import { routes } from './routes'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ TanStackRouterVite({ target: 'solid', virtualRouteConfig: routes }),
+ solid(),
+ ],
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/.gitignore b/e2e/solid-router/basic-virtual-named-export-config-file-based/.gitignore
new file mode 100644
index 00000000000..ea2b6bb1fc7
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+
+src/routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/index.html b/e2e/solid-router/basic-virtual-named-export-config-file-based/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/package.json b/e2e/solid-router/basic-virtual-named-export-config-file-based/package.json
new file mode 100644
index 00000000000..82d97318b3a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "tanstack-router-e2e-solid-basic-virtual-named-export-config-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/router-plugin": "workspace:^",
+ "@tanstack/virtual-file-routes": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/playwright.config.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/postcss.config.mjs b/e2e/solid-router/basic-virtual-named-export-config-file-based/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/routes.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/routes.ts
new file mode 100644
index 00000000000..6c2c144ec5a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/routes.ts
@@ -0,0 +1,22 @@
+import {
+ index,
+ layout,
+ physical,
+ rootRoute,
+ route,
+} from '@tanstack/virtual-file-routes'
+
+export const routes = rootRoute('root.tsx', [
+ index('home.tsx'),
+ route('/posts', 'posts/posts.tsx', [
+ index('posts/posts-home.tsx'),
+ route('$postId', 'posts/posts-detail.tsx'),
+ ]),
+ layout('first', 'layout/first-layout.tsx', [
+ layout('second', 'layout/second-layout.tsx', [
+ route('/layout-a', 'a.tsx'),
+ route('/layout-b', 'b.tsx'),
+ ]),
+ ]),
+ physical('/classic', 'file-based-subtree'),
+])
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/main.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/main.tsx
new file mode 100644
index 00000000000..8128384193c
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/main.tsx
@@ -0,0 +1,25 @@
+import { render } from 'solid-js/web'
+import { RouterProvider, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultStaleTime: 5000,
+ scrollRestoration: true,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(() => , rootElement)
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/posts.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/posts.tsx
new file mode 100644
index 00000000000..a42cb8d24e7
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/posts.tsx
@@ -0,0 +1,32 @@
+import { notFound } from '@tanstack/solid-router'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx
new file mode 100644
index 00000000000..055cba1e6f6
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx
new file mode 100644
index 00000000000..c5bb8051af9
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx
new file mode 100644
index 00000000000..f7ff5379165
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/')({
+ component: () => This is the index
,
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx
new file mode 100644
index 00000000000..f4f30d84256
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx
@@ -0,0 +1,27 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello')({
+ component: () => (
+
+ Hello!
+ {' '}
+
+ say hello to the universe
+ {' '}
+
+ say hello to the world
+
+
+
+ ),
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx
new file mode 100644
index 00000000000..2a6bf16c377
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/universe')({
+ component: () => Hello /classic/hello/universe!
,
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx
new file mode 100644
index 00000000000..03edc7f484a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/world')({
+ component: () => Hello /classic/hello/world!
,
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx
new file mode 100644
index 00000000000..bdfb4c76768
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx
new file mode 100644
index 00000000000..5c77421bb29
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx
new file mode 100644
index 00000000000..9ab147de5e9
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx
new file mode 100644
index 00000000000..990f473ae8a
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx
@@ -0,0 +1,27 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { fetchPost } from '../../posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ errorComponent: PostErrorComponent as any,
+ notFoundComponent: () => {
+ return Post not found
+ },
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post().title}
+
{post().body}
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx
new file mode 100644
index 00000000000..33d0386c195
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx
new file mode 100644
index 00000000000..9ae6dfc747d
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { fetchPosts } from '../../posts'
+
+export const Route = createFileRoute('/posts')({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx
new file mode 100644
index 00000000000..95685a9cd3b
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx
@@ -0,0 +1,69 @@
+import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ Subtree
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+ {/* Start rendering router matches */}
+ {/* */}
+ >
+ )
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/styles.css b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/tailwind.config.mjs b/e2e/solid-router/basic-virtual-named-export-config-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts
new file mode 100644
index 00000000000..3e5a69ccaae
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/tsconfig.json b/e2e/solid-router/basic-virtual-named-export-config-file-based/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/vite.config.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/vite.config.ts
new file mode 100644
index 00000000000..c20765965a7
--- /dev/null
+++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/vite.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ TanStackRouterVite({ target: 'solid', virtualRouteConfig: './routes.ts' }),
+ solid(),
+ ],
+})
diff --git a/e2e/solid-router/basic/.gitignore b/e2e/solid-router/basic/.gitignore
new file mode 100644
index 00000000000..8354e4d50d5
--- /dev/null
+++ b/e2e/solid-router/basic/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
\ No newline at end of file
diff --git a/e2e/solid-router/basic/index.html b/e2e/solid-router/basic/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/basic/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/basic/package.json b/e2e/solid-router/basic/package.json
new file mode 100644
index 00000000000..a39bbb9c796
--- /dev/null
+++ b/e2e/solid-router/basic/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "tanstack-router-e2e-solid-basic",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/basic/playwright.config.ts b/e2e/solid-router/basic/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/basic/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/basic/postcss.config.mjs b/e2e/solid-router/basic/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/basic/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/basic/src/main.tsx b/e2e/solid-router/basic/src/main.tsx
new file mode 100644
index 00000000000..f9d53db711b
--- /dev/null
+++ b/e2e/solid-router/basic/src/main.tsx
@@ -0,0 +1,235 @@
+import { render } from 'solid-js/web'
+import {
+ ErrorComponent,
+ Link,
+ Outlet,
+ RouterProvider,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+import { NotFoundError, fetchPost, fetchPosts } from './posts'
+import './styles.css'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+const rootRoute = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ View Transition
+ {' '}
+
+ View Transition types
+ {' '}
+
+ Layout
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+ {/* */}
+ >
+ )
+}
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+ )
+}
+
+export const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'posts',
+ loader: () => fetchPosts(),
+}).lazy(() => import('./posts.lazy').then((d) => d.Route))
+
+const postsIndexRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '/',
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
+
+function PostErrorComponent({ error }: ErrorComponentProps) {
+ if (error instanceof NotFoundError) {
+ return {error.message}
+ }
+
+ return
+}
+
+const postRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '$postId',
+ errorComponent: PostErrorComponent,
+ loader: ({ params }) => fetchPost(params.postId),
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = postRoute.useLoaderData()
+
+ return (
+
+
{post().title}
+
+
{post().body}
+
+ )
+}
+
+const layoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_layout',
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
+
+const layout2Route = createRoute({
+ getParentRoute: () => layoutRoute,
+ id: '_layout-2',
+ component: Layout2Component,
+})
+
+function Layout2Component() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
+
+const layoutARoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-a',
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
+
+const layoutBRoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-b',
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
+
+const routeTree = rootRoute.addChildren([
+ postsRoute.addChildren([postRoute, postsIndexRoute]),
+ layoutRoute.addChildren([
+ layout2Route.addChildren([layoutARoute, layoutBRoute]),
+ ]),
+ indexRoute,
+])
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultStaleTime: 5000,
+ scrollRestoration: true,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(() => , rootElement)
+}
diff --git a/e2e/solid-router/basic/src/posts.lazy.tsx b/e2e/solid-router/basic/src/posts.lazy.tsx
new file mode 100644
index 00000000000..5277a5979a8
--- /dev/null
+++ b/e2e/solid-router/basic/src/posts.lazy.tsx
@@ -0,0 +1,35 @@
+import { Link, Outlet, createLazyRoute } from '@tanstack/solid-router'
+
+export const Route = createLazyRoute('/posts')({
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/basic/src/posts.ts b/e2e/solid-router/basic/src/posts.ts
new file mode 100644
index 00000000000..54d62e57886
--- /dev/null
+++ b/e2e/solid-router/basic/src/posts.ts
@@ -0,0 +1,32 @@
+import axios from 'redaxios'
+
+export class NotFoundError extends Error {}
+
+type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!post) {
+ throw new NotFoundError(`Post with id "${postId}" not found!`)
+ }
+
+ return post
+}
diff --git a/e2e/solid-router/basic/src/styles.css b/e2e/solid-router/basic/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/basic/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/basic/tailwind.config.mjs b/e2e/solid-router/basic/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/basic/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/basic/tests/app.spec.ts b/e2e/solid-router/basic/tests/app.spec.ts
new file mode 100644
index 00000000000..5ccdafa513b
--- /dev/null
+++ b/e2e/solid-router/basic/tests/app.spec.ts
@@ -0,0 +1,47 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts', exact: true }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test('Navigating to a post page with viewTransition', async ({ page }) => {
+ await page.getByRole('link', { name: 'View Transition', exact: true }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating to a post page with viewTransition types', async ({
+ page,
+}) => {
+ await page.getByRole('link', { name: 'View Transition types' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
diff --git a/e2e/solid-router/basic/tsconfig.json b/e2e/solid-router/basic/tsconfig.json
new file mode 100644
index 00000000000..e011e786b59
--- /dev/null
+++ b/e2e/solid-router/basic/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "skipLibCheck": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/basic/vite.config.js b/e2e/solid-router/basic/vite.config.js
new file mode 100644
index 00000000000..05041cc6d83
--- /dev/null
+++ b/e2e/solid-router/basic/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/e2e/solid-router/rspack-basic-file-based/.gitignore b/e2e/solid-router/rspack-basic-file-based/.gitignore
new file mode 100644
index 00000000000..fbb2bd02932
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/.gitignore
@@ -0,0 +1,20 @@
+# Local
+.DS_Store
+*.local
+*.log*
+
+# Dist
+node_modules
+dist/
+
+# IDE
+.vscode/*
+!.vscode/extensions.json
+.idea
+
+# E2E
+src/routeTree.gen.ts
+test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-router/rspack-basic-file-based/README.md b/e2e/solid-router/rspack-basic-file-based/README.md
new file mode 100644
index 00000000000..93f18812e1c
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `pnpm install`
+- `pnpm dev`
diff --git a/e2e/solid-router/rspack-basic-file-based/package.json b/e2e/solid-router/rspack-basic-file-based/package.json
new file mode 100644
index 00000000000..ff73441f2ca
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "tanstack-router-e2e-solid-rspack-basic-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "rsbuild dev --port 3000",
+ "build": "rsbuild build && tsc --noEmit",
+ "preview": "rsbuild preview",
+ "start": "rsbuild preview",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@rsbuild/core": "^1.2.4",
+ "@rsbuild/plugin-babel": "^1.0.3",
+ "@rsbuild/plugin-solid": "^1.0.4",
+ "@tanstack/router-plugin": "workspace:^",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/playwright.config.ts b/e2e/solid-router/rspack-basic-file-based/playwright.config.ts
new file mode 100644
index 00000000000..5989adb1c7b
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `PUBLIC_SERVER_PORT=${PORT} pnpm build && PUBLIC_SERVER_PORT=${PORT} pnpm preview --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/rspack-basic-file-based/postcss.config.mjs b/e2e/solid-router/rspack-basic-file-based/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts b/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts
new file mode 100644
index 00000000000..93cd6b0f54f
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from '@rsbuild/core'
+import { pluginSolid } from '@rsbuild/plugin-solid'
+import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'
+import { pluginBabel } from '@rsbuild/plugin-babel'
+
+export default defineConfig({
+ plugins: [
+ pluginBabel({
+ include: /\.(?:jsx|tsx)$/,
+ }),
+ pluginSolid(),
+ ],
+ tools: {
+ rspack: {
+ plugins: [TanStackRouterRspack({ target: 'solid' })],
+ },
+ },
+})
diff --git a/e2e/solid-router/rspack-basic-file-based/src/app.tsx b/e2e/solid-router/rspack-basic-file-based/src/app.tsx
new file mode 100644
index 00000000000..0981f90396a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/app.tsx
@@ -0,0 +1,23 @@
+import { RouterProvider, createRouter } from '@tanstack/solid-router'
+
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ scrollRestoration: true,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+const App = () => {
+ return
+}
+
+export default App
diff --git a/e2e/solid-router/rspack-basic-file-based/src/env.d.ts b/e2e/solid-router/rspack-basic-file-based/src/env.d.ts
new file mode 100644
index 00000000000..b0ac762b091
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/e2e/solid-router/rspack-basic-file-based/src/index.tsx b/e2e/solid-router/rspack-basic-file-based/src/index.tsx
new file mode 100644
index 00000000000..3403ff7cbfc
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/index.tsx
@@ -0,0 +1,8 @@
+import { render } from 'solid-js/web'
+import App from './app'
+
+const rootEl = document.getElementById('root')
+
+if (rootEl) {
+ render(() => , rootEl)
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/posts.tsx b/e2e/solid-router/rspack-basic-file-based/src/posts.tsx
new file mode 100644
index 00000000000..a42cb8d24e7
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/posts.tsx
@@ -0,0 +1,32 @@
+import { notFound } from '@tanstack/solid-router'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/__root.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/__root.tsx
new file mode 100644
index 00000000000..eb544e319f7
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/__root.tsx
@@ -0,0 +1,61 @@
+import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+ {/* Start rendering router matches */}
+ {/* */}
+ >
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx
new file mode 100644
index 00000000000..d43b4ef5f5e
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 00000000000..7a5a3623a03
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 00000000000..b69951b2465
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 00000000000..30dbcce90fa
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx
new file mode 100644
index 00000000000..bdfb4c76768
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..55f8871d03f
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.$postId.tsx
@@ -0,0 +1,27 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { fetchPost } from '../posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ errorComponent: PostErrorComponent,
+ notFoundComponent: () => {
+ return Post not found
+ },
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post().title}
+
{post().body}
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.index.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..33d0386c195
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx
new file mode 100644
index 00000000000..11a999f50aa
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { fetchPosts } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/src/styles.css b/e2e/solid-router/rspack-basic-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/tailwind.config.mjs b/e2e/solid-router/rspack-basic-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/tests/app.spec.ts b/e2e/solid-router/rspack-basic-file-based/tests/app.spec.ts
new file mode 100644
index 00000000000..10beff655f0
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/tests/app.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#root')).toContainText("I'm a layout")
+ await expect(page.locator('#root')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#root')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#root')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/solid-router/rspack-basic-file-based/tsconfig.json b/e2e/solid-router/rspack-basic-file-based/tsconfig.json
new file mode 100644
index 00000000000..e4741a7cfe8
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "module": "ESNext",
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "strict": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "useDefineForClassFields": true,
+ "allowJs": true
+ },
+ "include": ["src", "playwright.config.ts", "tests"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/rspack-basic-file-based/tsr.config.json b/e2e/solid-router/rspack-basic-file-based/tsr.config.json
new file mode 100644
index 00000000000..15b57e5ea33
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-file-based/tsr.config.json
@@ -0,0 +1,4 @@
+{
+ "routesDirectory": "./src/routes",
+ "generatedRouteTree": "./src/routeTree.gen.ts"
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/.gitignore b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/.gitignore
new file mode 100644
index 00000000000..fbb2bd02932
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/.gitignore
@@ -0,0 +1,20 @@
+# Local
+.DS_Store
+*.local
+*.log*
+
+# Dist
+node_modules
+dist/
+
+# IDE
+.vscode/*
+!.vscode/extensions.json
+.idea
+
+# E2E
+src/routeTree.gen.ts
+test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/README.md b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/README.md
new file mode 100644
index 00000000000..93f18812e1c
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `pnpm install`
+- `pnpm dev`
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json
new file mode 100644
index 00000000000..0861b672d44
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "tanstack-router-e2e-solid-rspack-basic-virtual-named-export-config-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "rsbuild dev --port 3000",
+ "build": "rsbuild build && tsc --noEmit",
+ "preview": "rsbuild preview",
+ "start": "rsbuild preview",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@rsbuild/core": "^1.2.4",
+ "@rsbuild/plugin-babel": "^1.0.3",
+ "@rsbuild/plugin-solid": "^1.0.4",
+ "@tanstack/router-plugin": "workspace:^",
+ "@tanstack/virtual-file-routes": "workspace:^",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts
new file mode 100644
index 00000000000..912664a64e0
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `PUBLIC_SERVER_PORT=${PORT} pnpm build && PUBLIC_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/postcss.config.mjs b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/routes.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/routes.ts
new file mode 100644
index 00000000000..6c2c144ec5a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/routes.ts
@@ -0,0 +1,22 @@
+import {
+ index,
+ layout,
+ physical,
+ rootRoute,
+ route,
+} from '@tanstack/virtual-file-routes'
+
+export const routes = rootRoute('root.tsx', [
+ index('home.tsx'),
+ route('/posts', 'posts/posts.tsx', [
+ index('posts/posts-home.tsx'),
+ route('$postId', 'posts/posts-detail.tsx'),
+ ]),
+ layout('first', 'layout/first-layout.tsx', [
+ layout('second', 'layout/second-layout.tsx', [
+ route('/layout-a', 'a.tsx'),
+ route('/layout-b', 'b.tsx'),
+ ]),
+ ]),
+ physical('/classic', 'file-based-subtree'),
+])
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts
new file mode 100644
index 00000000000..93cd6b0f54f
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from '@rsbuild/core'
+import { pluginSolid } from '@rsbuild/plugin-solid'
+import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'
+import { pluginBabel } from '@rsbuild/plugin-babel'
+
+export default defineConfig({
+ plugins: [
+ pluginBabel({
+ include: /\.(?:jsx|tsx)$/,
+ }),
+ pluginSolid(),
+ ],
+ tools: {
+ rspack: {
+ plugins: [TanStackRouterRspack({ target: 'solid' })],
+ },
+ },
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx
new file mode 100644
index 00000000000..0981f90396a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx
@@ -0,0 +1,23 @@
+import { RouterProvider, createRouter } from '@tanstack/solid-router'
+
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ scrollRestoration: true,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+const App = () => {
+ return
+}
+
+export default App
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts
new file mode 100644
index 00000000000..b0ac762b091
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx
new file mode 100644
index 00000000000..3403ff7cbfc
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx
@@ -0,0 +1,8 @@
+import { render } from 'solid-js/web'
+import App from './app'
+
+const rootEl = document.getElementById('root')
+
+if (rootEl) {
+ render(() => , rootEl)
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx
new file mode 100644
index 00000000000..a42cb8d24e7
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx
@@ -0,0 +1,32 @@
+import { notFound } from '@tanstack/solid-router'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx
new file mode 100644
index 00000000000..055cba1e6f6
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx
new file mode 100644
index 00000000000..c5bb8051af9
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx
new file mode 100644
index 00000000000..f7ff5379165
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/')({
+ component: () => This is the index
,
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx
new file mode 100644
index 00000000000..f4f30d84256
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx
@@ -0,0 +1,27 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello')({
+ component: () => (
+
+ Hello!
+ {' '}
+
+ say hello to the universe
+ {' '}
+
+ say hello to the world
+
+
+
+ ),
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx
new file mode 100644
index 00000000000..2a6bf16c377
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/universe')({
+ component: () => Hello /classic/hello/universe!
,
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx
new file mode 100644
index 00000000000..03edc7f484a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/classic/hello/world')({
+ component: () => Hello /classic/hello/world!
,
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx
new file mode 100644
index 00000000000..bdfb4c76768
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx
new file mode 100644
index 00000000000..5c77421bb29
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx
new file mode 100644
index 00000000000..9ab147de5e9
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_first/_second')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx
new file mode 100644
index 00000000000..990f473ae8a
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx
@@ -0,0 +1,27 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { fetchPost } from '../../posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ errorComponent: PostErrorComponent as any,
+ notFoundComponent: () => {
+ return Post not found
+ },
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post().title}
+
{post().body}
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx
new file mode 100644
index 00000000000..33d0386c195
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx
new file mode 100644
index 00000000000..9ae6dfc747d
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { fetchPosts } from '../../posts'
+
+export const Route = createFileRoute('/posts')({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx
new file mode 100644
index 00000000000..95685a9cd3b
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx
@@ -0,0 +1,69 @@
+import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ Subtree
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+ {/* Start rendering router matches */}
+ {/* */}
+ >
+ )
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/styles.css b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tailwind.config.mjs b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts
new file mode 100644
index 00000000000..10beff655f0
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#root')).toContainText("I'm a layout")
+ await expect(page.locator('#root')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#root')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#root')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json
new file mode 100644
index 00000000000..88b60cea21f
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "module": "ESNext",
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "strict": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "useDefineForClassFields": true,
+ "allowJs": true
+ },
+ "include": ["src", "playwright.config.ts", "tests", "./routes.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json
new file mode 100644
index 00000000000..2759341ba21
--- /dev/null
+++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json
@@ -0,0 +1,5 @@
+{
+ "routesDirectory": "./src/routes",
+ "generatedRouteTree": "./src/routeTree.gen.ts",
+ "virtualRouteConfig": "./routes.ts"
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/.gitignore b/e2e/solid-router/scroll-restoration-sandbox-vite/.gitignore
new file mode 100644
index 00000000000..7baa77502b8
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+test-results
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/README.md b/e2e/solid-router/scroll-restoration-sandbox-vite/README.md
new file mode 100644
index 00000000000..31385a50056
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/README.md
@@ -0,0 +1,16 @@
+# Scroll Restoration Testing Sandbox with Vite
+
+To run this example:
+
+- `npm install`
+- `npm start`
+
+This sandbox is for testing the scroll restoration behavior.
+
+## Setup
+
+- Create your files in `src/routes` directory.
+- Make sure you update the arrays in the following files with the expected routes
+ - `tests/app.spec.ts` > routes array
+ - `src/routes/__root.tsx` > Nav component, routes array
+ - `src/routes/index.tsx` > Navigation test suite, routes array
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/index.html b/e2e/solid-router/scroll-restoration-sandbox-vite/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/package.json b/e2e/solid-router/scroll-restoration-sandbox-vite/package.json
new file mode 100644
index 00000000000..eaf3d8beaf9
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "tanstack-router-e2e-solid-scroll-restoration-sandbox-vite",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/router-devtools": "workspace:^",
+ "@tanstack/router-plugin": "workspace:^",
+ "@tanstack/zod-adapter": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/playwright.config.ts b/e2e/solid-router/scroll-restoration-sandbox-vite/playwright.config.ts
new file mode 100644
index 00000000000..2eeb79844fc
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/postcss.config.mjs b/e2e/solid-router/scroll-restoration-sandbox-vite/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/main.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/main.tsx
new file mode 100644
index 00000000000..65b505e952c
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/main.tsx
@@ -0,0 +1,23 @@
+import { render } from 'solid-js/web'
+import { RouterProvider, createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(() => , rootElement)
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/posts.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/posts.tsx
new file mode 100644
index 00000000000..56007aa0f6d
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/posts.tsx
@@ -0,0 +1,37 @@
+import axios from 'redaxios'
+
+export function sleep(ms: number) {
+ return new Promise((r) => setTimeout(r, ms))
+}
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export class PostNotFoundError extends Error {}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw new PostNotFoundError(`Post with id "${postId}" not found!`)
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts
new file mode 100644
index 00000000000..969ebba8d6d
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts
@@ -0,0 +1,199 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { createFileRoute } from '@tanstack/solid-router'
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as IndexImport } from './routes/index'
+import { Route as testsPageWithSearchImport } from './routes/(tests)/page-with-search'
+import { Route as testsNormalPageImport } from './routes/(tests)/normal-page'
+import { Route as testsLazyWithLoaderPageImport } from './routes/(tests)/lazy-with-loader-page'
+import { Route as testsLazyPageImport } from './routes/(tests)/lazy-page'
+
+// Create Virtual Routes
+
+const testsVirtualPageLazyImport = createFileRoute('/(tests)/virtual-page')()
+
+// Create/Update Routes
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const testsVirtualPageLazyRoute = testsVirtualPageLazyImport
+ .update({
+ id: '/(tests)/virtual-page',
+ path: '/virtual-page',
+ getParentRoute: () => rootRoute,
+ } as any)
+ .lazy(() => import('./routes/(tests)/virtual-page.lazy').then((d) => d.Route))
+
+const testsPageWithSearchRoute = testsPageWithSearchImport.update({
+ id: '/(tests)/page-with-search',
+ path: '/page-with-search',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const testsNormalPageRoute = testsNormalPageImport.update({
+ id: '/(tests)/normal-page',
+ path: '/normal-page',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const testsLazyWithLoaderPageRoute = testsLazyWithLoaderPageImport
+ .update({
+ id: '/(tests)/lazy-with-loader-page',
+ path: '/lazy-with-loader-page',
+ getParentRoute: () => rootRoute,
+ } as any)
+ .lazy(() =>
+ import('./routes/(tests)/lazy-with-loader-page.lazy').then((d) => d.Route),
+ )
+
+const testsLazyPageRoute = testsLazyPageImport
+ .update({
+ id: '/(tests)/lazy-page',
+ path: '/lazy-page',
+ getParentRoute: () => rootRoute,
+ } as any)
+ .lazy(() => import('./routes/(tests)/lazy-page.lazy').then((d) => d.Route))
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/lazy-page': {
+ id: '/(tests)/lazy-page'
+ path: '/lazy-page'
+ fullPath: '/lazy-page'
+ preLoaderRoute: typeof testsLazyPageImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/lazy-with-loader-page': {
+ id: '/(tests)/lazy-with-loader-page'
+ path: '/lazy-with-loader-page'
+ fullPath: '/lazy-with-loader-page'
+ preLoaderRoute: typeof testsLazyWithLoaderPageImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/normal-page': {
+ id: '/(tests)/normal-page'
+ path: '/normal-page'
+ fullPath: '/normal-page'
+ preLoaderRoute: typeof testsNormalPageImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/page-with-search': {
+ id: '/(tests)/page-with-search'
+ path: '/page-with-search'
+ fullPath: '/page-with-search'
+ preLoaderRoute: typeof testsPageWithSearchImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/virtual-page': {
+ id: '/(tests)/virtual-page'
+ path: '/virtual-page'
+ fullPath: '/virtual-page'
+ preLoaderRoute: typeof testsVirtualPageLazyImport
+ parentRoute: typeof rootRoute
+ }
+ }
+}
+
+// Create and export the route tree
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/lazy-page': typeof testsLazyPageRoute
+ '/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute
+ '/normal-page': typeof testsNormalPageRoute
+ '/page-with-search': typeof testsPageWithSearchRoute
+ '/virtual-page': typeof testsVirtualPageLazyRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/lazy-page': typeof testsLazyPageRoute
+ '/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute
+ '/normal-page': typeof testsNormalPageRoute
+ '/page-with-search': typeof testsPageWithSearchRoute
+ '/virtual-page': typeof testsVirtualPageLazyRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/(tests)/lazy-page': typeof testsLazyPageRoute
+ '/(tests)/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute
+ '/(tests)/normal-page': typeof testsNormalPageRoute
+ '/(tests)/page-with-search': typeof testsPageWithSearchRoute
+ '/(tests)/virtual-page': typeof testsVirtualPageLazyRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/lazy-page'
+ | '/lazy-with-loader-page'
+ | '/normal-page'
+ | '/page-with-search'
+ | '/virtual-page'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/lazy-page'
+ | '/lazy-with-loader-page'
+ | '/normal-page'
+ | '/page-with-search'
+ | '/virtual-page'
+ id:
+ | '__root__'
+ | '/'
+ | '/(tests)/lazy-page'
+ | '/(tests)/lazy-with-loader-page'
+ | '/(tests)/normal-page'
+ | '/(tests)/page-with-search'
+ | '/(tests)/virtual-page'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ testsLazyPageRoute: typeof testsLazyPageRoute
+ testsLazyWithLoaderPageRoute: typeof testsLazyWithLoaderPageRoute
+ testsNormalPageRoute: typeof testsNormalPageRoute
+ testsPageWithSearchRoute: typeof testsPageWithSearchRoute
+ testsVirtualPageLazyRoute: typeof testsVirtualPageLazyRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ testsLazyPageRoute: testsLazyPageRoute,
+ testsLazyWithLoaderPageRoute: testsLazyWithLoaderPageRoute,
+ testsNormalPageRoute: testsNormalPageRoute,
+ testsPageWithSearchRoute: testsPageWithSearchRoute,
+ testsVirtualPageLazyRoute: testsVirtualPageLazyRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.lazy.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.lazy.tsx
new file mode 100644
index 00000000000..2e7b3306d21
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.lazy.tsx
@@ -0,0 +1,16 @@
+import { createLazyFileRoute } from '@tanstack/solid-router'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createLazyFileRoute('/(tests)/lazy-page')({
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
lazy-page
+
+
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.tsx
new file mode 100644
index 00000000000..042a0537656
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.tsx
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/(tests)/lazy-page')({})
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.lazy.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.lazy.tsx
new file mode 100644
index 00000000000..2079777df0e
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.lazy.tsx
@@ -0,0 +1,16 @@
+import { createLazyFileRoute } from '@tanstack/solid-router'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createLazyFileRoute('/(tests)/lazy-with-loader-page')({
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
lazy-with-loader-page
+
+
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.tsx
new file mode 100644
index 00000000000..6f2cf280a47
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { sleep } from '../../posts'
+
+export const Route = createFileRoute('/(tests)/lazy-with-loader-page')({
+ loader: async () => {
+ await sleep(1000)
+ return { foo: 'bar' }
+ },
+})
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/normal-page.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/normal-page.tsx
new file mode 100644
index 00000000000..6151049c357
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/normal-page.tsx
@@ -0,0 +1,16 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createFileRoute('/(tests)/normal-page')({
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
normal-page
+
+
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/page-with-search.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/page-with-search.tsx
new file mode 100644
index 00000000000..4d5953ed67c
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/page-with-search.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createFileRoute('/(tests)/page-with-search')({
+ validateSearch: zodValidator(z.object({ where: z.string() })),
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
page-with-search
+
+
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/virtual-page.lazy.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/virtual-page.lazy.tsx
new file mode 100644
index 00000000000..8c6c8ef3530
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/virtual-page.lazy.tsx
@@ -0,0 +1,16 @@
+import { createLazyFileRoute } from '@tanstack/solid-router'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createLazyFileRoute('/(tests)/virtual-page')({
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
virtual-page
+
+
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/-components/scroll-block.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/-components/scroll-block.tsx
new file mode 100644
index 00000000000..0539293f92e
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/-components/scroll-block.tsx
@@ -0,0 +1,16 @@
+export const atTheTopId = 'at-the-top'
+export const atTheBottomId = 'at-the-bottom'
+
+export function ScrollBlock({ number = 100 }: { number?: number }) {
+ return (
+ <>
+
+ {Array.from({ length: number }).map((_, i) => (
+ {i}
+ ))}
+
+ At the bottom
+
+ >
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/__root.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/__root.tsx
new file mode 100644
index 00000000000..60d576d62be
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/__root.tsx
@@ -0,0 +1,61 @@
+import {
+ Link,
+ Outlet,
+ createRootRoute,
+ linkOptions,
+} from '@tanstack/solid-router'
+import { Dynamic } from 'solid-js/web'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+
+
+
+ {/* */}
+ >
+ )
+}
+
+function Nav({ type }: { type: 'header' | 'footer' }) {
+ const Elem = type === 'header' ? 'header' : 'footer'
+ const prefix = type === 'header' ? 'Head' : 'Foot'
+ return (
+
+
+ {prefix}-/
+ {' '}
+ {(
+ [
+ linkOptions({ to: '/normal-page' }),
+ linkOptions({ to: '/lazy-page' }),
+ linkOptions({ to: '/virtual-page' }),
+ linkOptions({ to: '/lazy-with-loader-page' }),
+ linkOptions({ to: '/page-with-search', search: { where: type } }),
+ ] as const
+ ).map((options, i) => (
+
+ {prefix}-{options.to}
+
+ ))}
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/index.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/index.tsx
new file mode 100644
index 00000000000..991ecc34353
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/index.tsx
@@ -0,0 +1,36 @@
+import { Link, createFileRoute, linkOptions } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: HomeComponent,
+})
+
+function HomeComponent() {
+ return (
+
+
Welcome Home!
+
+ The are the links to be tested when navigating away from the index page.
+ Otherwise known as NOT first-load tests, rather known as navigation
+ tests.
+
+ {(
+ [
+ linkOptions({ to: '/normal-page' }),
+ linkOptions({ to: '/lazy-page' }),
+ linkOptions({ to: '/virtual-page' }),
+ linkOptions({ to: '/lazy-with-loader-page' }),
+ linkOptions({ to: '/page-with-search', search: { where: 'footer' } }),
+ ] as const
+ ).map((options, i) => (
+
+
{options.to} tests
+
+
+ {options.to}#at-the-bottom
+
+
+
+ ))}
+
+ )
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/styles.css b/e2e/solid-router/scroll-restoration-sandbox-vite/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/tailwind.config.mjs b/e2e/solid-router/scroll-restoration-sandbox-vite/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/tests/app.spec.ts b/e2e/solid-router/scroll-restoration-sandbox-vite/tests/app.spec.ts
new file mode 100644
index 00000000000..e631efe77f9
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/tests/app.spec.ts
@@ -0,0 +1,51 @@
+import { expect, test } from '@playwright/test'
+
+test('Smoke - Renders home', async ({ page }) => {
+ await page.goto('/')
+ await expect(
+ page.getByRole('heading', { name: 'Welcome Home!' }),
+ ).toBeVisible()
+})
+
+// Test for scroll related stuff
+;[
+ { to: '/normal-page' },
+ { to: '/lazy-page' },
+ { to: '/virtual-page' },
+ { to: '/lazy-with-loader-page' },
+ { to: '/page-with-search', search: { where: 'footer' } },
+].forEach((options) => {
+ test(`On navigate to ${options.to} (from the header), scroll should be at top`, async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page.getByRole('link', { name: `Head-${options.to}` }).click()
+ await page.waitForTimeout(0)
+ await expect(page.getByTestId('at-the-top')).toBeInViewport()
+ })
+
+ // scroll should be at the bottom on navigation after the page is loaded
+ test(`On navigate via index page tests to ${options.to}, scroll should resolve at the bottom`, async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page
+ .getByRole('link', { name: `${options.to}#at-the-bottom` })
+ .click()
+ await page.waitForTimeout(0)
+ await expect(page.getByTestId('at-the-bottom')).toBeInViewport()
+ })
+
+ // scroll should be at the bottom on first load
+ test(`On first load of ${options.to}, scroll should resolve resolve at the bottom`, async ({
+ page,
+ }) => {
+ let url: string = options.to
+ if ('search' in options) {
+ url = `${url}?where=${options.search}`
+ }
+ await page.goto(`${url}#at-the-bottom`)
+ await page.waitForTimeout(0)
+ await expect(page.getByTestId('at-the-bottom')).toBeInViewport()
+ })
+})
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/tsconfig.dev.json b/e2e/solid-router/scroll-restoration-sandbox-vite/tsconfig.dev.json
new file mode 100644
index 00000000000..dc6e4479e9a
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/tsconfig.dev.json
@@ -0,0 +1,11 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/tsconfig.json b/e2e/solid-router/scroll-restoration-sandbox-vite/tsconfig.json
new file mode 100644
index 00000000000..98f80160757
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js b/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js
new file mode 100644
index 00000000000..0d2f08b695a
--- /dev/null
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [TanStackRouterVite({ target: 'solid' }), solid()],
+})
diff --git a/examples/solid/basic-solid-query/.gitignore b/examples/solid/basic-solid-query/.gitignore
new file mode 100644
index 00000000000..a6ea47e5085
--- /dev/null
+++ b/examples/solid/basic-solid-query/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/examples/solid/basic-solid-query/.vscode/settings.json b/examples/solid/basic-solid-query/.vscode/settings.json
new file mode 100644
index 00000000000..00b5278e580
--- /dev/null
+++ b/examples/solid/basic-solid-query/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "files.watcherExclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "search.exclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "files.readonlyInclude": {
+ "**/routeTree.gen.ts": true
+ }
+}
diff --git a/examples/solid/basic-solid-query/README.md b/examples/solid/basic-solid-query/README.md
new file mode 100644
index 00000000000..115199d292c
--- /dev/null
+++ b/examples/solid/basic-solid-query/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm start` or `yarn start`
diff --git a/examples/solid/basic-solid-query/index.html b/examples/solid/basic-solid-query/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/examples/solid/basic-solid-query/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/examples/solid/basic-solid-query/package.json b/examples/solid/basic-solid-query/package.json
new file mode 100644
index 00000000000..6e0c23b42a8
--- /dev/null
+++ b/examples/solid/basic-solid-query/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "tanstack-router-solid-example-solid-query",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tanstack/solid-query": "^5.66.0",
+ "@tanstack/solid-query-devtools": "^5.66.0",
+ "@tanstack/solid-router": "workspace:^",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17"
+ },
+ "devDependencies": {
+ "vite-plugin-solid": "^2.11.2",
+ "typescript": "^5.7.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/examples/solid/basic-solid-query/postcss.config.mjs b/examples/solid/basic-solid-query/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/examples/solid/basic-solid-query/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/solid/basic-solid-query/src/main.tsx b/examples/solid/basic-solid-query/src/main.tsx
new file mode 100644
index 00000000000..5326a9569b5
--- /dev/null
+++ b/examples/solid/basic-solid-query/src/main.tsx
@@ -0,0 +1,276 @@
+import {
+ ErrorComponent,
+ Link,
+ Outlet,
+ RouterProvider,
+ createRootRouteWithContext,
+ createRoute,
+ createRouter,
+ useRouter,
+} from '@tanstack/solid-router'
+
+import {
+ createQuery,
+ QueryClient,
+ QueryClientProvider,
+} from '@tanstack/solid-query'
+import { NotFoundError, postQueryOptions, postsQueryOptions } from './posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+import './styles.css'
+import { render } from 'solid-js/web'
+import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
+import { createEffect, createMemo, Suspense } from 'solid-js'
+
+const rootRoute = createRootRouteWithContext<{
+ queryClient: QueryClient
+}>()({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Layout
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+
+ {/* */}
+ >
+ )
+}
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexRouteComponent,
+})
+
+function IndexRouteComponent() {
+ return (
+
+
Welcome Home!
+
+ )
+}
+
+const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'posts',
+ loader: ({ context: { queryClient } }) =>
+ queryClient.ensureQueryData(postsQueryOptions),
+}).lazy(() => import('./posts.lazy').then((d) => d.Route))
+
+const postsIndexRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '/',
+ component: PostsIndexRouteComponent,
+})
+
+function PostsIndexRouteComponent() {
+ return Select a post.
+}
+
+const postRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '$postId',
+ errorComponent: PostErrorComponent,
+ loader: ({ context: { queryClient }, params: { postId } }) =>
+ queryClient.ensureQueryData(postQueryOptions(postId)),
+ component: PostRouteComponent,
+})
+
+function PostErrorComponent({ error, reset }: ErrorComponentProps) {
+ const router = useRouter()
+ if (error instanceof NotFoundError) {
+ return {error.message}
+ }
+
+ createEffect(() => {
+ reset()
+ queryClient.resetQueries()
+ })
+
+ return (
+
+ {
+ router.invalidate()
+ }}
+ >
+ retry
+
+
+
+ )
+}
+
+function PostRouteComponent() {
+ const params = postRoute.useParams()
+ const postQuery = createQuery(() => postQueryOptions(params().postId))
+ const post = createMemo(() => postQuery.data)
+
+ return (
+
+
{post()?.title}
+
{post()?.body}
+
+ )
+}
+
+const layoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_layout',
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
+
+const layout2Route = createRoute({
+ getParentRoute: () => layoutRoute,
+ id: '_layout-2',
+ component: Layout2Component,
+})
+
+function Layout2Component() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
+
+const layoutARoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-a',
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
+
+const layoutBRoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-b',
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
+
+const routeTree = rootRoute.addChildren([
+ postsRoute.addChildren([postRoute, postsIndexRoute]),
+ layoutRoute.addChildren([
+ layout2Route.addChildren([layoutARoute, layoutBRoute]),
+ ]),
+ indexRoute,
+])
+
+const queryClient = new QueryClient()
+
+// Set up a Router instance
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ // Since we're using React Query, we don't want loader calls to ever be stale
+ // This will ensure that the loader is always called when the route is preloaded or visited
+ defaultPreloadStaleTime: 0,
+ scrollRestoration: true,
+ context: {
+ queryClient,
+ },
+})
+
+// Register things for typesafety
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ render(
+ () => (
+
+
+
+ ),
+ rootElement,
+ )
+}
diff --git a/examples/solid/basic-solid-query/src/posts.lazy.tsx b/examples/solid/basic-solid-query/src/posts.lazy.tsx
new file mode 100644
index 00000000000..466926dd1be
--- /dev/null
+++ b/examples/solid/basic-solid-query/src/posts.lazy.tsx
@@ -0,0 +1,48 @@
+import * as React from 'react'
+import { Link, Outlet, createLazyRoute } from '@tanstack/solid-router'
+import { createQuery } from '@tanstack/solid-query'
+import { postsQueryOptions } from './posts'
+import { createMemo } from 'solid-js'
+
+export const Route = createLazyRoute('/posts')({
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const postsQuery = createQuery(() => postsQueryOptions)
+
+ const posts = createMemo(() => {
+ if (postsQuery.data) {
+ return postsQuery.data
+ } else {
+ return []
+ }
+ })
+
+ return (
+
+ )
+}
diff --git a/examples/solid/basic-solid-query/src/posts.ts b/examples/solid/basic-solid-query/src/posts.ts
new file mode 100644
index 00000000000..cc07134ab2d
--- /dev/null
+++ b/examples/solid/basic-solid-query/src/posts.ts
@@ -0,0 +1,44 @@
+import axios from 'redaxios'
+import { queryOptions } from '@tanstack/solid-query'
+
+export class NotFoundError extends Error {}
+
+type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
+
+const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!post) {
+ throw new NotFoundError(`Post with id "${postId}" not found!`)
+ }
+
+ return post
+}
+
+export const postQueryOptions = (postId: string) =>
+ queryOptions({
+ queryKey: ['posts', { postId }],
+ queryFn: () => fetchPost(postId),
+ })
+
+export const postsQueryOptions = queryOptions({
+ queryKey: ['posts'],
+ queryFn: () => fetchPosts(),
+})
diff --git a/examples/solid/basic-solid-query/src/styles.css b/examples/solid/basic-solid-query/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/examples/solid/basic-solid-query/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/examples/solid/basic-solid-query/tailwind.config.mjs b/examples/solid/basic-solid-query/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/examples/solid/basic-solid-query/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/examples/solid/basic-solid-query/tsconfig.dev.json b/examples/solid/basic-solid-query/tsconfig.dev.json
new file mode 100644
index 00000000000..285a09b0dcf
--- /dev/null
+++ b/examples/solid/basic-solid-query/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/examples/solid/basic-solid-query/tsconfig.json b/examples/solid/basic-solid-query/tsconfig.json
new file mode 100644
index 00000000000..a972899814c
--- /dev/null
+++ b/examples/solid/basic-solid-query/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/solid/basic-solid-query/vite.config.js b/examples/solid/basic-solid-query/vite.config.js
new file mode 100644
index 00000000000..05041cc6d83
--- /dev/null
+++ b/examples/solid/basic-solid-query/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/examples/solid/kitchen-sink-file-based/.gitignore b/examples/solid/kitchen-sink-file-based/.gitignore
new file mode 100644
index 00000000000..d451ff16c10
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
diff --git a/examples/solid/kitchen-sink-file-based/README.md b/examples/solid/kitchen-sink-file-based/README.md
new file mode 100644
index 00000000000..115199d292c
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm start` or `yarn start`
diff --git a/examples/solid/kitchen-sink-file-based/index.html b/examples/solid/kitchen-sink-file-based/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/examples/solid/kitchen-sink-file-based/package.json b/examples/solid/kitchen-sink-file-based/package.json
new file mode 100644
index 00000000000..fa171c899c5
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "tanstack-router-solid-example-kitchen-sink-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:*",
+ "@tanstack/router-devtools": "^1.99.6",
+ "@tanstack/router-plugin": "workspace:*",
+ "immer": "^10.1.1",
+ "solid-js": "^1.9.4",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "vite-plugin-solid": "^2.11.2",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.11"
+ }
+}
diff --git a/examples/solid/kitchen-sink-file-based/postcss.config.mjs b/examples/solid/kitchen-sink-file-based/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/components/Breadcrumbs.tsx b/examples/solid/kitchen-sink-file-based/src/components/Breadcrumbs.tsx
new file mode 100644
index 00000000000..0714a0d8460
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/components/Breadcrumbs.tsx
@@ -0,0 +1,28 @@
+import { isMatch, Link, useMatches } from '@tanstack/solid-router'
+
+export const Breadcrumbs = () => {
+ const matches = useMatches()
+
+ if (matches().some((match) => match.status === 'pending')) return null
+
+ const matchesWithCrumbs = matches().filter((match) =>
+ isMatch(match, 'loaderData.crumb'),
+ )
+
+ return (
+
+
+ {matchesWithCrumbs.map((match, i) => (
+
+
+ {match.loaderData?.crumb}
+
+ {i + 1 < matchesWithCrumbs.length ? (
+ {'>'}
+ ) : null}
+
+ ))}
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/components/InvoiceFields.tsx b/examples/solid/kitchen-sink-file-based/src/components/InvoiceFields.tsx
new file mode 100644
index 00000000000..a3da8363072
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/components/InvoiceFields.tsx
@@ -0,0 +1,28 @@
+/* eslint-disable @typescript-eslint/no-unnecessary-condition */
+import type { Invoice } from '../utils/mockTodos'
+
+export function InvoiceFields(props: { invoice: Invoice; disabled?: boolean }) {
+ return (
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/components/Spinner.tsx b/examples/solid/kitchen-sink-file-based/src/components/Spinner.tsx
new file mode 100644
index 00000000000..2c853ed6ffa
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/components/Spinner.tsx
@@ -0,0 +1,13 @@
+export function Spinner(props: { show?: boolean; wait?: `delay-${number}` }) {
+ return (
+
+ ⍥
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/hooks/useMutation.tsx b/examples/solid/kitchen-sink-file-based/src/hooks/useMutation.tsx
new file mode 100644
index 00000000000..a63b9cd3671
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/hooks/useMutation.tsx
@@ -0,0 +1,41 @@
+import * as Solid from 'solid-js'
+
+export function useMutation(opts: {
+ fn: (variables: TVariables) => Promise
+ onSuccess?: (ctx: { data: TData }) => void | Promise
+}) {
+ const [submittedAt, setSubmittedAt] = Solid.createSignal()
+ const [variables, setVariables] = Solid.createSignal()
+ const [error, setError] = Solid.createSignal()
+ const [data, setData] = Solid.createSignal()
+ const [status, setStatus] = Solid.createSignal<
+ 'idle' | 'pending' | 'success' | 'error'
+ >('idle')
+
+ const mutate = async (variables: TVariables): Promise => {
+ setStatus(() => 'pending')
+ setSubmittedAt(() => Date.now())
+ setVariables(() => variables)
+ //
+ try {
+ const data = await opts.fn(variables)
+ await opts.onSuccess?.({ data })
+ setStatus(() => 'success')
+ setError(() => undefined)
+ setData((prev) => data)
+ return data
+ } catch (err: any) {
+ setStatus(() => 'error')
+ setError(() => err)
+ }
+ }
+
+ return {
+ status,
+ variables,
+ submittedAt,
+ mutate,
+ error,
+ data,
+ }
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/hooks/useSessionStorage.tsx b/examples/solid/kitchen-sink-file-based/src/hooks/useSessionStorage.tsx
new file mode 100644
index 00000000000..c08528dde60
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/hooks/useSessionStorage.tsx
@@ -0,0 +1,21 @@
+import * as Solid from 'solid-js'
+
+export function useSessionStorage(key: string, initialValue: any) {
+ const [state, setState] = Solid.createSignal(
+ (() => {
+ const stored = sessionStorage.getItem(key)
+ console.log('stored', key, stored, initialValue, typeof stored)
+ const reutrnval =
+ stored && stored !== 'undefined' ? JSON.parse(stored) : initialValue
+ console.log('returning', reutrnval)
+ return reutrnval
+ })(),
+ )
+
+ Solid.createEffect(() => {
+ console.log('setting', JSON.stringify(state()))
+ sessionStorage.setItem(key, JSON.stringify(state()))
+ })
+
+ return [state, setState]
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/main.tsx b/examples/solid/kitchen-sink-file-based/src/main.tsx
new file mode 100644
index 00000000000..c30c1c6515f
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/main.tsx
@@ -0,0 +1,144 @@
+import * as Solid from 'solid-js'
+import {
+ ErrorComponent,
+ RouterProvider,
+ createRouter,
+} from '@tanstack/solid-router'
+import { auth } from './utils/auth'
+import { Spinner } from './components/Spinner'
+import { routeTree } from './routeTree.gen'
+import { useSessionStorage } from './hooks/useSessionStorage'
+import './styles.css'
+import { render } from 'solid-js/web'
+
+//
+
+const router = createRouter({
+ routeTree,
+ defaultPendingComponent: () => (
+
+
+
+ ),
+ defaultErrorComponent: ({ error }) => ,
+ context: {
+ auth: undefined!, // We'll inject this when we render
+ },
+ defaultPreload: 'intent',
+ scrollRestoration: true,
+})
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+function App() {
+ // This stuff is just to tweak our sandbox setup in real-time
+ const [loaderDelay, setLoaderDelay] = useSessionStorage(
+ 'loaderDelay',
+ 500,
+ )
+ const [pendingMs, setPendingMs] = useSessionStorage('pendingMs', 1000)
+ const [pendingMinMs, setPendingMinMs] = useSessionStorage(
+ 'pendingMinMs',
+ 500,
+ )
+
+ return (
+ <>
+
+
+
+ {
+ setLoaderDelay(() => 150)
+ }}
+ >
+ Fast
+
+ {
+ setLoaderDelay(() => 500)
+ }}
+ >
+ Fast 3G
+
+ {
+ setLoaderDelay(() => 2000)
+ }}
+ >
+ Slow 3G
+
+
+
+
Loader Delay: {loaderDelay()}ms
+
setLoaderDelay(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
+
+ {
+ setPendingMs(() => 1000)
+ setPendingMinMs(() => 500)
+ }}
+ >
+ Reset to Default
+
+
+
+
defaultPendingMs: {pendingMs()}ms
+
setPendingMs(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
defaultPendingMinMs: {pendingMinMs()}ms
+
setPendingMinMs(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
+
+ >
+ )
+}
+
+const rootElement = document.getElementById('app')!
+render(() => , rootElement!)
diff --git a/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts b/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts
new file mode 100644
index 00000000000..84714b24de1
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts
@@ -0,0 +1,473 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { createFileRoute } from '@tanstack/solid-router'
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as LoginImport } from './routes/login'
+import { Route as DashboardImport } from './routes/dashboard'
+import { Route as LayoutImport } from './routes/_layout'
+import { Route as AuthImport } from './routes/_auth'
+import { Route as IndexImport } from './routes/index'
+import { Route as DashboardIndexImport } from './routes/dashboard.index'
+import { Route as DashboardUsersImport } from './routes/dashboard.users'
+import { Route as DashboardInvoicesImport } from './routes/dashboard.invoices'
+import { Route as LayoutLayoutBImport } from './routes/_layout.layout-b'
+import { Route as LayoutLayoutAImport } from './routes/_layout.layout-a'
+import { Route as AuthProfileImport } from './routes/_auth.profile'
+import { Route as thisFolderIsNotInTheUrlRouteGroupImport } from './routes/(this-folder-is-not-in-the-url)/route-group'
+import { Route as DashboardUsersIndexImport } from './routes/dashboard.users.index'
+import { Route as DashboardInvoicesIndexImport } from './routes/dashboard.invoices.index'
+import { Route as DashboardUsersUserImport } from './routes/dashboard.users.user'
+import { Route as DashboardInvoicesInvoiceIdImport } from './routes/dashboard.invoices.$invoiceId'
+
+// Create Virtual Routes
+
+const ExpensiveIndexLazyImport = createFileRoute('/expensive/')()
+
+// Create/Update Routes
+
+const LoginRoute = LoginImport.update({
+ id: '/login',
+ path: '/login',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const DashboardRoute = DashboardImport.update({
+ id: '/dashboard',
+ path: '/dashboard',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LayoutRoute = LayoutImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const AuthRoute = AuthImport.update({
+ id: '/_auth',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ExpensiveIndexLazyRoute = ExpensiveIndexLazyImport.update({
+ id: '/expensive/',
+ path: '/expensive/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() =>
+ import('./routes/expensive/index.lazy').then((d) => d.Route),
+)
+
+const DashboardIndexRoute = DashboardIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
+const DashboardUsersRoute = DashboardUsersImport.update({
+ id: '/users',
+ path: '/users',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
+const DashboardInvoicesRoute = DashboardInvoicesImport.update({
+ id: '/invoices',
+ path: '/invoices',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
+const LayoutLayoutBRoute = LayoutLayoutBImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutRoute,
+} as any)
+
+const LayoutLayoutARoute = LayoutLayoutAImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutRoute,
+} as any)
+
+const AuthProfileRoute = AuthProfileImport.update({
+ id: '/profile',
+ path: '/profile',
+ getParentRoute: () => AuthRoute,
+} as any)
+
+const thisFolderIsNotInTheUrlRouteGroupRoute =
+ thisFolderIsNotInTheUrlRouteGroupImport.update({
+ id: '/(this-folder-is-not-in-the-url)/route-group',
+ path: '/route-group',
+ getParentRoute: () => rootRoute,
+ } as any)
+
+const DashboardUsersIndexRoute = DashboardUsersIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardUsersRoute,
+} as any)
+
+const DashboardInvoicesIndexRoute = DashboardInvoicesIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardInvoicesRoute,
+} as any)
+
+const DashboardUsersUserRoute = DashboardUsersUserImport.update({
+ id: '/user',
+ path: '/user',
+ getParentRoute: () => DashboardUsersRoute,
+} as any)
+
+const DashboardInvoicesInvoiceIdRoute = DashboardInvoicesInvoiceIdImport.update(
+ {
+ id: '/$invoiceId',
+ path: '/$invoiceId',
+ getParentRoute: () => DashboardInvoicesRoute,
+ } as any,
+)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/_auth': {
+ id: '/_auth'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof AuthImport
+ parentRoute: typeof rootRoute
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutImport
+ parentRoute: typeof rootRoute
+ }
+ '/dashboard': {
+ id: '/dashboard'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof DashboardImport
+ parentRoute: typeof rootRoute
+ }
+ '/login': {
+ id: '/login'
+ path: '/login'
+ fullPath: '/login'
+ preLoaderRoute: typeof LoginImport
+ parentRoute: typeof rootRoute
+ }
+ '/(this-folder-is-not-in-the-url)/route-group': {
+ id: '/(this-folder-is-not-in-the-url)/route-group'
+ path: '/route-group'
+ fullPath: '/route-group'
+ preLoaderRoute: typeof thisFolderIsNotInTheUrlRouteGroupImport
+ parentRoute: typeof rootRoute
+ }
+ '/_auth/profile': {
+ id: '/_auth/profile'
+ path: '/profile'
+ fullPath: '/profile'
+ preLoaderRoute: typeof AuthProfileImport
+ parentRoute: typeof AuthImport
+ }
+ '/_layout/layout-a': {
+ id: '/_layout/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayoutAImport
+ parentRoute: typeof LayoutImport
+ }
+ '/_layout/layout-b': {
+ id: '/_layout/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayoutBImport
+ parentRoute: typeof LayoutImport
+ }
+ '/dashboard/invoices': {
+ id: '/dashboard/invoices'
+ path: '/invoices'
+ fullPath: '/dashboard/invoices'
+ preLoaderRoute: typeof DashboardInvoicesImport
+ parentRoute: typeof DashboardImport
+ }
+ '/dashboard/users': {
+ id: '/dashboard/users'
+ path: '/users'
+ fullPath: '/dashboard/users'
+ preLoaderRoute: typeof DashboardUsersImport
+ parentRoute: typeof DashboardImport
+ }
+ '/dashboard/': {
+ id: '/dashboard/'
+ path: '/'
+ fullPath: '/dashboard/'
+ preLoaderRoute: typeof DashboardIndexImport
+ parentRoute: typeof DashboardImport
+ }
+ '/expensive/': {
+ id: '/expensive/'
+ path: '/expensive'
+ fullPath: '/expensive'
+ preLoaderRoute: typeof ExpensiveIndexLazyImport
+ parentRoute: typeof rootRoute
+ }
+ '/dashboard/invoices/$invoiceId': {
+ id: '/dashboard/invoices/$invoiceId'
+ path: '/$invoiceId'
+ fullPath: '/dashboard/invoices/$invoiceId'
+ preLoaderRoute: typeof DashboardInvoicesInvoiceIdImport
+ parentRoute: typeof DashboardInvoicesImport
+ }
+ '/dashboard/users/user': {
+ id: '/dashboard/users/user'
+ path: '/user'
+ fullPath: '/dashboard/users/user'
+ preLoaderRoute: typeof DashboardUsersUserImport
+ parentRoute: typeof DashboardUsersImport
+ }
+ '/dashboard/invoices/': {
+ id: '/dashboard/invoices/'
+ path: '/'
+ fullPath: '/dashboard/invoices/'
+ preLoaderRoute: typeof DashboardInvoicesIndexImport
+ parentRoute: typeof DashboardInvoicesImport
+ }
+ '/dashboard/users/': {
+ id: '/dashboard/users/'
+ path: '/'
+ fullPath: '/dashboard/users/'
+ preLoaderRoute: typeof DashboardUsersIndexImport
+ parentRoute: typeof DashboardUsersImport
+ }
+ }
+}
+
+// Create and export the route tree
+
+interface AuthRouteChildren {
+ AuthProfileRoute: typeof AuthProfileRoute
+}
+
+const AuthRouteChildren: AuthRouteChildren = {
+ AuthProfileRoute: AuthProfileRoute,
+}
+
+const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
+
+interface LayoutRouteChildren {
+ LayoutLayoutARoute: typeof LayoutLayoutARoute
+ LayoutLayoutBRoute: typeof LayoutLayoutBRoute
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayoutARoute: LayoutLayoutARoute,
+ LayoutLayoutBRoute: LayoutLayoutBRoute,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface DashboardInvoicesRouteChildren {
+ DashboardInvoicesInvoiceIdRoute: typeof DashboardInvoicesInvoiceIdRoute
+ DashboardInvoicesIndexRoute: typeof DashboardInvoicesIndexRoute
+}
+
+const DashboardInvoicesRouteChildren: DashboardInvoicesRouteChildren = {
+ DashboardInvoicesInvoiceIdRoute: DashboardInvoicesInvoiceIdRoute,
+ DashboardInvoicesIndexRoute: DashboardInvoicesIndexRoute,
+}
+
+const DashboardInvoicesRouteWithChildren =
+ DashboardInvoicesRoute._addFileChildren(DashboardInvoicesRouteChildren)
+
+interface DashboardUsersRouteChildren {
+ DashboardUsersUserRoute: typeof DashboardUsersUserRoute
+ DashboardUsersIndexRoute: typeof DashboardUsersIndexRoute
+}
+
+const DashboardUsersRouteChildren: DashboardUsersRouteChildren = {
+ DashboardUsersUserRoute: DashboardUsersUserRoute,
+ DashboardUsersIndexRoute: DashboardUsersIndexRoute,
+}
+
+const DashboardUsersRouteWithChildren = DashboardUsersRoute._addFileChildren(
+ DashboardUsersRouteChildren,
+)
+
+interface DashboardRouteChildren {
+ DashboardInvoicesRoute: typeof DashboardInvoicesRouteWithChildren
+ DashboardUsersRoute: typeof DashboardUsersRouteWithChildren
+ DashboardIndexRoute: typeof DashboardIndexRoute
+}
+
+const DashboardRouteChildren: DashboardRouteChildren = {
+ DashboardInvoicesRoute: DashboardInvoicesRouteWithChildren,
+ DashboardUsersRoute: DashboardUsersRouteWithChildren,
+ DashboardIndexRoute: DashboardIndexRoute,
+}
+
+const DashboardRouteWithChildren = DashboardRoute._addFileChildren(
+ DashboardRouteChildren,
+)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '': typeof LayoutRouteWithChildren
+ '/dashboard': typeof DashboardRouteWithChildren
+ '/login': typeof LoginRoute
+ '/route-group': typeof thisFolderIsNotInTheUrlRouteGroupRoute
+ '/profile': typeof AuthProfileRoute
+ '/layout-a': typeof LayoutLayoutARoute
+ '/layout-b': typeof LayoutLayoutBRoute
+ '/dashboard/invoices': typeof DashboardInvoicesRouteWithChildren
+ '/dashboard/users': typeof DashboardUsersRouteWithChildren
+ '/dashboard/': typeof DashboardIndexRoute
+ '/expensive': typeof ExpensiveIndexLazyRoute
+ '/dashboard/invoices/$invoiceId': typeof DashboardInvoicesInvoiceIdRoute
+ '/dashboard/users/user': typeof DashboardUsersUserRoute
+ '/dashboard/invoices/': typeof DashboardInvoicesIndexRoute
+ '/dashboard/users/': typeof DashboardUsersIndexRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '': typeof LayoutRouteWithChildren
+ '/login': typeof LoginRoute
+ '/route-group': typeof thisFolderIsNotInTheUrlRouteGroupRoute
+ '/profile': typeof AuthProfileRoute
+ '/layout-a': typeof LayoutLayoutARoute
+ '/layout-b': typeof LayoutLayoutBRoute
+ '/dashboard': typeof DashboardIndexRoute
+ '/expensive': typeof ExpensiveIndexLazyRoute
+ '/dashboard/invoices/$invoiceId': typeof DashboardInvoicesInvoiceIdRoute
+ '/dashboard/users/user': typeof DashboardUsersUserRoute
+ '/dashboard/invoices': typeof DashboardInvoicesIndexRoute
+ '/dashboard/users': typeof DashboardUsersIndexRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/_auth': typeof AuthRouteWithChildren
+ '/_layout': typeof LayoutRouteWithChildren
+ '/dashboard': typeof DashboardRouteWithChildren
+ '/login': typeof LoginRoute
+ '/(this-folder-is-not-in-the-url)/route-group': typeof thisFolderIsNotInTheUrlRouteGroupRoute
+ '/_auth/profile': typeof AuthProfileRoute
+ '/_layout/layout-a': typeof LayoutLayoutARoute
+ '/_layout/layout-b': typeof LayoutLayoutBRoute
+ '/dashboard/invoices': typeof DashboardInvoicesRouteWithChildren
+ '/dashboard/users': typeof DashboardUsersRouteWithChildren
+ '/dashboard/': typeof DashboardIndexRoute
+ '/expensive/': typeof ExpensiveIndexLazyRoute
+ '/dashboard/invoices/$invoiceId': typeof DashboardInvoicesInvoiceIdRoute
+ '/dashboard/users/user': typeof DashboardUsersUserRoute
+ '/dashboard/invoices/': typeof DashboardInvoicesIndexRoute
+ '/dashboard/users/': typeof DashboardUsersIndexRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | ''
+ | '/dashboard'
+ | '/login'
+ | '/route-group'
+ | '/profile'
+ | '/layout-a'
+ | '/layout-b'
+ | '/dashboard/invoices'
+ | '/dashboard/users'
+ | '/dashboard/'
+ | '/expensive'
+ | '/dashboard/invoices/$invoiceId'
+ | '/dashboard/users/user'
+ | '/dashboard/invoices/'
+ | '/dashboard/users/'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | ''
+ | '/login'
+ | '/route-group'
+ | '/profile'
+ | '/layout-a'
+ | '/layout-b'
+ | '/dashboard'
+ | '/expensive'
+ | '/dashboard/invoices/$invoiceId'
+ | '/dashboard/users/user'
+ | '/dashboard/invoices'
+ | '/dashboard/users'
+ id:
+ | '__root__'
+ | '/'
+ | '/_auth'
+ | '/_layout'
+ | '/dashboard'
+ | '/login'
+ | '/(this-folder-is-not-in-the-url)/route-group'
+ | '/_auth/profile'
+ | '/_layout/layout-a'
+ | '/_layout/layout-b'
+ | '/dashboard/invoices'
+ | '/dashboard/users'
+ | '/dashboard/'
+ | '/expensive/'
+ | '/dashboard/invoices/$invoiceId'
+ | '/dashboard/users/user'
+ | '/dashboard/invoices/'
+ | '/dashboard/users/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ AuthRoute: typeof AuthRouteWithChildren
+ LayoutRoute: typeof LayoutRouteWithChildren
+ DashboardRoute: typeof DashboardRouteWithChildren
+ LoginRoute: typeof LoginRoute
+ thisFolderIsNotInTheUrlRouteGroupRoute: typeof thisFolderIsNotInTheUrlRouteGroupRoute
+ ExpensiveIndexLazyRoute: typeof ExpensiveIndexLazyRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ AuthRoute: AuthRouteWithChildren,
+ LayoutRoute: LayoutRouteWithChildren,
+ DashboardRoute: DashboardRouteWithChildren,
+ LoginRoute: LoginRoute,
+ thisFolderIsNotInTheUrlRouteGroupRoute:
+ thisFolderIsNotInTheUrlRouteGroupRoute,
+ ExpensiveIndexLazyRoute: ExpensiveIndexLazyRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/(this-folder-is-not-in-the-url)/route-group.tsx b/examples/solid/kitchen-sink-file-based/src/routes/(this-folder-is-not-in-the-url)/route-group.tsx
new file mode 100644
index 00000000000..c06e439a1b7
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/(this-folder-is-not-in-the-url)/route-group.tsx
@@ -0,0 +1,17 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute(
+ '/(this-folder-is-not-in-the-url)/route-group',
+)({
+ component: RouteGroupExample,
+})
+
+function RouteGroupExample() {
+ return (
+
+
Welcome to the Route Group Example!
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/__root.tsx b/examples/solid/kitchen-sink-file-based/src/routes/__root.tsx
new file mode 100644
index 00000000000..8b2feb3bee7
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/__root.tsx
@@ -0,0 +1,81 @@
+import * as Solid from 'solid-js'
+import {
+ Link,
+ Outlet,
+ createRootRouteWithContext,
+ useRouterState,
+} from '@tanstack/solid-router'
+// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+import { Spinner } from '../components/Spinner'
+import { Breadcrumbs } from '../components/Breadcrumbs'
+import type { Auth } from '../utils/auth'
+
+function RouterSpinner() {
+ const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
+ return
+}
+
+export const Route = createRootRouteWithContext<{
+ auth: Auth
+}>()({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+
Kitchen Sink
+
+ {/* Show a global spinner when the router is transitioning */}
+
+
+
+
+
+
+ {(
+ [
+ ['/', 'Home'],
+ ['/dashboard', 'Dashboard'],
+ ['/expensive', 'Expensive'],
+ ['/layout-a', 'Layout A'],
+ ['/layout-b', 'Layout B'],
+ ['/profile', 'Profile'],
+ ['/login', 'Login'],
+ ['/route-group', 'Route Group'],
+ ] as const
+ ).map(([to, label]) => {
+ return (
+
+
+ {label}
+
+
+ )
+ })}
+
+
+ {/* Render our first route match */}
+
+
+
+
+ {/* */}
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/_auth.profile.tsx b/examples/solid/kitchen-sink-file-based/src/routes/_auth.profile.tsx
new file mode 100644
index 00000000000..f67d367ee18
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/_auth.profile.tsx
@@ -0,0 +1,18 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_auth/profile')({
+ component: ProfileComponent,
+})
+
+function ProfileComponent() {
+ const context = Route.useRouteContext()
+
+ return (
+
+
+ Username:{context().username}
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/_auth.tsx b/examples/solid/kitchen-sink-file-based/src/routes/_auth.tsx
new file mode 100644
index 00000000000..a5b98f86317
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/_auth.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+import { auth } from '../utils/auth'
+
+export const Route = createFileRoute('/_auth')({
+ // Before loading, authenticate the user via our auth context
+ // This will also happen during prefetching (e.g. hovering over links, etc)
+ beforeLoad: ({ context, location }) => {
+ // If the user is logged out, redirect them to the login page
+ if (context.auth.status === 'loggedOut') {
+ throw redirect({
+ to: '/login',
+ search: {
+ // Use the current location to power a redirect after login
+ // (Do not use `router.state.resolvedLocation` as it can
+ // potentially lag behind the actual current location)
+ redirect: location.href,
+ },
+ })
+ }
+
+ // Otherwise, return the user in context
+ return {
+ username: auth.username,
+ }
+ },
+})
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/_layout.layout-a.tsx b/examples/solid/kitchen-sink-file-based/src/routes/_layout.layout-a.tsx
new file mode 100644
index 00000000000..fe5b0c8bcc0
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/_layout.layout-a.tsx
@@ -0,0 +1,10 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm A!
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/_layout.layout-b.tsx b/examples/solid/kitchen-sink-file-based/src/routes/_layout.layout-b.tsx
new file mode 100644
index 00000000000..e83216593aa
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/_layout.layout-b.tsx
@@ -0,0 +1,10 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm B!
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/_layout.tsx b/examples/solid/kitchen-sink-file-based/src/routes/_layout.tsx
new file mode 100644
index 00000000000..a7f5441d539
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import * as Solid from 'solid-js'
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.index.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.index.tsx
new file mode 100644
index 00000000000..f967ed4f4b3
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.index.tsx
@@ -0,0 +1,21 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+import { fetchInvoices } from '../utils/mockTodos'
+
+export const Route = createFileRoute('/dashboard/')({
+ loader: () => fetchInvoices(),
+ component: DashboardIndexComponent,
+})
+
+function DashboardIndexComponent() {
+ const invoices = Route.useLoaderData()
+
+ return (
+
+
+ Welcome to the dashboard! You have{' '}
+ {invoices.length} total invoices .
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
new file mode 100644
index 00000000000..c5c35e15b98
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
@@ -0,0 +1,126 @@
+import * as Solid from 'solid-js'
+import {
+ Link,
+ createFileRoute,
+ useNavigate,
+ useRouter,
+} from '@tanstack/solid-router'
+import { z } from 'zod'
+import { InvoiceFields } from '../components/InvoiceFields'
+import { useMutation } from '../hooks/useMutation'
+import { fetchInvoiceById, patchInvoice } from '../utils/mockTodos'
+
+export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
+ params: {
+ parse: (params) => ({
+ invoiceId: z.number().int().parse(Number(params.invoiceId)),
+ }),
+ stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
+ },
+ validateSearch: (search) =>
+ z
+ .object({
+ showNotes: z.boolean().optional(),
+ notes: z.string().optional(),
+ })
+ .parse(search),
+ loader: ({ params: { invoiceId } }) => fetchInvoiceById(invoiceId),
+ component: InvoiceComponent,
+})
+
+function InvoiceComponent() {
+ const search = Route.useSearch()
+ const navigate = useNavigate({ from: Route.fullPath })
+ const invoice = Route.useLoaderData()
+ const router = useRouter()
+ const updateInvoiceMutation = useMutation({
+ fn: patchInvoice,
+ onSuccess: () => router.invalidate(),
+ })
+ const [notes, setNotes] = Solid.createSignal(search().notes ?? '')
+
+ Solid.createEffect(() => {
+ navigate({
+ search: (old) => ({
+ ...old,
+ notes: notes() ? notes() : undefined,
+ }),
+ replace: true,
+ params: true,
+ })
+ })
+
+ return (
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx
new file mode 100644
index 00000000000..936a2b01b37
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.index.tsx
@@ -0,0 +1,65 @@
+import * as Solid from 'solid-js'
+import { createFileRoute, useRouter } from '@tanstack/solid-router'
+import { InvoiceFields } from '../components/InvoiceFields'
+import { Spinner } from '../components/Spinner'
+import { useMutation } from '../hooks/useMutation'
+import { postInvoice } from '../utils/mockTodos'
+import type { Invoice } from '../utils/mockTodos'
+
+export const Route = createFileRoute('/dashboard/invoices/')({
+ component: InvoicesIndexComponent,
+})
+
+function InvoicesIndexComponent() {
+ const router = useRouter()
+
+ const createInvoiceMutation = useMutation({
+ fn: postInvoice,
+ onSuccess: () => router.invalidate(),
+ })
+
+ return (
+ <>
+
+
{
+ event.preventDefault()
+ event.stopPropagation()
+ const formData = new FormData(event.target as HTMLFormElement)
+ createInvoiceMutation.mutate({
+ title: formData.get('title') as string,
+ body: formData.get('body') as string,
+ })
+ }}
+ class="space-y-2"
+ >
+ Create a new Invoice:
+
+
+
+ {createInvoiceMutation.status() === 'pending' ? (
+ <>
+ Creating
+ >
+ ) : (
+ 'Create'
+ )}
+
+
+ {createInvoiceMutation.status() === 'success' ? (
+
+ Created!
+
+ ) : createInvoiceMutation.status() === 'error' ? (
+
+ Failed to create.
+
+ ) : null}
+
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx
new file mode 100644
index 00000000000..11668cac8ec
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.invoices.tsx
@@ -0,0 +1,58 @@
+import * as Solid from 'solid-js'
+import {
+ Link,
+ MatchRoute,
+ Outlet,
+ createFileRoute,
+} from '@tanstack/solid-router'
+import { Spinner } from '../components/Spinner'
+import { fetchInvoices } from '../utils/mockTodos'
+
+export const Route = createFileRoute('/dashboard/invoices')({
+ loader: () => fetchInvoices(),
+ component: InvoicesComponent,
+})
+
+function InvoicesComponent() {
+ const invoices = Route.useLoaderData()
+
+ return (
+
+
+
+ {(invoice) => {
+ return (
+
+
+
+ #{invoice.id} - {invoice.title.slice(0, 10)}{' '}
+
+ {(match) => }
+
+
+
+
+ )
+ }}
+
+
+
+
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.tsx
new file mode 100644
index 00000000000..c10209e8417
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.tsx
@@ -0,0 +1,53 @@
+import * as Solid from 'solid-js'
+import {
+ Link,
+ Outlet,
+ createFileRoute,
+ linkOptions,
+} from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/dashboard')({
+ component: DashboardComponent,
+ loader: () => ({
+ crumb: 'Dashboard',
+ }),
+})
+
+const options = [
+ linkOptions({
+ to: '/dashboard',
+ label: 'Summary',
+ activeOptions: { exact: true },
+ }),
+ linkOptions({
+ to: '/dashboard/invoices',
+ label: 'Invoices',
+ }),
+ linkOptions({
+ to: '/dashboard/users',
+ label: 'Users',
+ }),
+]
+
+function DashboardComponent() {
+ return (
+ <>
+
+
Dashboard
+
+
+
+ {options.map((option) => {
+ return (
+
+ {option.label}
+
+ )
+ })}
+
+
+
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx
new file mode 100644
index 00000000000..268107589da
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.index.tsx
@@ -0,0 +1,30 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/dashboard/users/')({
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return (
+
+
+ Normally, setting default search parameters would either need to be done
+ manually in every link to a page, or as a side-effect (not a great
+ experience).
+
+
+ Instead, we can use search filters to provide defaults
+ or even persist search params for links to routes (and child routes).
+
+
+ A good example of this is the sorting and filtering of the users list.
+ In a traditional router, both would be lost while navigating around
+ individual users or even changing each sort/filter option unless each
+ state was manually passed from the current route into each new link we
+ created (that's a lot of tedious and error-prone work). With TanStack
+ router and search filters, they are persisted with little effort.
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.tsx
new file mode 100644
index 00000000000..dee3a070ecc
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.tsx
@@ -0,0 +1,146 @@
+import * as Solid from 'solid-js'
+import {
+ Link,
+ MatchRoute,
+ Outlet,
+ createFileRoute,
+ retainSearchParams,
+ useNavigate,
+ useRouterState,
+} from '@tanstack/solid-router'
+import { z } from 'zod'
+import { createMemo } from 'solid-js'
+import { Spinner } from '../components/Spinner'
+import { fetchUsers } from '../utils/mockTodos'
+
+type UsersViewSortBy = 'name' | 'id' | 'email'
+
+export const Route = createFileRoute('/dashboard/users')({
+ validateSearch: z.object({
+ usersView: z
+ .object({
+ sortBy: z.enum(['name', 'id', 'email']).optional(),
+ filterBy: z.string().optional(),
+ })
+ .optional(),
+ }).parse,
+ search: {
+ // Retain the usersView search param while navigating
+ // within or to this route (or it's children!)
+ middlewares: [retainSearchParams(['usersView'])],
+ },
+ loaderDeps: ({ search }) => ({
+ filterBy: search.usersView?.filterBy,
+ sortBy: search.usersView?.sortBy,
+ }),
+ loader: async ({ deps }) => {
+ const users = await fetchUsers(deps)
+ return { users, crumb: 'Users' }
+ },
+ component: UsersComponent,
+})
+
+function UsersComponent() {
+ const navigate = useNavigate({ from: Route.fullPath })
+ const search = Route.useSearch()
+ const loaderData = Route.useLoaderData()
+
+ const users = createMemo(() => loaderData()?.users)
+ const sortBy = createMemo(() => search().usersView?.sortBy ?? 'name')
+ const filterBy = createMemo(() => search().usersView?.filterBy)
+
+ const [filterDraft, setFilterDraft] = Solid.createSignal(filterBy() ?? '')
+
+ Solid.createEffect(() => {
+ setFilterDraft(filterBy() ?? '')
+ })
+
+ const setSortBy = (sortBy: UsersViewSortBy) =>
+ navigate({
+ search: (old) => {
+ return {
+ ...old,
+ usersView: {
+ ...(old.usersView ?? {}),
+ sortBy,
+ },
+ }
+ },
+ replace: true,
+ })
+
+ Solid.createEffect(() => {
+ navigate({
+ search: (old) => {
+ return {
+ ...old,
+ usersView: {
+ ...old.usersView,
+ filterBy: filterDraft() || undefined,
+ },
+ }
+ },
+ replace: true,
+ })
+ })
+
+ return (
+
+
+
+
Sort By:
+
setSortBy(e.target.value as UsersViewSortBy)}
+ class="flex-1 border p-1 px-2 rounded"
+ >
+ {['name', 'id', 'email'].map((d) => {
+ return
+ })}
+
+
+
+
Filter By:
+
setFilterDraft(e.target.value)}
+ placeholder="Search Names..."
+ class="min-w-0 flex-1 border p-1 px-2 rounded"
+ />
+
+
+ {(user) => {
+ return (
+
+
+
+ {user.name}{' '}
+
+ {(match) => }
+
+
+
+
+ )
+ }}
+
+
+
+
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.user.tsx b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.user.tsx
new file mode 100644
index 00000000000..84cecbe1c01
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/dashboard.users.user.tsx
@@ -0,0 +1,32 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { fetchUserById } from '../utils/mockTodos'
+
+export const Route = createFileRoute('/dashboard/users/user')({
+ validateSearch: z.object({
+ userId: z.number(),
+ }),
+ loaderDeps: ({ search: { userId } }) => ({ userId }),
+ loader: async ({ deps: { userId } }) => {
+ const user = await fetchUserById(userId)
+ return {
+ user,
+ crumb: user?.name,
+ }
+ },
+ component: UserComponent,
+})
+
+function UserComponent() {
+ const data = Route.useLoaderData()
+
+ return (
+ <>
+ {data()?.user?.name}
+
+ {JSON.stringify(data()?.user, null, 2)}
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/expensive/-components/Expensive.tsx b/examples/solid/kitchen-sink-file-based/src/routes/expensive/-components/Expensive.tsx
new file mode 100644
index 00000000000..23ec0826c13
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/expensive/-components/Expensive.tsx
@@ -0,0 +1,10 @@
+import * as Solid from 'solid-js'
+
+export default function Expensive() {
+ return (
+
+ I am an "expensive" component... which really just means that I was
+ code-split 😉
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/expensive/index.lazy.tsx b/examples/solid/kitchen-sink-file-based/src/routes/expensive/index.lazy.tsx
new file mode 100644
index 00000000000..0cef077ce7a
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/expensive/index.lazy.tsx
@@ -0,0 +1,6 @@
+import { createLazyFileRoute } from '@tanstack/solid-router'
+import Expensive from './-components/Expensive'
+
+export const Route = createLazyFileRoute('/expensive/')({
+ component: Expensive,
+})
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/index.tsx b/examples/solid/kitchen-sink-file-based/src/routes/index.tsx
new file mode 100644
index 00000000000..3301318ef35
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/index.tsx
@@ -0,0 +1,37 @@
+import * as Solid from 'solid-js'
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+
+ 1 New Invoice
+
+
+
+ As you navigate around take note of the UX. It should feel
+ suspense-like, where routes are only rendered once all of their data and
+ elements are ready.
+
+ To exaggerate async effects, play with the artificial request delay
+ slider in the bottom-left corner.
+
+ The last 2 sliders determine if link-hover preloading is enabled (and
+ how long those preloads stick around) and also whether to cache rendered
+ route data (and for how long). Both of these default to 0 (or off).
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/routes/login.tsx b/examples/solid/kitchen-sink-file-based/src/routes/login.tsx
new file mode 100644
index 00000000000..1a4bed3e9c3
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/routes/login.tsx
@@ -0,0 +1,76 @@
+import * as Solid from 'solid-js'
+import { createFileRoute, useRouter } from '@tanstack/solid-router'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/login')({
+ validateSearch: z.object({
+ redirect: z.string().optional(),
+ }),
+}).update({
+ component: LoginComponent,
+})
+
+function LoginComponent() {
+ const router = useRouter()
+
+ const context = Route.useRouteContext({
+ select: ({ auth }) => ({ auth, status: auth.status }),
+ })
+
+ const search = Route.useSearch()
+ const [username, setUsername] = Solid.createSignal('')
+
+ const onSubmit = (e: any) => {
+ e.preventDefault()
+ context().auth.login(username())
+ router.invalidate()
+ }
+
+ // Ah, the subtle nuances of client side auth. 🙄
+ Solid.createEffect(() => {
+ if (context().status === 'loggedIn' && search().redirect) {
+ router.history.push(search().redirect!)
+ }
+ })
+
+ return (
+
+ You must log in!
+
+
+ setUsername(e.target.value)}
+ placeholder="Username"
+ class="border p-1 px-2 rounded"
+ />
+
+ Login
+
+
+
+ }
+ >
+
+ Logged in as
{context().auth.username}
+
+
{
+ context().auth.logout()
+ router.invalidate()
+ }}
+ class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded"
+ >
+ Log out
+
+
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/styles.css b/examples/solid/kitchen-sink-file-based/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/utils/auth.tsx b/examples/solid/kitchen-sink-file-based/src/utils/auth.tsx
new file mode 100644
index 00000000000..b46954c5651
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/utils/auth.tsx
@@ -0,0 +1,25 @@
+import { createStore } from 'solid-js/store'
+
+export const [auth, setAuth] = createStore({
+ status: 'loggedOut',
+ username: undefined,
+ login: (username: string) => {
+ setAuth({
+ status: 'loggedIn',
+ username,
+ })
+ },
+ logout: () => {
+ setAuth({
+ status: 'loggedOut',
+ username: undefined,
+ })
+ },
+})
+
+export type Auth = {
+ login: (username: string) => void
+ logout: () => void
+ status: 'loggedOut' | 'loggedIn'
+ username?: string
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/utils/mockTodos.ts b/examples/solid/kitchen-sink-file-based/src/utils/mockTodos.ts
new file mode 100644
index 00000000000..475d0fa88fb
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/utils/mockTodos.ts
@@ -0,0 +1,174 @@
+import axios from 'redaxios'
+import { produce } from 'immer'
+import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
+
+type PickAsRequired = Omit &
+ Required>
+
+export type Invoice = {
+ id: number
+ title: string
+ body: string
+}
+
+export interface User {
+ id: number
+ name: string
+ username: string
+ email: string
+ address: Address
+ phone: string
+ website: string
+ company: Company
+}
+
+export interface Address {
+ street: string
+ suite: string
+ city: string
+ zipcode: string
+ geo: Geo
+}
+
+export interface Geo {
+ lat: string
+ lng: string
+}
+
+export interface Company {
+ name: string
+ catchPhrase: string
+ bs: string
+}
+
+let invoices: Array = null!
+let users: Array = null!
+
+let invoicesPromise: Promise | undefined = undefined
+let usersPromise: Promise | undefined = undefined
+
+const ensureInvoices = async () => {
+ if (!invoicesPromise) {
+ invoicesPromise = Promise.resolve().then(async () => {
+ const { data } = await axios.get(
+ 'https://jsonplaceholder.typicode.com/posts',
+ )
+ invoices = data.slice(0, 10)
+ })
+ }
+
+ await invoicesPromise
+}
+
+const ensureUsers = async () => {
+ if (!usersPromise) {
+ usersPromise = Promise.resolve().then(async () => {
+ const { data } = await axios.get(
+ 'https://jsonplaceholder.typicode.com/users',
+ )
+ users = data.slice(0, 10)
+ })
+ }
+
+ await usersPromise
+}
+
+export async function fetchInvoices() {
+ return loaderDelayFn(() => ensureInvoices().then(() => invoices))
+}
+
+export async function fetchInvoiceById(id: number) {
+ return loaderDelayFn(() =>
+ ensureInvoices().then(() => {
+ const invoice = invoices.find((d) => d.id === id)
+ if (!invoice) {
+ throw new Error('Invoice not found')
+ }
+ return invoice
+ }),
+ )
+}
+
+export async function postInvoice(partialInvoice: Partial) {
+ return actionDelayFn(() => {
+ if (partialInvoice.title?.includes('error')) {
+ console.error('error')
+ throw new Error('Ouch!')
+ }
+ const invoice = {
+ id: invoices.length + 1,
+ title:
+ partialInvoice.title ?? `New Invoice ${String(Date.now()).slice(0, 5)}`,
+ body:
+ partialInvoice.body ??
+ shuffle(
+ `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam nec ante.
+ Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. Sed lectus. Integer euismod lacus luctus magna. Integer id quam. Morbi mi. Quisque nisl felis, venenatis tristique, dignissim in, ultrices sit amet, augue. Proin sodales libero eget ante.
+ `.split(' '),
+ ).join(' '),
+ }
+ invoices = [...invoices, invoice]
+ return invoice
+ })
+}
+
+export async function patchInvoice({
+ id,
+ ...updatedInvoice
+}: PickAsRequired, 'id'>) {
+ return actionDelayFn(() => {
+ invoices = produce(invoices, (draft) => {
+ const invoice = draft.find((d) => d.id === id)
+ if (!invoice) {
+ throw new Error('Invoice not found.')
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (updatedInvoice.title?.toLocaleLowerCase()?.includes('error')) {
+ throw new Error('Ouch!')
+ }
+ Object.assign(invoice, updatedInvoice)
+ })
+
+ return invoices.find((d) => d.id === id)
+ })
+}
+
+export type UsersSortBy = 'name' | 'id' | 'email'
+
+export async function fetchUsers({
+ filterBy,
+ sortBy,
+}: { filterBy?: string; sortBy?: UsersSortBy } = {}) {
+ return loaderDelayFn(() =>
+ ensureUsers().then(() => {
+ let usersDraft = users
+
+ if (filterBy) {
+ usersDraft = usersDraft.filter((d) =>
+ d.name.toLowerCase().includes(filterBy.toLowerCase()),
+ )
+ }
+
+ if (sortBy) {
+ usersDraft = [...usersDraft].sort((a, b) => {
+ return a[sortBy] > b[sortBy] ? 1 : -1
+ })
+ }
+
+ return usersDraft
+ }),
+ )
+}
+
+export async function fetchUserById(id: number) {
+ return loaderDelayFn(() =>
+ ensureUsers().then(() => users.find((d) => d.id === id)),
+ )
+}
+
+export async function fetchRandomNumber() {
+ return loaderDelayFn(() => {
+ return Math.random()
+ })
+}
diff --git a/examples/solid/kitchen-sink-file-based/src/utils/utils.tsx b/examples/solid/kitchen-sink-file-based/src/utils/utils.tsx
new file mode 100644
index 00000000000..6435657b12a
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/src/utils/utils.tsx
@@ -0,0 +1,33 @@
+export async function loaderDelayFn(
+ fn: (...args: Array) => Promise | T,
+) {
+ const delay = Number(sessionStorage.getItem('loaderDelay') ?? 0)
+ const delayPromise = new Promise((r) => setTimeout(r, delay))
+
+ await delayPromise
+ const res = await fn()
+
+ return res
+}
+
+export async function actionDelayFn(
+ fn: (...args: Array) => Promise | T,
+) {
+ const delay = Number(sessionStorage.getItem('actionDelay') ?? 0)
+ await new Promise((r) => setTimeout(r, delay))
+ return fn()
+}
+
+export function shuffle(arr: Array): Array {
+ let i = arr.length
+ if (i == 0) return arr
+ const copy = [...arr]
+ while (--i) {
+ const j = Math.floor(Math.random() * (i + 1))
+ const a = copy[i]
+ const b = copy[j]
+ copy[i] = b!
+ copy[j] = a!
+ }
+ return copy
+}
diff --git a/examples/solid/kitchen-sink-file-based/tailwind.config.mjs b/examples/solid/kitchen-sink-file-based/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/examples/solid/kitchen-sink-file-based/tsconfig.dev.json b/examples/solid/kitchen-sink-file-based/tsconfig.dev.json
new file mode 100644
index 00000000000..285a09b0dcf
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/examples/solid/kitchen-sink-file-based/tsconfig.json b/examples/solid/kitchen-sink-file-based/tsconfig.json
new file mode 100644
index 00000000000..a972899814c
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/solid/kitchen-sink-file-based/vite.config.js b/examples/solid/kitchen-sink-file-based/vite.config.js
new file mode 100644
index 00000000000..c1946647527
--- /dev/null
+++ b/examples/solid/kitchen-sink-file-based/vite.config.js
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+import solidPlugin from 'vite-plugin-solid'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [TanStackRouterVite({ target: 'solid' }), solidPlugin()],
+ // plugins: [solidPlugin()],
+})
diff --git a/package.json b/package.json
index 8f10a9e1339..e6976896fc2 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
+ "@vitest/browser": "^3.0.6",
+ "@vitest/ui": "^3.0.6",
"eslint": "^9.20.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-unused-imports": "^4.1.4",
@@ -59,7 +61,7 @@
"typescript55": "npm:typescript@5.5",
"typescript56": "npm:typescript@5.6",
"vite": "6.1.0",
- "vitest": "^3.0.5"
+ "vitest": "^3.0.6"
},
"resolutions": {
"use-sync-external-store": "1.2.2"
diff --git a/packages/router-core/eslint.config.js b/packages/router-core/eslint.config.js
index 8ce6ad05fcd..657b7e420b7 100644
--- a/packages/router-core/eslint.config.js
+++ b/packages/router-core/eslint.config.js
@@ -2,4 +2,13 @@
import rootConfig from '../../eslint.config.js'
-export default [...rootConfig]
+export default [
+ ...rootConfig,
+ {
+ files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
+ rules: {
+ 'unused-imports/no-unused-vars': 'off',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ },
+ },
+]
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index 5866e14bd10..1173806aa60 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -35,7 +35,7 @@ export function trimPath(path: string) {
}
export function removeTrailingSlash(value: string, basepath: string): string {
- if (value.endsWith('/') && value !== '/' && value !== `${basepath}/`) {
+ if (value?.endsWith('/') && value !== '/' && value !== `${basepath}/`) {
return value.slice(0, -1)
}
return value
diff --git a/packages/router-generator/src/template.ts b/packages/router-generator/src/template.ts
index 02016a598f6..914738cda96 100644
--- a/packages/router-generator/src/template.ts
+++ b/packages/router-generator/src/template.ts
@@ -101,6 +101,59 @@ export function getTargetTemplate(target: Config['target']): TargetTemplate {
},
},
}
+ case 'solid':
+ return {
+ fullPkg: '@tanstack/solid-router',
+ subPkg: 'solid-router',
+ rootRoute: {
+ template: () =>
+ [
+ 'import * as Solid from "solid-js"\n',
+ '%%tsrImports%%',
+ '\n\n',
+ '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n',
+ 'function RootComponent() { return (<>Hello "%%tsrPath%%"!
>) };\n',
+ ].join(''),
+ imports: {
+ tsrImports: () =>
+ "import { Outlet, createRootRoute } from '@tanstack/solid-router';",
+ tsrExportStart: () => 'export const Route = createRootRoute(',
+ tsrExportEnd: () => ');',
+ },
+ },
+ route: {
+ template: () =>
+ [
+ '%%tsrImports%%',
+ '\n\n',
+ '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n',
+ 'function RouteComponent() { return Hello "%%tsrPath%%"!
};\n',
+ ].join(''),
+ imports: {
+ tsrImports: () =>
+ "import { createFileRoute } from '@tanstack/solid-router';",
+ tsrExportStart: (routePath) =>
+ `export const Route = createFileRoute('${routePath}')(`,
+ tsrExportEnd: () => ');',
+ },
+ },
+ lazyRoute: {
+ template: () =>
+ [
+ '%%tsrImports%%',
+ '\n\n',
+ '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n',
+ 'function RouteComponent() { return Hello "%%tsrPath%%"!
};\n',
+ ].join(''),
+ imports: {
+ tsrImports: () =>
+ "import { createLazyFileRoute } from '@tanstack/solid-router';",
+ tsrExportStart: (routePath) =>
+ `export const Route = createLazyFileRoute('${routePath}')(`,
+ tsrExportEnd: () => ');',
+ },
+ },
+ }
default:
throw new Error(`router-generator: Unknown target type: ${target}`)
}
diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json
index e36cd061fc8..558d217a587 100644
--- a/packages/router-plugin/package.json
+++ b/packages/router-plugin/package.json
@@ -125,6 +125,7 @@
"@rsbuild/core": ">=1.0.2",
"@tanstack/react-router": "workspace:^",
"vite": ">=5.0.0 || >=6.0.0",
+ "vite-plugin-solid": "^2.11.2",
"webpack": ">=5.92.0"
},
"peerDependenciesMeta": {
@@ -137,6 +138,9 @@
"vite": {
"optional": true
},
+ "vite-plugin-solid": {
+ "optional": true
+ },
"webpack": {
"optional": true
}
diff --git a/packages/solid-router/README.md b/packages/solid-router/README.md
new file mode 100644
index 00000000000..fab2fd013cc
--- /dev/null
+++ b/packages/solid-router/README.md
@@ -0,0 +1,29 @@
+
+
+# TanStack Solid Router
+
+
+
+🤖 Type-safe router w/ built-in caching & URL state management for Solid!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
diff --git a/packages/solid-router/eslint.config.ts b/packages/solid-router/eslint.config.ts
new file mode 100644
index 00000000000..ed917540b25
--- /dev/null
+++ b/packages/solid-router/eslint.config.ts
@@ -0,0 +1,16 @@
+import solidPlugin from 'vite-plugin-solid'
+import rootConfig from '../../eslint.config.js'
+
+export default [
+ ...rootConfig,
+ {
+ files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
+ plugins: {
+ solidPlugin: solidPlugin(),
+ },
+ rules: {
+ 'unused-imports/no-unused-vars': 'off',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ },
+ },
+]
diff --git a/packages/solid-router/package.json b/packages/solid-router/package.json
new file mode 100644
index 00000000000..96fba0c17aa
--- /dev/null
+++ b/packages/solid-router/package.json
@@ -0,0 +1,91 @@
+{
+ "name": "@tanstack/solid-router",
+ "version": "1.106.0",
+ "description": "Modern and scalable routing for Solid applications",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TanStack/router.git",
+ "directory": "packages/solid-router"
+ },
+ "homepage": "https://tanstack.com/router",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "solidjs",
+ "location",
+ "router",
+ "routing",
+ "async",
+ "async router",
+ "typescript"
+ ],
+ "scripts": {
+ "clean": "rimraf ./dist && rimraf ./coverage",
+ "test:eslint": "eslint",
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+ "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js -p tsconfig.legacy.json",
+ "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js -p tsconfig.legacy.json",
+ "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js -p tsconfig.legacy.json",
+ "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js -p tsconfig.legacy.json",
+ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js -p tsconfig.legacy.json",
+ "test:types:ts57": "tsc -p tsconfig.legacy.json",
+ "test:unit": "vitest",
+ "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests",
+ "test:perf": "vitest bench",
+ "test:perf:dev": "pnpm run test:perf --watch --hideSkippedTests",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "main": "dist/cjs/index.cjs",
+ "module": "dist/esm/index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "dependencies": {
+ "@solid-devtools/logger": "^0.9.4",
+ "@solid-primitives/refs": "^1.0.8",
+ "@tanstack/history": "workspace:*",
+ "@tanstack/router-core": "workspace:*",
+ "@tanstack/solid-store": "^0.7.0",
+ "jsesc": "^3.0.2",
+ "tiny-invariant": "^1.3.3",
+ "tiny-warning": "^1.0.3"
+ },
+ "devDependencies": {
+ "@solidjs/testing-library": "^0.8.10",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@types/jsesc": "^3.0.3",
+ "combinate": "^1.1.11",
+ "eslint-plugin-solid": "^0.14.5",
+ "solid-js": "^1",
+ "vite-plugin-solid": "^2.11.2",
+ "zod": "^3.23.8"
+ },
+ "peerDependencies": {
+ "solid-js": "^1"
+ }
+}
diff --git a/packages/solid-router/src/Asset.tsx b/packages/solid-router/src/Asset.tsx
new file mode 100644
index 00000000000..6c620ba41f1
--- /dev/null
+++ b/packages/solid-router/src/Asset.tsx
@@ -0,0 +1,23 @@
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export function Asset({ tag, attrs, children }: RouterManagedTag): any {
+ switch (tag) {
+ case 'title':
+ return {children}
+ case 'meta':
+ return
+ case 'link':
+ return
+ case 'style':
+ return
+ case 'script':
+ if ((attrs as any) && (attrs as any).src) {
+ return
+ }
+ if (typeof children === 'string')
+ return
+ return null
+ default:
+ return null
+ }
+}
diff --git a/packages/solid-router/src/CatchBoundary.tsx b/packages/solid-router/src/CatchBoundary.tsx
new file mode 100644
index 00000000000..10d5e83ca6f
--- /dev/null
+++ b/packages/solid-router/src/CatchBoundary.tsx
@@ -0,0 +1,78 @@
+import * as Solid from 'solid-js'
+import { Dynamic } from 'solid-js/web'
+import type { ErrorRouteComponent } from './route'
+
+export function CatchBoundary(
+ props: {
+ getResetKey: () => number | string
+ children: Solid.JSX.Element
+ errorComponent?: ErrorRouteComponent
+ onCatch?: (error: Error) => void
+ } & Solid.ParentProps,
+) {
+ return (
+ {
+ props.onCatch?.(error)
+
+ Solid.createEffect(
+ Solid.on([props.getResetKey], () => reset(), { defer: true }),
+ )
+
+ return (
+
+ )
+ }}
+ >
+ {props.children}
+
+ )
+}
+
+export function ErrorComponent({ error }: { error: any }) {
+ const [show, setShow] = Solid.createSignal(
+ process.env.NODE_ENV !== 'production',
+ )
+
+ return (
+
+
+ Something went wrong!
+ setShow((d) => !d)}
+ >
+ {show() ? 'Hide Error' : 'Show Error'}
+
+
+
+ {show() ? (
+
+
+ {error.message ? {error.message} : null}
+
+
+ ) : null}
+
+ )
+}
diff --git a/packages/solid-router/src/HeadContent.tsx b/packages/solid-router/src/HeadContent.tsx
new file mode 100644
index 00000000000..e43e373f017
--- /dev/null
+++ b/packages/solid-router/src/HeadContent.tsx
@@ -0,0 +1,146 @@
+import * as Solid from 'solid-js'
+import { Asset } from './Asset'
+import { useRouter } from './useRouter'
+import { useRouterState } from './useRouterState'
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export const useTags = () => {
+ const router = useRouter()
+
+ const routeMeta = useRouterState({
+ select: (state) => {
+ return state.matches.map((match) => match.meta!).filter(Boolean)
+ },
+ })
+
+ const meta: Solid.Accessor> = Solid.createMemo(() => {
+ const resultMeta: Array = []
+ const metaByAttribute: Record = {}
+ let title: RouterManagedTag | undefined
+ ;[...routeMeta()].reverse().forEach((metas) => {
+ ;[...metas].reverse().forEach((m) => {
+ if (!m) return
+
+ if (m.title) {
+ if (!title) {
+ title = {
+ tag: 'title',
+ children: m.title,
+ }
+ }
+ } else {
+ const attribute = m.name ?? m.property
+ if (attribute) {
+ if (metaByAttribute[attribute]) {
+ return
+ } else {
+ metaByAttribute[attribute] = true
+ }
+ }
+
+ resultMeta.push({
+ tag: 'meta',
+ attrs: {
+ ...m,
+ },
+ })
+ }
+ })
+ })
+
+ if (title) {
+ resultMeta.push(title)
+ }
+
+ resultMeta.reverse()
+
+ return resultMeta
+ }, [routeMeta])
+
+ const links = useRouterState({
+ select: (state) =>
+ state.matches
+ .map((match) => match.links!)
+ .filter(Boolean)
+ .flat(1)
+ .map((link) => ({
+ tag: 'link',
+ attrs: {
+ ...link,
+ },
+ })) as Array,
+ })
+
+ const preloadMeta = useRouterState({
+ select: (state) => {
+ const preloadMeta: Array = []
+
+ state.matches
+ .map((match) => router.looseRoutesById[match.routeId]!)
+ .forEach((route) =>
+ router.ssr?.manifest?.routes[route.id]?.preloads
+ ?.filter(Boolean)
+ .forEach((preload) => {
+ preloadMeta.push({
+ tag: 'link',
+ attrs: {
+ rel: 'modulepreload',
+ href: preload,
+ },
+ })
+ }),
+ )
+
+ return preloadMeta
+ },
+ })
+
+ const headScripts = useRouterState({
+ select: (state) =>
+ (
+ state.matches
+ .map((match) => match.headScripts!)
+ .flat(1)
+ .filter(Boolean) as Array
+ ).map(({ children, ...script }) => ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ },
+ children,
+ })),
+ })
+
+ return uniqBy(
+ [
+ ...meta(),
+ ...preloadMeta(),
+ ...links(),
+ ...headScripts(),
+ ] as Array,
+ (d) => {
+ return JSON.stringify(d)
+ },
+ )
+}
+
+/**
+ * @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route.
+ * It should be rendered in the `` of your document.
+ */
+export function HeadContent() {
+ const tags = useTags()
+ return tags.map((tag) => )
+}
+
+function uniqBy(arr: Array, fn: (item: T) => string) {
+ const seen = new Set()
+ return arr.filter((item) => {
+ const key = fn(item)
+ if (seen.has(key)) {
+ return false
+ }
+ seen.add(key)
+ return true
+ })
+}
diff --git a/packages/solid-router/src/Match.tsx b/packages/solid-router/src/Match.tsx
new file mode 100644
index 00000000000..b6585cb0e88
--- /dev/null
+++ b/packages/solid-router/src/Match.tsx
@@ -0,0 +1,356 @@
+import * as Solid from 'solid-js'
+import invariant from 'tiny-invariant'
+import warning from 'tiny-warning'
+import {
+ createControlledPromise,
+ getLocationChangeInfo,
+ pick,
+ rootRouteId,
+} from '@tanstack/router-core'
+import { Dynamic } from 'solid-js/web'
+import { CatchBoundary, ErrorComponent } from './CatchBoundary'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import { CatchNotFound, isNotFound } from './not-found'
+import { isRedirect } from './redirects'
+import { matchContext } from './matchContext'
+import { SafeFragment } from './SafeFragment'
+import { renderRouteNotFound } from './renderRouteNotFound'
+import { ScrollRestoration } from './scroll-restoration'
+import type { AnyRoute } from './route'
+
+export const Match = (props: { matchId: string }) => {
+ const router = useRouter()
+ const routeId = useRouterState({
+ select: (s) => {
+ return s.matches.find((d) => d.id === props.matchId)?.routeId as string
+ },
+ })
+
+ invariant(
+ routeId,
+ `Could not find routeId for matchId "${props.matchId}". Please file an issue!`,
+ )
+
+ const route: () => AnyRoute = () => router.routesById[routeId()]
+
+ const PendingComponent = () =>
+ route().options.pendingComponent ?? router.options.defaultPendingComponent
+
+ const routeErrorComponent = () =>
+ route().options.errorComponent ?? router.options.defaultErrorComponent
+
+ const routeOnCatch = () =>
+ route().options.onCatch ?? router.options.defaultOnCatch
+
+ const routeNotFoundComponent = () =>
+ route().isRoot
+ ? // If it's the root route, use the globalNotFound option, with fallback to the notFoundRoute's component
+ (route().options.notFoundComponent ??
+ router.options.notFoundRoute?.options.component)
+ : route().options.notFoundComponent
+
+ const ResolvedSuspenseBoundary = () =>
+ // If we're on the root route, allow forcefully wrapping in suspense
+ (!route().isRoot || route().options.wrapInSuspense) &&
+ (route().options.wrapInSuspense ??
+ PendingComponent() ??
+ (route().options.errorComponent as any)?.preload)
+ ? Solid.Suspense
+ : SafeFragment
+
+ const ResolvedCatchBoundary = () =>
+ routeErrorComponent() ? CatchBoundary : SafeFragment
+
+ const ResolvedNotFoundBoundary = () =>
+ routeNotFoundComponent() ? CatchNotFound : SafeFragment
+
+ const resetKey = useRouterState({
+ select: (s) => s.loadedAt,
+ })
+
+ const parentRouteId = useRouterState({
+ select: (s) => {
+ const index = s.matches.findIndex((d) => d.id === props.matchId)
+ return s.matches[index - 1]?.routeId as string
+ },
+ })
+
+ return (
+ <>
+ props.matchId}>
+ }
+ >
+ resetKey()}
+ errorComponent={routeErrorComponent() || ErrorComponent}
+ onCatch={(error: Error) => {
+ // Forward not found errors (we don't want to show the error component for these)
+ if (isNotFound(error)) throw error
+ warning(false, `Error in route match: ${props.matchId}`)
+ routeOnCatch()?.(error)
+ }}
+ >
+ {
+ // If the current not found handler doesn't exist or it has a
+ // route ID which doesn't match the current route, rethrow the error
+ if (
+ !routeNotFoundComponent() ||
+ (error.routeId && error.routeId !== routeId) ||
+ (!error.routeId && !route().isRoot)
+ )
+ throw error
+
+ return (
+
+ )
+ }}
+ >
+
+
+
+
+
+
+ {parentRouteId() === rootRouteId ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ )
+}
+
+// On Rendered can't happen above the root layout because it actually
+// renders a dummy dom element to track the rendered state of the app.
+// We render a script tag with a key that changes based on the current
+// location state.key. Also, because it's below the root layout, it
+// allows us to fire onRendered events even after a hydration mismatch
+// error that occurred above the root layout (like bad head/link tags,
+// which is common).
+function OnRendered() {
+ const router = useRouter()
+
+ const location = useRouterState({
+ select: (s) => {
+ return s.resolvedLocation?.state.key
+ },
+ })
+ Solid.createEffect(
+ Solid.on([location], () => {
+ router.emit({
+ type: 'onRendered',
+ ...getLocationChangeInfo(router.state),
+ })
+ }),
+ )
+ return null
+}
+
+export const MatchInner = (props: { matchId: string }): any => {
+ const router = useRouter()
+
+ // { match, key, routeId } =
+ const matchState: Solid.Accessor = useRouterState({
+ select: (s) => {
+ const matchIndex = s.matches.findIndex((d) => d.id === props.matchId)
+ const match = s.matches[matchIndex]!
+ const routeId = match.routeId as string
+
+ const remountFn =
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
+ router.options.defaultRemountDeps
+ const remountDeps = remountFn?.({
+ routeId,
+ loaderDeps: match.loaderDeps,
+ params: match._strictParams,
+ search: match._strictSearch,
+ })
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
+
+ return {
+ key,
+ routeId,
+ match: pick(match, ['id', 'status', 'error']),
+ }
+ },
+ })
+
+ const route = () => router.routesById[matchState().routeId]!
+
+ // function useChangedDiff(value: any) {
+ // const ref = Solid.useRef(value)
+ // const changed = ref.current !== value
+ // if (changed) {
+ // console.log(
+ // 'Changed:',
+ // value,
+ // Object.fromEntries(
+ // Object.entries(value).filter(
+ // ([key, val]) => val !== ref.current[key],
+ // ),
+ // ),
+ // )
+ // }
+ // ref.current = value
+ // }
+
+ // useChangedDiff(match)
+ const match = () => matchState().match
+
+ const out = () => {
+ const Comp = route().options.component ?? router.options.defaultComponent
+ if (Comp) {
+ return
+ }
+ return
+ }
+
+ return (
+
+
+ {(_) => {
+ invariant(isNotFound(match().error), 'Expected a notFound error')
+
+ return renderRouteNotFound(router, route(), match().error)
+ }}
+
+
+ {(_) => {
+ invariant(isRedirect(match().error), 'Expected a redirect error')
+
+ const [loaderResult] = Solid.createResource(async () => {
+ await new Promise((r) => setTimeout(r, 0))
+ return router.getMatch(match().id)?.loadPromise
+ })
+
+ return <>{loaderResult()}>
+ }}
+
+
+ {(_) => {
+ if (router.isServer) {
+ const RouteErrorComponent =
+ (route().options.errorComponent ??
+ router.options.defaultErrorComponent) ||
+ ErrorComponent
+
+ return (
+
+ )
+ }
+
+ throw match().error
+ }}
+
+
+ {(_) => {
+ const pendingMinMs =
+ route().options.pendingMinMs ?? router.options.defaultPendingMinMs
+
+ if (pendingMinMs && !router.getMatch(match().id)?.minPendingPromise) {
+ // Create a promise that will resolve after the minPendingMs
+ if (!router.isServer) {
+ const minPendingPromise = createControlledPromise()
+
+ Promise.resolve().then(() => {
+ router.updateMatch(match().id, (prev) => ({
+ ...prev,
+ minPendingPromise,
+ }))
+ })
+
+ setTimeout(() => {
+ minPendingPromise.resolve()
+
+ // We've handled the minPendingPromise, so we can delete it
+ router.updateMatch(match().id, (prev) => ({
+ ...prev,
+ minPendingPromise: undefined,
+ }))
+ }, pendingMinMs)
+ }
+ }
+
+ const [loaderResult] = Solid.createResource(async () => {
+ await new Promise((r) => setTimeout(r, 0))
+ return router.getMatch(match().id)?.loadPromise
+ })
+
+ return <>{loaderResult()}>
+ }}
+
+ {out()}
+
+ )
+}
+
+export const Outlet = () => {
+ const router = useRouter()
+ const matchId = Solid.useContext(matchContext)
+ const routeId = useRouterState({
+ select: (s) => s.matches.find((d) => d.id === matchId())?.routeId as string,
+ })
+
+ const route = () => router.routesById[routeId()]!
+
+ const parentGlobalNotFound = useRouterState({
+ select: (s) => {
+ const matches = s.matches
+ const parentMatch = matches.find((d) => d.id === matchId())
+ invariant(
+ parentMatch,
+ `Could not find parent match for matchId "${matchId()}"`,
+ )
+ return parentMatch.globalNotFound
+ },
+ })
+
+ const childMatchId = useRouterState({
+ select: (s) => {
+ const matches = s.matches
+ const index = matches.findIndex((d) => d.id === matchId())
+ const v = matches[index + 1]?.id
+ return v
+ },
+ })
+
+ return (
+
+
+ {renderRouteNotFound(router, route(), undefined)}
+
+
+ {(matchId) => {
+ // const nextMatch =
+
+ return (
+ }
+ >
+
+ }
+ >
+
+
+
+ )
+ }}
+
+
+ )
+}
diff --git a/packages/solid-router/src/Matches.tsx b/packages/solid-router/src/Matches.tsx
new file mode 100644
index 00000000000..cc68f8bc96d
--- /dev/null
+++ b/packages/solid-router/src/Matches.tsx
@@ -0,0 +1,348 @@
+import * as Solid from 'solid-js'
+import warning from 'tiny-warning'
+import { CatchBoundary, ErrorComponent } from './CatchBoundary'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import { Transitioner } from './Transitioner'
+import { matchContext } from './matchContext'
+import { Match } from './Match'
+import { SafeFragment } from './SafeFragment'
+import type { AnyRoute } from './route'
+import type {
+ ControlledPromise,
+ DeepPartial,
+ NoInfer,
+ ResolveRelativePath,
+ StaticDataRouteOption,
+} from '@tanstack/router-core'
+import type { AnyRouter, RegisteredRouter, RouterState } from './router'
+import type {
+ MakeOptionalPathParams,
+ MakeOptionalSearchParams,
+ MaskOptions,
+ ResolveRoute,
+ ToSubOptionsProps,
+} from './link'
+import type {
+ AllContext,
+ AllLoaderData,
+ AllParams,
+ FullSearchSchema,
+ ParseRoute,
+ RouteById,
+ RouteByPath,
+ RouteIds,
+} from './routeInfo'
+
+export type MakeRouteMatchFromRoute = RouteMatch<
+ TRoute['types']['id'],
+ TRoute['types']['fullPath'],
+ TRoute['types']['allParams'],
+ TRoute['types']['fullSearchSchema'],
+ TRoute['types']['loaderData'],
+ TRoute['types']['allContext'],
+ TRoute['types']['loaderDeps']
+>
+
+export interface RouteMatch<
+ out TRouteId,
+ out TFullPath,
+ out TAllParams,
+ out TFullSearchSchema,
+ out TLoaderData,
+ out TAllContext,
+ out TLoaderDeps,
+> {
+ id: string
+ routeId: TRouteId
+ fullPath: TFullPath
+ index: number
+ pathname: string
+ params: TAllParams
+ _strictParams: TAllParams
+ status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
+ isFetching: false | 'beforeLoad' | 'loader'
+ error: unknown
+ paramsError: unknown
+ searchError: unknown
+ updatedAt: number
+ loadPromise?: ControlledPromise
+ beforeLoadPromise?: ControlledPromise
+ loaderPromise?: ControlledPromise
+ loaderData?: TLoaderData
+ __routeContext: Record
+ __beforeLoadContext: Record
+ context: TAllContext
+ search: TFullSearchSchema
+ _strictSearch: TFullSearchSchema
+ fetchCount: number
+ abortController: AbortController
+ cause: 'preload' | 'enter' | 'stay'
+ loaderDeps: TLoaderDeps
+ preload: boolean
+ invalid: boolean
+ meta?: Array
+ links?: Array
+ scripts?: Array
+ headScripts?: Array
+ headers?: Record
+ globalNotFound?: boolean
+ staticData: StaticDataRouteOption
+ minPendingPromise?: ControlledPromise
+ pendingTimeout?: ReturnType
+}
+
+export type MakeRouteMatch<
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
+ TRouteId = RouteIds,
+ TStrict extends boolean = true,
+> = RouteMatch<
+ TRouteId,
+ RouteById['types']['fullPath'],
+ TStrict extends false
+ ? AllParams
+ : RouteById['types']['allParams'],
+ TStrict extends false
+ ? FullSearchSchema
+ : RouteById['types']['fullSearchSchema'],
+ TStrict extends false
+ ? AllLoaderData
+ : RouteById['types']['loaderData'],
+ TStrict extends false
+ ? AllContext
+ : RouteById['types']['allContext'],
+ RouteById['types']['loaderDeps']
+>
+
+export type AnyRouteMatch = RouteMatch
+
+export function Matches() {
+ const router = useRouter()
+
+ const pendingElement = router.options.defaultPendingComponent ? (
+
+ ) : null
+
+ // Do not render a root Suspense during SSR or hydrating from SSR
+ const ResolvedSuspense =
+ router.isServer || (typeof document !== 'undefined' && router.clientSsr)
+ ? SafeFragment
+ : Solid.Suspense
+
+ const inner = (
+
+
+
+
+ )
+
+ return router.options.InnerWrap ? (
+ {inner}
+ ) : (
+ inner
+ )
+}
+
+function MatchesInner() {
+ const matchId = useRouterState({
+ select: (s) => {
+ return s.matches[0]?.id
+ },
+ })
+
+ const resetKey = useRouterState({
+ select: (s) => s.loadedAt,
+ })
+
+ return (
+
+ resetKey()}
+ errorComponent={ErrorComponent}
+ onCatch={(error) => {
+ warning(
+ false,
+ `The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
+ )
+ warning(false, error.message || error.toString())
+ }}
+ >
+ {matchId() ? : null}
+
+
+ )
+}
+
+export interface MatchRouteOptions {
+ pending?: boolean
+ caseSensitive?: boolean
+ includeSearch?: boolean
+ fuzzy?: boolean
+}
+
+export type UseMatchRouteOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = undefined,
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '',
+> = ToSubOptionsProps &
+ DeepPartial> &
+ DeepPartial> &
+ MaskOptions &
+ MatchRouteOptions
+
+export function useMatchRoute() {
+ const router = useRouter()
+
+ const status = useRouterState({
+ select: (s) => s.status,
+ })
+
+ return <
+ const TFrom extends string = string,
+ const TTo extends string | undefined = undefined,
+ const TMaskFrom extends string = TFrom,
+ const TMaskTo extends string = '',
+ >(
+ opts: UseMatchRouteOptions,
+ ): Solid.Accessor<
+ false | ResolveRoute['types']['allParams']
+ > => {
+ const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts
+
+ const matchRoute = Solid.createMemo(() => {
+ status()
+ return router.matchRoute(rest as any, {
+ pending,
+ caseSensitive,
+ fuzzy,
+ includeSearch,
+ })
+ })
+
+ return matchRoute
+ }
+}
+
+export type MakeMatchRouteOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = undefined,
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '',
+> = UseMatchRouteOptions & {
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
+ children?:
+ | ((
+ params?: RouteByPath<
+ TRouter['routeTree'],
+ ResolveRelativePath>
+ >['types']['allParams'],
+ ) => Solid.JSX.Element)
+ | Solid.JSX.Element
+}
+
+export function MatchRoute<
+ TRouter extends AnyRouter = RegisteredRouter,
+ const TFrom extends string = string,
+ const TTo extends string | undefined = undefined,
+ const TMaskFrom extends string = TFrom,
+ const TMaskTo extends string = '',
+>(props: MakeMatchRouteOptions): any {
+ const status = useRouterState({
+ select: (s) => s.status,
+ })
+
+ return (
+
+ {(_) => {
+ const matchRoute = useMatchRoute()
+ const params = matchRoute(props as any)() as boolean
+
+ if (typeof props.children === 'function') {
+ return (props.children as any)(params)
+ }
+
+ return params ? props.children : null
+ }}
+
+ )
+}
+
+export type MakeRouteMatchUnion<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TRoute extends AnyRoute = ParseRoute,
+> = TRoute extends any
+ ? RouteMatch<
+ TRoute['id'],
+ TRoute['fullPath'],
+ TRoute['types']['allParams'],
+ TRoute['types']['fullSearchSchema'],
+ TRoute['types']['loaderData'],
+ TRoute['types']['allContext'],
+ TRoute['types']['loaderDeps']
+ >
+ : never
+
+export interface UseMatchesBaseOptions {
+ select?: (matches: Array>) => TSelected
+}
+
+export type UseMatchesResult<
+ TRouter extends AnyRouter,
+ TSelected,
+> = unknown extends TSelected ? Array> : TSelected
+
+export function useMatches<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TSelected = unknown,
+>(
+ opts?: UseMatchesBaseOptions,
+): Solid.Accessor> {
+ return useRouterState({
+ select: (state: RouterState) => {
+ const matches = state.matches
+ return opts?.select
+ ? opts.select(matches as Array>)
+ : matches
+ },
+ } as any) as Solid.Accessor>
+}
+
+export function useParentMatches<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TSelected = unknown,
+>(
+ opts?: UseMatchesBaseOptions,
+): Solid.Accessor> {
+ const contextMatchId = Solid.useContext(matchContext)
+
+ return useMatches({
+ select: (matches: Array>) => {
+ matches = matches.slice(
+ 0,
+ matches.findIndex((d) => d.id === contextMatchId()),
+ )
+ return opts?.select ? opts.select(matches) : matches
+ },
+ } as any)
+}
+
+export function useChildMatches<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TSelected = unknown,
+>(
+ opts?: UseMatchesBaseOptions,
+): Solid.Accessor> {
+ const contextMatchId = Solid.useContext(matchContext)
+
+ return useMatches({
+ select: (matches: Array>) => {
+ matches = matches.slice(
+ matches.findIndex((d) => d.id === contextMatchId()) + 1,
+ )
+ return opts?.select ? opts.select(matches) : matches
+ },
+ } as any)
+}
diff --git a/packages/solid-router/src/RouterProvider.tsx b/packages/solid-router/src/RouterProvider.tsx
new file mode 100644
index 00000000000..453df099d7f
--- /dev/null
+++ b/packages/solid-router/src/RouterProvider.tsx
@@ -0,0 +1,130 @@
+import { Matches } from './Matches'
+import { getRouterContext } from './routerContext'
+import type * as Solid from 'solid-js'
+import type { NavigateOptions, ToOptions } from './link'
+import type {
+ ParsedLocation,
+ ViewTransitionOptions,
+} from '@tanstack/router-core'
+import type { RoutePaths } from './routeInfo'
+import type {
+ AnyRouter,
+ RegisteredRouter,
+ Router,
+ RouterOptions,
+} from './router'
+
+export interface CommitLocationOptions {
+ replace?: boolean
+ resetScroll?: boolean
+ hashScrollIntoView?: boolean | ScrollIntoViewOptions
+ viewTransition?: boolean | ViewTransitionOptions
+ /**
+ * @deprecated All navigations use Solid transitions under the hood now
+ **/
+ startTransition?: boolean
+ ignoreBlocker?: boolean
+}
+
+export interface MatchLocation {
+ to?: string | number | null
+ fuzzy?: boolean
+ caseSensitive?: boolean
+ from?: string
+}
+
+export type NavigateFn = <
+ TRouter extends RegisteredRouter,
+ TTo extends string | undefined,
+ TFrom extends RoutePaths | string = string,
+ TMaskFrom extends RoutePaths | string = TFrom,
+ TMaskTo extends string = '',
+>(
+ opts: NavigateOptions,
+) => Promise | void
+
+export type BuildLocationFn = <
+ TRouter extends RegisteredRouter,
+ TTo extends string | undefined,
+ TFrom extends RoutePaths | string = string,
+ TMaskFrom extends RoutePaths | string = TFrom,
+ TMaskTo extends string = '',
+>(
+ opts: ToOptions & {
+ leaveParams?: boolean
+ _includeValidateSearch?: boolean
+ },
+) => ParsedLocation
+
+export function RouterContextProvider<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TDehydrated extends Record = Record,
+>({
+ router,
+ children,
+ ...rest
+}: RouterProps & {
+ children: () => Solid.JSX.Element
+}) {
+ // Allow the router to update options on the router instance
+ router.update({
+ ...router.options,
+ ...rest,
+ context: {
+ ...router.options.context,
+ ...rest.context,
+ },
+ } as any)
+
+ const routerContext = getRouterContext()
+
+ const provider = (
+
+ {children()}
+
+ )
+
+ if (router.options.Wrap) {
+ return {provider}
+ }
+
+ return provider
+}
+
+export function RouterProvider<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TDehydrated extends Record = Record,
+>({ router, ...rest }: RouterProps) {
+ return (
+
+ {() => }
+
+ )
+}
+
+export type RouterProps<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TDehydrated extends Record = Record,
+> = Omit<
+ RouterOptions<
+ TRouter['routeTree'],
+ NonNullable,
+ TRouter['history'],
+ TDehydrated
+ >,
+ 'context'
+> & {
+ router: Router<
+ TRouter['routeTree'],
+ NonNullable,
+ TRouter['history']
+ >
+ context?: Partial<
+ RouterOptions<
+ TRouter['routeTree'],
+ NonNullable,
+ TRouter['history'],
+ TDehydrated
+ >['context']
+ >
+}
diff --git a/packages/solid-router/src/SafeFragment.tsx b/packages/solid-router/src/SafeFragment.tsx
new file mode 100644
index 00000000000..f95bf0d2b32
--- /dev/null
+++ b/packages/solid-router/src/SafeFragment.tsx
@@ -0,0 +1,3 @@
+export function SafeFragment(props: any) {
+ return <>{props.children}>
+}
diff --git a/packages/solid-router/src/ScriptOnce.tsx b/packages/solid-router/src/ScriptOnce.tsx
new file mode 100644
index 00000000000..e85cb9c060f
--- /dev/null
+++ b/packages/solid-router/src/ScriptOnce.tsx
@@ -0,0 +1,30 @@
+import jsesc from 'jsesc'
+
+export function ScriptOnce({
+ children,
+ log,
+}: {
+ children: string
+ log?: boolean
+ sync?: boolean
+}) {
+ if (typeof document !== 'undefined') {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/solid-router/src/Scripts.tsx b/packages/solid-router/src/Scripts.tsx
new file mode 100644
index 00000000000..e3fdca99a43
--- /dev/null
+++ b/packages/solid-router/src/Scripts.tsx
@@ -0,0 +1,65 @@
+import { Asset } from './Asset'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export const Scripts = () => {
+ const router = useRouter()
+
+ const assetScripts = useRouterState({
+ select: (state) => {
+ const assetScripts: Array = []
+ const manifest = router.ssr?.manifest
+
+ if (!manifest) {
+ return []
+ }
+
+ state.matches
+ .map((match) => router.looseRoutesById[match.routeId]!)
+ .forEach((route) =>
+ manifest.routes[route.id]?.assets
+ ?.filter((d) => d.tag === 'script')
+ .forEach((asset) => {
+ assetScripts.push({
+ tag: 'script',
+ attrs: asset.attrs,
+ children: asset.children,
+ } as any)
+ }),
+ )
+
+ return assetScripts
+ },
+ })
+
+ const scripts = useRouterState({
+ select: (state) => ({
+ scripts: (
+ state.matches
+ .map((match) => match.scripts!)
+ .flat(1)
+ .filter(Boolean) as Array
+ ).map(({ children, ...script }) => ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ },
+ children,
+ })),
+ }),
+ })
+
+ const allScripts = [
+ ...scripts().scripts,
+ ...assetScripts(),
+ ] as Array
+
+ return (
+ <>
+ {allScripts.map((asset, i) => (
+
+ ))}
+ >
+ )
+}
diff --git a/packages/solid-router/src/ScrollRestoration.tsx b/packages/solid-router/src/ScrollRestoration.tsx
new file mode 100644
index 00000000000..2f4285ac95e
--- /dev/null
+++ b/packages/solid-router/src/ScrollRestoration.tsx
@@ -0,0 +1,65 @@
+import { useRouter } from './useRouter'
+import {
+ defaultGetScrollRestorationKey,
+ getCssSelector,
+ scrollRestorationCache,
+ setupScrollRestoration,
+} from './scroll-restoration'
+import type { ScrollRestorationOptions } from './scroll-restoration'
+import type { ParsedLocation } from '@tanstack/router-core'
+
+function useScrollRestoration() {
+ const router = useRouter()
+ setupScrollRestoration(router, true)
+}
+
+/**
+ * @deprecated use createRouter's `scrollRestoration` option instead
+ */
+export function ScrollRestoration(_props: ScrollRestorationOptions) {
+ useScrollRestoration()
+
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(
+ "The ScrollRestoration component is deprecated. Use createRouter's `scrollRestoration` option instead.",
+ )
+ }
+
+ return null
+}
+
+export function useElementScrollRestoration(
+ options: (
+ | {
+ id: string
+ getElement?: () => Element | undefined | null
+ }
+ | {
+ id?: string
+ getElement: () => Element | undefined | null
+ }
+ ) & {
+ getKey?: (location: ParsedLocation) => string
+ },
+) {
+ useScrollRestoration()
+
+ const router = useRouter()
+ const getKey = options.getKey || defaultGetScrollRestorationKey
+
+ let elementSelector = ''
+
+ if (options.id) {
+ elementSelector = `[data-scroll-restoration-id="${options.id}"]`
+ } else {
+ const element = options.getElement?.()
+ if (!element) {
+ return
+ }
+ elementSelector = getCssSelector(element)
+ }
+
+ const restoreKey = getKey(router.latestLocation)
+ const byKey = scrollRestorationCache.state[restoreKey]
+ return byKey?.[elementSelector]
+}
diff --git a/packages/solid-router/src/Transitioner.tsx b/packages/solid-router/src/Transitioner.tsx
new file mode 100644
index 00000000000..5700d591c11
--- /dev/null
+++ b/packages/solid-router/src/Transitioner.tsx
@@ -0,0 +1,152 @@
+import * as Solid from 'solid-js'
+import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'
+import { useRouter } from './useRouter'
+import { useRouterState } from './useRouterState'
+import { usePrevious } from './utils'
+
+export function Transitioner() {
+ const router = useRouter()
+ let mountLoadForRouter = { router, mounted: false }
+ const isLoading = useRouterState({
+ select: ({ isLoading }) => isLoading,
+ })
+
+ const [isTransitioning, setIsTransitioning] = Solid.createSignal(false)
+ // Track pending state changes
+ const hasPendingMatches = useRouterState({
+ select: (s) => s.matches.some((d) => d.status === 'pending'),
+ })
+
+ const previousIsLoading = usePrevious(isLoading)
+
+ const isAnyPending = () =>
+ isLoading() || isTransitioning() || hasPendingMatches()
+ const previousIsAnyPending = usePrevious(isAnyPending)
+
+ const isPagePending = () => isLoading() || hasPendingMatches()
+ const previousIsPagePending = usePrevious(isPagePending)
+
+ if (!router.isServer) {
+ router.startSolidTransition = (fn: () => void) => {
+ setIsTransitioning(true)
+ fn()
+ setIsTransitioning(false)
+ }
+ }
+
+ // Subscribe to location changes
+ // and try to load the new location
+ Solid.onMount(() => {
+ const unsub = router.history.subscribe(router.load)
+
+ const nextLocation = router.buildLocation({
+ to: router.latestLocation.pathname,
+ search: true,
+ params: true,
+ hash: true,
+ state: true,
+ _includeValidateSearch: true,
+ })
+
+ if (
+ trimPathRight(router.latestLocation.href) !==
+ trimPathRight(nextLocation.href)
+ ) {
+ router.commitLocation({ ...nextLocation, replace: true })
+ }
+
+ Solid.onCleanup(() => {
+ unsub()
+ })
+ })
+
+ // Try to load the initial location
+ Solid.createRenderEffect(() => {
+ Solid.untrack(() => {
+ if (
+ (typeof window !== 'undefined' && router.clientSsr) ||
+ (mountLoadForRouter.router === router && mountLoadForRouter.mounted)
+ ) {
+ return
+ }
+ mountLoadForRouter = { router, mounted: true }
+ const tryLoad = async () => {
+ try {
+ await router.load()
+ } catch (err) {
+ console.error(err)
+ }
+ }
+ tryLoad()
+ })
+ })
+
+ Solid.createRenderEffect(
+ Solid.on(
+ [previousIsLoading, isLoading],
+ ([previousIsLoading, isLoading]) => {
+ if (previousIsLoading.previous && !isLoading) {
+ router.emit({
+ type: 'onLoad',
+ ...getLocationChangeInfo(router.state),
+ })
+ }
+ },
+ ),
+ )
+ Solid.createRenderEffect(
+ Solid.on(
+ [isPagePending, previousIsPagePending],
+ ([isPagePending, previousIsPagePending]) => {
+ // emit onBeforeRouteMount
+ if (previousIsPagePending.previous && !isPagePending) {
+ router.emit({
+ type: 'onBeforeRouteMount',
+ ...getLocationChangeInfo(router.state),
+ })
+ }
+ },
+ ),
+ )
+
+ Solid.createRenderEffect(
+ Solid.on(
+ [isAnyPending, previousIsAnyPending],
+ ([isAnyPending, previousIsAnyPending]) => {
+ // The router was pending and now it's not
+ if (previousIsAnyPending.previous && !isAnyPending) {
+ router.emit({
+ type: 'onResolved',
+ ...getLocationChangeInfo(router.state),
+ })
+
+ router.__store.setState((s) => ({
+ ...s,
+ status: 'idle',
+ resolvedLocation: s.location,
+ }))
+
+ if (
+ typeof document !== 'undefined' &&
+ (document as any).querySelector
+ ) {
+ const hashScrollIntoViewOptions =
+ router.state.location.state.__hashScrollIntoViewOptions ?? true
+
+ if (
+ hashScrollIntoViewOptions &&
+ router.state.location.hash !== ''
+ ) {
+ const el = document.getElementById(router.state.location.hash)
+ if (el) {
+ el.scrollIntoView(hashScrollIntoViewOptions)
+ }
+ }
+ }
+ }
+ },
+ ),
+ )
+
+ return null
+}
diff --git a/packages/solid-router/src/awaited.tsx b/packages/solid-router/src/awaited.tsx
new file mode 100644
index 00000000000..7bb231ebb77
--- /dev/null
+++ b/packages/solid-router/src/awaited.tsx
@@ -0,0 +1,49 @@
+import * as Solid from 'solid-js'
+
+import { TSR_DEFERRED_PROMISE, defer } from '@tanstack/router-core'
+import type { DeferredPromise } from '@tanstack/router-core'
+import type { SolidNode } from './route'
+
+export type AwaitOptions = {
+ promise: Promise
+}
+
+export function useAwaited({
+ promise: _promise,
+}: AwaitOptions): [T, DeferredPromise] {
+ const promise = defer(_promise)
+
+ if (promise[TSR_DEFERRED_PROMISE].status === 'pending') {
+ throw promise
+ }
+
+ if (promise[TSR_DEFERRED_PROMISE].status === 'error') {
+ throw promise[TSR_DEFERRED_PROMISE].error
+ }
+
+ return [promise[TSR_DEFERRED_PROMISE].data, promise]
+}
+
+export function Await(
+ props: AwaitOptions & {
+ fallback?: SolidNode
+ children: (result: T) => SolidNode
+ },
+) {
+ const inner =
+ if (props.fallback) {
+ return {inner}
+ }
+ return inner
+}
+
+function AwaitInner(
+ props: AwaitOptions & {
+ fallback?: SolidNode
+ children: (result: T) => SolidNode
+ },
+): Solid.JSX.Element {
+ const [data] = useAwaited(props)
+
+ return props.children(data)
+}
diff --git a/packages/solid-router/src/fileRoute.ts b/packages/solid-router/src/fileRoute.ts
new file mode 100644
index 00000000000..08a173b6dd5
--- /dev/null
+++ b/packages/solid-router/src/fileRoute.ts
@@ -0,0 +1,274 @@
+import warning from 'tiny-warning'
+import { createRoute } from './route'
+
+import { useMatch } from './useMatch'
+import { useLoaderDeps } from './useLoaderDeps'
+import { useLoaderData } from './useLoaderData'
+import { useSearch } from './useSearch'
+import { useParams } from './useParams'
+import { useNavigate } from './useNavigate'
+import type { UseParamsRoute } from './useParams'
+import type { UseMatchRoute } from './useMatch'
+import type { UseSearchRoute } from './useSearch'
+import type {
+ AnyContext,
+ AnyPathParams,
+ AnyValidator,
+ Constrain,
+ ResolveParams,
+} from '@tanstack/router-core'
+
+import type {
+ AnyRoute,
+ FileBaseRouteOptions,
+ RootRoute,
+ Route,
+ RouteConstraints,
+ RouteLoaderFn,
+ UpdatableRouteOptions,
+} from './route'
+import type { RegisteredRouter } from './router'
+import type { RouteById, RouteIds } from './routeInfo'
+import type { UseLoaderDepsRoute } from './useLoaderDeps'
+import type { UseLoaderDataRoute } from './useLoaderData'
+import type { UseRouteContextRoute } from './useRouteContext'
+
+export interface FileRoutesByPath {
+ // '/': {
+ // parentRoute: typeof rootRoute
+ // }
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: any
+ fullPaths: any
+ to: any
+ fileRoutesByTo: any
+ id: any
+ fileRoutesById: any
+}
+
+export type InferFileRouteTypes =
+ TRouteTree extends RootRoute<
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ infer TFileRouteTypes extends FileRouteTypes
+ >
+ ? TFileRouteTypes
+ : never
+
+export function createFileRoute<
+ TFilePath extends keyof FileRoutesByPath,
+ TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
+ TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
+ TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
+ TFullPath extends
+ RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
+>(
+ path: TFilePath,
+): FileRoute['createRoute'] {
+ return new FileRoute(path, {
+ silent: true,
+ }).createRoute
+}
+
+/**
+ @deprecated It's no longer recommended to use the `FileRoute` class directly.
+ Instead, use `createFileRoute('/path/to/file')(options)` to create a file route.
+*/
+export class FileRoute<
+ TFilePath extends keyof FileRoutesByPath,
+ TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
+ TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
+ TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
+ TFullPath extends
+ RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
+> {
+ silent?: boolean
+
+ constructor(
+ public path: TFilePath,
+ _opts?: { silent: boolean },
+ ) {
+ this.silent = _opts?.silent
+ }
+
+ createRoute = <
+ TSearchValidator = undefined,
+ TParams = ResolveParams,
+ TRouteContextFn = AnyContext,
+ TBeforeLoadFn = AnyContext,
+ TLoaderDeps extends Record = {},
+ TLoaderFn = undefined,
+ TChildren = unknown,
+ >(
+ options?: FileBaseRouteOptions<
+ TParentRoute,
+ TId,
+ TPath,
+ TSearchValidator,
+ TParams,
+ TLoaderDeps,
+ TLoaderFn,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ > &
+ UpdatableRouteOptions<
+ TParentRoute,
+ TId,
+ TFullPath,
+ TParams,
+ TSearchValidator,
+ TLoaderFn,
+ TLoaderDeps,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ >,
+ ): Route<
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TFilePath,
+ TId,
+ TSearchValidator,
+ TParams,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren
+ > => {
+ warning(
+ this.silent,
+ 'FileRoute is deprecated and will be removed in the next major version. Use the createFileRoute(path)(options) function instead.',
+ )
+ const route = createRoute(options as any)
+ ;(route as any).isRoot = false
+ return route as any
+ }
+}
+
+/**
+ @deprecated It's recommended not to split loaders into separate files.
+ Instead, place the loader function in the the main route file, inside the
+ `createFileRoute('/path/to/file)(options)` options.
+*/
+export function FileRouteLoader<
+ TFilePath extends keyof FileRoutesByPath,
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
+>(
+ _path: TFilePath,
+): (
+ loaderFn: Constrain<
+ TLoaderFn,
+ RouteLoaderFn<
+ TRoute['parentRoute'],
+ TRoute['types']['id'],
+ TRoute['types']['params'],
+ TRoute['types']['loaderDeps'],
+ TRoute['types']['routerContext'],
+ TRoute['types']['routeContextFn'],
+ TRoute['types']['beforeLoadFn']
+ >
+ >,
+) => TLoaderFn {
+ warning(
+ false,
+ `FileRouteLoader is deprecated and will be removed in the next major version. Please place the loader function in the the main route file, inside the \`createFileRoute('/path/to/file')(options)\` options`,
+ )
+ return (loaderFn) => loaderFn as any
+}
+
+export type LazyRouteOptions = Pick<
+ UpdatableRouteOptions<
+ AnyRoute,
+ string,
+ string,
+ AnyPathParams,
+ AnyValidator,
+ {},
+ AnyContext,
+ AnyContext,
+ AnyContext,
+ AnyContext
+ >,
+ 'component' | 'errorComponent' | 'pendingComponent' | 'notFoundComponent'
+>
+
+export class LazyRoute {
+ options: {
+ id: string
+ } & LazyRouteOptions
+
+ constructor(
+ opts: {
+ id: string
+ } & LazyRouteOptions,
+ ) {
+ this.options = opts
+ }
+
+ useMatch: UseMatchRoute = (opts) => {
+ return useMatch({
+ select: opts?.select,
+ from: this.options.id,
+ } as any) as any
+ }
+
+ useRouteContext: UseRouteContextRoute = (opts) => {
+ return useMatch({
+ from: this.options.id,
+ select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
+ }) as any
+ }
+
+ useSearch: UseSearchRoute = (opts) => {
+ return useSearch({
+ select: opts?.select,
+ from: this.options.id,
+ } as any) as any
+ }
+
+ useParams: UseParamsRoute = (opts) => {
+ return useParams({
+ select: opts?.select,
+ from: this.options.id,
+ } as any) as any
+ }
+
+ useLoaderDeps: UseLoaderDepsRoute = (opts) => {
+ return useLoaderDeps({ ...opts, from: this.options.id } as any)
+ }
+
+ useLoaderData: UseLoaderDataRoute = (opts) => {
+ return useLoaderData({ ...opts, from: this.options.id } as any)
+ }
+
+ useNavigate = () => {
+ return useNavigate({ from: this.options.id })
+ }
+}
+
+export function createLazyRoute<
+ TId extends RouteIds,
+ TRoute extends AnyRoute = RouteById,
+>(id: TId) {
+ return (opts: LazyRouteOptions) => {
+ return new LazyRoute({ id: id as any, ...opts })
+ }
+}
+
+export function createLazyFileRoute<
+ TFilePath extends keyof FileRoutesByPath,
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
+>(id: TFilePath) {
+ return (opts: LazyRouteOptions) => new LazyRoute({ id, ...opts })
+}
diff --git a/packages/solid-router/src/history.ts b/packages/solid-router/src/history.ts
new file mode 100644
index 00000000000..2271af692cd
--- /dev/null
+++ b/packages/solid-router/src/history.ts
@@ -0,0 +1,9 @@
+import type { HistoryLocation } from '@tanstack/history'
+
+declare module '@tanstack/history' {
+ interface HistoryState {
+ __tempLocation?: HistoryLocation
+ __tempKey?: string
+ __hashScrollIntoViewOptions?: boolean | ScrollIntoViewOptions
+ }
+}
diff --git a/packages/solid-router/src/index.tsx b/packages/solid-router/src/index.tsx
new file mode 100644
index 00000000000..1c0a4e6d01b
--- /dev/null
+++ b/packages/solid-router/src/index.tsx
@@ -0,0 +1,359 @@
+export { default as invariant } from 'tiny-invariant'
+export { default as warning } from 'tiny-warning'
+
+export {
+ defer,
+ TSR_DEFERRED_PROMISE,
+ isMatch,
+ joinPaths,
+ cleanPath,
+ trimPathLeft,
+ trimPathRight,
+ trimPath,
+ resolvePath,
+ parsePathname,
+ interpolatePath,
+ matchPathname,
+ removeBasepath,
+ matchByPath,
+ encode,
+ decode,
+ rootRouteId,
+ defaultSerializeError,
+ defaultParseSearch,
+ defaultStringifySearch,
+ parseSearchWith,
+ stringifySearchWith,
+ escapeJSON, // SSR
+ pick,
+ functionalUpdate,
+ replaceEqualDeep,
+ isPlainObject,
+ isPlainArray,
+ deepEqual,
+ shallow,
+ createControlledPromise,
+ retainSearchParams,
+ stripSearchParams,
+} from '@tanstack/router-core'
+
+export type {
+ StartSerializer,
+ Serializable,
+ SerializerParse,
+ SerializerParseBy,
+ SerializerStringify,
+ SerializerStringifyBy,
+ DeferredPromiseState,
+ DeferredPromise,
+ ParsedLocation,
+ ParsePathParams,
+ RemoveTrailingSlashes,
+ RemoveLeadingSlashes,
+ ActiveOptions,
+ Segment,
+ ResolveRelativePath,
+ RootRouteId,
+ AnyPathParams,
+ ResolveParams,
+ SearchSchemaInput,
+ AnyContext,
+ RouteContext,
+ PreloadableObj,
+ RoutePathOptions,
+ StaticDataRouteOption,
+ RoutePathOptionsIntersection,
+ UpdatableStaticRouteOption,
+ MetaDescriptor,
+ RouteLinkEntry,
+ ParseParamsFn,
+ SearchFilter,
+ ResolveId,
+ InferFullSearchSchema,
+ InferFullSearchSchemaInput,
+ ErrorRouteProps,
+ ErrorComponentProps,
+ NotFoundRouteProps,
+ TrimPath,
+ TrimPathLeft,
+ TrimPathRight,
+ ParseSplatParams,
+ SplatParams,
+ StringifyParamsFn,
+ ParamsOptions,
+ InferAllParams,
+ InferAllContext,
+ LooseReturnType,
+ LooseAsyncReturnType,
+ ContextReturnType,
+ ContextAsyncReturnType,
+ ResolveLoaderData,
+ ResolveRouteContext,
+ SearchSerializer,
+ SearchParser,
+ TrailingSlashOption,
+ ExtractedEntry,
+ ExtractedStream,
+ ExtractedPromise,
+ StreamState,
+ Manifest,
+ RouterManagedTag,
+ ControlledPromise,
+ Constrain,
+ Expand,
+ MergeAll,
+ Assign,
+ IntersectAssign,
+ ResolveValidatorInput,
+ ResolveValidatorOutput,
+ AnyValidator,
+ DefaultValidator,
+ ValidatorFn,
+ AnySchema,
+ AnyValidatorAdapter,
+ AnyValidatorFn,
+ AnyValidatorObj,
+ ResolveValidatorInputFn,
+ ResolveValidatorOutputFn,
+ ResolveSearchValidatorInput,
+ ResolveSearchValidatorInputFn,
+ Validator,
+ ValidatorAdapter,
+ ValidatorObj,
+} from '@tanstack/router-core'
+
+export {
+ createHistory,
+ createBrowserHistory,
+ createHashHistory,
+ createMemoryHistory,
+} from '@tanstack/history'
+
+export type {
+ BlockerFn,
+ HistoryLocation,
+ RouterHistory,
+ ParsedPath,
+ HistoryState,
+} from '@tanstack/history'
+
+export { useAwaited, Await } from './awaited'
+export type { AwaitOptions } from './awaited'
+
+export { CatchBoundary, ErrorComponent } from './CatchBoundary'
+
+export {
+ FileRoute,
+ createFileRoute,
+ FileRouteLoader,
+ LazyRoute,
+ createLazyRoute,
+ createLazyFileRoute,
+} from './fileRoute'
+export type {
+ FileRoutesByPath,
+ FileRouteTypes,
+ LazyRouteOptions,
+} from './fileRoute'
+
+export * from './history'
+
+export { lazyRouteComponent } from './lazyRouteComponent'
+
+export { useLinkProps, createLink, Link, linkOptions } from './link'
+export type {
+ InferDescendantToPaths,
+ RelativeToPath,
+ RelativeToParentPath,
+ RelativeToCurrentPath,
+ AbsoluteToPath,
+ RelativeToPathAutoComplete,
+ NavigateOptions,
+ ToOptions,
+ ToMaskOptions,
+ ToSubOptions,
+ ResolveRoute,
+ SearchParamOptions,
+ PathParamOptions,
+ ToPathOption,
+ LinkOptions,
+ UseLinkPropsOptions,
+ ActiveLinkOptions,
+ LinkProps,
+ LinkComponent,
+ LinkComponentProps,
+ CreateLinkProps,
+ MakeOptionalPathParams,
+} from './link'
+
+export {
+ Matches,
+ useMatchRoute,
+ MatchRoute,
+ useMatches,
+ useParentMatches,
+ useChildMatches,
+} from './Matches'
+
+export type {
+ RouteMatch,
+ AnyRouteMatch,
+ MatchRouteOptions,
+ UseMatchRouteOptions,
+ MakeMatchRouteOptions,
+ MakeRouteMatch,
+ MakeRouteMatchUnion,
+} from './Matches'
+
+export { matchContext } from './matchContext'
+export { Match, Outlet } from './Match'
+
+export { useMatch } from './useMatch'
+export { useLoaderDeps } from './useLoaderDeps'
+export { useLoaderData } from './useLoaderData'
+
+export { redirect, isRedirect } from './redirects'
+export type { AnyRedirect, Redirect, ResolvedRedirect } from './redirects'
+
+export {
+ RouteApi,
+ getRouteApi,
+ Route,
+ createRoute,
+ RootRoute,
+ rootRouteWithContext,
+ createRootRoute,
+ createRootRouteWithContext,
+ createRouteMask,
+ NotFoundRoute,
+} from './route'
+export type {
+ RouteOptions,
+ FileBaseRouteOptions,
+ BaseRouteOptions,
+ UpdatableRouteOptions,
+ RouteLoaderFn,
+ LoaderFnContext,
+ ResolveFullSearchSchema,
+ ResolveFullSearchSchemaInput,
+ AnyRoute,
+ RouteConstraints,
+ AnyRootRoute,
+ ResolveFullPath,
+ RouteMask,
+ SolidNode,
+ SyncRouteComponent,
+ AsyncRouteComponent,
+ RouteComponent,
+ ErrorRouteComponent,
+ NotFoundRouteComponent,
+ RootRouteOptions,
+ AnyRouteWithContext,
+ FullSearchSchemaOption,
+ RouteContextFn,
+ RouteContextOptions,
+ BeforeLoadFn,
+ BeforeLoadContextOptions,
+ ContextOptions,
+ RouteContextParameter,
+ BeforeLoadContextParameter,
+ ResolveAllContext,
+ ResolveAllParamsFromParent,
+ MakeRemountDepsOptionsUnion,
+ RemountDepsOptions,
+} from './route'
+
+export type {
+ ParseRoute,
+ RoutesById,
+ RouteById,
+ RouteIds,
+ RoutesByPath,
+ RouteByPath,
+ RoutePaths,
+ FullSearchSchema,
+ AllParams,
+ AllLoaderData,
+ FullSearchSchemaInput,
+ AllContext,
+} from './routeInfo'
+
+export {
+ componentTypes,
+ createRouter,
+ Router,
+ lazyFn,
+ SearchParamError,
+ PathParamError,
+ getInitialRouterState,
+} from './router'
+
+export type {
+ Register,
+ AnyRouter,
+ RegisteredRouter,
+ RouterContextOptions,
+ RouterOptions,
+ RouterErrorSerializer,
+ RouterState,
+ ListenerFn,
+ BuildNextOptions,
+ RouterConstructorOptions,
+ RouterEvents,
+ RouterEvent,
+ RouterListener,
+ AnyRouterWithContext,
+ ControllablePromise,
+ InjectedHtmlEntry,
+} from './router'
+
+export { RouterProvider, RouterContextProvider } from './RouterProvider'
+export type {
+ RouterProps,
+ CommitLocationOptions,
+ MatchLocation,
+ NavigateFn,
+ BuildLocationFn,
+} from './RouterProvider'
+
+export {
+ useElementScrollRestoration,
+ ScrollRestoration,
+} from './ScrollRestoration'
+
+export type { UseBlockerOpts, ShouldBlockFn } from './useBlocker'
+export { useBlocker, Block } from './useBlocker'
+
+export { useNavigate, Navigate } from './useNavigate'
+export type { UseNavigateResult } from './useNavigate'
+
+export { useParams } from './useParams'
+export { useSearch } from './useSearch'
+
+export {
+ getRouterContext, // SSR
+} from './routerContext'
+
+export { useRouteContext } from './useRouteContext'
+export { useRouter } from './useRouter'
+export { useRouterState } from './useRouterState'
+export { useLocation } from './useLocation'
+export { useCanGoBack } from './useCanGoBack'
+
+export { useLayoutEffect } from './utils'
+
+export {
+ notFound,
+ isNotFound,
+ CatchNotFound,
+ DefaultGlobalNotFound,
+} from './not-found'
+export type { NotFoundError } from './not-found'
+
+export * from './typePrimitives'
+
+export { ScriptOnce } from './ScriptOnce'
+
+export { Asset } from './Asset'
+export { HeadContent } from './HeadContent'
+export { Scripts } from './Scripts'
diff --git a/packages/solid-router/src/lazyRouteComponent.tsx b/packages/solid-router/src/lazyRouteComponent.tsx
new file mode 100644
index 00000000000..0e4da39ee81
--- /dev/null
+++ b/packages/solid-router/src/lazyRouteComponent.tsx
@@ -0,0 +1,114 @@
+import { Dynamic, isServer } from 'solid-js/web'
+import { Outlet } from './Match'
+import type * as Solid from 'solid-js'
+import type { AsyncRouteComponent } from './route'
+
+// If the load fails due to module not found, it may mean a new version of
+// the build was deployed and the user's browser is still using an old version.
+// If this happens, the old version in the user's browser would have an outdated
+// URL to the lazy module.
+// In that case, we want to attempt one window refresh to get the latest.
+function isModuleNotFoundError(error: any): boolean {
+ return (
+ typeof error?.message === 'string' &&
+ /Failed to fetch dynamically imported module/.test(error.message)
+ )
+}
+
+export function ClientOnly(
+ props: Solid.ParentProps<{ fallback?: Solid.JSX.Element }>,
+) {
+ return useHydrated() ? <>{props.children}> : <>{props.fallback}>
+}
+
+export function useHydrated() {
+ return isServer
+}
+
+export function lazyRouteComponent<
+ T extends Record,
+ TKey extends keyof T = 'default',
+>(
+ importer: () => Promise,
+ exportName?: TKey,
+ ssr?: () => boolean,
+): T[TKey] extends (props: infer TProps) => any
+ ? AsyncRouteComponent
+ : never {
+ let loadPromise: Promise | undefined
+ let comp: T[TKey] | T['default']
+ let error: any
+
+ const load = () => {
+ if (typeof document === 'undefined' && ssr?.() === false) {
+ comp = (() => null) as any
+ return Promise.resolve()
+ }
+ if (!loadPromise) {
+ loadPromise = importer()
+ .then((res) => {
+ loadPromise = undefined
+ comp = res[exportName ?? 'default']
+ })
+ .catch((err) => {
+ error = err
+ })
+ }
+
+ return loadPromise
+ }
+
+ const lazyComp = function Lazy(props: any) {
+ // Now that we're out of preload and into actual render path,
+ // throw the error if it was a module not found error during preload
+ if (error) {
+ if (isModuleNotFoundError(error)) {
+ // We don't want an error thrown from preload in this case, because
+ // there's nothing we want to do about module not found during preload.
+ // Record the error, recover the promise with a null return,
+ // and we will attempt module not found resolution during the render path.
+
+ if (
+ error instanceof Error &&
+ typeof window !== 'undefined' &&
+ typeof sessionStorage !== 'undefined'
+ ) {
+ // Again, we want to reload one time on module not found error and not enter
+ // a reload loop if there is some other issue besides an old deploy.
+ // That's why we store our reload attempt in sessionStorage.
+ // Use error.message as key because it contains the module path that failed.
+ const storageKey = `tanstack_router_reload:${error.message}`
+ if (!sessionStorage.getItem(storageKey)) {
+ sessionStorage.setItem(storageKey, '1')
+ window.location.reload()
+
+ // Return empty component while we wait for window to reload
+ return {
+ default: () => null,
+ }
+ }
+ }
+ }
+
+ // Otherwise, just throw the error
+ throw error
+ }
+
+ if (!comp) {
+ throw load()
+ }
+
+ if (ssr?.() === false) {
+ return (
+ }>
+
+
+ )
+ }
+ return
+ }
+
+ ;(lazyComp as any).preload = load
+
+ return lazyComp as any
+}
diff --git a/packages/solid-router/src/link.tsx b/packages/solid-router/src/link.tsx
new file mode 100644
index 00000000000..041d0e68310
--- /dev/null
+++ b/packages/solid-router/src/link.tsx
@@ -0,0 +1,1002 @@
+import * as Solid from 'solid-js'
+
+import { mergeRefs } from '@solid-primitives/refs'
+
+import {
+ deepEqual,
+ exactPathTest,
+ functionalUpdate,
+ preloadWarning,
+ removeTrailingSlash,
+} from '@tanstack/router-core'
+import { Dynamic } from 'solid-js/web'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+
+import { useIntersectionObserver } from './utils'
+
+import { useMatch } from './useMatch'
+import type {
+ Constrain,
+ ConstrainLiteral,
+ Expand,
+ IsRequiredParams,
+ LinkOptionsProps,
+ MakeDifferenceOptional,
+ NoInfer,
+ NonNullableUpdater,
+ ParsedLocation,
+ PickRequired,
+ RemoveTrailingSlashes,
+ ResolveRelativePath,
+ Updater,
+ ViewTransitionOptions,
+ WithoutEmpty,
+} from '@tanstack/router-core'
+import type { HistoryState, ParsedHistoryState } from '@tanstack/history'
+import type {
+ AllParams,
+ CatchAllPaths,
+ CurrentPath,
+ FullSearchSchema,
+ FullSearchSchemaInput,
+ ParentPath,
+ RouteByPath,
+ RouteByToPath,
+ RoutePaths,
+ RouteToPath,
+ ToPath,
+} from './routeInfo'
+import type { AnyRouter, RegisteredRouter } from './router'
+import type {
+ ValidateLinkOptions,
+ ValidateLinkOptionsArray,
+} from './typePrimitives'
+
+export type FindDescendantToPaths<
+ TRouter extends AnyRouter,
+ TPrefix extends string,
+> = `${TPrefix}/${string}` & RouteToPath
+
+export type InferDescendantToPaths<
+ TRouter extends AnyRouter,
+ TPrefix extends string,
+ TPaths = FindDescendantToPaths,
+> = TPaths extends `${TPrefix}/`
+ ? never
+ : TPaths extends `${TPrefix}/${infer TRest}`
+ ? TRest
+ : never
+
+export type RelativeToPath<
+ TRouter extends AnyRouter,
+ TTo extends string,
+ TResolvedPath extends string,
+> =
+ | (TResolvedPath & RouteToPath extends never
+ ? never
+ : ToPath)
+ | `${RemoveTrailingSlashes}/${InferDescendantToPaths>}`
+
+export type RelativeToParentPath<
+ TRouter extends AnyRouter,
+ TFrom extends string,
+ TTo extends string,
+ TResolvedPath extends string = ResolveRelativePath,
+> =
+ | RelativeToPath
+ | (TTo extends `${string}..` | `${string}../`
+ ? TResolvedPath extends '/' | ''
+ ? never
+ : FindDescendantToPaths<
+ TRouter,
+ RemoveTrailingSlashes
+ > extends never
+ ? never
+ : `${RemoveTrailingSlashes}/${ParentPath}`
+ : never)
+
+export type RelativeToCurrentPath<
+ TRouter extends AnyRouter,
+ TFrom extends string,
+ TTo extends string,
+ TResolvedPath extends string = ResolveRelativePath,
+> = RelativeToPath | CurrentPath
+
+export type AbsoluteToPath =
+ | (string extends TFrom
+ ? CurrentPath
+ : TFrom extends `/`
+ ? never
+ : CurrentPath)
+ | (string extends TFrom
+ ? ParentPath
+ : TFrom extends `/`
+ ? never
+ : ParentPath)
+ | RouteToPath
+ | (TFrom extends '/'
+ ? never
+ : string extends TFrom
+ ? never
+ : InferDescendantToPaths>)
+
+export type RelativeToPathAutoComplete<
+ TRouter extends AnyRouter,
+ TFrom extends string,
+ TTo extends string,
+> = string extends TTo
+ ? string
+ : string extends TFrom
+ ? AbsoluteToPath
+ : TTo & `..${string}` extends never
+ ? TTo & `.${string}` extends never
+ ? AbsoluteToPath
+ : RelativeToCurrentPath
+ : RelativeToParentPath
+
+export type NavigateOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '.',
+> = ToOptions & NavigateOptionProps
+
+export interface NavigateOptionProps {
+ // if set to `true`, the router will scroll the element with an id matching the hash into view with default ScrollIntoViewOptions.
+ // if set to `false`, the router will not scroll the element with an id matching the hash into view.
+ // if set to `ScrollIntoViewOptions`, the router will scroll the element with an id matching the hash into view with the provided options.
+ hashScrollIntoView?: boolean | ScrollIntoViewOptions
+ // `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one.
+ replace?: boolean
+ resetScroll?: boolean
+ /** @deprecated All navigations now use startTransition under the hood */
+ startTransition?: boolean
+ // if set to `true`, the router will wrap the resulting navigation in a document.startViewTransition() call.
+ // if set to `ViewTransitionOptions`, the router will pass the `types` field to document.startViewTransition({update: fn, types: viewTransition.types}) call
+ viewTransition?: boolean | ViewTransitionOptions
+ ignoreBlocker?: boolean
+ reloadDocument?: boolean
+ href?: string
+}
+
+export type ToOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '.',
+> = ToSubOptions & MaskOptions
+
+export interface MaskOptions<
+ in out TRouter extends AnyRouter,
+ in out TMaskFrom extends string,
+ in out TMaskTo extends string,
+> {
+ _fromLocation?: ParsedLocation
+ mask?: ToMaskOptions
+}
+
+export type ToMaskOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TMaskFrom extends string = string,
+ TMaskTo extends string = '.',
+> = ToSubOptions & {
+ unmaskOnReload?: boolean
+}
+
+export type ToSubOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = '.',
+> = ToSubOptionsProps &
+ SearchParamOptions &
+ PathParamOptions
+
+export interface RequiredToOptions<
+ in out TRouter extends AnyRouter,
+ in out TFrom extends string,
+ in out TTo extends string | undefined,
+> {
+ to: ToPathOption & {}
+}
+
+export interface OptionalToOptions<
+ in out TRouter extends AnyRouter,
+ in out TFrom extends string,
+ in out TTo extends string | undefined,
+> {
+ to?: ToPathOption & {}
+}
+
+export type MakeToRequired<
+ TRouter extends AnyRouter,
+ TFrom extends string,
+ TTo extends string | undefined,
+> = string extends TFrom
+ ? string extends TTo
+ ? OptionalToOptions
+ : TTo & CatchAllPaths extends never
+ ? RequiredToOptions
+ : OptionalToOptions
+ : OptionalToOptions
+
+export type ToSubOptionsProps<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends RoutePaths | string = string,
+ TTo extends string | undefined = '.',
+> = MakeToRequired & {
+ hash?: true | Updater
+ state?: true | NonNullableUpdater
+ from?: FromPathOption & {}
+}
+
+export type ParamsReducerFn<
+ in out TRouter extends AnyRouter,
+ in out TParamVariant extends ParamVariant,
+ in out TFrom,
+ in out TTo,
+> = (
+ current: Expand>,
+) => Expand>
+
+type ParamsReducer<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+ TFrom,
+ TTo,
+> =
+ | Expand>
+ | (ParamsReducerFn & {})
+
+type ParamVariant = 'PATH' | 'SEARCH'
+
+export type ResolveRoute<
+ TRouter extends AnyRouter,
+ TFrom,
+ TTo,
+ TPath = ResolveRelativePath,
+> = TPath extends string
+ ? TFrom extends TPath
+ ? RouteByPath
+ : RouteByToPath
+ : never
+
+type ResolveFromParamType =
+ TParamVariant extends 'PATH' ? 'allParams' : 'fullSearchSchema'
+
+type ResolveFromAllParams<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+> = TParamVariant extends 'PATH'
+ ? AllParams
+ : FullSearchSchema
+
+type ResolveFromParams<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+ TFrom,
+> = string extends TFrom
+ ? ResolveFromAllParams
+ : RouteByPath<
+ TRouter['routeTree'],
+ TFrom
+ >['types'][ResolveFromParamType]
+
+type ResolveToParamType =
+ TParamVariant extends 'PATH' ? 'allParams' : 'fullSearchSchemaInput'
+
+type ResolveAllToParams<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+> = TParamVariant extends 'PATH'
+ ? AllParams
+ : FullSearchSchemaInput
+
+export type ResolveToParams<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+ TFrom,
+ TTo,
+> =
+ ResolveRelativePath extends infer TPath
+ ? undefined extends TPath
+ ? never
+ : string extends TPath
+ ? ResolveAllToParams
+ : TPath extends CatchAllPaths
+ ? ResolveAllToParams
+ : ResolveRoute<
+ TRouter,
+ TFrom,
+ TTo
+ >['types'][ResolveToParamType]
+ : never
+
+type ResolveRelativeToParams<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+ TFrom,
+ TTo,
+ TToParams = ResolveToParams,
+> = TParamVariant extends 'SEARCH'
+ ? TToParams
+ : string extends TFrom
+ ? TToParams
+ : MakeDifferenceOptional<
+ ResolveFromParams,
+ TToParams
+ >
+
+export interface MakeOptionalSearchParams<
+ in out TRouter extends AnyRouter,
+ in out TFrom,
+ in out TTo,
+> {
+ search?: true | (ParamsReducer & {})
+}
+
+export interface MakeOptionalPathParams<
+ in out TRouter extends AnyRouter,
+ in out TFrom,
+ in out TTo,
+> {
+ params?: true | (ParamsReducer & {})
+}
+
+type MakeRequiredParamsReducer<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+ TFrom,
+ TTo,
+> =
+ | (string extends TFrom
+ ? never
+ : ResolveFromParams extends WithoutEmpty<
+ PickRequired<
+ ResolveRelativeToParams
+ >
+ >
+ ? true
+ : never)
+ | (ParamsReducer & {})
+
+export interface MakeRequiredPathParams<
+ in out TRouter extends AnyRouter,
+ in out TFrom,
+ in out TTo,
+> {
+ params: MakeRequiredParamsReducer & {}
+}
+
+export interface MakeRequiredSearchParams<
+ in out TRouter extends AnyRouter,
+ in out TFrom,
+ in out TTo,
+> {
+ search: MakeRequiredParamsReducer & {}
+}
+
+export type IsRequired<
+ TRouter extends AnyRouter,
+ TParamVariant extends ParamVariant,
+ TFrom,
+ TTo,
+> =
+ ResolveRelativePath extends infer TPath
+ ? undefined extends TPath
+ ? never
+ : TPath extends CatchAllPaths