Hello!
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
index 2a6bf16c37..20b07c41cc 100644
--- 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
@@ -1,5 +1,3 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/classic/hello/universe')({
+export const Route = createFileRoute({
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
index 03edc7f484..4af11357a2 100644
--- 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
@@ -1,5 +1,3 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/classic/hello/world')({
+export const Route = createFileRoute({
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
index bdfb4c7676..510db79b73 100644
--- a/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx
+++ b/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/')({
+export const Route = createFileRoute({
component: 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
index 5c77421bb2..c549175638 100644
--- 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
@@ -1,6 +1,6 @@
-import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_first')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
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
index 9ab147de5e..efeca5ce86 100644
--- 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
@@ -1,6 +1,6 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_first/_second')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
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
index 990f473ae8..ef413c1be0 100644
--- 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
@@ -1,4 +1,4 @@
-import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { ErrorComponent } from '@tanstack/solid-router'
import { fetchPost } from '../../posts'
import type { ErrorComponentProps } from '@tanstack/solid-router'
@@ -6,7 +6,7 @@ export function PostErrorComponent({ error }: ErrorComponentProps) {
return
}
-export const Route = createFileRoute('/posts/$postId')({
+export const Route = createFileRoute({
loader: async ({ params: { postId } }) => fetchPost(postId),
errorComponent: PostErrorComponent as any,
notFoundComponent: () => {
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
index 33d0386c19..13529228bb 100644
--- 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
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/posts/')({
+export const Route = createFileRoute({
component: PostsIndexComponent,
})
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
index 9ae6dfc747..46203594d1 100644
--- 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
@@ -1,7 +1,7 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
import { fetchPosts } from '../../posts'
-export const Route = createFileRoute('/posts')({
+export const Route = createFileRoute({
loader: fetchPosts,
component: PostsComponent,
})
diff --git a/e2e/solid-router/basic-virtual-file-based/vite.config.ts b/e2e/solid-router/basic-virtual-file-based/vite.config.ts
index f87f32286a..1bebd08210 100644
--- a/e2e/solid-router/basic-virtual-file-based/vite.config.ts
+++ b/e2e/solid-router/basic-virtual-file-based/vite.config.ts
@@ -1,14 +1,15 @@
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
-import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
import { routes } from './routes'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
- TanStackRouterVite({
+ tanstackRouter({
target: 'solid',
autoCodeSplitting: true,
+ verboseFileRoutes: false,
virtualRouteConfig: routes,
}),
solid(),
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
index b79d571fb7..5103593208 100644
--- 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
@@ -26,6 +26,6 @@
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"vite-plugin-solid": "^2.11.2",
- "vite": "^6.1.0"
+ "vite": "^6.3.5"
}
}
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
index 055cba1e6f..a190b24202 100644
--- 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
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/_first/_second/layout-a')({
+export const Route = createFileRoute({
component: LayoutAComponent,
})
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
index c5bb8051af..505f8f6fbf 100644
--- 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
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/_first/_second/layout-b')({
+export const Route = createFileRoute({
component: LayoutBComponent,
})
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
index f7ff537916..f86335e291 100644
--- 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
@@ -1,5 +1,3 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/classic/hello/')({
+export const Route = createFileRoute({
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
index f4f30d8425..5b95eea555 100644
--- 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
@@ -1,6 +1,6 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/classic/hello')({
+export const Route = createFileRoute({
component: () => (
Hello!
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
index 2a6bf16c37..20b07c41cc 100644
--- 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
@@ -1,5 +1,3 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/classic/hello/universe')({
+export const Route = createFileRoute({
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
index 03edc7f484..4af11357a2 100644
--- 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
@@ -1,5 +1,3 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/classic/hello/world')({
+export const Route = createFileRoute({
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
index bdfb4c7676..510db79b73 100644
--- 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
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/')({
+export const Route = createFileRoute({
component: 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
index 5c77421bb2..c549175638 100644
--- 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
@@ -1,6 +1,6 @@
-import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_first')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
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
index 9ab147de5e..efeca5ce86 100644
--- 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
@@ -1,6 +1,6 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_first/_second')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
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
index 990f473ae8..ef413c1be0 100644
--- 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
@@ -1,4 +1,4 @@
-import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { ErrorComponent } from '@tanstack/solid-router'
import { fetchPost } from '../../posts'
import type { ErrorComponentProps } from '@tanstack/solid-router'
@@ -6,7 +6,7 @@ export function PostErrorComponent({ error }: ErrorComponentProps) {
return
}
-export const Route = createFileRoute('/posts/$postId')({
+export const Route = createFileRoute({
loader: async ({ params: { postId } }) => fetchPost(postId),
errorComponent: PostErrorComponent as any,
notFoundComponent: () => {
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
index 33d0386c19..13529228bb 100644
--- 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
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/posts/')({
+export const Route = createFileRoute({
component: PostsIndexComponent,
})
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
index 9ae6dfc747..46203594d1 100644
--- 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
@@ -1,7 +1,7 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
import { fetchPosts } from '../../posts'
-export const Route = createFileRoute('/posts')({
+export const Route = createFileRoute({
loader: fetchPosts,
component: PostsComponent,
})
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
index 91280315f2..635aa70940 100644
--- 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
@@ -1,13 +1,14 @@
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
-import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
- TanStackRouterVite({
+ tanstackRouter({
target: 'solid',
autoCodeSplitting: true,
+ verboseFileRoutes: false,
virtualRouteConfig: './routes.ts',
}),
solid(),
diff --git a/e2e/solid-router/basic/package.json b/e2e/solid-router/basic/package.json
index a3316c5ec2..1f557d00c3 100644
--- a/e2e/solid-router/basic/package.json
+++ b/e2e/solid-router/basic/package.json
@@ -23,6 +23,6 @@
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"vite-plugin-solid": "^2.11.2",
- "vite": "^6.1.0"
+ "vite": "^6.3.5"
}
}
diff --git a/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts b/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts
index 19909e9d77..27be8f1dfa 100644
--- a/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts
+++ b/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts
@@ -1,6 +1,6 @@
import { defineConfig } from '@rsbuild/core'
import { pluginSolid } from '@rsbuild/plugin-solid'
-import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'
+import { tanstackRouter } from '@tanstack/router-plugin/rspack'
import { pluginBabel } from '@rsbuild/plugin-babel'
export default defineConfig({
@@ -12,9 +12,7 @@ export default defineConfig({
],
tools: {
rspack: {
- plugins: [
- TanStackRouterRspack({ target: 'solid', autoCodeSplitting: true }),
- ],
+ plugins: [tanstackRouter({ target: 'solid', autoCodeSplitting: true })],
},
},
})
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
index d43b4ef5f5..5525c0c297 100644
--- a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx
@@ -1,4 +1,5 @@
-import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Outlet } from '@tanstack/solid-router'
export const Route = createFileRoute('/_layout')({
component: LayoutComponent,
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
index 7a5a3623a0..000a19e988 100644
--- 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
@@ -1,4 +1,5 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
export const Route = createFileRoute('/_layout/_layout-2')({
component: LayoutComponent,
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
index b69951b246..997e6caf80 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
component: LayoutAComponent,
})
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
index 30dbcce90f..70135c5809 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
component: LayoutBComponent,
})
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
index bdfb4c7676..3f905db60c 100644
--- a/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/')({
component: 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
index 55f8871d03..7a74c9adec 100644
--- 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
@@ -1,4 +1,5 @@
-import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { ErrorComponent } from '@tanstack/solid-router'
import { fetchPost } from '../posts'
import type { ErrorComponentProps } from '@tanstack/solid-router'
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
index 33d0386c19..812b581e0a 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
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
index 11a999f50a..86fb7a1841 100644
--- a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx
+++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx
@@ -1,4 +1,5 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
import { fetchPosts } from '../posts'
export const Route = createFileRoute('/posts')({
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
index f7b2c574f6..956d9edb08 100644
--- 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
@@ -1,6 +1,6 @@
import { defineConfig } from '@rsbuild/core'
import { pluginSolid } from '@rsbuild/plugin-solid'
-import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'
+import { tanstackRouter } from '@tanstack/router-plugin/rspack'
import { pluginBabel } from '@rsbuild/plugin-babel'
export default defineConfig({
@@ -13,7 +13,7 @@ export default defineConfig({
tools: {
rspack: {
plugins: [
- TanStackRouterRspack({
+ tanstackRouter({
target: 'solid',
autoCodeSplitting: true,
virtualRouteConfig: './routes.ts',
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
index 055cba1e6f..be88a87cbe 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/_first/_second/layout-a')({
component: LayoutAComponent,
})
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
index c5bb8051af..c26087837b 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/_first/_second/layout-b')({
component: LayoutBComponent,
})
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
index f7ff537916..6cc8bb1a6f 100644
--- 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
@@ -1,5 +1,4 @@
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
index f4f30d8425..3808e5346d 100644
--- 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
@@ -1,4 +1,5 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
export const Route = createFileRoute('/classic/hello')({
component: () => (
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
index 2a6bf16c37..f250de10c1 100644
--- 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
@@ -1,5 +1,4 @@
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
index 03edc7f484..d207a49114 100644
--- 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
@@ -1,5 +1,4 @@
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
index bdfb4c7676..3f905db60c 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/')({
component: 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
index 5c77421bb2..79135e7727 100644
--- 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
@@ -1,4 +1,5 @@
-import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Outlet } from '@tanstack/solid-router'
export const Route = createFileRoute('/_first')({
component: LayoutComponent,
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
index 9ab147de5e..8d843581a0 100644
--- 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
@@ -1,4 +1,5 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
export const Route = createFileRoute('/_first/_second')({
component: LayoutComponent,
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
index 990f473ae8..05383bbade 100644
--- 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
@@ -1,4 +1,5 @@
-import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { ErrorComponent } from '@tanstack/solid-router'
import { fetchPost } from '../../posts'
import type { ErrorComponentProps } from '@tanstack/solid-router'
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
index 33d0386c19..812b581e0a 100644
--- 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
@@ -1,5 +1,4 @@
import { createFileRoute } from '@tanstack/solid-router'
-
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
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
index 9ae6dfc747..228fab45f1 100644
--- 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
@@ -1,4 +1,5 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
import { fetchPosts } from '../../posts'
export const Route = createFileRoute('/posts')({
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/package.json b/e2e/solid-router/scroll-restoration-sandbox-vite/package.json
index 936a4a3214..acbbd56681 100644
--- a/e2e/solid-router/scroll-restoration-sandbox-vite/package.json
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/package.json
@@ -29,6 +29,6 @@
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"vite-plugin-solid": "^2.11.2",
- "vite": "^6.1.0"
+ "vite": "^6.3.5"
}
}
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
index 03f257bbfa..17dd854433 100644
--- a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts
@@ -13,25 +13,27 @@ 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'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as testsPageWithSearchRouteImport } from './routes/(tests)/page-with-search'
+import { Route as testsNormalPageRouteImport } from './routes/(tests)/normal-page'
+import { Route as testsLazyWithLoaderPageRouteImport } from './routes/(tests)/lazy-with-loader-page'
+import { Route as testsLazyPageRouteImport } from './routes/(tests)/lazy-page'
// Create Virtual Routes
-const testsVirtualPageLazyImport = createFileRoute('/(tests)/virtual-page')()
+const testsVirtualPageLazyRouteImport = createFileRoute(
+ '/(tests)/virtual-page',
+)()
// Create/Update Routes
-const IndexRoute = IndexImport.update({
+const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
-const testsVirtualPageLazyRoute = testsVirtualPageLazyImport
+const testsVirtualPageLazyRoute = testsVirtualPageLazyRouteImport
.update({
id: '/(tests)/virtual-page',
path: '/virtual-page',
@@ -39,19 +41,19 @@ const testsVirtualPageLazyRoute = testsVirtualPageLazyImport
} as any)
.lazy(() => import('./routes/(tests)/virtual-page.lazy').then((d) => d.Route))
-const testsPageWithSearchRoute = testsPageWithSearchImport.update({
+const testsPageWithSearchRoute = testsPageWithSearchRouteImport.update({
id: '/(tests)/page-with-search',
path: '/page-with-search',
getParentRoute: () => rootRoute,
} as any)
-const testsNormalPageRoute = testsNormalPageImport.update({
+const testsNormalPageRoute = testsNormalPageRouteImport.update({
id: '/(tests)/normal-page',
path: '/normal-page',
getParentRoute: () => rootRoute,
} as any)
-const testsLazyWithLoaderPageRoute = testsLazyWithLoaderPageImport
+const testsLazyWithLoaderPageRoute = testsLazyWithLoaderPageRouteImport
.update({
id: '/(tests)/lazy-with-loader-page',
path: '/lazy-with-loader-page',
@@ -61,7 +63,7 @@ const testsLazyWithLoaderPageRoute = testsLazyWithLoaderPageImport
import('./routes/(tests)/lazy-with-loader-page.lazy').then((d) => d.Route),
)
-const testsLazyPageRoute = testsLazyPageImport
+const testsLazyPageRoute = testsLazyPageRouteImport
.update({
id: '/(tests)/lazy-page',
path: '/lazy-page',
@@ -77,42 +79,42 @@ declare module '@tanstack/solid-router' {
id: '/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof IndexImport
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRoute
}
'/(tests)/lazy-page': {
id: '/(tests)/lazy-page'
path: '/lazy-page'
fullPath: '/lazy-page'
- preLoaderRoute: typeof testsLazyPageImport
+ preLoaderRoute: typeof testsLazyPageRouteImport
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
+ preLoaderRoute: typeof testsLazyWithLoaderPageRouteImport
parentRoute: typeof rootRoute
}
'/(tests)/normal-page': {
id: '/(tests)/normal-page'
path: '/normal-page'
fullPath: '/normal-page'
- preLoaderRoute: typeof testsNormalPageImport
+ preLoaderRoute: typeof testsNormalPageRouteImport
parentRoute: typeof rootRoute
}
'/(tests)/page-with-search': {
id: '/(tests)/page-with-search'
path: '/page-with-search'
fullPath: '/page-with-search'
- preLoaderRoute: typeof testsPageWithSearchImport
+ preLoaderRoute: typeof testsPageWithSearchRouteImport
parentRoute: typeof rootRoute
}
'/(tests)/virtual-page': {
id: '/(tests)/virtual-page'
path: '/virtual-page'
fullPath: '/virtual-page'
- preLoaderRoute: typeof testsVirtualPageLazyImport
+ preLoaderRoute: typeof testsVirtualPageLazyRouteImport
parentRoute: typeof rootRoute
}
}
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
index 042a053765..bec395f32c 100644
--- 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
@@ -1,3 +1,2 @@
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/index.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/index.tsx
index 991ecc3435..ddb3755915 100644
--- a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/index.tsx
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/index.tsx
@@ -1,4 +1,5 @@
-import { Link, createFileRoute, linkOptions } from '@tanstack/solid-router'
+import { createFileRoute } from '@tanstack/solid-router'
+import { Link, linkOptions } from '@tanstack/solid-router'
export const Route = createFileRoute('/')({
component: HomeComponent,
diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js b/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js
index 0d2f08b695..1fc0fce40a 100644
--- a/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js
+++ b/e2e/solid-router/scroll-restoration-sandbox-vite/vite.config.js
@@ -1,8 +1,8 @@
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
-import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [TanStackRouterVite({ target: 'solid' }), solid()],
+ plugins: [tanstackRouter({ target: 'solid' }), solid()],
})
diff --git a/e2e/solid-start/basic-tsr-config/.gitignore b/e2e/solid-start/basic-tsr-config/.gitignore
index 2b76174be5..14577724eb 100644
--- a/e2e/solid-start/basic-tsr-config/.gitignore
+++ b/e2e/solid-start/basic-tsr-config/.gitignore
@@ -7,13 +7,11 @@ yarn.lock
.env
.vercel
.output
-.vinxi
/build/
/api/
/server/build
/public/build
-.vinxi
# Sentry Config File
.env.sentry-build-plugin
/test-results/
diff --git a/e2e/solid-start/basic-tsr-config/.tanstack-start/server-routes/routeTree.gen.ts b/e2e/solid-start/basic-tsr-config/.tanstack-start/server-routes/routeTree.gen.ts
new file mode 100644
index 0000000000..2dc6bfc4bd
--- /dev/null
+++ b/e2e/solid-start/basic-tsr-config/.tanstack-start/server-routes/routeTree.gen.ts
@@ -0,0 +1,70 @@
+/* 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 type {
+ FileRoutesByPath,
+ CreateServerFileRoute,
+} from '@tanstack/solid-start/server'
+import {
+ createServerRoute,
+ createServerFileRoute,
+} from '@tanstack/solid-start/server'
+
+// Create/Update Routes
+
+const rootRoute = createServerRoute()
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-start/server' {
+ interface FileRoutesByPath {}
+}
+
+// Add type-safety to the createFileRoute function across the route tree
+
+// Create and export the route tree
+
+export interface FileRoutesByFullPath {}
+
+export interface FileRoutesByTo {}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: never
+ fileRoutesByTo: FileRoutesByTo
+ to: never
+ id: '__root__'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {}
+
+const rootRouteChildren: RootRouteChildren = {}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes
()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": []
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/solid-start/basic-tsr-config/app.config.ts b/e2e/solid-start/basic-tsr-config/app.config.ts
deleted file mode 100644
index d798414e98..0000000000
--- a/e2e/solid-start/basic-tsr-config/app.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// app.config.ts
-import { defineConfig } from '@tanstack/solid-start/config'
-
-export default defineConfig({
- tsr: {
- appDirectory: './src/app',
- },
-})
diff --git a/e2e/solid-start/basic-tsr-config/package.json b/e2e/solid-start/basic-tsr-config/package.json
index 475a1d746b..5ad3489cac 100644
--- a/e2e/solid-start/basic-tsr-config/package.json
+++ b/e2e/solid-start/basic-tsr-config/package.json
@@ -4,10 +4,10 @@
"sideEffects": false,
"type": "module",
"scripts": {
- "dev": "vinxi dev --port 3000",
- "dev:e2e": "vinxi dev",
- "build": "rimraf ./count.txt && vinxi build && tsc --noEmit",
- "start": "vinxi start",
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "rimraf ./count.txt && vite build && tsc --noEmit",
+ "start": "node .output/server/index.mjs",
"test:e2e": "playwright test --project=chromium"
},
"dependencies": {
@@ -15,11 +15,12 @@
"@tanstack/solid-router-devtools": "workspace:^",
"@tanstack/solid-start": "workspace:^",
"solid-js": "^1.9.5",
- "vinxi": "0.5.3"
+ "vite": "6.3.5"
},
"devDependencies": {
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
- "typescript": "^5.7.2"
+ "typescript": "^5.7.2",
+ "vite-tsconfig-paths": "^5.1.4"
}
}
diff --git a/e2e/solid-start/basic-tsr-config/playwright.config.ts b/e2e/solid-start/basic-tsr-config/playwright.config.ts
index bb77d0cf70..e834d88cf4 100644
--- a/e2e/solid-start/basic-tsr-config/playwright.config.ts
+++ b/e2e/solid-start/basic-tsr-config/playwright.config.ts
@@ -4,6 +4,7 @@ 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.
*/
@@ -19,7 +20,7 @@ export default defineConfig({
},
webServer: {
- command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ command: `pnpm build && VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
diff --git a/e2e/solid-start/basic-tsr-config/src/app/client.tsx b/e2e/solid-start/basic-tsr-config/src/app/client.tsx
deleted file mode 100644
index ba0f02fac0..0000000000
--- a/e2e/solid-start/basic-tsr-config/src/app/client.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-///
-import { hydrate } from 'solid-js/web'
-import { StartClient } from '@tanstack/solid-start'
-import { createRouter } from './router'
-
-const router = createRouter()
-
-hydrate(() => , document.body)
diff --git a/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts b/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts
index c260510053..3a60fd4405 100644
--- a/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts
+++ b/e2e/solid-start/basic-tsr-config/src/app/routeTree.gen.ts
@@ -8,14 +8,16 @@
// 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 type { CreateFileRoute, FileRoutesByPath } from '@tanstack/solid-router'
+
// Import Routes
import { Route as rootRoute } from './routes/__root'
-import { Route as IndexImport } from './routes/index'
+import { Route as IndexRouteImport } from './routes/index'
// Create/Update Routes
-const IndexRoute = IndexImport.update({
+const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
@@ -29,12 +31,24 @@ declare module '@tanstack/solid-router' {
id: '/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof IndexImport
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRoute
}
}
}
+// Add type-safety to the createFileRoute function across the route tree
+
+declare module './routes/index' {
+ const createFileRoute: CreateFileRoute<
+ '/',
+ FileRoutesByPath['/']['parentRoute'],
+ FileRoutesByPath['/']['id'],
+ FileRoutesByPath['/']['path'],
+ FileRoutesByPath['/']['fullPath']
+ >
+}
+
// Create and export the route tree
export interface FileRoutesByFullPath {
diff --git a/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx b/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx
index 4d15620109..32e530dbbf 100644
--- a/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx
+++ b/e2e/solid-start/basic-tsr-config/src/app/routes/index.tsx
@@ -1,5 +1,5 @@
import fs from 'node:fs'
-import { createFileRoute, useRouter } from '@tanstack/solid-router'
+import { useRouter } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
const filePath = 'count.txt'
@@ -17,7 +17,7 @@ const updateCount = createServerFn({ method: 'POST' })
const count = await getCount()
await fs.promises.writeFile(filePath, `${count + data}`)
})
-export const Route = createFileRoute('/')({
+export const Route = createFileRoute({
component: Home,
loader: async () => await getCount(),
})
diff --git a/e2e/solid-start/basic-tsr-config/src/app/tanstack-start.d.ts b/e2e/solid-start/basic-tsr-config/src/app/tanstack-start.d.ts
new file mode 100644
index 0000000000..df36e6de73
--- /dev/null
+++ b/e2e/solid-start/basic-tsr-config/src/app/tanstack-start.d.ts
@@ -0,0 +1,2 @@
+///
+import '../../.tanstack-start/server-routes/routeTree.gen'
diff --git a/e2e/solid-start/basic-tsr-config/src/app/vite-env.d.ts b/e2e/solid-start/basic-tsr-config/src/app/vite-env.d.ts
new file mode 100644
index 0000000000..0b2af560d6
--- /dev/null
+++ b/e2e/solid-start/basic-tsr-config/src/app/vite-env.d.ts
@@ -0,0 +1,4 @@
+declare module '*?url' {
+ const url: string
+ export default url
+}
diff --git a/e2e/solid-start/basic-tsr-config/vite.config.ts b/e2e/solid-start/basic-tsr-config/vite.config.ts
new file mode 100644
index 0000000000..347962b83d
--- /dev/null
+++ b/e2e/solid-start/basic-tsr-config/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ tsr: {
+ srcDirectory: './src/app',
+ },
+ }),
+ ],
+})
diff --git a/e2e/solid-start/basic/.gitignore b/e2e/solid-start/basic/.gitignore
index be342025da..a79d5cf129 100644
--- a/e2e/solid-start/basic/.gitignore
+++ b/e2e/solid-start/basic/.gitignore
@@ -7,13 +7,11 @@ yarn.lock
.env
.vercel
.output
-.vinxi
/build/
/api/
/server/build
/public/build
-.vinxi
# Sentry Config File
.env.sentry-build-plugin
/test-results/
diff --git a/e2e/solid-start/basic/.tanstack-start/server-routes/routeTree.gen.ts b/e2e/solid-start/basic/.tanstack-start/server-routes/routeTree.gen.ts
new file mode 100644
index 0000000000..e36fccb479
--- /dev/null
+++ b/e2e/solid-start/basic/.tanstack-start/server-routes/routeTree.gen.ts
@@ -0,0 +1,155 @@
+/* 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 type {
+ FileRoutesByPath,
+ CreateServerFileRoute,
+} from '@tanstack/solid-start/server'
+import {
+ createServerRoute,
+ createServerFileRoute,
+} from '@tanstack/solid-start/server'
+
+import { ServerRoute as ApiUsersRouteImport } from './../../src/routes/api/users'
+import { ServerRoute as ApiUsersUserIdRouteImport } from './../../src/routes/api/users.$userId'
+
+// Create/Update Routes
+
+const rootRoute = createServerRoute()
+
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-start/server' {
+ interface FileRoutesByPath {
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRoute
+ }
+ '/api/users/$userId': {
+ id: '/api/users/$userId'
+ path: '/$userId'
+ fullPath: '/api/users/$userId'
+ preLoaderRoute: typeof ApiUsersUserIdRouteImport
+ parentRoute: typeof ApiUsersRouteImport
+ }
+ }
+}
+
+// Add type-safety to the createFileRoute function across the route tree
+
+declare module './../../src/routes/api/users' {
+ const createServerFileRoute: CreateServerFileRoute<
+ FileRoutesByPath['/api/users']['parentRoute'],
+ FileRoutesByPath['/api/users']['id'],
+ FileRoutesByPath['/api/users']['path'],
+ FileRoutesByPath['/api/users']['fullPath'],
+ ApiUsersRouteChildren
+ >
+}
+declare module './../../src/routes/api/users.$userId' {
+ const createServerFileRoute: CreateServerFileRoute<
+ FileRoutesByPath['/api/users/$userId']['parentRoute'],
+ FileRoutesByPath['/api/users/$userId']['id'],
+ FileRoutesByPath['/api/users/$userId']['path'],
+ FileRoutesByPath['/api/users/$userId']['fullPath'],
+ unknown
+ >
+}
+
+// Create and export the route tree
+
+interface ApiUsersRouteChildren {
+ ApiUsersUserIdRoute: typeof ApiUsersUserIdRoute
+}
+
+const ApiUsersRouteChildren: ApiUsersRouteChildren = {
+ ApiUsersUserIdRoute: ApiUsersUserIdRoute,
+}
+
+const ApiUsersRouteWithChildren = ApiUsersRoute._addFileChildren(
+ ApiUsersRouteChildren,
+)
+
+export interface FileRoutesByFullPath {
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/api/users/$userId': typeof ApiUsersUserIdRoute
+}
+
+export interface FileRoutesByTo {
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/api/users/$userId': typeof ApiUsersUserIdRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/api/users/$userId': typeof ApiUsersUserIdRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/api/users' | '/api/users/$userId'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/api/users' | '/api/users/$userId'
+ id: '__root__' | '/api/users' | '/api/users/$userId'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ ApiUsersRoute: typeof ApiUsersRouteWithChildren
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ApiUsersRoute: ApiUsersRouteWithChildren,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/api/users"
+ ]
+ },
+ "/api/users": {
+ "filePath": "api/users.ts",
+ "children": [
+ "/api/users/$userId"
+ ]
+ },
+ "/api/users/$userId": {
+ "filePath": "api/users.$userId.ts",
+ "parent": "/api/users"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/solid-start/basic/app.config.ts b/e2e/solid-start/basic/app.config.ts
deleted file mode 100644
index 2a06e3d3f0..0000000000
--- a/e2e/solid-start/basic/app.config.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineConfig } from '@tanstack/solid-start/config'
-import tsConfigPaths from 'vite-tsconfig-paths'
-
-export default defineConfig({
- tsr: {
- appDirectory: 'src',
- },
- vite: {
- plugins: [
- tsConfigPaths({
- projects: ['./tsconfig.json'],
- }),
- ],
- },
-})
diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json
index a10fb47d62..791b179916 100644
--- a/e2e/solid-start/basic/package.json
+++ b/e2e/solid-start/basic/package.json
@@ -4,32 +4,32 @@
"sideEffects": false,
"type": "module",
"scripts": {
- "dev": "vinxi dev --port 3000",
- "dev:e2e": "vinxi dev",
- "build": "vinxi build && tsc --noEmit",
- "start": "vinxi start",
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "start": "node .output/server/index.mjs",
"test:e2e": "playwright test --project=chromium"
},
"dependencies": {
"@tanstack/solid-router": "workspace:^",
"@tanstack/solid-router-devtools": "workspace:^",
"@tanstack/solid-start": "workspace:^",
- "solid-js": "^1.9.5",
"redaxios": "^0.5.1",
+ "solid-js": "^1.9.5",
"tailwind-merge": "^2.6.0",
- "vinxi": "0.5.3",
+ "vite": "6.3.5",
"zod": "^3.24.2"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
- "@types/node": "^22.10.2",
"@tanstack/router-e2e-utils": "workspace:^",
- "vite-plugin-solid": "^2.11.2",
+ "@types/node": "^22.10.2",
+ "autoprefixer": "^10.4.20",
"combinate": "^1.1.11",
"postcss": "^8.5.1",
- "autoprefixer": "^10.4.20",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
+ "vite-plugin-solid": "^2.11.2",
"vite-tsconfig-paths": "^5.1.4"
}
}
diff --git a/e2e/solid-start/basic/playwright.config.ts b/e2e/solid-start/basic/playwright.config.ts
index 95d043d48b..e834d88cf4 100644
--- a/e2e/solid-start/basic/playwright.config.ts
+++ b/e2e/solid-start/basic/playwright.config.ts
@@ -20,7 +20,7 @@ export default defineConfig({
},
webServer: {
- command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ command: `pnpm build && VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
diff --git a/e2e/solid-start/basic/src/api.ts b/e2e/solid-start/basic/src/api.ts
deleted file mode 100644
index ed511bcd26..0000000000
--- a/e2e/solid-start/basic/src/api.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- createStartAPIHandler,
- defaultAPIFileRouteHandler,
-} from '@tanstack/solid-start/api'
-
-export default createStartAPIHandler(defaultAPIFileRouteHandler)
diff --git a/e2e/solid-start/basic/src/client.tsx b/e2e/solid-start/basic/src/client.tsx
index ba0f02fac0..b2fdcc9505 100644
--- a/e2e/solid-start/basic/src/client.tsx
+++ b/e2e/solid-start/basic/src/client.tsx
@@ -1,8 +1,11 @@
-///
+// DO NOT DELETE THIS FILE!!!
+// This file is a good smoke test to make sure the custom client entry is working
import { hydrate } from 'solid-js/web'
import { StartClient } from '@tanstack/solid-start'
import { createRouter } from './router'
+console.log("[client-entry]: using custom client entry in 'src/client.tsx'")
+
const router = createRouter()
hydrate(() => , document.body)
diff --git a/e2e/solid-start/basic/src/routeTree.gen.ts b/e2e/solid-start/basic/src/routeTree.gen.ts
index db7101d5f5..d4db3b87d4 100644
--- a/e2e/solid-start/basic/src/routeTree.gen.ts
+++ b/e2e/solid-start/basic/src/routeTree.gen.ts
@@ -8,220 +8,222 @@
// 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 type { CreateFileRoute, FileRoutesByPath } from '@tanstack/solid-router'
+
// Import Routes
import { Route as rootRoute } from './routes/__root'
-import { Route as UsersImport } from './routes/users'
-import { Route as StreamImport } from './routes/stream'
-import { Route as SearchParamsImport } from './routes/search-params'
-import { Route as ScriptsImport } from './routes/scripts'
-import { Route as PostsImport } from './routes/posts'
-import { Route as LinksImport } from './routes/links'
-import { Route as DeferredImport } from './routes/deferred'
-import { Route as LayoutImport } from './routes/_layout'
-import { Route as NotFoundRouteImport } from './routes/not-found/route'
-import { Route as IndexImport } from './routes/index'
-import { Route as UsersIndexImport } from './routes/users.index'
-import { Route as RedirectIndexImport } from './routes/redirect/index'
-import { Route as PostsIndexImport } from './routes/posts.index'
-import { Route as NotFoundIndexImport } from './routes/not-found/index'
-import { Route as UsersUserIdImport } from './routes/users.$userId'
-import { Route as RedirectTargetImport } from './routes/redirect/$target'
-import { Route as PostsPostIdImport } from './routes/posts.$postId'
-import { Route as NotFoundViaLoaderImport } from './routes/not-found/via-loader'
-import { Route as NotFoundViaBeforeLoadImport } from './routes/not-found/via-beforeLoad'
-import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2'
-import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index'
-import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader'
-import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad'
-import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep'
-import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b'
-import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a'
-import { Route as RedirectTargetServerFnIndexImport } from './routes/redirect/$target/serverFn/index'
-import { Route as RedirectTargetServerFnViaUseServerFnImport } from './routes/redirect/$target/serverFn/via-useServerFn'
-import { Route as RedirectTargetServerFnViaLoaderImport } from './routes/redirect/$target/serverFn/via-loader'
-import { Route as RedirectTargetServerFnViaBeforeLoadImport } from './routes/redirect/$target/serverFn/via-beforeLoad'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as StreamRouteImport } from './routes/stream'
+import { Route as SearchParamsRouteImport } from './routes/search-params'
+import { Route as ScriptsRouteImport } from './routes/scripts'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as LinksRouteImport } from './routes/links'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as NotFoundRouteRouteImport } from './routes/not-found/route'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as RedirectIndexRouteImport } from './routes/redirect/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as NotFoundIndexRouteImport } from './routes/not-found/index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
+import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
+import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
+import { Route as RedirectTargetViaLoaderRouteImport } from './routes/redirect/$target/via-loader'
+import { Route as RedirectTargetViaBeforeLoadRouteImport } from './routes/redirect/$target/via-beforeLoad'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as RedirectTargetServerFnIndexRouteImport } from './routes/redirect/$target/serverFn/index'
+import { Route as RedirectTargetServerFnViaUseServerFnRouteImport } from './routes/redirect/$target/serverFn/via-useServerFn'
+import { Route as RedirectTargetServerFnViaLoaderRouteImport } from './routes/redirect/$target/serverFn/via-loader'
+import { Route as RedirectTargetServerFnViaBeforeLoadRouteImport } from './routes/redirect/$target/serverFn/via-beforeLoad'
// Create/Update Routes
-const UsersRoute = UsersImport.update({
+const UsersRoute = UsersRouteImport.update({
id: '/users',
path: '/users',
getParentRoute: () => rootRoute,
} as any)
-const StreamRoute = StreamImport.update({
+const StreamRoute = StreamRouteImport.update({
id: '/stream',
path: '/stream',
getParentRoute: () => rootRoute,
} as any)
-const SearchParamsRoute = SearchParamsImport.update({
+const SearchParamsRoute = SearchParamsRouteImport.update({
id: '/search-params',
path: '/search-params',
getParentRoute: () => rootRoute,
} as any)
-const ScriptsRoute = ScriptsImport.update({
+const ScriptsRoute = ScriptsRouteImport.update({
id: '/scripts',
path: '/scripts',
getParentRoute: () => rootRoute,
} as any)
-const PostsRoute = PostsImport.update({
+const PostsRoute = PostsRouteImport.update({
id: '/posts',
path: '/posts',
getParentRoute: () => rootRoute,
} as any)
-const LinksRoute = LinksImport.update({
+const LinksRoute = LinksRouteImport.update({
id: '/links',
path: '/links',
getParentRoute: () => rootRoute,
} as any)
-const DeferredRoute = DeferredImport.update({
+const DeferredRoute = DeferredRouteImport.update({
id: '/deferred',
path: '/deferred',
getParentRoute: () => rootRoute,
} as any)
-const LayoutRoute = LayoutImport.update({
+const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRoute,
} as any)
-const NotFoundRouteRoute = NotFoundRouteImport.update({
+const NotFoundRouteRoute = NotFoundRouteRouteImport.update({
id: '/not-found',
path: '/not-found',
getParentRoute: () => rootRoute,
} as any)
-const IndexRoute = IndexImport.update({
+const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
-const UsersIndexRoute = UsersIndexImport.update({
+const UsersIndexRoute = UsersIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => UsersRoute,
} as any)
-const RedirectIndexRoute = RedirectIndexImport.update({
+const RedirectIndexRoute = RedirectIndexRouteImport.update({
id: '/redirect/',
path: '/redirect/',
getParentRoute: () => rootRoute,
} as any)
-const PostsIndexRoute = PostsIndexImport.update({
+const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const NotFoundIndexRoute = NotFoundIndexImport.update({
+const NotFoundIndexRoute = NotFoundIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => NotFoundRouteRoute,
} as any)
-const UsersUserIdRoute = UsersUserIdImport.update({
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
-const RedirectTargetRoute = RedirectTargetImport.update({
+const RedirectTargetRoute = RedirectTargetRouteImport.update({
id: '/redirect/$target',
path: '/redirect/$target',
getParentRoute: () => rootRoute,
} as any)
-const PostsPostIdRoute = PostsPostIdImport.update({
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const NotFoundViaLoaderRoute = NotFoundViaLoaderImport.update({
+const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
id: '/via-loader',
path: '/via-loader',
getParentRoute: () => NotFoundRouteRoute,
} as any)
-const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadImport.update({
+const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
id: '/via-beforeLoad',
path: '/via-beforeLoad',
getParentRoute: () => NotFoundRouteRoute,
} as any)
-const LayoutLayout2Route = LayoutLayout2Import.update({
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
} as any)
-const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({
+const RedirectTargetIndexRoute = RedirectTargetIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => RedirectTargetRoute,
} as any)
-const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({
+const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderRouteImport.update({
id: '/via-loader',
path: '/via-loader',
getParentRoute: () => RedirectTargetRoute,
} as any)
const RedirectTargetViaBeforeLoadRoute =
- RedirectTargetViaBeforeLoadImport.update({
+ RedirectTargetViaBeforeLoadRouteImport.update({
id: '/via-beforeLoad',
path: '/via-beforeLoad',
getParentRoute: () => RedirectTargetRoute,
} as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
id: '/posts_/$postId/deep',
path: '/posts/$postId/deep',
getParentRoute: () => rootRoute,
} as any)
-const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
id: '/layout-b',
path: '/layout-b',
getParentRoute: () => LayoutLayout2Route,
} as any)
-const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
id: '/layout-a',
path: '/layout-a',
getParentRoute: () => LayoutLayout2Route,
} as any)
const RedirectTargetServerFnIndexRoute =
- RedirectTargetServerFnIndexImport.update({
+ RedirectTargetServerFnIndexRouteImport.update({
id: '/serverFn/',
path: '/serverFn/',
getParentRoute: () => RedirectTargetRoute,
} as any)
const RedirectTargetServerFnViaUseServerFnRoute =
- RedirectTargetServerFnViaUseServerFnImport.update({
+ RedirectTargetServerFnViaUseServerFnRouteImport.update({
id: '/serverFn/via-useServerFn',
path: '/serverFn/via-useServerFn',
getParentRoute: () => RedirectTargetRoute,
} as any)
const RedirectTargetServerFnViaLoaderRoute =
- RedirectTargetServerFnViaLoaderImport.update({
+ RedirectTargetServerFnViaLoaderRouteImport.update({
id: '/serverFn/via-loader',
path: '/serverFn/via-loader',
getParentRoute: () => RedirectTargetRoute,
} as any)
const RedirectTargetServerFnViaBeforeLoadRoute =
- RedirectTargetServerFnViaBeforeLoadImport.update({
+ RedirectTargetServerFnViaBeforeLoadRouteImport.update({
id: '/serverFn/via-beforeLoad',
path: '/serverFn/via-beforeLoad',
getParentRoute: () => RedirectTargetRoute,
@@ -235,215 +237,488 @@ declare module '@tanstack/solid-router' {
id: '/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof IndexImport
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRoute
}
'/not-found': {
id: '/not-found'
path: '/not-found'
fullPath: '/not-found'
- preLoaderRoute: typeof NotFoundRouteImport
+ preLoaderRoute: typeof NotFoundRouteRouteImport
parentRoute: typeof rootRoute
}
'/_layout': {
id: '/_layout'
path: ''
fullPath: ''
- preLoaderRoute: typeof LayoutImport
+ preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRoute
}
'/deferred': {
id: '/deferred'
path: '/deferred'
fullPath: '/deferred'
- preLoaderRoute: typeof DeferredImport
+ preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRoute
}
'/links': {
id: '/links'
path: '/links'
fullPath: '/links'
- preLoaderRoute: typeof LinksImport
+ preLoaderRoute: typeof LinksRouteImport
parentRoute: typeof rootRoute
}
'/posts': {
id: '/posts'
path: '/posts'
fullPath: '/posts'
- preLoaderRoute: typeof PostsImport
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRoute
}
'/scripts': {
id: '/scripts'
path: '/scripts'
fullPath: '/scripts'
- preLoaderRoute: typeof ScriptsImport
+ preLoaderRoute: typeof ScriptsRouteImport
parentRoute: typeof rootRoute
}
'/search-params': {
id: '/search-params'
path: '/search-params'
fullPath: '/search-params'
- preLoaderRoute: typeof SearchParamsImport
+ preLoaderRoute: typeof SearchParamsRouteImport
parentRoute: typeof rootRoute
}
'/stream': {
id: '/stream'
path: '/stream'
fullPath: '/stream'
- preLoaderRoute: typeof StreamImport
+ preLoaderRoute: typeof StreamRouteImport
parentRoute: typeof rootRoute
}
'/users': {
id: '/users'
path: '/users'
fullPath: '/users'
- preLoaderRoute: typeof UsersImport
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRoute
}
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
fullPath: ''
- preLoaderRoute: typeof LayoutLayout2Import
- parentRoute: typeof LayoutImport
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRouteImport
}
'/not-found/via-beforeLoad': {
id: '/not-found/via-beforeLoad'
path: '/via-beforeLoad'
fullPath: '/not-found/via-beforeLoad'
- preLoaderRoute: typeof NotFoundViaBeforeLoadImport
- parentRoute: typeof NotFoundRouteImport
+ preLoaderRoute: typeof NotFoundViaBeforeLoadRouteImport
+ parentRoute: typeof NotFoundRouteRouteImport
}
'/not-found/via-loader': {
id: '/not-found/via-loader'
path: '/via-loader'
fullPath: '/not-found/via-loader'
- preLoaderRoute: typeof NotFoundViaLoaderImport
- parentRoute: typeof NotFoundRouteImport
+ preLoaderRoute: typeof NotFoundViaLoaderRouteImport
+ parentRoute: typeof NotFoundRouteRouteImport
}
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
fullPath: '/posts/$postId'
- preLoaderRoute: typeof PostsPostIdImport
- parentRoute: typeof PostsImport
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRouteImport
}
'/redirect/$target': {
id: '/redirect/$target'
path: '/redirect/$target'
fullPath: '/redirect/$target'
- preLoaderRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetRouteImport
parentRoute: typeof rootRoute
}
'/users/$userId': {
id: '/users/$userId'
path: '/$userId'
fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdImport
- parentRoute: typeof UsersImport
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRouteImport
}
'/not-found/': {
id: '/not-found/'
path: '/'
fullPath: '/not-found/'
- preLoaderRoute: typeof NotFoundIndexImport
- parentRoute: typeof NotFoundRouteImport
+ preLoaderRoute: typeof NotFoundIndexRouteImport
+ parentRoute: typeof NotFoundRouteRouteImport
}
'/posts/': {
id: '/posts/'
path: '/'
fullPath: '/posts/'
- preLoaderRoute: typeof PostsIndexImport
- parentRoute: typeof PostsImport
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRouteImport
}
'/redirect/': {
id: '/redirect/'
path: '/redirect'
fullPath: '/redirect'
- preLoaderRoute: typeof RedirectIndexImport
+ preLoaderRoute: typeof RedirectIndexRouteImport
parentRoute: typeof rootRoute
}
'/users/': {
id: '/users/'
path: '/'
fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexImport
- parentRoute: typeof UsersImport
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRouteImport
}
'/_layout/_layout-2/layout-a': {
id: '/_layout/_layout-2/layout-a'
path: '/layout-a'
fullPath: '/layout-a'
- preLoaderRoute: typeof LayoutLayout2LayoutAImport
- parentRoute: typeof LayoutLayout2Import
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2RouteImport
}
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
path: '/layout-b'
fullPath: '/layout-b'
- preLoaderRoute: typeof LayoutLayout2LayoutBImport
- parentRoute: typeof LayoutLayout2Import
+ preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
+ parentRoute: typeof LayoutLayout2RouteImport
}
'/posts_/$postId/deep': {
id: '/posts_/$postId/deep'
path: '/posts/$postId/deep'
fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepImport
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
parentRoute: typeof rootRoute
}
'/redirect/$target/via-beforeLoad': {
id: '/redirect/$target/via-beforeLoad'
path: '/via-beforeLoad'
fullPath: '/redirect/$target/via-beforeLoad'
- preLoaderRoute: typeof RedirectTargetViaBeforeLoadImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetViaBeforeLoadRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
'/redirect/$target/via-loader': {
id: '/redirect/$target/via-loader'
path: '/via-loader'
fullPath: '/redirect/$target/via-loader'
- preLoaderRoute: typeof RedirectTargetViaLoaderImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetViaLoaderRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
'/redirect/$target/': {
id: '/redirect/$target/'
path: '/'
fullPath: '/redirect/$target/'
- preLoaderRoute: typeof RedirectTargetIndexImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetIndexRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
'/redirect/$target/serverFn/via-beforeLoad': {
id: '/redirect/$target/serverFn/via-beforeLoad'
path: '/serverFn/via-beforeLoad'
fullPath: '/redirect/$target/serverFn/via-beforeLoad'
- preLoaderRoute: typeof RedirectTargetServerFnViaBeforeLoadImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetServerFnViaBeforeLoadRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
'/redirect/$target/serverFn/via-loader': {
id: '/redirect/$target/serverFn/via-loader'
path: '/serverFn/via-loader'
fullPath: '/redirect/$target/serverFn/via-loader'
- preLoaderRoute: typeof RedirectTargetServerFnViaLoaderImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetServerFnViaLoaderRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
'/redirect/$target/serverFn/via-useServerFn': {
id: '/redirect/$target/serverFn/via-useServerFn'
path: '/serverFn/via-useServerFn'
fullPath: '/redirect/$target/serverFn/via-useServerFn'
- preLoaderRoute: typeof RedirectTargetServerFnViaUseServerFnImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetServerFnViaUseServerFnRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
'/redirect/$target/serverFn/': {
id: '/redirect/$target/serverFn/'
path: '/serverFn'
fullPath: '/redirect/$target/serverFn'
- preLoaderRoute: typeof RedirectTargetServerFnIndexImport
- parentRoute: typeof RedirectTargetImport
+ preLoaderRoute: typeof RedirectTargetServerFnIndexRouteImport
+ parentRoute: typeof RedirectTargetRouteImport
}
}
}
+// Add type-safety to the createFileRoute function across the route tree
+
+declare module './routes/index' {
+ const createFileRoute: CreateFileRoute<
+ '/',
+ FileRoutesByPath['/']['parentRoute'],
+ FileRoutesByPath['/']['id'],
+ FileRoutesByPath['/']['path'],
+ FileRoutesByPath['/']['fullPath']
+ >
+}
+declare module './routes/not-found/route' {
+ const createFileRoute: CreateFileRoute<
+ '/not-found',
+ FileRoutesByPath['/not-found']['parentRoute'],
+ FileRoutesByPath['/not-found']['id'],
+ FileRoutesByPath['/not-found']['path'],
+ FileRoutesByPath['/not-found']['fullPath']
+ >
+}
+declare module './routes/_layout' {
+ const createFileRoute: CreateFileRoute<
+ '/_layout',
+ FileRoutesByPath['/_layout']['parentRoute'],
+ FileRoutesByPath['/_layout']['id'],
+ FileRoutesByPath['/_layout']['path'],
+ FileRoutesByPath['/_layout']['fullPath']
+ >
+}
+declare module './routes/deferred' {
+ const createFileRoute: CreateFileRoute<
+ '/deferred',
+ FileRoutesByPath['/deferred']['parentRoute'],
+ FileRoutesByPath['/deferred']['id'],
+ FileRoutesByPath['/deferred']['path'],
+ FileRoutesByPath['/deferred']['fullPath']
+ >
+}
+declare module './routes/links' {
+ const createFileRoute: CreateFileRoute<
+ '/links',
+ FileRoutesByPath['/links']['parentRoute'],
+ FileRoutesByPath['/links']['id'],
+ FileRoutesByPath['/links']['path'],
+ FileRoutesByPath['/links']['fullPath']
+ >
+}
+declare module './routes/posts' {
+ const createFileRoute: CreateFileRoute<
+ '/posts',
+ FileRoutesByPath['/posts']['parentRoute'],
+ FileRoutesByPath['/posts']['id'],
+ FileRoutesByPath['/posts']['path'],
+ FileRoutesByPath['/posts']['fullPath']
+ >
+}
+declare module './routes/scripts' {
+ const createFileRoute: CreateFileRoute<
+ '/scripts',
+ FileRoutesByPath['/scripts']['parentRoute'],
+ FileRoutesByPath['/scripts']['id'],
+ FileRoutesByPath['/scripts']['path'],
+ FileRoutesByPath['/scripts']['fullPath']
+ >
+}
+declare module './routes/search-params' {
+ const createFileRoute: CreateFileRoute<
+ '/search-params',
+ FileRoutesByPath['/search-params']['parentRoute'],
+ FileRoutesByPath['/search-params']['id'],
+ FileRoutesByPath['/search-params']['path'],
+ FileRoutesByPath['/search-params']['fullPath']
+ >
+}
+declare module './routes/stream' {
+ const createFileRoute: CreateFileRoute<
+ '/stream',
+ FileRoutesByPath['/stream']['parentRoute'],
+ FileRoutesByPath['/stream']['id'],
+ FileRoutesByPath['/stream']['path'],
+ FileRoutesByPath['/stream']['fullPath']
+ >
+}
+declare module './routes/users' {
+ const createFileRoute: CreateFileRoute<
+ '/users',
+ FileRoutesByPath['/users']['parentRoute'],
+ FileRoutesByPath['/users']['id'],
+ FileRoutesByPath['/users']['path'],
+ FileRoutesByPath['/users']['fullPath']
+ >
+}
+declare module './routes/_layout/_layout-2' {
+ const createFileRoute: CreateFileRoute<
+ '/_layout/_layout-2',
+ FileRoutesByPath['/_layout/_layout-2']['parentRoute'],
+ FileRoutesByPath['/_layout/_layout-2']['id'],
+ FileRoutesByPath['/_layout/_layout-2']['path'],
+ FileRoutesByPath['/_layout/_layout-2']['fullPath']
+ >
+}
+declare module './routes/not-found/via-beforeLoad' {
+ const createFileRoute: CreateFileRoute<
+ '/not-found/via-beforeLoad',
+ FileRoutesByPath['/not-found/via-beforeLoad']['parentRoute'],
+ FileRoutesByPath['/not-found/via-beforeLoad']['id'],
+ FileRoutesByPath['/not-found/via-beforeLoad']['path'],
+ FileRoutesByPath['/not-found/via-beforeLoad']['fullPath']
+ >
+}
+declare module './routes/not-found/via-loader' {
+ const createFileRoute: CreateFileRoute<
+ '/not-found/via-loader',
+ FileRoutesByPath['/not-found/via-loader']['parentRoute'],
+ FileRoutesByPath['/not-found/via-loader']['id'],
+ FileRoutesByPath['/not-found/via-loader']['path'],
+ FileRoutesByPath['/not-found/via-loader']['fullPath']
+ >
+}
+declare module './routes/posts.$postId' {
+ const createFileRoute: CreateFileRoute<
+ '/posts/$postId',
+ FileRoutesByPath['/posts/$postId']['parentRoute'],
+ FileRoutesByPath['/posts/$postId']['id'],
+ FileRoutesByPath['/posts/$postId']['path'],
+ FileRoutesByPath['/posts/$postId']['fullPath']
+ >
+}
+declare module './routes/redirect/$target' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target',
+ FileRoutesByPath['/redirect/$target']['parentRoute'],
+ FileRoutesByPath['/redirect/$target']['id'],
+ FileRoutesByPath['/redirect/$target']['path'],
+ FileRoutesByPath['/redirect/$target']['fullPath']
+ >
+}
+declare module './routes/users.$userId' {
+ const createFileRoute: CreateFileRoute<
+ '/users/$userId',
+ FileRoutesByPath['/users/$userId']['parentRoute'],
+ FileRoutesByPath['/users/$userId']['id'],
+ FileRoutesByPath['/users/$userId']['path'],
+ FileRoutesByPath['/users/$userId']['fullPath']
+ >
+}
+declare module './routes/not-found/index' {
+ const createFileRoute: CreateFileRoute<
+ '/not-found/',
+ FileRoutesByPath['/not-found/']['parentRoute'],
+ FileRoutesByPath['/not-found/']['id'],
+ FileRoutesByPath['/not-found/']['path'],
+ FileRoutesByPath['/not-found/']['fullPath']
+ >
+}
+declare module './routes/posts.index' {
+ const createFileRoute: CreateFileRoute<
+ '/posts/',
+ FileRoutesByPath['/posts/']['parentRoute'],
+ FileRoutesByPath['/posts/']['id'],
+ FileRoutesByPath['/posts/']['path'],
+ FileRoutesByPath['/posts/']['fullPath']
+ >
+}
+declare module './routes/redirect/index' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/',
+ FileRoutesByPath['/redirect/']['parentRoute'],
+ FileRoutesByPath['/redirect/']['id'],
+ FileRoutesByPath['/redirect/']['path'],
+ FileRoutesByPath['/redirect/']['fullPath']
+ >
+}
+declare module './routes/users.index' {
+ const createFileRoute: CreateFileRoute<
+ '/users/',
+ FileRoutesByPath['/users/']['parentRoute'],
+ FileRoutesByPath['/users/']['id'],
+ FileRoutesByPath['/users/']['path'],
+ FileRoutesByPath['/users/']['fullPath']
+ >
+}
+declare module './routes/_layout/_layout-2/layout-a' {
+ const createFileRoute: CreateFileRoute<
+ '/_layout/_layout-2/layout-a',
+ FileRoutesByPath['/_layout/_layout-2/layout-a']['parentRoute'],
+ FileRoutesByPath['/_layout/_layout-2/layout-a']['id'],
+ FileRoutesByPath['/_layout/_layout-2/layout-a']['path'],
+ FileRoutesByPath['/_layout/_layout-2/layout-a']['fullPath']
+ >
+}
+declare module './routes/_layout/_layout-2/layout-b' {
+ const createFileRoute: CreateFileRoute<
+ '/_layout/_layout-2/layout-b',
+ FileRoutesByPath['/_layout/_layout-2/layout-b']['parentRoute'],
+ FileRoutesByPath['/_layout/_layout-2/layout-b']['id'],
+ FileRoutesByPath['/_layout/_layout-2/layout-b']['path'],
+ FileRoutesByPath['/_layout/_layout-2/layout-b']['fullPath']
+ >
+}
+declare module './routes/posts_.$postId.deep' {
+ const createFileRoute: CreateFileRoute<
+ '/posts_/$postId/deep',
+ FileRoutesByPath['/posts_/$postId/deep']['parentRoute'],
+ FileRoutesByPath['/posts_/$postId/deep']['id'],
+ FileRoutesByPath['/posts_/$postId/deep']['path'],
+ FileRoutesByPath['/posts_/$postId/deep']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/via-beforeLoad' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/via-beforeLoad',
+ FileRoutesByPath['/redirect/$target/via-beforeLoad']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/via-beforeLoad']['id'],
+ FileRoutesByPath['/redirect/$target/via-beforeLoad']['path'],
+ FileRoutesByPath['/redirect/$target/via-beforeLoad']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/via-loader' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/via-loader',
+ FileRoutesByPath['/redirect/$target/via-loader']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/via-loader']['id'],
+ FileRoutesByPath['/redirect/$target/via-loader']['path'],
+ FileRoutesByPath['/redirect/$target/via-loader']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/index' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/',
+ FileRoutesByPath['/redirect/$target/']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/']['id'],
+ FileRoutesByPath['/redirect/$target/']['path'],
+ FileRoutesByPath['/redirect/$target/']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/serverFn/via-beforeLoad' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/serverFn/via-beforeLoad',
+ FileRoutesByPath['/redirect/$target/serverFn/via-beforeLoad']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-beforeLoad']['id'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-beforeLoad']['path'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-beforeLoad']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/serverFn/via-loader' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/serverFn/via-loader',
+ FileRoutesByPath['/redirect/$target/serverFn/via-loader']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-loader']['id'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-loader']['path'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-loader']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/serverFn/via-useServerFn' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/serverFn/via-useServerFn',
+ FileRoutesByPath['/redirect/$target/serverFn/via-useServerFn']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-useServerFn']['id'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-useServerFn']['path'],
+ FileRoutesByPath['/redirect/$target/serverFn/via-useServerFn']['fullPath']
+ >
+}
+declare module './routes/redirect/$target/serverFn/index' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect/$target/serverFn/',
+ FileRoutesByPath['/redirect/$target/serverFn/']['parentRoute'],
+ FileRoutesByPath['/redirect/$target/serverFn/']['id'],
+ FileRoutesByPath['/redirect/$target/serverFn/']['path'],
+ FileRoutesByPath['/redirect/$target/serverFn/']['fullPath']
+ >
+}
+
// Create and export the route tree
interface NotFoundRouteRouteChildren {
diff --git a/e2e/solid-start/basic/src/routes/__root.tsx b/e2e/solid-start/basic/src/routes/__root.tsx
index 5ef4d4653b..89c60f9113 100644
--- a/e2e/solid-start/basic/src/routes/__root.tsx
+++ b/e2e/solid-start/basic/src/routes/__root.tsx
@@ -1,9 +1,9 @@
import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
+import { TanStackRouterDevtoolsInProd } from '@tanstack/solid-router-devtools'
import { NotFound } from '~/components/NotFound'
import appCss from '~/styles/app.css?url'
import { seo } from '~/utils/seo'
-import { TanStackRouterDevtoolsInProd } from '@tanstack/solid-router-devtools'
export const Route = createRootRoute({
head: () => ({
diff --git a/e2e/solid-start/basic/src/routes/_layout.tsx b/e2e/solid-start/basic/src/routes/_layout.tsx
index d43b4ef5f5..c549175638 100644
--- a/e2e/solid-start/basic/src/routes/_layout.tsx
+++ b/e2e/solid-start/basic/src/routes/_layout.tsx
@@ -1,6 +1,6 @@
-import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_layout')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
diff --git a/e2e/solid-start/basic/src/routes/_layout/_layout-2.tsx b/e2e/solid-start/basic/src/routes/_layout/_layout-2.tsx
index 7a5a3623a0..efeca5ce86 100644
--- a/e2e/solid-start/basic/src/routes/_layout/_layout-2.tsx
+++ b/e2e/solid-start/basic/src/routes/_layout/_layout-2.tsx
@@ -1,6 +1,6 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_layout/_layout-2')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
diff --git a/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-a.tsx
index b69951b246..a190b24202 100644
--- a/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-a.tsx
+++ b/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-a.tsx
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+export const Route = createFileRoute({
component: LayoutAComponent,
})
diff --git a/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-b.tsx
index 30dbcce90f..505f8f6fbf 100644
--- a/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-b.tsx
+++ b/e2e/solid-start/basic/src/routes/_layout/_layout-2/layout-b.tsx
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+export const Route = createFileRoute({
component: LayoutBComponent,
})
diff --git a/e2e/solid-start/basic/src/routes/api.users.ts b/e2e/solid-start/basic/src/routes/api.users.ts
deleted file mode 100644
index 45ac83b2f0..0000000000
--- a/e2e/solid-start/basic/src/routes/api.users.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { json } from '@tanstack/solid-start'
-import { createAPIFileRoute } from '@tanstack/solid-start/api'
-import axios from 'redaxios'
-
-import type { User } from '~/utils/users'
-
-export const APIRoute = createAPIFileRoute('/api/users')({
- GET: async ({ request }) => {
- console.info('Fetching users... @', request.url)
- const res = await axios.get>(
- 'https://jsonplaceholder.typicode.com/users',
- )
-
- const list = res.data.slice(0, 10)
-
- return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email })))
- },
-})
diff --git a/e2e/solid-start/basic/src/routes/api/users.$id.ts b/e2e/solid-start/basic/src/routes/api/users.$id.ts
deleted file mode 100644
index 7ee1fccbb4..0000000000
--- a/e2e/solid-start/basic/src/routes/api/users.$id.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { json } from '@tanstack/solid-start'
-import { createAPIFileRoute } from '@tanstack/solid-start/api'
-import axios from 'redaxios'
-
-import type { User } from '~/utils/users'
-
-export const APIRoute = createAPIFileRoute('/api/users/$id')({
- GET: async ({ request, params }) => {
- console.info(`Fetching users by id=${params.id}... @`, request.url)
- try {
- const res = await axios.get(
- 'https://jsonplaceholder.typicode.com/users/' + params.id,
- )
-
- return json({
- id: res.data.id,
- name: res.data.name,
- email: res.data.email,
- })
- } catch (e) {
- console.error(e)
- return json({ error: 'User not found' }, { status: 404 })
- }
- },
-})
diff --git a/e2e/solid-start/basic/src/routes/api/users.$userId.ts b/e2e/solid-start/basic/src/routes/api/users.$userId.ts
new file mode 100644
index 0000000000..69d966d982
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/api/users.$userId.ts
@@ -0,0 +1,27 @@
+import { json } from '@tanstack/solid-start'
+import type { User } from '~/utils/users'
+
+export const ServerRoute = createServerFileRoute().methods({
+ GET: async ({ params, request }) => {
+ console.info(`Fetching users by id=${params.userId}... @`, request.url)
+ try {
+ const res = await fetch(
+ 'https://jsonplaceholder.typicode.com/users/' + params.userId,
+ )
+ if (!res.ok) {
+ throw new Error('Failed to fetch user')
+ }
+
+ const user = (await res.json()) as User
+
+ return json({
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ })
+ } catch (e) {
+ console.error(e)
+ return json({ error: 'User not found' }, { status: 404 })
+ }
+ },
+})
diff --git a/e2e/solid-start/basic/src/routes/api/users.ts b/e2e/solid-start/basic/src/routes/api/users.ts
new file mode 100644
index 0000000000..c0a6a1a6a2
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/api/users.ts
@@ -0,0 +1,58 @@
+import { createMiddleware, json } from '@tanstack/solid-start'
+import type { User } from '~/utils/users'
+
+const userLoggerMiddleware = createMiddleware({ type: 'request' }).server(
+ async ({ next, request }) => {
+ console.info('In: /users')
+ const result = await next()
+ result.response.headers.set('x-users', 'true')
+ console.info('Out: /users')
+ return result
+ },
+)
+
+const testParentMiddleware = createMiddleware({ type: 'request' }).server(
+ async ({ next, request }) => {
+ console.info('In: testParentMiddleware')
+ const result = await next()
+ result.response.headers.set('x-test-parent', 'true')
+ console.info('Out: testParentMiddleware')
+ return result
+ },
+)
+
+const testMiddleware = createMiddleware({ type: 'request' })
+ .middleware([testParentMiddleware])
+ .server(async ({ next, request }) => {
+ console.info('In: testMiddleware')
+ const result = await next()
+ result.response.headers.set('x-test', 'true')
+
+ // if (Math.random() > 0.5) {
+ // throw new Response(null, {
+ // status: 302,
+ // headers: { Location: 'https://www.google.com' },
+ // })
+ // }
+
+ console.info('Out: testMiddleware')
+ return result
+ })
+
+export const ServerRoute = createServerFileRoute()
+ .middleware([testMiddleware, userLoggerMiddleware, testParentMiddleware])
+ .methods({
+ GET: async ({ request }) => {
+ console.info('Fetching users... @', request.url)
+ const res = await fetch('https://jsonplaceholder.typicode.com/users')
+ if (!res.ok) {
+ throw new Error('Failed to fetch users')
+ }
+
+ const data = (await res.json()) as Array
+
+ const list = data.slice(0, 10)
+
+ return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email })))
+ },
+ })
diff --git a/e2e/solid-start/basic/src/routes/deferred.tsx b/e2e/solid-start/basic/src/routes/deferred.tsx
index 2a53643453..5bb96ca571 100644
--- a/e2e/solid-start/basic/src/routes/deferred.tsx
+++ b/e2e/solid-start/basic/src/routes/deferred.tsx
@@ -1,4 +1,4 @@
-import { Await, createFileRoute } from '@tanstack/solid-router'
+import { Await } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
import { Suspense, createSignal } from 'solid-js'
@@ -15,7 +15,7 @@ const slowServerFn = createServerFn({ method: 'GET' })
return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
})
-export const Route = createFileRoute('/deferred')({
+export const Route = createFileRoute({
loader: async () => {
return {
deferredStuff: new Promise((r) =>
diff --git a/e2e/solid-start/basic/src/routes/index.tsx b/e2e/solid-start/basic/src/routes/index.tsx
index 6b23caf87b..4024800b8e 100644
--- a/e2e/solid-start/basic/src/routes/index.tsx
+++ b/e2e/solid-start/basic/src/routes/index.tsx
@@ -1,7 +1,6 @@
-import { createFileRoute } from '@tanstack/solid-router'
import { CustomMessage } from '~/components/CustomMessage'
-export const Route = createFileRoute('/')({
+export const Route = createFileRoute({
component: Home,
})
diff --git a/e2e/solid-start/basic/src/routes/links.tsx b/e2e/solid-start/basic/src/routes/links.tsx
index d6ce6a449b..05fd8f0494 100644
--- a/e2e/solid-start/basic/src/routes/links.tsx
+++ b/e2e/solid-start/basic/src/routes/links.tsx
@@ -1,6 +1,6 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
-export const Route = createFileRoute('/links')({
+export const Route = createFileRoute({
component: () => {
const navigate = Route.useNavigate()
return (
diff --git a/e2e/solid-start/basic/src/routes/not-found/index.tsx b/e2e/solid-start/basic/src/routes/not-found/index.tsx
index 34c8ef6146..79e7b5ae56 100644
--- a/e2e/solid-start/basic/src/routes/not-found/index.tsx
+++ b/e2e/solid-start/basic/src/routes/not-found/index.tsx
@@ -1,6 +1,6 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
-export const Route = createFileRoute('/not-found/')({
+export const Route = createFileRoute({
component: () => {
const preload = Route.useSearch({ select: (s) => s.preload })
return (
diff --git a/e2e/solid-start/basic/src/routes/not-found/route.tsx b/e2e/solid-start/basic/src/routes/not-found/route.tsx
index 84f5ef81a5..b8343167f5 100644
--- a/e2e/solid-start/basic/src/routes/not-found/route.tsx
+++ b/e2e/solid-start/basic/src/routes/not-found/route.tsx
@@ -1,7 +1,6 @@
-import { createFileRoute } from '@tanstack/solid-router'
import z from 'zod'
-export const Route = createFileRoute('/not-found')({
+export const Route = createFileRoute({
validateSearch: z.object({
preload: z.literal(false).optional(),
}),
diff --git a/e2e/solid-start/basic/src/routes/not-found/via-beforeLoad.tsx b/e2e/solid-start/basic/src/routes/not-found/via-beforeLoad.tsx
index 5badde63bd..5277edd901 100644
--- a/e2e/solid-start/basic/src/routes/not-found/via-beforeLoad.tsx
+++ b/e2e/solid-start/basic/src/routes/not-found/via-beforeLoad.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute, notFound } from '@tanstack/solid-router'
+import { notFound } from '@tanstack/solid-router'
-export const Route = createFileRoute('/not-found/via-beforeLoad')({
+export const Route = createFileRoute({
beforeLoad: () => {
throw notFound()
},
diff --git a/e2e/solid-start/basic/src/routes/not-found/via-loader.tsx b/e2e/solid-start/basic/src/routes/not-found/via-loader.tsx
index 20956cc43d..bc0fad6c58 100644
--- a/e2e/solid-start/basic/src/routes/not-found/via-loader.tsx
+++ b/e2e/solid-start/basic/src/routes/not-found/via-loader.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute, notFound } from '@tanstack/solid-router'
+import { notFound } from '@tanstack/solid-router'
-export const Route = createFileRoute('/not-found/via-loader')({
+export const Route = createFileRoute({
loader: () => {
throw notFound()
},
diff --git a/e2e/solid-start/basic/src/routes/posts.$postId.tsx b/e2e/solid-start/basic/src/routes/posts.$postId.tsx
index c6b2fcf5f9..cefc75cdf1 100644
--- a/e2e/solid-start/basic/src/routes/posts.$postId.tsx
+++ b/e2e/solid-start/basic/src/routes/posts.$postId.tsx
@@ -1,11 +1,11 @@
-import { ErrorComponent, Link, createFileRoute } from '@tanstack/solid-router'
+import { ErrorComponent, Link } from '@tanstack/solid-router'
import type { ErrorComponentProps } from '@tanstack/solid-router'
import { fetchPost } from '~/utils/posts'
import { NotFound } from '~/components/NotFound'
import { PostErrorComponent } from '~/components/PostErrorComponent'
-export const Route = createFileRoute('/posts/$postId')({
+export const Route = createFileRoute({
loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
errorComponent: PostErrorComponent,
component: PostComponent,
diff --git a/e2e/solid-start/basic/src/routes/posts.index.tsx b/e2e/solid-start/basic/src/routes/posts.index.tsx
index c7d8cfe19c..07e41d1b0b 100644
--- a/e2e/solid-start/basic/src/routes/posts.index.tsx
+++ b/e2e/solid-start/basic/src/routes/posts.index.tsx
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/posts/')({
+export const Route = createFileRoute({
component: PostsIndexComponent,
})
diff --git a/e2e/solid-start/basic/src/routes/posts.tsx b/e2e/solid-start/basic/src/routes/posts.tsx
index 0e94cd4d2c..c053d0de56 100644
--- a/e2e/solid-start/basic/src/routes/posts.tsx
+++ b/e2e/solid-start/basic/src/routes/posts.tsx
@@ -1,9 +1,9 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
import { For } from 'solid-js'
import { fetchPosts } from '~/utils/posts'
-export const Route = createFileRoute('/posts')({
+export const Route = createFileRoute({
head: () => ({
meta: [
{
diff --git a/e2e/solid-start/basic/src/routes/posts_.$postId.deep.tsx b/e2e/solid-start/basic/src/routes/posts_.$postId.deep.tsx
index f8d4627914..4133a9ade9 100644
--- a/e2e/solid-start/basic/src/routes/posts_.$postId.deep.tsx
+++ b/e2e/solid-start/basic/src/routes/posts_.$postId.deep.tsx
@@ -1,9 +1,9 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
import { PostErrorComponent } from '~/components/PostErrorComponent'
import { fetchPost } from '~/utils/posts'
-export const Route = createFileRoute('/posts_/$postId/deep')({
+export const Route = createFileRoute({
loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
errorComponent: PostErrorComponent,
component: PostDeepComponent,
diff --git a/e2e/solid-start/basic/src/routes/redirect/$target.tsx b/e2e/solid-start/basic/src/routes/redirect/$target.tsx
index 525dd9da25..c4c78070d3 100644
--- a/e2e/solid-start/basic/src/routes/redirect/$target.tsx
+++ b/e2e/solid-start/basic/src/routes/redirect/$target.tsx
@@ -1,7 +1,7 @@
-import { createFileRoute, retainSearchParams } from '@tanstack/solid-router'
+import { retainSearchParams } from '@tanstack/solid-router'
import z from 'zod'
-export const Route = createFileRoute('/redirect/$target')({
+export const Route = createFileRoute({
params: {
parse: (p) =>
z
diff --git a/e2e/solid-start/basic/src/routes/redirect/$target/index.tsx b/e2e/solid-start/basic/src/routes/redirect/$target/index.tsx
index 916afd450b..0dde05d9e1 100644
--- a/e2e/solid-start/basic/src/routes/redirect/$target/index.tsx
+++ b/e2e/solid-start/basic/src/routes/redirect/$target/index.tsx
@@ -1,6 +1,6 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
-export const Route = createFileRoute('/redirect/$target/')({
+export const Route = createFileRoute({
component: () => {
const preload = Route.useSearch({ select: (s) => s.preload })
return (
diff --git a/e2e/solid-start/basic/src/routes/redirect/$target/serverFn/index.tsx b/e2e/solid-start/basic/src/routes/redirect/$target/serverFn/index.tsx
index d404672372..6be32366eb 100644
--- a/e2e/solid-start/basic/src/routes/redirect/$target/serverFn/index.tsx
+++ b/e2e/solid-start/basic/src/routes/redirect/$target/serverFn/index.tsx
@@ -1,6 +1,6 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
-export const Route = createFileRoute('/redirect/$target/serverFn/')({
+export const Route = createFileRoute({
component: () => (
+ )
+}
diff --git a/examples/solid/start-basic-static/src/routes/index.tsx b/examples/solid/start-basic-static/src/routes/index.tsx
new file mode 100644
index 0000000000..2f35891abb
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/index.tsx
@@ -0,0 +1,11 @@
+export const Route = createFileRoute({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!!!
+
+ )
+}
diff --git a/examples/solid/start-basic-static/src/routes/posts.$postId.tsx b/examples/solid/start-basic-static/src/routes/posts.$postId.tsx
new file mode 100644
index 0000000000..b33d1ae326
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/posts.$postId.tsx
@@ -0,0 +1,38 @@
+import { ErrorComponent, Link } from '@tanstack/solid-router'
+import { fetchPost } from '../utils/posts'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+import { NotFound } from '~/components/NotFound'
+
+export const Route = createFileRoute({
+ loader: ({ params: { postId } }) => fetchPost({ data: postId }),
+ errorComponent: PostErrorComponent,
+ component: PostComponent,
+ notFoundComponent: () => {
+ return Post not found
+ },
+})
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post.title}
+
{post.body}
+
+ Deep View
+
+
+ )
+}
diff --git a/examples/solid/start-basic-static/src/routes/posts.index.tsx b/examples/solid/start-basic-static/src/routes/posts.index.tsx
new file mode 100644
index 0000000000..13529228bb
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/posts.index.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/examples/solid/start-basic-static/src/routes/posts.tsx b/examples/solid/start-basic-static/src/routes/posts.tsx
new file mode 100644
index 0000000000..4f0b47fa08
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/posts.tsx
@@ -0,0 +1,38 @@
+import { Link, Outlet } from '@tanstack/solid-router'
+import { fetchPosts } from '../utils/posts'
+
+export const Route = createFileRoute({
+ loader: async () => fetchPosts(),
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/examples/solid/start-basic-static/src/routes/posts_.$postId.deep.tsx b/examples/solid/start-basic-static/src/routes/posts_.$postId.deep.tsx
new file mode 100644
index 0000000000..b36e86eb53
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/posts_.$postId.deep.tsx
@@ -0,0 +1,26 @@
+import { Link } from '@tanstack/solid-router'
+import { fetchPost } from '../utils/posts'
+import { PostErrorComponent } from './posts.$postId'
+
+export const Route = createFileRoute({
+ loader: ({ params: { postId } }) =>
+ fetchPost({
+ data: postId,
+ }),
+ errorComponent: PostErrorComponent,
+ component: PostDeepComponent,
+})
+
+function PostDeepComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ ← All Posts
+
+
{post().title}
+
{post().body}
+
+ )
+}
diff --git a/examples/solid/start-basic-static/src/routes/redirect.tsx b/examples/solid/start-basic-static/src/routes/redirect.tsx
new file mode 100644
index 0000000000..d80d290638
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/redirect.tsx
@@ -0,0 +1,9 @@
+import { redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute({
+ beforeLoad: async () => {
+ throw redirect({
+ to: '/posts',
+ })
+ },
+})
diff --git a/examples/solid/start-basic-static/src/routes/users.$userId.tsx b/examples/solid/start-basic-static/src/routes/users.$userId.tsx
new file mode 100644
index 0000000000..414436f8d5
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/users.$userId.tsx
@@ -0,0 +1,49 @@
+import { ErrorComponent } from '@tanstack/solid-router'
+import axios from 'redaxios'
+import { createServerFn } from '@tanstack/solid-start'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+import type { User } from '~/utils/users'
+import { NotFound } from '~/components/NotFound'
+
+const fetchUser = createServerFn({ method: 'GET', type: 'static' })
+ .validator((d: string) => d)
+ .handler(async ({ data: userId }) => {
+ return axios
+ .get('https://jsonplaceholder.typicode.com/users/' + userId)
+ .then((d) => ({
+ id: d.data.id,
+ name: d.data.name,
+ email: d.data.email,
+ }))
+ .catch((e) => {
+ throw new Error('Failed to fetch user')
+ })
+ })
+
+export const Route = createFileRoute({
+ loader: ({ params: { userId } }) => fetchUser({ data: userId }),
+ errorComponent: UserErrorComponent,
+ component: UserComponent,
+ notFoundComponent: () => {
+ return User not found
+ },
+})
+
+export function UserErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function UserComponent() {
+ const user = Route.useLoaderData()
+
+ if ('error' in user()) {
+ return User not found
+ }
+
+ return (
+
+
{user().name}
+
{user().email}
+
+ )
+}
diff --git a/examples/solid/start-basic-static/src/routes/users.index.tsx b/examples/solid/start-basic-static/src/routes/users.index.tsx
new file mode 100644
index 0000000000..662e8b6c68
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/users.index.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return Select a user.
+}
diff --git a/examples/solid/start-basic-static/src/routes/users.tsx b/examples/solid/start-basic-static/src/routes/users.tsx
new file mode 100644
index 0000000000..440d3c93a5
--- /dev/null
+++ b/examples/solid/start-basic-static/src/routes/users.tsx
@@ -0,0 +1,54 @@
+import { Link, Outlet } from '@tanstack/solid-router'
+import axios from 'redaxios'
+import { createServerFn } from '@tanstack/solid-start'
+import type { User } from '../utils/users'
+
+const fetchUsers = createServerFn({ method: 'GET', type: 'static' }).handler(
+ async () => {
+ console.info('Fetching users...')
+ const res = await axios.get>(
+ 'https://jsonplaceholder.typicode.com/users',
+ )
+
+ return res.data
+ .slice(0, 10)
+ .map((u) => ({ id: u.id, name: u.name, email: u.email }))
+ },
+)
+
+export const Route = createFileRoute({
+ loader: async () => fetchUsers(),
+ component: UsersComponent,
+})
+
+function UsersComponent() {
+ const users = Route.useLoaderData()
+
+ return (
+
+
+ {[
+ ...users(),
+ { id: 'i-do-not-exist', name: 'Non-existent User', email: '' },
+ ].map((user) => {
+ return (
+ -
+
+
{user.name}
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/examples/solid/start-basic-static/src/styles/app.css b/examples/solid/start-basic-static/src/styles/app.css
new file mode 100644
index 0000000000..d6426ccb72
--- /dev/null
+++ b/examples/solid/start-basic-static/src/styles/app.css
@@ -0,0 +1,14 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html,
+ body {
+ @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200;
+ }
+
+ .using-mouse * {
+ outline: none !important;
+ }
+}
diff --git a/examples/solid/start-basic-static/src/tanstack-start.d.ts b/examples/solid/start-basic-static/src/tanstack-start.d.ts
new file mode 100644
index 0000000000..3adaf59556
--- /dev/null
+++ b/examples/solid/start-basic-static/src/tanstack-start.d.ts
@@ -0,0 +1,2 @@
+///
+import '../.tanstack-start/server-routes/routeTree.gen'
diff --git a/examples/solid/start-basic-static/src/utils/loggingMiddleware.tsx b/examples/solid/start-basic-static/src/utils/loggingMiddleware.tsx
new file mode 100644
index 0000000000..c2362c834c
--- /dev/null
+++ b/examples/solid/start-basic-static/src/utils/loggingMiddleware.tsx
@@ -0,0 +1,42 @@
+import { createMiddleware } from '@tanstack/solid-start'
+
+export const logMiddleware = createMiddleware({ type: 'function' })
+ .middleware([
+ createMiddleware({ type: 'function' })
+ .client(async (ctx) => {
+ const clientTime = new Date()
+
+ return await ctx.next({
+ context: {
+ clientTime,
+ },
+ sendContext: {
+ clientTime,
+ },
+ })
+ })
+ .server(async (ctx) => {
+ const serverTime = new Date()
+
+ return await ctx.next({
+ sendContext: {
+ serverTime,
+ durationToServer:
+ serverTime.getTime() - ctx.context.clientTime.getTime(),
+ },
+ })
+ }),
+ ])
+ .client(async (options) => {
+ const result = await options.next()
+
+ const now = new Date()
+
+ console.log('Client Req/Res:', {
+ duration: result.context.clientTime.getTime() - now.getTime(),
+ durationToServer: result.context.durationToServer,
+ durationFromServer: now.getTime() - result.context.serverTime.getTime(),
+ })
+
+ return result
+ })
diff --git a/examples/solid/start-basic-static/src/utils/posts.tsx b/examples/solid/start-basic-static/src/utils/posts.tsx
new file mode 100644
index 0000000000..204230b7fe
--- /dev/null
+++ b/examples/solid/start-basic-static/src/utils/posts.tsx
@@ -0,0 +1,37 @@
+import { createServerFn } from '@tanstack/solid-start'
+import axios from 'redaxios'
+import { notFound } from '@tanstack/solid-router'
+import { logMiddleware } from './loggingMiddleware'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPost = createServerFn({ method: 'GET', type: 'static' })
+ .middleware([logMiddleware])
+ .validator((d: string) => d)
+ .handler(async ({ data }) => {
+ console.info(`Fetching post with id ${data}...`)
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${data}`)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+ .then((r) => r.data)
+
+ return post
+ })
+
+export const fetchPosts = createServerFn({ method: 'GET', type: 'static' })
+ .middleware([logMiddleware])
+ .handler(async () => {
+ console.info('Fetching posts...')
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+ })
diff --git a/examples/solid/start-basic-static/src/utils/seo.ts b/examples/solid/start-basic-static/src/utils/seo.ts
new file mode 100644
index 0000000000..d18ad84b74
--- /dev/null
+++ b/examples/solid/start-basic-static/src/utils/seo.ts
@@ -0,0 +1,33 @@
+export const seo = ({
+ title,
+ description,
+ keywords,
+ image,
+}: {
+ title: string
+ description?: string
+ image?: string
+ keywords?: string
+}) => {
+ const tags = [
+ { title },
+ { name: 'description', content: description },
+ { name: 'keywords', content: keywords },
+ { name: 'twitter:title', content: title },
+ { name: 'twitter:description', content: description },
+ { name: 'twitter:creator', content: '@tannerlinsley' },
+ { name: 'twitter:site', content: '@tannerlinsley' },
+ { name: 'og:type', content: 'website' },
+ { name: 'og:title', content: title },
+ { name: 'og:description', content: description },
+ ...(image
+ ? [
+ { name: 'twitter:image', content: image },
+ { name: 'twitter:card', content: 'summary_large_image' },
+ { name: 'og:image', content: image },
+ ]
+ : []),
+ ]
+
+ return tags
+}
diff --git a/examples/solid/start-basic-static/src/utils/users.tsx b/examples/solid/start-basic-static/src/utils/users.tsx
new file mode 100644
index 0000000000..b810f455fe
--- /dev/null
+++ b/examples/solid/start-basic-static/src/utils/users.tsx
@@ -0,0 +1,7 @@
+export type User = {
+ id: number
+ name: string
+ email: string
+}
+
+export const DEPLOY_URL = 'http://localhost:3000'
diff --git a/examples/solid/start-basic-static/tailwind.config.cjs b/examples/solid/start-basic-static/tailwind.config.cjs
new file mode 100644
index 0000000000..10c9224f8c
--- /dev/null
+++ b/examples/solid/start-basic-static/tailwind.config.cjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
+}
diff --git a/examples/solid/start-basic-static/tsconfig.json b/examples/solid/start-basic-static/tsconfig.json
new file mode 100644
index 0000000000..a40235b863
--- /dev/null
+++ b/examples/solid/start-basic-static/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/examples/solid/start-basic-static/vite.config.ts b/examples/solid/start-basic-static/vite.config.ts
new file mode 100644
index 0000000000..213bd9189e
--- /dev/null
+++ b/examples/solid/start-basic-static/vite.config.ts
@@ -0,0 +1,19 @@
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ spa: {
+ enabled: true,
+ },
+ }),
+ ],
+})
diff --git a/examples/solid/start-basic/.tanstack-start/server-routes/routeTree.gen.ts b/examples/solid/start-basic/.tanstack-start/server-routes/routeTree.gen.ts
new file mode 100644
index 0000000000..e36fccb479
--- /dev/null
+++ b/examples/solid/start-basic/.tanstack-start/server-routes/routeTree.gen.ts
@@ -0,0 +1,155 @@
+/* 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 type {
+ FileRoutesByPath,
+ CreateServerFileRoute,
+} from '@tanstack/solid-start/server'
+import {
+ createServerRoute,
+ createServerFileRoute,
+} from '@tanstack/solid-start/server'
+
+import { ServerRoute as ApiUsersRouteImport } from './../../src/routes/api/users'
+import { ServerRoute as ApiUsersUserIdRouteImport } from './../../src/routes/api/users.$userId'
+
+// Create/Update Routes
+
+const rootRoute = createServerRoute()
+
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-start/server' {
+ interface FileRoutesByPath {
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRoute
+ }
+ '/api/users/$userId': {
+ id: '/api/users/$userId'
+ path: '/$userId'
+ fullPath: '/api/users/$userId'
+ preLoaderRoute: typeof ApiUsersUserIdRouteImport
+ parentRoute: typeof ApiUsersRouteImport
+ }
+ }
+}
+
+// Add type-safety to the createFileRoute function across the route tree
+
+declare module './../../src/routes/api/users' {
+ const createServerFileRoute: CreateServerFileRoute<
+ FileRoutesByPath['/api/users']['parentRoute'],
+ FileRoutesByPath['/api/users']['id'],
+ FileRoutesByPath['/api/users']['path'],
+ FileRoutesByPath['/api/users']['fullPath'],
+ ApiUsersRouteChildren
+ >
+}
+declare module './../../src/routes/api/users.$userId' {
+ const createServerFileRoute: CreateServerFileRoute<
+ FileRoutesByPath['/api/users/$userId']['parentRoute'],
+ FileRoutesByPath['/api/users/$userId']['id'],
+ FileRoutesByPath['/api/users/$userId']['path'],
+ FileRoutesByPath['/api/users/$userId']['fullPath'],
+ unknown
+ >
+}
+
+// Create and export the route tree
+
+interface ApiUsersRouteChildren {
+ ApiUsersUserIdRoute: typeof ApiUsersUserIdRoute
+}
+
+const ApiUsersRouteChildren: ApiUsersRouteChildren = {
+ ApiUsersUserIdRoute: ApiUsersUserIdRoute,
+}
+
+const ApiUsersRouteWithChildren = ApiUsersRoute._addFileChildren(
+ ApiUsersRouteChildren,
+)
+
+export interface FileRoutesByFullPath {
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/api/users/$userId': typeof ApiUsersUserIdRoute
+}
+
+export interface FileRoutesByTo {
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/api/users/$userId': typeof ApiUsersUserIdRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/api/users/$userId': typeof ApiUsersUserIdRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/api/users' | '/api/users/$userId'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/api/users' | '/api/users/$userId'
+ id: '__root__' | '/api/users' | '/api/users/$userId'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ ApiUsersRoute: typeof ApiUsersRouteWithChildren
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ ApiUsersRoute: ApiUsersRouteWithChildren,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/api/users"
+ ]
+ },
+ "/api/users": {
+ "filePath": "api/users.ts",
+ "children": [
+ "/api/users/$userId"
+ ]
+ },
+ "/api/users/$userId": {
+ "filePath": "api/users.$userId.ts",
+ "parent": "/api/users"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/examples/solid/start-basic/app.config.ts b/examples/solid/start-basic/app.config.ts
deleted file mode 100644
index 2a06e3d3f0..0000000000
--- a/examples/solid/start-basic/app.config.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineConfig } from '@tanstack/solid-start/config'
-import tsConfigPaths from 'vite-tsconfig-paths'
-
-export default defineConfig({
- tsr: {
- appDirectory: 'src',
- },
- vite: {
- plugins: [
- tsConfigPaths({
- projects: ['./tsconfig.json'],
- }),
- ],
- },
-})
diff --git a/examples/solid/start-basic/package.json b/examples/solid/start-basic/package.json
index 9503c2499a..2580875d57 100644
--- a/examples/solid/start-basic/package.json
+++ b/examples/solid/start-basic/package.json
@@ -4,19 +4,18 @@
"sideEffects": false,
"type": "module",
"scripts": {
- "dev": "vinxi dev",
- "build": "vinxi build",
- "start": "vinxi start"
+ "dev": "vite dev",
+ "build": "vite build",
+ "start": "vite start"
},
"dependencies": {
- "@tanstack/solid-router": "^1.120.3",
- "@tanstack/solid-router-devtools": "^1.120.3",
- "@tanstack/solid-start": "^1.120.3",
+ "@tanstack/solid-router": "^1.121.0-alpha.11",
+ "@tanstack/solid-router-devtools": "^1.121.0-alpha.11",
+ "@tanstack/solid-start": "^1.121.0-alpha.11",
"solid-js": "^1.9.5",
"redaxios": "^0.5.1",
"tailwind-merge": "^2.6.0",
- "vite": "6.1.4",
- "vinxi": "0.5.3"
+ "vite": "^6.3.5"
},
"devDependencies": {
"@types/node": "^22.5.4",
diff --git a/examples/solid/start-basic/src/api.ts b/examples/solid/start-basic/src/api.ts
deleted file mode 100644
index ed511bcd26..0000000000
--- a/examples/solid/start-basic/src/api.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- createStartAPIHandler,
- defaultAPIFileRouteHandler,
-} from '@tanstack/solid-start/api'
-
-export default createStartAPIHandler(defaultAPIFileRouteHandler)
diff --git a/examples/solid/start-basic/src/client.tsx b/examples/solid/start-basic/src/client.tsx
deleted file mode 100644
index ba0f02fac0..0000000000
--- a/examples/solid/start-basic/src/client.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-///
-import { hydrate } from 'solid-js/web'
-import { StartClient } from '@tanstack/solid-start'
-import { createRouter } from './router'
-
-const router = createRouter()
-
-hydrate(() => , document.body)
diff --git a/examples/solid/start-basic/src/components/DefaultCatchBoundary.tsx b/examples/solid/start-basic/src/components/DefaultCatchBoundary.tsx
index 0dff3919fd..bf759681a4 100644
--- a/examples/solid/start-basic/src/components/DefaultCatchBoundary.tsx
+++ b/examples/solid/start-basic/src/components/DefaultCatchBoundary.tsx
@@ -28,7 +28,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
>
Try Again
- {isRoot() ? (
+ {isRoot ? (
rootRoute,
} as any)
-const RedirectRoute = RedirectImport.update({
+const RedirectRoute = RedirectRouteImport.update({
id: '/redirect',
path: '/redirect',
getParentRoute: () => rootRoute,
} as any)
-const PostsRoute = PostsImport.update({
+const PostsRoute = PostsRouteImport.update({
id: '/posts',
path: '/posts',
getParentRoute: () => rootRoute,
} as any)
-const DeferredRoute = DeferredImport.update({
+const DeferredRoute = DeferredRouteImport.update({
id: '/deferred',
path: '/deferred',
getParentRoute: () => rootRoute,
} as any)
-const PathlessLayoutRoute = PathlessLayoutImport.update({
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
id: '/_pathlessLayout',
getParentRoute: () => rootRoute,
} as any)
-const IndexRoute = IndexImport.update({
+const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
-const UsersIndexRoute = UsersIndexImport.update({
+const UsersIndexRoute = UsersIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => UsersRoute,
} as any)
-const PostsIndexRoute = PostsIndexImport.update({
+const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const UsersUserIdRoute = UsersUserIdImport.update({
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
-const PostsPostIdRoute = PostsPostIdImport.update({
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const PathlessLayoutNestedLayoutRoute = PathlessLayoutNestedLayoutImport.update(
- {
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
id: '/_nested-layout',
getParentRoute: () => PathlessLayoutRoute,
- } as any,
-)
+ } as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
id: '/posts_/$postId/deep',
path: '/posts/$postId/deep',
getParentRoute: () => rootRoute,
} as any)
const PathlessLayoutNestedLayoutRouteBRoute =
- PathlessLayoutNestedLayoutRouteBImport.update({
+ PathlessLayoutNestedLayoutRouteBRouteImport.update({
id: '/route-b',
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
const PathlessLayoutNestedLayoutRouteARoute =
- PathlessLayoutNestedLayoutRouteAImport.update({
+ PathlessLayoutNestedLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
@@ -122,103 +123,232 @@ declare module '@tanstack/solid-router' {
id: '/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof IndexImport
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRoute
}
'/_pathlessLayout': {
id: '/_pathlessLayout'
path: ''
fullPath: ''
- preLoaderRoute: typeof PathlessLayoutImport
+ preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRoute
}
'/deferred': {
id: '/deferred'
path: '/deferred'
fullPath: '/deferred'
- preLoaderRoute: typeof DeferredImport
+ preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRoute
}
'/posts': {
id: '/posts'
path: '/posts'
fullPath: '/posts'
- preLoaderRoute: typeof PostsImport
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRoute
}
'/redirect': {
id: '/redirect'
path: '/redirect'
fullPath: '/redirect'
- preLoaderRoute: typeof RedirectImport
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRoute
}
'/users': {
id: '/users'
path: '/users'
fullPath: '/users'
- preLoaderRoute: typeof UsersImport
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRoute
}
'/_pathlessLayout/_nested-layout': {
id: '/_pathlessLayout/_nested-layout'
path: ''
fullPath: ''
- preLoaderRoute: typeof PathlessLayoutNestedLayoutImport
- parentRoute: typeof PathlessLayoutImport
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRouteImport
}
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
fullPath: '/posts/$postId'
- preLoaderRoute: typeof PostsPostIdImport
- parentRoute: typeof PostsImport
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRouteImport
}
'/users/$userId': {
id: '/users/$userId'
path: '/$userId'
fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdImport
- parentRoute: typeof UsersImport
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRouteImport
}
'/posts/': {
id: '/posts/'
path: '/'
fullPath: '/posts/'
- preLoaderRoute: typeof PostsIndexImport
- parentRoute: typeof PostsImport
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRouteImport
}
'/users/': {
id: '/users/'
path: '/'
fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexImport
- parentRoute: typeof UsersImport
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRouteImport
}
'/_pathlessLayout/_nested-layout/route-a': {
id: '/_pathlessLayout/_nested-layout/route-a'
path: '/route-a'
fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteAImport
- parentRoute: typeof PathlessLayoutNestedLayoutImport
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRouteImport
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
path: '/route-b'
fullPath: '/route-b'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBImport
- parentRoute: typeof PathlessLayoutNestedLayoutImport
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRouteImport
}
'/posts_/$postId/deep': {
id: '/posts_/$postId/deep'
path: '/posts/$postId/deep'
fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepImport
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
parentRoute: typeof rootRoute
}
}
}
+// Add type-safety to the createFileRoute function across the route tree
+
+declare module './routes/index' {
+ const createFileRoute: CreateFileRoute<
+ '/',
+ FileRoutesByPath['/']['parentRoute'],
+ FileRoutesByPath['/']['id'],
+ FileRoutesByPath['/']['path'],
+ FileRoutesByPath['/']['fullPath']
+ >
+}
+declare module './routes/_pathlessLayout' {
+ const createFileRoute: CreateFileRoute<
+ '/_pathlessLayout',
+ FileRoutesByPath['/_pathlessLayout']['parentRoute'],
+ FileRoutesByPath['/_pathlessLayout']['id'],
+ FileRoutesByPath['/_pathlessLayout']['path'],
+ FileRoutesByPath['/_pathlessLayout']['fullPath']
+ >
+}
+declare module './routes/deferred' {
+ const createFileRoute: CreateFileRoute<
+ '/deferred',
+ FileRoutesByPath['/deferred']['parentRoute'],
+ FileRoutesByPath['/deferred']['id'],
+ FileRoutesByPath['/deferred']['path'],
+ FileRoutesByPath['/deferred']['fullPath']
+ >
+}
+declare module './routes/posts' {
+ const createFileRoute: CreateFileRoute<
+ '/posts',
+ FileRoutesByPath['/posts']['parentRoute'],
+ FileRoutesByPath['/posts']['id'],
+ FileRoutesByPath['/posts']['path'],
+ FileRoutesByPath['/posts']['fullPath']
+ >
+}
+declare module './routes/redirect' {
+ const createFileRoute: CreateFileRoute<
+ '/redirect',
+ FileRoutesByPath['/redirect']['parentRoute'],
+ FileRoutesByPath['/redirect']['id'],
+ FileRoutesByPath['/redirect']['path'],
+ FileRoutesByPath['/redirect']['fullPath']
+ >
+}
+declare module './routes/users' {
+ const createFileRoute: CreateFileRoute<
+ '/users',
+ FileRoutesByPath['/users']['parentRoute'],
+ FileRoutesByPath['/users']['id'],
+ FileRoutesByPath['/users']['path'],
+ FileRoutesByPath['/users']['fullPath']
+ >
+}
+declare module './routes/_pathlessLayout/_nested-layout' {
+ const createFileRoute: CreateFileRoute<
+ '/_pathlessLayout/_nested-layout',
+ FileRoutesByPath['/_pathlessLayout/_nested-layout']['parentRoute'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout']['id'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout']['path'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout']['fullPath']
+ >
+}
+declare module './routes/posts.$postId' {
+ const createFileRoute: CreateFileRoute<
+ '/posts/$postId',
+ FileRoutesByPath['/posts/$postId']['parentRoute'],
+ FileRoutesByPath['/posts/$postId']['id'],
+ FileRoutesByPath['/posts/$postId']['path'],
+ FileRoutesByPath['/posts/$postId']['fullPath']
+ >
+}
+declare module './routes/users.$userId' {
+ const createFileRoute: CreateFileRoute<
+ '/users/$userId',
+ FileRoutesByPath['/users/$userId']['parentRoute'],
+ FileRoutesByPath['/users/$userId']['id'],
+ FileRoutesByPath['/users/$userId']['path'],
+ FileRoutesByPath['/users/$userId']['fullPath']
+ >
+}
+declare module './routes/posts.index' {
+ const createFileRoute: CreateFileRoute<
+ '/posts/',
+ FileRoutesByPath['/posts/']['parentRoute'],
+ FileRoutesByPath['/posts/']['id'],
+ FileRoutesByPath['/posts/']['path'],
+ FileRoutesByPath['/posts/']['fullPath']
+ >
+}
+declare module './routes/users.index' {
+ const createFileRoute: CreateFileRoute<
+ '/users/',
+ FileRoutesByPath['/users/']['parentRoute'],
+ FileRoutesByPath['/users/']['id'],
+ FileRoutesByPath['/users/']['path'],
+ FileRoutesByPath['/users/']['fullPath']
+ >
+}
+declare module './routes/_pathlessLayout/_nested-layout/route-a' {
+ const createFileRoute: CreateFileRoute<
+ '/_pathlessLayout/_nested-layout/route-a',
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-a']['parentRoute'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-a']['id'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-a']['path'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-a']['fullPath']
+ >
+}
+declare module './routes/_pathlessLayout/_nested-layout/route-b' {
+ const createFileRoute: CreateFileRoute<
+ '/_pathlessLayout/_nested-layout/route-b',
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-b']['parentRoute'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-b']['id'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-b']['path'],
+ FileRoutesByPath['/_pathlessLayout/_nested-layout/route-b']['fullPath']
+ >
+}
+declare module './routes/posts_.$postId.deep' {
+ const createFileRoute: CreateFileRoute<
+ '/posts_/$postId/deep',
+ FileRoutesByPath['/posts_/$postId/deep']['parentRoute'],
+ FileRoutesByPath['/posts_/$postId/deep']['id'],
+ FileRoutesByPath['/posts_/$postId/deep']['path'],
+ FileRoutesByPath['/posts_/$postId/deep']['fullPath']
+ >
+}
+
// Create and export the route tree
interface PathlessLayoutNestedLayoutRouteChildren {
diff --git a/examples/solid/start-basic/src/routes/__root.tsx b/examples/solid/start-basic/src/routes/__root.tsx
index a9380d3209..374a4fb944 100644
--- a/examples/solid/start-basic/src/routes/__root.tsx
+++ b/examples/solid/start-basic/src/routes/__root.tsx
@@ -1,6 +1,13 @@
-import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
-
-import { TanStackRouterDevtoolsInProd } from '@tanstack/solid-router-devtools'
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
+import type * as Solid from 'solid-js'
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
import { NotFound } from '~/components/NotFound'
import appCss from '~/styles/app.css?url'
import { seo } from '~/utils/seo'
@@ -8,6 +15,9 @@ import { seo } from '~/utils/seo'
export const Route = createRootRoute({
head: () => ({
meta: [
+ {
+ charset: 'utf-8',
+ },
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
@@ -41,14 +51,29 @@ export const Route = createRootRoute({
{ rel: 'icon', href: '/favicon.ico' },
],
}),
- errorComponent: (props) => {props.error.stack}
,
+ errorComponent: (props) => {
+ return (
+
+
+
+ )
+ },
notFoundComponent: () => ,
component: RootComponent,
})
function RootComponent() {
+ return (
+
+
+
+ )
+}
+
+function RootDocument({ children }: { children: Solid.JSX.Element }) {
return (
<>
+
{' '}
- Deferred
+ Pathless Layout
{' '}
- redirect
+ Deferred
{' '}
-
-
+
+ {children}
+
+
>
)
}
diff --git a/examples/solid/start-basic/src/routes/_pathlessLayout.tsx b/examples/solid/start-basic/src/routes/_pathlessLayout.tsx
index af197bc038..c549175638 100644
--- a/examples/solid/start-basic/src/routes/_pathlessLayout.tsx
+++ b/examples/solid/start-basic/src/routes/_pathlessLayout.tsx
@@ -1,6 +1,6 @@
-import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_pathlessLayout')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
diff --git a/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout.tsx b/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout.tsx
index 24e4b2545b..03bcac9d47 100644
--- a/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout.tsx
+++ b/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout.tsx
@@ -1,6 +1,6 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
-export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({
+export const Route = createFileRoute({
component: LayoutComponent,
})
diff --git a/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-a.tsx b/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-a.tsx
index a22902a271..a0bd5240b7 100644
--- a/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-a.tsx
+++ b/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-a.tsx
@@ -1,10 +1,6 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')(
- {
- component: LayoutAComponent,
- },
-)
+export const Route = createFileRoute({
+ component: LayoutAComponent,
+})
function LayoutAComponent() {
return I'm A!
diff --git a/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-b.tsx b/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-b.tsx
index 36231d2153..2864ec1f28 100644
--- a/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-b.tsx
+++ b/examples/solid/start-basic/src/routes/_pathlessLayout/_nested-layout/route-b.tsx
@@ -1,10 +1,6 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')(
- {
- component: LayoutBComponent,
- },
-)
+export const Route = createFileRoute({
+ component: LayoutBComponent,
+})
function LayoutBComponent() {
return I'm B!
diff --git a/examples/solid/start-basic/src/routes/api/users.$id.ts b/examples/solid/start-basic/src/routes/api/users.$id.ts
deleted file mode 100644
index b1786f6a30..0000000000
--- a/examples/solid/start-basic/src/routes/api/users.$id.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { json } from '@tanstack/solid-start'
-import { createAPIFileRoute } from '@tanstack/solid-start/api'
-import axios from 'redaxios'
-import type { User } from '../../utils/users'
-
-export const APIRoute = createAPIFileRoute('/api/users/$id')({
- GET: async ({ request, params }) => {
- console.info(`Fetching users by id=${params.id}... @`, request.url)
- try {
- const res = await axios.get(
- 'https://jsonplaceholder.typicode.com/users/' + params.id,
- )
-
- return json({
- id: res.data.id,
- name: res.data.name,
- email: res.data.email,
- })
- } catch (e) {
- console.error(e)
- return json({ error: 'User not found' }, { status: 404 })
- }
- },
-})
diff --git a/examples/solid/start-basic/src/routes/api/users.$userId.ts b/examples/solid/start-basic/src/routes/api/users.$userId.ts
new file mode 100644
index 0000000000..69d966d982
--- /dev/null
+++ b/examples/solid/start-basic/src/routes/api/users.$userId.ts
@@ -0,0 +1,27 @@
+import { json } from '@tanstack/solid-start'
+import type { User } from '~/utils/users'
+
+export const ServerRoute = createServerFileRoute().methods({
+ GET: async ({ params, request }) => {
+ console.info(`Fetching users by id=${params.userId}... @`, request.url)
+ try {
+ const res = await fetch(
+ 'https://jsonplaceholder.typicode.com/users/' + params.userId,
+ )
+ if (!res.ok) {
+ throw new Error('Failed to fetch user')
+ }
+
+ const user = (await res.json()) as User
+
+ return json({
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ })
+ } catch (e) {
+ console.error(e)
+ return json({ error: 'User not found' }, { status: 404 })
+ }
+ },
+})
diff --git a/examples/solid/start-basic/src/routes/api/users.ts b/examples/solid/start-basic/src/routes/api/users.ts
index c10bcfc94a..c0a6a1a6a2 100644
--- a/examples/solid/start-basic/src/routes/api/users.ts
+++ b/examples/solid/start-basic/src/routes/api/users.ts
@@ -1,17 +1,58 @@
-import { json } from '@tanstack/solid-start'
-import { createAPIFileRoute } from '@tanstack/solid-start/api'
-import axios from 'redaxios'
-import type { User } from '../../utils/users'
+import { createMiddleware, json } from '@tanstack/solid-start'
+import type { User } from '~/utils/users'
-export const APIRoute = createAPIFileRoute('/api/users')({
- GET: async ({ request }) => {
- console.info('Fetching users... @', request.url)
- const res = await axios.get>(
- 'https://jsonplaceholder.typicode.com/users',
- )
-
- const list = res.data.slice(0, 10)
+const userLoggerMiddleware = createMiddleware({ type: 'request' }).server(
+ async ({ next, request }) => {
+ console.info('In: /users')
+ const result = await next()
+ result.response.headers.set('x-users', 'true')
+ console.info('Out: /users')
+ return result
+ },
+)
- return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email })))
+const testParentMiddleware = createMiddleware({ type: 'request' }).server(
+ async ({ next, request }) => {
+ console.info('In: testParentMiddleware')
+ const result = await next()
+ result.response.headers.set('x-test-parent', 'true')
+ console.info('Out: testParentMiddleware')
+ return result
},
-})
+)
+
+const testMiddleware = createMiddleware({ type: 'request' })
+ .middleware([testParentMiddleware])
+ .server(async ({ next, request }) => {
+ console.info('In: testMiddleware')
+ const result = await next()
+ result.response.headers.set('x-test', 'true')
+
+ // if (Math.random() > 0.5) {
+ // throw new Response(null, {
+ // status: 302,
+ // headers: { Location: 'https://www.google.com' },
+ // })
+ // }
+
+ console.info('Out: testMiddleware')
+ return result
+ })
+
+export const ServerRoute = createServerFileRoute()
+ .middleware([testMiddleware, userLoggerMiddleware, testParentMiddleware])
+ .methods({
+ GET: async ({ request }) => {
+ console.info('Fetching users... @', request.url)
+ const res = await fetch('https://jsonplaceholder.typicode.com/users')
+ if (!res.ok) {
+ throw new Error('Failed to fetch users')
+ }
+
+ const data = (await res.json()) as Array
+
+ const list = data.slice(0, 10)
+
+ return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email })))
+ },
+ })
diff --git a/examples/solid/start-basic/src/routes/deferred.tsx b/examples/solid/start-basic/src/routes/deferred.tsx
index 1860f3af9a..c510571e5d 100644
--- a/examples/solid/start-basic/src/routes/deferred.tsx
+++ b/examples/solid/start-basic/src/routes/deferred.tsx
@@ -1,6 +1,6 @@
-import { Await, createFileRoute } from '@tanstack/solid-router'
+import { Await } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
-import { createSignal, Suspense } from 'solid-js'
+import { Suspense, createSignal } from 'solid-js'
const personServerFn = createServerFn({ method: 'GET' })
.validator((d: string) => d)
@@ -15,7 +15,7 @@ const slowServerFn = createServerFn({ method: 'GET' })
return { name, randomNumber: Math.floor(Math.random() * 100) }
})
-export const Route = createFileRoute('/deferred')({
+export const Route = createFileRoute({
loader: async () => {
return {
deferredStuff: new Promise((r) =>
diff --git a/examples/solid/start-basic/src/routes/index.tsx b/examples/solid/start-basic/src/routes/index.tsx
index a128aeca0e..2f35891abb 100644
--- a/examples/solid/start-basic/src/routes/index.tsx
+++ b/examples/solid/start-basic/src/routes/index.tsx
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/')({
+export const Route = createFileRoute({
component: Home,
})
diff --git a/examples/solid/start-basic/src/routes/posts.$postId.tsx b/examples/solid/start-basic/src/routes/posts.$postId.tsx
index d13735a4db..7d06791adb 100644
--- a/examples/solid/start-basic/src/routes/posts.$postId.tsx
+++ b/examples/solid/start-basic/src/routes/posts.$postId.tsx
@@ -1,9 +1,9 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
import { fetchPost } from '../utils/posts'
import { NotFound } from '~/components/NotFound'
import { PostErrorComponent } from '~/components/PostError'
-export const Route = createFileRoute('/posts/$postId')({
+export const Route = createFileRoute({
loader: ({ params: { postId } }) => fetchPost({ data: postId }),
errorComponent: PostErrorComponent,
component: PostComponent,
diff --git a/examples/solid/start-basic/src/routes/posts.index.tsx b/examples/solid/start-basic/src/routes/posts.index.tsx
index 33d0386c19..13529228bb 100644
--- a/examples/solid/start-basic/src/routes/posts.index.tsx
+++ b/examples/solid/start-basic/src/routes/posts.index.tsx
@@ -1,6 +1,4 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/posts/')({
+export const Route = createFileRoute({
component: PostsIndexComponent,
})
diff --git a/examples/solid/start-basic/src/routes/posts.tsx b/examples/solid/start-basic/src/routes/posts.tsx
index 326372478f..4f0b47fa08 100644
--- a/examples/solid/start-basic/src/routes/posts.tsx
+++ b/examples/solid/start-basic/src/routes/posts.tsx
@@ -1,7 +1,7 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { Link, Outlet } from '@tanstack/solid-router'
import { fetchPosts } from '../utils/posts'
-export const Route = createFileRoute('/posts')({
+export const Route = createFileRoute({
loader: async () => fetchPosts(),
component: PostsComponent,
})
diff --git a/examples/solid/start-basic/src/routes/posts_.$postId.deep.tsx b/examples/solid/start-basic/src/routes/posts_.$postId.deep.tsx
index 7c52bc19eb..b71b51e8cc 100644
--- a/examples/solid/start-basic/src/routes/posts_.$postId.deep.tsx
+++ b/examples/solid/start-basic/src/routes/posts_.$postId.deep.tsx
@@ -1,8 +1,8 @@
-import { Link, createFileRoute } from '@tanstack/solid-router'
+import { Link } from '@tanstack/solid-router'
import { fetchPost } from '../utils/posts'
import { PostErrorComponent } from '~/components/PostError'
-export const Route = createFileRoute('/posts_/$postId/deep')({
+export const Route = createFileRoute({
loader: async ({ params: { postId } }) =>
fetchPost({
data: postId,
diff --git a/examples/solid/start-basic/src/routes/redirect.tsx b/examples/solid/start-basic/src/routes/redirect.tsx
index ca017f0635..d80d290638 100644
--- a/examples/solid/start-basic/src/routes/redirect.tsx
+++ b/examples/solid/start-basic/src/routes/redirect.tsx
@@ -1,6 +1,6 @@
-import { createFileRoute, redirect } from '@tanstack/solid-router'
+import { redirect } from '@tanstack/solid-router'
-export const Route = createFileRoute('/redirect')({
+export const Route = createFileRoute({
beforeLoad: async () => {
throw redirect({
to: '/posts',
diff --git a/examples/solid/start-basic/src/routes/users.$userId.tsx b/examples/solid/start-basic/src/routes/users.$userId.tsx
index a2d99fbf6b..9bdb09d835 100644
--- a/examples/solid/start-basic/src/routes/users.$userId.tsx
+++ b/examples/solid/start-basic/src/routes/users.$userId.tsx
@@ -1,18 +1,20 @@
-import { createFileRoute } from '@tanstack/solid-router'
-import axios from 'redaxios'
-import type { User } from '~/utils/users'
-import { DEPLOY_URL } from '~/utils/users'
-import { NotFound } from '~/components/NotFound'
-import { UserErrorComponent } from '~/components/UserError'
+import { NotFound } from 'src/components/NotFound'
+import { UserErrorComponent } from 'src/components/UserError'
-export const Route = createFileRoute('/users/$userId')({
+export const Route = createFileRoute({
loader: async ({ params: { userId } }) => {
- return await axios
- .get(DEPLOY_URL + '/api/users/' + userId)
- .then((r) => r.data)
- .catch(() => {
- throw new Error('Failed to fetch user')
- })
+ try {
+ const res = await fetch('/api/users/' + userId)
+ if (!res.ok) {
+ throw new Error('Unexpected status code')
+ }
+
+ const data = await res.json()
+
+ return data
+ } catch {
+ throw new Error('Failed to fetch user')
+ }
},
errorComponent: UserErrorComponent,
component: UserComponent,
@@ -28,6 +30,14 @@ function UserComponent() {
{user().name}
{user().email}
+
)
}
diff --git a/examples/solid/start-basic/src/routes/users.index.tsx b/examples/solid/start-basic/src/routes/users.index.tsx
index bbc96801a9..f30b0712b9 100644
--- a/examples/solid/start-basic/src/routes/users.index.tsx
+++ b/examples/solid/start-basic/src/routes/users.index.tsx
@@ -1,9 +1,14 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/users/')({
+export const Route = createFileRoute({
component: UsersIndexComponent,
})
function UsersIndexComponent() {
- return Select a user.
+ return (
+
+ )
}
diff --git a/examples/solid/start-basic/src/routes/users.tsx b/examples/solid/start-basic/src/routes/users.tsx
index 2caca046e0..c3a2a1fe15 100644
--- a/examples/solid/start-basic/src/routes/users.tsx
+++ b/examples/solid/start-basic/src/routes/users.tsx
@@ -1,16 +1,17 @@
-import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
-import axios from 'redaxios'
-import { DEPLOY_URL } from '../utils/users'
+import { Link, Outlet } from '@tanstack/solid-router'
import type { User } from '../utils/users'
-export const Route = createFileRoute('/users')({
+export const Route = createFileRoute({
loader: async () => {
- return await axios
- .get>(DEPLOY_URL + '/api/users')
- .then((r) => r.data)
- .catch(() => {
- throw new Error('Failed to fetch users')
- })
+ const res = await fetch('/api/users')
+
+ if (!res.ok) {
+ throw new Error('Unexpected status code')
+ }
+
+ const data = (await res.json()) as Array
+
+ return data
},
component: UsersComponent,
})
diff --git a/examples/solid/start-basic/src/ssr.tsx b/examples/solid/start-basic/src/ssr.tsx
deleted file mode 100644
index 6d10bea05f..0000000000
--- a/examples/solid/start-basic/src/ssr.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import {
- createStartHandler,
- defaultStreamHandler,
-} from '@tanstack/solid-start/server'
-import { getRouterManifest } from '@tanstack/solid-start/router-manifest'
-
-import { createRouter } from './router'
-
-export default createStartHandler({
- createRouter,
- getRouterManifest,
-})(defaultStreamHandler)
diff --git a/examples/solid/start-basic/src/tanstack-start.d.ts b/examples/solid/start-basic/src/tanstack-start.d.ts
new file mode 100644
index 0000000000..3adaf59556
--- /dev/null
+++ b/examples/solid/start-basic/src/tanstack-start.d.ts
@@ -0,0 +1,2 @@
+///
+import '../.tanstack-start/server-routes/routeTree.gen'
diff --git a/examples/solid/start-basic/src/utils/loggingMiddleware.tsx b/examples/solid/start-basic/src/utils/loggingMiddleware.tsx
index 928fedfd25..fae87c5965 100644
--- a/examples/solid/start-basic/src/utils/loggingMiddleware.tsx
+++ b/examples/solid/start-basic/src/utils/loggingMiddleware.tsx
@@ -1,6 +1,6 @@
import { createMiddleware } from '@tanstack/solid-start'
-const preLogMiddleware = createMiddleware()
+const preLogMiddleware = createMiddleware({ type: 'function' })
.client(async (ctx) => {
const clientTime = new Date()
@@ -25,7 +25,7 @@ const preLogMiddleware = createMiddleware()
})
})
-export const logMiddleware = createMiddleware()
+export const logMiddleware = createMiddleware({ type: 'function' })
.middleware([preLogMiddleware])
.client(async (ctx) => {
const res = await ctx.next()
diff --git a/examples/solid/start-basic/src/utils/posts.tsx b/examples/solid/start-basic/src/utils/posts.tsx
index 96eaa78afc..e29706d38c 100644
--- a/examples/solid/start-basic/src/utils/posts.tsx
+++ b/examples/solid/start-basic/src/utils/posts.tsx
@@ -8,7 +8,7 @@ export type PostType = {
body: string
}
-export const fetchPost = createServerFn({ method: 'GET' })
+export const fetchPost = createServerFn({ method: 'GET', type: 'static' })
.validator((d: string) => d)
.handler(async ({ data }) => {
console.info(`Fetching post with id ${data}...`)
@@ -26,11 +26,12 @@ export const fetchPost = createServerFn({ method: 'GET' })
return post
})
-export const fetchPosts = createServerFn({ method: 'GET' }).handler(
- async () => {
- console.info('Fetching posts...')
- return axios
- .get>('https://jsonplaceholder.typicode.com/posts')
- .then((r) => r.data.slice(0, 10))
- },
-)
+export const fetchPosts = createServerFn({
+ method: 'GET',
+ type: 'static',
+}).handler(async () => {
+ console.info('Fetching posts...')
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+})
diff --git a/examples/solid/start-basic/src/utils/users.tsx b/examples/solid/start-basic/src/utils/users.tsx
index b810f455fe..7ba645b383 100644
--- a/examples/solid/start-basic/src/utils/users.tsx
+++ b/examples/solid/start-basic/src/utils/users.tsx
@@ -3,5 +3,3 @@ export type User = {
name: string
email: string
}
-
-export const DEPLOY_URL = 'http://localhost:3000'
diff --git a/examples/solid/start-basic/vite.config.ts b/examples/solid/start-basic/vite.config.ts
new file mode 100644
index 0000000000..3af67d62ad
--- /dev/null
+++ b/examples/solid/start-basic/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({}),
+ ],
+})
diff --git a/package.json b/package.json
index 9ac5606a3a..9798cfeaba 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"rimraf": "^6.0.1",
"tinyglobby": "^0.2.12",
"typescript": "^5.8.2",
- "vite": "6.1.4",
+ "vite": "6.3.5",
"vitest": "^3.0.6",
"typescript53": "npm:typescript@5.3",
"typescript54": "npm:typescript@5.4",
@@ -107,12 +107,10 @@
"@tanstack/solid-start-plugin": "workspace:*",
"@tanstack/solid-start-router-manifest": "workspace:*",
"@tanstack/solid-start-server": "workspace:*",
- "@tanstack/start-api-routes": "workspace:*",
"@tanstack/start-server-functions-fetcher": "workspace:*",
- "@tanstack/start-server-functions-handler": "workspace:*",
"@tanstack/start-server-functions-client": "workspace:*",
- "@tanstack/start-server-functions-ssr": "workspace:*",
"@tanstack/start-server-functions-server": "workspace:*",
+ "@tanstack/start-plugin-core": "workspace:*",
"@tanstack/start-client-core": "workspace:*",
"@tanstack/start-server-core": "workspace:*",
"@tanstack/eslint-plugin-router": "workspace:*",
diff --git a/packages/arktype-adapter/package.json b/packages/arktype-adapter/package.json
index ebe26da7e3..56c99f6f71 100644
--- a/packages/arktype-adapter/package.json
+++ b/packages/arktype-adapter/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/arktype-adapter",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/directive-functions-plugin/package.json b/packages/directive-functions-plugin/package.json
index 7b07f0e0d4..c857c27d07 100644
--- a/packages/directive-functions-plugin/package.json
+++ b/packages/directive-functions-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/directive-functions-plugin",
- "version": "1.119.2",
+ "version": "1.121.0-alpha.2",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -74,14 +74,16 @@
"@babel/types": "^7.26.8",
"@tanstack/router-utils": "workspace:^",
"babel-dead-code-elimination": "^1.0.10",
- "dedent": "^1.5.3",
- "tiny-invariant": "^1.3.3",
- "vite": "6.1.4"
+ "tiny-invariant": "^1.3.3"
},
"devDependencies": {
"@types/babel__code-frame": "^7.0.6",
"@types/babel__core": "^7.20.5",
"@types/babel__template": "^7.4.4",
- "@types/babel__traverse": "^7.20.6"
+ "@types/babel__traverse": "^7.20.6",
+ "vite": "^6.0.0"
+ },
+ "peerDependencies": {
+ "vite": ">=6.0.0"
}
}
diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts
index 463f5e879e..43830231b9 100644
--- a/packages/directive-functions-plugin/src/compilers.ts
+++ b/packages/directive-functions-plugin/src/compilers.ts
@@ -40,6 +40,8 @@ export type CompileDirectivesOpts = ParseAstOptions & {
}) => string
replacer: ReplacerFn
// devSplitImporter: string
+ filename: string
+ root: string
}
function buildDirectiveSplitParam(opts: CompileDirectivesOpts) {
@@ -230,6 +232,8 @@ export function findDirectives(
directiveLabel: string
replacer?: ReplacerFn
directiveSplitParam: string
+ filename: string
+ root: string
},
): Record {
const directiveFnsById: Record = {}
diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts
index 25ed1df746..d0fd8aee06 100644
--- a/packages/directive-functions-plugin/src/index.ts
+++ b/packages/directive-functions-plugin/src/index.ts
@@ -15,23 +15,28 @@ export type {
ReplacerFn,
} from './compilers'
-export type DirectiveFunctionsViteOptions = Pick<
+export type DirectiveFunctionsViteEnvOptions = Pick<
CompileDirectivesOpts,
- 'directive' | 'directiveLabel' | 'getRuntimeCode' | 'replacer'
- // | 'devSplitImporter'
+ 'getRuntimeCode' | 'replacer'
> & {
envLabel: string
}
+export type DirectiveFunctionsViteOptions = Pick<
+ CompileDirectivesOpts,
+ 'directive' | 'directiveLabel'
+> &
+ DirectiveFunctionsViteEnvOptions & {
+ onDirectiveFnsById?: (directiveFnsById: Record) => void
+ }
+
const createDirectiveRx = (directive: string) =>
new RegExp(`"${directive}"|'${directive}'`, 'gm')
export function TanStackDirectiveFunctionsPlugin(
- opts: DirectiveFunctionsViteOptions & {
- onDirectiveFnsById?: (directiveFnsById: Record) => void
- },
+ opts: DirectiveFunctionsViteOptions,
): Plugin {
- let ROOT: string = process.cwd()
+ let root: string = process.cwd()
const directiveRx = createDirectiveRx(opts.directive)
@@ -39,36 +44,123 @@ export function TanStackDirectiveFunctionsPlugin(
name: 'tanstack-start-directive-vite-plugin',
enforce: 'pre',
configResolved: (config) => {
- ROOT = config.root
+ root = config.root
},
transform(code, id) {
- const url = pathToFileURL(id)
- url.searchParams.delete('v')
- id = fileURLToPath(url).replace(/\\/g, '/')
+ return transformCode({ ...opts, code, id, directiveRx, root })
+ },
+ }
+}
- if (!code.match(directiveRx)) {
- return null
- }
+export type DirectiveFunctionsVitePluginEnvOptions = Pick<
+ CompileDirectivesOpts,
+ 'directive' | 'directiveLabel'
+> & {
+ environments: {
+ client: DirectiveFunctionsViteEnvOptions & { envName?: string }
+ server: DirectiveFunctionsViteEnvOptions & { envName?: string }
+ }
+ onDirectiveFnsById?: (directiveFnsById: Record) => void
+}
- if (debug) console.info(`${opts.envLabel}: Compiling Directives: `, id)
+export function TanStackDirectiveFunctionsPluginEnv(
+ opts: DirectiveFunctionsVitePluginEnvOptions,
+): Plugin {
+ opts = {
+ ...opts,
+ environments: {
+ client: {
+ envName: 'client',
+ ...opts.environments.client,
+ },
+ server: {
+ envName: 'server',
+ ...opts.environments.server,
+ },
+ },
+ }
- const { compiledResult, directiveFnsById } = compileDirectives({
+ let root: string = process.cwd()
+
+ const directiveRx = createDirectiveRx(opts.directive)
+
+ return {
+ name: 'tanstack-start-directive-vite-plugin',
+ enforce: 'pre',
+ buildStart() {
+ root = this.environment.config.root
+ },
+ // applyToEnvironment(env) {
+ // return [
+ // opts.environments.client.envName,
+ // opts.environments.server.envName,
+ // ].includes(env.name)
+ // },
+ transform(code, id) {
+ const envOptions = [
+ opts.environments.client,
+ opts.environments.server,
+ ].find((e) => e.envName === this.environment.name)
+
+ if (!envOptions) {
+ throw new Error(`Environment ${this.environment.name} not found`)
+ }
+
+ return transformCode({
...opts,
+ ...envOptions,
code,
- root: ROOT,
- filename: id,
- // globalThis.app currently refers to Vinxi's app instance. In the future, it can just be the
- // vite dev server instance we get from Nitro.
+ id,
+ directiveRx,
+ root,
})
+ },
+ }
+}
- opts.onDirectiveFnsById?.(directiveFnsById)
+function transformCode({
+ code,
+ id,
+ directiveRx,
+ envLabel,
+ directive,
+ directiveLabel,
+ getRuntimeCode,
+ replacer,
+ onDirectiveFnsById,
+ root,
+}: DirectiveFunctionsViteOptions & {
+ code: string
+ id: string
+ directiveRx: RegExp
+ root: string
+}) {
+ const url = pathToFileURL(id)
+ url.searchParams.delete('v')
+ id = fileURLToPath(url).replace(/\\/g, '/')
- if (debug) {
- logDiff(code, compiledResult.code)
- console.log('Output:\n', compiledResult.code + '\n\n')
- }
+ if (!code.match(directiveRx)) {
+ return null
+ }
- return compiledResult
- },
+ if (debug) console.info(`${envLabel}: Compiling Directives: `, id)
+
+ const { compiledResult, directiveFnsById } = compileDirectives({
+ directive,
+ directiveLabel,
+ getRuntimeCode,
+ replacer,
+ code,
+ root,
+ filename: id,
+ })
+
+ onDirectiveFnsById?.(directiveFnsById)
+
+ if (debug) {
+ logDiff(code, compiledResult.code)
+ console.log('Output:\n', compiledResult.code + '\n\n')
}
+
+ return compiledResult
}
diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts
index a2c7fd3fc5..e405a7411e 100644
--- a/packages/directive-functions-plugin/tests/compiler.test.ts
+++ b/packages/directive-functions-plugin/tests/compiler.test.ts
@@ -713,8 +713,6 @@ describe('server function compilation', () => {
.extractedFilename,
})
- console.log(ssr.directiveFnsById)
-
expect(client.compiledResult.code).toMatchInlineSnapshot(`
"'use server';
diff --git a/packages/eslint-plugin-router/package.json b/packages/eslint-plugin-router/package.json
index 39c59420bd..34ca334a7e 100644
--- a/packages/eslint-plugin-router/package.json
+++ b/packages/eslint-plugin-router/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/eslint-plugin-router",
- "version": "1.115.0",
+ "version": "1.121.0-alpha.1",
"description": "ESLint plugin for TanStack Router",
"author": "Manuel Schiller",
"license": "MIT",
diff --git a/packages/history/package.json b/packages/history/package.json
index 2943dc0247..ae12d350d3 100644
--- a/packages/history/package.json
+++ b/packages/history/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/history",
- "version": "1.115.0",
+ "version": "1.121.0-alpha.1",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/react-router-devtools/package.json b/packages/react-router-devtools/package.json
index 940def08b5..8f70f10dd8 100644
--- a/packages/react-router-devtools/package.json
+++ b/packages/react-router-devtools/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-router-devtools",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/react-router-with-query/package.json b/packages/react-router-with-query/package.json
index fbfa01431f..d841497993 100644
--- a/packages/react-router-with-query/package.json
+++ b/packages/react-router-with-query/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-router-with-query",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/react-router-with-query/src/index.tsx b/packages/react-router-with-query/src/index.tsx
index e26bcf2975..7858816513 100644
--- a/packages/react-router-with-query/src/index.tsx
+++ b/packages/react-router-with-query/src/index.tsx
@@ -117,12 +117,8 @@ export function routerWithQueryClient(
...ogMutationCacheConfig,
onError: (error, _variables, _context, _mutation) => {
if (isRedirect(error)) {
- return router.navigate(
- router.resolveRedirect({
- ...error,
- _fromLocation: router.state.location,
- }),
- )
+ error.options._fromLocation = router.state.location
+ return router.navigate(router.resolveRedirect(error).options)
}
return ogMutationCacheConfig.onError?.(
@@ -139,12 +135,8 @@ export function routerWithQueryClient(
...ogQueryCacheConfig,
onError: (error, _query) => {
if (isRedirect(error)) {
- return router.navigate(
- router.resolveRedirect({
- ...error,
- _fromLocation: router.state.location,
- }),
- )
+ error.options._fromLocation = router.state.location
+ return router.navigate(router.resolveRedirect(error).options)
}
return ogQueryCacheConfig.onError?.(error, _query)
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index caac296dc9..d824933002 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-router",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/react-router/src/HeadContent.tsx b/packages/react-router/src/HeadContent.tsx
index 638dee8acc..b0856e5d49 100644
--- a/packages/react-router/src/HeadContent.tsx
+++ b/packages/react-router/src/HeadContent.tsx
@@ -58,8 +58,8 @@ export const useTags = () => {
}, [routeMeta])
const links = useRouterState({
- select: (state) =>
- state.matches
+ select: (state) => {
+ const constructed = state.matches
.map((match) => match.links!)
.filter(Boolean)
.flat(1)
@@ -68,7 +68,30 @@ export const useTags = () => {
attrs: {
...link,
},
- })) as Array,
+ })) satisfies Array
+
+ const manifest = router.ssr?.manifest
+
+ // These are the assets extracted from the ViteManifest
+ // using the `startManifestPlugin`
+ const assets = state.matches
+ .map((match) => manifest?.routes[match.routeId]?.assets ?? [])
+ .filter(Boolean)
+ .flat(1)
+ .filter((asset) => asset.tag === 'link')
+ .map(
+ (asset) =>
+ ({
+ tag: 'link',
+ attrs: {
+ ...asset.attrs,
+ suppressHydrationWarning: true,
+ },
+ }) satisfies RouterManagedTag,
+ )
+
+ return [...constructed, ...assets]
+ },
structuralSharing: true as any,
})
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index 79dd4ed01a..bf549f362b 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -306,6 +306,17 @@ export const Outlet = React.memo(function OutletImpl() {
},
})
+ const pendingElement = router.options.defaultPendingComponent ? (
+
+ ) : null
+
+ if (router.isShell)
+ return (
+
+
+
+ )
+
if (parentGlobalNotFound) {
return renderRouteNotFound(router, route, undefined)
}
@@ -316,10 +327,6 @@ export const Outlet = React.memo(function OutletImpl() {
const nextMatch =
- const pendingElement = router.options.defaultPendingComponent ? (
-
- ) : null
-
if (matchId === rootRouteId) {
return (
{nextMatch}
@@ -328,3 +335,7 @@ export const Outlet = React.memo(function OutletImpl() {
return nextMatch
})
+
+function ShellInner(): React.ReactElement {
+ throw new Error('ShellBoundaryError')
+}
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index d5a0638ad3..bc96173a21 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -15,6 +15,7 @@ import type { ReactNode } from './route'
import type {
AnyRouter,
DeepPartial,
+ Expand,
MakeOptionalPathParams,
MakeOptionalSearchParams,
MakeRouteMatchUnion,
@@ -123,7 +124,9 @@ export function useMatchRoute() {
const TMaskTo extends string = '',
>(
opts: UseMatchRouteOptions,
- ): false | ResolveRoute['types']['allParams'] => {
+ ):
+ | false
+ | Expand['types']['allParams']> => {
const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts
return router.matchRoute(rest as any, {
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index 92e882fef5..9c845df096 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -17,15 +17,17 @@ export function RouterContextProvider<
}: RouterProps & {
children: React.ReactNode
}) {
- // Allow the router to update options on the router instance
- router.update({
- ...router.options,
- ...rest,
- context: {
- ...router.options.context,
- ...rest.context,
- },
- } as any)
+ if (Object.keys(rest).length > 0) {
+ // 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()
diff --git a/packages/react-router/src/Scripts.tsx b/packages/react-router/src/Scripts.tsx
index 2c28537164..bc2b84c6d0 100644
--- a/packages/react-router/src/Scripts.tsx
+++ b/packages/react-router/src/Scripts.tsx
@@ -50,6 +50,7 @@ export const Scripts = () => {
children,
})),
}),
+ structuralSharing: true as any,
})
const allScripts = [...scripts, ...assetScripts] as Array
diff --git a/packages/react-router/src/ScrollRestoration.tsx b/packages/react-router/src/ScrollRestoration.tsx
index 063d93914f..71d472723d 100644
--- a/packages/react-router/src/ScrollRestoration.tsx
+++ b/packages/react-router/src/ScrollRestoration.tsx
@@ -64,6 +64,6 @@ export function useElementScrollRestoration(
}
const restoreKey = getKey(router.latestLocation)
- const byKey = scrollRestorationCache.state[restoreKey]
+ const byKey = scrollRestorationCache?.state[restoreKey]
return byKey?.[elementSelector]
}
diff --git a/packages/react-router/src/fileRoute.ts b/packages/react-router/src/fileRoute.ts
index d645cc840d..0b77e986e7 100644
--- a/packages/react-router/src/fileRoute.ts
+++ b/packages/react-router/src/fileRoute.ts
@@ -28,6 +28,7 @@ import type {
RouteIds,
RouteLoaderFn,
UpdatableRouteOptions,
+ UseNavigateResult,
} from '@tanstack/router-core'
import type { UseLoaderDepsRoute } from './useLoaderDeps'
import type { UseLoaderDataRoute } from './useLoaderData'
@@ -41,8 +42,13 @@ export function createFileRoute<
TFullPath extends
RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
>(
- path: TFilePath,
+ path?: TFilePath,
): FileRoute['createRoute'] {
+ if (typeof path === 'object') {
+ return new FileRoute(path, {
+ silent: true,
+ }).createRoute(path) as any
+ }
return new FileRoute(path, {
silent: true,
}).createRoute
@@ -63,7 +69,7 @@ export class FileRoute<
silent?: boolean
constructor(
- public path: TFilePath,
+ public path?: TFilePath,
_opts?: { silent: boolean },
) {
this.silent = _opts?.silent
@@ -159,6 +165,18 @@ export function FileRouteLoader<
return (loaderFn) => loaderFn as any
}
+declare module '@tanstack/router-core' {
+ export interface LazyRoute {
+ useMatch: UseMatchRoute
+ useRouteContext: UseRouteContextRoute
+ useSearch: UseSearchRoute
+ useParams: UseParamsRoute
+ useLoaderDeps: UseLoaderDepsRoute
+ useLoaderData: UseLoaderDataRoute
+ useNavigate: () => UseNavigateResult
+ }
+}
+
export class LazyRoute {
options: {
id: string
@@ -214,7 +232,7 @@ export class LazyRoute {
return useLoaderData({ ...opts, from: this.options.id } as any)
}
- useNavigate = () => {
+ useNavigate = (): UseNavigateResult => {
const router = useRouter()
return useNavigate({ from: router.routesById[this.options.id].fullPath })
}
@@ -236,6 +254,10 @@ export function createLazyRoute<
export function createLazyFileRoute<
TFilePath extends keyof FileRoutesByPath,
TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
->(id: TFilePath) {
+>(id: TFilePath): (opts: LazyRouteOptions) => LazyRoute {
+ if (typeof id === 'object') {
+ return new LazyRoute(id) as any
+ }
+
return (opts: LazyRouteOptions) => new LazyRoute({ id, ...opts })
}
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index b848fc7210..c662c21fc0 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -48,7 +48,6 @@ export type {
DeferredPromiseState,
DeferredPromise,
ParsedLocation,
- ParsePathParams,
RemoveTrailingSlashes,
RemoveLeadingSlashes,
ActiveOptions,
@@ -57,6 +56,8 @@ export type {
RootRouteId,
AnyPathParams,
ResolveParams,
+ ResolveOptionalParams,
+ ResolveRequiredParams,
SearchSchemaInput,
AnyContext,
RouteContext,
@@ -78,8 +79,6 @@ export type {
TrimPath,
TrimPathLeft,
TrimPathRight,
- ParseSplatParams,
- SplatParams,
StringifyParamsFn,
ParamsOptions,
InferAllParams,
@@ -126,6 +125,7 @@ export type {
RouteById,
RootRouteOptions,
SerializerExtensions,
+ CreateFileRoute,
} from '@tanstack/router-core'
export type * from './serializer'
@@ -237,6 +237,7 @@ export type {
RouteConstraints,
RouteMask,
MatchRouteOptions,
+ CreateLazyFileRoute,
} from '@tanstack/router-core'
export type {
UseLinkPropsOptions,
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index 3023a10288..58a5ed6862 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -16,7 +16,7 @@ import {
useLayoutEffect,
} from './utils'
-import { useMatches } from './Matches'
+import { useMatch } from './useMatch'
import type {
AnyRouter,
Constrain,
@@ -105,26 +105,28 @@ export function useLinkProps<
structuralSharing: true as any,
})
- // when `from` is not supplied, use the leaf route of the current matches as the `from` location
- // so relative routing works as expected
- const from = useMatches({
- select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
+ const nearestFrom = useMatch({
+ strict: false,
+ select: (match) => match.fullPath,
})
+
+ const from = options.from ?? nearestFrom
+
// Use it as the default `from` location
- const _options = React.useMemo(() => ({ ...options, from }), [options, from])
+ options = { ...options, from }
const next = React.useMemo(
- () => router.buildLocation(_options as any),
+ () => router.buildLocation(options as any),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [router, _options, currentSearch],
+ [router, options, currentSearch],
)
const preload = React.useMemo(() => {
- if (_options.reloadDocument) {
+ if (options.reloadDocument) {
return false
}
return userPreload ?? router.options.defaultPreload
- }, [router.options.defaultPreload, userPreload, _options.reloadDocument])
+ }, [router.options.defaultPreload, userPreload, options.reloadDocument])
const preloadDelay =
userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
@@ -175,11 +177,11 @@ export function useLinkProps<
})
const doPreload = React.useCallback(() => {
- router.preloadRoute(_options as any).catch((err) => {
+ router.preloadRoute(options as any).catch((err) => {
console.warn(err)
console.warn(preloadWarning)
})
- }, [_options, router])
+ }, [options, router])
const preloadViewportIoCallback = React.useCallback(
(entry: IntersectionObserverEntry | undefined) => {
@@ -249,14 +251,14 @@ export function useLinkProps<
// All is well? Navigate!
// N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
return router.navigate({
- ..._options,
+ ...options,
replace,
resetScroll,
hashScrollIntoView,
startTransition,
viewTransition,
ignoreBlocker,
- } as any)
+ })
}
}
@@ -279,10 +281,14 @@ export function useLinkProps<
return
}
- eventTarget.preloadTimeout = setTimeout(() => {
- eventTarget.preloadTimeout = null
+ if (!preloadDelay) {
doPreload()
- }, preloadDelay)
+ } else {
+ eventTarget.preloadTimeout = setTimeout(() => {
+ eventTarget.preloadTimeout = null
+ doPreload()
+ }, preloadDelay)
+ }
}
}
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 16b25cdcc9..3680337ba6 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1,4 +1,5 @@
import { RouterCore } from '@tanstack/router-core'
+import { createFileRoute, createLazyFileRoute } from './fileRoute'
import type { RouterHistory } from '@tanstack/history'
import type {
AnyRoute,
@@ -105,3 +106,11 @@ export class Router<
super(options)
}
}
+
+if (typeof globalThis !== 'undefined') {
+ ;(globalThis as any).createFileRoute = createFileRoute
+ ;(globalThis as any).createLazyFileRoute = createLazyFileRoute
+} else if (typeof window !== 'undefined') {
+ ;(window as any).createFileRoute = createFileRoute
+ ;(window as any).createFileRoute = createLazyFileRoute
+}
diff --git a/packages/react-router/src/useBlocker.tsx b/packages/react-router/src/useBlocker.tsx
index 1757dee80d..84a8eba9db 100644
--- a/packages/react-router/src/useBlocker.tsx
+++ b/packages/react-router/src/useBlocker.tsx
@@ -177,7 +177,10 @@ export function useBlocker(
location: HistoryLocation,
): AnyShouldBlockFnLocation {
const parsedLocation = router.parseLocation(undefined, location)
- const matchedRoutes = router.getMatchedRoutes(parsedLocation)
+ const matchedRoutes = router.getMatchedRoutes(
+ parsedLocation.pathname,
+ undefined,
+ )
if (matchedRoutes.foundRoute === undefined) {
throw new Error(`No route found for location ${location.href}`)
}
diff --git a/packages/react-router/src/useNavigate.tsx b/packages/react-router/src/useNavigate.tsx
index 64fdeb470a..1fcef97967 100644
--- a/packages/react-router/src/useNavigate.tsx
+++ b/packages/react-router/src/useNavigate.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import { useRouter } from './useRouter'
+import { useMatch } from './useMatch'
import type {
AnyRouter,
FromPathOption,
@@ -14,15 +15,28 @@ export function useNavigate<
>(_defaultOpts?: {
from?: FromPathOption
}): UseNavigateResult {
- const { navigate } = useRouter()
+ const { navigate, state } = useRouter()
+
+ // Just get the index of the current match to avoid rerenders
+ // as much as possible
+ const matchIndex = useMatch({
+ strict: false,
+ select: (match) => match.index,
+ })
return React.useCallback(
(options: NavigateOptions) => {
+ const from =
+ options.from ??
+ _defaultOpts?.from ??
+ state.matches[matchIndex]!.fullPath
+
return navigate({
- from: _defaultOpts?.from,
...options,
+ from,
})
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[_defaultOpts?.from, navigate],
) as UseNavigateResult
}
@@ -35,6 +49,7 @@ export function Navigate<
const TMaskTo extends string = '',
>(props: NavigateOptions): null {
const router = useRouter()
+ const navigate = useNavigate()
const previousPropsRef = React.useRef | null>(null)
React.useEffect(() => {
if (previousPropsRef.current !== props) {
- router.navigate({
- ...props,
- })
+ navigate(props)
previousPropsRef.current = props
}
- }, [router, props])
+ }, [router, props, navigate])
return null
}
diff --git a/packages/react-router/tests/ClientOnly.test.tsx b/packages/react-router/tests/ClientOnly.test.tsx
index b238d4e349..4bfe863d0b 100644
--- a/packages/react-router/tests/ClientOnly.test.tsx
+++ b/packages/react-router/tests/ClientOnly.test.tsx
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
-import { cleanup, render, screen } from '@testing-library/react'
+import { act, cleanup, render, screen } from '@testing-library/react'
import {
RouterProvider,
createMemoryHistory,
@@ -10,16 +10,14 @@ import {
createRouter,
} from '../src'
import { ClientOnly } from '../src/ClientOnly'
-import type { RouterHistory } from '../src'
afterEach(() => {
vi.resetAllMocks()
cleanup()
})
-function createTestRouter(initialHistory?: RouterHistory) {
- const history =
- initialHistory ?? createMemoryHistory({ initialEntries: ['/'] })
+function createTestRouter(opts: { isServer: boolean }) {
+ const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute({})
@@ -35,9 +33,18 @@ function createTestRouter(initialHistory?: RouterHistory) {
),
})
+ const otherRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/other',
+ component: () => (
+
+ ),
+ })
- const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree, history })
+ const routeTree = rootRoute.addChildren([indexRoute, otherRoute])
+ const router = createRouter({ routeTree, history, ...opts })
return {
router,
@@ -47,7 +54,7 @@ function createTestRouter(initialHistory?: RouterHistory) {
describe('ClientOnly', () => {
it('should render fallback during SSR', async () => {
- const { router } = createTestRouter()
+ const { router } = createTestRouter({ isServer: true })
await router.load()
// Initial render (SSR)
@@ -59,7 +66,7 @@ describe('ClientOnly', () => {
})
it('should render client content after hydration', async () => {
- const { router } = createTestRouter()
+ const { router } = createTestRouter({ isServer: false })
await router.load()
// Mock useSyncExternalStore to simulate hydration
@@ -67,12 +74,12 @@ describe('ClientOnly', () => {
render(
)
- expect(screen.getByText('Client Only Content')).toBeInTheDocument()
+ expect(await screen.findByTestId('client-only-content')).toBeInTheDocument()
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
})
it('should handle navigation with client-only content', async () => {
- const { router } = createTestRouter()
+ const { router } = createTestRouter({ isServer: false })
await router.load()
// Simulate hydration
@@ -81,11 +88,15 @@ describe('ClientOnly', () => {
// Re-render after hydration
render(
)
+ expect(await screen.findByTestId('client-only-content')).toBeInTheDocument()
+
// Navigate to a different route and back
- await router.navigate({ to: '/other' })
- await router.navigate({ to: '/' })
+ await act(() => router.navigate({ to: '/other' }))
+ expect(await screen.findByTestId('other-route')).toBeInTheDocument()
+
+ await act(() => router.navigate({ to: '/' }))
// Content should still be visible after navigation
- expect(screen.getByText('Client Only Content')).toBeInTheDocument()
+ expect(await screen.findByTestId('client-only-content')).toBeInTheDocument()
})
})
diff --git a/packages/react-router/tests/Scripts.test.tsx b/packages/react-router/tests/Scripts.test.tsx
index 13a9f0571c..315b3e528e 100644
--- a/packages/react-router/tests/Scripts.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -1,9 +1,10 @@
import { describe, expect, test } from 'vitest'
-import { render } from '@testing-library/react'
+import { act, render, screen } from '@testing-library/react'
import ReactDOMServer from 'react-dom/server'
import {
HeadContent,
+ Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
@@ -15,7 +16,6 @@ import { Scripts } from '../src/Scripts'
describe('ssr scripts', () => {
test('it works', async () => {
const rootRoute = createRootRoute({
- // loader: () => new Promise((r) => setTimeout(r, 1)),
head: () => {
return {
scripts: [
@@ -29,14 +29,19 @@ describe('ssr scripts', () => {
}
},
component: () => {
- return
+ return (
+
+ )
},
})
const indexRoute = createRoute({
path: '/',
getParentRoute: () => rootRoute,
- // loader: () => new Promise((r) => setTimeout(r, 2)),
head: () => {
return {
scripts: [
@@ -53,10 +58,9 @@ describe('ssr scripts', () => {
initialEntries: ['/'],
}),
routeTree: rootRoute.addChildren([indexRoute]),
+ isServer: true,
})
- router.isServer = true
-
await router.load()
expect(router.state.matches.map((d) => d.headScripts).flat(1)).toEqual([
@@ -73,7 +77,13 @@ describe('ssr scripts', () => {
undefined, // 'script2.js' opted out by certain conditions, such as `NODE_ENV=production`.
],
component: () => {
- return
+ return (
+
+ )
},
})
@@ -81,6 +91,9 @@ describe('ssr scripts', () => {
path: '/',
getParentRoute: () => rootRoute,
scripts: () => [{ src: 'script3.js' }],
+ component: () => {
+ return
index
+ },
})
const router = createRouter({
@@ -88,10 +101,9 @@ describe('ssr scripts', () => {
initialEntries: ['/'],
}),
routeTree: rootRoute.addChildren([indexRoute]),
+ isServer: true,
})
- router.isServer = true
-
await router.load()
expect(router.state.matches.map((d) => d.scripts).flat(1)).toEqual([
@@ -100,10 +112,14 @@ describe('ssr scripts', () => {
{ src: 'script3.js' },
])
- const { container } = render(
)
+ const { container } = await act(() =>
+ render(
),
+ )
+ expect(await screen.findByTestId('root')).toBeInTheDocument()
+ expect(await screen.findByTestId('index')).toBeInTheDocument()
expect(container.innerHTML).toEqual(
- ``,
+ `
`,
)
})
})
@@ -123,7 +139,7 @@ describe('ssr HeadContent', () => {
},
{
name: 'description',
- content: loaderData.description,
+ content: loaderData?.description,
},
{
name: 'image',
@@ -160,7 +176,7 @@ describe('ssr HeadContent', () => {
},
{
name: 'description',
- content: loaderData.description,
+ content: loaderData?.description,
},
{
name: 'last-modified',
@@ -180,10 +196,9 @@ describe('ssr HeadContent', () => {
initialEntries: ['/'],
}),
routeTree: rootRoute.addChildren([indexRoute]),
+ isServer: true,
})
- router.isServer = true
-
await router.load()
expect(router.state.matches.map((d) => d.meta).flat(1)).toEqual([
@@ -202,7 +217,7 @@ describe('ssr HeadContent', () => {
,
)
expect(html).toEqual(
- `
Index`,
+ `
Index`,
)
})
})
diff --git a/packages/react-router/tests/blocker.test.tsx b/packages/react-router/tests/blocker.test.tsx
index 545a99699a..60d1e8a821 100644
--- a/packages/react-router/tests/blocker.test.tsx
+++ b/packages/react-router/tests/blocker.test.tsx
@@ -1,11 +1,12 @@
import React from 'react'
import '@testing-library/jest-dom/vitest'
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import combinate from 'combinate'
import {
Link,
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouter,
@@ -13,9 +14,17 @@ import {
useBlocker,
useNavigate,
} from '../src'
-import type { ShouldBlockFn } from '../src'
+import type { RouterHistory, ShouldBlockFn } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
afterEach(() => {
+ history.destroy()
window.history.replaceState(null, 'root', '/')
vi.resetAllMocks()
cleanup()
@@ -91,6 +100,7 @@ async function setup({ blockerFn, disabled, ignoreBlocker }: BlockerTestOpts) {
fooRoute,
barRoute,
]),
+ history,
})
render(
)
diff --git a/packages/react-router/tests/errorComponent.test.tsx b/packages/react-router/tests/errorComponent.test.tsx
index fd6a5fa8ca..2c0062a9ac 100644
--- a/packages/react-router/tests/errorComponent.test.tsx
+++ b/packages/react-router/tests/errorComponent.test.tsx
@@ -1,14 +1,15 @@
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import {
Link,
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'
-import type { ErrorComponentProps } from '../src'
+import type { ErrorComponentProps, RouterHistory } from '../src'
function MyErrorComponent(props: ErrorComponentProps) {
return
Error: {props.error.message}
@@ -23,7 +24,15 @@ function throwFn() {
throw new Error('error thrown')
}
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
+
afterEach(() => {
+ history.destroy()
vi.resetAllMocks()
window.history.replaceState(null, 'root', '/')
cleanup()
@@ -71,6 +80,7 @@ describe.each([{ preload: false }, { preload: 'intent' }] as const)(
const router = createRouter({
routeTree,
defaultPreload: options.preload,
+ history,
})
render(
)
@@ -89,7 +99,9 @@ describe.each([{ preload: false }, { preload: 'intent' }] as const)(
undefined,
{ timeout: 1500 },
)
- expect(screen.findByText('About route content')).rejects.toThrow()
+ await expect(
+ screen.findByText('About route content'),
+ ).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)
@@ -114,6 +126,7 @@ describe.each([{ preload: false }, { preload: 'intent' }] as const)(
const router = createRouter({
routeTree,
defaultPreload: options.preload,
+ history,
})
render(
)
@@ -123,7 +136,9 @@ describe.each([{ preload: false }, { preload: 'intent' }] as const)(
undefined,
{ timeout: 750 },
)
- expect(screen.findByText('Index route content')).rejects.toThrow()
+ await expect(
+ screen.findByText('Index route content'),
+ ).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)
diff --git a/packages/react-router/tests/index.test.tsx b/packages/react-router/tests/index.test.tsx
deleted file mode 100644
index 657db70915..0000000000
--- a/packages/react-router/tests/index.test.tsx
+++ /dev/null
@@ -1,564 +0,0 @@
-import { expect, test } from 'vitest'
-
-// keeping this dummy test in since in the future
-// we may want to grab the commented out tests from here
-
-test('index true=true', () => {
- expect(true).toBe(true)
-})
-// import { render } from '@testing-library/react'
-
-// import React from 'react'
-
-// import {
-// Outlet,
-// RouterProvider,
-// cleanPath,
-// createMemoryHistory,
-// createRootRoute,
-// createRoute,
-// createRouter,
-// // Location,
-// matchPathname,
-// // ParsedLocation,
-// parsePathname,
-// redirect,
-// // Route,
-// // createMemoryHistory,
-// resolvePath,
-// // Segment,
-// trimPath,
-// trimPathLeft,
-// } from '../src'
-
-// import { createTimer, sleep } from './utils'
-
-// function RouterInstance(opts?: { initialEntries?: string[] }) {
-// return new RouterInstance({
-// routes: [],
-// history: createMemoryHistory({
-// initialEntries: opts?.initialEntries ?? ['/'],
-// }),
-// })
-// }
-
-// function createLocation(location: Partial
): ParsedLocation {
-// return {
-// pathname: '',
-// href: '',
-// search: {},
-// searchStr: '',
-// state: {},
-// hash: '',
-// ...location,
-// }
-// }
-
-// describe('Router', () => {
-// test('mounts to /', async () => {
-// const router = RouterInstance()
-
-// const routes = [
-// {
-// path: '/',
-// },
-// ]
-
-// router.update({
-// routes,
-// })
-
-// const promise = router.mount()
-// expect(router.store.pendingMatches[0].id).toBe('/')
-
-// await promise
-// expect(router.state.matches[0].id).toBe('/')
-// })
-
-// test('mounts to /a', async () => {
-// const router = RouterInstance({ initialEntries: ['/a'] })
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: '/a',
-// },
-// ]
-
-// router.update({
-// routes,
-// })
-
-// let promise = router.mount()
-
-// expect(router.store.pendingMatches[0].id).toBe('/a')
-// await promise
-// expect(router.state.matches[0].id).toBe('/a')
-// })
-
-// test('mounts to /a/b', async () => {
-// const router = RouterInstance({
-// initialEntries: ['/a/b'],
-// })
-
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: '/a',
-// children: [
-// {
-// path: '/b',
-// },
-// ],
-// },
-// ]
-
-// router.update({
-// routes,
-// })
-
-// let promise = router.mount()
-
-// expect(router.store.pendingMatches[1].id).toBe('/a/b')
-// await promise
-// expect(router.state.matches[1].id).toBe('/a/b')
-// })
-
-// test('navigates to /a', async () => {
-// const router = RouterInstance()
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: 'a',
-// },
-// ]
-
-// router.update({
-// routes,
-// })
-
-// let promise = router.mount()
-
-// expect(router.store.pendingMatches[0].id).toBe('/')
-
-// await promise
-// expect(router.state.matches[0].id).toBe('/')
-
-// promise = router.navigate({ to: 'a' })
-// expect(router.state.matches[0].id).toBe('/')
-// expect(router.store.pendingMatches[0].id).toBe('a')
-
-// await promise
-// expect(router.state.matches[0].id).toBe('a')
-// expect(router.store.pending).toBe(undefined)
-// })
-
-// test('navigates to /a to /a/b', async () => {
-// const router = RouterInstance()
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: 'a',
-// children: [
-// {
-// path: 'b',
-// },
-// ],
-// },
-// ]
-
-// router.update({
-// routes,
-// })
-
-// await router.mount()
-// expect(router.state.location.href).toBe('/')
-
-// let promise = router.navigate({ to: 'a' })
-// expect(router.store.pendingLocation.href).toBe('/a')
-// await promise
-// expect(router.state.location.href).toBe('/a')
-
-// promise = router.navigate({ to: './b' })
-// expect(router.store.pendingLocation.href).toBe('/a/b')
-// await promise
-// expect(router.state.location.href).toBe('/a/b')
-
-// expect(router.store.pending).toBe(undefined)
-// })
-
-// test('async navigates to /a/b', async () => {
-// const router = RouterInstance()
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: 'a',
-// loader: () => sleep(10).then((d) => ({ a: true })),
-// children: [
-// {
-// path: 'b',
-// loader: () => sleep(10).then((d) => ({ b: true })),
-// },
-// ],
-// },
-// ]
-
-// const timer = createTimer()
-
-// router.update({
-// routes,
-// })
-
-// router.mount()
-
-// timer.start()
-// await router.navigate({ to: 'a/b' })
-// expect(router.store.loaderData).toEqual({
-// a: true,
-// b: true,
-// })
-// expect(timer.getTime()).toBeLessThan(30)
-// })
-
-// test('async navigates with import + loader', async () => {
-// const router = RouterInstance()
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: 'a',
-// import: async () => {
-// await sleep(10)
-// return {
-// loader: () => sleep(10).then((d) => ({ a: true })),
-// }
-// },
-// children: [
-// {
-// path: 'b',
-// import: async () => {
-// await sleep(10)
-// return {
-// loader: () =>
-// sleep(10).then((d) => ({
-// b: true,
-// })),
-// }
-// },
-// },
-// ],
-// },
-// ]
-
-// const timer = createTimer()
-
-// router.update({
-// routes,
-// })
-
-// router.mount()
-
-// timer.start()
-// await router.navigate({ to: 'a/b' })
-// expect(router.store.loaderData).toEqual({
-// a: true,
-// b: true,
-// })
-// expect(timer.getTime()).toBeLessThan(28)
-// })
-
-// test('async navigates with import + elements + loader', async () => {
-// const router = RouterInstance()
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: 'a',
-// import: async () => {
-// await sleep(10)
-// return {
-// element: async () => {
-// await sleep(20)
-// return 'element'
-// },
-// loader: () => sleep(30).then((d) => ({ a: true })),
-// }
-// },
-// children: [
-// {
-// path: 'b',
-// import: async () => {
-// await sleep(10)
-// return {
-// element: async () => {
-// await sleep(20)
-// return 'element'
-// },
-// loader: () =>
-// sleep(30).then((d) => ({
-// b: true,
-// })),
-// }
-// },
-// },
-// ],
-// },
-// ]
-
-// const timer = createTimer()
-
-// router.update({
-// routes,
-// })
-
-// router.mount()
-
-// await router.navigate({ to: 'a/b' })
-// expect(router.store.loaderData).toEqual({
-// a: true,
-// b: true,
-// })
-// expect(timer.getTime()).toBeLessThan(55)
-// })
-
-// test('async navigates with pending state', async () => {
-// const router = RouterInstance()
-// const routes: Route[] = [
-// {
-// path: '/',
-// },
-// {
-// path: 'a',
-// pendingMs: 10,
-// loader: () => sleep(20),
-// children: [
-// {
-// path: 'b',
-// pendingMs: 30,
-// loader: () => sleep(40),
-// },
-// ],
-// },
-// ]
-
-// router.update({
-// routes,
-// })
-
-// await router.mount()
-
-// const timer = createTimer()
-// await router.navigate({ to: 'a/b' })
-// expect(timer.getTime()).toBeLessThan(46)
-// })
-// })
-
-// describe('matchRoute', () => {
-// describe('fuzzy', () => {
-// ;(
-// [
-// [
-// '/',
-// {
-// to: '/',
-// fuzzy: true,
-// },
-// {},
-// ],
-// [
-// '/a',
-// {
-// to: '/',
-// fuzzy: true,
-// },
-// {},
-// ],
-// [
-// '/a',
-// {
-// to: '/$',
-// fuzzy: true,
-// },
-// { '*': 'a' },
-// ],
-// [
-// '/a/b',
-// {
-// to: '/$',
-// fuzzy: true,
-// },
-// { '*': 'a/b' },
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/$',
-// fuzzy: true,
-// },
-// { '*': 'a/b/c' },
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/',
-// fuzzy: true,
-// },
-// {},
-// ],
-// [
-// '/a/b',
-// {
-// to: '/a/b/',
-// fuzzy: true,
-// },
-// {},
-// ],
-// ] as const
-// ).forEach(([a, b, eq]) => {
-// test(`${a} == ${b.to}`, () => {
-// expect(matchPathname('', a, b)).toEqual(eq)
-// })
-// })
-// })
-
-// describe('exact', () => {
-// ;(
-// [
-// [
-// '/a/b/c',
-// {
-// to: '/',
-// },
-// undefined,
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/a/b',
-// },
-// undefined,
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/a/b/c',
-// },
-// {},
-// ],
-// ] as const
-// ).forEach(([a, b, eq]) => {
-// test(`${a} == ${b.to}`, () => {
-// expect(matchPathname('', a, b)).toEqual(eq)
-// })
-// })
-// })
-
-// describe('basepath', () => {
-// ;(
-// [
-// [
-// '/base',
-// '/base',
-// {
-// to: '/',
-// },
-// {},
-// ],
-// [
-// '/base',
-// '/base/a',
-// {
-// to: '/a',
-// },
-// {},
-// ],
-// [
-// '/base',
-// '/base/a/b/c',
-// {
-// to: '/a/b/c',
-// },
-// {},
-// ],
-// [
-// '/base',
-// '/base/posts',
-// {
-// fuzzy: true,
-// to: '/',
-// },
-// {},
-// ],
-// [
-// '/base',
-// '/base/a',
-// {
-// to: '/b',
-// },
-// undefined,
-// ],
-// ] as const
-// ).forEach(([a, b, c, eq]) => {
-// test(`${b} == ${a} + ${c.to}`, () => {
-// expect(matchPathname(a, b, c)).toEqual(eq)
-// })
-// })
-// })
-
-// describe('params', () => {
-// ;(
-// [
-// [
-// '/a/b',
-// {
-// to: '/a/$b',
-// },
-// { b: 'b' },
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/a/$b/$c',
-// },
-// { b: 'b', c: 'c' },
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/$a/$b/$c',
-// },
-// { a: 'a', b: 'b', c: 'c' },
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/$a/$',
-// },
-// { a: 'a', '*': 'b/c' },
-// ],
-// [
-// '/a/b/c',
-// {
-// to: '/a/$b/c',
-// },
-// { b: 'b' },
-// ],
-// ] as const
-// ).forEach(([a, b, eq]) => {
-// test(`${a} == ${b.to}`, () => {
-// expect(matchPathname('', a, b)).toEqual(eq)
-// })
-// })
-// })
-// })
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index 65ebcbe66c..fe5672f7af 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -16,6 +16,7 @@ import {
Link,
Outlet,
RouterProvider,
+ createBrowserHistory,
createLink,
createMemoryHistory,
createRootRoute,
@@ -38,9 +39,11 @@ import {
getSearchParamsFromURI,
sleep,
} from './utils'
+import type { RouterHistory } from '../src'
const ioObserveMock = vi.fn()
const ioDisconnectMock = vi.fn()
+let history: RouterHistory
beforeEach(() => {
const io = getIntersectionObserverMock({
@@ -48,10 +51,13 @@ beforeEach(() => {
disconnect: ioDisconnectMock,
})
vi.stubGlobal('IntersectionObserver', io)
- window.history.replaceState(null, 'root', '/')
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
})
afterEach(() => {
+ history.destroy()
+ window.history.replaceState(null, 'root', '/')
vi.resetAllMocks()
cleanup()
})
@@ -99,6 +105,7 @@ describe('Link', () => {
const memoedRouter = React.useMemo(() => {
const router = createRouter({
routeTree: memoedRouteTree,
+ history,
})
return router
@@ -148,6 +155,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -196,6 +204,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -269,6 +278,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
+ history,
})
render()
@@ -345,7 +355,7 @@ describe('Link', () => {
expect(indexFooBarLink).not.toHaveAttribute('data-status', 'active')
// navigate to /?foo=bar
- fireEvent.click(indexFooBarLink)
+ await act(() => fireEvent.click(indexFooBarLink))
expect(indexExactLink).toHaveClass('inactive')
expect(indexExactLink).not.toHaveClass('active')
@@ -441,6 +451,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -484,13 +495,14 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const postsHeading = await screen.findByRole('heading', { name: 'Posts' })
expect(postsHeading).toBeInTheDocument()
@@ -544,6 +556,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
basepath: '/app',
})
@@ -551,7 +564,7 @@ describe('Link', () => {
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const postsHeading = await screen.findByRole('heading', { name: 'Posts' })
expect(postsHeading).toBeInTheDocument()
@@ -609,6 +622,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -617,7 +631,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts?page=0')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const postsHeading = await screen.findByRole('heading', { name: 'Posts' })
expect(postsHeading).toBeInTheDocument()
@@ -680,6 +694,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -688,7 +703,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts?page=invalid')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
await waitFor(() => expect(onError).toHaveBeenCalledOnce())
@@ -747,6 +762,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -755,7 +771,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts?page=2')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const pageFour = await screen.findByText('Page: 4')
expect(pageFour).toBeInTheDocument()
@@ -814,6 +830,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -822,7 +839,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts?page=2')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const errorText = await screen.findByText('Something went wrong!')
expect(errorText).toBeInTheDocument()
@@ -880,13 +897,14 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const postsErrorText = await screen.findByText('PostsError')
expect(postsErrorText).toBeInTheDocument()
@@ -895,9 +913,9 @@ describe('Link', () => {
expect(postsOnError).toHaveBeenCalledWith(error)
const indexLink = await screen.findByRole('link', { name: 'Index' })
- fireEvent.click(indexLink)
+ await act(() => fireEvent.click(indexLink))
- expect(screen.findByText('IndexError')).rejects.toThrow()
+ await expect(screen.findByText('IndexError')).rejects.toThrow()
expect(indexOnError).not.toHaveBeenCalledOnce()
})
@@ -941,13 +959,14 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute, authRoute]),
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const authText = await screen.findByText('Auth!')
expect(authText).toBeInTheDocument()
@@ -995,13 +1014,14 @@ describe('Link', () => {
const router = createRouter({
context: { userId: 'userId' },
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const userId = await screen.findByText('UserId: userId')
expect(userId).toBeInTheDocument()
@@ -1041,13 +1061,14 @@ describe('Link', () => {
const router = createRouter({
context: { userId: 'userId' },
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const errorText = await screen.findByText('Oops! Something went wrong!')
expect(errorText).toBeInTheDocument()
@@ -1089,13 +1110,14 @@ describe('Link', () => {
const router = createRouter({
context: { userId: 'userId' },
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const errorText = await screen.findByText('Oops! Something went wrong!')
expect(errorText).toBeInTheDocument()
@@ -1151,13 +1173,14 @@ describe('Link', () => {
indexRoute,
postsRoute.addChildren([postRoute]),
]),
+ history,
})
render()
const postLink = await screen.findByRole('link', { name: 'Post' })
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const errorText = await screen.findByText('Oops! Something went wrong!')
expect(errorText).toBeInTheDocument()
@@ -1183,6 +1206,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
+ history,
})
render()
@@ -1243,6 +1267,7 @@ describe('Link', () => {
indexRoute,
postsRoute.addChildren([postRoute]),
]),
+ history,
})
render()
@@ -1253,7 +1278,7 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const paramText = await screen.findByText('Params: id1')
expect(paramText).toBeInTheDocument()
@@ -1330,6 +1355,7 @@ describe('Link', () => {
indexRoute,
postsRoute.addChildren([postsIndexRoute, postRoute]),
]),
+ history,
})
render()
@@ -1338,7 +1364,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const postsText = await screen.findByText('Posts Index')
expect(postsText).toBeInTheDocument()
@@ -1349,7 +1375,7 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const paramText = await screen.findByText('Params: id1')
expect(paramText).toBeInTheDocument()
@@ -1428,6 +1454,7 @@ describe('Link', () => {
indexRoute,
postsRoute.addChildren([postsIndexRoute, postRoute]),
]),
+ history,
})
render()
@@ -1436,7 +1463,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const postsIndexText = await screen.findByText('Posts Index')
expect(postsIndexText).toBeInTheDocument()
@@ -1447,7 +1474,7 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const paramText = await screen.findByText('Params: id1')
expect(paramText).toBeInTheDocument()
@@ -1556,6 +1583,7 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
@@ -1564,7 +1592,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -1577,7 +1605,7 @@ describe('Link', () => {
expect(informationLink).toHaveAttribute('href', '/posts/id1/info')
- fireEvent.click(informationLink)
+ await act(() => fireEvent.click(informationLink))
const informationText = await screen.findByText('Information')
expect(informationText).toBeInTheDocument()
@@ -1693,6 +1721,7 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
@@ -1701,7 +1730,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -1714,7 +1743,7 @@ describe('Link', () => {
expect(informationLink).toHaveAttribute('href', '/posts/id1/info')
- fireEvent.click(informationLink)
+ await act(() => fireEvent.click(informationLink))
const informationText = await screen.findByText('Information')
expect(informationText).toBeInTheDocument()
@@ -1830,6 +1859,7 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
@@ -1838,7 +1868,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -1851,7 +1881,7 @@ describe('Link', () => {
expect(informationLink).toHaveAttribute('href', '/posts/id1/info')
- fireEvent.click(informationLink)
+ await act(() => fireEvent.click(informationLink))
const informationText = await screen.findByText('Information')
expect(informationText).toBeInTheDocument()
@@ -1955,6 +1985,7 @@ describe('Link', () => {
postsRoute.addChildren([postRoute.addChildren([detailsRoute])]),
]),
]),
+ history,
})
render()
@@ -1963,7 +1994,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -1976,7 +2007,7 @@ describe('Link', () => {
expect(rootLink).toHaveAttribute('href', '/')
- fireEvent.click(rootLink)
+ await act(() => fireEvent.click(rootLink))
const indexText = await screen.findByText('Index')
expect(indexText).toBeInTheDocument()
@@ -2099,6 +2130,7 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
@@ -2107,7 +2139,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details?page=2')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -2123,7 +2155,7 @@ describe('Link', () => {
'/posts/id1/info?page=2&more=true',
)
- fireEvent.click(informationLink)
+ await act(() => fireEvent.click(informationLink))
const informationText = await screen.findByText('Information')
expect(informationText).toBeInTheDocument()
@@ -2239,6 +2271,7 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
@@ -2247,7 +2280,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -2260,7 +2293,7 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const postsText = await screen.findByText('Posts')
expect(postsText).toBeInTheDocument()
@@ -2383,6 +2416,7 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
@@ -2391,7 +2425,7 @@ describe('Link', () => {
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const paramsText1 = await screen.findByText('Params: id1')
expect(paramsText1).toBeInTheDocument()
@@ -2404,7 +2438,7 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const postsText = await screen.findByText('Posts')
expect(postsText).toBeInTheDocument()
@@ -2415,6 +2449,8 @@ describe('Link', () => {
})
test('when navigating from /invoices to ./invoiceId and the current route is /posts/$postId/details', async () => {
+ const consoleWarnSpy = vi.spyOn(console, 'warn')
+
const rootRoute = createRootRoute()
const indexRoute = createRoute({
@@ -2540,20 +2576,24 @@ describe('Link', () => {
]),
]),
]),
+ history,
})
render()
- const postsLink = await screen.findByRole('link', { name: 'To first post' })
+ const postsLink = await screen.findByRole('link', {
+ name: 'To first post',
+ })
expect(postsLink).toHaveAttribute('href', '/posts/id1/details')
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
- const invoicesErrorText = await screen.findByText(
- 'Invariant failed: Could not find match for from: /invoices',
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ 'Could not find match for from: /invoices',
)
- expect(invoicesErrorText).toBeInTheDocument()
+
+ consoleWarnSpy.mockRestore()
})
test('when navigating to /posts/$postId/info which is declaratively masked as /posts/$postId', async () => {
@@ -2633,6 +2673,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
routeMasks: [routeMask],
+ history,
})
render()
@@ -2720,6 +2761,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
+ history,
})
render()
@@ -2838,6 +2880,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
+ history,
})
render()
@@ -2848,13 +2891,13 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.mouseOver(postLink)
+ await act(() => fireEvent.mouseOver(postLink))
await waitFor(() => expect(loaderFn).toHaveBeenCalled())
await waitFor(() => expect(search).toHaveBeenCalledWith({ postPage: 0 }))
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const loginText = await screen.findByText('Login!')
expect(loginText).toBeInTheDocument()
@@ -2926,7 +2969,7 @@ describe('Link', () => {
})
const LoginComponent = () => {
- return <>Login!>
+ return Login!
}
const loginRoute = createRoute({
@@ -2944,6 +2987,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
+ history,
})
render()
@@ -2954,10 +2998,9 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
- const loginText = await screen.findByText('Login!')
- expect(loginText).toBeInTheDocument()
+ expect(await screen.findByTestId('login')).toBeInTheDocument()
expect(ErrorComponent).not.toHaveBeenCalled()
})
@@ -3044,6 +3087,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
+ history,
})
render()
@@ -3054,7 +3098,7 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const loginText = await screen.findByText('Login!')
expect(loginText).toBeInTheDocument()
@@ -3139,6 +3183,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
+ history,
})
render()
@@ -3149,11 +3194,11 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.mouseOver(postLink)
+ await act(() => fireEvent.mouseOver(postLink))
await waitFor(() => expect(search).toHaveBeenCalledWith({ postPage: 0 }))
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const loginText = await screen.findByText('Login!')
expect(loginText).toBeInTheDocument()
@@ -3175,7 +3220,11 @@ describe('Link', () => {
return (
<>
Index
-
+
To first post
>
@@ -3240,6 +3289,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
+ history,
})
render()
@@ -3250,11 +3300,11 @@ describe('Link', () => {
expect(postLink).toHaveAttribute('href', '/posts/id1')
- fireEvent.mouseOver(postLink)
+ await act(() => fireEvent.mouseOver(postLink))
await waitFor(() => expect(search).toHaveBeenCalledWith({ postPage: 0 }))
- fireEvent.click(postLink)
+ await act(() => fireEvent.click(postLink))
const loginText = await screen.findByText('Login!')
expect(loginText).toBeInTheDocument()
@@ -3332,13 +3382,14 @@ describe('Link', () => {
const router = createRouter({
routeTree,
+ history,
})
render()
const postsLink = await screen.findByRole('link', { name: 'Go to posts' })
- fireEvent.click(postsLink)
+ await act(() => fireEvent.click(postsLink))
const fromPostsLink = await screen.findByRole('link', {
name: 'From posts',
@@ -3350,7 +3401,7 @@ describe('Link', () => {
name: 'To invoices',
})
- fireEvent.click(toInvoicesLink)
+ await act(() => fireEvent.click(toInvoicesLink))
const fromInvoicesLink = await screen.findByRole('link', {
name: 'From invoices',
@@ -3364,7 +3415,7 @@ describe('Link', () => {
name: 'To posts',
})
- fireEvent.click(toPostsLink)
+ await act(() => fireEvent.click(toPostsLink))
const onPostsText = await screen.findByText('On Posts')
expect(onPostsText).toBeInTheDocument()
@@ -3402,7 +3453,7 @@ describe('Link', () => {
})
const routeTree = rootRoute.addChildren([indexRoute, postRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -3464,7 +3515,7 @@ describe('Link', () => {
})
const routeTree = rootRoute.addChildren([indexRoute, postRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -3528,7 +3579,7 @@ describe('Link', () => {
})
const routeTree = rootRoute.addChildren([indexRoute, postRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -3597,7 +3648,7 @@ describe('Link', () => {
})
const routeTree = rootRoute.addChildren([indexRoute, postRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -3633,6 +3684,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
defaultPreload: preload,
+ history,
})
render()
@@ -3665,6 +3717,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
defaultPreload: preload,
+ history,
})
render()
@@ -3692,6 +3745,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
defaultPreload: 'viewport',
+ history,
})
render()
@@ -3736,6 +3790,7 @@ describe('Link', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([aboutRoute, indexRoute]),
defaultPreload: 'render',
+ history,
})
render()
@@ -3776,6 +3831,7 @@ describe('Link', () => {
defaultPreload: 'intent',
defaultPendingMs: 200,
defaultPendingComponent: () => Loading...
,
+ history,
})
render()
@@ -3889,6 +3945,7 @@ describe('Link', () => {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
+ history,
})
render()
@@ -3940,6 +3997,7 @@ describe('createLink', () => {
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
+ history,
})
render()
@@ -3969,6 +4027,7 @@ describe('createLink', () => {
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
+ history,
})
render()
@@ -4048,6 +4107,7 @@ describe('createLink', () => {
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -4309,6 +4369,7 @@ describe('search middleware', () => {
postsRoute.addChildren([postsNewRoute]),
invoicesRoute,
]),
+ history,
})
window.history.replaceState(null, 'root', '/?root=abc')
@@ -4400,6 +4461,7 @@ describe('search middleware', () => {
indexRoute,
postsRoute.addChildren([postRoute]),
]),
+ history,
})
render()
@@ -4410,3 +4472,348 @@ describe('search middleware', () => {
})
})
})
+
+describe('relative links', () => {
+ const setupRouter = () => {
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => {
+ return Index Route
+ },
+ })
+ const aRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'a',
+ component: () => {
+ return (
+ <>
+ A Route
+
+ >
+ )
+ },
+ })
+
+ const bRoute = createRoute({
+ getParentRoute: () => aRoute,
+ path: 'b',
+ component: () => {
+ return (
+ <>
+ B Route
+ Link to Parent
+ >
+ )
+ },
+ })
+
+ const paramRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'param/$param',
+ component: () => {
+ return (
+ <>
+ Param Route
+ Link to ./a
+
+ Link to c
+
+
+ Link to ../c
+
+
+ >
+ )
+ },
+ })
+
+ const paramARoute = createRoute({
+ getParentRoute: () => paramRoute,
+ path: 'a',
+ component: () => {
+ return (
+ <>
+ Param A Route
+ Link to .. from /param/foo/a
+
+ >
+ )
+ },
+ })
+
+ const paramBRoute = createRoute({
+ getParentRoute: () => paramARoute,
+ path: 'b',
+ component: () => {
+ return (
+ <>
+ Param B Route
+ Link to Parent
+
+ Link to Parent with param:bar
+
+ ({ ...prev, param: 'bar' })}
+ >
+ Link to Parent with param:bar functional
+
+ >
+ )
+ },
+ })
+
+ const paramCRoute = createRoute({
+ getParentRoute: () => paramARoute,
+ path: 'c',
+ component: () => {
+ return Param C Route
+ },
+ })
+
+ const splatRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'splat/$',
+ component: () => {
+ return (
+ <>
+ Splat Route
+
+ Unsafe link to ..
+
+
+ Unsafe link to .
+
+
+ Unsafe link to ./child
+
+ >
+ )
+ },
+ })
+
+ return createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ aRoute.addChildren([bRoute]),
+ paramRoute.addChildren([
+ paramARoute.addChildren([paramBRoute, paramCRoute]),
+ ]),
+ splatRoute,
+ ]),
+ history,
+ })
+ }
+
+ test('should navigate to the parent route', async () => {
+ const router = setupRouter()
+
+ render()
+
+ // Navigate to /a/b
+ await act(async () => {
+ history.push('/a/b')
+ })
+
+ // Inspect the link to go up a parent
+ const parentLink = await screen.findByText('Link to Parent')
+ expect(parentLink.getAttribute('href')).toBe('/a')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/a')
+ })
+
+ test('should navigate to the parent route and keep params', async () => {
+ const router = setupRouter()
+
+ render()
+
+ // Navigate to /param/oldParamValue/a/b
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the link to go up a parent and keep the params
+ const parentLink = await screen.findByText('Link to Parent')
+ expect(parentLink.getAttribute('href')).toBe('/param/foo/a')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo/a')
+ })
+
+ test('should navigate to the parent route and change params', async () => {
+ const router = setupRouter()
+
+ render()
+
+ // Navigate to /param/oldParamValue/a/b
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the link to go up a parent and keep the params
+ const parentLink = await screen.findByText('Link to Parent with param:bar')
+ expect(parentLink.getAttribute('href')).toBe('/param/bar/a')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/bar/a')
+ })
+
+ test('should navigate to a relative link based on render location', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the relative link to ./a
+ const relativeLink = await screen.findByText('Link to ./a')
+ expect(relativeLink.getAttribute('href')).toBe('/param/foo/a')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo/a')
+ })
+
+ test('should navigate to a parent link based on render location', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the relative link to ./a
+ const relativeLink = await screen.findByText('Link to .. from /param/foo/a')
+ expect(relativeLink.getAttribute('href')).toBe('/param/foo')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo')
+ })
+
+ test('should navigate to a child link based on pathname', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the relative link to ./a
+ const relativeLink = await screen.findByText('Link to c')
+ expect(relativeLink.getAttribute('href')).toBe('/param/foo/a/b/c')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo/a/b/c')
+ })
+
+ test('should navigate to a relative link based on pathname', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the relative link to ./a
+ const relativeLink = await screen.findByText('Link to ../c')
+ expect(relativeLink.getAttribute('href')).toBe('/param/foo/a/c')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo/a/c')
+ })
+
+ test('should navigate to parent inside of splat route based on pathname', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/splat/a/b/c/d')
+ })
+
+ const relativeLink = await screen.findByText('Unsafe link to ..')
+ expect(relativeLink.getAttribute('href')).toBe('/splat/a/b/c')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/splat/a/b/c')
+ })
+
+ test('should navigate to same route inside of splat route based on pathname', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/splat/a/b/c')
+ })
+
+ const relativeLink = await screen.findByText('Unsafe link to .')
+ expect(relativeLink.getAttribute('href')).toBe('/splat/a/b/c')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/splat/a/b/c')
+ })
+
+ test('should navigate to child route inside of splat route based on pathname', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/splat/a/b/c')
+ })
+
+ const relativeLink = await screen.findByText('Unsafe link to ./child')
+ expect(relativeLink.getAttribute('href')).toBe('/splat/a/b/c/child')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/splat/a/b/c/child')
+ })
+})
diff --git a/packages/react-router/tests/loaders.test.tsx b/packages/react-router/tests/loaders.test.tsx
index 85a9dc15f9..ab1e671fad 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -6,21 +6,31 @@ import {
screen,
} from '@testing-library/react'
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { z } from 'zod'
import {
Link,
Outlet,
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'
import { sleep } from './utils'
+import type { RouterHistory } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
afterEach(() => {
+ history.destroy()
vi.resetAllMocks()
window.history.replaceState(null, 'root', '/')
cleanup()
@@ -45,7 +55,7 @@ describe('loaders are being called', () => {
component: () => Index page
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -96,7 +106,7 @@ describe('loaders are being called', () => {
nestedRoute.addChildren([fooRoute]),
indexRoute,
])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -152,7 +162,7 @@ describe('loaders parentMatchPromise', () => {
nestedRoute.addChildren([fooRoute]),
indexRoute,
])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -190,7 +200,7 @@ test('reproducer for #2031', async () => {
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -220,6 +230,7 @@ test('reproducer for #2053', async () => {
const router = createRouter({
routeTree,
+ history,
})
render()
@@ -244,6 +255,7 @@ test('reproducer for #2198 - throw error from beforeLoad upon initial load', asy
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({
routeTree,
+ history,
defaultErrorComponent: () => {
return defaultErrorComponent
},
@@ -271,6 +283,7 @@ test('throw error from loader upon initial load', async () => {
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({
routeTree,
+ history,
defaultErrorComponent: () => {
return defaultErrorComponent
},
@@ -309,6 +322,7 @@ test('throw error from beforeLoad when navigating to route', async () => {
const routeTree = rootRoute.addChildren([indexRoute, fooRoute])
const router = createRouter({
routeTree,
+ history,
defaultErrorComponent: () => {
return defaultErrorComponent
},
diff --git a/packages/react-router/tests/navigate.test.tsx b/packages/react-router/tests/navigate.test.tsx
index b0175eb26b..033b9702db 100644
--- a/packages/react-router/tests/navigate.test.tsx
+++ b/packages/react-router/tests/navigate.test.tsx
@@ -566,4 +566,21 @@ describe('relative navigation', () => {
expect(router.state.location.pathname).toBe('/posts/tkdodo')
})
+
+ it('should navigate to a parent route with .. from unsafe relative path', async () => {
+ const { router } = createTestRouter(
+ createMemoryHistory({ initialEntries: ['/posts/tanner/child'] }),
+ )
+
+ await router.load()
+
+ expect(router.state.location.pathname).toBe('/posts/tanner/child')
+
+ await router.navigate({
+ to: '..',
+ unsafeRelative: 'path',
+ })
+
+ expect(router.state.location.pathname).toBe('/posts/tanner')
+ })
})
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index 91f0196841..56d0d80049 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -6,11 +6,13 @@ import {
screen,
} from '@testing-library/react'
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
+import invariant from 'tiny-invariant'
import {
Link,
RouterProvider,
+ createBrowserHistory,
createMemoryHistory,
createRootRoute,
createRoute,
@@ -20,8 +22,17 @@ import {
} from '../src'
import { sleep } from './utils'
+import type { RouterHistory } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
afterEach(() => {
+ history.destroy()
vi.clearAllMocks()
vi.resetAllMocks()
window.history.replaceState(null, 'root', '/')
@@ -80,7 +91,7 @@ describe('redirect', () => {
aboutRoute,
indexRoute,
])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -157,7 +168,7 @@ describe('redirect', () => {
aboutRoute,
indexRoute,
])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -235,7 +246,7 @@ describe('redirect', () => {
indexRoute,
finalRoute,
])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
@@ -280,11 +291,18 @@ describe('redirect', () => {
routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
// Mock server mode
isServer: true,
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
})
await router.load()
- expect(router.state.redirect).toEqual({
+ expect(router.state.redirect).toBeDefined()
+ expect(router.state.redirect).toBeInstanceOf(Response)
+ invariant(router.state.redirect)
+
+ expect(router.state.redirect.options).toEqual({
_fromLocation: expect.objectContaining({
hash: '',
href: '/',
@@ -293,12 +311,7 @@ describe('redirect', () => {
searchStr: '',
}),
to: '/about',
- headers: {},
- reloadDocument: false,
href: '/about',
- isRedirect: true,
- routeId: '/',
- routerCode: 'BEFORE_LOAD',
statusCode: 307,
})
})
@@ -329,27 +342,33 @@ describe('redirect', () => {
initialEntries: ['/'],
}),
routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
+ // Mock server mode
+ isServer: true,
})
- // Mock server mode
- router.isServer = true
-
await router.load()
- expect(router.state.redirect).toEqual({
- _fromLocation: expect.objectContaining({
+ const currentRedirect = router.state.redirect
+
+ expect(currentRedirect).toBeDefined()
+ expect(currentRedirect).toBeInstanceOf(Response)
+ invariant(currentRedirect)
+ expect(currentRedirect.status).toEqual(307)
+ expect(currentRedirect.headers.get('Location')).toEqual('/about')
+ expect(currentRedirect.options).toEqual({
+ _fromLocation: {
hash: '',
href: '/',
pathname: '/',
search: {},
searchStr: '',
- }),
- to: '/about',
- headers: {},
+ state: {
+ __TSR_index: 0,
+ key: currentRedirect.options._fromLocation!.state.key,
+ },
+ },
href: '/about',
- isRedirect: true,
- reloadDocument: false,
- routeId: '/',
+ to: '/about',
statusCode: 307,
})
})
diff --git a/packages/react-router/tests/route.test-d.tsx b/packages/react-router/tests/route.test-d.tsx
index 2183cf4af7..ed2ece1bc9 100644
--- a/packages/react-router/tests/route.test-d.tsx
+++ b/packages/react-router/tests/route.test-d.tsx
@@ -643,6 +643,8 @@ test('when creating a child route with a param and splat param from the root rou
routeTree: rootRoute.addChildren([invoicesRoute]),
})
+ const params = invoicesRoute.useParams()
+
expectTypeOf(invoicesRoute.useParams()).toEqualTypeOf<{
invoiceId: string
_splat?: string
@@ -1776,3 +1778,149 @@ test('when creating a child route with an explicit search input', () => {
.parameter(0)
.toEqualTypeOf<{ page: string }>()
})
+
+test('when creating a route with a prefix and suffix', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'prefix{$postId}suffix',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ postId: string
+ }>()
+})
+
+test('when creating a route with a optional prefix and suffix', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'prefix{-$postId}suffix',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ postId?: string
+ }>()
+})
+
+test('when creating a route with a splat prefix and suffix', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'prefix{$}suffix',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ _splat?: string
+ }>()
+})
+
+test('when creating a route with a splat, optional param and required param', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'docs/$docId/$/{-$detailId}/{$myFile}.pdf',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ docId: string
+ _splat?: string
+ myFile: string
+ detailId?: string
+ }>()
+})
+
+test('when creating a route with a boundary splat, optional param and required param', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'docs/$docId/before{$}after/detail{-$detailId}/file-{$myFile}.pdf',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ docId: string
+ _splat?: string
+ myFile: string
+ detailId?: string
+ }>()
+})
+
+test('when creating a route with a nested boundary splat, optional param and required param', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'docs/$docId/before{$}after/{-detail{$detailId}suffix}/file-{$myFile}.pdf',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ docId: string
+ _splat?: string
+ myFile: string
+ detailId?: string
+ }>()
+})
+
+test('when creating a route with a nested boundary splat, optional param, required param and escaping', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'docs/$docId/before{$}after/{-detail{$detailId}suffix}[.$test]/file-{$myFile}[.]pdf/escape-param[$postId]',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{
+ docId: string
+ _splat?: string
+ myFile: string
+ detailId?: string
+ }>()
+})
+
+test('when creating a route with escaped path param', () => {
+ const rootRoute = createRootRoute()
+
+ const prefixSuffixRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '[$postId]',
+ })
+
+ const routeTree = rootRoute.addChildren([prefixSuffixRoute])
+
+ const router = createRouter({ routeTree })
+
+ expectTypeOf(prefixSuffixRoute.useParams()).toEqualTypeOf<{}>()
+})
diff --git a/packages/react-router/tests/route.test.tsx b/packages/react-router/tests/route.test.tsx
index 9c6fbbfc0e..ba30768dea 100644
--- a/packages/react-router/tests/route.test.tsx
+++ b/packages/react-router/tests/route.test.tsx
@@ -1,16 +1,27 @@
import React from 'react'
-import { afterEach, describe, expect, it, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import {
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouter,
getRouteApi,
+ notFound,
} from '../src'
+import type { RouterHistory } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
afterEach(() => {
+ history.destroy()
vi.resetAllMocks()
window.history.replaceState(null, 'root', '/')
cleanup()
@@ -162,7 +173,7 @@ describe('onEnter event', () => {
},
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree, context: { foo: 'bar' } })
+ const router = createRouter({ routeTree, context: { foo: 'bar' }, history })
await router.load()
@@ -183,7 +194,7 @@ describe('onEnter event', () => {
},
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree, context: { foo: 'bar' } })
+ const router = createRouter({ routeTree, context: { foo: 'bar' }, history })
render()
@@ -215,7 +226,7 @@ describe('route.head', () => {
component: () => Index
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
@@ -255,7 +266,7 @@ describe('route.head', () => {
component: () => Index
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
@@ -272,6 +283,85 @@ describe('route.head', () => {
])
})
+ test('meta is set when loader throws notFound', async () => {
+ const rootRoute = createRootRoute({
+ head: () => ({
+ meta: [
+ { title: 'Root' },
+ {
+ charSet: 'utf-8',
+ },
+ ],
+ }),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ head: () => ({
+ meta: [{ title: 'Index' }],
+ }),
+ loader: async () => {
+ throw notFound()
+ },
+ component: () => Index
,
+ })
+ const routeTree = rootRoute.addChildren([indexRoute])
+ const router = createRouter({ routeTree })
+ render()
+ expect(await screen.findByText('Not Found')).toBeInTheDocument()
+
+ const metaState = router.state.matches.map((m) => m.meta)
+ expect(metaState).toEqual([
+ [
+ { title: 'Root' },
+ {
+ charSet: 'utf-8',
+ },
+ ],
+ [{ title: 'Index' }],
+ ])
+ })
+
+ test('meta is set when loader throws an error', async () => {
+ const rootRoute = createRootRoute({
+ head: () => ({
+ meta: [
+ { title: 'Root' },
+ {
+ charSet: 'utf-8',
+ },
+ ],
+ }),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ head: () => ({
+ meta: [{ title: 'Index' }],
+ }),
+ loader: async () => {
+ throw new Error('Fly, you fools!')
+ },
+ component: () => Index
,
+ })
+ const routeTree = rootRoute.addChildren([indexRoute])
+ const router = createRouter({ routeTree })
+ render()
+
+ expect(await screen.findByText('Fly, you fools!')).toBeInTheDocument()
+
+ const metaState = router.state.matches.map((m) => m.meta)
+ expect(metaState).toEqual([
+ [
+ { title: 'Root' },
+ {
+ charSet: 'utf-8',
+ },
+ ],
+ [{ title: 'Index' }],
+ ])
+ })
+
test('scripts', async () => {
const rootRoute = createRootRoute({
head: () => ({
@@ -287,7 +377,7 @@ describe('route.head', () => {
component: () => Index
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
@@ -317,7 +407,7 @@ describe('route.head', () => {
component: () => Index
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
@@ -344,7 +434,7 @@ describe('route.head', () => {
component: () => Index
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
@@ -374,7 +464,7 @@ describe('route.head', () => {
component: () => Index
,
})
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
render()
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()
diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx
index c62decee7f..7a38a272d9 100644
--- a/packages/react-router/tests/routeContext.test.tsx
+++ b/packages/react-router/tests/routeContext.test.tsx
@@ -122,7 +122,6 @@ describe('context function', () => {
await clickButton('detail-1')
await findByText('Detail page: 1')
- console.log(mockContextFn.mock.calls)
expect(mockContextFn).toHaveBeenCalledOnce()
expect(mockContextFn).toHaveBeenCalledWith({ id: 1 })
mockContextFn.mockClear()
@@ -1457,7 +1456,7 @@ describe('beforeLoad in the route definition', () => {
}
await check(1)
- await router.invalidate()
+ await act(async () => await router.invalidate())
await check(2)
})
})
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 1a295a3b0b..0b720dd58d 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -13,6 +13,7 @@ import {
Outlet,
RouterProvider,
SearchParamError,
+ createBrowserHistory,
createMemoryHistory,
createRootRoute,
createRoute,
@@ -24,14 +25,23 @@ import type {
AnyRoute,
AnyRouter,
MakeRemountDepsOptionsUnion,
+ RouterHistory,
RouterOptions,
ValidatorFn,
ValidatorObj,
} from '../src'
+let history: RouterHistory
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
+
afterEach(() => {
- vi.resetAllMocks()
+ history.destroy()
window.history.replaceState(null, 'root', '/')
+ vi.clearAllMocks()
+ vi.resetAllMocks()
cleanup()
})
@@ -48,7 +58,10 @@ export function validateSearchParams<
expect(router.state.location.search).toEqual(expected)
}
-function createTestRouter(options?: RouterOptions) {
+function createTestRouter(
+ options: RouterOptions &
+ Required, 'history'>>,
+) {
const rootRoute = createRootRoute({
validateSearch: z.object({ root: z.string().optional() }),
component: () => {
@@ -975,6 +988,7 @@ describe('router rendering stability', () => {
const router = createRouter({
routeTree,
defaultRemountDeps: opts?.remountDeps.default,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
})
await act(() => render())
@@ -1196,6 +1210,15 @@ describe('invalidate', () => {
})
describe('search params in URL', () => {
+ let history: RouterHistory
+ beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+ })
+ afterEach(() => {
+ history.destroy()
+ window.history.replaceState(null, 'root', '/')
+ })
const testCases = [
{ route: '/', search: { root: 'world' } },
{ route: '/', search: { root: 'world', unknown: 'asdf' } },
@@ -1212,7 +1235,7 @@ describe('search params in URL', () => {
it.each(testCases)(
'at $route with search params $search',
async ({ route, search }) => {
- const { router } = createTestRouter({ search: { strict } })
+ const { router } = createTestRouter({ search: { strict }, history })
window.history.replaceState(
null,
'',
@@ -1238,7 +1261,7 @@ describe('search params in URL', () => {
describe('removes unknown search params in the URL when search.strict=true', () => {
it.each(testCases)('%j', async ({ route, search }) => {
- const { router } = createTestRouter({ search: { strict: true } })
+ const { router } = createTestRouter({ search: { strict: true }, history })
window.history.replaceState(
null,
'',
@@ -1264,7 +1287,7 @@ describe('search params in URL', () => {
describe.each([false, true, undefined])('default search params', (strict) => {
let router: AnyRouter
beforeEach(() => {
- const result = createTestRouter({ search: { strict } })
+ const result = createTestRouter({ search: { strict }, history })
router = result.router
})
@@ -1584,6 +1607,7 @@ const createHistoryRouter = () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
return { router }
@@ -1716,7 +1740,13 @@ it('does not push to history if url and state are the same', async () => {
})
describe('does not strip search params if search validation fails', () => {
+ let history: RouterHistory
+
+ beforeEach(() => {
+ history = createBrowserHistory()
+ })
afterEach(() => {
+ history.destroy()
window.history.replaceState(null, 'root', '/')
cleanup()
})
@@ -1751,7 +1781,7 @@ describe('does not strip search params if search validation fails', () => {
const routeTree = rootRoute.addChildren([indexRoute])
- const router = createRouter({ routeTree })
+ const router = createRouter({ routeTree, history })
return router
}
diff --git a/packages/react-router/tests/searchMiddleware.test.tsx b/packages/react-router/tests/searchMiddleware.test.tsx
index 36b4ebb58e..6629dab085 100644
--- a/packages/react-router/tests/searchMiddleware.test.tsx
+++ b/packages/react-router/tests/searchMiddleware.test.tsx
@@ -1,9 +1,10 @@
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import {
Link,
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouter,
@@ -11,12 +12,21 @@ import {
stripSearchParams,
} from '../src'
import { getSearchParamsFromURI } from './utils'
-import type { AnyRouter } from '../src'
+import type { AnyRouter, RouterHistory } from '../src'
import type { SearchMiddleware } from '@tanstack/router-core'
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
+
afterEach(() => {
- vi.resetAllMocks()
+ history.destroy()
window.history.replaceState(null, 'root', '/')
+ vi.clearAllMocks()
+ vi.resetAllMocks()
cleanup()
})
@@ -70,6 +80,7 @@ function setupTest(opts: {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
window.history.replaceState(
null,
diff --git a/packages/react-router/tests/useBlocker.test.tsx b/packages/react-router/tests/useBlocker.test.tsx
index 01dc7cf548..3b780775bd 100644
--- a/packages/react-router/tests/useBlocker.test.tsx
+++ b/packages/react-router/tests/useBlocker.test.tsx
@@ -1,20 +1,32 @@
import React from 'react'
import '@testing-library/jest-dom/vitest'
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { z } from 'zod'
import {
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouter,
useBlocker,
useNavigate,
} from '../src'
+import type { RouterHistory } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
afterEach(() => {
+ history.destroy()
window.history.replaceState(null, 'root', '/')
+ vi.clearAllMocks()
+ vi.resetAllMocks()
cleanup()
})
@@ -56,6 +68,7 @@ describe('useBlocker', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -108,6 +121,7 @@ describe('useBlocker', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -160,6 +174,7 @@ describe('useBlocker', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -216,6 +231,7 @@ describe('useBlocker', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -302,6 +318,7 @@ describe('useBlocker', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -394,6 +411,7 @@ describe('useBlocker', () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute, invoicesRoute]),
+ history,
})
type Router = typeof router
diff --git a/packages/react-router/tests/useMatch.test.tsx b/packages/react-router/tests/useMatch.test.tsx
index 67f68e3011..9bf809f666 100644
--- a/packages/react-router/tests/useMatch.test.tsx
+++ b/packages/react-router/tests/useMatch.test.tsx
@@ -39,6 +39,8 @@ describe('useMatch', () => {
),
})
+ history = history || createMemoryHistory({ initialEntries: ['/'] })
+
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx
index 7449de1918..95d5742ea3 100644
--- a/packages/react-router/tests/useNavigate.test.tsx
+++ b/packages/react-router/tests/useNavigate.test.tsx
@@ -1,6 +1,6 @@
-import React from 'react'
+import React, { act } from 'react'
import '@testing-library/jest-dom/vitest'
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import {
cleanup,
configure,
@@ -14,6 +14,7 @@ import {
Navigate,
Outlet,
RouterProvider,
+ createBrowserHistory,
createRootRoute,
createRoute,
createRouteMask,
@@ -22,9 +23,20 @@ import {
useNavigate,
useParams,
} from '../src'
+import type { RouterHistory } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
afterEach(() => {
+ history.destroy()
window.history.replaceState(null, 'root', '/')
+ vi.clearAllMocks()
+ vi.resetAllMocks()
cleanup()
})
@@ -62,6 +74,7 @@ test('when navigating to /posts', async () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
render()
@@ -275,6 +288,7 @@ test('when navigating from /posts to ../posts/$postId', async () => {
indexRoute,
postsRoute.addChildren([postsIndexRoute, postRoute]),
]),
+ history,
})
render()
@@ -412,6 +426,7 @@ test('when navigating from /posts/$postId to /posts/$postId/info and the current
]),
]),
]),
+ history,
})
render()
@@ -555,6 +570,7 @@ test('when navigating from /posts/$postId to ./info and the current route is /po
]),
]),
]),
+ history,
})
render()
@@ -698,6 +714,7 @@ test('when navigating from /posts/$postId to ../$postId and the current route is
]),
]),
]),
+ history,
})
render()
@@ -849,6 +866,7 @@ test('when navigating from /posts/$postId with an index to ../$postId and the cu
]),
]),
]),
+ history,
})
render()
@@ -1029,8 +1047,11 @@ test('when navigating from /invoices to ./invoiceId and the current route is /po
]),
]),
]),
+ history,
})
+ const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+
render()
const postsButton = await screen.findByRole('button', {
@@ -1045,7 +1066,11 @@ test('when navigating from /invoices to ./invoiceId and the current route is /po
fireEvent.click(invoicesButton)
- expect(await screen.findByText('Something went wrong!')).toBeInTheDocument()
+ expect(consoleWarn).toHaveBeenCalledWith(
+ 'Could not find match for from: /invoices',
+ )
+
+ consoleWarn.mockRestore()
})
test('when navigating to /posts/$postId/info which is masked as /posts/$postId', async () => {
@@ -1132,6 +1157,7 @@ test('when navigating to /posts/$postId/info which is masked as /posts/$postId',
const router = createRouter({
routeTree,
routeMasks: [routeMask],
+ history,
})
render()
@@ -1226,6 +1252,7 @@ test('when navigating to /posts/$postId/info which is imperatively masked as /po
const router = createRouter({
routeTree,
+ history,
})
render()
@@ -1283,6 +1310,7 @@ test('when setting search params with 2 parallel navigate calls', async () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute]),
+ history,
})
render()
@@ -1327,6 +1355,7 @@ test(' navigates only once in ', async () => {
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
})
const navigateSpy = vi.spyOn(router, 'navigate')
@@ -1486,3 +1515,237 @@ describe('when on /posts/$postId and navigating to ../ with default `from` /post
test('Route', () => runTest('Route'))
test('RouteApi', () => runTest('RouteApi'))
})
+
+describe('relative useNavigate', () => {
+ const setupRouter = () => {
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => {
+ return Index Route
+ },
+ })
+ const aRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'a',
+ component: () => {
+ return (
+ <>
+ A Route
+
+ >
+ )
+ },
+ })
+
+ const bRoute = createRoute({
+ getParentRoute: () => aRoute,
+ path: 'b',
+ component: function BRoute() {
+ const navigate = useNavigate()
+ return (
+ <>
+ B Route
+
+ >
+ )
+ },
+ })
+
+ const paramRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'param/$param',
+ component: function ParamRoute() {
+ const navigate = useNavigate()
+ return (
+ <>
+ Param Route
+
+
+
+ >
+ )
+ },
+ })
+
+ const paramARoute = createRoute({
+ getParentRoute: () => paramRoute,
+ path: 'a',
+ component: function ParamARoute() {
+ const navigate = useNavigate()
+ return (
+ <>
+ Param A Route
+
+
+ >
+ )
+ },
+ })
+
+ const paramBRoute = createRoute({
+ getParentRoute: () => paramARoute,
+ path: 'b',
+ component: function ParamBRoute() {
+ const navigate = useNavigate()
+ return (
+ <>
+ Param B Route
+
+
+
+ >
+ )
+ },
+ })
+
+ return createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ aRoute.addChildren([bRoute]),
+ paramRoute.addChildren([paramARoute, paramBRoute]),
+ ]),
+ history,
+ })
+ }
+
+ test('should navigate to the parent route', async () => {
+ const router = setupRouter()
+
+ render()
+
+ // Navigate to /a/b
+ await act(async () => {
+ history.push('/a/b')
+ })
+
+ // Inspect the link to go up a parent
+ const parentLink = await screen.findByText('Link to Parent')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/a')
+ })
+
+ test('should navigate to the parent route and keep params', async () => {
+ const router = setupRouter()
+
+ render()
+
+ // Navigate to /param/oldParamValue/a/b
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the link to go up a parent and keep the params
+ const parentLink = await screen.findByText('Link to Parent')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo/a')
+ })
+
+ test('should navigate to the parent route and change params', async () => {
+ const router = setupRouter()
+
+ render()
+
+ // Navigate to /param/oldParamValue/a/b
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the link to go up a parent and keep the params
+ const parentLink = await screen.findByText('Link to Parent with param:bar')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/bar/a')
+ })
+
+ test('should navigate to a relative link based on render location', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the relative link to ./a
+ const relativeLink = await screen.findByText('Link to ./a')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo/a')
+ })
+
+ test('should navigate to a parent link based on render location', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ // Inspect the relative link to ./a
+ const relativeLink = await screen.findByText('Link to .. from /param/foo/a')
+
+ // Click the link and ensure the new location
+ await act(async () => {
+ fireEvent.click(relativeLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/foo')
+ })
+
+ test('should navigate to same route with different params', async () => {
+ const router = setupRouter()
+
+ render()
+
+ await act(async () => {
+ history.push('/param/foo/a/b')
+ })
+
+ const parentLink = await screen.findByText('Link to . with param:bar')
+
+ await act(async () => {
+ fireEvent.click(parentLink)
+ })
+
+ expect(window.location.pathname).toBe('/param/bar/a/b')
+ })
+})
diff --git a/packages/react-start-client/README.md b/packages/react-start-client/README.md
index bb009b0c87..ba94d88108 100644
--- a/packages/react-start-client/README.md
+++ b/packages/react-start-client/README.md
@@ -1,33 +1,9 @@
-> 🤫 we're cooking up something special!
-
-# TanStack Start
-
-
-
-🤖 Type-safe router w/ built-in caching & URL state management for React!
+# TanStack React Start - Client
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+This package is not meant to be used directly. It is a dependency of [`@tanstack/react-start`](https://www.npmjs.com/package/@tanstack/react-start).
-Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)
+TanStack React Start is a fullstack-framework made for SSR, Streaming, Server Functions, API Routes, bundling and more powered by [TanStack Router](https://tanstack.com/router).
-## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
+Head over to [tanstack.com/start](https://tanstack.com/start) for more information about getting started.
diff --git a/packages/react-start-client/package.json b/packages/react-start-client/package.json
index a83ed07e55..5680a185b4 100644
--- a/packages/react-start-client/package.json
+++ b/packages/react-start-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start-client",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -69,8 +69,7 @@
"cookie-es": "^1.2.2",
"jsesc": "^3.1.0",
"tiny-invariant": "^1.3.3",
- "tiny-warning": "^1.0.3",
- "vinxi": "^0.5.3"
+ "tiny-warning": "^1.0.3"
},
"devDependencies": {
"@testing-library/react": "^16.2.0",
diff --git a/packages/react-start-client/src/index.tsx b/packages/react-start-client/src/index.tsx
index 6b9c30ee8f..3aebc26282 100644
--- a/packages/react-start-client/src/index.tsx
+++ b/packages/react-start-client/src/index.tsx
@@ -1,6 +1,15 @@
-///
-export { mergeHeaders } from '@tanstack/start-client-core'
-export { startSerializer } from '@tanstack/start-client-core'
+export {
+ mergeHeaders,
+ startSerializer,
+ createIsomorphicFn,
+ createServerFn,
+ createMiddleware,
+ registerGlobalMiddleware,
+ globalMiddleware,
+ serverOnly,
+ clientOnly,
+ json,
+} from '@tanstack/start-client-core'
export {
type DehydratedRouter,
type ClientExtractedBaseEntry,
@@ -10,16 +19,10 @@ export {
type ClientExtractedPromise,
type ClientExtractedStream,
type ResolvePromiseState,
-} from '@tanstack/start-client-core'
-export {
- createIsomorphicFn,
type IsomorphicFn,
type ServerOnlyFn,
type ClientOnlyFn,
type IsomorphicFnBase,
-} from '@tanstack/start-client-core'
-export { createServerFn } from '@tanstack/start-client-core'
-export {
type ServerFn as FetchFn,
type ServerFnCtx as FetchFnCtx,
type CompiledFetcherFnOptions,
@@ -31,43 +34,33 @@ export {
type ServerFn,
type ServerFnCtx,
type ServerFnResponseType,
-} from '@tanstack/start-client-core'
-export { type JsonResponse } from '@tanstack/start-client-core'
-export {
- createMiddleware,
+ type JsonResponse,
type IntersectAllValidatorInputs,
type IntersectAllValidatorOutputs,
- type MiddlewareServerFn,
- type AnyMiddleware,
- type MiddlewareOptions,
- type MiddlewareWithTypes,
- type MiddlewareValidator,
- type MiddlewareServer,
- type MiddlewareAfterClient,
- type MiddlewareAfterMiddleware,
- type MiddlewareAfterServer,
- type Middleware,
- type MiddlewareClientFnOptions,
- type MiddlewareClientFnResult,
- type MiddlewareClientNextFn,
- type ClientResultWithContext,
+ type FunctionMiddlewareServerFn,
+ type AnyFunctionMiddleware,
+ type FunctionMiddlewareOptions,
+ type FunctionMiddlewareWithTypes,
+ type FunctionMiddlewareValidator,
+ type FunctionMiddlewareServer,
+ type FunctionMiddlewareAfterClient,
+ type FunctionMiddlewareAfterServer,
+ type FunctionMiddleware,
+ type FunctionMiddlewareClientFnOptions,
+ type FunctionMiddlewareClientFnResult,
+ type FunctionMiddlewareClientNextFn,
+ type FunctionClientResultWithContext,
type AssignAllClientContextBeforeNext,
type AssignAllMiddleware,
type AssignAllServerContext,
- type MiddlewareAfterValidator,
- type MiddlewareClientFn,
- type MiddlewareServerFnResult,
- type MiddlewareClient,
- type MiddlewareServerFnOptions,
- type MiddlewareServerNextFn,
- type ServerResultWithContext,
-} from '@tanstack/start-client-core'
-export {
- registerGlobalMiddleware,
- globalMiddleware,
+ type FunctionMiddlewareAfterValidator,
+ type FunctionMiddlewareClientFn,
+ type FunctionMiddlewareServerFnResult,
+ type FunctionMiddlewareClient,
+ type FunctionMiddlewareServerFnOptions,
+ type FunctionMiddlewareServerNextFn,
+ type FunctionServerResultWithContext,
} from '@tanstack/start-client-core'
-export { serverOnly, clientOnly } from '@tanstack/start-client-core'
-export { json } from '@tanstack/start-client-core'
export { Meta } from './Meta'
export { Scripts } from './Scripts'
export { StartClient } from './StartClient'
diff --git a/packages/react-start-client/src/renderRSC.tsx b/packages/react-start-client/src/renderRSC.tsx
index 6201bfa476..4832f074b6 100644
--- a/packages/react-start-client/src/renderRSC.tsx
+++ b/packages/react-start-client/src/renderRSC.tsx
@@ -1,6 +1,4 @@
// TODO: RSCs
-// // @ts-expect-error
-// import * as reactDom from '@vinxi/react-server-dom/client'
import { isValidElement } from 'react'
import invariant from 'tiny-invariant'
import type React from 'react'
diff --git a/packages/react-start-client/src/useServerFn.ts b/packages/react-start-client/src/useServerFn.ts
index c046bcb9ed..971b3a6685 100644
--- a/packages/react-start-client/src/useServerFn.ts
+++ b/packages/react-start-client/src/useServerFn.ts
@@ -17,12 +17,8 @@ export function useServerFn) => Promise>(
return res
} catch (err) {
if (isRedirect(err)) {
- const resolvedRedirect = router.resolveRedirect({
- ...err,
- _fromLocation: router.state.location,
- })
-
- return router.navigate(resolvedRedirect)
+ err.options._fromLocation = router.state.location
+ return router.navigate(router.resolveRedirect(err).options)
}
throw err
diff --git a/packages/react-start-config/README.md b/packages/react-start-config/README.md
deleted file mode 100644
index bb009b0c87..0000000000
--- a/packages/react-start-config/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-> 🤫 we're cooking up something special!
-
-
-
-# TanStack Start
-
-
-
-🤖 Type-safe router w/ built-in caching & URL state management for React!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)
-
-## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
diff --git a/packages/react-start-config/eslint.config.js b/packages/react-start-config/eslint.config.js
deleted file mode 100644
index 931f0ec774..0000000000
--- a/packages/react-start-config/eslint.config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// @ts-check
-
-import pluginReact from '@eslint-react/eslint-plugin'
-import pluginReactHooks from 'eslint-plugin-react-hooks'
-import rootConfig from '../../eslint.config.js'
-
-export default [
- ...rootConfig,
- {
- ...pluginReact.configs.recommended,
- files: ['**/*.{ts,tsx}'],
- },
- {
- plugins: {
- 'react-hooks': pluginReactHooks,
- },
- rules: {
- '@eslint-react/no-unstable-context-value': 'off',
- '@eslint-react/no-unstable-default-props': 'off',
- '@eslint-react/dom/no-missing-button-type': 'off',
- 'react-hooks/exhaustive-deps': 'error',
- 'react-hooks/rules-of-hooks': 'error',
- },
- },
- {
- files: ['**/__tests__/**'],
- rules: {
- '@typescript-eslint/no-unnecessary-condition': 'off',
- },
- },
-]
diff --git a/packages/react-start-config/package.json b/packages/react-start-config/package.json
deleted file mode 100644
index 23efef7bfd..0000000000
--- a/packages/react-start-config/package.json
+++ /dev/null
@@ -1,73 +0,0 @@
-{
- "name": "@tanstack/react-start-config",
- "version": "1.120.3",
- "description": "Modern and scalable routing for React applications",
- "author": "Tanner Linsley",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "https://github.com/TanStack/router.git",
- "directory": "packages/react-start-config"
- },
- "homepage": "https://tanstack.com/start",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "keywords": [
- "react",
- "location",
- "router",
- "routing",
- "async",
- "async router",
- "typescript"
- ],
- "scripts": {
- "clean": "rimraf ./dist && rimraf ./coverage",
- "build": "tsc",
- "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
- "test:unit": "exit 0;vitest",
- "test:eslint": "eslint ./src",
- "test:types": "exit 0; vitest"
- },
- "type": "module",
- "types": "dist/esm/index.d.ts",
- "exports": {
- ".": {
- "import": {
- "types": "./dist/esm/index.d.ts",
- "default": "./dist/esm/index.js"
- }
- },
- "./package.json": "./package.json"
- },
- "sideEffects": false,
- "files": [
- "dist",
- "src"
- ],
- "engines": {
- "node": ">=12"
- },
- "dependencies": {
- "@tanstack/router-core": "workspace:^",
- "@tanstack/router-generator": "workspace:^",
- "@tanstack/router-plugin": "workspace:^",
- "@tanstack/server-functions-plugin": "workspace:^",
- "@tanstack/react-start-plugin": "workspace:^",
- "@tanstack/start-server-functions-handler": "workspace:^",
- "@vitejs/plugin-react": "^4.3.4",
- "import-meta-resolve": "^4.1.0",
- "nitropack": "^2.10.4",
- "ofetch": "^1.4.1",
- "vite": "^6.1.0",
- "vinxi": "0.5.3",
- "zod": "^3.24.2"
- },
- "peerDependencies": {
- "react": ">=18.0.0 || >=19.0.0",
- "react-dom": ">=18.0.0 || >=19.0.0",
- "vite": "^6.0.0"
- }
-}
diff --git a/packages/react-start-config/src/index.ts b/packages/react-start-config/src/index.ts
deleted file mode 100644
index ac30bf2140..0000000000
--- a/packages/react-start-config/src/index.ts
+++ /dev/null
@@ -1,663 +0,0 @@
-import path from 'node:path'
-import { existsSync, readFileSync } from 'node:fs'
-import { readFile } from 'node:fs/promises'
-import { fileURLToPath } from 'node:url'
-import viteReact from '@vitejs/plugin-react'
-import { resolve } from 'import-meta-resolve'
-import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
-import { getConfig } from '@tanstack/router-generator'
-import { createApp } from 'vinxi'
-import { config } from 'vinxi/plugins/config'
-// // @ts-expect-error
-// import { serverComponents } from '@vinxi/server-components/plugin'
-import { createTanStackServerFnPlugin } from '@tanstack/server-functions-plugin'
-import { createTanStackStartPlugin } from '@tanstack/react-start-plugin'
-import { createFetch } from 'ofetch'
-import { createNitro } from 'nitropack'
-import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js'
-import {
- checkDeploymentPresetInput,
- getUserViteConfig,
- inlineConfigSchema,
- serverSchema,
-} from './schema.js'
-import type { configSchema } from '@tanstack/router-generator'
-import type { z } from 'zod'
-import type {
- TanStackStartInputConfig,
- TanStackStartOutputConfig,
-} from './schema.js'
-import type { App as VinxiApp } from 'vinxi'
-import type { Manifest } from '@tanstack/router-core'
-import type * as vite from 'vite'
-
-export type {
- TanStackStartInputConfig,
- TanStackStartOutputConfig,
-} from './schema.js'
-
-function setTsrDefaults(config: TanStackStartOutputConfig['tsr']) {
- // Normally these are `./src/___`, but we're using `./app/___` for Start stuff
- const appDirectory = config?.appDirectory ?? './app'
- return {
- ...config,
- appDirectory: config?.appDirectory ?? appDirectory,
- routesDirectory:
- config?.routesDirectory ?? path.join(appDirectory, 'routes'),
- generatedRouteTree:
- config?.generatedRouteTree ?? path.join(appDirectory, 'routeTree.gen.ts'),
- }
-}
-
-function mergeSsrOptions(options: Array) {
- let ssrOptions: vite.SSROptions = {}
- let noExternal: vite.SSROptions['noExternal'] = []
- for (const option of options) {
- if (!option) {
- continue
- }
-
- if (option.noExternal) {
- if (option.noExternal === true) {
- noExternal = true
- } else if (noExternal !== true) {
- if (Array.isArray(option.noExternal)) {
- noExternal.push(...option.noExternal)
- } else {
- noExternal.push(option.noExternal)
- }
- }
- }
-
- ssrOptions = {
- ...ssrOptions,
- ...option,
- noExternal,
- }
- }
-
- return ssrOptions
-}
-
-export async function defineConfig(
- inlineConfig: TanStackStartInputConfig = {},
-): Promise {
- const opts = inlineConfigSchema.parse(inlineConfig)
-
- const { preset: configDeploymentPreset, ...serverOptions } =
- serverSchema.parse(opts.server || {})
-
- const deploymentPreset = checkDeploymentPresetInput(configDeploymentPreset)
- const tsr = setTsrDefaults(opts.tsr)
- const tsrConfig = getConfig(tsr)
-
- const appDirectory = tsr.appDirectory
- const publicDir = opts.routers?.public?.dir || './public'
-
- const publicBase = opts.routers?.public?.base || '/'
- const clientBase = opts.routers?.client?.base || '/_build'
- const apiBase = opts.tsr?.apiBase || '/api'
- const serverBase = opts.routers?.server?.base || '/_server'
-
- const apiMiddleware = opts.routers?.api?.middleware || undefined
- const serverMiddleware = opts.routers?.server?.middleware || undefined
- const ssrMiddleware = opts.routers?.ssr?.middleware || undefined
-
- const clientEntry =
- opts.routers?.client?.entry || path.join(appDirectory, 'client.tsx')
- const ssrEntry =
- opts.routers?.ssr?.entry || path.join(appDirectory, 'ssr.tsx')
- const apiEntry = opts.routers?.api?.entry || path.join(appDirectory, 'api.ts')
- const globalMiddlewareEntry =
- opts.routers?.server?.globalMiddlewareEntry ||
- path.join(appDirectory, 'global-middleware.ts')
- const apiEntryExists = existsSync(apiEntry)
-
- const viteConfig = getUserViteConfig(opts.vite)
-
- const TanStackServerFnsPlugin = createTanStackServerFnPlugin({
- // This is the ID that will be available to look up and import
- // our server function manifest and resolve its module
- manifestVirtualImportId: 'tsr:server-fn-manifest',
- client: {
- getRuntimeCode: () =>
- `import { createClientRpc } from '@tanstack/react-start/server-functions-client'`,
- replacer: (opts) =>
- `createClientRpc('${opts.functionId}', '${serverBase}')`,
- },
- ssr: {
- getRuntimeCode: () =>
- `import { createSsrRpc } from '@tanstack/react-start/server-functions-ssr'`,
- replacer: (opts) => `createSsrRpc('${opts.functionId}', '${serverBase}')`,
- },
- server: {
- getRuntimeCode: () =>
- `import { createServerRpc } from '@tanstack/react-start/server-functions-server'`,
- replacer: (opts) =>
- `createServerRpc('${opts.functionId}', '${serverBase}', ${opts.fn})`,
- },
- })
-
- const TanStackStartPlugin = createTanStackStartPlugin({
- globalMiddlewareEntry,
- })
-
- // Create a dummy nitro app to get the resolved public output path
- const dummyNitroApp = await createNitro({
- preset: deploymentPreset,
- compatibilityDate: '2024-12-01',
- })
-
- const nitroOutputPublicDir = dummyNitroApp.options.output.publicDir
- await dummyNitroApp.close()
-
- let vinxiApp = createApp({
- server: {
- ...serverOptions,
- preset: deploymentPreset,
- experimental: {
- ...serverOptions.experimental,
- asyncContext: true,
- },
- },
- routers: [
- {
- name: 'public',
- type: 'static',
- dir: publicDir,
- base: publicBase,
- },
- {
- name: 'client',
- type: 'client',
- target: 'browser',
- handler: clientEntry,
- base: clientBase,
- // @ts-expect-error
- build: {
- sourcemap: true,
- },
- plugins: () => {
- const routerType = 'client'
- const clientViteConfig = getUserViteConfig(
- opts.routers?.[routerType]?.vite,
- )
-
- return [
- config('tss-vite-config-client', {
- ...viteConfig.userConfig,
- ...clientViteConfig.userConfig,
- define: {
- ...(viteConfig.userConfig.define || {}),
- ...(clientViteConfig.userConfig.define || {}),
- ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase),
- ...injectDefineEnv('TSS_CLIENT_BASE', clientBase),
- ...injectDefineEnv('TSS_API_BASE', apiBase),
- ...injectDefineEnv(
- 'TSS_OUTPUT_PUBLIC_DIR',
- nitroOutputPublicDir,
- ),
- },
- ssr: mergeSsrOptions([
- viteConfig.userConfig.ssr,
- clientViteConfig.userConfig.ssr,
- {
- noExternal,
- },
- ]),
- optimizeDeps: {
- entries: [],
- ...(viteConfig.userConfig.optimizeDeps || {}),
- ...(clientViteConfig.userConfig.optimizeDeps || {}),
- },
- }),
- TanStackRouterVite({
- ...tsrConfig,
- enableRouteGeneration: true,
- autoCodeSplitting: true,
- __enableAPIRoutesGeneration: true,
- experimental: {
- ...tsrConfig.experimental,
- },
- }),
- TanStackStartPlugin.client,
- TanStackServerFnsPlugin.client,
- ...(viteConfig.plugins || []),
- ...(clientViteConfig.plugins || []),
- viteReact(opts.react),
- // TODO: RSCS - enable this
- // serverComponents.client(),
- ]
- },
- },
- {
- name: 'ssr',
- type: 'http',
- target: 'server',
- handler: ssrEntry,
- middleware: ssrMiddleware,
- // @ts-expect-error
- link: {
- client: 'client',
- },
- plugins: () => {
- const routerType = 'ssr'
- const ssrViteConfig = getUserViteConfig(
- opts.routers?.[routerType]?.vite,
- )
-
- return [
- config('tss-vite-config-ssr', {
- ...viteConfig.userConfig,
- ...ssrViteConfig.userConfig,
- define: {
- ...(viteConfig.userConfig.define || {}),
- ...(ssrViteConfig.userConfig.define || {}),
- ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase),
- ...injectDefineEnv('TSS_CLIENT_BASE', clientBase),
- ...injectDefineEnv('TSS_API_BASE', apiBase),
- ...injectDefineEnv(
- 'TSS_OUTPUT_PUBLIC_DIR',
- nitroOutputPublicDir,
- ),
- },
- ssr: mergeSsrOptions([
- viteConfig.userConfig.ssr,
- ssrViteConfig.userConfig.ssr,
- {
- noExternal,
- external: ['@vinxi/react-server-dom/client'],
- },
- ]),
- optimizeDeps: {
- entries: [],
- ...(viteConfig.userConfig.optimizeDeps || {}),
- ...(ssrViteConfig.userConfig.optimizeDeps || {}),
- },
- }),
- TanStackRouterVite({
- ...tsrConfig,
- enableRouteGeneration: false,
- autoCodeSplitting: true,
- __enableAPIRoutesGeneration: true,
- experimental: {
- ...tsrConfig.experimental,
- },
- }),
- TanStackStartPlugin.ssr,
- TanStackServerFnsPlugin.ssr,
- tsrRoutesManifest({
- tsrConfig,
- clientBase,
- }),
- ...(getUserViteConfig(opts.vite).plugins || []),
- ...(getUserViteConfig(opts.routers?.ssr?.vite).plugins || []),
- viteReact(opts.react),
- ]
- },
- },
- {
- name: 'server',
- type: 'http',
- target: 'server',
- base: serverBase,
- middleware: serverMiddleware,
- // TODO: RSCS - enable this
- // worker: true,
- handler: importToProjectRelative(
- '@tanstack/start-server-functions-handler',
- ),
- plugins: () => {
- const routerType = 'server'
- const serverViteConfig = getUserViteConfig(
- opts.routers?.[routerType]?.vite,
- )
-
- return [
- config('tss-vite-config-server', {
- ...viteConfig.userConfig,
- ...serverViteConfig.userConfig,
- define: {
- ...(viteConfig.userConfig.define || {}),
- ...(serverViteConfig.userConfig.define || {}),
- ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase),
- ...injectDefineEnv('TSS_CLIENT_BASE', clientBase),
- ...injectDefineEnv('TSS_API_BASE', apiBase),
- ...injectDefineEnv('TSS_SERVER_FN_BASE', serverBase),
- ...injectDefineEnv(
- 'TSS_OUTPUT_PUBLIC_DIR',
- nitroOutputPublicDir,
- ),
- },
- ssr: mergeSsrOptions([
- viteConfig.userConfig.ssr,
- serverViteConfig.userConfig.ssr,
- {
- noExternal,
- },
- ]),
- optimizeDeps: {
- entries: [],
- ...(viteConfig.userConfig.optimizeDeps || {}),
- ...(serverViteConfig.userConfig.optimizeDeps || {}),
- },
- }),
- TanStackRouterVite({
- ...tsrConfig,
- enableRouteGeneration: false,
- autoCodeSplitting: true,
- __enableAPIRoutesGeneration: true,
- experimental: {
- ...tsrConfig.experimental,
- },
- }),
- TanStackStartPlugin.server,
- TanStackServerFnsPlugin.server,
- // TODO: RSCS - remove this
- // resolve: {
- // conditions: [],
- // },
- // TODO: RSCs - add this
- // serverComponents.serverActions({
- // resolve: {
- // conditions: [
- // 'react-server',
- // // 'node',
- // 'import',
- // process.env.NODE_ENV,
- // ],
- // },
- // runtime: '@vinxi/react-server-dom/runtime',
- // transpileDeps: ['react', 'react-dom', '@vinxi/react-server-dom'],
- // }),
- ...(viteConfig.plugins || []),
- ...(serverViteConfig.plugins || []),
- ]
- },
- },
- ],
- })
-
- const noExternal = [
- '@tanstack/start',
- '@tanstack/react-start',
- '@tanstack/react-start/server',
- '@tanstack/react-start-client',
- '@tanstack/react-start-server',
- '@tanstack/start-server-functions-fetcher',
- '@tanstack/start-server-functions-handler',
- '@tanstack/start-server-functions-client',
- '@tanstack/start-server-functions-ssr',
- '@tanstack/start-server-functions-server',
- '@tanstack/react-start-router-manifest',
- '@tanstack/react-start-config',
- '@tanstack/start-api-routes',
- '@tanstack/server-functions-plugin',
- 'tsr:routes-manifest',
- 'tsr:server-fn-manifest',
- ]
-
- // If API routes handler exists, add a router for it
- if (apiEntryExists) {
- vinxiApp = vinxiApp.addRouter({
- name: 'api',
- type: 'http',
- target: 'server',
- base: apiBase,
- handler: apiEntry,
- middleware: apiMiddleware,
- routes: tanstackStartVinxiFileRouter({ tsrConfig, apiBase }),
- plugins: () => {
- const viteConfig = getUserViteConfig(opts.vite)
- const apiViteConfig = getUserViteConfig(opts.routers?.api?.vite)
-
- return [
- config('tsr-vite-config-api', {
- ...viteConfig.userConfig,
- ...apiViteConfig.userConfig,
- ssr: mergeSsrOptions([
- viteConfig.userConfig.ssr,
- apiViteConfig.userConfig.ssr,
- {
- noExternal,
- },
- ]),
- optimizeDeps: {
- entries: [],
- ...(viteConfig.userConfig.optimizeDeps || {}),
- ...(apiViteConfig.userConfig.optimizeDeps || {}),
- },
- define: {
- ...(viteConfig.userConfig.define || {}),
- ...(apiViteConfig.userConfig.define || {}),
- ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase),
- ...injectDefineEnv('TSS_CLIENT_BASE', clientBase),
- ...injectDefineEnv('TSS_API_BASE', apiBase),
- ...injectDefineEnv('TSS_OUTPUT_PUBLIC_DIR', nitroOutputPublicDir),
- },
- }),
- TanStackRouterVite({
- ...tsrConfig,
- enableRouteGeneration: false,
- autoCodeSplitting: true,
- __enableAPIRoutesGeneration: true,
- experimental: {
- ...tsrConfig.experimental,
- },
- }),
- ...(viteConfig.plugins || []),
- ...(apiViteConfig.plugins || []),
- ]
- },
- })
- }
-
- // Because Vinxi doesn't use the normal nitro dev server, it doesn't
- // supply $fetch during dev. We need to hook into the dev server creation,
- // nab the proper utils from the custom nitro instance that is used
- // during dev and supply the $fetch to app.
- // Hopefully and likely, this will just get removed when we move to
- // Nitro directly.
- vinxiApp.hooks.hook('app:dev:nitro:config', (devServer) => {
- vinxiApp.hooks.hook(
- 'app:dev:server:created',
- ({ devApp: { localFetch } }) => {
- const $fetch = createFetch({
- fetch: localFetch,
- defaults: {
- baseURL: devServer.nitro.options.runtimeConfig.app.baseURL,
- },
- })
-
- // @ts-expect-error
- globalThis.$fetch = $fetch
- },
- )
- })
-
- return vinxiApp
-}
-
-function importToProjectRelative(p: string) {
- const resolved = fileURLToPath(resolve(p, import.meta.url))
-
- const relative = path.relative(process.cwd(), resolved)
-
- return relative
-}
-
-function tsrRoutesManifest(opts: {
- tsrConfig: z.infer
- clientBase: string
-}): vite.Plugin {
- let config: vite.ResolvedConfig
-
- return {
- name: 'tsr-routes-manifest',
- configResolved(resolvedConfig) {
- config = resolvedConfig
- },
- resolveId(id) {
- if (id === 'tsr:routes-manifest') {
- return id
- }
- return
- },
- async load(id) {
- if (id === 'tsr:routes-manifest') {
- // If we're in development, return a dummy manifest
-
- if (config.command === 'serve') {
- return `export default () => ({
- routes: {}
- })`
- }
-
- const clientViteManifestPath = path.resolve(
- config.build.outDir,
- `../client/${opts.clientBase}/.vite/manifest.json`,
- )
-
- type ViteManifest = Record<
- string,
- {
- file: string
- isEntry: boolean
- imports: Array
- }
- >
-
- let manifest: ViteManifest
- try {
- manifest = JSON.parse(await readFile(clientViteManifestPath, 'utf-8'))
- } catch (err) {
- console.error(err)
- throw new Error(
- `Could not find the production client vite manifest at '${clientViteManifestPath}'!`,
- )
- }
-
- const routeTreePath = path.resolve(opts.tsrConfig.generatedRouteTree)
-
- let routeTreeContent: string
- try {
- routeTreeContent = readFileSync(routeTreePath, 'utf-8')
- } catch (err) {
- console.error(err)
- throw new Error(
- `Could not find the generated route tree at '${routeTreePath}'!`,
- )
- }
-
- // Extract the routesManifest JSON from the route tree file.
- // It's located between the /* ROUTE_MANIFEST_START and ROUTE_MANIFEST_END */ comment block.
-
- const routerManifest = JSON.parse(
- routeTreeContent.match(
- /\/\* ROUTE_MANIFEST_START([\s\S]*?)ROUTE_MANIFEST_END \*\//,
- )?.[1] || '{ routes: {} }',
- ) as Manifest
-
- const routes = routerManifest.routes
-
- let entryFile:
- | {
- file: string
- imports: Array
- }
- | undefined
-
- const filesByRouteFilePath: ViteManifest = Object.fromEntries(
- Object.entries(manifest).map(([k, v]) => {
- if (v.isEntry) {
- entryFile = v
- }
-
- const rPath = k.split('?')[0]
-
- return [rPath, v]
- }, {}),
- )
-
- // Add preloads to the routes from the vite manifest
- Object.entries(routes).forEach(([k, v]) => {
- const file =
- filesByRouteFilePath[
- path.join(opts.tsrConfig.routesDirectory, v.filePath as string)
- ]
-
- if (file) {
- const preloads = file.imports.map((d) =>
- path.join(opts.clientBase, manifest[d]!.file),
- )
-
- preloads.unshift(path.join(opts.clientBase, file.file))
-
- routes[k] = {
- ...v,
- preloads,
- }
- }
- })
-
- if (entryFile) {
- routes.__root__!.preloads = [
- path.join(opts.clientBase, entryFile.file),
- ...entryFile.imports.map((d) =>
- path.join(opts.clientBase, manifest[d]!.file),
- ),
- ]
- }
-
- const recurseRoute = (
- route: {
- preloads?: Array
- children?: Array
- },
- seenPreloads = {} as Record,
- ) => {
- route.preloads = route.preloads?.filter((preload) => {
- if (seenPreloads[preload]) {
- return false
- }
- seenPreloads[preload] = true
- return true
- })
-
- if (route.children) {
- route.children.forEach((child) => {
- const childRoute = routes[child]!
- recurseRoute(childRoute, { ...seenPreloads })
- })
- }
- }
-
- // @ts-expect-error
- recurseRoute(routes.__root__)
-
- const routesManifest = {
- routes,
- }
-
- if (process.env.TSR_VITE_DEBUG) {
- console.info(
- 'Routes Manifest: \n' + JSON.stringify(routesManifest, null, 2),
- )
- }
-
- return `export default () => (${JSON.stringify(routesManifest)})`
- }
- return
- },
- }
-}
-
-function injectDefineEnv(
- key: TKey,
- value: TValue,
-): { [P in `process.env.${TKey}` | `import.meta.env.${TKey}`]: TValue } {
- return {
- [`process.env.${key}`]: JSON.stringify(value),
- [`import.meta.env.${key}`]: JSON.stringify(value),
- } as { [P in `process.env.${TKey}` | `import.meta.env.${TKey}`]: TValue }
-}
diff --git a/packages/react-start-config/src/schema.ts b/packages/react-start-config/src/schema.ts
deleted file mode 100644
index 4529483793..0000000000
--- a/packages/react-start-config/src/schema.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-import { configSchema } from '@tanstack/router-generator'
-import { z } from 'zod'
-import type { PluginOption } from 'vite'
-import type { AppOptions as VinxiAppOptions } from 'vinxi'
-import type { NitroOptions } from 'nitropack'
-import type { Options as ViteReactOptions } from '@vitejs/plugin-react'
-import type { CustomizableConfig } from 'vinxi/dist/types/lib/vite-dev'
-
-type StartUserViteConfig = CustomizableConfig | (() => CustomizableConfig)
-
-export function getUserViteConfig(config?: StartUserViteConfig): {
- plugins: Array | undefined
- userConfig: CustomizableConfig
-} {
- const { plugins, ...userConfig } =
- typeof config === 'function' ? config() : { ...config }
- return { plugins, userConfig }
-}
-
-/**
- * Not all the deployment presets are fully functional or tested.
- * @see https://github.com/TanStack/router/pull/2002
- */
-const vinxiDeploymentPresets = [
- 'alwaysdata', // untested
- 'aws-amplify', // untested
- 'aws-lambda', // untested
- 'azure', // untested
- 'azure-functions', // untested
- 'base-worker', // untested
- 'bun', // ✅ working
- 'cleavr', // untested
- 'cli', // untested
- 'cloudflare', // untested
- 'cloudflare-module', // untested
- 'cloudflare-pages', // ✅ working
- 'cloudflare-pages-static', // untested
- 'deno', // untested
- 'deno-deploy', // untested
- 'deno-server', // untested
- 'digital-ocean', // untested
- 'edgio', // untested
- 'firebase', // untested
- 'flight-control', // untested
- 'github-pages', // untested
- 'heroku', // untested
- 'iis', // untested
- 'iis-handler', // untested
- 'iis-node', // untested
- 'koyeb', // untested
- 'layer0', // untested
- 'netlify', // ✅ working
- 'netlify-builder', // untested
- 'netlify-edge', // untested
- 'netlify-static', // untested
- 'nitro-dev', // untested
- 'nitro-prerender', // untested
- 'node', // partially working
- 'node-cluster', // untested
- 'node-server', // ✅ working
- 'platform-sh', // untested
- 'service-worker', // untested
- 'static', // 🟧 partially working
- 'stormkit', // untested
- 'vercel', // ✅ working
- 'vercel-edge', // untested
- 'vercel-static', // untested
- 'winterjs', // untested
- 'zeabur', // untested
- 'zeabur-static', // untested
-] as const
-
-type DeploymentPreset = (typeof vinxiDeploymentPresets)[number] | (string & {})
-
-const testedDeploymentPresets: Array = [
- 'bun',
- 'netlify',
- 'vercel',
- 'cloudflare-pages',
- 'node-server',
-]
-
-export function checkDeploymentPresetInput(
- preset?: string,
-): DeploymentPreset | undefined {
- if (preset) {
- if (!vinxiDeploymentPresets.includes(preset as any)) {
- console.warn(
- `Invalid deployment preset "${preset}". Available presets are: ${vinxiDeploymentPresets
- .map((p) => `"${p}"`)
- .join(', ')}.`,
- )
- }
-
- if (!testedDeploymentPresets.includes(preset as any)) {
- console.warn(
- `The deployment preset '${preset}' is not fully supported yet and may not work as expected.`,
- )
- }
- }
-
- return preset
-}
-
-type HTTPSOptions = {
- cert?: string
- key?: string
- pfx?: string
- passphrase?: string
- validityDays?: number
- domains?: Array
-}
-
-type ServerOptions_ = VinxiAppOptions['server'] & {
- https?: boolean | HTTPSOptions
-}
-
-type ServerOptions = {
- [K in keyof ServerOptions_]: ServerOptions_[K]
-}
-
-export const serverSchema = z
- .object({
- routeRules: z.custom().optional(),
- preset: z.custom().optional(),
- static: z.boolean().optional(),
- prerender: z
- .object({
- routes: z.array(z.string()),
- ignore: z
- .array(
- z.custom<
- string | RegExp | ((path: string) => undefined | null | boolean)
- >(),
- )
- .optional(),
- crawlLinks: z.boolean().optional(),
- })
- .optional(),
- })
- .and(z.custom())
-
-const viteSchema = z.custom()
-
-const viteReactSchema = z.custom()
-
-const routersSchema = z.object({
- ssr: z
- .object({
- entry: z.string().optional(),
- middleware: z.string().optional(),
- vite: viteSchema.optional(),
- })
- .optional(),
- client: z
- .object({
- entry: z.string().optional(),
- base: z.string().optional(),
- vite: viteSchema.optional(),
- })
- .optional(),
- server: z
- .object({
- base: z.string().optional(),
- globalMiddlewareEntry: z.string().optional(),
- middleware: z.string().optional(),
- vite: viteSchema.optional(),
- })
- .optional(),
- api: z
- .object({
- entry: z.string().optional(),
- middleware: z.string().optional(),
- vite: viteSchema.optional(),
- })
- .optional(),
- public: z
- .object({
- dir: z.string().optional(),
- base: z.string().optional(),
- })
- .optional(),
-})
-
-const tsrConfig = configSchema.partial().extend({
- appDirectory: z.string().optional(),
-})
-
-export const inlineConfigSchema = z.object({
- react: viteReactSchema.optional(),
- vite: viteSchema.optional(),
- tsr: tsrConfig.optional(),
- routers: routersSchema.optional(),
- server: serverSchema.optional(),
-})
-
-export type TanStackStartInputConfig = z.input
-export type TanStackStartOutputConfig = z.infer
diff --git a/packages/react-start-config/src/vinxi-file-router.ts b/packages/react-start-config/src/vinxi-file-router.ts
deleted file mode 100644
index 9e6d829d1b..0000000000
--- a/packages/react-start-config/src/vinxi-file-router.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import {
- BaseFileSystemRouter as VinxiBaseFileSystemRouter,
- analyzeModule as vinxiFsRouterAnalyzeModule,
- cleanPath as vinxiFsRouterCleanPath,
-} from 'vinxi/fs-router'
-import {
- CONSTANTS as GENERATOR_CONSTANTS,
- startAPIRouteSegmentsFromTSRFilePath,
-} from '@tanstack/router-generator'
-import type { configSchema } from '@tanstack/router-generator'
-import type {
- AppOptions as VinxiAppOptions,
- RouterSchemaInput as VinxiRouterSchemaInput,
-} from 'vinxi'
-import type { z } from 'zod'
-
-export function tanstackStartVinxiFileRouter(opts: {
- tsrConfig: z.infer
- apiBase: string
-}) {
- const apiBaseSegment = opts.apiBase.split('/').filter(Boolean).join('/')
- const isAPIPath = new RegExp(`/${apiBaseSegment}/`)
-
- return function (router: VinxiRouterSchemaInput, app: VinxiAppOptions) {
- // Our own custom File Router that extends the VinxiBaseFileSystemRouter
- // for splitting the API routes into its own "bundle"
- // and adding the $APIRoute metadata to the route object
- // This could be customized in future to support more complex splits
- class TanStackStartFsRouter extends VinxiBaseFileSystemRouter {
- toPath(src: string): string {
- const inputPath = vinxiFsRouterCleanPath(src, this.config)
-
- const segments = startAPIRouteSegmentsFromTSRFilePath(
- inputPath,
- opts.tsrConfig,
- )
-
- const pathname = segments
- .map((part) => {
- if (part.type === 'splat') {
- return `*splat`
- }
-
- if (part.type === 'param') {
- return `:${part.value}?`
- }
-
- return part.value
- })
- .join('/')
-
- return pathname.length > 0 ? `/${pathname}` : '/'
- }
-
- toRoute(src: string) {
- const webPath = this.toPath(src)
-
- const [_, exports] = vinxiFsRouterAnalyzeModule(src)
-
- const hasAPIRoute = exports.find(
- (exp) => exp.n === GENERATOR_CONSTANTS.APIRouteExportVariable,
- )
-
- return {
- path: webPath,
- filePath: src,
- $APIRoute:
- isAPIPath.test(webPath) && hasAPIRoute
- ? {
- src,
- pick: [GENERATOR_CONSTANTS.APIRouteExportVariable],
- }
- : undefined,
- }
- }
- }
-
- return new TanStackStartFsRouter(
- {
- dir: opts.tsrConfig.routesDirectory,
- extensions: ['js', 'jsx', 'ts', 'tsx'],
- },
- router,
- app,
- )
- }
-}
diff --git a/packages/react-start-config/tsconfig.json b/packages/react-start-config/tsconfig.json
deleted file mode 100644
index 940a9cce0a..0000000000
--- a/packages/react-start-config/tsconfig.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "include": ["src/index.ts"],
- "compilerOptions": {
- "rootDir": "src",
- "outDir": "dist/esm",
- "target": "esnext",
- "noEmit": false
- }
-}
diff --git a/packages/react-start-plugin/README.md b/packages/react-start-plugin/README.md
index 90488bbcd8..45e57a4d5b 100644
--- a/packages/react-start-plugin/README.md
+++ b/packages/react-start-plugin/README.md
@@ -1,5 +1,9 @@
-# TanStack Start Vite Plugin
+# TanStack React Start - Plugin
-See https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing
+This package is not meant to be used directly. It is a dependency of [`@tanstack/react-start`](https://www.npmjs.com/package/@tanstack/react-start).
+
+TanStack React Start is a fullstack-framework made for SSR, Streaming, Server Functions, API Routes, bundling and more powered by [TanStack Router](https://tanstack.com/router).
+
+Head over to [tanstack.com/start](https://tanstack.com/start) for more information about getting started.
diff --git a/packages/react-start-plugin/package.json b/packages/react-start-plugin/package.json
index f13d4951cc..d67c053e03 100644
--- a/packages/react-start-plugin/package.json
+++ b/packages/react-start-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start-plugin",
- "version": "1.115.0",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -27,7 +27,7 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"clean:snapshots": "rimraf **/*snapshot* --glob",
"test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
- "test:unit": "vitest",
+ "test:unit": "exit 0; vitest",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
"test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
@@ -65,22 +65,16 @@
"node": ">=12"
},
"dependencies": {
- "@babel/code-frame": "7.26.2",
- "@babel/core": "^7.26.8",
- "@babel/plugin-syntax-jsx": "^7.25.9",
- "@babel/plugin-syntax-typescript": "^7.25.9",
- "@babel/template": "^7.26.8",
- "@babel/traverse": "^7.26.8",
- "@babel/types": "^7.26.8",
"@tanstack/router-utils": "workspace:^",
- "babel-dead-code-elimination": "^1.0.10",
- "tiny-invariant": "^1.3.3",
- "vite": "6.1.4"
+ "@tanstack/start-plugin-core": "workspace:^",
+ "zod": "^3.24.2"
},
"devDependencies": {
- "@types/babel__code-frame": "^7.0.6",
- "@types/babel__core": "^7.20.5",
- "@types/babel__template": "^7.4.4",
- "@types/babel__traverse": "^7.20.6"
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.0"
+ },
+ "peerDependencies": {
+ "@vitejs/plugin-react": ">=4.3.4",
+ "vite": ">=6.0.0"
}
}
diff --git a/packages/react-start-plugin/src/compilers.ts b/packages/react-start-plugin/src/compilers.ts
deleted file mode 100644
index ca806fc991..0000000000
--- a/packages/react-start-plugin/src/compilers.ts
+++ /dev/null
@@ -1,584 +0,0 @@
-import * as babel from '@babel/core'
-import * as t from '@babel/types'
-import { codeFrameColumns } from '@babel/code-frame'
-import {
- deadCodeElimination,
- findReferencedIdentifiers,
-} from 'babel-dead-code-elimination'
-import { generateFromAst, parseAst } from '@tanstack/router-utils'
-import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils'
-
-// build these once and reuse them
-const handleServerOnlyCallExpression =
- buildEnvOnlyCallExpressionHandler('server')
-const handleClientOnlyCallExpression =
- buildEnvOnlyCallExpressionHandler('client')
-
-type CompileOptions = ParseAstOptions & {
- env: 'server' | 'client' | 'ssr'
- dce?: boolean
-}
-
-type IdentifierConfig = {
- name: string
- type: 'ImportSpecifier' | 'ImportNamespaceSpecifier'
- namespaceId: string
- handleCallExpression: (
- path: babel.NodePath,
- opts: CompileOptions,
- ) => void
- paths: Array
-}
-
-export function compileStartOutput(opts: CompileOptions): GeneratorResult {
- const ast = parseAst(opts)
-
- const doDce = opts.dce ?? true
- // find referenced identifiers *before* we transform anything
- const refIdents = doDce ? findReferencedIdentifiers(ast) : undefined
-
- babel.traverse(ast, {
- Program: {
- enter(programPath) {
- const identifiers: {
- createServerFn: IdentifierConfig
- createMiddleware: IdentifierConfig
- serverOnly: IdentifierConfig
- clientOnly: IdentifierConfig
- createIsomorphicFn: IdentifierConfig
- } = {
- createServerFn: {
- name: 'createServerFn',
- type: 'ImportSpecifier',
- namespaceId: '',
- handleCallExpression: handleCreateServerFnCallExpression,
- paths: [],
- },
- createMiddleware: {
- name: 'createMiddleware',
- type: 'ImportSpecifier',
- namespaceId: '',
- handleCallExpression: handleCreateMiddlewareCallExpression,
- paths: [],
- },
- serverOnly: {
- name: 'serverOnly',
- type: 'ImportSpecifier',
- namespaceId: '',
- handleCallExpression: handleServerOnlyCallExpression,
- paths: [],
- },
- clientOnly: {
- name: 'clientOnly',
- type: 'ImportSpecifier',
- namespaceId: '',
- handleCallExpression: handleClientOnlyCallExpression,
- paths: [],
- },
- createIsomorphicFn: {
- name: 'createIsomorphicFn',
- type: 'ImportSpecifier',
- namespaceId: '',
- handleCallExpression: handleCreateIsomorphicFnCallExpression,
- paths: [],
- },
- }
-
- const identifierKeys = Object.keys(identifiers) as Array<
- keyof typeof identifiers
- >
-
- programPath.traverse({
- ImportDeclaration: (path) => {
- if (path.node.source.value !== '@tanstack/react-start') {
- return
- }
-
- // handle a destructured imports being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/react-start';"
- path.node.specifiers.forEach((specifier) => {
- identifierKeys.forEach((identifierKey) => {
- const identifier = identifiers[identifierKey]
-
- if (
- specifier.type === 'ImportSpecifier' &&
- specifier.imported.type === 'Identifier'
- ) {
- if (specifier.imported.name === identifierKey) {
- identifier.name = specifier.local.name
- identifier.type = 'ImportSpecifier'
- }
- }
-
- // handle namespace imports like "import * as TanStackStart from '@tanstack/react-start';"
- if (specifier.type === 'ImportNamespaceSpecifier') {
- identifier.type = 'ImportNamespaceSpecifier'
- identifier.namespaceId = specifier.local.name
- identifier.name = `${identifier.namespaceId}.${identifierKey}`
- }
- })
- })
- },
- CallExpression: (path) => {
- identifierKeys.forEach((identifierKey) => {
- // Check to see if the call expression is a call to the
- // identifiers[identifierKey].name
- if (
- t.isIdentifier(path.node.callee) &&
- path.node.callee.name === identifiers[identifierKey].name
- ) {
- // The identifier could be a call to the original function
- // in the source code. If this is case, we need to ignore it.
- // Check the scope to see if the identifier is a function declaration.
- // if it is, then we can ignore it.
-
- if (
- path.scope.getBinding(identifiers[identifierKey].name)?.path
- .node.type === 'FunctionDeclaration'
- ) {
- return
- }
-
- return identifiers[identifierKey].paths.push(path)
- }
-
- if (t.isMemberExpression(path.node.callee)) {
- if (
- t.isIdentifier(path.node.callee.object) &&
- t.isIdentifier(path.node.callee.property)
- ) {
- const callname = [
- path.node.callee.object.name,
- path.node.callee.property.name,
- ].join('.')
-
- if (callname === identifiers[identifierKey].name) {
- identifiers[identifierKey].paths.push(path)
- }
- }
- }
-
- return
- })
- },
- })
-
- identifierKeys.forEach((identifierKey) => {
- identifiers[identifierKey].paths.forEach((path) => {
- identifiers[identifierKey].handleCallExpression(
- path as babel.NodePath,
- opts,
- )
- })
- })
- },
- },
- })
-
- if (doDce) {
- deadCodeElimination(ast, refIdents)
- }
-
- return generateFromAst(ast, {
- sourceMaps: true,
- sourceFileName: opts.filename,
- filename: opts.filename,
- })
-}
-
-function handleCreateServerFnCallExpression(
- path: babel.NodePath,
- opts: ParseAstOptions,
-) {
- // The function is the 'fn' property of the object passed to createServerFn
-
- // const firstArg = path.node.arguments[0]
- // if (t.isObjectExpression(firstArg)) {
- // // Was called with some options
- // }
-
- // Traverse the member expression and find the call expressions for
- // the validator, handler, and middleware methods. Check to make sure they
- // are children of the createServerFn call expression.
-
- const calledOptions = path.node.arguments[0]
- ? (path.get('arguments.0') as babel.NodePath)
- : null
-
- const shouldValidateClient = !!calledOptions?.node.properties.find((prop) => {
- return (
- t.isObjectProperty(prop) &&
- t.isIdentifier(prop.key) &&
- prop.key.name === 'validateClient' &&
- t.isBooleanLiteral(prop.value) &&
- prop.value.value === true
- )
- })
-
- const callExpressionPaths = {
- middleware: null as babel.NodePath | null,
- validator: null as babel.NodePath | null,
- handler: null as babel.NodePath | null,
- }
-
- const validMethods = Object.keys(callExpressionPaths)
-
- const rootCallExpression = getRootCallExpression(path)
-
- // if (debug)
- // console.info(
- // 'Handling createServerFn call expression:',
- // rootCallExpression.toString(),
- // )
-
- // Check if the call is assigned to a variable
- if (!rootCallExpression.parentPath.isVariableDeclarator()) {
- throw new Error('createServerFn must be assigned to a variable!')
- }
-
- // Get the identifier name of the variable
- const variableDeclarator = rootCallExpression.parentPath.node
- const existingVariableName = (variableDeclarator.id as t.Identifier).name
-
- rootCallExpression.traverse({
- MemberExpression(memberExpressionPath) {
- if (t.isIdentifier(memberExpressionPath.node.property)) {
- const name = memberExpressionPath.node.property
- .name as keyof typeof callExpressionPaths
-
- if (
- validMethods.includes(name) &&
- memberExpressionPath.parentPath.isCallExpression()
- ) {
- callExpressionPaths[name] = memberExpressionPath.parentPath
- }
- }
- },
- })
-
- if (callExpressionPaths.validator) {
- const innerInputExpression = callExpressionPaths.validator.node.arguments[0]
-
- if (!innerInputExpression) {
- throw new Error(
- 'createServerFn().validator() must be called with a validator!',
- )
- }
-
- // If we're on the client, and we're not validating the client, remove the validator call expression
- if (
- opts.env === 'client' &&
- !shouldValidateClient &&
- t.isMemberExpression(callExpressionPaths.validator.node.callee)
- ) {
- callExpressionPaths.validator.replaceWith(
- callExpressionPaths.validator.node.callee.object,
- )
- }
- }
-
- // First, we need to move the handler function to a nested function call
- // that is applied to the arguments passed to the server function.
-
- const handlerFnPath = callExpressionPaths.handler?.get(
- 'arguments.0',
- ) as babel.NodePath
-
- if (!callExpressionPaths.handler || !handlerFnPath.node) {
- throw codeFrameError(
- opts.code,
- path.node.callee.loc!,
- `createServerFn must be called with a "handler" property!`,
- )
- }
-
- const handlerFn = handlerFnPath.node
-
- // So, the way we do this is we give the handler function a way
- // to access the serverFn ctx on the server via function scope.
- // The 'use server' extracted function will be called with the
- // payload from the client, then use the scoped serverFn ctx
- // to execute the handler function.
- // This way, we can do things like data and middleware validation
- // in the __execute function without having to AST transform the
- // handler function too much itself.
-
- // .handler((optsOut, ctx) => {
- // return ((optsIn) => {
- // 'use server'
- // ctx.__execute(handlerFn, optsIn)
- // })(optsOut)
- // })
-
- // If the handler function is an identifier and we're on the client, we need to
- // remove the bound function from the file.
- // If we're on the server, you can leave it, since it will get referenced
- // as a second argument.
-
- if (t.isIdentifier(handlerFn)) {
- if (opts.env === 'client' || opts.env === 'ssr') {
- // Find the binding for the handler function
- const binding = handlerFnPath.scope.getBinding(handlerFn.name)
- // Remove it
- if (binding) {
- binding.path.remove()
- }
- }
- // If the env is server, just leave it alone
- }
-
- handlerFnPath.replaceWith(
- t.arrowFunctionExpression(
- [t.identifier('opts'), t.identifier('signal')],
- t.blockStatement(
- // Everything in here is server-only, since the client
- // will strip out anything in the 'use server' directive.
- [
- t.returnStatement(
- t.callExpression(
- t.identifier(`${existingVariableName}.__executeServer`),
- [t.identifier('opts'), t.identifier('signal')],
- ),
- ),
- ],
- [t.directive(t.directiveLiteral('use server'))],
- ),
- ),
- )
-
- if (opts.env === 'server') {
- callExpressionPaths.handler.node.arguments.push(handlerFn)
- }
-}
-
-function handleCreateMiddlewareCallExpression(
- path: babel.NodePath,
- opts: ParseAstOptions,
-) {
- const rootCallExpression = getRootCallExpression(path)
-
- // if (debug)
- // console.info(
- // 'Handling createMiddleware call expression:',
- // rootCallExpression.toString(),
- // )
-
- const callExpressionPaths = {
- middleware: null as babel.NodePath | null,
- validator: null as babel.NodePath | null,
- client: null as babel.NodePath | null,
- server: null as babel.NodePath | null,
- }
-
- const validMethods = Object.keys(callExpressionPaths)
-
- rootCallExpression.traverse({
- MemberExpression(memberExpressionPath) {
- if (t.isIdentifier(memberExpressionPath.node.property)) {
- const name = memberExpressionPath.node.property
- .name as keyof typeof callExpressionPaths
-
- if (
- validMethods.includes(name) &&
- memberExpressionPath.parentPath.isCallExpression()
- ) {
- callExpressionPaths[name] = memberExpressionPath.parentPath
- }
- }
- },
- })
-
- if (callExpressionPaths.validator) {
- const innerInputExpression = callExpressionPaths.validator.node.arguments[0]
-
- if (!innerInputExpression) {
- throw new Error(
- 'createMiddleware().validator() must be called with a validator!',
- )
- }
-
- // If we're on the client or ssr, remove the validator call expression
- if (opts.env === 'client' || opts.env === 'ssr') {
- if (t.isMemberExpression(callExpressionPaths.validator.node.callee)) {
- callExpressionPaths.validator.replaceWith(
- callExpressionPaths.validator.node.callee.object,
- )
- }
- }
- }
-
- const serverFnPath = callExpressionPaths.server?.get(
- 'arguments.0',
- ) as babel.NodePath
-
- if (
- callExpressionPaths.server &&
- serverFnPath.node &&
- (opts.env === 'client' || opts.env === 'ssr')
- ) {
- // If we're on the client, remove the server call expression
- if (t.isMemberExpression(callExpressionPaths.server.node.callee)) {
- callExpressionPaths.server.replaceWith(
- callExpressionPaths.server.node.callee.object,
- )
- }
- }
-}
-
-function buildEnvOnlyCallExpressionHandler(env: 'client' | 'server') {
- return function envOnlyCallExpressionHandler(
- path: babel.NodePath,
- opts: ParseAstOptions,
- ) {
- // if (debug)
- // console.info(`Handling ${env}Only call expression:`, path.toString())
-
- const isEnvMatch =
- env === 'client'
- ? opts.env === 'client'
- : opts.env === 'server' || opts.env === 'ssr'
-
- if (isEnvMatch) {
- // extract the inner function from the call expression
- const innerInputExpression = path.node.arguments[0]
-
- if (!t.isExpression(innerInputExpression)) {
- throw new Error(
- `${env}Only() functions must be called with a function!`,
- )
- }
-
- path.replaceWith(innerInputExpression)
- return
- }
-
- // If we're on the wrong environment, replace the call expression
- // with a function that always throws an error.
- path.replaceWith(
- t.arrowFunctionExpression(
- [],
- t.blockStatement([
- t.throwStatement(
- t.newExpression(t.identifier('Error'), [
- t.stringLiteral(
- `${env}Only() functions can only be called on the ${env}!`,
- ),
- ]),
- ),
- ]),
- ),
- )
- }
-}
-
-function handleCreateIsomorphicFnCallExpression(
- path: babel.NodePath,
- opts: CompileOptions,
-) {
- const rootCallExpression = getRootCallExpression(path)
-
- // if (debug)
- // console.info(
- // 'Handling createIsomorphicFn call expression:',
- // rootCallExpression.toString(),
- // )
-
- const callExpressionPaths = {
- client: null as babel.NodePath | null,
- server: null as babel.NodePath | null,
- }
-
- const validMethods = Object.keys(callExpressionPaths)
-
- rootCallExpression.traverse({
- MemberExpression(memberExpressionPath) {
- if (t.isIdentifier(memberExpressionPath.node.property)) {
- const name = memberExpressionPath.node.property
- .name as keyof typeof callExpressionPaths
-
- if (
- validMethods.includes(name) &&
- memberExpressionPath.parentPath.isCallExpression()
- ) {
- callExpressionPaths[name] = memberExpressionPath.parentPath
- }
- }
- },
- })
-
- if (
- validMethods.every(
- (method) =>
- !callExpressionPaths[method as keyof typeof callExpressionPaths],
- )
- ) {
- const variableId = rootCallExpression.parentPath.isVariableDeclarator()
- ? rootCallExpression.parentPath.node.id
- : null
- console.warn(
- 'createIsomorphicFn called without a client or server implementation!',
- 'This will result in a no-op function.',
- 'Variable name:',
- t.isIdentifier(variableId) ? variableId.name : 'unknown',
- )
- }
-
- const resolvedEnv = opts.env === 'ssr' ? 'server' : opts.env
-
- const envCallExpression = callExpressionPaths[resolvedEnv]
-
- if (!envCallExpression) {
- // if we don't have an implementation for this environment, default to a no-op
- rootCallExpression.replaceWith(
- t.arrowFunctionExpression([], t.blockStatement([])),
- )
- return
- }
-
- const innerInputExpression = envCallExpression.node.arguments[0]
-
- if (!t.isExpression(innerInputExpression)) {
- throw new Error(
- `createIsomorphicFn().${resolvedEnv}(func) must be called with a function!`,
- )
- }
-
- rootCallExpression.replaceWith(innerInputExpression)
-}
-
-function getRootCallExpression(path: babel.NodePath) {
- // Find the highest callExpression parent
- let rootCallExpression: babel.NodePath = path
-
- // Traverse up the chain of CallExpressions
- while (rootCallExpression.parentPath.isMemberExpression()) {
- const parent = rootCallExpression.parentPath
- if (parent.parentPath.isCallExpression()) {
- rootCallExpression = parent.parentPath
- }
- }
-
- return rootCallExpression
-}
-
-function codeFrameError(
- code: string,
- loc: {
- start: { line: number; column: number }
- end: { line: number; column: number }
- },
- message: string,
-) {
- const frame = codeFrameColumns(
- code,
- {
- start: loc.start,
- end: loc.end,
- },
- {
- highlightCode: true,
- message,
- },
- )
-
- return new Error(frame)
-}
diff --git a/packages/react-start-plugin/src/index.ts b/packages/react-start-plugin/src/index.ts
index 6ab529afca..83c3857ac3 100644
--- a/packages/react-start-plugin/src/index.ts
+++ b/packages/react-start-plugin/src/index.ts
@@ -1,143 +1,110 @@
-import { fileURLToPath, pathToFileURL } from 'node:url'
import path from 'node:path'
-import { existsSync } from 'node:fs'
-import { logDiff } from '@tanstack/router-utils'
-import { compileStartOutput } from './compilers'
-import type { Plugin } from 'vite'
-
-const debug =
- process.env.TSR_VITE_DEBUG &&
- ['true', 'react-start-plugin'].includes(process.env.TSR_VITE_DEBUG)
-
-export type TanStackStartViteOptions = {
- globalMiddlewareEntry: string
-}
-
-const transformFuncs = [
- 'createServerFn',
- 'createMiddleware',
- 'serverOnly',
- 'clientOnly',
- 'createIsomorphicFn',
-]
-const tokenRegex = new RegExp(transformFuncs.join('|'))
-// const eitherFuncRegex = new RegExp(
-// `(function ${transformFuncs.join('|function ')})`,
-// )
-
-export function createTanStackStartPlugin(opts: TanStackStartViteOptions): {
- client: Array
- ssr: Array
- server: Array
-} {
- const globalMiddlewarePlugin = (): Plugin => {
- let entry: string | null = null
- let resolvedGlobalMiddlewareEntry: string | null = null
- let globalMiddlewareEntryExists = false
- let ROOT: string = process.cwd()
- return {
- name: 'vite-plugin-tanstack-start-ensure-global-middleware',
- enforce: 'pre',
+import viteReact from '@vitejs/plugin-react'
+import { TanStackStartVitePluginCore } from '@tanstack/start-plugin-core'
+import * as vite from 'vite'
+import { getTanStackStartOptions } from './schema'
+import type { TanStackStartInputConfig, WithReactPlugin } from './schema'
+import type { PluginOption, ResolvedConfig } from 'vite'
+
+export type {
+ TanStackStartInputConfig,
+ TanStackStartOutputConfig,
+ WithReactPlugin,
+} from './schema'
+
+export function TanStackStartVitePlugin(
+ opts?: TanStackStartInputConfig & WithReactPlugin,
+): Array {
+ type OptionsWithReact = ReturnType &
+ WithReactPlugin
+ const options: OptionsWithReact = getTanStackStartOptions(opts)
+
+ let resolvedConfig: ResolvedConfig
+
+ return [
+ TanStackStartVitePluginCore({ framework: 'react' }, options),
+ {
+ name: 'tanstack-react-start:resolve-entries',
configResolved: (config) => {
- ROOT = config.root
- entry = path.resolve(ROOT, (config as any).router.handler)
- resolvedGlobalMiddlewareEntry = path.resolve(
- ROOT,
- opts.globalMiddlewareEntry,
- )
- globalMiddlewareEntryExists = existsSync(resolvedGlobalMiddlewareEntry)
-
- if (!entry) {
- throw new Error(
- '@tanstack/react-start-plugin: No server entry found!',
- )
- }
+ resolvedConfig = config
},
- transform(code, id) {
- if (entry && id.includes(entry)) {
- if (globalMiddlewareEntryExists) {
- return {
- code: `${code}\n\nimport '${path.resolve(ROOT, opts.globalMiddlewareEntry)}'`,
- map: null,
- }
- }
+ resolveId(id) {
+ if (
+ [
+ '/~start/server-entry',
+ '/~start/default-server-entry',
+ '/~start/default-client-entry',
+ ].includes(id)
+ ) {
+ return `${id}.tsx`
}
+ if (id === '/~start/server-entry.tsx') {
+ return id
+ }
+
return null
},
- }
- }
+ load(id) {
+ const routerImportPath = JSON.stringify(
+ path.resolve(options.root, options.tsr.srcDirectory, 'router'),
+ )
- return {
- client: [
- globalMiddlewarePlugin(),
- TanStackStartServerFnsAndMiddleware({ ...opts, env: 'client' }),
- ],
- ssr: [
- globalMiddlewarePlugin(),
- TanStackStartServerFnsAndMiddleware({ ...opts, env: 'ssr' }),
- ],
- server: [
- globalMiddlewarePlugin(),
- TanStackStartServerFnsAndMiddleware({ ...opts, env: 'server' }),
- ],
- }
-}
+ if (id === '/~start/server-entry.tsx') {
+ const ssrEntryPath = options.serverEntryPath.startsWith(
+ '/~start/default-server-entry',
+ )
+ ? options.serverEntryPath
+ : vite.normalizePath(
+ path.resolve(resolvedConfig.root, options.serverEntryPath),
+ )
+
+ return `
+import { toWebRequest, defineEventHandler } from '@tanstack/react-start/server';
+import serverEntry from '${ssrEntryPath}';
+
+export default defineEventHandler(function(event) {
+ const request = toWebRequest(event);
+ return serverEntry({ request });
+})
+`
+ }
-export function TanStackStartServerFnsAndMiddleware(opts: {
- env: 'server' | 'ssr' | 'client'
-}): Plugin {
- let ROOT: string = process.cwd()
+ if (id === '/~start/default-client-entry.tsx') {
+ return `
+import { StrictMode, startTransition } from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { StartClient } from '@tanstack/react-start'
+import { createRouter } from ${routerImportPath}
+
+const router = createRouter()
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ )
+})
+`
+ }
- return {
- name: 'vite-plugin-tanstack-start-create-server-fn',
- enforce: 'pre',
- configResolved: (config) => {
- ROOT = config.root
- },
- transform(code, id) {
- const url = pathToFileURL(id)
- url.searchParams.delete('v')
- id = fileURLToPath(url).replace(/\\/g, '/')
+ if (id === '/~start/default-server-entry.tsx') {
+ return `
+import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server'
+import { createRouter } from ${routerImportPath}
- const includesToken = tokenRegex.test(code)
- // const includesEitherFunc = eitherFuncRegex.test(code)
+export default createStartHandler({
+ createRouter,
+})(defaultStreamHandler)
+`
+ }
- if (
- !includesToken
- // includesEitherFunc
- // /node_modules/.test(id)
- ) {
return null
- }
-
- if (code.includes('@react-refresh')) {
- throw new Error(
- `We detected that the '@vitejs/plugin-react' was passed before '@tanstack/react-start-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again:
-e.g.
-
-plugins: [
- TanStackStartVite(), // Place this before viteReact()
- viteReact(),
-]
-`,
- )
- }
-
- if (debug) console.info(`${opts.env} Compiling Start: `, id)
-
- const compiled = compileStartOutput({
- code,
- root: ROOT,
- filename: id,
- env: opts.env,
- })
-
- if (debug) {
- logDiff(code, compiled.code)
- console.log('Output:\n', compiled.code + '\n\n')
- }
-
- return compiled
+ },
},
- }
+ viteReact(options.react),
+ ]
}
+
+export { TanStackStartVitePlugin as tanstackStart }
diff --git a/packages/react-start-plugin/src/schema.ts b/packages/react-start-plugin/src/schema.ts
new file mode 100644
index 0000000000..4522cc801a
--- /dev/null
+++ b/packages/react-start-plugin/src/schema.ts
@@ -0,0 +1,31 @@
+import { z } from 'zod'
+import {
+ createTanStackConfig,
+ createTanStackStartOptionsSchema,
+} from '@tanstack/start-plugin-core'
+import type { Options as ViteReactOptions } from '@vitejs/plugin-react'
+
+export type WithReactPlugin = {
+ react?: ViteReactOptions
+}
+
+const frameworkPlugin = {
+ react: z.custom().optional(),
+}
+
+// eslint-disable-next-line unused-imports/no-unused-vars
+const TanStackStartOptionsSchema =
+ createTanStackStartOptionsSchema(frameworkPlugin)
+
+const defaultConfig = createTanStackConfig(frameworkPlugin)
+
+export function getTanStackStartOptions(opts?: TanStackStartInputConfig) {
+ return defaultConfig.parse(opts)
+}
+
+export type TanStackStartInputConfig = z.input<
+ typeof TanStackStartOptionsSchema
+>
+export type TanStackStartOutputConfig = ReturnType<
+ typeof getTanStackStartOptions
+>
diff --git a/packages/react-start-plugin/tsconfig.json b/packages/react-start-plugin/tsconfig.json
index 37d21ef6ca..090ec3917d 100644
--- a/packages/react-start-plugin/tsconfig.json
+++ b/packages/react-start-plugin/tsconfig.json
@@ -3,6 +3,8 @@
"include": ["src", "vite.config.ts", "tests"],
"exclude": ["tests/**/test-files/**", "tests/**/snapshots/**"],
"compilerOptions": {
- "jsx": "react-jsx"
+ "outDir": "dist/esm",
+ "target": "esnext",
+ "noEmit": true
}
}
diff --git a/packages/react-start-plugin/vite.config.ts b/packages/react-start-plugin/vite.config.ts
index 5389f0f739..2c711fd181 100644
--- a/packages/react-start-plugin/vite.config.ts
+++ b/packages/react-start-plugin/vite.config.ts
@@ -16,5 +16,6 @@ export default mergeConfig(
tanstackViteConfig({
entry: './src/index.ts',
srcDir: './src',
+ outDir: './dist',
}),
)
diff --git a/packages/react-start-router-manifest/README.md b/packages/react-start-router-manifest/README.md
deleted file mode 100644
index bb009b0c87..0000000000
--- a/packages/react-start-router-manifest/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-> 🤫 we're cooking up something special!
-
-
-
-# TanStack Start
-
-
-
-🤖 Type-safe router w/ built-in caching & URL state management for React!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)
-
-## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
diff --git a/packages/react-start-router-manifest/eslint.config.js b/packages/react-start-router-manifest/eslint.config.js
deleted file mode 100644
index 931f0ec774..0000000000
--- a/packages/react-start-router-manifest/eslint.config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// @ts-check
-
-import pluginReact from '@eslint-react/eslint-plugin'
-import pluginReactHooks from 'eslint-plugin-react-hooks'
-import rootConfig from '../../eslint.config.js'
-
-export default [
- ...rootConfig,
- {
- ...pluginReact.configs.recommended,
- files: ['**/*.{ts,tsx}'],
- },
- {
- plugins: {
- 'react-hooks': pluginReactHooks,
- },
- rules: {
- '@eslint-react/no-unstable-context-value': 'off',
- '@eslint-react/no-unstable-default-props': 'off',
- '@eslint-react/dom/no-missing-button-type': 'off',
- 'react-hooks/exhaustive-deps': 'error',
- 'react-hooks/rules-of-hooks': 'error',
- },
- },
- {
- files: ['**/__tests__/**'],
- rules: {
- '@typescript-eslint/no-unnecessary-condition': 'off',
- },
- },
-]
diff --git a/packages/react-start-router-manifest/package.json b/packages/react-start-router-manifest/package.json
deleted file mode 100644
index f367442a9c..0000000000
--- a/packages/react-start-router-manifest/package.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "name": "@tanstack/react-start-router-manifest",
- "version": "1.120.3",
- "description": "Modern and scalable routing for React applications",
- "author": "Tanner Linsley",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "https://github.com/TanStack/router.git",
- "directory": "packages/react-start-router-manifest"
- },
- "homepage": "https://tanstack.com/start",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "keywords": [
- "react",
- "location",
- "router",
- "routing",
- "async",
- "async router",
- "typescript"
- ],
- "scripts": {
- "clean": "rimraf ./dist && rimraf ./coverage",
- "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
- "test:unit": "exit 0; vitest",
- "test:eslint": "eslint ./src",
- "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
- "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
- "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
- "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
- "test:types:ts58": "tsc",
- "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
- "build": "tsc"
- },
- "type": "module",
- "types": "dist/esm/index.d.ts",
- "exports": {
- ".": {
- "import": {
- "types": "./dist/esm/index.d.ts",
- "default": "./dist/esm/index.js"
- }
- },
- "./package.json": "./package.json"
- },
- "sideEffects": false,
- "files": [
- "dist",
- "src"
- ],
- "engines": {
- "node": ">=12"
- },
- "dependencies": {
- "@tanstack/router-core": "workspace:^",
- "tiny-invariant": "^1.3.3",
- "vinxi": "0.5.3"
- },
- "devDependencies": {
- "typescript": "^5.7.2"
- }
-}
diff --git a/packages/react-start-router-manifest/src/index.ts b/packages/react-start-router-manifest/src/index.ts
deleted file mode 100644
index 3f0a37f609..0000000000
--- a/packages/react-start-router-manifest/src/index.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-// @ts-expect-error
-import tsrGetManifest from 'tsr:routes-manifest'
-import { getManifest } from 'vinxi/manifest'
-import { default as invariant } from 'tiny-invariant'
-import type { Manifest } from '@tanstack/router-core'
-
-function sanitizeBase(base: string) {
- return base.replace(/^\/|\/$/g, '')
-}
-
-/**
- * @description Returns the full, unfiltered router manifest. This includes relationships
- * between routes, assets, and preloads and is NOT what you want to serialize and
- * send to the client.
- */
-export function getFullRouterManifest() {
- const routerManifest = tsrGetManifest() as Manifest
-
- const rootRoute = (routerManifest.routes.__root__ =
- routerManifest.routes.__root__ || {})
-
- rootRoute.assets = rootRoute.assets || []
-
- let script = ''
- // Always fake that HMR is ready
- if (process.env.NODE_ENV === 'development') {
- const CLIENT_BASE = sanitizeBase(process.env.TSS_CLIENT_BASE || '')
-
- if (!CLIENT_BASE) {
- throw new Error(
- 'tanstack/start-router-manifest: TSS_CLIENT_BASE must be defined in your environment for getFullRouterManifest()',
- )
- }
- script = `import RefreshRuntime from "/${CLIENT_BASE}/@react-refresh";
- RefreshRuntime.injectIntoGlobalHook(window)
- window.$RefreshReg$ = () => {}
- window.$RefreshSig$ = () => (type) => type
- window.__vite_plugin_react_preamble_installed__ = true;`
- }
-
- // Get the entry for the client from vinxi
- const vinxiClientManifest = getManifest('client')
-
- const importPath =
- vinxiClientManifest.inputs[vinxiClientManifest.handler]?.output.path
- if (!importPath) {
- invariant(importPath, 'Could not find client entry in vinxi manifest')
- }
-
- rootRoute.assets.push({
- tag: 'script',
- attrs: {
- type: 'module',
- suppressHydrationWarning: true,
- async: true,
- },
- children: `${script}import("${importPath}")`,
- })
-
- return routerManifest
-}
-
-/**
- * @description Returns the router manifest that should be sent to the client.
- * This includes only the assets and preloads for the current route and any
- * special assets that are needed for the client. It does not include relationships
- * between routes or any other data that is not needed for the client.
- */
-export function getRouterManifest() {
- const routerManifest = getFullRouterManifest()
-
- // Strip out anything that isn't needed for the client
- return {
- ...routerManifest,
- routes: Object.fromEntries(
- Object.entries(routerManifest.routes).map(([k, v]: any) => {
- const { preloads, assets } = v
- return [
- k,
- {
- preloads,
- assets,
- },
- ]
- }),
- ),
- }
-}
diff --git a/packages/react-start-router-manifest/tsconfig.json b/packages/react-start-router-manifest/tsconfig.json
deleted file mode 100644
index 940a9cce0a..0000000000
--- a/packages/react-start-router-manifest/tsconfig.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "include": ["src/index.ts"],
- "compilerOptions": {
- "rootDir": "src",
- "outDir": "dist/esm",
- "target": "esnext",
- "noEmit": false
- }
-}
diff --git a/packages/react-start-router-manifest/vite.config.ts b/packages/react-start-router-manifest/vite.config.ts
deleted file mode 100644
index d6472068fb..0000000000
--- a/packages/react-start-router-manifest/vite.config.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { defineConfig, mergeConfig } from 'vitest/config'
-import { tanstackViteConfig } from '@tanstack/config/vite'
-import packageJson from './package.json'
-import type { ViteUserConfig } from 'vitest/config'
-
-const config = defineConfig({
- plugins: [] as ViteUserConfig['plugins'],
- test: {
- name: packageJson.name,
- watch: false,
- environment: 'jsdom',
- },
-})
-
-export default mergeConfig(
- config,
- tanstackViteConfig({
- entry: './src/index.ts',
- srcDir: './src',
- externalDeps: ['tsr:routes-manifest'],
- }),
-)
diff --git a/packages/react-start-server/README.md b/packages/react-start-server/README.md
index bb009b0c87..7be47d33fd 100644
--- a/packages/react-start-server/README.md
+++ b/packages/react-start-server/README.md
@@ -1,33 +1,9 @@
-> 🤫 we're cooking up something special!
-
-# TanStack Start
-
-
-
-🤖 Type-safe router w/ built-in caching & URL state management for React!
+# TanStack React Start - Server
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+This package is not meant to be used directly. It is a dependency of [`@tanstack/react-start`](https://www.npmjs.com/package/@tanstack/react-start).
-Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)
+TanStack React Start is a fullstack-framework made for SSR, Streaming, Server Functions, API Routes, bundling and more powered by [TanStack Router](https://tanstack.com/router).
-## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
+Head over to [tanstack.com/start](https://tanstack.com/start) for more information about getting started.
diff --git a/packages/react-start-server/package.json b/packages/react-start-server/package.json
index 9bbf439abe..a845a9d8e1 100644
--- a/packages/react-start-server/package.json
+++ b/packages/react-start-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start-server",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -68,6 +68,7 @@
"@tanstack/start-client-core": "workspace:^",
"@tanstack/start-server-core": "workspace:^",
"tiny-warning": "^1.0.3",
+ "tiny-invariant": "^1.3.3",
"h3": "1.13.0",
"isbot": "^5.1.22",
"jsesc": "^3.1.0",
diff --git a/packages/react-start-server/src/defaultStreamHandler.tsx b/packages/react-start-server/src/defaultStreamHandler.tsx
index e95a9fcb87..8891dfd218 100644
--- a/packages/react-start-server/src/defaultStreamHandler.tsx
+++ b/packages/react-start-server/src/defaultStreamHandler.tsx
@@ -54,6 +54,11 @@ export const defaultStreamHandler = defineHandlerCallback(
},
}),
onError: (error, info) => {
+ if (
+ error instanceof Error &&
+ error.message === 'ShellBoundaryError'
+ )
+ return
console.error('Error in renderToPipeableStream:', error, info)
},
},
diff --git a/packages/react-start-server/tsconfig.json b/packages/react-start-server/tsconfig.json
index 134e51f065..b447592593 100644
--- a/packages/react-start-server/tsconfig.json
+++ b/packages/react-start-server/tsconfig.json
@@ -8,6 +8,6 @@
"src",
"tests",
"vite.config.ts",
- "../start-client/src/tsrScript.ts"
+ "../start-server-core/src/server-functions-handler.ts"
]
}
diff --git a/packages/react-start-server/vite.config.ts b/packages/react-start-server/vite.config.ts
index e05e5cc394..3a4c1c54ef 100644
--- a/packages/react-start-server/vite.config.ts
+++ b/packages/react-start-server/vite.config.ts
@@ -19,5 +19,10 @@ export default mergeConfig(
tanstackViteConfig({
srcDir: './src',
entry: './src/index.tsx',
+ externalDeps: [
+ 'tanstack-start-server-fn-manifest:v',
+ 'tanstack-start-router-manifest:v',
+ 'tanstack-start-server-routes-manifest:v',
+ ],
}),
)
diff --git a/packages/react-start/README.md b/packages/react-start/README.md
index bb009b0c87..a36c48bd14 100644
--- a/packages/react-start/README.md
+++ b/packages/react-start/README.md
@@ -1,33 +1,42 @@
-> 🤫 we're cooking up something special!
-
-# TanStack Start
+# TanStack React Start
-
+
-🤖 Type-safe router w/ built-in caching & URL state management for React!
+SSR, Streaming, Server Functions, API Routes, bundling and more powered by [TanStack Router](https://tanstack.com/router) and Vite. Ready to deploy to your favorite hosting provider.
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
+
+
-
+
+
Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)
-## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
+Visit [tanstack.com/start](https://tanstack.com/start) for docs, guides, API and more!
diff --git a/packages/react-start/package.json b/packages/react-start/package.json
index a08b503980..761ed13bd6 100644
--- a/packages/react-start/package.json
+++ b/packages/react-start/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -62,34 +62,14 @@
"default": "./dist/cjs/server.cjs"
}
},
- "./config": {
+ "./plugin/vite": {
"import": {
- "types": "./dist/esm/config.d.ts",
- "default": "./dist/esm/config.js"
+ "types": "./dist/esm/plugin-vite.d.ts",
+ "default": "./dist/esm/plugin-vite.js"
},
"require": {
- "types": "./dist/cjs/config.d.cts",
- "default": "./dist/cjs/config.cjs"
- }
- },
- "./api": {
- "import": {
- "types": "./dist/esm/api.d.ts",
- "default": "./dist/esm/api.js"
- },
- "require": {
- "types": "./dist/cjs/api.d.cts",
- "default": "./dist/cjs/api.cjs"
- }
- },
- "./router-manifest": {
- "import": {
- "types": "./dist/esm/router-manifest.d.ts",
- "default": "./dist/esm/router-manifest.js"
- },
- "require": {
- "types": "./dist/cjs/router-manifest.d.cts",
- "default": "./dist/cjs/router-manifest.cjs"
+ "types": "./dist/cjs/plugin-vite.d.cts",
+ "default": "./dist/cjs/plugin-vite.cjs"
}
},
"./server-functions-client": {
@@ -112,26 +92,6 @@
"default": "./dist/cjs/server-functions-server.cjs"
}
},
- "./server-functions-handler": {
- "import": {
- "types": "./dist/esm/server-functions-handler.d.ts",
- "default": "./dist/esm/server-functions-handler.js"
- },
- "require": {
- "types": "./dist/cjs/server-functions-handler.d.cts",
- "default": "./dist/cjs/server-functions-handler.cjs"
- }
- },
- "./server-functions-ssr": {
- "import": {
- "types": "./dist/esm/server-functions-ssr.d.ts",
- "default": "./dist/esm/server-functions-ssr.js"
- },
- "require": {
- "types": "./dist/cjs/server-functions-ssr.d.cts",
- "default": "./dist/cjs/server-functions-ssr.cjs"
- }
- },
"./package.json": "./package.json"
},
"sideEffects": false,
@@ -145,18 +105,15 @@
"dependencies": {
"@tanstack/react-start-client": "workspace:^",
"@tanstack/react-start-server": "workspace:^",
- "@tanstack/react-start-config": "workspace:^",
- "@tanstack/react-start-router-manifest": "workspace:^",
+ "@tanstack/react-start-plugin": "workspace:^",
"@tanstack/start-server-functions-client": "workspace:^",
- "@tanstack/start-server-functions-server": "workspace:^",
- "@tanstack/start-server-functions-handler": "workspace:^",
- "@tanstack/start-server-functions-ssr": "workspace:^",
- "@tanstack/start-api-routes": "workspace:^"
+ "@tanstack/start-server-functions-server": "workspace:^"
},
"peerDependencies": {
+ "@vitejs/plugin-react": ">=4.3.4",
"react": ">=18.0.0 || >=19.0.0",
"react-dom": ">=18.0.0 || >=19.0.0",
- "vite": "^6.0.0"
+ "vite": ">=6.0.0"
},
"devDependencies": {
"esbuild": "^0.25.0"
diff --git a/packages/react-start/src/api.tsx b/packages/react-start/src/api.tsx
deleted file mode 100644
index fff3bec5ea..0000000000
--- a/packages/react-start/src/api.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from '@tanstack/start-api-routes'
diff --git a/packages/react-start/src/config.tsx b/packages/react-start/src/config.tsx
deleted file mode 100644
index 57a7ae394d..0000000000
--- a/packages/react-start/src/config.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from '@tanstack/react-start-config'
diff --git a/packages/react-start/src/plugin-vite.ts b/packages/react-start/src/plugin-vite.ts
new file mode 100644
index 0000000000..d991c3b71c
--- /dev/null
+++ b/packages/react-start/src/plugin-vite.ts
@@ -0,0 +1 @@
+export * from '@tanstack/react-start-plugin'
diff --git a/packages/react-start/src/router-manifest.tsx b/packages/react-start/src/router-manifest.tsx
deleted file mode 100644
index ce430d7884..0000000000
--- a/packages/react-start/src/router-manifest.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from '@tanstack/react-start-router-manifest'
diff --git a/packages/react-start/src/server-functions-handler.tsx b/packages/react-start/src/server-functions-handler.tsx
deleted file mode 100644
index c3cc9770d2..0000000000
--- a/packages/react-start/src/server-functions-handler.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from '@tanstack/start-server-functions-handler'
diff --git a/packages/react-start/src/server-functions-ssr.tsx b/packages/react-start/src/server-functions-ssr.tsx
deleted file mode 100644
index 7359e26f45..0000000000
--- a/packages/react-start/src/server-functions-ssr.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from '@tanstack/start-server-functions-ssr'
diff --git a/packages/react-start/vite.config.ts b/packages/react-start/vite.config.ts
index 46bc2ea875..99fd4a8b00 100644
--- a/packages/react-start/vite.config.ts
+++ b/packages/react-start/vite.config.ts
@@ -17,22 +17,16 @@ export default mergeConfig(
entry: [
'./src/client.tsx',
'./src/server.tsx',
- './src/config.tsx',
- './src/router-manifest.tsx',
+ './src/plugin-vite.ts',
'./src/server-functions-client.tsx',
'./src/server-functions-server.tsx',
- './src/server-functions-ssr.tsx',
- './src/api.tsx',
],
externalDeps: [
'@tanstack/react-start-client',
'@tanstack/react-start-server',
- '@tanstack/react-start-config',
- '@tanstack/react-start-router-manifest',
+ '@tanstack/react-start-plugin',
'@tanstack/start-server-functions-client',
'@tanstack/start-server-functions-server',
- '@tanstack/start-server-functions-ssr',
- '@tanstack/start-api-routes',
],
}),
)
diff --git a/packages/router-cli/package.json b/packages/router-cli/package.json
index 3d620859f8..fd4a56148c 100644
--- a/packages/router-cli/package.json
+++ b/packages/router-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/router-cli",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/router-core/package.json b/packages/router-core/package.json
index ce60e3f821..c691a964c0 100644
--- a/packages/router-core/package.json
+++ b/packages/router-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/router-core",
- "version": "1.120.3",
+ "version": "1.121.0-alpha.11",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
diff --git a/packages/router-core/src/fileRoute.ts b/packages/router-core/src/fileRoute.ts
index 8d2e5b33a5..2c689c3929 100644
--- a/packages/router-core/src/fileRoute.ts
+++ b/packages/router-core/src/fileRoute.ts
@@ -2,6 +2,10 @@ import type {
AnyContext,
AnyPathParams,
AnyRoute,
+ FileBaseRouteOptions,
+ ResolveParams,
+ Route,
+ RouteConstraints,
UpdatableRouteOptions,
} from './route'
import type { AnyValidator } from './validators'
@@ -28,6 +32,87 @@ export interface FileRoutesByPath {
// }
}
+export interface FileRouteOptions<
+ TFilePath extends string,
+ TParentRoute extends AnyRoute,
+ TId extends RouteConstraints['TId'],
+ TPath extends RouteConstraints['TPath'],
+ TFullPath extends RouteConstraints['TFullPath'],
+ TSearchValidator = undefined,
+ TParams = ResolveParams,
+ TRouteContextFn = AnyContext,
+ TBeforeLoadFn = AnyContext,
+ TLoaderDeps extends Record = {},
+ TLoaderFn = undefined,
+> extends FileBaseRouteOptions<
+ TParentRoute,
+ TId,
+ TPath,
+ TSearchValidator,
+ TParams,
+ TLoaderDeps,
+ TLoaderFn,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ >,
+ UpdatableRouteOptions<
+ TParentRoute,
+ TId,
+ TFullPath,
+ TParams,
+ TSearchValidator,
+ TLoaderFn,
+ TLoaderDeps,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ > {}
+
+export type CreateFileRoute<
+ TFilePath extends string,
+ TParentRoute extends AnyRoute,
+ TId extends RouteConstraints['TId'],
+ TPath extends RouteConstraints['TPath'],
+ TFullPath extends RouteConstraints['TFullPath'],
+> = <
+ TSearchValidator = undefined,
+ TParams = ResolveParams,
+ TRouteContextFn = AnyContext,
+ TBeforeLoadFn = AnyContext,
+ TLoaderDeps extends Record = {},
+ TLoaderFn = undefined,
+>(
+ options?: FileRouteOptions<
+ TFilePath,
+ TParentRoute,
+ TId,
+ TPath,
+ TFullPath,
+ TSearchValidator,
+ TParams,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn
+ >,
+) => Route<
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TFilePath,
+ TId,
+ TSearchValidator,
+ TParams,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ unknown,
+ unknown
+>
+
export type LazyRouteOptions = Pick<
UpdatableRouteOptions<
AnyRoute,
@@ -44,8 +129,12 @@ export type LazyRouteOptions = Pick<
'component' | 'errorComponent' | 'pendingComponent' | 'notFoundComponent'
>
-export interface LazyRoute {
+export interface LazyRoute {
options: {
id: string
} & LazyRouteOptions
}
+
+export type CreateLazyFileRoute = (
+ opts: LazyRouteOptions,
+) => LazyRoute
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index 4988400855..9f47c3ac3a 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -3,7 +3,6 @@ export type { DeferredPromiseState, DeferredPromise } from './defer'
export { preloadWarning } from './link'
export type {
IsRequiredParams,
- ParsePathParams,
AddTrailingSlash,
RemoveTrailingSlashes,
AddLeadingSlash,
@@ -60,8 +59,10 @@ export type {
InferFileRouteTypes,
FileRouteTypes,
FileRoutesByPath,
+ CreateFileRoute,
LazyRoute,
LazyRouteOptions,
+ CreateLazyFileRoute,
} from './fileRoute'
export type {
@@ -142,15 +143,11 @@ export type {
ErrorRouteProps,
ErrorComponentProps,
NotFoundRouteProps,
- ParseSplatParams,
- SplatParams,
ResolveParams,
ParseParamsFn,
StringifyParamsFn,
ParamsOptions,
UpdatableStaticRouteOption,
- LooseReturnType,
- LooseAsyncReturnType,
ContextReturnType,
ContextAsyncReturnType,
ResolveRouteContext,
@@ -198,6 +195,8 @@ export type {
RouteAddChildrenFn,
RouteAddFileChildrenFn,
RouteAddFileTypesFn,
+ ResolveOptionalParams,
+ ResolveRequiredParams,
} from './route'
export {
@@ -209,6 +208,8 @@ export {
SearchParamError,
PathParamError,
getInitialRouterState,
+ processRouteTree,
+ getMatchedRoutes,
} from './router'
export type {
ViewTransitionOptions,
@@ -242,7 +243,6 @@ export type {
ControllablePromise,
InjectedHtmlEntry,
RouterErrorSerializer,
- MatchedRoutesResult,
EmitFn,
LoadFn,
GetMatchFn,
@@ -322,6 +322,8 @@ export type {
MergeAll,
ValidateJSON,
StrictOrFrom,
+ LooseReturnType,
+ LooseAsyncReturnType,
} from './utils'
export type {
@@ -370,7 +372,12 @@ export type { UseLoaderDataResult, ResolveUseLoaderData } from './useLoaderData'
export type { Redirect, ResolvedRedirect, AnyRedirect } from './redirect'
-export { redirect, isRedirect, isResolvedRedirect } from './redirect'
+export {
+ redirect,
+ isRedirect,
+ isResolvedRedirect,
+ parseRedirect,
+} from './redirect'
export type { NotFoundError } from './not-found'
export { isNotFound, notFound } from './not-found'
diff --git a/packages/router-core/src/link.ts b/packages/router-core/src/link.ts
index 26d038e769..6f488b10cc 100644
--- a/packages/router-core/src/link.ts
+++ b/packages/router-core/src/link.ts
@@ -30,18 +30,98 @@ import type { ParsedLocation } from './location'
export type IsRequiredParams =
Record extends TParams ? never : true
-export type ParsePathParams = T &
- `${string}$${string}` extends never
- ? TAcc
- : T extends `${string}$${infer TPossiblyParam}`
- ? TPossiblyParam extends ''
- ? TAcc
- : TPossiblyParam & `${string}/${string}` extends never
- ? TPossiblyParam | TAcc
- : TPossiblyParam extends `${infer TParam}/${infer TRest}`
- ? ParsePathParams
+export interface ParsePathParamsResult<
+ in out TRequired,
+ in out TOptional,
+ in out TRest,
+> {
+ required: TRequired
+ optional: TOptional
+ rest: TRest
+}
+
+export type AnyParsePathParamsResult = ParsePathParamsResult<
+ string,
+ string,
+ string
+>
+
+export type ParsePathParamsBoundaryStart =
+ T extends `${infer TLeft}{-${infer TRight}`
+ ? ParsePathParamsResult<
+ ParsePathParams['required'],
+ | ParsePathParams['optional']
+ | ParsePathParams['required']
+ | ParsePathParams['optional'],
+ ParsePathParams['rest']
+ >
+ : T extends `${infer TLeft}{${infer TRight}`
+ ? ParsePathParamsResult<
+ | ParsePathParams['required']
+ | ParsePathParams['required'],
+ | ParsePathParams['optional']
+ | ParsePathParams['optional'],
+ ParsePathParams['rest']
+ >
+ : never
+
+export type ParsePathParamsSymbol =
+ T extends `${string}$${infer TRight}`
+ ? TRight extends `${string}/${string}`
+ ? TRight extends `${infer TParam}/${infer TRest}`
+ ? TParam extends ''
+ ? ParsePathParamsResult<
+ ParsePathParams['required'],
+ '_splat' | ParsePathParams['optional'],
+ ParsePathParams['rest']
+ >
+ : ParsePathParamsResult<
+ TParam | ParsePathParams['required'],
+ ParsePathParams['optional'],
+ ParsePathParams['rest']
+ >
+ : never
+ : TRight extends ''
+ ? ParsePathParamsResult
+ : ParsePathParamsResult
+ : never
+
+export type ParsePathParamsBoundaryEnd =
+ T extends `${infer TLeft}}${infer TRight}`
+ ? ParsePathParamsResult<
+ | ParsePathParams['required']
+ | ParsePathParams['required'],
+ | ParsePathParams['optional']
+ | ParsePathParams['optional'],
+ ParsePathParams['rest']
+ >
+ : never
+
+export type ParsePathParamsEscapeStart =
+ T extends `${infer TLeft}[${infer TRight}`
+ ? ParsePathParamsResult<
+ | ParsePathParams['required']
+ | ParsePathParams['required'],
+ | ParsePathParams['optional']
+ | ParsePathParams['optional'],
+ ParsePathParams['rest']
+ >
+ : never
+
+export type ParsePathParamsEscapeEnd =
+ T extends `${string}]${infer TRight}` ? ParsePathParams : never
+
+export type ParsePathParams = T extends `${string}[${string}`
+ ? ParsePathParamsEscapeStart
+ : T extends `${string}]${string}`
+ ? ParsePathParamsEscapeEnd
+ : T extends `${string}}${string}`
+ ? ParsePathParamsBoundaryEnd
+ : T extends `${string}{${string}`
+ ? ParsePathParamsBoundaryStart
+ : T extends `${string}$${string}`
+ ? ParsePathParamsSymbol
: never
- : TAcc
export type AddTrailingSlash = T extends `${string}/` ? T : `${T & string}/`
@@ -344,6 +424,7 @@ export type ToSubOptionsProps<
hash?: true | Updater
state?: true | NonNullableUpdater
from?: FromPathOption & {}
+ unsafeRelative?: 'path'
}
export type ParamsReducerFn<
@@ -591,6 +672,11 @@ export interface LinkOptionsProps {
* @default false
*/
disabled?: boolean
+ /**
+ * When the preload strategy is set to `intent`, this controls the proximity of the link to the cursor before it is preloaded.
+ * If the user exits this proximity before this delay, the preload will be cancelled.
+ */
+ preloadIntentProximity?: number
}
export type LinkOptions<
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index 4d002673cd..e1f6d737df 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -5,6 +5,9 @@ import type { AnyPathParams } from './route'
export interface Segment {
type: 'pathname' | 'param' | 'wildcard'
value: string
+ // Add a new property to store the static segment if present
+ prefixSegment?: string
+ suffixSegment?: string
}
export function joinPaths(paths: Array) {
@@ -137,10 +140,52 @@ export function resolvePath({
}
}
- const joined = joinPaths([basepath, ...baseSegments.map((d) => d.value)])
+ const segmentValues = baseSegments.map((segment) => {
+ if (segment.type === 'param') {
+ const param = segment.value.substring(1)
+ if (segment.prefixSegment && segment.suffixSegment) {
+ return `${segment.prefixSegment}{$${param}}${segment.suffixSegment}`
+ } else if (segment.prefixSegment) {
+ return `${segment.prefixSegment}{$${param}}`
+ } else if (segment.suffixSegment) {
+ return `{$${param}}${segment.suffixSegment}`
+ }
+ }
+ if (segment.type === 'wildcard') {
+ if (segment.prefixSegment && segment.suffixSegment) {
+ return `${segment.prefixSegment}{$}${segment.suffixSegment}`
+ } else if (segment.prefixSegment) {
+ return `${segment.prefixSegment}{$}`
+ } else if (segment.suffixSegment) {
+ return `{$}${segment.suffixSegment}`
+ }
+ }
+ return segment.value
+ })
+ const joined = joinPaths([basepath, ...segmentValues])
return cleanPath(joined)
}
+const PARAM_RE = /^\$.{1,}$/ // $paramName
+const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
+const WILDCARD_RE = /^\$$/ // $
+const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
+
+/**
+ * Required: `/foo/$bar` ✅
+ * Prefix and Suffix: `/foo/prefix${bar}suffix` ✅
+ * Wildcard: `/foo/$` ✅
+ * Wildcard with Prefix and Suffix: `/foo/prefix{$}suffix` ✅
+ *
+ * Future:
+ * Optional: `/foo/{-bar}`
+ * Optional named segment: `/foo/{bar}`
+ * Optional named segment with Prefix and Suffix: `/foo/prefix{-bar}suffix`
+ * Escape special characters:
+ * - `/foo/[$]` - Static route
+ * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
+ * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
+ */
export function parsePathname(pathname?: string): Array {
if (!pathname) {
return []
@@ -167,20 +212,55 @@ export function parsePathname(pathname?: string): Array {
segments.push(
...split.map((part): Segment => {
- if (part === '$' || part === '*') {
+ // Check for wildcard with curly braces: prefix{$}suffix
+ const wildcardBracesMatch = part.match(WILDCARD_W_CURLY_BRACES_RE)
+ if (wildcardBracesMatch) {
+ const prefix = wildcardBracesMatch[1]
+ const suffix = wildcardBracesMatch[2]
return {
type: 'wildcard',
- value: part,
+ value: '$',
+ prefixSegment: prefix || undefined,
+ suffixSegment: suffix || undefined,
}
}
- if (part.charAt(0) === '$') {
+ // Check for the new parameter format: prefix{$paramName}suffix
+ const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE)
+ if (paramBracesMatch) {
+ const prefix = paramBracesMatch[1]
+ const paramName = paramBracesMatch[2]
+ const suffix = paramBracesMatch[3]
return {
type: 'param',
- value: part,
+ value: '' + paramName,
+ prefixSegment: prefix || undefined,
+ suffixSegment: suffix || undefined,
}
}
+ // Check for bare parameter format: $paramName (without curly braces)
+ if (PARAM_RE.test(part)) {
+ const paramName = part.substring(1)
+ return {
+ type: 'param',
+ value: '$' + paramName,
+ prefixSegment: undefined,
+ suffixSegment: undefined,
+ }
+ }
+
+ // Check for bare wildcard: $ (without curly braces)
+ if (WILDCARD_RE.test(part)) {
+ return {
+ type: 'wildcard',
+ value: '$',
+ prefixSegment: undefined,
+ suffixSegment: undefined,
+ }
+ }
+
+ // Handle regular pathname segment
return {
type: 'pathname',
value: part.includes('%25')
@@ -248,9 +328,13 @@ export function interpolatePath({
interpolatedPathSegments.map((segment) => {
if (segment.type === 'wildcard') {
usedParams._splat = params._splat
+ const segmentPrefix = segment.prefixSegment || ''
+ const segmentSuffix = segment.suffixSegment || ''
const value = encodeParam('_splat')
- if (leaveWildcards) return `${segment.value}${value ?? ''}`
- return value
+ if (leaveWildcards) {
+ return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
+ }
+ return `${segmentPrefix}${value}${segmentSuffix}`
}
if (segment.type === 'param') {
@@ -259,11 +343,14 @@ export function interpolatePath({
isMissingParams = true
}
usedParams[key] = params[key]
+
+ const segmentPrefix = segment.prefixSegment || ''
+ const segmentSuffix = segment.suffixSegment || ''
if (leaveParams) {
const value = encodeParam(segment.value)
- return `${segment.value}${value ?? ''}`
+ return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
}
- return encodeParam(key) ?? 'undefined'
+ return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
}
return segment.value
@@ -390,9 +477,57 @@ export function matchByPath(
if (routeSegment) {
if (routeSegment.type === 'wildcard') {
- const _splat = decodeURI(
- joinPaths(baseSegments.slice(i).map((d) => d.value)),
- )
+ // Capture all remaining segments for a wildcard
+ const remainingBaseSegments = baseSegments.slice(i)
+
+ let _splat: string
+
+ // If this is a wildcard with prefix/suffix, we need to handle the first segment specially
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
+ if (!baseSegment) return false
+
+ const prefix = routeSegment.prefixSegment || ''
+ const suffix = routeSegment.suffixSegment || ''
+
+ // Check if the base segment starts with prefix and ends with suffix
+ const baseValue = baseSegment.value
+ if ('prefixSegment' in routeSegment) {
+ if (!baseValue.startsWith(prefix)) {
+ return false
+ }
+ }
+ if ('suffixSegment' in routeSegment) {
+ if (
+ !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
+ ) {
+ return false
+ }
+ }
+
+ let rejoinedSplat = decodeURI(
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
+ )
+
+ // Remove the prefix and suffix from the rejoined splat
+ if (prefix && rejoinedSplat.startsWith(prefix)) {
+ rejoinedSplat = rejoinedSplat.slice(prefix.length)
+ }
+
+ if (suffix && rejoinedSplat.endsWith(suffix)) {
+ rejoinedSplat = rejoinedSplat.slice(
+ 0,
+ rejoinedSplat.length - suffix.length,
+ )
+ }
+
+ _splat = rejoinedSplat
+ } else {
+ // If no prefix/suffix, just rejoin the remaining segments
+ _splat = decodeURI(
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
+ )
+ }
+
// TODO: Deprecate *
params['*'] = _splat
params['_splat'] = _splat
@@ -426,11 +561,41 @@ export function matchByPath(
if (baseSegment.value === '/') {
return false
}
- if (baseSegment.value.charAt(0) !== '$') {
- params[routeSegment.value.substring(1)] = decodeURIComponent(
- baseSegment.value,
- )
+
+ let _paramValue: string
+
+ // If this param has prefix/suffix, we need to extract the actual parameter value
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
+ const prefix = routeSegment.prefixSegment || ''
+ const suffix = routeSegment.suffixSegment || ''
+
+ // Check if the base segment starts with prefix and ends with suffix
+ const baseValue = baseSegment.value
+ if (prefix && !baseValue.startsWith(prefix)) {
+ return false
+ }
+ if (suffix && !baseValue.endsWith(suffix)) {
+ return false
+ }
+
+ let paramValue = baseValue
+ if (prefix && paramValue.startsWith(prefix)) {
+ paramValue = paramValue.slice(prefix.length)
+ }
+ if (suffix && paramValue.endsWith(suffix)) {
+ paramValue = paramValue.slice(
+ 0,
+ paramValue.length - suffix.length,
+ )
+ }
+
+ _paramValue = decodeURIComponent(paramValue)
+ } else {
+ // If no prefix/suffix, just decode the base segment value
+ _paramValue = decodeURIComponent(baseSegment.value)
}
+
+ params[routeSegment.value.substring(1)] = _paramValue
}
}
diff --git a/packages/router-core/src/redirect.ts b/packages/router-core/src/redirect.ts
index 24de9024b2..02e85221b9 100644
--- a/packages/router-core/src/redirect.ts
+++ b/packages/router-core/src/redirect.ts
@@ -1,6 +1,5 @@
import type { NavigateOptions } from './link'
import type { AnyRouter, RegisteredRouter } from './router'
-import type { PickAsRequired } from './utils'
export type AnyRedirect = Redirect
@@ -13,6 +12,17 @@ export type Redirect<
TTo extends string | undefined = undefined,
TMaskFrom extends string = TFrom,
TMaskTo extends string = '.',
+> = Response & {
+ options: NavigateOptions
+ redirectHandled?: boolean
+}
+
+export type RedirectOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = undefined,
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '.',
> = {
href?: string
/**
@@ -42,12 +52,7 @@ export type ResolvedRedirect<
TTo extends string = '',
TMaskFrom extends string = TFrom,
TMaskTo extends string = '',
-> = PickAsRequired<
- Redirect,
- 'code' | 'statusCode' | 'headers'
-> & {
- href: string
-}
+> = Redirect
export function redirect<
TRouter extends AnyRouter = RegisteredRouter,
@@ -56,30 +61,48 @@ export function redirect<
const TMaskFrom extends string = TFrom,
const TMaskTo extends string = '',
>(
- opts: Redirect,
+ opts: RedirectOptions,
): Redirect {
- ;(opts as any).isRedirect = true
opts.statusCode = opts.statusCode || opts.code || 307
- opts.headers = opts.headers || {}
+
if (!opts.reloadDocument) {
- opts.reloadDocument = false
try {
new URL(`${opts.href}`)
opts.reloadDocument = true
} catch {}
}
+ const headers = new Headers(opts.headers || {})
+
+ const response = new Response(null, {
+ status: opts.statusCode,
+ headers,
+ })
+
+ ;(response as Redirect).options =
+ opts
+
if (opts.throw) {
- throw opts
+ throw response
}
- return opts
+ return response as Redirect
}
export function isRedirect(obj: any): obj is AnyRedirect {
- return !!obj?.isRedirect
+ return obj instanceof Response && !!(obj as any).options
}
-export function isResolvedRedirect(obj: any): obj is ResolvedRedirect {
- return !!obj?.isRedirect && obj.href
+export function isResolvedRedirect(
+ obj: any,
+): obj is AnyRedirect & { options: { href: string } } {
+ return isRedirect(obj) && !!obj.options.href
+}
+
+export function parseRedirect(obj: any) {
+ if (typeof obj === 'object' && obj.isSerializedRedirect) {
+ return redirect(obj)
+ }
+
+ return undefined
}
diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts
index 6344357790..c87b77fe9e 100644
--- a/packages/router-core/src/route.ts
+++ b/packages/router-core/src/route.ts
@@ -1,3 +1,4 @@
+import invariant from 'tiny-invariant'
import { joinPaths, trimPathLeft } from './path'
import { notFound } from './not-found'
import { rootRouteId } from './root'
@@ -17,9 +18,12 @@ import type { AnyRouter, RegisteredRouter } from './router'
import type { BuildLocationFn, NavigateFn } from './RouterProvider'
import type {
Assign,
+ Awaitable,
Constrain,
Expand,
IntersectAssign,
+ LooseAsyncReturnType,
+ LooseReturnType,
NoInfer,
} from './utils'
import type {
@@ -150,27 +154,24 @@ export type ResolveSearchSchema =
? ResolveSearchSchemaFn
: ResolveSearchSchemaFn
-export type ParseSplatParams = TPath &
- `${string}$` extends never
- ? TPath & `${string}$/${string}` extends never
- ? never
- : '_splat'
- : '_splat'
+export type ResolveRequiredParams = {
+ [K in ParsePathParams['required']]: T
+}
-export interface SplatParams {
- _splat?: string
+export type ResolveOptionalParams = {
+ [K in ParsePathParams['optional']]?: T
}
-export type ResolveParams =
- ParseSplatParams extends never
- ? Record, string>
- : Record, string> & SplatParams
+export type ResolveParams<
+ TPath extends string,
+ T = string,
+> = ResolveRequiredParams & ResolveOptionalParams
export type ParseParamsFn = (
- rawParams: ResolveParams,
-) => TParams extends Record, any>
+ rawParams: Expand>,
+) => TParams extends ResolveParams
? TParams
- : Record, any>
+ : ResolveParams
export type StringifyParamsFn = (
params: TParams,
@@ -270,20 +271,6 @@ export type TrimPathRight = T extends '/'
? TrimPathRight
: T
-export type LooseReturnType = T extends (
- ...args: Array
-) => infer TReturn
- ? TReturn
- : never
-
-export type LooseAsyncReturnType = T extends (
- ...args: Array
-) => infer TReturn
- ? TReturn extends Promise
- ? TReturn
- : TReturn
- : never
-
export type ContextReturnType = unknown extends TContextFn
? TContextFn
: LooseReturnType extends never
@@ -448,7 +435,7 @@ export interface RouteExtensions {
}
export type RouteLazyFn = (
- lazyFn: () => Promise,
+ lazyFn: () => Promise>,
) => TRoute
export type RouteAddChildrenFn<
@@ -602,7 +589,26 @@ export interface Route<
>
isRoot: TParentRoute extends AnyRoute ? true : false
_componentsPromise?: Promise>
- lazyFn?: () => Promise
+ lazyFn?: () => Promise<
+ LazyRoute<
+ Route<
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes
+ >
+ >
+ >
_lazyPromise?: Promise
rank: number
to: TrimPathRight
@@ -621,7 +627,24 @@ export interface Route<
TBeforeLoadFn
>,
) => this
- lazy: RouteLazyFn
+ lazy: RouteLazyFn<
+ Route<
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes
+ >
+ >
addChildren: RouteAddChildrenFn<
TParentRoute,
TPath,
@@ -960,7 +983,7 @@ type AssetFnContextOptions<
TLoaderDeps
>
params: ResolveAllParamsFromParent
- loaderData: ResolveLoaderData
+ loaderData?: ResolveLoaderData
}
export interface DefaultUpdatableRouteOptionsExtensions {
@@ -1070,9 +1093,20 @@ export interface UpdatableRouteOptions<
TLoaderDeps
>,
) => void
- headers?: (ctx: {
- loaderData: ResolveLoaderData
- }) => Record
+ headers?: (
+ ctx: AssetFnContextOptions<
+ TRouteId,
+ TFullPath,
+ TParentRoute,
+ TParams,
+ TSearchValidator,
+ TLoaderFn,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps
+ >,
+ ) => Awaitable>
head?: (
ctx: AssetFnContextOptions<
TRouteId,
@@ -1086,11 +1120,11 @@ export interface UpdatableRouteOptions<
TBeforeLoadFn,
TLoaderDeps
>,
- ) => {
+ ) => Awaitable<{
links?: AnyRouteMatch['links']
scripts?: AnyRouteMatch['headScripts']
meta?: AnyRouteMatch['meta']
- }
+ }>
scripts?: (
ctx: AssetFnContextOptions<
TRouteId,
@@ -1104,7 +1138,7 @@ export interface UpdatableRouteOptions<
TBeforeLoadFn,
TLoaderDeps
>,
- ) => AnyRouteMatch['scripts']
+ ) => Awaitable
ssr?: boolean
codeSplitGroupings?: Array<
Array<
@@ -1336,7 +1370,26 @@ export class BaseRoute<
children?: TChildren
originalIndex?: number
rank!: number
- lazyFn?: () => Promise
+ lazyFn?: () => Promise<
+ LazyRoute<
+ Route<
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes
+ >
+ >
+ >
_lazyPromise?: Promise
_componentsPromise?: Promise>
@@ -1409,7 +1462,8 @@ export class BaseRoute<
if (isRoot) {
this._path = rootRouteId as TPath
} else if (!this.parentRoute) {
- throw new Error(
+ invariant(
+ false,
`Child Route instances must pass a 'getParentRoute: () => ParentRoute' option that returns a Route instance.`,
)
}
@@ -1449,6 +1503,16 @@ export class BaseRoute<
this._ssr = options?.ssr ?? opts.defaultSsr ?? true
}
+ clone = (other: typeof this) => {
+ this._path = other._path
+ this._id = other._id
+ this._fullPath = other._fullPath
+ this._to = other._to
+ this._ssr = other._ssr
+ this.options.getParentRoute = other.options.getParentRoute
+ this.children = other.children
+ }
+
addChildren: RouteAddChildrenFn<
TParentRoute,
TPath,
@@ -1562,7 +1626,24 @@ export class BaseRoute<
return this
}
- lazy: RouteLazyFn = (lazyFn) => {
+ lazy: RouteLazyFn<
+ Route<
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes
+ >
+ > = (lazyFn) => {
this.lazyFn = lazyFn
return this
}
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 3a510d6506..7c45828fa1 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -28,7 +28,7 @@ import { isNotFound } from './not-found'
import { setupScrollRestoration } from './scroll-restoration'
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
import { rootRouteId } from './root'
-import { isRedirect, isResolvedRedirect } from './redirect'
+import { isRedirect } from './redirect'
import type { SearchParser, SearchSerializer } from './searchParams'
import type { AnyRedirect, ResolvedRedirect } from './redirect'
import type {
@@ -165,6 +165,14 @@ export interface RouterOptions<
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-delay)
*/
defaultPreloadDelay?: number
+ /**
+ * The default `preloadIntentProximity` a route should use if no preloadIntentProximity is provided.
+ *
+ * @default 0
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadintentproximity-property)
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-intent-proximity)
+ */
+ defaultPreloadIntentProximity?: number
/**
* The default `pendingMs` a route should use if no pendingMs is provided.
*
@@ -407,7 +415,7 @@ export interface RouterState<
location: ParsedLocation>
resolvedLocation?: ParsedLocation>
statusCode: number
- redirect?: ResolvedRedirect
+ redirect?: AnyRedirect
}
export interface BuildNextOptions {
@@ -425,8 +433,9 @@ export interface BuildNextOptions {
unmaskOnReload?: boolean
}
from?: string
- _fromLocation?: ParsedLocation
href?: string
+ _fromLocation?: ParsedLocation
+ unsafeRelative?: 'path'
}
type NavigationEventInfo = {
@@ -513,11 +522,6 @@ export interface RouterErrorSerializer {
deserialize: (err: TSerializedError) => unknown
}
-export interface MatchedRoutesResult {
- matchedRoutes: Array
- routeParams: Record
-}
-
export type PreloadRouteFn<
TRouteTree extends AnyRoute,
TTrailingSlashOption extends TrailingSlashOption,
@@ -593,8 +597,8 @@ export type ParseLocationFn = (
) => ParsedLocation>
export type GetMatchRoutesFn = (
- next: ParsedLocation,
- dest?: BuildNextOptions,
+ pathname: string,
+ routePathname: string | undefined,
) => {
matchedRoutes: Array
routeParams: Record
@@ -836,6 +840,8 @@ export class RouterCore<
// router can be used in a non-react environment if necessary
startTransition: StartTransitionFn = (fn) => fn()
+ isShell = false
+
update: UpdateFn<
TRouteTree,
TTrailingSlashOption,
@@ -882,7 +888,6 @@ export class RouterCore<
}
if (
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
!this.history ||
(this.options.history && this.options.history !== this.history)
) {
@@ -901,7 +906,6 @@ export class RouterCore<
this.buildRouteTree()
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!this.__store) {
this.__store = new Store(getInitialRouterState(this.latestLocation), {
onUpdate: () => {
@@ -920,13 +924,16 @@ export class RouterCore<
if (
typeof window !== 'undefined' &&
'CSS' in window &&
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
typeof window.CSS?.supports === 'function'
) {
this.isViewTransitionTypesSupported = window.CSS.supports(
'selector(:active-view-transition-type(a)',
)
}
+
+ if ((this.latestLocation.search as any).__TSS_SHELL) {
+ this.isShell = true
+ }
}
get state() {
@@ -934,124 +941,29 @@ export class RouterCore<
}
buildRouteTree = () => {
- this.routesById = {} as RoutesById
- this.routesByPath = {} as RoutesByPath
-
- const notFoundRoute = this.options.notFoundRoute
- if (notFoundRoute) {
- notFoundRoute.init({
- originalIndex: 99999999999,
- defaultSsr: this.options.defaultSsr,
- })
- ;(this.routesById as any)[notFoundRoute.id] = notFoundRoute
- }
-
- const recurseRoutes = (childRoutes: Array) => {
- childRoutes.forEach((childRoute, i) => {
- childRoute.init({
+ const { routesById, routesByPath, flatRoutes } = processRouteTree({
+ routeTree: this.routeTree,
+ initRoute: (route, i) => {
+ route.init({
originalIndex: i,
defaultSsr: this.options.defaultSsr,
})
-
- const existingRoute = (this.routesById as any)[childRoute.id]
-
- invariant(
- !existingRoute,
- `Duplicate routes found with id: ${String(childRoute.id)}`,
- )
- ;(this.routesById as any)[childRoute.id] = childRoute
-
- if (!childRoute.isRoot && childRoute.path) {
- const trimmedFullPath = trimPathRight(childRoute.fullPath)
- if (
- !(this.routesByPath as any)[trimmedFullPath] ||
- childRoute.fullPath.endsWith('/')
- ) {
- ;(this.routesByPath as any)[trimmedFullPath] = childRoute
- }
- }
-
- const children = childRoute.children
-
- if (children?.length) {
- recurseRoutes(children)
- }
- })
- }
-
- recurseRoutes([this.routeTree])
-
- const scoredRoutes: Array<{
- child: AnyRoute
- trimmed: string
- parsed: ReturnType
- index: number
- scores: Array
- }> = []
-
- const routes: Array = Object.values(this.routesById)
-
- routes.forEach((d, i) => {
- if (d.isRoot || !d.path) {
- return
- }
-
- const trimmed = trimPathLeft(d.fullPath)
- const parsed = parsePathname(trimmed)
-
- while (parsed.length > 1 && parsed[0]?.value === '/') {
- parsed.shift()
- }
-
- const scores = parsed.map((segment) => {
- if (segment.value === '/') {
- return 0.75
- }
-
- if (segment.type === 'param') {
- return 0.5
- }
-
- if (segment.type === 'wildcard') {
- return 0.25
- }
-
- return 1
- })
-
- scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
+ },
})
- this.flatRoutes = scoredRoutes
- .sort((a, b) => {
- const minLength = Math.min(a.scores.length, b.scores.length)
+ this.routesById = routesById as RoutesById
+ this.routesByPath = routesByPath as RoutesByPath
+ this.flatRoutes = flatRoutes as Array
- // Sort by min available score
- for (let i = 0; i < minLength; i++) {
- if (a.scores[i] !== b.scores[i]) {
- return b.scores[i]! - a.scores[i]!
- }
- }
-
- // Sort by length of score
- if (a.scores.length !== b.scores.length) {
- return b.scores.length - a.scores.length
- }
-
- // Sort by min available parsed value
- for (let i = 0; i < minLength; i++) {
- if (a.parsed[i]!.value !== b.parsed[i]!.value) {
- return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
- }
- }
+ const notFoundRoute = this.options.notFoundRoute
- // Sort by original index
- return a.index - b.index
- })
- .map((d, i) => {
- d.child.rank = i
- return d.child
+ if (notFoundRoute) {
+ notFoundRoute.init({
+ originalIndex: 99999999999,
+ defaultSsr: this.options.defaultSsr,
})
+ this.routesById[notFoundRoute.id] = notFoundRoute
+ }
}
subscribe: SubscribeFn = (eventType, fn) => {
@@ -1155,9 +1067,9 @@ export class RouterCore<
} as ParsedLocation,
opts,
)
- } else {
- return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
}
+
+ return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
}
private matchRoutesInternal(
@@ -1165,8 +1077,8 @@ export class RouterCore<
opts?: MatchRoutesOpts,
): Array {
const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
- next,
- opts?.dest,
+ next.pathname,
+ opts?.dest?.to as string,
)
let isGlobalNotFound = false
@@ -1418,7 +1330,8 @@ export class RouterCore<
const route = this.looseRoutesById[match.routeId]!
const existingMatch = this.getMatch(match.id)
- // only execute `context` if we are not just building a location
+ // only execute `context` if we are not calling from router.buildLocation
+
if (!existingMatch && opts?._buildLocation !== true) {
const parentMatch = matches[index - 1]
const parentContext = getParentContext(parentMatch)
@@ -1447,72 +1360,24 @@ export class RouterCore<
...match.__beforeLoadContext,
}
}
-
- // If it's already a success, update headers and head content
- // These may get updated again if the match is refreshed
- // due to being stale
- if (match.status === 'success') {
- match.headers = route.options.headers?.({
- loaderData: match.loaderData,
- })
- const assetContext = {
- matches,
- match,
- params: match.params,
- loaderData: match.loaderData,
- }
- const headFnContent = route.options.head?.(assetContext)
- match.links = headFnContent?.links
- match.headScripts = headFnContent?.scripts
- match.meta = headFnContent?.meta
- match.scripts = route.options.scripts?.(assetContext)
- }
})
return matches
}
- getMatchedRoutes: GetMatchRoutesFn = (next, dest) => {
- let routeParams: Record = {}
- const trimmedPath = trimPathRight(next.pathname)
- const getMatchedParams = (route: AnyRoute) => {
- const result = matchPathname(this.basepath, trimmedPath, {
- to: route.fullPath,
- caseSensitive:
- route.options.caseSensitive ?? this.options.caseSensitive,
- fuzzy: true,
- })
- return result
- }
-
- let foundRoute: AnyRoute | undefined =
- dest?.to !== undefined ? this.routesByPath[dest.to!] : undefined
- if (foundRoute) {
- routeParams = getMatchedParams(foundRoute)!
- } else {
- foundRoute = this.flatRoutes.find((route) => {
- const matchedParams = getMatchedParams(route)
-
- if (matchedParams) {
- routeParams = matchedParams
- return true
- }
-
- return false
- })
- }
-
- let routeCursor: AnyRoute =
- foundRoute || (this.routesById as any)[rootRouteId]
-
- const matchedRoutes: Array = [routeCursor]
-
- while (routeCursor.parentRoute) {
- routeCursor = routeCursor.parentRoute
- matchedRoutes.unshift(routeCursor)
- }
-
- return { matchedRoutes, routeParams, foundRoute }
+ getMatchedRoutes: GetMatchRoutesFn = (
+ pathname: string,
+ routePathname: string | undefined,
+ ) => {
+ return getMatchedRoutes({
+ pathname,
+ routePathname,
+ basepath: this.basepath,
+ caseSensitive: this.options.caseSensitive,
+ routesByPath: this.routesByPath,
+ routesById: this.routesById,
+ flatRoutes: this.flatRoutes,
+ })
}
cancelMatch = (id: string) => {
@@ -1535,75 +1400,67 @@ export class RouterCore<
dest: BuildNextOptions & {
unmaskOnReload?: boolean
} = {},
- matchedRoutesResult?: MatchedRoutesResult,
): ParsedLocation => {
- const fromMatches = dest._fromLocation
- ? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
- : this.state.matches
-
- const fromMatch =
- dest.from != null
- ? fromMatches.find((d) =>
- matchPathname(this.basepath, trimPathRight(d.pathname), {
- to: dest.from,
- caseSensitive: false,
- fuzzy: false,
- }),
- )
- : undefined
+ // We allow the caller to override the current location
+ const currentLocation = dest._fromLocation || this.latestLocation
- const fromPath = fromMatch?.pathname || this.latestLocation.pathname
-
- invariant(
- dest.from == null || fromMatch != null,
- 'Could not find match for from: ' + dest.from,
- )
+ const allFromMatches = this.matchRoutes(currentLocation, {
+ _buildLocation: true,
+ })
- const fromSearch = this.state.pendingMatches?.length
- ? last(this.state.pendingMatches)?.search
- : last(fromMatches)?.search || this.latestLocation.search
+ const lastMatch = last(allFromMatches)!
+
+ // First let's find the starting pathname
+ // By default, start with the current location
+ let fromPath = lastMatch.fullPath
+
+ // If there is a to, it means we are changing the path in some way
+ // So we need to find the relative fromPath
+ if (dest.unsafeRelative === 'path') {
+ fromPath = currentLocation.pathname
+ } else if (dest.to && dest.from) {
+ fromPath = dest.from
+ const existingFrom = [...allFromMatches].reverse().find((d) => {
+ return (
+ d.fullPath === fromPath || d.fullPath === joinPaths([fromPath, '/'])
+ )
+ })
- const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
- fromMatches.find((e) => e.routeId === d.id),
- )
- let pathname: string
- if (dest.to) {
- const resolvePathTo =
- fromMatch?.fullPath ||
- last(fromMatches)?.fullPath ||
- this.latestLocation.pathname
- pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
- } else {
- const fromRouteByFromPathRouteId =
- this.routesById[
- stayingMatches?.find((route) => {
- const interpolatedPath = interpolatePath({
- path: route.fullPath,
- params: matchedRoutesResult?.routeParams ?? {},
- decodeCharMap: this.pathParamsDecodeCharMap,
- }).interpolatedPath
- const pathname = joinPaths([this.basepath, interpolatedPath])
- return pathname === fromPath
- })?.id as keyof this['routesById']
- ]
- pathname = this.resolvePathWithBase(
- fromPath,
- fromRouteByFromPathRouteId?.to ?? fromPath,
- )
+ if (!existingFrom) {
+ console.warn(`Could not find match for from: ${dest.from}`)
+ }
}
- const prevParams = { ...last(fromMatches)?.params }
+ // From search should always use the current location
+ const fromSearch = lastMatch.search
+ // Same with params. It can't hurt to provide as many as possible
+ const fromParams = { ...lastMatch.params }
+
+ // Resolve the next to
+ const nextTo = dest.to
+ ? this.resolvePathWithBase(fromPath, `${dest.to}`)
+ : fromPath
+ // Resolve the next params
let nextParams =
(dest.params ?? true) === true
- ? prevParams
+ ? fromParams
: {
- ...prevParams,
- ...functionalUpdate(dest.params as any, prevParams),
+ ...fromParams,
+ ...functionalUpdate(dest.params as any, fromParams),
}
+ const destRoutes = this.matchRoutes(
+ nextTo,
+ {},
+ {
+ _buildLocation: true,
+ },
+ ).map((d) => this.looseRoutesById[d.routeId]!)
+
+ // If there are any params, we need to stringify them
if (Object.keys(nextParams).length > 0) {
- matchedRoutesResult?.matchedRoutes
+ destRoutes
.map((route) => {
return (
route.options.params?.stringify ?? route.options.stringifyParams
@@ -1615,25 +1472,27 @@ export class RouterCore<
})
}
- pathname = interpolatePath({
- path: pathname,
+ // Interpolate the next to into the next pathname
+ const nextPathname = interpolatePath({
+ path: nextTo,
params: nextParams ?? {},
leaveWildcards: false,
leaveParams: opts.leaveParams,
decodeCharMap: this.pathParamsDecodeCharMap,
}).interpolatedPath
- let search = fromSearch
+ // Resolve the next search
+ let nextSearch = fromSearch
if (opts._includeValidateSearch && this.options.search?.strict) {
let validatedSearch = {}
- matchedRoutesResult?.matchedRoutes.forEach((route) => {
+ destRoutes.forEach((route) => {
try {
if (route.options.validateSearch) {
validatedSearch = {
...validatedSearch,
...(validateSearch(route.options.validateSearch, {
...validatedSearch,
- ...search,
+ ...nextSearch,
}) ?? {}),
}
}
@@ -1641,137 +1500,52 @@ export class RouterCore<
// ignore errors here because they are already handled in matchRoutes
}
})
- search = validatedSearch
+ nextSearch = validatedSearch
}
- const applyMiddlewares = (search: any) => {
- const allMiddlewares =
- matchedRoutesResult?.matchedRoutes.reduce(
- (acc, route) => {
- const middlewares: Array> = []
- if ('search' in route.options) {
- if (route.options.search?.middlewares) {
- middlewares.push(...route.options.search.middlewares)
- }
- }
- // TODO remove preSearchFilters and postSearchFilters in v2
- else if (
- route.options.preSearchFilters ||
- route.options.postSearchFilters
- ) {
- const legacyMiddleware: SearchMiddleware = ({
- search,
- next,
- }) => {
- let nextSearch = search
- if (
- 'preSearchFilters' in route.options &&
- route.options.preSearchFilters
- ) {
- nextSearch = route.options.preSearchFilters.reduce(
- (prev, next) => next(prev),
- search,
- )
- }
- const result = next(nextSearch)
- if (
- 'postSearchFilters' in route.options &&
- route.options.postSearchFilters
- ) {
- return route.options.postSearchFilters.reduce(
- (prev, next) => next(prev),
- result,
- )
- }
- return result
- }
- middlewares.push(legacyMiddleware)
- }
- if (opts._includeValidateSearch && route.options.validateSearch) {
- const validate: SearchMiddleware = ({ search, next }) => {
- const result = next(search)
- try {
- const validatedSearch = {
- ...result,
- ...(validateSearch(
- route.options.validateSearch,
- result,
- ) ?? {}),
- }
- return validatedSearch
- } catch {
- // ignore errors here because they are already handled in matchRoutes
- return result
- }
- }
- middlewares.push(validate)
- }
- return acc.concat(middlewares)
- },
- [] as Array>,
- ) ?? []
-
- // the chain ends here since `next` is not called
- const final: SearchMiddleware = ({ search }) => {
- if (!dest.search) {
- return {}
- }
- if (dest.search === true) {
- return search
- }
- return functionalUpdate(dest.search, search)
- }
- allMiddlewares.push(final)
-
- const applyNext = (index: number, currentSearch: any): any => {
- // no more middlewares left, return the current search
- if (index >= allMiddlewares.length) {
- return currentSearch
- }
-
- const middleware = allMiddlewares[index]!
-
- const next = (newSearch: any): any => {
- return applyNext(index + 1, newSearch)
- }
-
- return middleware({ search: currentSearch, next })
- }
-
- // Start applying middlewares
- return applyNext(0, search)
- }
+ nextSearch = applySearchMiddleware({
+ search: nextSearch,
+ dest,
+ destRoutes,
+ _includeValidateSearch: opts._includeValidateSearch,
+ })
- search = applyMiddlewares(search)
+ // Replace the equal deep
+ nextSearch = replaceEqualDeep(fromSearch, nextSearch)
- search = replaceEqualDeep(fromSearch, search)
- const searchStr = this.options.stringifySearch(search)
+ // Stringify the next search
+ const searchStr = this.options.stringifySearch(nextSearch)
+ // Resolve the next hash
const hash =
dest.hash === true
- ? this.latestLocation.hash
+ ? currentLocation.hash
: dest.hash
- ? functionalUpdate(dest.hash, this.latestLocation.hash)
+ ? functionalUpdate(dest.hash, currentLocation.hash)
: undefined
+ // Resolve the next hash string
const hashStr = hash ? `#${hash}` : ''
+ // Resolve the next state
let nextState =
dest.state === true
- ? this.latestLocation.state
+ ? currentLocation.state
: dest.state
- ? functionalUpdate(dest.state, this.latestLocation.state)
+ ? functionalUpdate(dest.state, currentLocation.state)
: {}
- nextState = replaceEqualDeep(this.latestLocation.state, nextState)
+ // Replace the equal deep
+ nextState = replaceEqualDeep(currentLocation.state, nextState)
+ // Return the next location
return {
- pathname,
- search,
+ pathname: nextPathname,
+ search: nextSearch,
searchStr,
state: nextState as any,
hash: hash ?? '',
- href: `${pathname}${searchStr}${hashStr}`,
+ href: `${nextPathname}${searchStr}${hashStr}`,
unmaskOnReload: dest.unmaskOnReload,
}
}
@@ -1781,6 +1555,7 @@ export class RouterCore<
maskedDest?: BuildNextOptions,
) => {
const next = build(dest)
+
let maskedNext = maskedDest ? build(maskedDest) : undefined
if (!maskedNext) {
@@ -1812,16 +1587,12 @@ export class RouterCore<
}
}
- const nextMatches = this.getMatchedRoutes(next, dest)
- const final = build(dest, nextMatches)
-
if (maskedNext) {
- const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
- const maskedFinal = build(maskedDest, maskedMatches)
- final.maskedLocation = maskedFinal
+ const maskedFinal = build(maskedDest)
+ next.maskedLocation = maskedFinal
}
- return final
+ return next
}
if (opts.mask) {
@@ -1958,6 +1729,13 @@ export class RouterCore<
}
navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
+ if (!reloadDocument && href) {
+ try {
+ new URL(`${href}`)
+ reloadDocument = true
+ } catch {}
+ }
+
if (reloadDocument) {
if (!href) {
const location = this.buildLocation({ to, ...rest } as any)
@@ -1980,10 +1758,30 @@ export class RouterCore<
latestLoadPromise: undefined | Promise
- load: LoadFn = async (opts?: { sync?: boolean }): Promise => {
+ beforeLoad = () => {
+ // Cancel any pending matches
+ this.cancelMatches()
this.latestLocation = this.parseLocation(this.latestLocation)
- let redirect: ResolvedRedirect | undefined
+ // Match the routes
+ const pendingMatches = this.matchRoutes(this.latestLocation)
+
+ // Ingest the new matches
+ this.__store.setState((s) => ({
+ ...s,
+ status: 'pending',
+ isLoading: true,
+ location: this.latestLocation,
+ pendingMatches,
+ // If a cached moved to pendingMatches, remove it from cachedMatches
+ cachedMatches: s.cachedMatches.filter((d) => {
+ return !pendingMatches.find((e) => e.id === d.id)
+ }),
+ }))
+ }
+
+ load: LoadFn = async (opts?: { sync?: boolean }): Promise => {
+ let redirect: AnyRedirect | undefined
let notFound: NotFoundError | undefined
let loadPromise: Promise
@@ -1992,36 +1790,10 @@ export class RouterCore<
loadPromise = new Promise((resolve) => {
this.startTransition(async () => {
try {
+ this.beforeLoad()
const next = this.latestLocation
const prevLocation = this.state.resolvedLocation
- // Cancel any pending matches
- this.cancelMatches()
-
- let pendingMatches!: Array
-
- batch(() => {
- // this call breaks a route context of destination route after a redirect
- // we should be fine not eagerly calling this since we call it later
- // this.clearExpiredCache()
-
- // Match the routes
- pendingMatches = this.matchRoutes(next)
-
- // Ingest the new matches
- this.__store.setState((s) => ({
- ...s,
- status: 'pending',
- isLoading: true,
- location: next,
- pendingMatches,
- // If a cached moved to pendingMatches, remove it from cachedMatches
- cachedMatches: s.cachedMatches.filter((d) => {
- return !pendingMatches.find((e) => e.id === d.id)
- }),
- }))
- })
-
if (!this.state.redirect) {
this.emit({
type: 'onBeforeNavigate',
@@ -2042,7 +1814,7 @@ export class RouterCore<
await this.loadMatches({
sync: opts?.sync,
- matches: pendingMatches,
+ matches: this.state.pendingMatches as Array,
location: next,
// eslint-disable-next-line @typescript-eslint/require-await
onReady: async () => {
@@ -2103,11 +1875,11 @@ export class RouterCore<
},
})
} catch (err) {
- if (isResolvedRedirect(err)) {
+ if (isRedirect(err)) {
redirect = err
if (!this.isServer) {
this.navigate({
- ...redirect,
+ ...redirect.options,
replace: true,
ignoreBlocker: true,
})
@@ -2119,7 +1891,7 @@ export class RouterCore<
this.__store.setState((s) => ({
...s,
statusCode: redirect
- ? redirect.statusCode
+ ? redirect.status
: notFound
? 404
: s.matches.some((d) => d.status === 'error')
@@ -2275,13 +2047,15 @@ export class RouterCore<
}
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
- if (isResolvedRedirect(err)) {
- if (!err.reloadDocument) {
- throw err
+ if (isRedirect(err) || isNotFound(err)) {
+ if (isRedirect(err)) {
+ if (err.redirectHandled) {
+ if (!err.options.reloadDocument) {
+ throw err
+ }
+ }
}
- }
- if (isRedirect(err) || isNotFound(err)) {
updateMatch(match.id, (prev) => ({
...prev,
status: isRedirect(err)
@@ -2305,7 +2079,9 @@ export class RouterCore<
if (isRedirect(err)) {
rendered = true
- err = this.resolveRedirect({ ...err, _fromLocation: location })
+ err.options._fromLocation = location
+ err.redirectHandled = true
+ err = this.resolveRedirect(err)
throw err
} else if (isNotFound(err)) {
this._handleNotFound(matches, err, {
@@ -2609,6 +2385,31 @@ export class RouterCore<
!this.state.matches.find((d) => d.id === matchId),
}))
+ const executeHead = async () => {
+ const match = this.getMatch(matchId)
+ // in case of a redirecting match during preload, the match does not exist
+ if (!match) {
+ return
+ }
+ const assetContext = {
+ matches,
+ match,
+ params: match.params,
+ loaderData: match.loaderData,
+ }
+ const headFnContent =
+ await route.options.head?.(assetContext)
+ const meta = headFnContent?.meta
+ const links = headFnContent?.links
+ const headScripts = headFnContent?.scripts
+
+ const scripts =
+ await route.options.scripts?.(assetContext)
+ const headers =
+ await route.options.headers?.(assetContext)
+ return { meta, links, headScripts, headers, scripts }
+ }
+
const runLoader = async () => {
try {
// If the Matches component rendered
@@ -2649,23 +2450,6 @@ export class RouterCore<
await potentialPendingMinPromise()
- const assetContext = {
- matches,
- match: this.getMatch(matchId)!,
- params: this.getMatch(matchId)!.params,
- loaderData,
- }
- const headFnContent =
- route.options.head?.(assetContext)
- const meta = headFnContent?.meta
- const links = headFnContent?.links
- const headScripts = headFnContent?.scripts
-
- const scripts = route.options.scripts?.(assetContext)
- const headers = route.options.headers?.({
- loaderData,
- })
-
// Last but not least, wait for the the components
// to be preloaded before we resolve the match
await route._componentsPromise
@@ -2677,11 +2461,11 @@ export class RouterCore<
isFetching: false,
updatedAt: Date.now(),
loaderData,
- meta,
- links,
- headScripts,
- headers,
- scripts,
+ }))
+ const head = await executeHead()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ ...head,
}))
} catch (e) {
let error = e
@@ -2699,12 +2483,13 @@ export class RouterCore<
onErrorError,
)
}
-
+ const head = await executeHead()
updateMatch(matchId, (prev) => ({
...prev,
error,
status: 'error',
isFetching: false,
+ ...head,
}))
}
@@ -2713,9 +2498,12 @@ export class RouterCore<
match: this.getMatch(matchId)!,
})
} catch (err) {
+ const head = await executeHead()
+
updateMatch(matchId, (prev) => ({
...prev,
loaderPromise: undefined,
+ ...head,
}))
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
}
@@ -2742,8 +2530,8 @@ export class RouterCore<
loaderPromise: undefined,
}))
} catch (err) {
- if (isResolvedRedirect(err)) {
- await this.navigate(err)
+ if (isRedirect(err)) {
+ await this.navigate(err.options)
}
}
})()
@@ -2752,6 +2540,15 @@ export class RouterCore<
(loaderShouldRunAsync && sync)
) {
await runLoader()
+ } else {
+ // if the loader did not run, still update head.
+ // reason: parent's beforeLoad may have changed the route context
+ // and only now do we know the route context (and that the loader would not run)
+ const head = await executeHead()
+ updateMatch(matchId, (prev) => ({
+ ...prev,
+ ...head,
+ }))
}
}
if (!loaderIsRunningAsync) {
@@ -2828,11 +2625,14 @@ export class RouterCore<
return this.load({ sync: opts?.sync })
}
- resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
- const redirect = err as ResolvedRedirect
+ resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
+ if (!redirect.options.href) {
+ redirect.options.href = this.buildLocation(redirect.options).href
+ redirect.headers.set('Location', redirect.options.href)
+ }
- if (!redirect.href) {
- redirect.href = this.buildLocation(redirect as any).href
+ if (!redirect.headers.get('Location')) {
+ redirect.headers.set('Location', redirect.options.href)
}
return redirect
@@ -2967,11 +2767,12 @@ export class RouterCore<
return matches
} catch (err) {
if (isRedirect(err)) {
- if (err.reloadDocument) {
+ if (err.options.reloadDocument) {
return undefined
}
+
return await this.preloadRoute({
- ...(err as any),
+ ...err.options,
_fromLocation: next,
})
}
@@ -3205,3 +3006,338 @@ function routeNeedsPreload(route: AnyRoute) {
}
return false
}
+
+interface RouteLike {
+ id: string
+ isRoot?: boolean
+ path?: string
+ fullPath: string
+ rank?: number
+ parentRoute?: RouteLike
+ children?: Array
+ options?: {
+ caseSensitive?: boolean
+ }
+}
+
+export function processRouteTree({
+ routeTree,
+ initRoute,
+}: {
+ routeTree: TRouteLike
+ initRoute?: (route: TRouteLike, index: number) => void
+}) {
+ const routesById = {} as Record
+ const routesByPath = {} as Record
+
+ const recurseRoutes = (childRoutes: Array) => {
+ childRoutes.forEach((childRoute, i) => {
+ initRoute?.(childRoute, i)
+
+ const existingRoute = routesById[childRoute.id]
+
+ invariant(
+ !existingRoute,
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
+ )
+
+ routesById[childRoute.id] = childRoute
+
+ if (!childRoute.isRoot && childRoute.path) {
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
+ if (
+ !routesByPath[trimmedFullPath] ||
+ childRoute.fullPath.endsWith('/')
+ ) {
+ routesByPath[trimmedFullPath] = childRoute
+ }
+ }
+
+ const children = childRoute.children as Array
+
+ if (children?.length) {
+ recurseRoutes(children)
+ }
+ })
+ }
+
+ recurseRoutes([routeTree])
+
+ const scoredRoutes: Array<{
+ child: TRouteLike
+ trimmed: string
+ parsed: ReturnType
+ index: number
+ scores: Array
+ }> = []
+
+ const routes: Array = Object.values(routesById)
+
+ routes.forEach((d, i) => {
+ if (d.isRoot || !d.path) {
+ return
+ }
+
+ const trimmed = trimPathLeft(d.fullPath)
+ const parsed = parsePathname(trimmed)
+
+ // Removes the leading slash if it is not the only remaining segment
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
+ parsed.shift()
+ }
+
+ const scores = parsed.map((segment) => {
+ if (segment.value === '/') {
+ return 0.75
+ }
+
+ if (
+ segment.type === 'param' &&
+ segment.prefixSegment &&
+ segment.suffixSegment
+ ) {
+ return 0.55
+ }
+
+ if (segment.type === 'param' && segment.prefixSegment) {
+ return 0.52
+ }
+
+ if (segment.type === 'param' && segment.suffixSegment) {
+ return 0.51
+ }
+
+ if (segment.type === 'param') {
+ return 0.5
+ }
+
+ if (
+ segment.type === 'wildcard' &&
+ segment.prefixSegment &&
+ segment.suffixSegment
+ ) {
+ return 0.3
+ }
+
+ if (segment.type === 'wildcard' && segment.prefixSegment) {
+ return 0.27
+ }
+
+ if (segment.type === 'wildcard' && segment.suffixSegment) {
+ return 0.26
+ }
+
+ if (segment.type === 'wildcard') {
+ return 0.25
+ }
+
+ return 1
+ })
+
+ scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
+ })
+
+ const flatRoutes = scoredRoutes
+ .sort((a, b) => {
+ const minLength = Math.min(a.scores.length, b.scores.length)
+
+ // Sort by min available score
+ for (let i = 0; i < minLength; i++) {
+ if (a.scores[i] !== b.scores[i]) {
+ return b.scores[i]! - a.scores[i]!
+ }
+ }
+
+ // Sort by length of score
+ if (a.scores.length !== b.scores.length) {
+ return b.scores.length - a.scores.length
+ }
+
+ // Sort by min available parsed value
+ for (let i = 0; i < minLength; i++) {
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
+ return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
+ }
+ }
+
+ // Sort by original index
+ return a.index - b.index
+ })
+ .map((d, i) => {
+ d.child.rank = i
+ return d.child
+ })
+
+ return { routesById, routesByPath, flatRoutes }
+}
+
+export function getMatchedRoutes({
+ pathname,
+ routePathname,
+ basepath,
+ caseSensitive,
+ routesByPath,
+ routesById,
+ flatRoutes,
+}: {
+ pathname: string
+ routePathname?: string
+ basepath: string
+ caseSensitive?: boolean
+ routesByPath: Record
+ routesById: Record
+ flatRoutes: Array
+}) {
+ let routeParams: Record