diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index b906ea1159..e031ae811f 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -48,4 +48,4 @@ jobs:
- name: Build Packages
run: pnpm run build:all
- name: Publish Previews
- run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*'
+ run: pnpx pkg-pr-new publish --pnpm './packages/*' --template './examples/*/*'
diff --git a/e2e/solid-start/basic/.gitignore b/e2e/solid-start/basic/.gitignore
new file mode 100644
index 0000000000..be342025da
--- /dev/null
+++ b/e2e/solid-start/basic/.gitignore
@@ -0,0 +1,22 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+.vinxi
+
+/build/
+/api/
+/server/build
+/public/build
+.vinxi
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-start/basic/.prettierignore b/e2e/solid-start/basic/.prettierignore
new file mode 100644
index 0000000000..2be5eaa6ec
--- /dev/null
+++ b/e2e/solid-start/basic/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/solid-start/basic/app.config.ts b/e2e/solid-start/basic/app.config.ts
new file mode 100644
index 0000000000..5c531d7e3d
--- /dev/null
+++ b/e2e/solid-start/basic/app.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from '@tanstack/solid-start/config'
+import tsConfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ vite: {
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ ],
+ },
+})
diff --git a/e2e/solid-start/basic/app/api.ts b/e2e/solid-start/basic/app/api.ts
new file mode 100644
index 0000000000..ed511bcd26
--- /dev/null
+++ b/e2e/solid-start/basic/app/api.ts
@@ -0,0 +1,6 @@
+import {
+ createStartAPIHandler,
+ defaultAPIFileRouteHandler,
+} from '@tanstack/solid-start/api'
+
+export default createStartAPIHandler(defaultAPIFileRouteHandler)
diff --git a/e2e/solid-start/basic/app/client.tsx b/e2e/solid-start/basic/app/client.tsx
new file mode 100644
index 0000000000..ba0f02fac0
--- /dev/null
+++ b/e2e/solid-start/basic/app/client.tsx
@@ -0,0 +1,8 @@
+///
+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/app/components/CustomMessage.tsx b/e2e/solid-start/basic/app/components/CustomMessage.tsx
new file mode 100644
index 0000000000..c038b66d2f
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/CustomMessage.tsx
@@ -0,0 +1,8 @@
+export function CustomMessage({ message }: { message: string }) {
+ return (
+
+
This is a custom message:
+
{message}
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx
new file mode 100644
index 0000000000..32aed20e67
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+ {
+ router.invalidate()
+ }}
+ class={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot() ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/components/NotFound.tsx b/e2e/solid-start/basic/app/components/NotFound.tsx
new file mode 100644
index 0000000000..eb0a968d39
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/solid-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+ window.history.back()}
+ class="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/components/PostErrorComponent.tsx b/e2e/solid-start/basic/app/components/PostErrorComponent.tsx
new file mode 100644
index 0000000000..3e5d62c79b
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/PostErrorComponent.tsx
@@ -0,0 +1,5 @@
+import { ErrorComponent, ErrorComponentProps } from '@tanstack/solid-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
diff --git a/e2e/solid-start/basic/app/components/RedirectOnClick.tsx b/e2e/solid-start/basic/app/components/RedirectOnClick.tsx
new file mode 100644
index 0000000000..385748e2a5
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/RedirectOnClick.tsx
@@ -0,0 +1,26 @@
+import { useServerFn } from '@tanstack/solid-start'
+import { throwRedirect } from './throwRedirect'
+
+interface RedirectOnClickProps {
+ target: 'internal' | 'external'
+ reloadDocument?: boolean
+ externalHost?: string
+}
+
+export function RedirectOnClick({
+ target,
+ reloadDocument,
+ externalHost,
+}: RedirectOnClickProps) {
+ const execute = useServerFn(throwRedirect)
+ return (
+
+ execute({ data: { target, reloadDocument, externalHost } })
+ }
+ >
+ click me
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/components/UserErrorComponent.tsx b/e2e/solid-start/basic/app/components/UserErrorComponent.tsx
new file mode 100644
index 0000000000..69762add4c
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/UserErrorComponent.tsx
@@ -0,0 +1,5 @@
+import { ErrorComponent, ErrorComponentProps } from '@tanstack/solid-router'
+
+export function UserErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
diff --git a/e2e/solid-start/basic/app/components/throwRedirect.ts b/e2e/solid-start/basic/app/components/throwRedirect.ts
new file mode 100644
index 0000000000..fd4a056324
--- /dev/null
+++ b/e2e/solid-start/basic/app/components/throwRedirect.ts
@@ -0,0 +1,20 @@
+import { redirect } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const throwRedirect = createServerFn()
+ .validator(
+ (opts: {
+ target: 'internal' | 'external'
+ reloadDocument?: boolean
+ externalHost?: string
+ }) => opts,
+ )
+ .handler((ctx) => {
+ if (ctx.data.target === 'internal') {
+ throw redirect({ to: '/posts', reloadDocument: ctx.data.reloadDocument })
+ }
+ const href = ctx.data.externalHost ?? 'http://example.com'
+ throw redirect({
+ href,
+ })
+ })
diff --git a/e2e/solid-start/basic/app/routeTree.gen.ts b/e2e/solid-start/basic/app/routeTree.gen.ts
new file mode 100644
index 0000000000..db7101d5f5
--- /dev/null
+++ b/e2e/solid-start/basic/app/routeTree.gen.ts
@@ -0,0 +1,923 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as 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'
+
+// Create/Update Routes
+
+const UsersRoute = UsersImport.update({
+ id: '/users',
+ path: '/users',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const StreamRoute = StreamImport.update({
+ id: '/stream',
+ path: '/stream',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const SearchParamsRoute = SearchParamsImport.update({
+ id: '/search-params',
+ path: '/search-params',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ScriptsRoute = ScriptsImport.update({
+ id: '/scripts',
+ path: '/scripts',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const PostsRoute = PostsImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LinksRoute = LinksImport.update({
+ id: '/links',
+ path: '/links',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const DeferredRoute = DeferredImport.update({
+ id: '/deferred',
+ path: '/deferred',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LayoutRoute = LayoutImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const NotFoundRouteRoute = NotFoundRouteImport.update({
+ id: '/not-found',
+ path: '/not-found',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const UsersIndexRoute = UsersIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
+} as any)
+
+const RedirectIndexRoute = RedirectIndexImport.update({
+ id: '/redirect/',
+ path: '/redirect/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const PostsIndexRoute = PostsIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any)
+
+const NotFoundIndexRoute = NotFoundIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+
+const UsersUserIdRoute = UsersUserIdImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => UsersRoute,
+} as any)
+
+const RedirectTargetRoute = RedirectTargetImport.update({
+ id: '/redirect/$target',
+ path: '/redirect/$target',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const PostsPostIdRoute = PostsPostIdImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any)
+
+const NotFoundViaLoaderRoute = NotFoundViaLoaderImport.update({
+ id: '/via-loader',
+ path: '/via-loader',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+
+const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadImport.update({
+ id: '/via-beforeLoad',
+ path: '/via-beforeLoad',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
+
+const LayoutLayout2Route = LayoutLayout2Import.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any)
+
+const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => RedirectTargetRoute,
+} as any)
+
+const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({
+ id: '/via-loader',
+ path: '/via-loader',
+ getParentRoute: () => RedirectTargetRoute,
+} as any)
+
+const RedirectTargetViaBeforeLoadRoute =
+ RedirectTargetViaBeforeLoadImport.update({
+ id: '/via-beforeLoad',
+ path: '/via-beforeLoad',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+
+const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+
+const RedirectTargetServerFnIndexRoute =
+ RedirectTargetServerFnIndexImport.update({
+ id: '/serverFn/',
+ path: '/serverFn/',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+
+const RedirectTargetServerFnViaUseServerFnRoute =
+ RedirectTargetServerFnViaUseServerFnImport.update({
+ id: '/serverFn/via-useServerFn',
+ path: '/serverFn/via-useServerFn',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+
+const RedirectTargetServerFnViaLoaderRoute =
+ RedirectTargetServerFnViaLoaderImport.update({
+ id: '/serverFn/via-loader',
+ path: '/serverFn/via-loader',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+
+const RedirectTargetServerFnViaBeforeLoadRoute =
+ RedirectTargetServerFnViaBeforeLoadImport.update({
+ id: '/serverFn/via-beforeLoad',
+ path: '/serverFn/via-beforeLoad',
+ getParentRoute: () => RedirectTargetRoute,
+ } as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/not-found': {
+ id: '/not-found'
+ path: '/not-found'
+ fullPath: '/not-found'
+ preLoaderRoute: typeof NotFoundRouteImport
+ parentRoute: typeof rootRoute
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutImport
+ parentRoute: typeof rootRoute
+ }
+ '/deferred': {
+ id: '/deferred'
+ path: '/deferred'
+ fullPath: '/deferred'
+ preLoaderRoute: typeof DeferredImport
+ parentRoute: typeof rootRoute
+ }
+ '/links': {
+ id: '/links'
+ path: '/links'
+ fullPath: '/links'
+ preLoaderRoute: typeof LinksImport
+ parentRoute: typeof rootRoute
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsImport
+ parentRoute: typeof rootRoute
+ }
+ '/scripts': {
+ id: '/scripts'
+ path: '/scripts'
+ fullPath: '/scripts'
+ preLoaderRoute: typeof ScriptsImport
+ parentRoute: typeof rootRoute
+ }
+ '/search-params': {
+ id: '/search-params'
+ path: '/search-params'
+ fullPath: '/search-params'
+ preLoaderRoute: typeof SearchParamsImport
+ parentRoute: typeof rootRoute
+ }
+ '/stream': {
+ id: '/stream'
+ path: '/stream'
+ fullPath: '/stream'
+ preLoaderRoute: typeof StreamImport
+ parentRoute: typeof rootRoute
+ }
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersImport
+ parentRoute: typeof rootRoute
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2Import
+ parentRoute: typeof LayoutImport
+ }
+ '/not-found/via-beforeLoad': {
+ id: '/not-found/via-beforeLoad'
+ path: '/via-beforeLoad'
+ fullPath: '/not-found/via-beforeLoad'
+ preLoaderRoute: typeof NotFoundViaBeforeLoadImport
+ parentRoute: typeof NotFoundRouteImport
+ }
+ '/not-found/via-loader': {
+ id: '/not-found/via-loader'
+ path: '/via-loader'
+ fullPath: '/not-found/via-loader'
+ preLoaderRoute: typeof NotFoundViaLoaderImport
+ parentRoute: typeof NotFoundRouteImport
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdImport
+ parentRoute: typeof PostsImport
+ }
+ '/redirect/$target': {
+ id: '/redirect/$target'
+ path: '/redirect/$target'
+ fullPath: '/redirect/$target'
+ preLoaderRoute: typeof RedirectTargetImport
+ parentRoute: typeof rootRoute
+ }
+ '/users/$userId': {
+ id: '/users/$userId'
+ path: '/$userId'
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdImport
+ parentRoute: typeof UsersImport
+ }
+ '/not-found/': {
+ id: '/not-found/'
+ path: '/'
+ fullPath: '/not-found/'
+ preLoaderRoute: typeof NotFoundIndexImport
+ parentRoute: typeof NotFoundRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexImport
+ parentRoute: typeof PostsImport
+ }
+ '/redirect/': {
+ id: '/redirect/'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectIndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexImport
+ parentRoute: typeof UsersImport
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutAImport
+ parentRoute: typeof LayoutLayout2Import
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBImport
+ parentRoute: typeof LayoutLayout2Import
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepImport
+ 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
+ }
+ '/redirect/$target/via-loader': {
+ id: '/redirect/$target/via-loader'
+ path: '/via-loader'
+ fullPath: '/redirect/$target/via-loader'
+ preLoaderRoute: typeof RedirectTargetViaLoaderImport
+ parentRoute: typeof RedirectTargetImport
+ }
+ '/redirect/$target/': {
+ id: '/redirect/$target/'
+ path: '/'
+ fullPath: '/redirect/$target/'
+ preLoaderRoute: typeof RedirectTargetIndexImport
+ parentRoute: typeof RedirectTargetImport
+ }
+ '/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
+ }
+ '/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
+ }
+ '/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
+ }
+ '/redirect/$target/serverFn/': {
+ id: '/redirect/$target/serverFn/'
+ path: '/serverFn'
+ fullPath: '/redirect/$target/serverFn'
+ preLoaderRoute: typeof RedirectTargetServerFnIndexImport
+ parentRoute: typeof RedirectTargetImport
+ }
+ }
+}
+
+// Create and export the route tree
+
+interface NotFoundRouteRouteChildren {
+ NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
+ NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
+ NotFoundIndexRoute: typeof NotFoundIndexRoute
+}
+
+const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
+ NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
+ NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
+ NotFoundIndexRoute: NotFoundIndexRoute,
+}
+
+const NotFoundRouteRouteWithChildren = NotFoundRouteRoute._addFileChildren(
+ NotFoundRouteRouteChildren,
+)
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface UsersRouteChildren {
+ UsersUserIdRoute: typeof UsersUserIdRoute
+ UsersIndexRoute: typeof UsersIndexRoute
+}
+
+const UsersRouteChildren: UsersRouteChildren = {
+ UsersUserIdRoute: UsersUserIdRoute,
+ UsersIndexRoute: UsersIndexRoute,
+}
+
+const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren)
+
+interface RedirectTargetRouteChildren {
+ RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute
+ RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute
+ RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute
+ RedirectTargetServerFnViaBeforeLoadRoute: typeof RedirectTargetServerFnViaBeforeLoadRoute
+ RedirectTargetServerFnViaLoaderRoute: typeof RedirectTargetServerFnViaLoaderRoute
+ RedirectTargetServerFnViaUseServerFnRoute: typeof RedirectTargetServerFnViaUseServerFnRoute
+ RedirectTargetServerFnIndexRoute: typeof RedirectTargetServerFnIndexRoute
+}
+
+const RedirectTargetRouteChildren: RedirectTargetRouteChildren = {
+ RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute,
+ RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute,
+ RedirectTargetIndexRoute: RedirectTargetIndexRoute,
+ RedirectTargetServerFnViaBeforeLoadRoute:
+ RedirectTargetServerFnViaBeforeLoadRoute,
+ RedirectTargetServerFnViaLoaderRoute: RedirectTargetServerFnViaLoaderRoute,
+ RedirectTargetServerFnViaUseServerFnRoute:
+ RedirectTargetServerFnViaUseServerFnRoute,
+ RedirectTargetServerFnIndexRoute: RedirectTargetServerFnIndexRoute,
+}
+
+const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren(
+ RedirectTargetRouteChildren,
+)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/not-found': typeof NotFoundRouteRouteWithChildren
+ '': typeof LayoutLayout2RouteWithChildren
+ '/deferred': typeof DeferredRoute
+ '/links': typeof LinksRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/scripts': typeof ScriptsRoute
+ '/search-params': typeof SearchParamsRoute
+ '/stream': typeof StreamRoute
+ '/users': typeof UsersRouteWithChildren
+ '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/$target': typeof RedirectTargetRouteWithChildren
+ '/users/$userId': typeof UsersUserIdRoute
+ '/not-found/': typeof NotFoundIndexRoute
+ '/posts/': typeof PostsIndexRoute
+ '/redirect': typeof RedirectIndexRoute
+ '/users/': typeof UsersIndexRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+ '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+ '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/redirect/$target/': typeof RedirectTargetIndexRoute
+ '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+ '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+ '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+ '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '': typeof LayoutLayout2RouteWithChildren
+ '/deferred': typeof DeferredRoute
+ '/links': typeof LinksRoute
+ '/scripts': typeof ScriptsRoute
+ '/search-params': typeof SearchParamsRoute
+ '/stream': typeof StreamRoute
+ '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/not-found': typeof NotFoundIndexRoute
+ '/posts': typeof PostsIndexRoute
+ '/redirect': typeof RedirectIndexRoute
+ '/users': typeof UsersIndexRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+ '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+ '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/redirect/$target': typeof RedirectTargetIndexRoute
+ '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+ '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+ '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+ '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/not-found': typeof NotFoundRouteRouteWithChildren
+ '/_layout': typeof LayoutRouteWithChildren
+ '/deferred': typeof DeferredRoute
+ '/links': typeof LinksRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/scripts': typeof ScriptsRoute
+ '/search-params': typeof SearchParamsRoute
+ '/stream': typeof StreamRoute
+ '/users': typeof UsersRouteWithChildren
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/$target': typeof RedirectTargetRouteWithChildren
+ '/users/$userId': typeof UsersUserIdRoute
+ '/not-found/': typeof NotFoundIndexRoute
+ '/posts/': typeof PostsIndexRoute
+ '/redirect/': typeof RedirectIndexRoute
+ '/users/': typeof UsersIndexRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts_/$postId/deep': typeof PostsPostIdDeepRoute
+ '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
+ '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/redirect/$target/': typeof RedirectTargetIndexRoute
+ '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
+ '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
+ '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
+ '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/not-found'
+ | ''
+ | '/deferred'
+ | '/links'
+ | '/posts'
+ | '/scripts'
+ | '/search-params'
+ | '/stream'
+ | '/users'
+ | '/not-found/via-beforeLoad'
+ | '/not-found/via-loader'
+ | '/posts/$postId'
+ | '/redirect/$target'
+ | '/users/$userId'
+ | '/not-found/'
+ | '/posts/'
+ | '/redirect'
+ | '/users/'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/deep'
+ | '/redirect/$target/via-beforeLoad'
+ | '/redirect/$target/via-loader'
+ | '/redirect/$target/'
+ | '/redirect/$target/serverFn/via-beforeLoad'
+ | '/redirect/$target/serverFn/via-loader'
+ | '/redirect/$target/serverFn/via-useServerFn'
+ | '/redirect/$target/serverFn'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | ''
+ | '/deferred'
+ | '/links'
+ | '/scripts'
+ | '/search-params'
+ | '/stream'
+ | '/not-found/via-beforeLoad'
+ | '/not-found/via-loader'
+ | '/posts/$postId'
+ | '/users/$userId'
+ | '/not-found'
+ | '/posts'
+ | '/redirect'
+ | '/users'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/deep'
+ | '/redirect/$target/via-beforeLoad'
+ | '/redirect/$target/via-loader'
+ | '/redirect/$target'
+ | '/redirect/$target/serverFn/via-beforeLoad'
+ | '/redirect/$target/serverFn/via-loader'
+ | '/redirect/$target/serverFn/via-useServerFn'
+ | '/redirect/$target/serverFn'
+ id:
+ | '__root__'
+ | '/'
+ | '/not-found'
+ | '/_layout'
+ | '/deferred'
+ | '/links'
+ | '/posts'
+ | '/scripts'
+ | '/search-params'
+ | '/stream'
+ | '/users'
+ | '/_layout/_layout-2'
+ | '/not-found/via-beforeLoad'
+ | '/not-found/via-loader'
+ | '/posts/$postId'
+ | '/redirect/$target'
+ | '/users/$userId'
+ | '/not-found/'
+ | '/posts/'
+ | '/redirect/'
+ | '/users/'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ | '/posts_/$postId/deep'
+ | '/redirect/$target/via-beforeLoad'
+ | '/redirect/$target/via-loader'
+ | '/redirect/$target/'
+ | '/redirect/$target/serverFn/via-beforeLoad'
+ | '/redirect/$target/serverFn/via-loader'
+ | '/redirect/$target/serverFn/via-useServerFn'
+ | '/redirect/$target/serverFn/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren
+ LayoutRoute: typeof LayoutRouteWithChildren
+ DeferredRoute: typeof DeferredRoute
+ LinksRoute: typeof LinksRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ ScriptsRoute: typeof ScriptsRoute
+ SearchParamsRoute: typeof SearchParamsRoute
+ StreamRoute: typeof StreamRoute
+ UsersRoute: typeof UsersRouteWithChildren
+ RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
+ RedirectIndexRoute: typeof RedirectIndexRoute
+ PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
+ LayoutRoute: LayoutRouteWithChildren,
+ DeferredRoute: DeferredRoute,
+ LinksRoute: LinksRoute,
+ PostsRoute: PostsRouteWithChildren,
+ ScriptsRoute: ScriptsRoute,
+ SearchParamsRoute: SearchParamsRoute,
+ StreamRoute: StreamRoute,
+ UsersRoute: UsersRouteWithChildren,
+ RedirectTargetRoute: RedirectTargetRouteWithChildren,
+ RedirectIndexRoute: RedirectIndexRoute,
+ PostsPostIdDeepRoute: PostsPostIdDeepRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/",
+ "/not-found",
+ "/_layout",
+ "/deferred",
+ "/links",
+ "/posts",
+ "/scripts",
+ "/search-params",
+ "/stream",
+ "/users",
+ "/redirect/$target",
+ "/redirect/",
+ "/posts_/$postId/deep"
+ ]
+ },
+ "/": {
+ "filePath": "index.tsx"
+ },
+ "/not-found": {
+ "filePath": "not-found/route.tsx",
+ "children": [
+ "/not-found/via-beforeLoad",
+ "/not-found/via-loader",
+ "/not-found/"
+ ]
+ },
+ "/_layout": {
+ "filePath": "_layout.tsx",
+ "children": [
+ "/_layout/_layout-2"
+ ]
+ },
+ "/deferred": {
+ "filePath": "deferred.tsx"
+ },
+ "/links": {
+ "filePath": "links.tsx"
+ },
+ "/posts": {
+ "filePath": "posts.tsx",
+ "children": [
+ "/posts/$postId",
+ "/posts/"
+ ]
+ },
+ "/scripts": {
+ "filePath": "scripts.tsx"
+ },
+ "/search-params": {
+ "filePath": "search-params.tsx"
+ },
+ "/stream": {
+ "filePath": "stream.tsx"
+ },
+ "/users": {
+ "filePath": "users.tsx",
+ "children": [
+ "/users/$userId",
+ "/users/"
+ ]
+ },
+ "/_layout/_layout-2": {
+ "filePath": "_layout/_layout-2.tsx",
+ "parent": "/_layout",
+ "children": [
+ "/_layout/_layout-2/layout-a",
+ "/_layout/_layout-2/layout-b"
+ ]
+ },
+ "/not-found/via-beforeLoad": {
+ "filePath": "not-found/via-beforeLoad.tsx",
+ "parent": "/not-found"
+ },
+ "/not-found/via-loader": {
+ "filePath": "not-found/via-loader.tsx",
+ "parent": "/not-found"
+ },
+ "/posts/$postId": {
+ "filePath": "posts.$postId.tsx",
+ "parent": "/posts"
+ },
+ "/redirect/$target": {
+ "filePath": "redirect/$target.tsx",
+ "children": [
+ "/redirect/$target/via-beforeLoad",
+ "/redirect/$target/via-loader",
+ "/redirect/$target/",
+ "/redirect/$target/serverFn/via-beforeLoad",
+ "/redirect/$target/serverFn/via-loader",
+ "/redirect/$target/serverFn/via-useServerFn",
+ "/redirect/$target/serverFn/"
+ ]
+ },
+ "/users/$userId": {
+ "filePath": "users.$userId.tsx",
+ "parent": "/users"
+ },
+ "/not-found/": {
+ "filePath": "not-found/index.tsx",
+ "parent": "/not-found"
+ },
+ "/posts/": {
+ "filePath": "posts.index.tsx",
+ "parent": "/posts"
+ },
+ "/redirect/": {
+ "filePath": "redirect/index.tsx"
+ },
+ "/users/": {
+ "filePath": "users.index.tsx",
+ "parent": "/users"
+ },
+ "/_layout/_layout-2/layout-a": {
+ "filePath": "_layout/_layout-2/layout-a.tsx",
+ "parent": "/_layout/_layout-2"
+ },
+ "/_layout/_layout-2/layout-b": {
+ "filePath": "_layout/_layout-2/layout-b.tsx",
+ "parent": "/_layout/_layout-2"
+ },
+ "/posts_/$postId/deep": {
+ "filePath": "posts_.$postId.deep.tsx"
+ },
+ "/redirect/$target/via-beforeLoad": {
+ "filePath": "redirect/$target/via-beforeLoad.tsx",
+ "parent": "/redirect/$target"
+ },
+ "/redirect/$target/via-loader": {
+ "filePath": "redirect/$target/via-loader.tsx",
+ "parent": "/redirect/$target"
+ },
+ "/redirect/$target/": {
+ "filePath": "redirect/$target/index.tsx",
+ "parent": "/redirect/$target"
+ },
+ "/redirect/$target/serverFn/via-beforeLoad": {
+ "filePath": "redirect/$target/serverFn/via-beforeLoad.tsx",
+ "parent": "/redirect/$target"
+ },
+ "/redirect/$target/serverFn/via-loader": {
+ "filePath": "redirect/$target/serverFn/via-loader.tsx",
+ "parent": "/redirect/$target"
+ },
+ "/redirect/$target/serverFn/via-useServerFn": {
+ "filePath": "redirect/$target/serverFn/via-useServerFn.tsx",
+ "parent": "/redirect/$target"
+ },
+ "/redirect/$target/serverFn/": {
+ "filePath": "redirect/$target/serverFn/index.tsx",
+ "parent": "/redirect/$target"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/solid-start/basic/app/router.tsx b/e2e/solid-start/basic/app/router.tsx
new file mode 100644
index 0000000000..c45bed4758
--- /dev/null
+++ b/e2e/solid-start/basic/app/router.tsx
@@ -0,0 +1,22 @@
+import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function createRouter() {
+ const router = createTanStackRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ scrollRestoration: true,
+ })
+
+ return router
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/e2e/solid-start/basic/app/routes/__root.tsx b/e2e/solid-start/basic/app/routes/__root.tsx
new file mode 100644
index 0000000000..5f92885c6b
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/__root.tsx
@@ -0,0 +1,122 @@
+import { Link, Outlet, createRootRoute } from '@tanstack/solid-router'
+
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+import { seo } from '~/utils/seo'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ...seo({
+ title:
+ 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
+ description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ {
+ rel: 'apple-touch-icon',
+ sizes: '180x180',
+ href: '/apple-touch-icon.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '32x32',
+ href: '/favicon-32x32.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '16x16',
+ href: '/favicon-16x16.png',
+ },
+ { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ }),
+ errorComponent: (props) => {props.error.stack}
,
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Users
+ {' '}
+
+ Layout
+ {' '}
+
+ Scripts
+ {' '}
+
+ Deferred
+ {' '}
+
+ redirect
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+ >
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/_layout.tsx b/e2e/solid-start/basic/app/routes/_layout.tsx
new file mode 100644
index 0000000000..d43b4ef5f5
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx
new file mode 100644
index 0000000000..7a5a3623a0
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 0000000000..b69951b246
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 0000000000..30dbcce90f
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/solid-start/basic/app/routes/api.users.ts b/e2e/solid-start/basic/app/routes/api.users.ts
new file mode 100644
index 0000000000..45ac83b2f0
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/api.users.ts
@@ -0,0 +1,18 @@
+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/app/routes/api/users.$id.ts b/e2e/solid-start/basic/app/routes/api/users.$id.ts
new file mode 100644
index 0000000000..7ee1fccbb4
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/api/users.$id.ts
@@ -0,0 +1,25 @@
+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/app/routes/deferred.tsx b/e2e/solid-start/basic/app/routes/deferred.tsx
new file mode 100644
index 0000000000..2a53643453
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/deferred.tsx
@@ -0,0 +1,65 @@
+import { Await, createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+import { Suspense, createSignal } from 'solid-js'
+
+const personServerFn = createServerFn({ method: 'GET' })
+ .validator((data: { name: string }) => data)
+ .handler(({ data }) => {
+ return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
+ })
+
+const slowServerFn = createServerFn({ method: 'GET' })
+ .validator((data: { name: string }) => data)
+ .handler(async ({ data }) => {
+ await new Promise((r) => setTimeout(r, 1000))
+ return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
+ })
+
+export const Route = createFileRoute('/deferred')({
+ loader: async () => {
+ return {
+ deferredStuff: new Promise((r) =>
+ setTimeout(() => r('Hello deferred!'), 2000),
+ ),
+ deferredPerson: slowServerFn({ data: { name: 'Tanner Linsley' } }),
+ person: await personServerFn({ data: { name: 'John Doe' } }),
+ }
+ },
+ component: Deferred,
+})
+
+function Deferred() {
+ const [count, setCount] = createSignal(0)
+ // const { deferredStuff, deferredPerson, person } = Route.useLoaderData()
+ const loaderData = Route.useLoaderData()
+
+ return (
+
+
+ {loaderData().person.name} - {loaderData().person.randomNumber}
+
+
Loading person... }>
+ (
+
+ {data.name} - {data.randomNumber}
+
+ )}
+ />
+
+ Loading stuff...}>
+ {data} }
+ />
+
+ Count: {count()}
+
+ setCount((count) => count + 1)}>
+ Increment
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/index.tsx b/e2e/solid-start/basic/app/routes/index.tsx
new file mode 100644
index 0000000000..6b23caf87b
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/index.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { CustomMessage } from '~/components/CustomMessage'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!!!
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/links.tsx b/e2e/solid-start/basic/app/routes/links.tsx
new file mode 100644
index 0000000000..d6ce6a449b
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/links.tsx
@@ -0,0 +1,47 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/links')({
+ component: () => {
+ const navigate = Route.useNavigate()
+ return (
+
+
+
+
+ Link to /posts
+
+
+
+
+ Link to /posts (reloadDocument=true)
+
+
+
+ navigate({ to: '/posts' })}>
+ navigate to /posts
+
+
+
+ navigate({ to: '/posts', reloadDocument: true })}
+ >
+ navigate to /posts (reloadDocument=true)
+
+
+
+ )
+ },
+})
diff --git a/e2e/solid-start/basic/app/routes/not-found/index.tsx b/e2e/solid-start/basic/app/routes/not-found/index.tsx
new file mode 100644
index 0000000000..34c8ef6146
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/not-found/index.tsx
@@ -0,0 +1,31 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/not-found/')({
+ component: () => {
+ const preload = Route.useSearch({ select: (s) => s.preload })
+ return (
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-loader
+
+
+
+ )
+ },
+})
diff --git a/e2e/solid-start/basic/app/routes/not-found/route.tsx b/e2e/solid-start/basic/app/routes/not-found/route.tsx
new file mode 100644
index 0000000000..84f5ef81a5
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/not-found/route.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/not-found')({
+ validateSearch: z.object({
+ preload: z.literal(false).optional(),
+ }),
+})
diff --git a/e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx b/e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx
new file mode 100644
index 0000000000..5badde63bd
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/not-found/via-beforeLoad.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/not-found/via-beforeLoad')({
+ beforeLoad: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-beforeLoad"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-beforeLoad"!
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/not-found/via-loader.tsx b/e2e/solid-start/basic/app/routes/not-found/via-loader.tsx
new file mode 100644
index 0000000000..20956cc43d
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/not-found/via-loader.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/not-found/via-loader')({
+ loader: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-loader"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-loader"!
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/posts.$postId.tsx b/e2e/solid-start/basic/app/routes/posts.$postId.tsx
new file mode 100644
index 0000000000..c6b2fcf5f9
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/posts.$postId.tsx
@@ -0,0 +1,36 @@
+import { ErrorComponent, Link, createFileRoute } 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')({
+ loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
+ errorComponent: PostErrorComponent,
+ component: PostComponent,
+ notFoundComponent: () => {
+ return Post not found
+ },
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post().title}
+
{post().body}
+
+ Deep View
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/posts.index.tsx b/e2e/solid-start/basic/app/routes/posts.index.tsx
new file mode 100644
index 0000000000..c7d8cfe19c
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/solid-start/basic/app/routes/posts.tsx b/e2e/solid-start/basic/app/routes/posts.tsx
new file mode 100644
index 0000000000..0e94cd4d2c
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/posts.tsx
@@ -0,0 +1,47 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import { For } from 'solid-js'
+
+import { fetchPosts } from '~/utils/posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: async () => fetchPosts(),
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+
+
+ {(post) => {
+ return (
+
+
+ {post.title.substring(0, 20)}
+
+
+ )
+ }}
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx b/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx
new file mode 100644
index 0000000000..f8d4627914
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/posts_.$postId.deep.tsx
@@ -0,0 +1,24 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+import { PostErrorComponent } from '~/components/PostErrorComponent'
+
+import { fetchPost } from '~/utils/posts'
+
+export const Route = createFileRoute('/posts_/$postId/deep')({
+ loader: async ({ 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/e2e/solid-start/basic/app/routes/redirect/$target.tsx b/e2e/solid-start/basic/app/routes/redirect/$target.tsx
new file mode 100644
index 0000000000..525dd9da25
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute, retainSearchParams } from '@tanstack/solid-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/redirect/$target')({
+ params: {
+ parse: (p) =>
+ z
+ .object({
+ target: z.union([z.literal('internal'), z.literal('external')]),
+ })
+ .parse(p),
+ },
+ validateSearch: z.object({
+ reloadDocument: z.boolean().optional(),
+ preload: z.literal(false).optional(),
+ externalHost: z.string().optional(),
+ }),
+ search: {
+ middlewares: [retainSearchParams(['externalHost'])],
+ },
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx
new file mode 100644
index 0000000000..916afd450b
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/index.tsx
@@ -0,0 +1,76 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/')({
+ component: () => {
+ const preload = Route.useSearch({ select: (s) => s.preload })
+ return (
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-beforeLoad (reloadDocument=true)
+
+
+
+
+ via-loader
+
+
+
+
+ via-loader (reloadDocument=true)
+
+
+
+
+ serverFn
+
+
+
+ )
+ },
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx
new file mode 100644
index 0000000000..d404672372
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/index.tsx
@@ -0,0 +1,87 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/serverFn/')({
+ component: () => (
+
+
+
+
+ via-beforeLoad
+
+
+
+
+ via-beforeLoad (reloadDocument=true)
+
+
+
+
+ via-loader
+
+
+
+
+ via-loader (reloadDocument=true)
+
+
+
+
+ via-useServerFn
+
+
+
+
+ via-useServerFn (reloadDocument=true)
+
+
+
+ ),
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx
new file mode 100644
index 0000000000..81f9dd2d36
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { throwRedirect } from '~/components/throwRedirect'
+
+export const Route = createFileRoute(
+ '/redirect/$target/serverFn/via-beforeLoad',
+)({
+ beforeLoad: ({
+ params: { target },
+ search: { reloadDocument, externalHost },
+ }) => throwRedirect({ data: { target, reloadDocument, externalHost } }),
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx
new file mode 100644
index 0000000000..0b6ac91c15
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx
@@ -0,0 +1,12 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { throwRedirect } from '~/components/throwRedirect'
+
+export const Route = createFileRoute('/redirect/$target/serverFn/via-loader')({
+ loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({
+ reloadDocument,
+ externalHost,
+ }),
+ loader: ({ params: { target }, deps: { reloadDocument, externalHost } }) =>
+ throwRedirect({ data: { target, reloadDocument, externalHost } }),
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx
new file mode 100644
index 0000000000..5b0c246474
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { RedirectOnClick } from '~/components/RedirectOnClick'
+
+export const Route = createFileRoute(
+ '/redirect/$target/serverFn/via-useServerFn',
+)({
+ component: () => {
+ const params = Route.useParams()
+ const search = Route.useSearch()
+ return (
+
+ )
+ },
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx
new file mode 100644
index 0000000000..c88cc07986
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/via-beforeLoad.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({
+ beforeLoad: ({
+ params: { target },
+ search: { reloadDocument, externalHost },
+ }) => {
+ switch (target) {
+ case 'internal':
+ throw redirect({ to: '/posts', reloadDocument })
+ case 'external':
+ const href = externalHost ?? 'http://example.com'
+ throw redirect({ href })
+ }
+ },
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx b/e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx
new file mode 100644
index 0000000000..5c059717c5
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/$target/via-loader.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/$target/via-loader')({
+ loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({
+ reloadDocument,
+ externalHost,
+ }),
+ loader: ({ params: { target }, deps: { externalHost, reloadDocument } }) => {
+ switch (target) {
+ case 'internal':
+ throw redirect({ to: '/posts', reloadDocument })
+ case 'external':
+ const href = externalHost ?? 'http://example.com'
+ throw redirect({ href })
+ }
+ },
+ component: () => {Route.fullPath}
,
+})
diff --git a/e2e/solid-start/basic/app/routes/redirect/index.tsx b/e2e/solid-start/basic/app/routes/redirect/index.tsx
new file mode 100644
index 0000000000..043f305e57
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/redirect/index.tsx
@@ -0,0 +1,28 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/redirect/')({
+ component: () => (
+
+
+ internal
+ {' '}
+
+ external
+
+
+ ),
+})
diff --git a/e2e/solid-start/basic/app/routes/scripts.tsx b/e2e/solid-start/basic/app/routes/scripts.tsx
new file mode 100644
index 0000000000..530d346762
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/scripts.tsx
@@ -0,0 +1,31 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+const isProd = import.meta.env.PROD
+
+export const Route = createFileRoute('/scripts')({
+ head: () => ({
+ scripts: [
+ {
+ src: 'script.js',
+ },
+ isProd
+ ? undefined
+ : {
+ src: 'script2.js',
+ },
+ ],
+ }),
+ component: ScriptsComponent,
+})
+
+function ScriptsComponent() {
+ return (
+
+
Scripts Test
+
+ Both `script.js` and `script2.js` are included in development, but only
+ `script.js` is included in production.
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/search-params.tsx b/e2e/solid-start/basic/app/routes/search-params.tsx
new file mode 100644
index 0000000000..ec941d9a4c
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/search-params.tsx
@@ -0,0 +1,27 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/search-params')({
+ component: () => {
+ const search = Route.useSearch()
+ return (
+
+
SearchParams
+
{search().step}
+
+ )
+ },
+ validateSearch: z.object({
+ step: z.enum(['a', 'b', 'c']).optional(),
+ }),
+ loaderDeps: ({ search: { step } }) => ({ step }),
+ loader: ({ deps: { step } }) => {
+ if (step === undefined) {
+ throw redirect({
+ to: '/search-params',
+ from: '/search-params',
+ search: { step: 'a' },
+ })
+ }
+ },
+})
diff --git a/e2e/solid-start/basic/app/routes/stream.tsx b/e2e/solid-start/basic/app/routes/stream.tsx
new file mode 100644
index 0000000000..d1d21e344a
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/stream.tsx
@@ -0,0 +1,64 @@
+import { Await, createFileRoute } from '@tanstack/solid-router'
+import { createEffect, createSignal } from 'solid-js'
+
+export const Route = createFileRoute('/stream')({
+ component: Home,
+ loader() {
+ return {
+ promise: new Promise((resolve) =>
+ setTimeout(() => resolve('promise-data'), 150),
+ ),
+ stream: new ReadableStream({
+ async start(controller) {
+ for (let i = 0; i < 5; i++) {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ controller.enqueue(`stream-data-${i} `)
+ }
+ controller.close()
+ },
+ }),
+ }
+ },
+})
+
+const decoder = new TextDecoder('utf-8')
+
+function Home() {
+ const loaderData = Route.useLoaderData()
+ const [streamData, setStreamData] = createSignal>([])
+
+ createEffect(() => {
+ async function fetchStream() {
+ const reader = loaderData().stream.getReader()
+ let chunk
+
+ while (!(chunk = await reader.read()).done) {
+ let value = chunk.value
+ if (typeof value !== 'string') {
+ value = decoder.decode(value, { stream: !chunk.done })
+ }
+ setStreamData((prev) => [...prev, value])
+ }
+ }
+
+ fetchStream()
+ })
+
+ return (
+ <>
+ (
+
+ {promiseData}
+
+ {streamData().map((d) => (
+
{d}
+ ))}
+
+
+ )}
+ />
+ >
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/users.$userId.tsx b/e2e/solid-start/basic/app/routes/users.$userId.tsx
new file mode 100644
index 0000000000..6f95652e53
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/users.$userId.tsx
@@ -0,0 +1,35 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import axios from 'redaxios'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+import type { User } from '~/utils/users'
+import { DEPLOY_URL } from '~/utils/users'
+import { NotFound } from '~/components/NotFound'
+import { UserErrorComponent } from '~/components/UserErrorComponent'
+
+export const Route = createFileRoute('/users/$userId')({
+ 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')
+ })
+ },
+ errorComponent: UserErrorComponent,
+ component: UserComponent,
+ notFoundComponent: () => {
+ return User not found
+ },
+})
+
+function UserComponent() {
+ const user = Route.useLoaderData()
+
+ return (
+
+
{user().name}
+
{user().email}
+
+ )
+}
diff --git a/e2e/solid-start/basic/app/routes/users.index.tsx b/e2e/solid-start/basic/app/routes/users.index.tsx
new file mode 100644
index 0000000000..bbc96801a9
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/users.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/users/')({
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return Select a user.
+}
diff --git a/e2e/solid-start/basic/app/routes/users.tsx b/e2e/solid-start/basic/app/routes/users.tsx
new file mode 100644
index 0000000000..3c26badbb4
--- /dev/null
+++ b/e2e/solid-start/basic/app/routes/users.tsx
@@ -0,0 +1,49 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+import axios from 'redaxios'
+
+import type { User } from '~/utils/users'
+import { DEPLOY_URL } from '~/utils/users'
+
+export const Route = createFileRoute('/users')({
+ loader: async () => {
+ return await axios
+ .get>(DEPLOY_URL + '/api/users')
+ .then((r) => r.data)
+ .catch(() => {
+ throw new Error('Failed to fetch users')
+ })
+ },
+ 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/e2e/solid-start/basic/app/ssr.tsx b/e2e/solid-start/basic/app/ssr.tsx
new file mode 100644
index 0000000000..6d10bea05f
--- /dev/null
+++ b/e2e/solid-start/basic/app/ssr.tsx
@@ -0,0 +1,12 @@
+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/e2e/solid-start/basic/app/styles/app.css b/e2e/solid-start/basic/app/styles/app.css
new file mode 100644
index 0000000000..c53c870665
--- /dev/null
+++ b/e2e/solid-start/basic/app/styles/app.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ color-scheme: light dark;
+ }
+
+ * {
+ @apply border-gray-200 dark:border-gray-800;
+ }
+
+ 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/e2e/solid-start/basic/app/utils/posts.tsx b/e2e/solid-start/basic/app/utils/posts.tsx
new file mode 100644
index 0000000000..6c12105ab8
--- /dev/null
+++ b/e2e/solid-start/basic/app/utils/posts.tsx
@@ -0,0 +1,36 @@
+import { notFound } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPost = createServerFn({ method: 'GET' })
+ .validator((postId: string) => postId)
+ .handler(async ({ data: postId }) => {
+ console.info(`Fetching post with id ${postId}...`)
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ console.error(err)
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ 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))
+ },
+)
diff --git a/e2e/solid-start/basic/app/utils/seo.ts b/e2e/solid-start/basic/app/utils/seo.ts
new file mode 100644
index 0000000000..d18ad84b74
--- /dev/null
+++ b/e2e/solid-start/basic/app/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/e2e/solid-start/basic/app/utils/users.tsx b/e2e/solid-start/basic/app/utils/users.tsx
new file mode 100644
index 0000000000..79b45867d5
--- /dev/null
+++ b/e2e/solid-start/basic/app/utils/users.tsx
@@ -0,0 +1,10 @@
+export type User = {
+ id: number
+ name: string
+ email: string
+}
+
+const PORT =
+ import.meta.env.VITE_SERVER_PORT || process.env.VITE_SERVER_PORT || 3000
+
+export const DEPLOY_URL = `http://localhost:${PORT}`
diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json
new file mode 100644
index 0000000000..b8321983bc
--- /dev/null
+++ b/e2e/solid-start/basic/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "tanstack-solid-start-e2e-basic",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vinxi dev --port 3000",
+ "dev:e2e": "vinxi dev",
+ "build": "vinxi build && tsc --noEmit",
+ "start": "vinxi start",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "solid-js": "^1.9.5",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0",
+ "vinxi": "0.5.3",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@types/node": "^22.10.2",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "vite-plugin-solid": "^2.11.2",
+ "combinate": "^1.1.11",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.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
new file mode 100644
index 0000000000..95d043d48b
--- /dev/null
+++ b/e2e/solid-start/basic/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-start/basic/postcss.config.mjs b/e2e/solid-start/basic/postcss.config.mjs
new file mode 100644
index 0000000000..2e7af2b7f1
--- /dev/null
+++ b/e2e/solid-start/basic/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-start/basic/public/android-chrome-192x192.png b/e2e/solid-start/basic/public/android-chrome-192x192.png
new file mode 100644
index 0000000000..09c8324f8c
Binary files /dev/null and b/e2e/solid-start/basic/public/android-chrome-192x192.png differ
diff --git a/e2e/solid-start/basic/public/android-chrome-512x512.png b/e2e/solid-start/basic/public/android-chrome-512x512.png
new file mode 100644
index 0000000000..11d626ea3d
Binary files /dev/null and b/e2e/solid-start/basic/public/android-chrome-512x512.png differ
diff --git a/e2e/solid-start/basic/public/apple-touch-icon.png b/e2e/solid-start/basic/public/apple-touch-icon.png
new file mode 100644
index 0000000000..5a9423cc02
Binary files /dev/null and b/e2e/solid-start/basic/public/apple-touch-icon.png differ
diff --git a/e2e/solid-start/basic/public/favicon-16x16.png b/e2e/solid-start/basic/public/favicon-16x16.png
new file mode 100644
index 0000000000..e3389b0044
Binary files /dev/null and b/e2e/solid-start/basic/public/favicon-16x16.png differ
diff --git a/e2e/solid-start/basic/public/favicon-32x32.png b/e2e/solid-start/basic/public/favicon-32x32.png
new file mode 100644
index 0000000000..900c77d444
Binary files /dev/null and b/e2e/solid-start/basic/public/favicon-32x32.png differ
diff --git a/e2e/solid-start/basic/public/favicon.ico b/e2e/solid-start/basic/public/favicon.ico
new file mode 100644
index 0000000000..1a1751676f
Binary files /dev/null and b/e2e/solid-start/basic/public/favicon.ico differ
diff --git a/e2e/solid-start/basic/public/favicon.png b/e2e/solid-start/basic/public/favicon.png
new file mode 100644
index 0000000000..1e77bc0609
Binary files /dev/null and b/e2e/solid-start/basic/public/favicon.png differ
diff --git a/e2e/solid-start/basic/public/script.js b/e2e/solid-start/basic/public/script.js
new file mode 100644
index 0000000000..897477e7d0
--- /dev/null
+++ b/e2e/solid-start/basic/public/script.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_1 loaded')
+window.SCRIPT_1 = true
diff --git a/e2e/solid-start/basic/public/script2.js b/e2e/solid-start/basic/public/script2.js
new file mode 100644
index 0000000000..819af30daf
--- /dev/null
+++ b/e2e/solid-start/basic/public/script2.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_2 loaded')
+window.SCRIPT_2 = true
diff --git a/e2e/solid-start/basic/public/site.webmanifest b/e2e/solid-start/basic/public/site.webmanifest
new file mode 100644
index 0000000000..fa99de77db
--- /dev/null
+++ b/e2e/solid-start/basic/public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/e2e/solid-start/basic/tailwind.config.mjs b/e2e/solid-start/basic/tailwind.config.mjs
new file mode 100644
index 0000000000..07c3598bac
--- /dev/null
+++ b/e2e/solid-start/basic/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./app/**/*.{js,jsx,ts,tsx}'],
+}
diff --git a/e2e/solid-start/basic/tests/fixture.ts b/e2e/solid-start/basic/tests/fixture.ts
new file mode 100644
index 0000000000..abb7b1d564
--- /dev/null
+++ b/e2e/solid-start/basic/tests/fixture.ts
@@ -0,0 +1,28 @@
+import { test as base, expect } from '@playwright/test'
+
+export interface TestFixtureOptions {
+ whitelistErrors: Array
+}
+export const test = base.extend({
+ whitelistErrors: [[], { option: true }],
+ page: async ({ page, whitelistErrors }, use) => {
+ const errorMessages: Array = []
+ page.on('console', (m) => {
+ if (m.type() === 'error') {
+ const text = m.text()
+ for (const whitelistError of whitelistErrors) {
+ if (
+ (typeof whitelistError === 'string' &&
+ text.includes(whitelistError)) ||
+ (whitelistError instanceof RegExp && whitelistError.test(text))
+ ) {
+ return
+ }
+ }
+ errorMessages.push(text)
+ }
+ })
+ await use(page)
+ expect(errorMessages).toEqual([])
+ },
+})
diff --git a/e2e/solid-start/basic/tests/navigation.spec.ts b/e2e/solid-start/basic/tests/navigation.spec.ts
new file mode 100644
index 0000000000..2c61b6fc01
--- /dev/null
+++ b/e2e/solid-start/basic/tests/navigation.spec.ts
@@ -0,0 +1,55 @@
+import { expect, test } from '@playwright/test'
+
+test('Navigating to post', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await page.getByRole('link', { name: 'Deep View' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating to user', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Users' }).click()
+ await page.getByRole('link', { name: 'Leanne Graham' }).click()
+ await expect(page.getByRole('heading')).toContainText('Leanne Graham')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('body')).toContainText("I'm a layout")
+ await expect(page.locator('body')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('body')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('body')).toContainText("I'm layout B!")
+})
+
+test('directly going to a route with scripts', async ({ page }) => {
+ await page.goto('/scripts')
+ expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
+ expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined)
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test('Should change title on client side navigation', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+
+ await expect(page).toHaveTitle('Posts page')
+})
diff --git a/e2e/solid-start/basic/tests/not-found.spec.ts b/e2e/solid-start/basic/tests/not-found.spec.ts
new file mode 100644
index 0000000000..0d83ab5280
--- /dev/null
+++ b/e2e/solid-start/basic/tests/not-found.spec.ts
@@ -0,0 +1,74 @@
+import { expect } from '@playwright/test'
+import combinateImport from 'combinate'
+import { test } from './fixture'
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ ],
+})
+test.describe('not-found', () => {
+ test(`global not found`, async ({ page }) => {
+ const response = await page.goto(`/this-page-does-not-exist/foo/bar`)
+
+ expect(response?.status()).toBe(404)
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ })
+
+ test.describe('throw notFound()', () => {
+ const navigationTestMatrix = combinate({
+ // TODO beforeLoad!
+ thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+ preload: [false, true] as const,
+ })
+
+ navigationTestMatrix.forEach(({ thrower, preload }) => {
+ test(`navigation: thrower: ${thrower}, preload: ${preload}`, async ({
+ page,
+ }) => {
+ await page.goto(
+ `/not-found/${preload === false ? '?preload=false' : ''}`,
+ )
+ const link = page.getByTestId(`via-${thrower}`)
+
+ if (preload) {
+ await link.focus()
+ await new Promise((r) => setTimeout(r, 250))
+ }
+
+ await link.click()
+
+ await expect(
+ page.getByTestId(`via-${thrower}-notFound-component`),
+ ).toBeInViewport()
+ await expect(
+ page.getByTestId(`via-${thrower}-route-component`),
+ ).not.toBeInViewport()
+ })
+ })
+ const directVisitTestMatrix = combinate({
+ // TODO beforeLoad!
+
+ thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+ })
+
+ directVisitTestMatrix.forEach(({ thrower }) => {
+ test(`direct visit: thrower: ${thrower}`, async ({ page }) => {
+ await page.goto(`/not-found/via-${thrower}`)
+ await page.waitForLoadState('networkidle')
+ await expect(
+ page.getByTestId(`via-${thrower}-notFound-component`),
+ ).toBeInViewport()
+ await expect(
+ page.getByTestId(`via-${thrower}-route-component`),
+ ).not.toBeInViewport()
+ })
+ })
+ })
+})
diff --git a/e2e/solid-start/basic/tests/redirect.spec.ts b/e2e/solid-start/basic/tests/redirect.spec.ts
new file mode 100644
index 0000000000..4f4cb73b1c
--- /dev/null
+++ b/e2e/solid-start/basic/tests/redirect.spec.ts
@@ -0,0 +1,209 @@
+import { expect } from '@playwright/test'
+import combinateImport from 'combinate'
+import { derivePort, localDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../package.json' with { type: 'json' }
+import { test } from './fixture'
+import { Server } from 'node:http'
+import queryString from 'node:querystring'
+
+// somehow playwright does not correctly import default exports
+const combinate = (combinateImport as any).default as typeof combinateImport
+
+const PORT = derivePort(packageJson.name)
+const EXTERNAL_HOST_PORT = derivePort(`${packageJson.name}-external`)
+
+test.describe('redirects', () => {
+ let server: Server
+ test.beforeAll(async () => {
+ server = await localDummyServer(EXTERNAL_HOST_PORT)
+ })
+ test.afterAll(async () => {
+ server.close()
+ })
+
+ const internalNavigationTestMatrix = combinate({
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ preload: [false, true] as const,
+ })
+
+ internalNavigationTestMatrix.forEach(
+ ({ thrower, reloadDocument, preload }) => {
+ test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({
+ page,
+ }) => {
+ await page.goto(
+ `/redirect/internal${preload === false ? '?preload=false' : ''}`,
+ )
+ const link = page.getByTestId(
+ `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+ )
+
+ await page.waitForLoadState('networkidle')
+ let requestHappened = false
+
+ const requestPromise = new Promise((resolve) => {
+ page.on('request', (request) => {
+ if (request.url().startsWith(`http://localhost:${PORT}/_server/`)) {
+ requestHappened = true
+ resolve()
+ }
+ })
+ })
+ await link.focus()
+
+ const expectRequestHappened = preload && !reloadDocument
+ const timeoutPromise = new Promise((resolve) =>
+ setTimeout(resolve, expectRequestHappened ? 5000 : 500),
+ )
+ await Promise.race([requestPromise, timeoutPromise])
+ expect(requestHappened).toBe(expectRequestHappened)
+ let fullPageLoad = false
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ await link.click()
+
+ const url = `http://localhost:${PORT}/posts`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ })
+ },
+ )
+
+ const internalDirectVisitTestMatrix = combinate({
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ internalDirectVisitTestMatrix.forEach(({ thrower, reloadDocument }) => {
+ test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ await page.goto(`/redirect/internal/via-${thrower}`)
+
+ const url = `http://localhost:${PORT}/posts`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ })
+ })
+
+ const externalTestMatrix = combinate({
+ scenario: ['navigate', 'direct_visit'] as const,
+ thrower: ['beforeLoad', 'loader'] as const,
+ })
+
+ externalTestMatrix.forEach(({ scenario, thrower }) => {
+ test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({
+ page,
+ }) => {
+ let q = queryString.stringify({
+ externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`,
+ })
+
+ if (scenario === 'navigate') {
+ await page.goto(`/redirect/external?${q}`)
+ await page.waitForLoadState('networkidle')
+ const link = page.getByTestId(`via-${thrower}`)
+ await link.focus()
+ await link.click()
+ } else {
+ await page.goto(`/redirect/external/via-${thrower}?${q}`)
+ }
+
+ const url = `http://localhost:${EXTERNAL_HOST_PORT}/`
+
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ })
+ })
+
+ const serverFnTestMatrix = combinate({
+ target: ['internal', 'external'] as const,
+ scenario: ['navigate', 'direct_visit'] as const,
+ thrower: ['beforeLoad', 'loader'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ serverFnTestMatrix.forEach(
+ ({ target, thrower, scenario, reloadDocument }) => {
+ test(`serverFn redirects to target: ${target}, scenario: ${scenario}, thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ let fullPageLoad = false
+ let q = queryString.stringify({
+ externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`,
+ reloadDocument,
+ })
+
+ if (scenario === 'navigate') {
+ await page.goto(`/redirect/${target}/serverFn?${q}`)
+ await page.waitForLoadState('networkidle')
+ const link = page.getByTestId(
+ `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`,
+ )
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+ await link.focus()
+ await link.click()
+ } else {
+ await page.goto(`/redirect/${target}/serverFn/via-${thrower}?${q}`)
+ }
+
+ const url =
+ target === 'internal'
+ ? `http://localhost:${PORT}/posts`
+ : `http://localhost:${EXTERNAL_HOST_PORT}/`
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ if (target === 'internal' && scenario === 'navigate') {
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ }
+ })
+ },
+ )
+
+ const useServerFnTestMatrix = combinate({
+ target: ['internal', 'external'] as const,
+ reloadDocument: [false, true] as const,
+ })
+
+ useServerFnTestMatrix.forEach(({ target, reloadDocument }) => {
+ test(`useServerFn redirects to target: ${target}, reloadDocument: ${reloadDocument}`, async ({
+ page,
+ }) => {
+ await page.goto(
+ `/redirect/${target}/serverFn/via-useServerFn${reloadDocument ? '?reloadDocument=true' : ''}`,
+ )
+
+ const button = page.getByTestId('redirect-on-click')
+
+ let fullPageLoad = false
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ await button.click()
+
+ const url =
+ target === 'internal'
+ ? `http://localhost:${PORT}/posts`
+ : 'http://example.com/'
+ await page.waitForURL(url)
+ expect(page.url()).toBe(url)
+ if (target === 'internal') {
+ await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport()
+ expect(fullPageLoad).toBe(reloadDocument)
+ }
+ })
+ })
+})
diff --git a/e2e/solid-start/basic/tests/search-params.spec.ts b/e2e/solid-start/basic/tests/search-params.spec.ts
new file mode 100644
index 0000000000..7d1ee6d374
--- /dev/null
+++ b/e2e/solid-start/basic/tests/search-params.spec.ts
@@ -0,0 +1,22 @@
+import { expect } from '@playwright/test'
+import { test } from './fixture'
+
+test('Directly visiting the search-params route without search param set', async ({
+ page,
+}) => {
+ await page.goto('/search-params')
+
+ await new Promise((r) => setTimeout(r, 500))
+ await expect(page.getByTestId('search-param')).toContainText('a')
+ expect(page.url().endsWith('/search-params?step=a'))
+})
+
+test('Directly visiting the search-params route with search param set', async ({
+ page,
+}) => {
+ await page.goto('/search-params?step=b')
+
+ await new Promise((r) => setTimeout(r, 500))
+ await expect(page.getByTestId('search-param')).toContainText('b')
+ expect(page.url().endsWith('/search-params?step=b'))
+})
diff --git a/e2e/solid-start/basic/tests/streaming.spec.ts b/e2e/solid-start/basic/tests/streaming.spec.ts
new file mode 100644
index 0000000000..252bb192aa
--- /dev/null
+++ b/e2e/solid-start/basic/tests/streaming.spec.ts
@@ -0,0 +1,34 @@
+import { expect, test } from '@playwright/test'
+
+test('Navigating to deferred route', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Deferred' }).click()
+
+ await expect(page.getByTestId('regular-person')).toContainText('John Doe')
+ await expect(page.getByTestId('deferred-person')).toContainText(
+ 'Tanner Linsley',
+ )
+ await expect(page.getByTestId('deferred-stuff')).toContainText(
+ 'Hello deferred!',
+ )
+})
+
+test('Directly visiting the deferred route', async ({ page }) => {
+ await page.goto('/deferred')
+
+ await expect(page.getByTestId('regular-person')).toContainText('John Doe')
+ await expect(page.getByTestId('deferred-person')).toContainText(
+ 'Tanner Linsley',
+ )
+ await expect(page.getByTestId('deferred-stuff')).toContainText(
+ 'Hello deferred!',
+ )
+})
+
+test('streaming loader data', async ({ page }) => {
+ await page.goto('/stream')
+
+ await expect(page.getByTestId('promise-data')).toContainText('promise-data')
+ await expect(page.getByTestId('stream-data')).toContainText('stream-data')
+})
diff --git a/e2e/solid-start/basic/tsconfig.json b/e2e/solid-start/basic/tsconfig.json
new file mode 100644
index 0000000000..73e4856648
--- /dev/null
+++ b/e2e/solid-start/basic/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "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": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/e2e/solid-start/scroll-restoration/.gitignore b/e2e/solid-start/scroll-restoration/.gitignore
new file mode 100644
index 0000000000..be342025da
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/.gitignore
@@ -0,0 +1,22 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+.vinxi
+
+/build/
+/api/
+/server/build
+/public/build
+.vinxi
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-start/scroll-restoration/.prettierignore b/e2e/solid-start/scroll-restoration/.prettierignore
new file mode 100644
index 0000000000..2be5eaa6ec
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/solid-start/scroll-restoration/app.config.ts b/e2e/solid-start/scroll-restoration/app.config.ts
new file mode 100644
index 0000000000..5c531d7e3d
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from '@tanstack/solid-start/config'
+import tsConfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ vite: {
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ ],
+ },
+})
diff --git a/e2e/solid-start/scroll-restoration/app/api.ts b/e2e/solid-start/scroll-restoration/app/api.ts
new file mode 100644
index 0000000000..ed511bcd26
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/api.ts
@@ -0,0 +1,6 @@
+import {
+ createStartAPIHandler,
+ defaultAPIFileRouteHandler,
+} from '@tanstack/solid-start/api'
+
+export default createStartAPIHandler(defaultAPIFileRouteHandler)
diff --git a/e2e/solid-start/scroll-restoration/app/client.tsx b/e2e/solid-start/scroll-restoration/app/client.tsx
new file mode 100644
index 0000000000..ba0f02fac0
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/client.tsx
@@ -0,0 +1,8 @@
+///
+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/scroll-restoration/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/scroll-restoration/app/components/DefaultCatchBoundary.tsx
new file mode 100644
index 0000000000..32aed20e67
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+ {
+ router.invalidate()
+ }}
+ class={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot() ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/components/NotFound.tsx b/e2e/solid-start/scroll-restoration/app/components/NotFound.tsx
new file mode 100644
index 0000000000..ca4c1960fa
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/solid-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+ window.history.back()}
+ class="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routeTree.gen.ts b/e2e/solid-start/scroll-restoration/app/routeTree.gen.ts
new file mode 100644
index 0000000000..d5cbca5b25
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routeTree.gen.ts
@@ -0,0 +1,162 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as IndexImport } from './routes/index'
+import { Route as testsWithSearchImport } from './routes/(tests)/with-search'
+import { Route as testsWithLoaderImport } from './routes/(tests)/with-loader'
+import { Route as testsNormalPageImport } from './routes/(tests)/normal-page'
+
+// Create/Update Routes
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const testsWithSearchRoute = testsWithSearchImport.update({
+ id: '/(tests)/with-search',
+ path: '/with-search',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const testsWithLoaderRoute = testsWithLoaderImport.update({
+ id: '/(tests)/with-loader',
+ path: '/with-loader',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const testsNormalPageRoute = testsNormalPageImport.update({
+ id: '/(tests)/normal-page',
+ path: '/normal-page',
+ getParentRoute: () => rootRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/normal-page': {
+ id: '/(tests)/normal-page'
+ path: '/normal-page'
+ fullPath: '/normal-page'
+ preLoaderRoute: typeof testsNormalPageImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/with-loader': {
+ id: '/(tests)/with-loader'
+ path: '/with-loader'
+ fullPath: '/with-loader'
+ preLoaderRoute: typeof testsWithLoaderImport
+ parentRoute: typeof rootRoute
+ }
+ '/(tests)/with-search': {
+ id: '/(tests)/with-search'
+ path: '/with-search'
+ fullPath: '/with-search'
+ preLoaderRoute: typeof testsWithSearchImport
+ parentRoute: typeof rootRoute
+ }
+ }
+}
+
+// Create and export the route tree
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/normal-page': typeof testsNormalPageRoute
+ '/with-loader': typeof testsWithLoaderRoute
+ '/with-search': typeof testsWithSearchRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/normal-page': typeof testsNormalPageRoute
+ '/with-loader': typeof testsWithLoaderRoute
+ '/with-search': typeof testsWithSearchRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/(tests)/normal-page': typeof testsNormalPageRoute
+ '/(tests)/with-loader': typeof testsWithLoaderRoute
+ '/(tests)/with-search': typeof testsWithSearchRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/normal-page' | '/with-loader' | '/with-search'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/normal-page' | '/with-loader' | '/with-search'
+ id:
+ | '__root__'
+ | '/'
+ | '/(tests)/normal-page'
+ | '/(tests)/with-loader'
+ | '/(tests)/with-search'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ testsNormalPageRoute: typeof testsNormalPageRoute
+ testsWithLoaderRoute: typeof testsWithLoaderRoute
+ testsWithSearchRoute: typeof testsWithSearchRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ testsNormalPageRoute: testsNormalPageRoute,
+ testsWithLoaderRoute: testsWithLoaderRoute,
+ testsWithSearchRoute: testsWithSearchRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/",
+ "/(tests)/normal-page",
+ "/(tests)/with-loader",
+ "/(tests)/with-search"
+ ]
+ },
+ "/": {
+ "filePath": "index.tsx"
+ },
+ "/(tests)/normal-page": {
+ "filePath": "(tests)/normal-page.tsx"
+ },
+ "/(tests)/with-loader": {
+ "filePath": "(tests)/with-loader.tsx"
+ },
+ "/(tests)/with-search": {
+ "filePath": "(tests)/with-search.tsx"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/solid-start/scroll-restoration/app/router.tsx b/e2e/solid-start/scroll-restoration/app/router.tsx
new file mode 100644
index 0000000000..b0449d7478
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/router.tsx
@@ -0,0 +1,22 @@
+import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function createRouter() {
+ const router = createTanStackRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: 'intent',
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ })
+
+ return router
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx b/e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx
new file mode 100644
index 0000000000..76d32719ef
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routes/(tests)/normal-page.tsx
@@ -0,0 +1,17 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createFileRoute('/(tests)/normal-page')({
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
normal-page
+
+
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx
new file mode 100644
index 0000000000..aae7611b51
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-loader.tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { ScrollBlock } from '../-components/scroll-block'
+import { sleep } from '~/utils/posts'
+
+export const Route = createFileRoute('/(tests)/with-loader')({
+ loader: async () => {
+ await sleep(1000)
+ return { foo: 'bar' }
+ },
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
lazy-with-loader-page
+
+
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx
new file mode 100644
index 0000000000..5d43b7f9bb
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routes/(tests)/with-search.tsx
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+import { ScrollBlock } from '../-components/scroll-block'
+
+export const Route = createFileRoute('/(tests)/with-search')({
+ validateSearch: zodValidator(z.object({ where: z.string() })),
+ component: Component,
+})
+
+function Component() {
+ return (
+
+
page-with-search
+
+
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx b/e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx
new file mode 100644
index 0000000000..0539293f92
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routes/-components/scroll-block.tsx
@@ -0,0 +1,16 @@
+export const atTheTopId = 'at-the-top'
+export const atTheBottomId = 'at-the-bottom'
+
+export function ScrollBlock({ number = 100 }: { number?: number }) {
+ return (
+ <>
+
+ {Array.from({ length: number }).map((_, i) => (
+ {i}
+ ))}
+
+ At the bottom
+
+ >
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routes/__root.tsx b/e2e/solid-start/scroll-restoration/app/routes/__root.tsx
new file mode 100644
index 0000000000..5f34c665b2
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routes/__root.tsx
@@ -0,0 +1,106 @@
+import * as Solid from 'solid-js'
+import {
+ Link,
+ Outlet,
+ createRootRoute,
+ linkOptions,
+ HeadContent,
+ Scripts,
+} from '@tanstack/solid-router'
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+import { seo } from '~/utils/seo'
+import { Dynamic } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charset: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ...seo({
+ title:
+ 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
+ description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ {
+ rel: 'apple-touch-icon',
+ sizes: '180x180',
+ href: '/apple-touch-icon.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '32x32',
+ href: '/favicon-32x32.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '16x16',
+ href: '/favicon-16x16.png',
+ },
+ { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ }),
+ errorComponent: (props) => {
+ return {props.error.stack}
+ },
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
+
+function Nav({ type }: { type: 'header' | 'footer' }) {
+ const Elem = type === 'header' ? 'header' : 'footer'
+ const prefix = type === 'header' ? 'Head' : 'Foot'
+ return (
+
+
+ {prefix}-/
+ {' '}
+ {(
+ [
+ linkOptions({ to: '/normal-page' }),
+ linkOptions({ to: '/with-loader' }),
+ linkOptions({ to: '/with-search', search: { where: type } }),
+ ] as const
+ ).map((options, i) => (
+
+ {prefix}-{options.to}
+
+ ))}
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/routes/index.tsx b/e2e/solid-start/scroll-restoration/app/routes/index.tsx
new file mode 100644
index 0000000000..e950ce86c9
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/routes/index.tsx
@@ -0,0 +1,35 @@
+import * as Solid from 'solid-js'
+import { Link, createFileRoute, linkOptions } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: HomeComponent,
+})
+
+function HomeComponent() {
+ return (
+
+
Welcome Home!
+
+ The are the links to be tested when navigating away from the index page.
+ Otherwise known as NOT first-load tests, rather known as navigation
+ tests.
+
+ {(
+ [
+ linkOptions({ to: '/normal-page' }),
+ linkOptions({ to: '/with-loader' }),
+ linkOptions({ to: '/with-search', search: { where: 'footer' } }),
+ ] as const
+ ).map((options, i) => (
+
+
{options.to} tests
+
+
+ {options.to}#at-the-bottom
+
+
+
+ ))}
+
+ )
+}
diff --git a/e2e/solid-start/scroll-restoration/app/ssr.tsx b/e2e/solid-start/scroll-restoration/app/ssr.tsx
new file mode 100644
index 0000000000..6d10bea05f
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/ssr.tsx
@@ -0,0 +1,12 @@
+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/e2e/solid-start/scroll-restoration/app/styles/app.css b/e2e/solid-start/scroll-restoration/app/styles/app.css
new file mode 100644
index 0000000000..c53c870665
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/styles/app.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ color-scheme: light dark;
+ }
+
+ * {
+ @apply border-gray-200 dark:border-gray-800;
+ }
+
+ 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/e2e/solid-start/scroll-restoration/app/utils/posts.tsx b/e2e/solid-start/scroll-restoration/app/utils/posts.tsx
new file mode 100644
index 0000000000..56007aa0f6
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/utils/posts.tsx
@@ -0,0 +1,37 @@
+import axios from 'redaxios'
+
+export function sleep(ms: number) {
+ return new Promise((r) => setTimeout(r, ms))
+}
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export class PostNotFoundError extends Error {}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ if (err.status === 404) {
+ throw new PostNotFoundError(`Post with id "${postId}" not found!`)
+ }
+ throw err
+ })
+
+ return post
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/solid-start/scroll-restoration/app/utils/seo.ts b/e2e/solid-start/scroll-restoration/app/utils/seo.ts
new file mode 100644
index 0000000000..d18ad84b74
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/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/e2e/solid-start/scroll-restoration/app/utils/users.tsx b/e2e/solid-start/scroll-restoration/app/utils/users.tsx
new file mode 100644
index 0000000000..79b45867d5
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/app/utils/users.tsx
@@ -0,0 +1,10 @@
+export type User = {
+ id: number
+ name: string
+ email: string
+}
+
+const PORT =
+ import.meta.env.VITE_SERVER_PORT || process.env.VITE_SERVER_PORT || 3000
+
+export const DEPLOY_URL = `http://localhost:${PORT}`
diff --git a/e2e/solid-start/scroll-restoration/package.json b/e2e/solid-start/scroll-restoration/package.json
new file mode 100644
index 0000000000..a9aac0817b
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "tanstack-solid-start-e2e-basic-scroll-restoration",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vinxi dev --port 3000",
+ "dev:e2e": "vinxi dev",
+ "build": "vinxi build && tsc --noEmit",
+ "start": "vinxi start",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "@tanstack/zod-adapter": "workspace:^",
+ "solid-js": "^1.9.5",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0",
+ "vinxi": "0.5.3",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "vite-plugin-solid": "^2.11.6",
+ "autoprefixer": "^10.4.20",
+ "combinate": "^1.1.11",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/solid-start/scroll-restoration/playwright.config.ts b/e2e/solid-start/scroll-restoration/playwright.config.ts
new file mode 100644
index 0000000000..95d043d48b
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-start/scroll-restoration/postcss.config.mjs b/e2e/solid-start/scroll-restoration/postcss.config.mjs
new file mode 100644
index 0000000000..2e7af2b7f1
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-start/scroll-restoration/public/android-chrome-192x192.png b/e2e/solid-start/scroll-restoration/public/android-chrome-192x192.png
new file mode 100644
index 0000000000..09c8324f8c
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/android-chrome-192x192.png differ
diff --git a/e2e/solid-start/scroll-restoration/public/android-chrome-512x512.png b/e2e/solid-start/scroll-restoration/public/android-chrome-512x512.png
new file mode 100644
index 0000000000..11d626ea3d
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/android-chrome-512x512.png differ
diff --git a/e2e/solid-start/scroll-restoration/public/apple-touch-icon.png b/e2e/solid-start/scroll-restoration/public/apple-touch-icon.png
new file mode 100644
index 0000000000..5a9423cc02
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/apple-touch-icon.png differ
diff --git a/e2e/solid-start/scroll-restoration/public/favicon-16x16.png b/e2e/solid-start/scroll-restoration/public/favicon-16x16.png
new file mode 100644
index 0000000000..e3389b0044
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/favicon-16x16.png differ
diff --git a/e2e/solid-start/scroll-restoration/public/favicon-32x32.png b/e2e/solid-start/scroll-restoration/public/favicon-32x32.png
new file mode 100644
index 0000000000..900c77d444
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/favicon-32x32.png differ
diff --git a/e2e/solid-start/scroll-restoration/public/favicon.ico b/e2e/solid-start/scroll-restoration/public/favicon.ico
new file mode 100644
index 0000000000..1a1751676f
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/favicon.ico differ
diff --git a/e2e/solid-start/scroll-restoration/public/favicon.png b/e2e/solid-start/scroll-restoration/public/favicon.png
new file mode 100644
index 0000000000..1e77bc0609
Binary files /dev/null and b/e2e/solid-start/scroll-restoration/public/favicon.png differ
diff --git a/e2e/solid-start/scroll-restoration/public/script.js b/e2e/solid-start/scroll-restoration/public/script.js
new file mode 100644
index 0000000000..897477e7d0
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/public/script.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_1 loaded')
+window.SCRIPT_1 = true
diff --git a/e2e/solid-start/scroll-restoration/public/script2.js b/e2e/solid-start/scroll-restoration/public/script2.js
new file mode 100644
index 0000000000..819af30daf
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/public/script2.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_2 loaded')
+window.SCRIPT_2 = true
diff --git a/e2e/solid-start/scroll-restoration/public/site.webmanifest b/e2e/solid-start/scroll-restoration/public/site.webmanifest
new file mode 100644
index 0000000000..fa99de77db
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/e2e/solid-start/scroll-restoration/tailwind.config.mjs b/e2e/solid-start/scroll-restoration/tailwind.config.mjs
new file mode 100644
index 0000000000..07c3598bac
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./app/**/*.{js,jsx,ts,tsx}'],
+}
diff --git a/e2e/solid-start/scroll-restoration/tests/app.spec.ts b/e2e/solid-start/scroll-restoration/tests/app.spec.ts
new file mode 100644
index 0000000000..413a68593e
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/tests/app.spec.ts
@@ -0,0 +1,46 @@
+import { expect, test } from '@playwright/test'
+
+test('Smoke - Renders home', async ({ page }) => {
+ await page.goto('/')
+ await expect(
+ page.getByRole('heading', { name: 'Welcome Home!' }),
+ ).toBeVisible()
+})
+
+// Test for scroll related stuff
+;[
+ { to: '/normal-page' },
+ { to: '/with-loader' },
+ { to: '/with-search', search: { where: 'footer' } },
+].forEach((options) => {
+ test(`On navigate to ${options.to} (from the header), scroll should be at top`, async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page.getByRole('link', { name: `Head-${options.to}` }).click()
+ await expect(page.getByTestId('at-the-top')).toBeInViewport()
+ })
+
+ // scroll should be at the bottom on navigation after the page is loaded
+ test(`On navigate via index page tests to ${options.to}, scroll should resolve at the bottom`, async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page
+ .getByRole('link', { name: `${options.to}#at-the-bottom` })
+ .click()
+ await expect(page.getByTestId('at-the-bottom')).toBeInViewport()
+ })
+
+ // scroll should be at the bottom on first load
+ test(`On first load of ${options.to}, scroll should resolve resolve at the bottom`, async ({
+ page,
+ }) => {
+ let url: string = options.to
+ if ('search' in options) {
+ url = `${url}?where=${options.search}`
+ }
+ await page.goto(`${url}#at-the-bottom`)
+ await expect(page.getByTestId('at-the-bottom')).toBeInViewport()
+ })
+})
diff --git a/e2e/solid-start/scroll-restoration/tsconfig.json b/e2e/solid-start/scroll-restoration/tsconfig.json
new file mode 100644
index 0000000000..73e4856648
--- /dev/null
+++ b/e2e/solid-start/scroll-restoration/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "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": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/e2e/solid-start/server-functions/.gitignore b/e2e/solid-start/server-functions/.gitignore
new file mode 100644
index 0000000000..be342025da
--- /dev/null
+++ b/e2e/solid-start/server-functions/.gitignore
@@ -0,0 +1,22 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+.vinxi
+
+/build/
+/api/
+/server/build
+/public/build
+.vinxi
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-start/server-functions/.prettierignore b/e2e/solid-start/server-functions/.prettierignore
new file mode 100644
index 0000000000..2be5eaa6ec
--- /dev/null
+++ b/e2e/solid-start/server-functions/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/solid-start/server-functions/app.config.ts b/e2e/solid-start/server-functions/app.config.ts
new file mode 100644
index 0000000000..5c531d7e3d
--- /dev/null
+++ b/e2e/solid-start/server-functions/app.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from '@tanstack/solid-start/config'
+import tsConfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ vite: {
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ ],
+ },
+})
diff --git a/e2e/solid-start/server-functions/app/client.tsx b/e2e/solid-start/server-functions/app/client.tsx
new file mode 100644
index 0000000000..ba0f02fac0
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/client.tsx
@@ -0,0 +1,8 @@
+///
+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/server-functions/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx
new file mode 100644
index 0000000000..32aed20e67
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+ {
+ router.invalidate()
+ }}
+ class={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot() ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/components/NotFound.tsx b/e2e/solid-start/server-functions/app/components/NotFound.tsx
new file mode 100644
index 0000000000..eb0a968d39
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/solid-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+ window.history.back()}
+ class="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routeTree.gen.ts b/e2e/solid-start/server-functions/app/routeTree.gen.ts
new file mode 100644
index 0000000000..4a34fccd1c
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routeTree.gen.ts
@@ -0,0 +1,456 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as SubmitPostFormdataImport } from './routes/submit-post-formdata'
+import { Route as StatusImport } from './routes/status'
+import { Route as SerializeFormDataImport } from './routes/serialize-form-data'
+import { Route as ReturnNullImport } from './routes/return-null'
+import { Route as RawResponseImport } from './routes/raw-response'
+import { Route as MultipartImport } from './routes/multipart'
+import { Route as IsomorphicFnsImport } from './routes/isomorphic-fns'
+import { Route as HeadersImport } from './routes/headers'
+import { Route as EnvOnlyImport } from './routes/env-only'
+import { Route as DeadCodePreserveImport } from './routes/dead-code-preserve'
+import { Route as ConsistentImport } from './routes/consistent'
+import { Route as AbortSignalImport } from './routes/abort-signal'
+import { Route as IndexImport } from './routes/index'
+import { Route as CookiesIndexImport } from './routes/cookies/index'
+import { Route as CookiesSetImport } from './routes/cookies/set'
+
+// Create/Update Routes
+
+const SubmitPostFormdataRoute = SubmitPostFormdataImport.update({
+ id: '/submit-post-formdata',
+ path: '/submit-post-formdata',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const StatusRoute = StatusImport.update({
+ id: '/status',
+ path: '/status',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const SerializeFormDataRoute = SerializeFormDataImport.update({
+ id: '/serialize-form-data',
+ path: '/serialize-form-data',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ReturnNullRoute = ReturnNullImport.update({
+ id: '/return-null',
+ path: '/return-null',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const RawResponseRoute = RawResponseImport.update({
+ id: '/raw-response',
+ path: '/raw-response',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const MultipartRoute = MultipartImport.update({
+ id: '/multipart',
+ path: '/multipart',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const IsomorphicFnsRoute = IsomorphicFnsImport.update({
+ id: '/isomorphic-fns',
+ path: '/isomorphic-fns',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const HeadersRoute = HeadersImport.update({
+ id: '/headers',
+ path: '/headers',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const EnvOnlyRoute = EnvOnlyImport.update({
+ id: '/env-only',
+ path: '/env-only',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const DeadCodePreserveRoute = DeadCodePreserveImport.update({
+ id: '/dead-code-preserve',
+ path: '/dead-code-preserve',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ConsistentRoute = ConsistentImport.update({
+ id: '/consistent',
+ path: '/consistent',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const AbortSignalRoute = AbortSignalImport.update({
+ id: '/abort-signal',
+ path: '/abort-signal',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const CookiesIndexRoute = CookiesIndexImport.update({
+ id: '/cookies/',
+ path: '/cookies/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const CookiesSetRoute = CookiesSetImport.update({
+ id: '/cookies/set',
+ path: '/cookies/set',
+ getParentRoute: () => rootRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/abort-signal': {
+ id: '/abort-signal'
+ path: '/abort-signal'
+ fullPath: '/abort-signal'
+ preLoaderRoute: typeof AbortSignalImport
+ parentRoute: typeof rootRoute
+ }
+ '/consistent': {
+ id: '/consistent'
+ path: '/consistent'
+ fullPath: '/consistent'
+ preLoaderRoute: typeof ConsistentImport
+ parentRoute: typeof rootRoute
+ }
+ '/dead-code-preserve': {
+ id: '/dead-code-preserve'
+ path: '/dead-code-preserve'
+ fullPath: '/dead-code-preserve'
+ preLoaderRoute: typeof DeadCodePreserveImport
+ parentRoute: typeof rootRoute
+ }
+ '/env-only': {
+ id: '/env-only'
+ path: '/env-only'
+ fullPath: '/env-only'
+ preLoaderRoute: typeof EnvOnlyImport
+ parentRoute: typeof rootRoute
+ }
+ '/headers': {
+ id: '/headers'
+ path: '/headers'
+ fullPath: '/headers'
+ preLoaderRoute: typeof HeadersImport
+ parentRoute: typeof rootRoute
+ }
+ '/isomorphic-fns': {
+ id: '/isomorphic-fns'
+ path: '/isomorphic-fns'
+ fullPath: '/isomorphic-fns'
+ preLoaderRoute: typeof IsomorphicFnsImport
+ parentRoute: typeof rootRoute
+ }
+ '/multipart': {
+ id: '/multipart'
+ path: '/multipart'
+ fullPath: '/multipart'
+ preLoaderRoute: typeof MultipartImport
+ parentRoute: typeof rootRoute
+ }
+ '/raw-response': {
+ id: '/raw-response'
+ path: '/raw-response'
+ fullPath: '/raw-response'
+ preLoaderRoute: typeof RawResponseImport
+ parentRoute: typeof rootRoute
+ }
+ '/return-null': {
+ id: '/return-null'
+ path: '/return-null'
+ fullPath: '/return-null'
+ preLoaderRoute: typeof ReturnNullImport
+ parentRoute: typeof rootRoute
+ }
+ '/serialize-form-data': {
+ id: '/serialize-form-data'
+ path: '/serialize-form-data'
+ fullPath: '/serialize-form-data'
+ preLoaderRoute: typeof SerializeFormDataImport
+ parentRoute: typeof rootRoute
+ }
+ '/status': {
+ id: '/status'
+ path: '/status'
+ fullPath: '/status'
+ preLoaderRoute: typeof StatusImport
+ parentRoute: typeof rootRoute
+ }
+ '/submit-post-formdata': {
+ id: '/submit-post-formdata'
+ path: '/submit-post-formdata'
+ fullPath: '/submit-post-formdata'
+ preLoaderRoute: typeof SubmitPostFormdataImport
+ parentRoute: typeof rootRoute
+ }
+ '/cookies/set': {
+ id: '/cookies/set'
+ path: '/cookies/set'
+ fullPath: '/cookies/set'
+ preLoaderRoute: typeof CookiesSetImport
+ parentRoute: typeof rootRoute
+ }
+ '/cookies/': {
+ id: '/cookies/'
+ path: '/cookies'
+ fullPath: '/cookies'
+ preLoaderRoute: typeof CookiesIndexImport
+ parentRoute: typeof rootRoute
+ }
+ }
+}
+
+// Create and export the route tree
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/abort-signal': typeof AbortSignalRoute
+ '/consistent': typeof ConsistentRoute
+ '/dead-code-preserve': typeof DeadCodePreserveRoute
+ '/env-only': typeof EnvOnlyRoute
+ '/headers': typeof HeadersRoute
+ '/isomorphic-fns': typeof IsomorphicFnsRoute
+ '/multipart': typeof MultipartRoute
+ '/raw-response': typeof RawResponseRoute
+ '/return-null': typeof ReturnNullRoute
+ '/serialize-form-data': typeof SerializeFormDataRoute
+ '/status': typeof StatusRoute
+ '/submit-post-formdata': typeof SubmitPostFormdataRoute
+ '/cookies/set': typeof CookiesSetRoute
+ '/cookies': typeof CookiesIndexRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/abort-signal': typeof AbortSignalRoute
+ '/consistent': typeof ConsistentRoute
+ '/dead-code-preserve': typeof DeadCodePreserveRoute
+ '/env-only': typeof EnvOnlyRoute
+ '/headers': typeof HeadersRoute
+ '/isomorphic-fns': typeof IsomorphicFnsRoute
+ '/multipart': typeof MultipartRoute
+ '/raw-response': typeof RawResponseRoute
+ '/return-null': typeof ReturnNullRoute
+ '/serialize-form-data': typeof SerializeFormDataRoute
+ '/status': typeof StatusRoute
+ '/submit-post-formdata': typeof SubmitPostFormdataRoute
+ '/cookies/set': typeof CookiesSetRoute
+ '/cookies': typeof CookiesIndexRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/abort-signal': typeof AbortSignalRoute
+ '/consistent': typeof ConsistentRoute
+ '/dead-code-preserve': typeof DeadCodePreserveRoute
+ '/env-only': typeof EnvOnlyRoute
+ '/headers': typeof HeadersRoute
+ '/isomorphic-fns': typeof IsomorphicFnsRoute
+ '/multipart': typeof MultipartRoute
+ '/raw-response': typeof RawResponseRoute
+ '/return-null': typeof ReturnNullRoute
+ '/serialize-form-data': typeof SerializeFormDataRoute
+ '/status': typeof StatusRoute
+ '/submit-post-formdata': typeof SubmitPostFormdataRoute
+ '/cookies/set': typeof CookiesSetRoute
+ '/cookies/': typeof CookiesIndexRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/abort-signal'
+ | '/consistent'
+ | '/dead-code-preserve'
+ | '/env-only'
+ | '/headers'
+ | '/isomorphic-fns'
+ | '/multipart'
+ | '/raw-response'
+ | '/return-null'
+ | '/serialize-form-data'
+ | '/status'
+ | '/submit-post-formdata'
+ | '/cookies/set'
+ | '/cookies'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/abort-signal'
+ | '/consistent'
+ | '/dead-code-preserve'
+ | '/env-only'
+ | '/headers'
+ | '/isomorphic-fns'
+ | '/multipart'
+ | '/raw-response'
+ | '/return-null'
+ | '/serialize-form-data'
+ | '/status'
+ | '/submit-post-formdata'
+ | '/cookies/set'
+ | '/cookies'
+ id:
+ | '__root__'
+ | '/'
+ | '/abort-signal'
+ | '/consistent'
+ | '/dead-code-preserve'
+ | '/env-only'
+ | '/headers'
+ | '/isomorphic-fns'
+ | '/multipart'
+ | '/raw-response'
+ | '/return-null'
+ | '/serialize-form-data'
+ | '/status'
+ | '/submit-post-formdata'
+ | '/cookies/set'
+ | '/cookies/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ AbortSignalRoute: typeof AbortSignalRoute
+ ConsistentRoute: typeof ConsistentRoute
+ DeadCodePreserveRoute: typeof DeadCodePreserveRoute
+ EnvOnlyRoute: typeof EnvOnlyRoute
+ HeadersRoute: typeof HeadersRoute
+ IsomorphicFnsRoute: typeof IsomorphicFnsRoute
+ MultipartRoute: typeof MultipartRoute
+ RawResponseRoute: typeof RawResponseRoute
+ ReturnNullRoute: typeof ReturnNullRoute
+ SerializeFormDataRoute: typeof SerializeFormDataRoute
+ StatusRoute: typeof StatusRoute
+ SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
+ CookiesSetRoute: typeof CookiesSetRoute
+ CookiesIndexRoute: typeof CookiesIndexRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ AbortSignalRoute: AbortSignalRoute,
+ ConsistentRoute: ConsistentRoute,
+ DeadCodePreserveRoute: DeadCodePreserveRoute,
+ EnvOnlyRoute: EnvOnlyRoute,
+ HeadersRoute: HeadersRoute,
+ IsomorphicFnsRoute: IsomorphicFnsRoute,
+ MultipartRoute: MultipartRoute,
+ RawResponseRoute: RawResponseRoute,
+ ReturnNullRoute: ReturnNullRoute,
+ SerializeFormDataRoute: SerializeFormDataRoute,
+ StatusRoute: StatusRoute,
+ SubmitPostFormdataRoute: SubmitPostFormdataRoute,
+ CookiesSetRoute: CookiesSetRoute,
+ CookiesIndexRoute: CookiesIndexRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/",
+ "/abort-signal",
+ "/consistent",
+ "/dead-code-preserve",
+ "/env-only",
+ "/headers",
+ "/isomorphic-fns",
+ "/multipart",
+ "/raw-response",
+ "/return-null",
+ "/serialize-form-data",
+ "/status",
+ "/submit-post-formdata",
+ "/cookies/set",
+ "/cookies/"
+ ]
+ },
+ "/": {
+ "filePath": "index.tsx"
+ },
+ "/abort-signal": {
+ "filePath": "abort-signal.tsx"
+ },
+ "/consistent": {
+ "filePath": "consistent.tsx"
+ },
+ "/dead-code-preserve": {
+ "filePath": "dead-code-preserve.tsx"
+ },
+ "/env-only": {
+ "filePath": "env-only.tsx"
+ },
+ "/headers": {
+ "filePath": "headers.tsx"
+ },
+ "/isomorphic-fns": {
+ "filePath": "isomorphic-fns.tsx"
+ },
+ "/multipart": {
+ "filePath": "multipart.tsx"
+ },
+ "/raw-response": {
+ "filePath": "raw-response.tsx"
+ },
+ "/return-null": {
+ "filePath": "return-null.tsx"
+ },
+ "/serialize-form-data": {
+ "filePath": "serialize-form-data.tsx"
+ },
+ "/status": {
+ "filePath": "status.tsx"
+ },
+ "/submit-post-formdata": {
+ "filePath": "submit-post-formdata.tsx"
+ },
+ "/cookies/set": {
+ "filePath": "cookies/set.tsx"
+ },
+ "/cookies/": {
+ "filePath": "cookies/index.tsx"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/solid-start/server-functions/app/router.tsx b/e2e/solid-start/server-functions/app/router.tsx
new file mode 100644
index 0000000000..c45bed4758
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/router.tsx
@@ -0,0 +1,22 @@
+import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function createRouter() {
+ const router = createTanStackRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ scrollRestoration: true,
+ })
+
+ return router
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/e2e/solid-start/server-functions/app/routes/__root.tsx b/e2e/solid-start/server-functions/app/routes/__root.tsx
new file mode 100644
index 0000000000..d25a8953b5
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/__root.tsx
@@ -0,0 +1,33 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ],
+ links: [{ rel: 'stylesheet', href: appCss }],
+ }),
+ errorComponent: (props) => {
+ return {props.error.stack}
+ },
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/abort-signal.tsx b/e2e/solid-start/server-functions/app/routes/abort-signal.tsx
new file mode 100644
index 0000000000..752346264e
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/abort-signal.tsx
@@ -0,0 +1,85 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+import * as Solid from 'solid-js'
+
+export const Route = createFileRoute('/abort-signal')({
+ component: RouteComponent,
+})
+
+const abortableServerFn = createServerFn().handler(
+ async ({ context, signal }) => {
+ console.log('server function started', { context, signal })
+ return new Promise((resolve, reject) => {
+ if (signal.aborted) {
+ return reject(new Error('Aborted before start'))
+ }
+ const timerId = setTimeout(() => {
+ console.log('server function finished')
+ resolve('server function result')
+ }, 1000)
+ const onAbort = () => {
+ clearTimeout(timerId)
+ console.log('server function aborted')
+ reject(new Error('Aborted'))
+ }
+ signal.addEventListener('abort', onAbort, { once: true })
+ })
+ },
+)
+
+function RouteComponent() {
+ const [errorMessage, setErrorMessage] = Solid.createSignal<
+ string | undefined
+ >(undefined)
+ const [result, setResult] = Solid.createSignal(undefined)
+
+ const reset = () => {
+ setErrorMessage(undefined)
+ setResult(undefined)
+ }
+ return (
+
+
{
+ reset()
+ const controller = new AbortController()
+ const serverFnPromise = abortableServerFn({
+ signal: controller.signal,
+ })
+ const timeoutPromise = new Promise((resolve) =>
+ setTimeout(resolve, 500),
+ )
+ await timeoutPromise
+ controller.abort()
+ try {
+ const serverFnResult = await serverFnPromise
+ setResult(serverFnResult)
+ } catch (error) {
+ setErrorMessage((error as any).message)
+ }
+ }}
+ >
+ call server function with abort signal
+
+
+
{
+ reset()
+ const serverFnResult = await abortableServerFn()
+ setResult(serverFnResult)
+ }}
+ >
+ call server function
+
+
+ result:
{result() ?? '$undefined'}
+
+
+ message:{' '}
+
{errorMessage() ?? '$undefined'}
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/consistent.tsx b/e2e/solid-start/server-functions/app/routes/consistent.tsx
new file mode 100644
index 0000000000..9e4c1b3b08
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/consistent.tsx
@@ -0,0 +1,121 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import * as Solid from 'solid-js'
+import { createServerFn } from '@tanstack/solid-start'
+
+/**
+ * This checks whether the returned payloads from a
+ * server function are the same, regardless of whether the server function is
+ * called directly from the client or from within the server function.
+ * @link https://github.com/TanStack/router/issues/1866
+ * @link https://github.com/TanStack/router/issues/2481
+ */
+
+export const Route = createFileRoute('/consistent')({
+ component: ConsistentServerFnCalls,
+ loader: async () => {
+ const data = await cons_serverGetFn1({ data: { username: 'TEST' } })
+ console.log('cons_serverGetFn1', data)
+ return { data }
+ },
+})
+
+const cons_getFn1 = createServerFn()
+ .validator((d: { username: string }) => d)
+ .handler(({ data }) => {
+ return { payload: data }
+ })
+
+const cons_serverGetFn1 = createServerFn()
+ .validator((d: { username: string }) => d)
+ .handler(async ({ data }) => {
+ return cons_getFn1({ data })
+ })
+
+const cons_postFn1 = createServerFn({ method: 'POST' })
+ .validator((d: { username: string }) => d)
+ .handler(({ data }) => {
+ return { payload: data }
+ })
+
+const cons_serverPostFn1 = createServerFn({ method: 'POST' })
+ .validator((d: { username: string }) => d)
+ .handler(({ data }) => {
+ return cons_postFn1({ data })
+ })
+
+function ConsistentServerFnCalls() {
+ const [getServerResult, setGetServerResult] = Solid.createSignal({})
+ const [getDirectResult, setGetDirectResult] = Solid.createSignal({})
+
+ const [postServerResult, setPostServerResult] = Solid.createSignal({})
+ const [postDirectResult, setPostDirectResult] = Solid.createSignal({})
+
+ return (
+
+
Consistent Server Fn GET Calls
+
+ This component checks whether the returned payloads from server function
+ are the same, regardless of whether the server function is called
+ directly from the client or from within the server function.
+
+
+ It should return{' '}
+
+
+ {JSON.stringify({ payload: { username: 'TEST' } })}
+
+
+
+
+ {`GET: cons_getFn1 called from server cons_serverGetFn1 returns`}
+
+
+ {JSON.stringify(getServerResult())}
+
+
+
+ {`GET: cons_getFn1 called directly returns`}
+
+
+ {JSON.stringify(getDirectResult())}
+
+
+
+ {`POST: cons_postFn1 called from cons_serverPostFn1 returns`}
+
+
+ {JSON.stringify(postServerResult())}
+
+
+
+ {`POST: cons_postFn1 called directly returns`}
+
+
+ {JSON.stringify(postDirectResult())}
+
+
+
{
+ // GET calls
+ cons_serverGetFn1({ data: { username: 'TEST' } }).then(
+ setGetServerResult,
+ )
+ cons_getFn1({ data: { username: 'TEST' } }).then(setGetDirectResult)
+
+ // POST calls
+ cons_serverPostFn1({ data: { username: 'TEST' } }).then(
+ setPostServerResult,
+ )
+ cons_postFn1({ data: { username: 'TEST' } }).then(setPostDirectResult)
+
+ cons_postFn1({ data: { username: 'TEST' } }).then(setPostDirectResult)
+ }}
+ >
+ Test Consistent server function responses
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/cookies/index.tsx b/e2e/solid-start/server-functions/app/routes/cookies/index.tsx
new file mode 100644
index 0000000000..5cccd18057
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/cookies/index.tsx
@@ -0,0 +1,24 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+import { z } from 'zod'
+
+const cookieSchema = z
+ .object({ value: z.string() })
+ .catch(() => ({ value: `CLIENT-${Date.now()}` }))
+export const Route = createFileRoute('/cookies/')({
+ validateSearch: cookieSchema,
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+ return (
+
+ got to route that sets the cookies with {JSON.stringify(search())}
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/cookies/set.tsx b/e2e/solid-start/server-functions/app/routes/cookies/set.tsx
new file mode 100644
index 0000000000..32e06725cf
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/cookies/set.tsx
@@ -0,0 +1,66 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+import { setCookie } from '@tanstack/solid-start/server'
+import { z } from 'zod'
+import Cookies from 'js-cookie'
+import * as Solid from 'solid-js'
+
+const cookieSchema = z.object({ value: z.string() })
+
+export const Route = createFileRoute('/cookies/set')({
+ validateSearch: cookieSchema,
+ loaderDeps: ({ search }) => search,
+ loader: async ({ deps }) => {
+ await setCookieServerFn1({ data: deps })
+ await setCookieServerFn2({ data: deps })
+ },
+ component: RouteComponent,
+})
+
+export const setCookieServerFn1 = createServerFn()
+ .validator(cookieSchema)
+ .handler(({ data }) => {
+ setCookie(`cookie-1-${data.value}`, data.value)
+ setCookie(`cookie-2-${data.value}`, data.value)
+ })
+
+export const setCookieServerFn2 = createServerFn()
+ .validator(cookieSchema)
+ .handler(({ data }) => {
+ setCookie(`cookie-3-${data.value}`, data.value)
+ setCookie(`cookie-4-${data.value}`, data.value)
+ })
+
+function RouteComponent() {
+ const search = Route.useSearch()
+ const [cookiesFromDocument, setCookiesFromDocument] = Solid.createSignal<
+ Record | undefined
+ >(undefined)
+ Solid.createEffect(() => {
+ const tempCookies: Record = {}
+ for (let i = 1; i <= 4; i++) {
+ const key = `cookie-${i}-${search().value}`
+ tempCookies[key] = Cookies.get(key)
+ }
+ setCookiesFromDocument(tempCookies)
+ }, [])
+ return (
+
+
cookies result
+
+
+
+ cookie
+ value
+
+ {Object.entries(cookiesFromDocument() || {}).map(([key, value]) => (
+
+ {key}
+ {value}
+
+ ))}
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx b/e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx
new file mode 100644
index 0000000000..96d7a3ca13
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/dead-code-preserve.tsx
@@ -0,0 +1,60 @@
+import * as fs from 'node:fs'
+import { createServerFn } from '@tanstack/solid-start'
+import { getRequestHeader } from '@tanstack/solid-start/server'
+import { createSignal } from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/dead-code-preserve')({
+ component: RouteComponent,
+})
+
+// by using this we make sure DCE still works - this errors when imported on the client
+
+const filePath = 'count-effect.txt'
+
+async function readCount() {
+ return parseInt(
+ await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
+ )
+}
+
+async function updateCount() {
+ const count = await readCount()
+ await fs.promises.writeFile(filePath, `${count + 1}`)
+ return true
+}
+
+const writeFileServerFn = createServerFn().handler(async () => {
+ // eslint-disable-next-line unused-imports/no-unused-vars
+ const test = await updateCount()
+ return getRequestHeader('X-Test')
+})
+
+const readFileServerFn = createServerFn().handler(async () => {
+ const data = await readCount()
+ return data
+})
+
+function RouteComponent() {
+ const [serverFnOutput, setServerFnOutput] = createSignal()
+ return (
+
+
Dead code test
+
+ This server function writes to a file as a side effect, then reads it.
+
+
{
+ await writeFileServerFn({ headers: { 'X-Test': 'test' } })
+ setServerFnOutput(await readFileServerFn())
+ }}
+ >
+ Call Dead Code Fn
+
+
Server output
+
{serverFnOutput()}
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/env-only.tsx b/e2e/solid-start/server-functions/app/routes/env-only.tsx
new file mode 100644
index 0000000000..df8f08eed2
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/env-only.tsx
@@ -0,0 +1,73 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { clientOnly, createServerFn, serverOnly } from '@tanstack/solid-start'
+import { createSignal } from 'solid-js'
+
+const serverEcho = serverOnly((input: string) => 'server got: ' + input)
+const clientEcho = clientOnly((input: string) => 'client got: ' + input)
+
+const testOnServer = createServerFn().handler(() => {
+ const serverOnServer = serverEcho('hello')
+ let clientOnServer: string
+ try {
+ clientOnServer = clientEcho('hello')
+ } catch (e) {
+ clientOnServer =
+ 'clientEcho threw an error: ' +
+ (e instanceof Error ? e.message : String(e))
+ }
+ return { serverOnServer, clientOnServer }
+})
+
+export const Route = createFileRoute('/env-only')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const [results, setResults] = createSignal>>()
+
+ async function handleClick() {
+ const { serverOnServer, clientOnServer } = await testOnServer()
+ const clientOnClient = clientEcho('hello')
+ let serverOnClient: string
+ try {
+ serverOnClient = serverEcho('hello')
+ } catch (e) {
+ serverOnClient =
+ 'serverEcho threw an error: ' +
+ (e instanceof Error ? e.message : String(e))
+ }
+ setResults({
+ serverOnServer,
+ clientOnServer,
+ clientOnClient,
+ serverOnClient,
+ })
+ }
+
+ return (
+
+
+ Run
+
+ {!!results() && (
+
+
+ serverEcho
+
+ When we called the function on the server:
+
{results()?.serverOnServer}
+ When we called the function on the client:
+
{results()?.serverOnClient}
+
+
+ clientEcho
+
+ When we called the function on the server:
+
{results()?.clientOnServer}
+ When we called the function on the client:
+
{results()?.clientOnClient}
+
+ )}
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/headers.tsx b/e2e/solid-start/server-functions/app/routes/headers.tsx
new file mode 100644
index 0000000000..94ef5a05c1
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/headers.tsx
@@ -0,0 +1,68 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import * as Solid from 'solid-js'
+import { createServerFn } from '@tanstack/solid-start'
+import { getHeaders, setHeader } from '@tanstack/solid-start/server'
+import type { HTTPHeaderName } from '@tanstack/solid-start/server'
+
+export const Route = createFileRoute('/headers')({
+ loader: async () => {
+ return {
+ testHeaders: await getTestHeaders(),
+ }
+ },
+ component: () => {
+ const loaderData = Route.useLoaderData()
+ return
+ },
+})
+
+export const getTestHeaders = createServerFn().handler(() => {
+ setHeader('x-test-header', 'test-value')
+
+ return {
+ serverHeaders: getHeaders(),
+ headers: getHeaders(),
+ }
+})
+
+type TestHeadersResult = {
+ headers?: Partial>
+ serverHeaders?: Partial>
+}
+
+function ResponseHeaders({
+ initialTestHeaders,
+}: {
+ initialTestHeaders: TestHeadersResult
+}) {
+ const [testHeadersResult, setTestHeadersResult] =
+ Solid.createSignal(initialTestHeaders)
+
+ return (
+
+
Headers Test
+
+
+
Headers:
+
+ {JSON.stringify(testHeadersResult().headers, null, 2)}
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/index.tsx b/e2e/solid-start/server-functions/app/routes/index.tsx
new file mode 100644
index 0000000000..bf28fb8b00
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/index.tsx
@@ -0,0 +1,78 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Server functions E2E tests
+
+
+
+ Consistent server function returns both on client and server for GET
+ and POST calls
+
+
+
+
+ submitting multipart/form-data as server function input
+
+
+
+
+ Server function can return null for GET and POST calls
+
+
+
+
+ Server function can correctly send and receive FormData
+
+
+
+
+ server function can correctly send and receive headers
+
+
+
+
+ Direct POST submitting FormData to a Server function returns the
+ correct message
+
+
+
+
+ invoking a server function with custom response status code
+
+
+
+
+ isomorphic functions can have different implementations on client
+ and server
+
+
+
+
+ env-only functions can only be called on the server or client
+ respectively
+
+
+
+ server function sets cookies
+
+
+
+ dead code elimation only affects code after transformation
+
+
+
+ aborting a server function call
+
+
+ server function returns raw response
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx
new file mode 100644
index 0000000000..3327b38af7
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/isomorphic-fns.tsx
@@ -0,0 +1,79 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createIsomorphicFn, createServerFn } from '@tanstack/solid-start'
+import { createSignal } from 'solid-js'
+
+const getEnv = createIsomorphicFn()
+ .server(() => 'server')
+ .client(() => 'client')
+
+const getServerEnv = createServerFn().handler(() => getEnv())
+
+const getEcho = createIsomorphicFn()
+ .server((input: string) => 'server received ' + input)
+ .client((input) => 'client received ' + input)
+
+const getServerEcho = createServerFn()
+ .validator((input: string) => input)
+ .handler(({ data }) => getEcho(data))
+
+export const Route = createFileRoute('/isomorphic-fns')({
+ component: RouteComponent,
+ loader() {
+ return {
+ envOnLoad: getEnv(),
+ }
+ },
+})
+
+function RouteComponent() {
+ const loaderData = Route.useLoaderData()
+ const [results, setResults] = createSignal>>()
+ async function handleClick() {
+ const envOnClick = getEnv()
+ const echo = getEcho('hello')
+ const [serverEnv, serverEcho] = await Promise.all([
+ getServerEnv(),
+ getServerEcho({ data: 'hello' }),
+ ])
+ setResults({ envOnClick, echo, serverEnv, serverEcho })
+ }
+
+ return (
+
+
+ Run
+
+ {!!results() && (
+
+
+ getEnv
+
+ When we called the function on the server it returned:
+
+ {JSON.stringify(results()?.serverEnv)}
+
+ When we called the function on the client it returned:
+
+ {JSON.stringify(results()?.envOnClick)}
+
+ When we called the function during SSR it returned:
+
+ {JSON.stringify(loaderData().envOnLoad)}
+
+
+
+ echo
+
+ When we called the function on the server it returned:
+
+ {JSON.stringify(results()?.serverEcho)}
+
+ When we called the function on the client it returned:
+
+ {JSON.stringify(results()?.echo)}
+
+
+ )}
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/multipart.tsx b/e2e/solid-start/server-functions/app/routes/multipart.tsx
new file mode 100644
index 0000000000..5e959da7ae
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/multipart.tsx
@@ -0,0 +1,107 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import * as Solid from 'solid-js'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const Route = createFileRoute('/multipart')({
+ component: MultipartServerFnCall,
+})
+
+const multipartFormDataServerFn = createServerFn({ method: 'POST' })
+ .validator((x: unknown) => {
+ if (!(x instanceof FormData)) {
+ throw new Error('Invalid form data')
+ }
+
+ const value = x.get('input_field')
+ const file = x.get('input_file')
+
+ if (typeof value !== 'string') {
+ throw new Error('Submitted value is not a string')
+ }
+
+ if (!(file instanceof File)) {
+ throw new Error('File is required')
+ }
+
+ return {
+ submittedValue: value,
+ file,
+ }
+ })
+ .handler(async ({ data }) => {
+ const contents = await data.file.text()
+ return {
+ value: data.submittedValue,
+ file: {
+ name: data.file.name,
+ size: data.file.size,
+ contents: contents,
+ },
+ }
+ })
+
+function MultipartServerFnCall() {
+ let formRef: HTMLFormElement | undefined
+ const [multipartResult, setMultipartResult] = Solid.createSignal({})
+
+ const handleSubmit = (e: any) => {
+ e.preventDefault()
+
+ if (!formRef) {
+ return
+ }
+
+ const formData = new FormData(formRef)
+ multipartFormDataServerFn({ data: formData }).then(setMultipartResult)
+ }
+
+ return (
+
+
Multipart Server Fn POST Call
+
+ It should return{' '}
+
+
+ {JSON.stringify({
+ value: 'test field value',
+ file: { name: 'my_file.txt', size: 9, contents: 'test data' },
+ })}
+
+
+
+
+
+
+ {JSON.stringify(multipartResult())}
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/raw-response.tsx b/e2e/solid-start/server-functions/app/routes/raw-response.tsx
new file mode 100644
index 0000000000..d0ff217944
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/raw-response.tsx
@@ -0,0 +1,46 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const Route = createFileRoute('/raw-response')({
+ component: RouteComponent,
+})
+
+const expectedValue = 'Hello from a server function!'
+export const rawResponseFn = createServerFn({ response: 'raw' }).handler(() => {
+ return new Response(expectedValue)
+})
+
+function RouteComponent() {
+ const [formDataResult, setFormDataResult] = Solid.createSignal({})
+
+ return (
+
+
Raw Response
+
+ It should return{' '}
+
+ {expectedValue}
+
+
+
+
{
+ const response = await rawResponseFn()
+ console.log('response', response)
+
+ const text = await response.text()
+ setFormDataResult(text)
+ }}
+ data-testid="button"
+ class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ Submit
+
+
+
+
{JSON.stringify(formDataResult())}
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/return-null.tsx b/e2e/solid-start/server-functions/app/routes/return-null.tsx
new file mode 100644
index 0000000000..2e789d2eef
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/return-null.tsx
@@ -0,0 +1,68 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+import * as Solid from 'solid-js'
+
+/**
+ * This checks whether the server function can
+ * return null without throwing an error or returning something else.
+ * @link https://github.com/TanStack/router/issues/2776
+ */
+
+export const Route = createFileRoute('/return-null')({
+ component: AllowServerFnReturnNull,
+})
+
+const $allow_return_null_getFn = createServerFn().handler(async () => {
+ return null
+})
+const $allow_return_null_postFn = createServerFn({ method: 'POST' }).handler(
+ async () => {
+ return null
+ },
+)
+
+function AllowServerFnReturnNull() {
+ const [getServerResult, setGetServerResult] = Solid.createSignal('-')
+ const [postServerResult, setPostServerResult] = Solid.createSignal('-')
+
+ return (
+
+
Allow ServerFn to return `null`
+
+ This component checks whether the server function can return null
+ without throwing an error.
+
+
+ It should return{' '}
+
+ {JSON.stringify(null)}
+
+
+
+ {`GET: $allow_return_null_getFn returns`}
+
+
+ {JSON.stringify(getServerResult())}
+
+
+
+ {`POST: $allow_return_null_postFn returns`}
+
+
+ {JSON.stringify(postServerResult())}
+
+
+
{
+ $allow_return_null_getFn().then(setGetServerResult)
+ $allow_return_null_postFn().then(setPostServerResult)
+ }}
+ >
+ Test Allow Server Fn Return Null
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx b/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx
new file mode 100644
index 0000000000..487d91e86d
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/serialize-form-data.tsx
@@ -0,0 +1,84 @@
+import * as Solid from 'solid-js'
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+const testValues = {
+ name: 'Sean',
+ age: 25,
+ pet1: 'dog',
+ pet2: 'cat',
+ __adder: 1,
+}
+
+export const greetUser = createServerFn()
+ .validator((data: FormData) => {
+ if (!(data instanceof FormData)) {
+ throw new Error('Invalid! FormData is required')
+ }
+ const name = data.get('name')
+ const age = data.get('age')
+ const pets = data.getAll('pet')
+
+ if (!name || !age || pets.length === 0) {
+ throw new Error('Name, age and pets are required')
+ }
+
+ return {
+ name: name.toString(),
+ age: parseInt(age.toString(), 10),
+ pets: pets.map((pet) => pet.toString()),
+ }
+ })
+ .handler(({ data: { name, age, pets } }) => {
+ return `Hello, ${name}! You are ${age + testValues.__adder} years old, and your favorite pets are ${pets.join(',')}.`
+ })
+
+export function SerializeFormDataFnCall() {
+ const [formDataResult, setFormDataResult] = Solid.createSignal({})
+
+ return (
+
+
Serialize FormData Fn POST Call
+
+ It should return{' '}
+
+
+ Hello, {testValues.name}! You are{' '}
+ {testValues.age + testValues.__adder} years old, and your favorite{' '}
+ pets are {testValues.pet1},{testValues.pet2}.
+
+
+
+
+
+
+ {JSON.stringify(formDataResult())}
+
+
+
+ )
+}
+
+export const Route = createFileRoute('/serialize-form-data')({
+ component: SerializeFormDataFnCall,
+})
diff --git a/e2e/solid-start/server-functions/app/routes/status.tsx b/e2e/solid-start/server-functions/app/routes/status.tsx
new file mode 100644
index 0000000000..22b45bb28f
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/status.tsx
@@ -0,0 +1,30 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn, useServerFn } from '@tanstack/solid-start'
+import { setResponseStatus } from '@tanstack/solid-start/server'
+
+const helloFn = createServerFn().handler(() => {
+ setResponseStatus(225, `hello`)
+ return {
+ hello: 'world',
+ }
+})
+
+export const Route = createFileRoute('/status')({
+ component: StatusComponent,
+})
+
+function StatusComponent() {
+ const hello = useServerFn(helloFn)
+
+ return (
+
+ hello()}
+ >
+ click me
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx b/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx
new file mode 100644
index 0000000000..3fb6ea356f
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/routes/submit-post-formdata.tsx
@@ -0,0 +1,60 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const Route = createFileRoute('/submit-post-formdata')({
+ component: SubmitPostFormDataFn,
+})
+
+const testValues = {
+ name: 'Sean',
+}
+
+export const greetUser = createServerFn({ method: 'POST', response: 'raw' })
+ .validator((data: FormData) => {
+ if (!(data instanceof FormData)) {
+ throw new Error('Invalid! FormData is required')
+ }
+ const name = data.get('name')
+
+ if (!name) {
+ throw new Error('Name is required')
+ }
+
+ return {
+ name: name.toString(),
+ }
+ })
+ .handler(({ data: { name } }) => {
+ return new Response(`Hello, ${name}!`)
+ })
+
+function SubmitPostFormDataFn() {
+ return (
+
+
Submit POST FormData Fn Call
+
+ It should return navigate and return{' '}
+
+
+ Hello, {testValues.name}!
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/app/ssr.tsx b/e2e/solid-start/server-functions/app/ssr.tsx
new file mode 100644
index 0000000000..6d10bea05f
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/ssr.tsx
@@ -0,0 +1,12 @@
+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/e2e/solid-start/server-functions/app/styles/app.css b/e2e/solid-start/server-functions/app/styles/app.css
new file mode 100644
index 0000000000..c53c870665
--- /dev/null
+++ b/e2e/solid-start/server-functions/app/styles/app.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ color-scheme: light dark;
+ }
+
+ * {
+ @apply border-gray-200 dark:border-gray-800;
+ }
+
+ 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/e2e/solid-start/server-functions/package.json b/e2e/solid-start/server-functions/package.json
new file mode 100644
index 0000000000..d46a4a8f85
--- /dev/null
+++ b/e2e/solid-start/server-functions/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "tanstack-solid-start-e2e-server-functions",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vinxi dev --port 3000",
+ "dev:e2e": "vinxi dev",
+ "build": "vinxi build && tsc --noEmit",
+ "start": "vinxi start",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "js-cookie": "^3.0.5",
+ "solid-js": "^1.9.5",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0",
+ "vinxi": "0.5.3",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/js-cookie": "^3.0.6",
+ "@types/node": "^22.10.2",
+ "vite-plugin-solid": "^2.11.6",
+ "autoprefixer": "^10.4.20",
+ "combinate": "^1.1.11",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/solid-start/server-functions/playwright.config.ts b/e2e/solid-start/server-functions/playwright.config.ts
new file mode 100644
index 0000000000..409765549f
--- /dev/null
+++ b/e2e/solid-start/server-functions/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+export const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-start/server-functions/postcss.config.mjs b/e2e/solid-start/server-functions/postcss.config.mjs
new file mode 100644
index 0000000000..2e7af2b7f1
--- /dev/null
+++ b/e2e/solid-start/server-functions/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-start/server-functions/public/favicon.ico b/e2e/solid-start/server-functions/public/favicon.ico
new file mode 100644
index 0000000000..1a1751676f
Binary files /dev/null and b/e2e/solid-start/server-functions/public/favicon.ico differ
diff --git a/e2e/solid-start/server-functions/public/favicon.png b/e2e/solid-start/server-functions/public/favicon.png
new file mode 100644
index 0000000000..1e77bc0609
Binary files /dev/null and b/e2e/solid-start/server-functions/public/favicon.png differ
diff --git a/e2e/solid-start/server-functions/tailwind.config.mjs b/e2e/solid-start/server-functions/tailwind.config.mjs
new file mode 100644
index 0000000000..07c3598bac
--- /dev/null
+++ b/e2e/solid-start/server-functions/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./app/**/*.{js,jsx,ts,tsx}'],
+}
diff --git a/e2e/solid-start/server-functions/tests/fixture.ts b/e2e/solid-start/server-functions/tests/fixture.ts
new file mode 100644
index 0000000000..abb7b1d564
--- /dev/null
+++ b/e2e/solid-start/server-functions/tests/fixture.ts
@@ -0,0 +1,28 @@
+import { test as base, expect } from '@playwright/test'
+
+export interface TestFixtureOptions {
+ whitelistErrors: Array
+}
+export const test = base.extend({
+ whitelistErrors: [[], { option: true }],
+ page: async ({ page, whitelistErrors }, use) => {
+ const errorMessages: Array = []
+ page.on('console', (m) => {
+ if (m.type() === 'error') {
+ const text = m.text()
+ for (const whitelistError of whitelistErrors) {
+ if (
+ (typeof whitelistError === 'string' &&
+ text.includes(whitelistError)) ||
+ (whitelistError instanceof RegExp && whitelistError.test(text))
+ ) {
+ return
+ }
+ }
+ errorMessages.push(text)
+ }
+ })
+ await use(page)
+ expect(errorMessages).toEqual([])
+ },
+})
diff --git a/e2e/solid-start/server-functions/tests/server-functions.spec.ts b/e2e/solid-start/server-functions/tests/server-functions.spec.ts
new file mode 100644
index 0000000000..e0decd0ddf
--- /dev/null
+++ b/e2e/solid-start/server-functions/tests/server-functions.spec.ts
@@ -0,0 +1,337 @@
+import * as fs from 'node:fs'
+import { expect, test } from '@playwright/test'
+import { PORT } from '../playwright.config'
+import type { Page } from '@playwright/test'
+
+test('invoking a server function with custom response status code', async ({
+ page,
+}) => {
+ await page.goto('/status')
+
+ await page.waitForLoadState('networkidle')
+
+ const requestPromise = new Promise((resolve) => {
+ page.on('response', async (response) => {
+ expect(response.status()).toBe(225)
+ expect(response.statusText()).toBe('hello')
+ expect(response.headers()['content-type']).toBe('application/json')
+ expect(await response.json()).toEqual(
+ expect.objectContaining({
+ result: { hello: 'world' },
+ context: {},
+ }),
+ )
+ resolve()
+ })
+ })
+ await page.getByTestId('invoke-server-fn').click()
+ await requestPromise
+})
+
+test('Consistent server function returns both on client and server for GET and POST calls', async ({
+ page,
+}) => {
+ await page.goto('/consistent')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-consistent-server-fns-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-consistent-server-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ // GET calls
+ await expect(page.getByTestId('cons_serverGetFn1-response')).toContainText(
+ expected,
+ )
+ await expect(page.getByTestId('cons_getFn1-response')).toContainText(expected)
+
+ // POST calls
+ await expect(page.getByTestId('cons_serverPostFn1-response')).toContainText(
+ expected,
+ )
+ await expect(page.getByTestId('cons_postFn1-response')).toContainText(
+ expected,
+ )
+})
+
+test('submitting multipart/form-data as server function input', async ({
+ page,
+}) => {
+ await page.goto('/multipart')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-multipart-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ const fileChooserPromise = page.waitForEvent('filechooser')
+ await page.getByTestId('multipart-form-file-input').click()
+ const fileChooser = await fileChooserPromise
+ await fileChooser.setFiles({
+ name: 'my_file.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from('test data', 'utf-8'),
+ })
+ await page.getByText('Submit (onClick)').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('multipart-form-response')).toContainText(
+ expected,
+ )
+})
+
+test('isomorphic functions can have different implementations on client and server', async ({
+ page,
+}) => {
+ await page.goto('/isomorphic-fns')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('test-isomorphic-results-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('server-result')).toContainText('server')
+ await expect(page.getByTestId('client-result')).toContainText('client')
+ await expect(page.getByTestId('ssr-result')).toContainText('server')
+
+ await expect(page.getByTestId('server-echo-result')).toContainText(
+ 'server received hello',
+ )
+ await expect(page.getByTestId('client-echo-result')).toContainText(
+ 'client received hello',
+ )
+})
+
+test('env-only functions can only be called on the server or client respectively', async ({
+ page,
+}) => {
+ await page.goto('/env-only')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('test-env-only-results-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('server-on-server')).toContainText(
+ 'server got: hello',
+ )
+ await expect(page.getByTestId('server-on-client')).toContainText(
+ 'serverEcho threw an error: serverOnly() functions can only be called on the server!',
+ )
+
+ await expect(page.getByTestId('client-on-server')).toContainText(
+ 'clientEcho threw an error: clientOnly() functions can only be called on the client!',
+ )
+ await expect(page.getByTestId('client-on-client')).toContainText(
+ 'client got: hello',
+ )
+})
+
+test('Server function can return null for GET and POST calls', async ({
+ page,
+}) => {
+ await page.goto('/return-null')
+
+ await page.waitForLoadState('networkidle')
+ await page.getByTestId('test-allow-server-fn-return-null-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ // GET call
+ await expect(
+ page.getByTestId('allow_return_null_getFn-response'),
+ ).toContainText(JSON.stringify(null))
+
+ // POST call
+ await expect(
+ page.getByTestId('allow_return_null_postFn-response'),
+ ).toContainText(JSON.stringify(null))
+})
+
+test('Server function can correctly send and receive FormData', async ({
+ page,
+}) => {
+ await page.goto('/serialize-form-data')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-serialize-formdata-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-serialize-formdata-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(
+ page.getByTestId('serialize-formdata-form-response'),
+ ).toContainText(expected)
+})
+
+test('server function can correctly send and receive headers', async ({
+ page,
+}) => {
+ await page.goto('/headers')
+
+ await page.waitForLoadState('networkidle')
+ // console.log(await page.getByTestId('test-headers-result').textContent())
+ await expect(page.getByTestId('test-headers-result')).toContainText(`{
+ "accept": "application/json",
+ "accept-encoding": "gzip, deflate, br, zstd",
+ "accept-language": "en-US",
+ "connection": "keep-alive",
+ "content-type": "application/json",
+ "host": "localhost:${PORT}",
+ "sec-ch-ua": "\\"Not(A:Brand\\";v=\\"99\\", \\"HeadlessChrome\\";v=\\"133\\", \\"Chromium\\";v=\\"133\\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\\"Windows\\"",
+ "sec-fetch-dest": "document",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-site": "none",
+ "sec-fetch-user": "?1",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36"
+}`)
+
+ await page.getByTestId('test-headers-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('test-headers-result')).toContainText(`{
+ "host": "localhost:${PORT}",
+ "connection": "keep-alive",
+ "sec-ch-ua-platform": "\\"Windows\\"",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
+ "accept": "application/json",
+ "sec-ch-ua": "\\"Not(A:Brand\\";v=\\"99\\", \\"HeadlessChrome\\";v=\\"133\\", \\"Chromium\\";v=\\"133\\"",
+ "content-type": "application/json",
+ "sec-ch-ua-mobile": "?0",
+ "accept-language": "en-US",
+ "sec-fetch-site": "same-origin",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-dest": "empty",
+ "referer": "http://localhost:${PORT}/headers",
+ "accept-encoding": "gzip, deflate, br, zstd"
+}`)
+})
+
+test('Direct POST submitting FormData to a Server function returns the correct message', async ({
+ page,
+}) => {
+ await page.goto('/submit-post-formdata')
+
+ await page.waitForLoadState('networkidle')
+
+ const expected =
+ (await page
+ .getByTestId('expected-submit-post-formdata-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ const result = await page.innerText('body')
+ expect(result).toBe(expected)
+})
+
+test("server function's dead code is preserved if already there", async ({
+ page,
+}) => {
+ await page.goto('/dead-code-preserve')
+
+ await page.waitForLoadState('networkidle')
+ await page.getByTestId('test-dead-code-fn-call-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('dead-code-fn-call-response')).toContainText(
+ '1',
+ )
+
+ await fs.promises.rm('count-effect.txt')
+})
+
+test.describe('server function sets cookies', () => {
+ async function runCookieTest(page: Page, expectedCookieValue: string) {
+ for (let i = 1; i <= 4; i++) {
+ const key = `cookie-${i}-${expectedCookieValue}`
+
+ const actualValue = await page.getByTestId(key).textContent()
+ expect(actualValue).toBe(expectedCookieValue)
+ }
+ }
+ test('SSR', async ({ page }) => {
+ const expectedCookieValue = `SSR-${Date.now()}`
+ await page.goto(`/cookies/set?value=${expectedCookieValue}`)
+ await runCookieTest(page, expectedCookieValue)
+ })
+
+ test('client side navigation', async ({ page }) => {
+ const expectedCookieValue = `CLIENT-${Date.now()}`
+ await page.goto(`/cookies?value=${expectedCookieValue}`)
+ await page.getByTestId('link-to-set').click()
+ await runCookieTest(page, expectedCookieValue)
+ })
+})
+
+test.describe('aborting a server function call', () => {
+ test('without aborting', async ({ page }) => {
+ await page.goto('/abort-signal')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('run-without-abort-btn').click()
+ await page.waitForLoadState('networkidle')
+ await page.waitForSelector(
+ '[data-testid="result"]:has-text("server function result")',
+ )
+ await page.waitForSelector(
+ '[data-testid="errorMessage"]:has-text("$undefined")',
+ )
+
+ const result = (await page.getByTestId('result').textContent()) || ''
+ expect(result).toBe('server function result')
+
+ const errorMessage =
+ (await page.getByTestId('errorMessage').textContent()) || ''
+ expect(errorMessage).toBe('$undefined')
+ })
+
+ test('aborting', async ({ page }) => {
+ await page.goto('/abort-signal')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('run-with-abort-btn').click()
+ await page.waitForLoadState('networkidle')
+ await page.waitForSelector('[data-testid="result"]:has-text("$undefined")')
+ await page.waitForSelector(
+ '[data-testid="errorMessage"]:has-text("aborted")',
+ )
+
+ const result = (await page.getByTestId('result').textContent()) || ''
+ expect(result).toBe('$undefined')
+
+ const errorMessage =
+ (await page.getByTestId('errorMessage').textContent()) || ''
+ expect(errorMessage).toContain('abort')
+ })
+})
+
+test('raw response', async ({ page }) => {
+ await page.goto('/raw-response')
+
+ await page.waitForLoadState('networkidle')
+
+ const expectedValue = (await page.getByTestId('expected').textContent()) || ''
+ expect(expectedValue).not.toBe('')
+
+ await page.getByTestId('button').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('response')).toContainText(expectedValue)
+})
diff --git a/e2e/solid-start/server-functions/tsconfig.json b/e2e/solid-start/server-functions/tsconfig.json
new file mode 100644
index 0000000000..7340ddd786
--- /dev/null
+++ b/e2e/solid-start/server-functions/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "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": {
+ "~/*": ["app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/e2e/solid-start/website/.gitignore b/e2e/solid-start/website/.gitignore
new file mode 100644
index 0000000000..be342025da
--- /dev/null
+++ b/e2e/solid-start/website/.gitignore
@@ -0,0 +1,22 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+.vinxi
+
+/build/
+/api/
+/server/build
+/public/build
+.vinxi
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-start/website/.prettierignore b/e2e/solid-start/website/.prettierignore
new file mode 100644
index 0000000000..2be5eaa6ec
--- /dev/null
+++ b/e2e/solid-start/website/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/solid-start/website/app.config.ts b/e2e/solid-start/website/app.config.ts
new file mode 100644
index 0000000000..5c531d7e3d
--- /dev/null
+++ b/e2e/solid-start/website/app.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from '@tanstack/solid-start/config'
+import tsConfigPaths from 'vite-tsconfig-paths'
+
+export default defineConfig({
+ vite: {
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ ],
+ },
+})
diff --git a/e2e/solid-start/website/app/client.tsx b/e2e/solid-start/website/app/client.tsx
new file mode 100644
index 0000000000..ba0f02fac0
--- /dev/null
+++ b/e2e/solid-start/website/app/client.tsx
@@ -0,0 +1,8 @@
+///
+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/website/app/components/DefaultCatchBoundary.tsx b/e2e/solid-start/website/app/components/DefaultCatchBoundary.tsx
new file mode 100644
index 0000000000..32aed20e67
--- /dev/null
+++ b/e2e/solid-start/website/app/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+ {
+ router.invalidate()
+ }}
+ class={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot() ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/solid-start/website/app/components/NotFound.tsx b/e2e/solid-start/website/app/components/NotFound.tsx
new file mode 100644
index 0000000000..ca4c1960fa
--- /dev/null
+++ b/e2e/solid-start/website/app/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/solid-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+ window.history.back()}
+ class="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/solid-start/website/app/routeTree.gen.ts b/e2e/solid-start/website/app/routeTree.gen.ts
new file mode 100644
index 0000000000..32515b924b
--- /dev/null
+++ b/e2e/solid-start/website/app/routeTree.gen.ts
@@ -0,0 +1,370 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as LibraryImport } from './routes/_library'
+import { Route as LibraryIndexImport } from './routes/_library.index'
+import { Route as ProjectIndexImport } from './routes/$project.index'
+import { Route as LibraryProjectImport } from './routes/_library.$project'
+import { Route as LibraryProjectVersionIndexImport } from './routes/_library.$project.$version.index'
+import { Route as ProjectVersionDocsIndexImport } from './routes/$project.$version.docs.index'
+import { Route as ProjectVersionDocsFrameworkFrameworkImport } from './routes/$project.$version.docs.framework.$framework'
+import { Route as ProjectVersionDocsFrameworkFrameworkIndexImport } from './routes/$project.$version.docs.framework.$framework.index'
+import { Route as ProjectVersionDocsFrameworkFrameworkSplatImport } from './routes/$project.$version.docs.framework.$framework.$'
+import { Route as ProjectVersionDocsFrameworkFrameworkExamplesSplatImport } from './routes/$project.$version.docs.framework.$framework.examples.$'
+
+// Create/Update Routes
+
+const LibraryRoute = LibraryImport.update({
+ id: '/_library',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LibraryIndexRoute = LibraryIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => LibraryRoute,
+} as any)
+
+const ProjectIndexRoute = ProjectIndexImport.update({
+ id: '/$project/',
+ path: '/$project/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const LibraryProjectRoute = LibraryProjectImport.update({
+ id: '/$project',
+ path: '/$project',
+ getParentRoute: () => LibraryRoute,
+} as any)
+
+const LibraryProjectVersionIndexRoute = LibraryProjectVersionIndexImport.update(
+ {
+ id: '/$version/',
+ path: '/$version/',
+ getParentRoute: () => LibraryProjectRoute,
+ } as any,
+)
+
+const ProjectVersionDocsIndexRoute = ProjectVersionDocsIndexImport.update({
+ id: '/$project/$version/docs/',
+ path: '/$project/$version/docs/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const ProjectVersionDocsFrameworkFrameworkRoute =
+ ProjectVersionDocsFrameworkFrameworkImport.update({
+ id: '/$project/$version/docs/framework/$framework',
+ path: '/$project/$version/docs/framework/$framework',
+ getParentRoute: () => rootRoute,
+ } as any)
+
+const ProjectVersionDocsFrameworkFrameworkIndexRoute =
+ ProjectVersionDocsFrameworkFrameworkIndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => ProjectVersionDocsFrameworkFrameworkRoute,
+ } as any)
+
+const ProjectVersionDocsFrameworkFrameworkSplatRoute =
+ ProjectVersionDocsFrameworkFrameworkSplatImport.update({
+ id: '/$',
+ path: '/$',
+ getParentRoute: () => ProjectVersionDocsFrameworkFrameworkRoute,
+ } as any)
+
+const ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute =
+ ProjectVersionDocsFrameworkFrameworkExamplesSplatImport.update({
+ id: '/examples/$',
+ path: '/examples/$',
+ getParentRoute: () => ProjectVersionDocsFrameworkFrameworkRoute,
+ } as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/_library': {
+ id: '/_library'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LibraryImport
+ parentRoute: typeof rootRoute
+ }
+ '/_library/$project': {
+ id: '/_library/$project'
+ path: '/$project'
+ fullPath: '/$project'
+ preLoaderRoute: typeof LibraryProjectImport
+ parentRoute: typeof LibraryImport
+ }
+ '/$project/': {
+ id: '/$project/'
+ path: '/$project'
+ fullPath: '/$project'
+ preLoaderRoute: typeof ProjectIndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/_library/': {
+ id: '/_library/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof LibraryIndexImport
+ parentRoute: typeof LibraryImport
+ }
+ '/$project/$version/docs/': {
+ id: '/$project/$version/docs/'
+ path: '/$project/$version/docs'
+ fullPath: '/$project/$version/docs'
+ preLoaderRoute: typeof ProjectVersionDocsIndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/_library/$project/$version/': {
+ id: '/_library/$project/$version/'
+ path: '/$version'
+ fullPath: '/$project/$version'
+ preLoaderRoute: typeof LibraryProjectVersionIndexImport
+ parentRoute: typeof LibraryProjectImport
+ }
+ '/$project/$version/docs/framework/$framework': {
+ id: '/$project/$version/docs/framework/$framework'
+ path: '/$project/$version/docs/framework/$framework'
+ fullPath: '/$project/$version/docs/framework/$framework'
+ preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkImport
+ parentRoute: typeof rootRoute
+ }
+ '/$project/$version/docs/framework/$framework/$': {
+ id: '/$project/$version/docs/framework/$framework/$'
+ path: '/$'
+ fullPath: '/$project/$version/docs/framework/$framework/$'
+ preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkSplatImport
+ parentRoute: typeof ProjectVersionDocsFrameworkFrameworkImport
+ }
+ '/$project/$version/docs/framework/$framework/': {
+ id: '/$project/$version/docs/framework/$framework/'
+ path: '/'
+ fullPath: '/$project/$version/docs/framework/$framework/'
+ preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkIndexImport
+ parentRoute: typeof ProjectVersionDocsFrameworkFrameworkImport
+ }
+ '/$project/$version/docs/framework/$framework/examples/$': {
+ id: '/$project/$version/docs/framework/$framework/examples/$'
+ path: '/examples/$'
+ fullPath: '/$project/$version/docs/framework/$framework/examples/$'
+ preLoaderRoute: typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatImport
+ parentRoute: typeof ProjectVersionDocsFrameworkFrameworkImport
+ }
+ }
+}
+
+// Create and export the route tree
+
+interface LibraryProjectRouteChildren {
+ LibraryProjectVersionIndexRoute: typeof LibraryProjectVersionIndexRoute
+}
+
+const LibraryProjectRouteChildren: LibraryProjectRouteChildren = {
+ LibraryProjectVersionIndexRoute: LibraryProjectVersionIndexRoute,
+}
+
+const LibraryProjectRouteWithChildren = LibraryProjectRoute._addFileChildren(
+ LibraryProjectRouteChildren,
+)
+
+interface LibraryRouteChildren {
+ LibraryProjectRoute: typeof LibraryProjectRouteWithChildren
+ LibraryIndexRoute: typeof LibraryIndexRoute
+}
+
+const LibraryRouteChildren: LibraryRouteChildren = {
+ LibraryProjectRoute: LibraryProjectRouteWithChildren,
+ LibraryIndexRoute: LibraryIndexRoute,
+}
+
+const LibraryRouteWithChildren =
+ LibraryRoute._addFileChildren(LibraryRouteChildren)
+
+interface ProjectVersionDocsFrameworkFrameworkRouteChildren {
+ ProjectVersionDocsFrameworkFrameworkSplatRoute: typeof ProjectVersionDocsFrameworkFrameworkSplatRoute
+ ProjectVersionDocsFrameworkFrameworkIndexRoute: typeof ProjectVersionDocsFrameworkFrameworkIndexRoute
+ ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute: typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute
+}
+
+const ProjectVersionDocsFrameworkFrameworkRouteChildren: ProjectVersionDocsFrameworkFrameworkRouteChildren =
+ {
+ ProjectVersionDocsFrameworkFrameworkSplatRoute:
+ ProjectVersionDocsFrameworkFrameworkSplatRoute,
+ ProjectVersionDocsFrameworkFrameworkIndexRoute:
+ ProjectVersionDocsFrameworkFrameworkIndexRoute,
+ ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute:
+ ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute,
+ }
+
+const ProjectVersionDocsFrameworkFrameworkRouteWithChildren =
+ ProjectVersionDocsFrameworkFrameworkRoute._addFileChildren(
+ ProjectVersionDocsFrameworkFrameworkRouteChildren,
+ )
+
+export interface FileRoutesByFullPath {
+ '': typeof LibraryRouteWithChildren
+ '/$project': typeof ProjectIndexRoute
+ '/': typeof LibraryIndexRoute
+ '/$project/$version/docs': typeof ProjectVersionDocsIndexRoute
+ '/$project/$version': typeof LibraryProjectVersionIndexRoute
+ '/$project/$version/docs/framework/$framework': typeof ProjectVersionDocsFrameworkFrameworkRouteWithChildren
+ '/$project/$version/docs/framework/$framework/$': typeof ProjectVersionDocsFrameworkFrameworkSplatRoute
+ '/$project/$version/docs/framework/$framework/': typeof ProjectVersionDocsFrameworkFrameworkIndexRoute
+ '/$project/$version/docs/framework/$framework/examples/$': typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute
+}
+
+export interface FileRoutesByTo {
+ '/$project': typeof ProjectIndexRoute
+ '/': typeof LibraryIndexRoute
+ '/$project/$version/docs': typeof ProjectVersionDocsIndexRoute
+ '/$project/$version': typeof LibraryProjectVersionIndexRoute
+ '/$project/$version/docs/framework/$framework/$': typeof ProjectVersionDocsFrameworkFrameworkSplatRoute
+ '/$project/$version/docs/framework/$framework': typeof ProjectVersionDocsFrameworkFrameworkIndexRoute
+ '/$project/$version/docs/framework/$framework/examples/$': typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/_library': typeof LibraryRouteWithChildren
+ '/_library/$project': typeof LibraryProjectRouteWithChildren
+ '/$project/': typeof ProjectIndexRoute
+ '/_library/': typeof LibraryIndexRoute
+ '/$project/$version/docs/': typeof ProjectVersionDocsIndexRoute
+ '/_library/$project/$version/': typeof LibraryProjectVersionIndexRoute
+ '/$project/$version/docs/framework/$framework': typeof ProjectVersionDocsFrameworkFrameworkRouteWithChildren
+ '/$project/$version/docs/framework/$framework/$': typeof ProjectVersionDocsFrameworkFrameworkSplatRoute
+ '/$project/$version/docs/framework/$framework/': typeof ProjectVersionDocsFrameworkFrameworkIndexRoute
+ '/$project/$version/docs/framework/$framework/examples/$': typeof ProjectVersionDocsFrameworkFrameworkExamplesSplatRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | ''
+ | '/$project'
+ | '/'
+ | '/$project/$version/docs'
+ | '/$project/$version'
+ | '/$project/$version/docs/framework/$framework'
+ | '/$project/$version/docs/framework/$framework/$'
+ | '/$project/$version/docs/framework/$framework/'
+ | '/$project/$version/docs/framework/$framework/examples/$'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/$project'
+ | '/'
+ | '/$project/$version/docs'
+ | '/$project/$version'
+ | '/$project/$version/docs/framework/$framework/$'
+ | '/$project/$version/docs/framework/$framework'
+ | '/$project/$version/docs/framework/$framework/examples/$'
+ id:
+ | '__root__'
+ | '/_library'
+ | '/_library/$project'
+ | '/$project/'
+ | '/_library/'
+ | '/$project/$version/docs/'
+ | '/_library/$project/$version/'
+ | '/$project/$version/docs/framework/$framework'
+ | '/$project/$version/docs/framework/$framework/$'
+ | '/$project/$version/docs/framework/$framework/'
+ | '/$project/$version/docs/framework/$framework/examples/$'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ LibraryRoute: typeof LibraryRouteWithChildren
+ ProjectIndexRoute: typeof ProjectIndexRoute
+ ProjectVersionDocsIndexRoute: typeof ProjectVersionDocsIndexRoute
+ ProjectVersionDocsFrameworkFrameworkRoute: typeof ProjectVersionDocsFrameworkFrameworkRouteWithChildren
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ LibraryRoute: LibraryRouteWithChildren,
+ ProjectIndexRoute: ProjectIndexRoute,
+ ProjectVersionDocsIndexRoute: ProjectVersionDocsIndexRoute,
+ ProjectVersionDocsFrameworkFrameworkRoute:
+ ProjectVersionDocsFrameworkFrameworkRouteWithChildren,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/_library",
+ "/$project/",
+ "/$project/$version/docs/",
+ "/$project/$version/docs/framework/$framework"
+ ]
+ },
+ "/_library": {
+ "filePath": "_library.tsx",
+ "children": [
+ "/_library/$project",
+ "/_library/"
+ ]
+ },
+ "/_library/$project": {
+ "filePath": "_library.$project.tsx",
+ "parent": "/_library",
+ "children": [
+ "/_library/$project/$version/"
+ ]
+ },
+ "/$project/": {
+ "filePath": "$project.index.tsx"
+ },
+ "/_library/": {
+ "filePath": "_library.index.tsx",
+ "parent": "/_library"
+ },
+ "/$project/$version/docs/": {
+ "filePath": "$project.$version.docs.index.tsx"
+ },
+ "/_library/$project/$version/": {
+ "filePath": "_library.$project.$version.index.tsx",
+ "parent": "/_library/$project"
+ },
+ "/$project/$version/docs/framework/$framework": {
+ "filePath": "$project.$version.docs.framework.$framework.tsx",
+ "children": [
+ "/$project/$version/docs/framework/$framework/$",
+ "/$project/$version/docs/framework/$framework/",
+ "/$project/$version/docs/framework/$framework/examples/$"
+ ]
+ },
+ "/$project/$version/docs/framework/$framework/$": {
+ "filePath": "$project.$version.docs.framework.$framework.$.tsx",
+ "parent": "/$project/$version/docs/framework/$framework"
+ },
+ "/$project/$version/docs/framework/$framework/": {
+ "filePath": "$project.$version.docs.framework.$framework.index.tsx",
+ "parent": "/$project/$version/docs/framework/$framework"
+ },
+ "/$project/$version/docs/framework/$framework/examples/$": {
+ "filePath": "$project.$version.docs.framework.$framework.examples.$.tsx",
+ "parent": "/$project/$version/docs/framework/$framework"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/solid-start/website/app/router.tsx b/e2e/solid-start/website/app/router.tsx
new file mode 100644
index 0000000000..cf079db611
--- /dev/null
+++ b/e2e/solid-start/website/app/router.tsx
@@ -0,0 +1,23 @@
+import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function createRouter() {
+ const router = createTanStackRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: 'intent',
+ defaultStaleTime: 5000,
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ })
+
+ return router
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx
new file mode 100644
index 0000000000..1df208a1c4
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.$.tsx
@@ -0,0 +1,44 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+import { NotFound } from '~/components/NotFound'
+import { getDocument } from '~/server/document'
+import { capitalize, seo } from '~/utils/seo'
+
+export const Route = createFileRoute(
+ '/$project/$version/docs/framework/$framework/$',
+)({
+ loader: ({ params: { _splat } }) =>
+ getDocument({
+ data: _splat!,
+ }),
+ head: ({ loaderData, params }) => ({
+ meta: seo({
+ title: `${loaderData?.title || 'Project'} | TanStack ${capitalize(params.project)} ${capitalize(params.framework)}`,
+ }),
+ }),
+ errorComponent: PostErrorComponent,
+ component: Page,
+ notFoundComponent: () => {
+ return Document not found
+ },
+})
+
+function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function Page() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ {post().title}
+
+
{post().content}
+
+ )
+}
diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx
new file mode 100644
index 0000000000..53e0c46c1b
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.examples.$.tsx
@@ -0,0 +1,32 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { NotFound } from '~/components/NotFound'
+import { capitalize, seo } from '~/utils/seo'
+
+export const Route = createFileRoute(
+ '/$project/$version/docs/framework/$framework/examples/$',
+)({
+ head: ({ params }) => ({
+ meta: seo({
+ title: `${capitalize(params._splat || '')} Example | TanStack ${capitalize(params.project)} ${capitalize(params.framework)}`,
+ }),
+ }),
+ component: Page,
+ notFoundComponent: () => {
+ return Example not found
+ },
+})
+
+function Page() {
+ const params = Route.useParams()
+
+ return (
+
+
+ {params()._splat} example
+
+
+ )
+}
diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx
new file mode 100644
index 0000000000..3f6e4af4ef
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.index.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute(
+ '/$project/$version/docs/framework/$framework/',
+)({
+ loader: () => {
+ throw redirect({
+ from: '/$project/$version/docs/framework/$framework/',
+ to: '/$project/$version/docs/framework/$framework/$',
+ params: {
+ _splat: 'overview',
+ },
+ })
+ },
+})
diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx
new file mode 100644
index 0000000000..62d59befae
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/$project.$version.docs.framework.$framework.tsx
@@ -0,0 +1,123 @@
+import {
+ Link,
+ Outlet,
+ createFileRoute,
+ useLocation,
+} from '@tanstack/solid-router'
+import { getDocumentHeads } from '~/server/document'
+import { getProject } from '~/server/projects'
+
+export const Route = createFileRoute(
+ '/$project/$version/docs/framework/$framework',
+)({
+ loader: async ({ params: { project } }) => {
+ const library = await getProject({ data: project })
+ const documents = await getDocumentHeads()
+ return {
+ library,
+ documents,
+ }
+ },
+ component: Page,
+})
+
+function Page() {
+ const project = Route.useLoaderData({ select: (s) => s.library })
+ const documents = Route.useLoaderData({ select: (s) => s.documents })
+ const pathname = useLocation({ select: (s) => s.pathname })
+
+ return (
+
+
+
+
+ Home
+
+
+
+
Version
+
+ {project().versions.map((version) => (
+
+
+ {version}
+
+
+ ))}
+
+
+
+
Framework
+
+ {project().frameworks.map((framework) => (
+
+
+ {framework}
+
+
+ ))}
+
+
+
+
Content
+
+ {documents().map((doc) => (
+
+
+ {doc.title}
+
+
+ ))}
+
+
+
+
Examples
+
+ {project().examples.map((example) => (
+
+
+ {example}
+
+
+ ))}
+
+
+
+
+
+ {pathname()}
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx b/e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx
new file mode 100644
index 0000000000..ff049e373c
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/$project.$version.docs.index.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/$project/$version/docs/')({
+ loader: () => {
+ throw redirect({
+ from: '/$project/$version/docs',
+ to: '/$project/$version/docs/framework/$framework/$',
+ params: {
+ framework: 'solid',
+ _splat: 'overview',
+ },
+ })
+ },
+})
diff --git a/e2e/solid-start/website/app/routes/$project.index.tsx b/e2e/solid-start/website/app/routes/$project.index.tsx
new file mode 100644
index 0000000000..6e250ac011
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/$project.index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/$project/')({
+ loader: ({ params }) => {
+ throw redirect({
+ to: '/$project/$version',
+ params: {
+ project: params.project,
+ version: 'latest',
+ },
+ })
+ },
+})
diff --git a/e2e/solid-start/website/app/routes/__root.tsx b/e2e/solid-start/website/app/routes/__root.tsx
new file mode 100644
index 0000000000..8c3ced26f1
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/__root.tsx
@@ -0,0 +1,57 @@
+import { Outlet, createRootRoute } from '@tanstack/solid-router'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+import { seo } from '~/utils/seo'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charset: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ...seo({
+ title: 'TanStack Website',
+ description: `TanStack projects are type-safe!!!`,
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ {
+ rel: 'apple-touch-icon',
+ sizes: '180x180',
+ href: '/apple-touch-icon.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '32x32',
+ href: '/favicon-32x32.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '16x16',
+ href: '/favicon-16x16.png',
+ },
+ { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ }),
+ errorComponent: (props) => {
+ return {props.error.stack}
+ },
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx b/e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx
new file mode 100644
index 0000000000..201e85f8e9
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/_library.$project.$version.index.tsx
@@ -0,0 +1,23 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_library/$project/$version/')({
+ component: Page,
+})
+
+function Page() {
+ const params = Route.useParams()
+
+ return (
+
+
+ {params().project} landing page
+
+
version: {params().version}
+
+
+ Get started with our documentation.
+
+
+
+ )
+}
diff --git a/e2e/solid-start/website/app/routes/_library.$project.tsx b/e2e/solid-start/website/app/routes/_library.$project.tsx
new file mode 100644
index 0000000000..6fdcb8e530
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/_library.$project.tsx
@@ -0,0 +1,15 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+import { getProject } from '~/server/projects'
+import { seo } from '~/utils/seo'
+
+export const Route = createFileRoute('/_library/$project')({
+ loader: ({ params: { project } }) => getProject({ data: project }),
+ head: ({ loaderData }) => ({
+ meta: seo({ title: `TanStack ${loaderData?.name || 'Project'}` }),
+ }),
+ component: () => (
+
+
+
+ ),
+})
diff --git a/e2e/solid-start/website/app/routes/_library.index.tsx b/e2e/solid-start/website/app/routes/_library.index.tsx
new file mode 100644
index 0000000000..25bf776480
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/_library.index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_library/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Website Landing Page
+
+ )
+}
diff --git a/e2e/solid-start/website/app/routes/_library.tsx b/e2e/solid-start/website/app/routes/_library.tsx
new file mode 100644
index 0000000000..c25ae5cfe1
--- /dev/null
+++ b/e2e/solid-start/website/app/routes/_library.tsx
@@ -0,0 +1,62 @@
+import {
+ Link,
+ Outlet,
+ createFileRoute,
+ useLocation,
+} from '@tanstack/solid-router'
+import { getProjects } from '~/server/projects'
+
+export const Route = createFileRoute('/_library')({
+ loader: async () => {
+ const projects = await getProjects()
+ return {
+ libraries: projects,
+ }
+ },
+ component: Layout,
+})
+
+function Layout() {
+ const loaderData = Route.useLoaderData()
+ const pathname = useLocation({ select: (s) => s.pathname })
+ return (
+
+
+
+
+ Home
+
+
+
+
Libraries
+
+ {loaderData().libraries.map((library) => (
+
+
+ {library}
+
+
+ ))}
+
+
+
+
+
+ {pathname()}
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/website/app/server/document.tsx b/e2e/solid-start/website/app/server/document.tsx
new file mode 100644
index 0000000000..0090c3b1b1
--- /dev/null
+++ b/e2e/solid-start/website/app/server/document.tsx
@@ -0,0 +1,55 @@
+import { notFound } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+const documents: Array<{ id: string; title: string; content: string }> = [
+ {
+ id: 'overview',
+ title: 'Overview',
+ content: 'This is the content of the overview document',
+ },
+ {
+ id: 'getting-started',
+ title: 'Getting Started',
+ content: 'To get started, you need to do the following...',
+ },
+ {
+ id: 'installation',
+ title: 'Installation',
+ content: 'To install this package, run the following command...',
+ },
+ {
+ id: 'ref/useQueryFunction',
+ title: 'useQuery Reference',
+ content: 'The useQuery function is used to...',
+ },
+ {
+ id: 'ref/useMutationFunction',
+ title: 'useMutation Reference',
+ content: 'The useMutation function is used to...',
+ },
+]
+
+export const getDocumentHeads = createServerFn({ method: 'GET' }).handler(
+ async () => {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+
+ return documents.map(({ id, title }) => ({
+ id,
+ title,
+ }))
+ },
+)
+
+export const getDocument = createServerFn({ method: 'GET' })
+ .validator((id: string) => id)
+ .handler(async ({ data: id }) => {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+
+ const document = documents.find((doc) => doc.id === id)
+
+ if (!document) {
+ throw notFound()
+ }
+
+ return document
+ })
diff --git a/e2e/solid-start/website/app/server/projects.tsx b/e2e/solid-start/website/app/server/projects.tsx
new file mode 100644
index 0000000000..a66de2b2ca
--- /dev/null
+++ b/e2e/solid-start/website/app/server/projects.tsx
@@ -0,0 +1,33 @@
+import { createServerFn } from '@tanstack/solid-start'
+import { notFound } from '@tanstack/solid-router'
+import { capitalize } from '~/utils/seo'
+
+const projects = ['router', 'table', 'query', 'form', 'ranger']
+
+export const getProjects = createServerFn({ method: 'GET' }).handler(
+ async () => {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+
+ return projects
+ },
+)
+
+export const getProject = createServerFn({ method: 'GET' })
+ .validator((project: string) => project)
+ .handler(async (ctx) => {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+
+ const selectedProject = projects.find((p) => p === ctx.data.toLowerCase())
+
+ if (!selectedProject) {
+ throw notFound()
+ }
+
+ return {
+ id: selectedProject,
+ name: capitalize(selectedProject),
+ versions: ['latest', 'v2', 'v1'],
+ frameworks: ['solid', 'react', 'vue', 'solidjs', 'svelte'],
+ examples: ['basic', 'kitchen-sink'],
+ }
+ })
diff --git a/e2e/solid-start/website/app/ssr.tsx b/e2e/solid-start/website/app/ssr.tsx
new file mode 100644
index 0000000000..ebd14c8120
--- /dev/null
+++ b/e2e/solid-start/website/app/ssr.tsx
@@ -0,0 +1,13 @@
+///
+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/e2e/solid-start/website/app/styles/app.css b/e2e/solid-start/website/app/styles/app.css
new file mode 100644
index 0000000000..c53c870665
--- /dev/null
+++ b/e2e/solid-start/website/app/styles/app.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ color-scheme: light dark;
+ }
+
+ * {
+ @apply border-gray-200 dark:border-gray-800;
+ }
+
+ 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/e2e/solid-start/website/app/utils/seo.ts b/e2e/solid-start/website/app/utils/seo.ts
new file mode 100644
index 0000000000..82cf2aec07
--- /dev/null
+++ b/e2e/solid-start/website/app/utils/seo.ts
@@ -0,0 +1,36 @@
+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
+}
+
+export const capitalize = (str: string) =>
+ str.charAt(0).toUpperCase() + str.slice(1)
diff --git a/e2e/solid-start/website/package.json b/e2e/solid-start/website/package.json
new file mode 100644
index 0000000000..0cc2b7e73b
--- /dev/null
+++ b/e2e/solid-start/website/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "tanstack-solid-start-e2e-website",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vinxi dev --port 3000",
+ "dev:e2e": "vinxi dev",
+ "build": "vinxi build && tsc --noEmit",
+ "start": "vinxi start",
+ "test:e2e": "playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "solid-js": "^1.9.5",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0",
+ "vinxi": "0.5.3",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "vite-plugin-solid": "^2.11.6",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/solid-start/website/playwright.config.ts b/e2e/solid-start/website/playwright.config.ts
new file mode 100644
index 0000000000..bb77d0cf70
--- /dev/null
+++ b/e2e/solid-start/website/playwright.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, devices } from '@playwright/test'
+import { derivePort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = derivePort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm start --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-start/website/postcss.config.mjs b/e2e/solid-start/website/postcss.config.mjs
new file mode 100644
index 0000000000..2e7af2b7f1
--- /dev/null
+++ b/e2e/solid-start/website/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/solid-start/website/public/android-chrome-192x192.png b/e2e/solid-start/website/public/android-chrome-192x192.png
new file mode 100644
index 0000000000..09c8324f8c
Binary files /dev/null and b/e2e/solid-start/website/public/android-chrome-192x192.png differ
diff --git a/e2e/solid-start/website/public/android-chrome-512x512.png b/e2e/solid-start/website/public/android-chrome-512x512.png
new file mode 100644
index 0000000000..11d626ea3d
Binary files /dev/null and b/e2e/solid-start/website/public/android-chrome-512x512.png differ
diff --git a/e2e/solid-start/website/public/apple-touch-icon.png b/e2e/solid-start/website/public/apple-touch-icon.png
new file mode 100644
index 0000000000..5a9423cc02
Binary files /dev/null and b/e2e/solid-start/website/public/apple-touch-icon.png differ
diff --git a/e2e/solid-start/website/public/favicon-16x16.png b/e2e/solid-start/website/public/favicon-16x16.png
new file mode 100644
index 0000000000..e3389b0044
Binary files /dev/null and b/e2e/solid-start/website/public/favicon-16x16.png differ
diff --git a/e2e/solid-start/website/public/favicon-32x32.png b/e2e/solid-start/website/public/favicon-32x32.png
new file mode 100644
index 0000000000..900c77d444
Binary files /dev/null and b/e2e/solid-start/website/public/favicon-32x32.png differ
diff --git a/e2e/solid-start/website/public/favicon.ico b/e2e/solid-start/website/public/favicon.ico
new file mode 100644
index 0000000000..1a1751676f
Binary files /dev/null and b/e2e/solid-start/website/public/favicon.ico differ
diff --git a/e2e/solid-start/website/public/favicon.png b/e2e/solid-start/website/public/favicon.png
new file mode 100644
index 0000000000..1e77bc0609
Binary files /dev/null and b/e2e/solid-start/website/public/favicon.png differ
diff --git a/e2e/solid-start/website/public/site.webmanifest b/e2e/solid-start/website/public/site.webmanifest
new file mode 100644
index 0000000000..fa99de77db
--- /dev/null
+++ b/e2e/solid-start/website/public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/e2e/solid-start/website/tailwind.config.mjs b/e2e/solid-start/website/tailwind.config.mjs
new file mode 100644
index 0000000000..07c3598bac
--- /dev/null
+++ b/e2e/solid-start/website/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./app/**/*.{js,jsx,ts,tsx}'],
+}
diff --git a/e2e/solid-start/website/tests/app.spec.ts b/e2e/solid-start/website/tests/app.spec.ts
new file mode 100644
index 0000000000..7282086ad4
--- /dev/null
+++ b/e2e/solid-start/website/tests/app.spec.ts
@@ -0,0 +1,19 @@
+import { expect, test } from '@playwright/test'
+
+const routeTestId = 'selected-route-label'
+
+test('resolves to the latest version on load of a project like "/router"', async ({
+ page,
+}) => {
+ await page.goto('/router')
+
+ await expect(page.getByTestId(routeTestId)).toContainText('/router/latest')
+})
+
+test('resolves to the overview docs page', async ({ page }) => {
+ await page.goto('/router/latest/docs')
+
+ await expect(page.getByTestId(routeTestId)).toContainText(
+ '/router/latest/docs/framework/solid/overview',
+ )
+})
diff --git a/e2e/solid-start/website/tsconfig.json b/e2e/solid-start/website/tsconfig.json
new file mode 100644
index 0000000000..86fe6d2cf5
--- /dev/null
+++ b/e2e/solid-start/website/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": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/examples/solid/start-bare/app.config.ts b/examples/solid/start-bare/app.config.ts
new file mode 100644
index 0000000000..cb3e4af661
--- /dev/null
+++ b/examples/solid/start-bare/app.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from '@tanstack/solid-start/config'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import tailwindcss from '@tailwindcss/vite'
+
+export default defineConfig({
+ vite: {
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tailwindcss(),
+ ],
+ },
+})
diff --git a/examples/solid/start-bare/app/client.tsx b/examples/solid/start-bare/app/client.tsx
new file mode 100644
index 0000000000..2dcbbce594
--- /dev/null
+++ b/examples/solid/start-bare/app/client.tsx
@@ -0,0 +1,11 @@
+///
+import { StartClient } from '@tanstack/solid-start'
+
+import { hydrate } from 'solid-js/web'
+import { createRouter } from './router'
+
+const router = createRouter()
+
+const appDiv = document.getElementById('app')!
+
+hydrate(() => , appDiv)
diff --git a/examples/solid/start-bare/app/components/Counter.css b/examples/solid/start-bare/app/components/Counter.css
new file mode 100644
index 0000000000..308965720f
--- /dev/null
+++ b/examples/solid/start-bare/app/components/Counter.css
@@ -0,0 +1,21 @@
+.increment {
+ font-family: inherit;
+ font-size: inherit;
+ padding: 1em 2em;
+ color: #335d92;
+ background-color: rgba(68, 107, 158, 0.1);
+ border-radius: 2em;
+ border: 2px solid rgba(68, 107, 158, 0);
+ outline: none;
+ width: 200px;
+ font-variant-numeric: tabular-nums;
+ cursor: pointer;
+}
+
+.increment:focus {
+ border: 2px solid #335d92;
+}
+
+.increment:active {
+ background-color: rgba(68, 107, 158, 0.2);
+}
diff --git a/examples/solid/start-bare/app/components/Counter.tsx b/examples/solid/start-bare/app/components/Counter.tsx
new file mode 100644
index 0000000000..fc3e5120cd
--- /dev/null
+++ b/examples/solid/start-bare/app/components/Counter.tsx
@@ -0,0 +1,15 @@
+import { createSignal } from 'solid-js'
+import './Counter.css'
+
+export default function Counter() {
+ const [count, setCount] = createSignal(0)
+ return (
+ setCount(count() + 1)}
+ type="button"
+ >
+ Clicks: {count()}
+
+ )
+}
diff --git a/examples/solid/start-bare/app/routeTree.gen.ts b/examples/solid/start-bare/app/routeTree.gen.ts
new file mode 100644
index 0000000000..cb367c516c
--- /dev/null
+++ b/examples/solid/start-bare/app/routeTree.gen.ts
@@ -0,0 +1,111 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as AboutImport } from './routes/about'
+import { Route as IndexImport } from './routes/index'
+
+// Create/Update Routes
+
+const AboutRoute = AboutImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexImport
+ parentRoute: typeof rootRoute
+ }
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutImport
+ parentRoute: typeof rootRoute
+ }
+ }
+}
+
+// Create and export the route tree
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/about': typeof AboutRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/about': typeof AboutRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexRoute
+ '/about': typeof AboutRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/about'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/about'
+ id: '__root__' | '/' | '/about'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ AboutRoute: typeof AboutRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ AboutRoute: AboutRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/",
+ "/about"
+ ]
+ },
+ "/": {
+ "filePath": "index.tsx"
+ },
+ "/about": {
+ "filePath": "about.tsx"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/examples/solid/start-bare/app/router.tsx b/examples/solid/start-bare/app/router.tsx
new file mode 100644
index 0000000000..a8d9cb45e7
--- /dev/null
+++ b/examples/solid/start-bare/app/router.tsx
@@ -0,0 +1,20 @@
+import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function createRouter() {
+ const router = createTanStackRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultErrorComponent: (err) => {err.error.stack}
,
+ defaultNotFoundComponent: () => not found
,
+ scrollRestoration: true,
+ })
+
+ return router
+}
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/examples/solid/start-bare/app/routes/__root.tsx b/examples/solid/start-bare/app/routes/__root.tsx
new file mode 100644
index 0000000000..7ae1783e07
--- /dev/null
+++ b/examples/solid/start-bare/app/routes/__root.tsx
@@ -0,0 +1,46 @@
+import {
+ createRootRoute,
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+} from '@tanstack/solid-router'
+import appCss from '~/styles/app.css?url'
+import * as Solid from 'solid-js'
+import { Hydration, HydrationScript, NoHydration } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ links: [{ rel: 'stylesheet', href: appCss }],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+ )
+}
+
+function RootDocument({ children }: { children: Solid.JSX.Element }) {
+ return (
+
+ <>
+
+
+
+
+ Index
+ About
+
+
+
{children}
+
+
+
+ >
+
+ )
+}
diff --git a/examples/solid/start-bare/app/routes/about.tsx b/examples/solid/start-bare/app/routes/about.tsx
new file mode 100644
index 0000000000..d66c7db828
--- /dev/null
+++ b/examples/solid/start-bare/app/routes/about.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/about')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+ About
+
+ )
+}
diff --git a/examples/solid/start-bare/app/routes/index.tsx b/examples/solid/start-bare/app/routes/index.tsx
new file mode 100644
index 0000000000..6a91bd1999
--- /dev/null
+++ b/examples/solid/start-bare/app/routes/index.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import Counter from '~/components/Counter'
+export const Route = createFileRoute('/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+ Hello world!
+
+
+ )
+}
diff --git a/examples/solid/start-bare/app/ssr.tsx b/examples/solid/start-bare/app/ssr.tsx
new file mode 100644
index 0000000000..6d10bea05f
--- /dev/null
+++ b/examples/solid/start-bare/app/ssr.tsx
@@ -0,0 +1,12 @@
+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-bare/app/styles/app.css b/examples/solid/start-bare/app/styles/app.css
new file mode 100644
index 0000000000..43d97fa7a7
--- /dev/null
+++ b/examples/solid/start-bare/app/styles/app.css
@@ -0,0 +1,17 @@
+@import 'tailwindcss';
+
+body {
+ font-family:
+ Gordita, Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
+ sans-serif;
+}
+
+a {
+ margin-right: 1rem;
+}
+
+main {
+ text-align: center;
+ padding: 1em;
+ margin: 0 auto;
+}
diff --git a/examples/solid/start-bare/package.json b/examples/solid/start-bare/package.json
new file mode 100644
index 0000000000..480804cfea
--- /dev/null
+++ b/examples/solid/start-bare/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "tanstack-solid-start-example-bare",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vinxi dev",
+ "build": "vinxi build",
+ "start": "vinxi start"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "solid-js": "^1.9.5",
+ "redaxios": "^0.5.1",
+ "tailwind-merge": "^2.6.0",
+ "vinxi": "0.5.3",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.8",
+ "@types/node": "^22.10.2",
+ "vite-plugin-solid": "^2.11.2",
+ "combinate": "^1.1.11",
+ "tailwindcss": "^4.0.0",
+ "typescript": "^5.7.2",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/examples/solid/start-bare/public/favicon.ico b/examples/solid/start-bare/public/favicon.ico
new file mode 100644
index 0000000000..1a1751676f
Binary files /dev/null and b/examples/solid/start-bare/public/favicon.ico differ
diff --git a/examples/solid/start-bare/tsconfig.json b/examples/solid/start-bare/tsconfig.json
new file mode 100644
index 0000000000..73e4856648
--- /dev/null
+++ b/examples/solid/start-bare/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "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": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/packages/solid-start-api-routes/README.md b/packages/solid-start-api-routes/README.md
new file mode 100644
index 0000000000..bb009b0c87
--- /dev/null
+++ b/packages/solid-start-api-routes/README.md
@@ -0,0 +1,33 @@
+> 🤫 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/solid-start-api-routes/eslint.config.js b/packages/solid-start-api-routes/eslint.config.js
new file mode 100644
index 0000000000..931f0ec774
--- /dev/null
+++ b/packages/solid-start-api-routes/eslint.config.js
@@ -0,0 +1,31 @@
+// @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/solid-start-api-routes/package.json b/packages/solid-start-api-routes/package.json
new file mode 100644
index 0000000000..554e6e59dc
--- /dev/null
+++ b/packages/solid-start-api-routes/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "@tanstack/solid-start-api-routes",
+ "version": "1.111.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/start"
+ },
+ "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:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+ "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": "tsc",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "dependencies": {
+ "@tanstack/router-core": "workspace:^",
+ "@tanstack/solid-start-server": "workspace:^",
+ "vinxi": "0.5.3"
+ },
+ "devDependencies": {
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/packages/solid-start-api-routes/src/index.ts b/packages/solid-start-api-routes/src/index.ts
new file mode 100644
index 0000000000..46b5d3db43
--- /dev/null
+++ b/packages/solid-start-api-routes/src/index.ts
@@ -0,0 +1,369 @@
+import { eventHandler, toWebRequest } from '@tanstack/solid-start-server'
+import vinxiFileRoutes from 'vinxi/routes'
+import type { ResolveParams } from '@tanstack/router-core'
+
+export type StartAPIHandlerCallback = (ctx: {
+ request: Request
+}) => Response | Promise
+
+export type StartAPIMethodCallback = (ctx: {
+ request: Request
+ params: ResolveParams
+}) => Response | Promise
+
+const HTTP_API_METHODS = [
+ 'GET',
+ 'POST',
+ 'PUT',
+ 'PATCH',
+ 'DELETE',
+ 'OPTIONS',
+ 'HEAD',
+] as const
+export type HTTP_API_METHOD = (typeof HTTP_API_METHODS)[number]
+
+/**
+ *
+ * @param cb The callback function that will be called when the API handler is invoked
+ * @returns The response from the callback function
+ */
+export function createStartAPIHandler(cb: StartAPIHandlerCallback) {
+ return eventHandler(async (event) => {
+ const request = toWebRequest(event)!
+ const res = await cb({ request })
+ return res
+ })
+}
+
+type APIRoute = {
+ path: TPath
+ methods: Partial>>
+}
+
+type CreateAPIRouteFn = (
+ methods: Partial>>,
+) => APIRoute
+
+type CreateAPIRoute = (
+ path: TPath,
+) => CreateAPIRouteFn
+
+type APIRouteReturnType = ReturnType>
+
+/**
+ * This function is used to create an API route that will be listening on a specific path when you are not using the file-based routes.
+ *
+ * @param path The path that the API route will be listening on. You need to make sure that this is a valid TanStack Router path in order for the route to be matched. This means that you can use the following syntax:
+ * /api/foo/$bar/name/$
+ * - The `$bar` is a parameter that will be extracted from the URL and passed to the handler
+ * - The `$` is a wildcard that will match any number of segments in the URL
+ * @returns A function that takes the methods that the route will be listening on and returns the API route object
+ */
+export const createAPIRoute: CreateAPIRoute = (path) => (methods) => ({
+ path,
+ methods,
+})
+
+/**
+ * This function is used to create an API route that will be listening on a specific path when you are using the file-based routes.
+ *
+ * @param filePath The path that the API file route will be listening on. This filePath should automatically be generated by the TSR plugin and should be a valid TanStack Router path
+ * @returns A function that takes the methods that the route will be listening on and returns the API route object
+ */
+export const createAPIFileRoute: CreateAPIRoute = (filePath) => (methods) => ({
+ path: filePath,
+ methods,
+})
+
+/**
+ * This function takes a URL object and a list of routes and finds the route that matches the URL.
+ *
+ * @param url URL object
+ * @param entryRoutes List of routes entries in the TSR format to find the current match by the URL
+ * @returns Returns the route that matches the URL or undefined if no route matches
+ */
+function findRoute(
+ url: URL,
+ entryRoutes: Array<{ routePath: string; payload: TPayload }>,
+):
+ | {
+ routePath: string
+ params: Record
+ payload: TPayload
+ }
+ | undefined {
+ const urlSegments = url.pathname.split('/').filter(Boolean)
+
+ const routes = entryRoutes
+ .sort((a, b) => {
+ const aParts = a.routePath.split('/').filter(Boolean)
+ const bParts = b.routePath.split('/').filter(Boolean)
+
+ return bParts.length - aParts.length
+ })
+ .filter((r) => {
+ const routeSegments = r.routePath.split('/').filter(Boolean)
+ return urlSegments.length >= routeSegments.length
+ })
+
+ for (const route of routes) {
+ const routeSegments = route.routePath.split('/').filter(Boolean)
+ const params: Record = {}
+ let matches = true
+ for (let i = 0; i < routeSegments.length; i++) {
+ const routeSegment = routeSegments[i] as string
+ const urlSegment = urlSegments[i] as string
+ if (routeSegment.startsWith('$')) {
+ if (routeSegment === '$') {
+ const wildcardValue = urlSegments.slice(i).join('/')
+ if (wildcardValue !== '') {
+ params['*'] = wildcardValue
+ params['_splat'] = wildcardValue
+ } else {
+ matches = false
+ break
+ }
+ } else {
+ const paramName = routeSegment.slice(1)
+ params[paramName] = urlSegment
+ }
+ } else if (routeSegment !== urlSegment) {
+ matches = false
+ break
+ }
+ }
+ if (matches) {
+ return { routePath: route.routePath, params, payload: route.payload }
+ }
+ }
+
+ return undefined
+}
+
+/**
+ * You should only be using this function if you are not using the file-based routes.
+ *
+ *
+ * @param opts - A map of TSR routes with the values being the route handlers
+ * @returns The handler for the incoming request
+ *
+ * @example
+ * ```ts
+ * // app/foo.ts
+ * import { createAPIRoute } from '@tanstack/start-api-routes'
+ * const fooBarRoute = createAPIRoute('/api/foo/$bar')({
+ * GET: ({ params }) => {
+ * return new Response(JSON.stringify({ params }))
+ * }
+ * })
+ *
+ * // app/api.ts
+ * import {
+ * createStartAPIHandler,
+ * defaultAPIRoutesHandler
+ * } from '@tanstack/start-api-routes'
+ *
+ * export default createStartAPIHandler(
+ * defaultAPIRoutesHandler({
+ * '/api/foo/$bar': fooBarRoute
+ * })
+ * )
+ * ```
+ */
+export const defaultAPIRoutesHandler: (opts: {
+ routes: { [TPath in string]: APIRoute }
+}) => StartAPIHandlerCallback = (opts) => {
+ return async ({ request }) => {
+ if (!HTTP_API_METHODS.includes(request.method as HTTP_API_METHOD)) {
+ return new Response('Method not allowed', { status: 405 })
+ }
+
+ const url = new URL(request.url, 'http://localhost:3000')
+
+ const routes = Object.entries(opts.routes).map(([routePath, route]) => ({
+ routePath,
+ payload: route,
+ }))
+
+ // Find the route that matches the request by the request URL
+ const match = findRoute(url, routes)
+
+ // If we don't have a route that could possibly handle the request, return a 404
+ if (!match) {
+ return new Response('Not found', { status: 404 })
+ }
+
+ // If the route path doesn't match the payload path, return a 404
+ if (match.routePath !== match.payload.path) {
+ console.error(
+ `Route path mismatch: ${match.routePath} !== ${match.payload.path}. Please make sure that the route path in \`createAPIRoute\` matches the path in the handler map in \`defaultAPIRoutesHandler\``,
+ )
+ return new Response('Not found', { status: 404 })
+ }
+
+ const method = request.method as HTTP_API_METHOD
+
+ // Get the handler for the request method based on the Request Method
+ const handler = match.payload.methods[method]
+
+ // If the handler is not defined, return a 405
+ if (!handler) {
+ return new Response('Method not allowed', { status: 405 })
+ }
+
+ return await handler({ request, params: match.params })
+ }
+}
+
+interface CustomizedVinxiFileRoute {
+ path: string // this path adheres to the h3 router path format
+ filePath: string // this is the file path on the system
+ $APIRoute?: {
+ src: string // this is the path to the source file
+ import: () => Promise<{
+ APIRoute: APIRouteReturnType
+ }>
+ }
+}
+
+/**
+ * This is populated by the work done in the config file using the tsrFileRouter
+ */
+const vinxiRoutes = (
+ vinxiFileRoutes as unknown as Array
+).filter((route) => route['$APIRoute'])
+
+/**
+ * This function takes the vinxi routes and interpolates them into a format that can be worked with in the API handler
+ *
+ * @param routes The vinxi routes that have been filtered to only include those with a $APIRoute property
+ * @returns An array of objects where the path `key` is interpolated to a valid TanStack Router path, with the `payload` being the original route object
+ *
+ * @example
+ * ```
+ * const input = [
+ * {
+ * path: '/api/boo/:$id?/name/*splat',
+ * filePath: '..../code/tanstack/router/examples/react/start-basic/app/routes/api.boo.$id.name.$.tsx',
+ * '$APIRoute': [Object]
+ * }
+ * ]
+ *
+ * toTSRFileBasedRoutes(input)
+ * [
+ * {
+ * path: '/api/boo/$id/name/$',
+ * route: {
+ * path: '/api/boo/:$id?/name/*splat',
+ * filePath: '..../code/tanstack/router/examples/react/start-basic/app/routes/api.boo.$id.name.$.tsx',
+ * '$APIRoute': [Object]
+ * }
+ * }
+ * ]
+ * ```
+ */
+function toTSRFileBasedRoutes(
+ routes: Array,
+): Array<{ routePath: string; payload: CustomizedVinxiFileRoute }> {
+ const pairs: Array<{
+ routePath: string
+ payload: CustomizedVinxiFileRoute
+ }> = []
+
+ routes.forEach((route) => {
+ const parts = route.path.split('/').filter(Boolean)
+
+ const path = parts
+ .map((part) => {
+ if (part === '*splat') {
+ return '$'
+ }
+
+ if (part.startsWith(':$') && part.endsWith('?')) {
+ return part.slice(1, -1)
+ }
+
+ return part
+ })
+ .join('/')
+
+ pairs.push({ routePath: `/${path}`, payload: route })
+ })
+
+ return pairs
+}
+
+/**
+ * This function is the default handler for the API routes when using file-based routes.
+ *
+ * @param StartAPIHandlerCallbackContext
+ * @returns The handler for the incoming request
+ *
+ * @example
+ * ```ts
+ * // app/api.ts
+ * import {
+ * createStartAPIHandler,
+ * defaultAPIFileRouteHandler
+ * } from '@tanstack/start-api-routes'
+ *
+ * export default createStartAPIHandler(defaultAPIFileRouteHandler)
+ * ```
+ */
+export const defaultAPIFileRouteHandler: StartAPIHandlerCallback = async ({
+ request,
+}) => {
+ // Simple early abort if there are no routes
+ if (!vinxiRoutes.length) {
+ return new Response('No routes found', { status: 404 })
+ }
+
+ if (!HTTP_API_METHODS.includes(request.method as HTTP_API_METHOD)) {
+ return new Response('Method not allowed', { status: 405 })
+ }
+
+ const routes = toTSRFileBasedRoutes(vinxiRoutes)
+
+ const url = new URL(request.url, 'http://localhost:3000')
+
+ // Find the route that file that matches the request by the request URL
+ const match = findRoute(url, routes)
+
+ // If we don't have a route that could possibly handle the request, return a 404
+ if (!match) {
+ return new Response('Not found', { status: 404 })
+ }
+
+ // The action is the route file that we need to import
+ // which contains the possible handlers for the incoming request
+ let action: APIRouteReturnType | undefined = undefined
+
+ try {
+ // We can guarantee that action is defined since we filtered for it earlier
+ action = await match.payload.$APIRoute!.import().then((m) => m.APIRoute)
+ } catch (err) {
+ // If we can't import the route file, return a 500
+ console.error('Error importing route file:', err)
+ return new Response('Internal server error', { status: 500 })
+ }
+
+ // If we don't have an action, return a 500
+ if (!action) {
+ return new Response('Internal server error', { status: 500 })
+ }
+
+ const method = request.method as HTTP_API_METHOD
+
+ // Get the handler for the request method based on the Request Method
+ const handler = action.methods[method]
+
+ // If the handler is not defined, return a 405
+ // What this means is that we have a route that matches the request
+ // but we don't have a handler for the request method
+ // i.e we have a route that matches /api/foo/$ but we don't have a POST handler
+ if (!handler) {
+ return new Response('Method not allowed', { status: 405 })
+ }
+
+ return await handler({ request, params: match.params })
+}
diff --git a/packages/solid-start-api-routes/tsconfig.json b/packages/solid-start-api-routes/tsconfig.json
new file mode 100644
index 0000000000..51dda9abf2
--- /dev/null
+++ b/packages/solid-start-api-routes/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "module": "esnext"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/packages/solid-start-api-routes/vite.config.ts b/packages/solid-start-api-routes/vite.config.ts
new file mode 100644
index 0000000000..976bb5c87f
--- /dev/null
+++ b/packages/solid-start-api-routes/vite.config.ts
@@ -0,0 +1,21 @@
+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',
+ }),
+)
diff --git a/packages/solid-start-client/README.md b/packages/solid-start-client/README.md
new file mode 100644
index 0000000000..fd6e98ac6d
--- /dev/null
+++ b/packages/solid-start-client/README.md
@@ -0,0 +1,33 @@
+> 🤫 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/solid-start-client/eslint.config.js b/packages/solid-start-client/eslint.config.js
new file mode 100644
index 0000000000..7e6e3be789
--- /dev/null
+++ b/packages/solid-start-client/eslint.config.js
@@ -0,0 +1,25 @@
+// @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: {},
+ },
+ {
+ files: ['**/__tests__/**'],
+ rules: {
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ },
+ },
+]
diff --git a/packages/solid-start-client/package.json b/packages/solid-start-client/package.json
new file mode 100644
index 0000000000..7d65e36186
--- /dev/null
+++ b/packages/solid-start-client/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "@tanstack/solid-start-client",
+ "version": "1.109.2",
+ "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/solid-start-client"
+ },
+ "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:deps && pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit",
+ "test:unit": "vitest",
+ "test:unit:dev": "vitest --watch",
+ "test:eslint": "eslint ./src",
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+ "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+ "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": "tsc",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/router-core": "workspace:^",
+ "cookie-es": "^1.2.2",
+ "jsesc": "^3.1.0",
+ "tiny-invariant": "^1.3.3",
+ "tiny-warning": "^1.0.3",
+ "vinxi": "^0.5.3"
+ },
+ "devDependencies": {
+ "@solidjs/testing-library": "^0.8.10",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@types/jsesc": "^3.0.3",
+ "vite-plugin-solid": "^2.11.2"
+ },
+ "peerDependencies": {
+ "solid-js": ">=1.0.0"
+ }
+}
diff --git a/packages/solid-start-client/src/Meta.tsx b/packages/solid-start-client/src/Meta.tsx
new file mode 100644
index 0000000000..5da9f9268b
--- /dev/null
+++ b/packages/solid-start-client/src/Meta.tsx
@@ -0,0 +1,10 @@
+import { HeadContent } from '@tanstack/solid-router'
+
+export const Meta = () => {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(
+ 'The Meta component is deprecated. Use `HeadContent` from `@tanstack/solid-router` instead.',
+ )
+ }
+ return
+}
diff --git a/packages/solid-start-client/src/Scripts.tsx b/packages/solid-start-client/src/Scripts.tsx
new file mode 100644
index 0000000000..3f782d7d54
--- /dev/null
+++ b/packages/solid-start-client/src/Scripts.tsx
@@ -0,0 +1,8 @@
+import { Scripts as RouterScripts } from '@tanstack/solid-router'
+
+export const Scripts = () => {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('The Scripts component was moved to `@tanstack/solid-router`')
+ }
+ return
+}
diff --git a/packages/solid-start-client/src/StartClient.tsx b/packages/solid-start-client/src/StartClient.tsx
new file mode 100644
index 0000000000..4336e15915
--- /dev/null
+++ b/packages/solid-start-client/src/StartClient.tsx
@@ -0,0 +1,41 @@
+import { Await, HeadContent, RouterProvider } from '@tanstack/solid-router'
+import { hydrate } from './ssr-client'
+import type { AnyRouter } from '@tanstack/solid-router'
+import type { JSXElement } from 'solid-js'
+
+let hydrationPromise: Promise>> | undefined
+
+const Dummy = (props: { children?: JSXElement }) => <>{props.children}>
+
+export function StartClient(props: { router: AnyRouter }) {
+ if (!hydrationPromise) {
+ if (!props.router.state.matches.length) {
+ hydrationPromise = hydrate(props.router)
+ } else {
+ hydrationPromise = Promise.resolve()
+ }
+ }
+ return (
+ (
+
+
+ (
+
+
+
+ {props.children}
+
+
+
+ )}
+ />
+
+
+ )}
+ />
+ )
+}
diff --git a/packages/solid-start-client/src/createIsomorphicFn.ts b/packages/solid-start-client/src/createIsomorphicFn.ts
new file mode 100644
index 0000000000..8703bd465b
--- /dev/null
+++ b/packages/solid-start-client/src/createIsomorphicFn.ts
@@ -0,0 +1,36 @@
+// a function that can have different implementations on the client and server.
+// implementations not provided will default to a no-op function.
+
+export type IsomorphicFn<
+ TArgs extends Array = [],
+ TServer = undefined,
+ TClient = undefined,
+> = (...args: TArgs) => TServer | TClient
+
+export interface ServerOnlyFn, TServer>
+ extends IsomorphicFn {
+ client: (
+ clientImpl: (...args: TArgs) => TClient,
+ ) => IsomorphicFn
+}
+
+export interface ClientOnlyFn, TClient>
+ extends IsomorphicFn {
+ server: (
+ serverImpl: (...args: TArgs) => TServer,
+ ) => IsomorphicFn
+}
+
+export interface IsomorphicFnBase extends IsomorphicFn {
+ server: , TServer>(
+ serverImpl: (...args: TArgs) => TServer,
+ ) => ServerOnlyFn
+ client: , TClient>(
+ clientImpl: (...args: TArgs) => TClient,
+ ) => ClientOnlyFn
+}
+
+// this is a dummy function, it will be replaced by the transformer
+export function createIsomorphicFn(): IsomorphicFnBase {
+ return null!
+}
diff --git a/packages/solid-start-client/src/createMiddleware.ts b/packages/solid-start-client/src/createMiddleware.ts
new file mode 100644
index 0000000000..654fe78271
--- /dev/null
+++ b/packages/solid-start-client/src/createMiddleware.ts
@@ -0,0 +1,595 @@
+import type {
+ ConstrainValidator,
+ Method,
+ ServerFnResponseType,
+ ServerFnTypeOrTypeFn,
+} from './createServerFn'
+import type {
+ Assign,
+ Constrain,
+ Expand,
+ IntersectAssign,
+ ResolveValidatorInput,
+ ResolveValidatorOutput,
+ SerializerStringify,
+} from '@tanstack/router-core'
+
+export type AssignAllMiddleware<
+ TMiddlewares,
+ TType extends keyof AnyMiddleware['_types'],
+ TAcc = undefined,
+> = TMiddlewares extends readonly [
+ infer TMiddleware extends AnyMiddleware,
+ ...infer TRest,
+]
+ ? AssignAllMiddleware<
+ TRest,
+ TType,
+ Assign
+ >
+ : TAcc
+
+/**
+ * Recursively resolve the client context type produced by a sequence of middleware
+ */
+export type AssignAllClientContextBeforeNext<
+ TMiddlewares,
+ TClientContext = undefined,
+> = unknown extends TClientContext
+ ? TClientContext
+ : Assign<
+ AssignAllMiddleware,
+ TClientContext
+ >
+
+export type AssignAllClientSendContext<
+ TMiddlewares,
+ TSendContext = undefined,
+> = unknown extends TSendContext
+ ? TSendContext
+ : Assign<
+ AssignAllMiddleware,
+ TSendContext
+ >
+
+export type AssignAllClientContextAfterNext<
+ TMiddlewares,
+ TClientContext = undefined,
+ TSendContext = undefined,
+> = unknown extends TClientContext
+ ? Assign
+ : Assign<
+ AssignAllMiddleware,
+ Assign
+ >
+
+/**
+ * Recursively resolve the server context type produced by a sequence of middleware
+ */
+export type AssignAllServerContext<
+ TMiddlewares,
+ TSendContext = undefined,
+ TServerContext = undefined,
+> = unknown extends TSendContext
+ ? Assign
+ : Assign<
+ AssignAllMiddleware,
+ Assign
+ >
+
+export type AssignAllServerSendContext<
+ TMiddlewares,
+ TSendContext = undefined,
+> = unknown extends TSendContext
+ ? TSendContext
+ : Assign<
+ AssignAllMiddleware,
+ TSendContext
+ >
+
+export type IntersectAllMiddleware<
+ TMiddlewares,
+ TType extends keyof AnyMiddleware['_types'],
+ TAcc = undefined,
+> = TMiddlewares extends readonly [
+ infer TMiddleware extends AnyMiddleware,
+ ...infer TRest,
+]
+ ? IntersectAllMiddleware<
+ TRest,
+ TType,
+ IntersectAssign
+ >
+ : TAcc
+
+/**
+ * Recursively resolve the input type produced by a sequence of middleware
+ */
+export type IntersectAllValidatorInputs =
+ unknown extends TValidator
+ ? TValidator
+ : IntersectAssign<
+ IntersectAllMiddleware,
+ TValidator extends undefined
+ ? undefined
+ : ResolveValidatorInput
+ >
+/**
+ * Recursively merge the output type produced by a sequence of middleware
+ */
+export type IntersectAllValidatorOutputs =
+ unknown extends TValidator
+ ? TValidator
+ : IntersectAssign<
+ IntersectAllMiddleware,
+ TValidator extends undefined
+ ? undefined
+ : ResolveValidatorOutput
+ >
+
+export interface MiddlewareOptions<
+ in out TMiddlewares,
+ in out TValidator,
+ in out TServerContext,
+ in out TClientContext,
+ in out TServerFnResponseType extends ServerFnResponseType,
+> {
+ validateClient?: boolean
+ middleware?: TMiddlewares
+ validator?: ConstrainValidator
+ client?: MiddlewareClientFn<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ TClientContext,
+ TServerFnResponseType
+ >
+ server?: MiddlewareServerFn<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ unknown,
+ unknown,
+ TServerFnResponseType
+ >
+}
+
+export type MiddlewareServerNextFn = <
+ TNewServerContext = undefined,
+ TSendContext = undefined,
+>(ctx?: {
+ context?: TNewServerContext
+ sendContext?: SerializerStringify
+}) => Promise<
+ ServerResultWithContext<
+ TMiddlewares,
+ TServerSendContext,
+ TNewServerContext,
+ TSendContext
+ >
+>
+
+export interface MiddlewareServerFnOptions<
+ in out TMiddlewares,
+ in out TValidator,
+ in out TServerSendContext,
+ in out TServerFnResponseType,
+> {
+ data: Expand>
+ context: Expand>
+ next: MiddlewareServerNextFn
+ response: TServerFnResponseType
+ method: Method
+ filename: string
+ functionId: string
+ signal: AbortSignal
+}
+
+export type MiddlewareServerFn<
+ TMiddlewares,
+ TValidator,
+ TServerSendContext,
+ TNewServerContext,
+ TSendContext,
+ TServerFnResponseType extends ServerFnResponseType,
+> = (
+ options: MiddlewareServerFnOptions<
+ TMiddlewares,
+ TValidator,
+ TServerSendContext,
+ TServerFnResponseType
+ >,
+) => MiddlewareServerFnResult<
+ TMiddlewares,
+ TServerSendContext,
+ TNewServerContext,
+ TSendContext
+>
+
+export type MiddlewareServerFnResult<
+ TMiddlewares,
+ TServerSendContext,
+ TServerContext,
+ TSendContext,
+> =
+ | Promise<
+ ServerResultWithContext<
+ TMiddlewares,
+ TServerSendContext,
+ TServerContext,
+ TSendContext
+ >
+ >
+ | ServerResultWithContext<
+ TMiddlewares,
+ TServerSendContext,
+ TServerContext,
+ TSendContext
+ >
+
+export type MiddlewareClientNextFn = <
+ TSendContext = undefined,
+ TNewClientContext = undefined,
+>(ctx?: {
+ context?: TNewClientContext
+ sendContext?: SerializerStringify
+ headers?: HeadersInit
+}) => Promise<
+ ClientResultWithContext
+>
+
+export interface MiddlewareClientFnOptions<
+ in out TMiddlewares,
+ in out TValidator,
+ in out TServerFnResponseType extends ServerFnResponseType,
+> {
+ data: Expand>
+ context: Expand>
+ sendContext: Expand>
+ method: Method
+ response: TServerFnResponseType
+ signal: AbortSignal
+ next: MiddlewareClientNextFn
+ filename: string
+ functionId: string
+ type: ServerFnTypeOrTypeFn<
+ Method,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator
+ >
+}
+
+export type MiddlewareClientFn<
+ TMiddlewares,
+ TValidator,
+ TSendContext,
+ TClientContext,
+ TServerFnResponseType extends ServerFnResponseType,
+> = (
+ options: MiddlewareClientFnOptions<
+ TMiddlewares,
+ TValidator,
+ TServerFnResponseType
+ >,
+) => MiddlewareClientFnResult
+
+export type MiddlewareClientFnResult<
+ TMiddlewares,
+ TSendContext,
+ TClientContext,
+> =
+ | Promise>
+ | ClientResultWithContext
+
+export type ServerResultWithContext<
+ in out TMiddlewares,
+ in out TServerSendContext,
+ in out TServerContext,
+ in out TSendContext,
+> = {
+ 'use functions must return the result of next()': true
+ _types: {
+ context: TServerContext
+ sendContext: TSendContext
+ }
+ context: Expand<
+ AssignAllServerContext
+ >
+ sendContext: Expand>
+}
+
+export type ClientResultWithContext<
+ in out TMiddlewares,
+ in out TSendContext,
+ in out TClientContext,
+> = {
+ 'use functions must return the result of next()': true
+ context: Expand>
+ sendContext: Expand>
+ headers: HeadersInit
+}
+
+export type AnyMiddleware = MiddlewareWithTypes<
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any
+>
+
+export interface MiddlewareTypes<
+ in out TMiddlewares,
+ in out TValidator,
+ in out TServerContext,
+ in out TServerSendContext,
+ in out TClientContext,
+ in out TClientSendContext,
+> {
+ middlewares: TMiddlewares
+ input: ResolveValidatorInput
+ allInput: IntersectAllValidatorInputs
+ output: ResolveValidatorOutput
+ allOutput: IntersectAllValidatorOutputs
+ clientContext: TClientContext
+ allClientContextBeforeNext: AssignAllClientContextBeforeNext<
+ TMiddlewares,
+ TClientContext
+ >
+ allClientContextAfterNext: AssignAllClientContextAfterNext<
+ TMiddlewares,
+ TClientContext,
+ TClientSendContext
+ >
+ serverContext: TServerContext
+ serverSendContext: TServerSendContext
+ allServerSendContext: AssignAllServerSendContext<
+ TMiddlewares,
+ TServerSendContext
+ >
+ allServerContext: AssignAllServerContext<
+ TMiddlewares,
+ TServerSendContext,
+ TServerContext
+ >
+ clientSendContext: TClientSendContext
+ allClientSendContext: AssignAllClientSendContext<
+ TMiddlewares,
+ TClientSendContext
+ >
+ validator: TValidator
+}
+
+export interface MiddlewareWithTypes<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ TServerSendContext,
+ TClientContext,
+ TClientSendContext,
+ TServerFnResponseType extends ServerFnResponseType,
+> {
+ _types: MiddlewareTypes<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ TServerSendContext,
+ TClientContext,
+ TClientSendContext
+ >
+ options: MiddlewareOptions<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ TClientContext,
+ TServerFnResponseType
+ >
+}
+
+export interface MiddlewareAfterValidator<
+ TMiddlewares,
+ TValidator,
+ TServerFnResponseType extends ServerFnResponseType,
+> extends MiddlewareWithTypes<
+ TMiddlewares,
+ TValidator,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ServerFnResponseType
+ >,
+ MiddlewareServer<
+ TMiddlewares,
+ TValidator,
+ undefined,
+ undefined,
+ TServerFnResponseType
+ >,
+ MiddlewareClient {}
+
+export interface MiddlewareValidator<
+ TMiddlewares,
+ TServerFnResponseType extends ServerFnResponseType,
+> {
+ validator: (
+ input: ConstrainValidator,
+ ) => MiddlewareAfterValidator<
+ TMiddlewares,
+ TNewValidator,
+ TServerFnResponseType
+ >
+}
+
+export interface MiddlewareAfterServer<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ TServerSendContext,
+ TClientContext,
+ TClientSendContext,
+ TServerFnResponseType extends ServerFnResponseType,
+> extends MiddlewareWithTypes<
+ TMiddlewares,
+ TValidator,
+ TServerContext,
+ TServerSendContext,
+ TClientContext,
+ TClientSendContext,
+ TServerFnResponseType
+ > {}
+
+export interface MiddlewareServer<
+ TMiddlewares,
+ TValidator,
+ TServerSendContext,
+ TClientContext,
+ TServerFnResponseType extends ServerFnResponseType,
+> {
+ server: (
+ server: MiddlewareServerFn<
+ TMiddlewares,
+ TValidator,
+ TServerSendContext,
+ TNewServerContext,
+ TSendContext,
+ TServerFnResponseType
+ >,
+ ) => MiddlewareAfterServer<
+ TMiddlewares,
+ TValidator,
+ TNewServerContext,
+ TServerSendContext,
+ TClientContext,
+ TSendContext,
+ ServerFnResponseType
+ >
+}
+
+export interface MiddlewareAfterClient<
+ TMiddlewares,
+ TValidator,
+ TServerSendContext,
+ TClientContext,
+ TServerFnResponseType extends ServerFnResponseType,
+> extends MiddlewareWithTypes<
+ TMiddlewares,
+ TValidator,
+ undefined,
+ TServerSendContext,
+ TClientContext,
+ undefined,
+ TServerFnResponseType
+ >,
+ MiddlewareServer<
+ TMiddlewares,
+ TValidator,
+ TServerSendContext,
+ TClientContext,
+ TServerFnResponseType
+ > {}
+
+export interface MiddlewareClient<
+ TMiddlewares,
+ TValidator,
+ TServerFnResponseType extends ServerFnResponseType,
+> {
+ client: (
+ client: MiddlewareClientFn<
+ TMiddlewares,
+ TValidator,
+ TSendServerContext,
+ TNewClientContext,
+ TServerFnResponseType
+ >,
+ ) => MiddlewareAfterClient<
+ TMiddlewares,
+ TValidator,
+ TSendServerContext,
+ TNewClientContext,
+ ServerFnResponseType
+ >
+}
+
+export interface MiddlewareAfterMiddleware<
+ TMiddlewares,
+ TServerFnResponseType extends ServerFnResponseType,
+> extends MiddlewareWithTypes<
+ TMiddlewares,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ TServerFnResponseType
+ >,
+ MiddlewareServer<
+ TMiddlewares,
+ undefined,
+ undefined,
+ undefined,
+ TServerFnResponseType
+ >,
+ MiddlewareClient,
+ MiddlewareValidator {}
+
+export interface Middleware
+ extends MiddlewareAfterMiddleware {
+ middleware: (
+ middlewares: Constrain>,
+ ) => MiddlewareAfterMiddleware
+}
+
+export function createMiddleware(
+ options?: {
+ validateClient?: boolean
+ },
+ __opts?: MiddlewareOptions<
+ unknown,
+ undefined,
+ undefined,
+ undefined,
+ ServerFnResponseType
+ >,
+): Middleware {
+ // const resolvedOptions = (__opts || options) as MiddlewareOptions<
+ const resolvedOptions =
+ __opts ||
+ ((options || {}) as MiddlewareOptions<
+ unknown,
+ undefined,
+ undefined,
+ undefined,
+ ServerFnResponseType
+ >)
+
+ return {
+ options: resolvedOptions as any,
+ middleware: (middleware: any) => {
+ return createMiddleware(
+ undefined,
+ Object.assign(resolvedOptions, { middleware }),
+ ) as any
+ },
+ validator: (validator: any) => {
+ return createMiddleware(
+ undefined,
+ Object.assign(resolvedOptions, { validator }),
+ ) as any
+ },
+ client: (client: any) => {
+ return createMiddleware(
+ undefined,
+ Object.assign(resolvedOptions, { client }),
+ ) as any
+ },
+ server: (server: any) => {
+ return createMiddleware(
+ undefined,
+ Object.assign(resolvedOptions, { server }),
+ ) as any
+ },
+ } as unknown as Middleware
+}
diff --git a/packages/solid-start-client/src/createServerFn.ts b/packages/solid-start-client/src/createServerFn.ts
new file mode 100644
index 0000000000..efc570590e
--- /dev/null
+++ b/packages/solid-start-client/src/createServerFn.ts
@@ -0,0 +1,942 @@
+import { default as invariant } from 'tiny-invariant'
+import { default as warning } from 'tiny-warning'
+import { isNotFound, isRedirect } from '@tanstack/solid-router'
+import { mergeHeaders } from './headers'
+import { globalMiddleware } from './registerGlobalMiddleware'
+import { startSerializer } from './serializer'
+import type { Readable } from 'node:stream'
+import type {
+ AnyValidator,
+ Constrain,
+ Expand,
+ ResolveValidatorInput,
+ SerializerParse,
+ SerializerStringify,
+ SerializerStringifyBy,
+ Validator,
+} from '@tanstack/router-core'
+import type {
+ AnyMiddleware,
+ AssignAllClientSendContext,
+ AssignAllServerContext,
+ IntersectAllValidatorInputs,
+ IntersectAllValidatorOutputs,
+ MiddlewareClientFnResult,
+ MiddlewareServerFnResult,
+} from './createMiddleware'
+
+export interface JsonResponse extends Response {
+ json: () => Promise
+}
+
+export type CompiledFetcherFnOptions = {
+ method: Method
+ data: unknown
+ response?: ServerFnResponseType
+ headers?: HeadersInit
+ signal?: AbortSignal
+ context?: any
+}
+
+export type Fetcher<
+ TMiddlewares,
+ TValidator,
+ TResponse,
+ TServerFnResponseType extends ServerFnResponseType,
+> =
+ undefined extends IntersectAllValidatorInputs
+ ? OptionalFetcher<
+ TMiddlewares,
+ TValidator,
+ TResponse,
+ TServerFnResponseType
+ >
+ : RequiredFetcher<
+ TMiddlewares,
+ TValidator,
+ TResponse,
+ TServerFnResponseType
+ >
+
+export interface FetcherBase {
+ url: string
+ __executeServer: (opts: {
+ method: Method
+ response?: ServerFnResponseType
+ data: unknown
+ headers?: HeadersInit
+ context?: any
+ signal: AbortSignal
+ }) => Promise
+}
+
+export type FetchResult<
+ TMiddlewares,
+ TResponse,
+ TServerFnResponseType extends ServerFnResponseType,
+> = TServerFnResponseType extends 'raw'
+ ? Promise
+ : TServerFnResponseType extends 'full'
+ ? Promise>
+ : Promise>
+
+export interface OptionalFetcher<
+ TMiddlewares,
+ TValidator,
+ TResponse,
+ TServerFnResponseType extends ServerFnResponseType,
+> extends FetcherBase {
+ (
+ options?: OptionalFetcherDataOptions,
+ ): FetchResult
+}
+
+export interface RequiredFetcher<
+ TMiddlewares,
+ TValidator,
+ TResponse,
+ TServerFnResponseType extends ServerFnResponseType,
+> extends FetcherBase {
+ (
+ opts: RequiredFetcherDataOptions,
+ ): FetchResult
+}
+
+export type FetcherBaseOptions = {
+ headers?: HeadersInit
+ type?: ServerFnType
+ signal?: AbortSignal
+}
+
+export type ServerFnType = 'static' | 'dynamic'
+
+export interface OptionalFetcherDataOptions
+ extends FetcherBaseOptions {
+ data?: Expand>
+}
+
+export interface RequiredFetcherDataOptions
+ extends FetcherBaseOptions {
+ data: Expand>
+}
+
+export interface FullFetcherData {
+ error: unknown
+ result: FetcherData
+ context: AssignAllClientSendContext
+}
+
+export type FetcherData =
+ TResponse extends JsonResponse
+ ? SerializerParse>
+ : SerializerParse
+
+export type RscStream = {
+ __cacheState: T
+}
+
+export type Method = 'GET' | 'POST'
+export type ServerFnResponseType = 'data' | 'full' | 'raw'
+
+// see https://h3.unjs.io/guide/event-handler#responses-types
+export type RawResponse = Response | ReadableStream | Readable | null | string
+
+export type ServerFnReturnType<
+ TServerFnResponseType extends ServerFnResponseType,
+ TResponse,
+> = TServerFnResponseType extends 'raw'
+ ? RawResponse | Promise
+ : Promise> | SerializerStringify
+export type ServerFn<
+ TMethod,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+ TResponse,
+> = (
+ ctx: ServerFnCtx,
+) => ServerFnReturnType
+
+export interface ServerFnCtx<
+ TMethod,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> {
+ method: TMethod
+ response: TServerFnResponseType
+ data: Expand>
+ context: Expand>
+ signal: AbortSignal
+}
+
+export type CompiledFetcherFn<
+ TResponse,
+ TServerFnResponseType extends ServerFnResponseType,
+> = {
+ (
+ opts: CompiledFetcherFnOptions &
+ ServerFnBaseOptions,
+ ): Promise
+ url: string
+}
+
+type ServerFnBaseOptions<
+ TMethod extends Method = 'GET',
+ TServerFnResponseType extends ServerFnResponseType = 'data',
+ TResponse = unknown,
+ TMiddlewares = unknown,
+ TInput = unknown,
+> = {
+ method: TMethod
+ response?: TServerFnResponseType
+ validateClient?: boolean
+ middleware?: Constrain>
+ validator?: ConstrainValidator
+ extractedFn?: CompiledFetcherFn
+ serverFn?: ServerFn<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TInput,
+ TResponse
+ >
+ functionId: string
+ type: ServerFnTypeOrTypeFn<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ AnyValidator
+ >
+}
+
+export type ValidatorSerializerStringify = Validator<
+ SerializerStringifyBy<
+ ResolveValidatorInput,
+ Date | undefined | FormData
+ >,
+ any
+>
+
+export type ConstrainValidator = unknown extends TValidator
+ ? TValidator
+ : Constrain>
+
+export interface ServerFnMiddleware<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TValidator,
+> {
+ middleware: (
+ middlewares: Constrain>,
+ ) => ServerFnAfterMiddleware<
+ TMethod,
+ TServerFnResponseType,
+ TNewMiddlewares,
+ TValidator
+ >
+}
+
+export interface ServerFnAfterMiddleware<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> extends ServerFnValidator,
+ ServerFnTyper,
+ ServerFnHandler {}
+
+export type ValidatorFn<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+> = (
+ validator: ConstrainValidator,
+) => ServerFnAfterValidator<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator
+>
+
+export interface ServerFnValidator<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+> {
+ validator: ValidatorFn
+}
+
+export interface ServerFnAfterValidator<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> extends ServerFnMiddleware,
+ ServerFnTyper,
+ ServerFnHandler {}
+
+// Typer
+export interface ServerFnTyper<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> {
+ type: (
+ typer: ServerFnTypeOrTypeFn<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator
+ >,
+ ) => ServerFnAfterTyper<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator
+ >
+}
+
+export type ServerFnTypeOrTypeFn<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> =
+ | ServerFnType
+ | ((
+ ctx: ServerFnCtx<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator
+ >,
+ ) => ServerFnType)
+
+export interface ServerFnAfterTyper<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> extends ServerFnHandler<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator
+ > {}
+
+// Handler
+export interface ServerFnHandler<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+> {
+ handler: (
+ fn?: ServerFn<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+ TNewResponse
+ >,
+ ) => Fetcher
+}
+
+export interface ServerFnBuilder<
+ TMethod extends Method = 'GET',
+ TServerFnResponseType extends ServerFnResponseType = 'data',
+> extends ServerFnMiddleware,
+ ServerFnValidator,
+ ServerFnTyper,
+ ServerFnHandler {
+ options: ServerFnBaseOptions<
+ TMethod,
+ TServerFnResponseType,
+ unknown,
+ undefined,
+ undefined
+ >
+}
+
+type StaticCachedResult = {
+ ctx?: {
+ result: any
+ context: any
+ }
+ error?: any
+}
+
+export type ServerFnStaticCache = {
+ getItem: (
+ ctx: MiddlewareResult,
+ ) => StaticCachedResult | Promise
+ setItem: (
+ ctx: MiddlewareResult,
+ response: StaticCachedResult,
+ ) => Promise
+ fetchItem: (
+ ctx: MiddlewareResult,
+ ) => StaticCachedResult | Promise
+}
+
+let serverFnStaticCache: ServerFnStaticCache | undefined
+
+export function setServerFnStaticCache(
+ cache?: ServerFnStaticCache | (() => ServerFnStaticCache | undefined),
+) {
+ const previousCache = serverFnStaticCache
+ serverFnStaticCache = typeof cache === 'function' ? cache() : cache
+
+ return () => {
+ serverFnStaticCache = previousCache
+ }
+}
+
+export function createServerFnStaticCache(
+ serverFnStaticCache: ServerFnStaticCache,
+) {
+ return serverFnStaticCache
+}
+
+setServerFnStaticCache(() => {
+ const getStaticCacheUrl = (options: MiddlewareResult, hash: string) => {
+ return `/__tsr/staticServerFnCache/${options.functionId}__${hash}.json`
+ }
+
+ const jsonToFilenameSafeString = (json: any) => {
+ // Custom replacer to sort keys
+ const sortedKeysReplacer = (key: string, value: any) =>
+ value && typeof value === 'object' && !Array.isArray(value)
+ ? Object.keys(value)
+ .sort()
+ .reduce((acc: any, curr: string) => {
+ acc[curr] = value[curr]
+ return acc
+ }, {})
+ : value
+
+ // Convert JSON to string with sorted keys
+ const jsonString = JSON.stringify(json ?? '', sortedKeysReplacer)
+
+ // Replace characters invalid in filenames
+ return jsonString
+ .replace(/[/\\?%*:|"<>]/g, '-') // Replace invalid characters with a dash
+ .replace(/\s+/g, '_') // Optionally replace whitespace with underscores
+ }
+
+ const staticClientCache =
+ typeof document !== 'undefined' ? new Map() : null
+
+ return createServerFnStaticCache({
+ getItem: async (ctx) => {
+ if (typeof document === 'undefined') {
+ const hash = jsonToFilenameSafeString(ctx.data)
+ const url = getStaticCacheUrl(ctx, hash)
+ const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR!
+
+ // Use fs instead of fetch to read from filesystem
+ const { promises: fs } = await import('node:fs')
+ const path = await import('node:path')
+ const filePath = path.join(publicUrl, url)
+
+ const [cachedResult, readError] = await fs
+ .readFile(filePath, 'utf-8')
+ .then((c) => [
+ startSerializer.parse(c) as {
+ ctx: unknown
+ error: any
+ },
+ null,
+ ])
+ .catch((e) => [null, e])
+
+ if (readError && readError.code !== 'ENOENT') {
+ throw readError
+ }
+
+ return cachedResult as StaticCachedResult
+ }
+
+ return undefined
+ },
+ setItem: async (ctx, response) => {
+ const { promises: fs } = await import('node:fs')
+ const path = await import('node:path')
+
+ const hash = jsonToFilenameSafeString(ctx.data)
+ const url = getStaticCacheUrl(ctx, hash)
+ const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR!
+ const filePath = path.join(publicUrl, url)
+
+ // Ensure the directory exists
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
+
+ // Store the result with fs
+ await fs.writeFile(filePath, startSerializer.stringify(response))
+ },
+ fetchItem: async (ctx) => {
+ const hash = jsonToFilenameSafeString(ctx.data)
+ const url = getStaticCacheUrl(ctx, hash)
+
+ let result: any = staticClientCache?.get(url)
+
+ if (!result) {
+ result = await fetch(url, {
+ method: 'GET',
+ })
+ .then((r) => r.text())
+ .then((d) => startSerializer.parse(d))
+
+ staticClientCache?.set(url, result)
+ }
+
+ return result
+ },
+ })
+})
+
+export function createServerFn<
+ TMethod extends Method,
+ TServerFnResponseType extends ServerFnResponseType = 'data',
+ TResponse = unknown,
+ TMiddlewares = undefined,
+ TValidator = undefined,
+>(
+ options?: {
+ method?: TMethod
+ response?: TServerFnResponseType
+ type?: ServerFnType
+ },
+ __opts?: ServerFnBaseOptions<
+ TMethod,
+ TServerFnResponseType,
+ TResponse,
+ TMiddlewares,
+ TValidator
+ >,
+): ServerFnBuilder {
+ const resolvedOptions = (__opts || options || {}) as ServerFnBaseOptions<
+ TMethod,
+ ServerFnResponseType,
+ TResponse,
+ TMiddlewares,
+ TValidator
+ >
+
+ if (typeof resolvedOptions.method === 'undefined') {
+ resolvedOptions.method = 'GET' as TMethod
+ }
+
+ return {
+ options: resolvedOptions as any,
+ middleware: (middleware) => {
+ return createServerFn<
+ TMethod,
+ ServerFnResponseType,
+ TResponse,
+ TMiddlewares,
+ TValidator
+ >(undefined, Object.assign(resolvedOptions, { middleware })) as any
+ },
+ validator: (validator) => {
+ return createServerFn<
+ TMethod,
+ ServerFnResponseType,
+ TResponse,
+ TMiddlewares,
+ TValidator
+ >(undefined, Object.assign(resolvedOptions, { validator })) as any
+ },
+ type: (type) => {
+ return createServerFn<
+ TMethod,
+ ServerFnResponseType,
+ TResponse,
+ TMiddlewares,
+ TValidator
+ >(undefined, Object.assign(resolvedOptions, { type })) as any
+ },
+ handler: (...args) => {
+ // This function signature changes due to AST transformations
+ // in the babel plugin. We need to cast it to the correct
+ // function signature post-transformation
+ const [extractedFn, serverFn] = args as unknown as [
+ CompiledFetcherFn,
+ ServerFn<
+ TMethod,
+ TServerFnResponseType,
+ TMiddlewares,
+ TValidator,
+ TResponse
+ >,
+ ]
+
+ // Keep the original function around so we can use it
+ // in the server environment
+ Object.assign(resolvedOptions, {
+ ...extractedFn,
+ extractedFn,
+ serverFn,
+ })
+
+ const resolvedMiddleware = [
+ ...(resolvedOptions.middleware || []),
+ serverFnBaseToMiddleware(resolvedOptions),
+ ]
+
+ // We want to make sure the new function has the same
+ // properties as the original function
+ return Object.assign(
+ async (opts?: CompiledFetcherFnOptions) => {
+ // Start by executing the client-side middleware chain
+ return executeMiddleware(resolvedMiddleware, 'client', {
+ ...extractedFn,
+ ...resolvedOptions,
+ data: opts?.data as any,
+ headers: opts?.headers,
+ signal: opts?.signal,
+ context: {},
+ }).then((d) => {
+ if (resolvedOptions.response === 'full') {
+ return d
+ }
+ if (d.error) throw d.error
+ return d.result
+ })
+ },
+ {
+ // This copies over the URL, function ID
+ ...extractedFn,
+ // The extracted function on the server-side calls
+ // this function
+ __executeServer: async (opts_: any, signal: AbortSignal) => {
+ const opts =
+ opts_ instanceof FormData ? extractFormDataContext(opts_) : opts_
+
+ opts.type =
+ typeof resolvedOptions.type === 'function'
+ ? resolvedOptions.type(opts)
+ : resolvedOptions.type
+
+ const ctx = {
+ ...extractedFn,
+ ...opts,
+ signal,
+ }
+
+ const run = () =>
+ executeMiddleware(resolvedMiddleware, 'server', ctx).then(
+ (d) => ({
+ // Only send the result and sendContext back to the client
+ result: d.result,
+ error: d.error,
+ context: d.sendContext,
+ }),
+ )
+
+ if (ctx.type === 'static') {
+ let response: StaticCachedResult | undefined
+
+ // If we can get the cached item, try to get it
+ if (serverFnStaticCache?.getItem) {
+ // If this throws, it's okay to let it bubble up
+ response = await serverFnStaticCache.getItem(ctx)
+ }
+
+ if (!response) {
+ // If there's no cached item, execute the server function
+ response = await run()
+ .then((d) => {
+ return {
+ ctx: d,
+ error: null,
+ }
+ })
+ .catch((e) => {
+ return {
+ ctx: undefined,
+ error: e,
+ }
+ })
+
+ if (serverFnStaticCache?.setItem) {
+ await serverFnStaticCache.setItem(ctx, response)
+ }
+ }
+
+ invariant(
+ response,
+ 'No response from both server and static cache!',
+ )
+
+ if (response.error) {
+ throw response.error
+ }
+
+ return response.ctx
+ }
+
+ return run()
+ },
+ },
+ ) as any
+ },
+ }
+}
+
+function extractFormDataContext(formData: FormData) {
+ const serializedContext = formData.get('__TSR_CONTEXT')
+ formData.delete('__TSR_CONTEXT')
+
+ if (typeof serializedContext !== 'string') {
+ return {
+ context: {},
+ data: formData,
+ }
+ }
+
+ try {
+ const context = startSerializer.parse(serializedContext)
+ return {
+ context,
+ data: formData,
+ }
+ } catch {
+ return {
+ data: formData,
+ }
+ }
+}
+
+function flattenMiddlewares(
+ middlewares: Array,
+): Array {
+ const seen = new Set()
+ const flattened: Array = []
+
+ const recurse = (middleware: Array) => {
+ middleware.forEach((m) => {
+ if (m.options.middleware) {
+ recurse(m.options.middleware)
+ }
+
+ if (!seen.has(m)) {
+ seen.add(m)
+ flattened.push(m)
+ }
+ })
+ }
+
+ recurse(middlewares)
+
+ return flattened
+}
+
+export type MiddlewareOptions = {
+ method: Method
+ response?: ServerFnResponseType
+ data: any
+ headers?: HeadersInit
+ signal?: AbortSignal
+ sendContext?: any
+ context?: any
+ type: ServerFnTypeOrTypeFn
+ functionId: string
+}
+
+export type MiddlewareResult = MiddlewareOptions & {
+ result?: unknown
+ error?: unknown
+ type: ServerFnTypeOrTypeFn
+}
+
+export type NextFn = (ctx: MiddlewareResult) => Promise
+
+export type MiddlewareFn = (
+ ctx: MiddlewareOptions & {
+ next: NextFn
+ },
+) => Promise
+
+const applyMiddleware = async (
+ middlewareFn: MiddlewareFn,
+ ctx: MiddlewareOptions,
+ nextFn: NextFn,
+) => {
+ return middlewareFn({
+ ...ctx,
+ next: (async (userCtx: MiddlewareResult | undefined = {} as any) => {
+ // Return the next middleware
+ return nextFn({
+ ...ctx,
+ ...userCtx,
+ context: {
+ ...ctx.context,
+ ...userCtx.context,
+ },
+ sendContext: {
+ ...ctx.sendContext,
+ ...(userCtx.sendContext ?? {}),
+ },
+ headers: mergeHeaders(ctx.headers, userCtx.headers),
+ result:
+ userCtx.result !== undefined
+ ? userCtx.result
+ : ctx.response === 'raw'
+ ? userCtx
+ : (ctx as any).result,
+ error: userCtx.error ?? (ctx as any).error,
+ })
+ }) as any,
+ } as any)
+}
+
+function execValidator(validator: AnyValidator, input: unknown): unknown {
+ if (validator == null) return {}
+
+ if ('~standard' in validator) {
+ const result = validator['~standard'].validate(input)
+
+ if (result instanceof Promise)
+ throw new Error('Async validation not supported')
+
+ if (result.issues)
+ throw new Error(JSON.stringify(result.issues, undefined, 2))
+
+ return result.value
+ }
+
+ if ('parse' in validator) {
+ return validator.parse(input)
+ }
+
+ if (typeof validator === 'function') {
+ return validator(input)
+ }
+
+ throw new Error('Invalid validator type!')
+}
+
+async function executeMiddleware(
+ middlewares: Array,
+ env: 'client' | 'server',
+ opts: MiddlewareOptions,
+): Promise {
+ const flattenedMiddlewares = flattenMiddlewares([
+ ...globalMiddleware,
+ ...middlewares,
+ ])
+
+ const next: NextFn = async (ctx) => {
+ // Get the next middleware
+ const nextMiddleware = flattenedMiddlewares.shift()
+
+ // If there are no more middlewares, return the context
+ if (!nextMiddleware) {
+ return ctx
+ }
+
+ if (
+ nextMiddleware.options.validator &&
+ (env === 'client' ? nextMiddleware.options.validateClient : true)
+ ) {
+ // Execute the middleware's input function
+ ctx.data = await execValidator(nextMiddleware.options.validator, ctx.data)
+ }
+
+ const middlewareFn = (
+ env === 'client'
+ ? nextMiddleware.options.client
+ : nextMiddleware.options.server
+ ) as MiddlewareFn | undefined
+
+ if (middlewareFn) {
+ // Execute the middleware
+ return applyMiddleware(middlewareFn, ctx, async (newCtx) => {
+ return next(newCtx).catch((error) => {
+ if (isRedirect(error) || isNotFound(error)) {
+ return {
+ ...newCtx,
+ error,
+ }
+ }
+
+ throw error
+ })
+ })
+ }
+
+ return next(ctx)
+ }
+
+ // Start the middleware chain
+ return next({
+ ...opts,
+ headers: opts.headers || {},
+ sendContext: opts.sendContext || {},
+ context: opts.context || {},
+ })
+}
+
+function serverFnBaseToMiddleware(
+ options: ServerFnBaseOptions,
+): AnyMiddleware {
+ return {
+ _types: undefined!,
+ options: {
+ validator: options.validator,
+ validateClient: options.validateClient,
+ client: async ({ next, sendContext, ...ctx }) => {
+ const payload = {
+ ...ctx,
+ // switch the sendContext over to context
+ context: sendContext,
+ type: typeof ctx.type === 'function' ? ctx.type(ctx) : ctx.type,
+ } as any
+
+ if (
+ ctx.type === 'static' &&
+ process.env.NODE_ENV === 'production' &&
+ typeof document !== 'undefined'
+ ) {
+ invariant(
+ serverFnStaticCache,
+ 'serverFnStaticCache.fetchItem is not available!',
+ )
+
+ const result = await serverFnStaticCache.fetchItem(payload)
+
+ if (result) {
+ if (result.error) {
+ throw result.error
+ }
+
+ return next(result.ctx)
+ }
+
+ warning(
+ result,
+ `No static cache item found for ${payload.functionId}__${JSON.stringify(payload.data)}, falling back to server function...`,
+ )
+ }
+
+ // Execute the extracted function
+ // but not before serializing the context
+ const res = await options.extractedFn?.(payload)
+
+ return next(res) as unknown as MiddlewareClientFnResult
+ },
+ server: async ({ next, ...ctx }) => {
+ // Execute the server function
+ const result = await options.serverFn?.(ctx)
+
+ return next({
+ ...ctx,
+ result,
+ } as any) as unknown as MiddlewareServerFnResult
+ },
+ },
+ }
+}
diff --git a/packages/solid-start-client/src/envOnly.ts b/packages/solid-start-client/src/envOnly.ts
new file mode 100644
index 0000000000..2b444578c2
--- /dev/null
+++ b/packages/solid-start-client/src/envOnly.ts
@@ -0,0 +1,9 @@
+type EnvOnlyFn = ) => any>(fn: TFn) => TFn
+
+// A function that will only be available in the server build
+// If called on the client, it will throw an error
+export const serverOnly: EnvOnlyFn = (fn) => fn
+
+// A function that will only be available in the client build
+// If called on the server, it will throw an error
+export const clientOnly: EnvOnlyFn = (fn) => fn
diff --git a/packages/solid-start-client/src/headers.ts b/packages/solid-start-client/src/headers.ts
new file mode 100644
index 0000000000..b9b1537452
--- /dev/null
+++ b/packages/solid-start-client/src/headers.ts
@@ -0,0 +1,50 @@
+import { splitSetCookieString } from 'cookie-es'
+import type { OutgoingHttpHeaders } from 'node:http2'
+// A utility function to turn HeadersInit into an object
+export function headersInitToObject(
+ headers: HeadersInit,
+): Record {
+ const obj: Record = {}
+ const headersInstance = new Headers(headers)
+ for (const [key, value] of headersInstance.entries()) {
+ obj[key] = value
+ }
+ return obj
+}
+
+type AnyHeaders =
+ | Headers
+ | HeadersInit
+ | Record
+ | Array<[string, string]>
+ | OutgoingHttpHeaders
+ | undefined
+
+// Helper function to convert various HeaderInit types to a Headers instance
+function toHeadersInstance(init: AnyHeaders) {
+ if (init instanceof Headers) {
+ return new Headers(init)
+ } else if (Array.isArray(init)) {
+ return new Headers(init)
+ } else if (typeof init === 'object') {
+ return new Headers(init as HeadersInit)
+ } else {
+ return new Headers()
+ }
+}
+
+// Function to merge headers with proper overrides
+export function mergeHeaders(...headers: Array) {
+ return headers.reduce((acc: Headers, header) => {
+ const headersInstance = toHeadersInstance(header)
+ for (const [key, value] of headersInstance.entries()) {
+ if (key === 'set-cookie') {
+ const splitCookies = splitSetCookieString(value)
+ splitCookies.forEach((cookie) => acc.append('set-cookie', cookie))
+ } else {
+ acc.set(key, value)
+ }
+ }
+ return acc
+ }, new Headers())
+}
diff --git a/packages/solid-start-client/src/index.tsx b/packages/solid-start-client/src/index.tsx
new file mode 100644
index 0000000000..bc97adc78e
--- /dev/null
+++ b/packages/solid-start-client/src/index.tsx
@@ -0,0 +1,75 @@
+///
+export {
+ createIsomorphicFn,
+ type IsomorphicFn,
+ type ServerOnlyFn,
+ type ClientOnlyFn,
+ type IsomorphicFnBase,
+} from './createIsomorphicFn'
+export {
+ createServerFn,
+ type JsonResponse,
+ type ServerFn as FetchFn,
+ type ServerFnCtx as FetchFnCtx,
+ type CompiledFetcherFnOptions,
+ type CompiledFetcherFn,
+ type Fetcher,
+ type RscStream,
+ type FetcherData,
+ type FetcherBaseOptions,
+ type ServerFn,
+ type ServerFnCtx,
+ type ServerFnResponseType,
+} from './createServerFn'
+export {
+ createMiddleware,
+ 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 AssignAllClientContextBeforeNext,
+ type AssignAllMiddleware,
+ type AssignAllServerContext,
+ type MiddlewareAfterValidator,
+ type MiddlewareClientFn,
+ type MiddlewareServerFnResult,
+ type MiddlewareClient,
+ type MiddlewareServerFnOptions,
+ type MiddlewareServerNextFn,
+ type ServerResultWithContext,
+} from './createMiddleware'
+export {
+ registerGlobalMiddleware,
+ globalMiddleware,
+} from './registerGlobalMiddleware'
+export { serverOnly, clientOnly } from './envOnly'
+export { json } from './json'
+export { Meta } from './Meta'
+export { Scripts } from './Scripts'
+export { StartClient } from './StartClient'
+export { mergeHeaders } from './headers'
+export { renderRsc } from './renderRSC'
+export { useServerFn } from './useServerFn'
+export {
+ type DehydratedRouter,
+ type ClientExtractedBaseEntry,
+ type StartSsrGlobal,
+ type ClientExtractedEntry,
+ type SsrMatch,
+ type ClientExtractedPromise,
+ type ClientExtractedStream,
+ type ResolvePromiseState,
+} from './ssr-client'
+export { startSerializer } from './serializer'
diff --git a/packages/solid-start-client/src/json.ts b/packages/solid-start-client/src/json.ts
new file mode 100644
index 0000000000..ba716bc922
--- /dev/null
+++ b/packages/solid-start-client/src/json.ts
@@ -0,0 +1,15 @@
+import { mergeHeaders } from './headers'
+import type { JsonResponse } from './createServerFn'
+
+export function json(
+ payload: TData,
+ init?: ResponseInit,
+): JsonResponse {
+ return new Response(JSON.stringify(payload), {
+ ...init,
+ headers: mergeHeaders(
+ { 'content-type': 'application/json' },
+ init?.headers,
+ ),
+ })
+}
diff --git a/packages/solid-start-client/src/registerGlobalMiddleware.ts b/packages/solid-start-client/src/registerGlobalMiddleware.ts
new file mode 100644
index 0000000000..3d7effc85b
--- /dev/null
+++ b/packages/solid-start-client/src/registerGlobalMiddleware.ts
@@ -0,0 +1,9 @@
+import type { AnyMiddleware } from './createMiddleware'
+
+export const globalMiddleware: Array = []
+
+export function registerGlobalMiddleware(options: {
+ middleware: Array
+}) {
+ globalMiddleware.push(...options.middleware)
+}
diff --git a/packages/solid-start-client/src/renderRSC.tsx b/packages/solid-start-client/src/renderRSC.tsx
new file mode 100644
index 0000000000..7c534ebf3a
--- /dev/null
+++ b/packages/solid-start-client/src/renderRSC.tsx
@@ -0,0 +1,92 @@
+// TODO: RSCs
+// // @ts-expect-error
+// import * as reactDom from '@vinxi/react-server-dom/client'
+// import { isValidElement } from 'solid-js'
+import invariant from 'tiny-invariant'
+import type * as Solid from 'solid-js'
+
+export function renderRsc(input: any): Solid.JSX.Element {
+ // TODO: isValidElement
+ // if (isValidElement(input)) {
+ // return input
+ // }
+
+ if (typeof input === 'object' && !input.state) {
+ input.state = {
+ status: 'pending',
+ promise: Promise.resolve()
+ .then(() => {
+ let element
+
+ // We're in node
+ // TODO: RSCs
+ // if (reactDom.createFromNodeStream) {
+ // const stream = await import('node:stream')
+
+ // let body: any = input
+
+ // // Unwrap the response
+ // if (input instanceof Response) {
+ // body = input.body
+ // }
+
+ // // Convert ReadableStream to NodeJS stream.Readable
+ // if (body instanceof ReadableStream) {
+ // body = stream.Readable.fromWeb(body as any)
+ // }
+
+ // if (stream.Readable.isReadable(body)) {
+ // // body = copyStreamToRaw(body)
+ // } else if (input.text) {
+ // // create a readable stream by awaiting the text method
+ // body = new stream.Readable({
+ // async read() {
+ // input.text().then((value: any) => {
+ // this.push(value)
+ // this.push(null)
+ // })
+ // },
+ // })
+ // } else {
+ // console.error('input', input)
+ // throw new Error('Unexpected rsc input type 👆')
+ // }
+
+ // element = await reactDom.createFromNodeStream(body)
+ // } else {
+ // // We're in the browser
+ // if (input.body instanceof ReadableStream) {
+ // input = input.body
+ // }
+
+ // if (input instanceof ReadableStream) {
+ // element = await reactDom.createFromReadableStream(input)
+ // }
+
+ // if (input instanceof Response) {
+ // // copy to the response body to cache the raw data
+ // element = await reactDom.createFromFetch(input)
+ // }
+ // }
+
+ // return element
+
+ invariant(false, 'renderRSC() is coming soon!')
+ })
+ .then((element) => {
+ input.state.value = element
+ input.state.status = 'success'
+ })
+ .catch((err) => {
+ input.state.status = 'error'
+ input.state.error = err
+ }),
+ }
+ }
+
+ if (input.state.status === 'pending') {
+ throw input.state.promise
+ }
+
+ return input.state.value
+}
diff --git a/packages/solid-start-client/src/routesManifest.ts b/packages/solid-start-client/src/routesManifest.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/solid-start-client/src/serializer.ts b/packages/solid-start-client/src/serializer.ts
new file mode 100644
index 0000000000..d9fd299b28
--- /dev/null
+++ b/packages/solid-start-client/src/serializer.ts
@@ -0,0 +1,177 @@
+import { isPlainObject } from '@tanstack/router-core'
+import type { StartSerializer } from '@tanstack/router-core'
+
+export const startSerializer: StartSerializer = {
+ stringify: (value: any) =>
+ JSON.stringify(value, function replacer(key, val) {
+ const ogVal = this[key]
+ const serializer = serializers.find((t) => t.stringifyCondition(ogVal))
+
+ if (serializer) {
+ return serializer.stringify(ogVal)
+ }
+
+ return val
+ }),
+ parse: (value: string) =>
+ JSON.parse(value, function parser(key, val) {
+ const ogVal = this[key]
+ if (isPlainObject(ogVal)) {
+ const serializer = serializers.find((t) => t.parseCondition(ogVal))
+
+ if (serializer) {
+ return serializer.parse(ogVal)
+ }
+ }
+
+ return val
+ }),
+ encode: (value: any) => {
+ // When encoding, dive first
+ if (Array.isArray(value)) {
+ return value.map((v) => startSerializer.encode(v))
+ }
+
+ if (isPlainObject(value)) {
+ return Object.fromEntries(
+ Object.entries(value).map(([key, v]) => [
+ key,
+ startSerializer.encode(v),
+ ]),
+ )
+ }
+
+ const serializer = serializers.find((t) => t.stringifyCondition(value))
+ if (serializer) {
+ return serializer.stringify(value)
+ }
+
+ return value
+ },
+ decode: (value: any) => {
+ // Attempt transform first
+ if (isPlainObject(value)) {
+ const serializer = serializers.find((t) => t.parseCondition(value))
+ if (serializer) {
+ return serializer.parse(value)
+ }
+ }
+
+ if (Array.isArray(value)) {
+ return value.map((v) => startSerializer.decode(v))
+ }
+
+ if (isPlainObject(value)) {
+ return Object.fromEntries(
+ Object.entries(value).map(([key, v]) => [
+ key,
+ startSerializer.decode(v),
+ ]),
+ )
+ }
+
+ return value
+ },
+}
+
+const createSerializer = (
+ key: TKey,
+ check: (value: any) => value is TInput,
+ toValue: (value: TInput) => TSerialized,
+ fromValue: (value: TSerialized) => TInput,
+) => ({
+ key,
+ stringifyCondition: check,
+ stringify: (value: any) => ({ [`$${key}`]: toValue(value) }),
+ parseCondition: (value: any) => Object.hasOwn(value, `$${key}`),
+ parse: (value: any) => fromValue(value[`$${key}`]),
+})
+
+// Keep these ordered by predicted frequency
+// Make sure to keep DefaultSerializable in sync with these serializers
+// Also, make sure that they are unit tested in serializer.test.tsx
+const serializers = [
+ createSerializer(
+ // Key
+ 'undefined',
+ // Check
+ (v): v is undefined => v === undefined,
+ // To
+ () => 0,
+ // From
+ () => undefined,
+ ),
+ createSerializer(
+ // Key
+ 'date',
+ // Check
+ (v): v is Date => v instanceof Date,
+ // To
+ (v) => v.toISOString(),
+ // From
+ (v) => new Date(v),
+ ),
+ createSerializer(
+ // Key
+ 'error',
+ // Check
+ (v): v is Error => v instanceof Error,
+ // To
+ (v) => ({
+ ...v,
+ message: v.message,
+ stack: process.env.NODE_ENV === 'development' ? v.stack : undefined,
+ cause: v.cause,
+ }),
+ // From
+ (v) => Object.assign(new Error(v.message), v),
+ ),
+ createSerializer(
+ // Key
+ 'formData',
+ // Check
+ (v): v is FormData => v instanceof FormData,
+ // To
+ (v) => {
+ const entries: Record<
+ string,
+ Array | FormDataEntryValue
+ > = {}
+ v.forEach((value, key) => {
+ const entry = entries[key]
+ if (entry !== undefined) {
+ if (Array.isArray(entry)) {
+ entry.push(value)
+ } else {
+ entries[key] = [entry, value]
+ }
+ } else {
+ entries[key] = value
+ }
+ })
+ return entries
+ },
+ // From
+ (v) => {
+ const formData = new FormData()
+ Object.entries(v).forEach(([key, value]) => {
+ if (Array.isArray(value)) {
+ value.forEach((val) => formData.append(key, val))
+ } else {
+ formData.append(key, value)
+ }
+ })
+ return formData
+ },
+ ),
+ createSerializer(
+ // Key
+ 'bigint',
+ // Check
+ (v): v is bigint => typeof v === 'bigint',
+ // To
+ (v) => v.toString(),
+ // From
+ (v) => BigInt(v),
+ ),
+] as const
diff --git a/packages/solid-start-client/src/ssr-client.tsx b/packages/solid-start-client/src/ssr-client.tsx
new file mode 100644
index 0000000000..d9bc984d59
--- /dev/null
+++ b/packages/solid-start-client/src/ssr-client.tsx
@@ -0,0 +1,246 @@
+import { isPlainObject } from '@tanstack/router-core'
+
+import invariant from 'tiny-invariant'
+
+import { startSerializer } from './serializer'
+import type {
+ AnyRouter,
+ ControllablePromise,
+ MakeRouteMatch,
+} from '@tanstack/solid-router'
+
+import type {
+ DeferredPromiseState,
+ Manifest,
+ RouteContextOptions,
+} from '@tanstack/router-core'
+
+declare global {
+ interface Window {
+ __TSR_SSR__?: StartSsrGlobal
+ }
+}
+
+export interface StartSsrGlobal {
+ matches: Array
+ streamedValues: Record<
+ string,
+ {
+ value: any
+ parsed: any
+ }
+ >
+ cleanScripts: () => void
+ dehydrated?: any
+ initMatch: (match: SsrMatch) => void
+ resolvePromise: (opts: {
+ matchId: string
+ id: number
+ promiseState: DeferredPromiseState
+ }) => void
+ injectChunk: (opts: { matchId: string; id: number; chunk: string }) => void
+ closeStream: (opts: { matchId: string; id: number }) => void
+}
+
+export interface SsrMatch {
+ id: string
+ __beforeLoadContext: string
+ loaderData?: string
+ error?: string
+ extracted?: Array
+ updatedAt: MakeRouteMatch['updatedAt']
+ status: MakeRouteMatch['status']
+}
+
+export type ClientExtractedEntry =
+ | ClientExtractedStream
+ | ClientExtractedPromise
+
+export interface ClientExtractedPromise extends ClientExtractedBaseEntry {
+ type: 'promise'
+ value?: ControllablePromise
+}
+
+export interface ClientExtractedStream extends ClientExtractedBaseEntry {
+ type: 'stream'
+ value?: ReadableStream & { controller?: ReadableStreamDefaultController }
+}
+
+export interface ClientExtractedBaseEntry {
+ type: string
+ path: Array
+}
+
+export interface ResolvePromiseState {
+ matchId: string
+ id: number
+ promiseState: DeferredPromiseState
+}
+
+export interface DehydratedRouter {
+ manifest: Manifest | undefined
+ dehydratedData: any
+}
+
+export function hydrate(router: AnyRouter) {
+ invariant(
+ window.__TSR_SSR__?.dehydrated,
+ 'Expected to find a dehydrated data on window.__TSR_SSR__.dehydrated... but we did not. Please file an issue!',
+ )
+
+ const { manifest, dehydratedData } = startSerializer.parse(
+ window.__TSR_SSR__.dehydrated,
+ ) as DehydratedRouter
+
+ router.ssr = {
+ manifest,
+ serializer: startSerializer,
+ }
+
+ router.clientSsr = {
+ getStreamedValue: (key: string): T | undefined => {
+ if (router.isServer) {
+ return undefined
+ }
+
+ const streamedValue = window.__TSR_SSR__?.streamedValues[key]
+
+ if (!streamedValue) {
+ return
+ }
+
+ if (!streamedValue.parsed) {
+ streamedValue.parsed = router.ssr!.serializer.parse(streamedValue.value)
+ }
+
+ return streamedValue.parsed
+ },
+ }
+
+ // Hydrate the router state
+ const matches = router.matchRoutes(router.state.location)
+ // kick off loading the route chunks
+ const routeChunkPromise = Promise.all(
+ matches.map((match) => {
+ const route = router.looseRoutesById[match.routeId]!
+ return router.loadRouteChunk(route)
+ }),
+ )
+ // Right after hydration and before the first render, we need to rehydrate each match
+ // First step is to reyhdrate loaderData and __beforeLoadContext
+ matches.forEach((match) => {
+ const dehydratedMatch = window.__TSR_SSR__!.matches.find(
+ (d) => d.id === match.id,
+ )
+
+ if (dehydratedMatch) {
+ Object.assign(match, dehydratedMatch)
+
+ // Handle beforeLoadContext
+ if (dehydratedMatch.__beforeLoadContext) {
+ match.__beforeLoadContext = router.ssr!.serializer.parse(
+ dehydratedMatch.__beforeLoadContext,
+ ) as any
+ }
+
+ // Handle loaderData
+ if (dehydratedMatch.loaderData) {
+ match.loaderData = router.ssr!.serializer.parse(
+ dehydratedMatch.loaderData,
+ )
+ }
+
+ // Handle error
+ if (dehydratedMatch.error) {
+ match.error = router.ssr!.serializer.parse(dehydratedMatch.error)
+ }
+
+ // Handle extracted
+ ;(match as unknown as SsrMatch).extracted?.forEach((ex) => {
+ deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value)
+ })
+ } else {
+ Object.assign(match, {
+ status: 'success',
+ updatedAt: Date.now(),
+ })
+ }
+
+ return match
+ })
+
+ router.__store.setState((s) => {
+ return {
+ ...s,
+ matches,
+ }
+ })
+
+ // Allow the user to handle custom hydration data
+ router.options.hydrate?.(dehydratedData)
+
+ // now that all necessary data is hydrated:
+ // 1) fully reconstruct the route context
+ // 2) execute `head()` and `scripts()` for each match
+ router.state.matches.forEach((match) => {
+ const route = router.looseRoutesById[match.routeId]!
+
+ const parentMatch = router.state.matches[match.index - 1]
+ const parentContext = parentMatch?.context ?? router.options.context ?? {}
+
+ // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
+ // so run it again and merge route context
+ const contextFnContext: RouteContextOptions = {
+ deps: match.loaderDeps,
+ params: match.params,
+ context: parentContext,
+ location: router.state.location,
+ navigate: (opts: any) =>
+ router.navigate({ ...opts, _fromLocation: router.state.location }),
+ buildLocation: router.buildLocation,
+ cause: match.cause,
+ abortController: match.abortController,
+ preload: false,
+ matches,
+ }
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
+
+ match.context = {
+ ...parentContext,
+ ...match.__routeContext,
+ ...match.__beforeLoadContext,
+ }
+
+ const assetContext = {
+ matches: router.state.matches,
+ match,
+ params: match.params,
+ loaderData: match.loaderData,
+ }
+ const headFnContent = route.options.head?.(assetContext)
+
+ const scripts = route.options.scripts?.(assetContext)
+
+ match.meta = headFnContent?.meta
+ match.links = headFnContent?.links
+ match.headScripts = headFnContent?.scripts
+ match.scripts = scripts
+ })
+
+ return routeChunkPromise
+}
+
+function deepMutableSetByPath(obj: T, path: Array, value: any) {
+ // mutable set by path retaining array and object references
+ if (path.length === 1) {
+ ;(obj as any)[path[0]!] = value
+ }
+
+ const [key, ...rest] = path
+
+ if (Array.isArray(obj)) {
+ deepMutableSetByPath(obj[Number(key)], rest, value)
+ } else if (isPlainObject(obj)) {
+ deepMutableSetByPath((obj as any)[key!], rest, value)
+ }
+}
diff --git a/packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts b/packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts
new file mode 100644
index 0000000000..89f427d8c6
--- /dev/null
+++ b/packages/solid-start-client/src/tests/createIsomorphicFn.test-d.ts
@@ -0,0 +1,72 @@
+import { expectTypeOf, test } from 'vitest'
+import { createIsomorphicFn } from '../createIsomorphicFn'
+
+test('createIsomorphicFn with no implementations', () => {
+ const fn = createIsomorphicFn()
+
+ expectTypeOf(fn).toBeCallableWith()
+ expectTypeOf(fn).returns.toBeUndefined()
+
+ expectTypeOf(fn).toHaveProperty('server')
+ expectTypeOf(fn).toHaveProperty('client')
+})
+
+test('createIsomorphicFn with server implementation', () => {
+ const fn = createIsomorphicFn().server(() => 'data')
+
+ expectTypeOf(fn).toBeCallableWith()
+ expectTypeOf(fn).returns.toEqualTypeOf()
+
+ expectTypeOf(fn).toHaveProperty('client')
+ expectTypeOf(fn).not.toHaveProperty('server')
+})
+
+test('createIsomorphicFn with client implementation', () => {
+ const fn = createIsomorphicFn().client(() => 'data')
+
+ expectTypeOf(fn).toBeCallableWith()
+ expectTypeOf(fn).returns.toEqualTypeOf()
+
+ expectTypeOf(fn).toHaveProperty('server')
+ expectTypeOf(fn).not.toHaveProperty('client')
+})
+
+test('createIsomorphicFn with server and client implementation', () => {
+ const fn = createIsomorphicFn()
+ .server(() => 'data')
+ .client(() => 'data')
+
+ expectTypeOf(fn).toBeCallableWith()
+ expectTypeOf(fn).returns.toEqualTypeOf()
+
+ expectTypeOf(fn).not.toHaveProperty('server')
+ expectTypeOf(fn).not.toHaveProperty('client')
+})
+
+test('createIsomorphicFn with varying returns', () => {
+ const fn = createIsomorphicFn()
+ .server(() => 'data')
+ .client(() => 1)
+ expectTypeOf(fn).toBeCallableWith()
+ expectTypeOf(fn).returns.toEqualTypeOf()
+})
+
+test('createIsomorphicFn with arguments', () => {
+ const fn = createIsomorphicFn()
+ .server((a: number, b: string) => 'data')
+ .client((...args) => {
+ expectTypeOf(args).toEqualTypeOf<[number, string]>()
+ return 1
+ })
+ expectTypeOf(fn).toBeCallableWith(1, 'a')
+ expectTypeOf(fn).returns.toEqualTypeOf()
+
+ const fn2 = createIsomorphicFn()
+ .client((a: number, b: string) => 'data')
+ .server((...args) => {
+ expectTypeOf(args).toEqualTypeOf<[number, string]>()
+ return 1
+ })
+ expectTypeOf(fn2).toBeCallableWith(1, 'a')
+ expectTypeOf(fn2).returns.toEqualTypeOf()
+})
diff --git a/packages/solid-start-client/src/tests/createServerFn.test-d.tsx b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx
new file mode 100644
index 0000000000..372c0a456b
--- /dev/null
+++ b/packages/solid-start-client/src/tests/createServerFn.test-d.tsx
@@ -0,0 +1,501 @@
+import { describe, expectTypeOf, test } from 'vitest'
+import { createServerFn } from '../createServerFn'
+import { createMiddleware } from '../createMiddleware'
+import type { Constrain, Validator } from '@tanstack/router-core'
+
+test('createServerFn method with autocomplete', () => {
+ createServerFn().handler((options) => {
+ expectTypeOf(options.method).toEqualTypeOf<'GET' | 'POST'>()
+ })
+})
+
+test('createServerFn without middleware', () => {
+ expectTypeOf(createServerFn()).toHaveProperty('handler')
+ expectTypeOf(createServerFn()).toHaveProperty('middleware')
+ expectTypeOf(createServerFn()).toHaveProperty('validator')
+
+ createServerFn({ method: 'GET' }).handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: undefined
+ signal: AbortSignal
+ response: 'data'
+ }>()
+ })
+})
+
+test('createServerFn with validator', () => {
+ const fnAfterValidator = createServerFn({
+ method: 'GET',
+ }).validator((input: { input: string }) => ({
+ a: input.input,
+ }))
+
+ expectTypeOf(fnAfterValidator).toHaveProperty('handler')
+ expectTypeOf(fnAfterValidator).toHaveProperty('middleware')
+ expectTypeOf(fnAfterValidator).not.toHaveProperty('validator')
+
+ const fn = fnAfterValidator.handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: {
+ a: string
+ }
+ signal: AbortSignal
+ response: 'data'
+ }>()
+ })
+
+ expectTypeOf(fn).parameter(0).toEqualTypeOf<{
+ data: { input: string }
+ headers?: HeadersInit
+ type?: 'static' | 'dynamic'
+ signal?: AbortSignal
+ }>()
+
+ expectTypeOf>().resolves.toEqualTypeOf()
+})
+
+test('createServerFn with middleware and context', () => {
+ const middleware1 = createMiddleware().server(({ next }) => {
+ return next({ context: { a: 'a' } as const })
+ })
+
+ const middleware2 = createMiddleware().server(({ next }) => {
+ return next({ context: { b: 'b' } as const })
+ })
+
+ const middleware3 = createMiddleware()
+ .middleware([middleware1, middleware2])
+ .client(({ next }) => {
+ return next({ context: { c: 'c' } as const })
+ })
+
+ const middleware4 = createMiddleware()
+ .middleware([middleware3])
+ .client(({ context, next }) => {
+ return next({ sendContext: context })
+ })
+ .server(({ context, next }) => {
+ expectTypeOf(context).toEqualTypeOf<{
+ readonly a: 'a'
+ readonly b: 'b'
+ readonly c: 'c'
+ }>()
+ return next({ context: { d: 'd' } as const })
+ })
+
+ const fnWithMiddleware = createServerFn({ method: 'GET' }).middleware([
+ middleware4,
+ ])
+
+ expectTypeOf(fnWithMiddleware).toHaveProperty('handler')
+ expectTypeOf(fnWithMiddleware).toHaveProperty('validator')
+ expectTypeOf(fnWithMiddleware).not.toHaveProperty('middleware')
+
+ fnWithMiddleware.handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: {
+ readonly a: 'a'
+ readonly b: 'b'
+ readonly c: 'c'
+ readonly d: 'd'
+ }
+ data: undefined
+ signal: AbortSignal
+ response: 'data'
+ }>()
+ })
+})
+
+describe('createServerFn with middleware and validator', () => {
+ const middleware1 = createMiddleware().validator(
+ (input: { readonly inputA: 'inputA' }) =>
+ ({
+ outputA: 'outputA',
+ }) as const,
+ )
+
+ const middleware2 = createMiddleware().validator(
+ (input: { readonly inputB: 'inputB' }) =>
+ ({
+ outputB: 'outputB',
+ }) as const,
+ )
+
+ const middleware3 = createMiddleware().middleware([middleware1, middleware2])
+
+ test(`response: 'data'`, () => {
+ const fn = createServerFn({ method: 'GET', response: 'data' })
+ .middleware([middleware3])
+ .validator(
+ (input: { readonly inputC: 'inputC' }) =>
+ ({
+ outputC: 'outputC',
+ }) as const,
+ )
+ .handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: {
+ readonly outputA: 'outputA'
+ readonly outputB: 'outputB'
+ readonly outputC: 'outputC'
+ }
+ signal: AbortSignal
+ response: 'data'
+ }>()
+
+ return 'some-data' as const
+ })
+
+ expectTypeOf(fn).parameter(0).toEqualTypeOf<{
+ data: {
+ readonly inputA: 'inputA'
+ readonly inputB: 'inputB'
+ readonly inputC: 'inputC'
+ }
+ headers?: HeadersInit
+ type?: 'static' | 'dynamic'
+ signal?: AbortSignal
+ }>()
+
+ expectTypeOf(fn).returns.resolves.toEqualTypeOf<'some-data'>()
+ expectTypeOf(() =>
+ fn({
+ data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' },
+ }),
+ ).returns.resolves.toEqualTypeOf<'some-data'>()
+ })
+
+ test(`response: 'full'`, () => {
+ const fn = createServerFn({ method: 'GET', response: 'full' })
+ .middleware([middleware3])
+ .validator(
+ (input: { readonly inputC: 'inputC' }) =>
+ ({
+ outputC: 'outputC',
+ }) as const,
+ )
+ .handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: {
+ readonly outputA: 'outputA'
+ readonly outputB: 'outputB'
+ readonly outputC: 'outputC'
+ }
+ signal: AbortSignal
+ response: 'full'
+ }>()
+
+ return 'some-data' as const
+ })
+
+ expectTypeOf(fn).parameter(0).toEqualTypeOf<{
+ data: {
+ readonly inputA: 'inputA'
+ readonly inputB: 'inputB'
+ readonly inputC: 'inputC'
+ }
+ headers?: HeadersInit
+ type?: 'static' | 'dynamic'
+ signal?: AbortSignal
+ }>()
+
+ expectTypeOf(() =>
+ fn({
+ data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' },
+ }),
+ ).returns.resolves.toEqualTypeOf<{
+ result: 'some-data'
+ context: undefined
+ error: unknown
+ }>()
+ })
+})
+
+test('createServerFn overrides properties', () => {
+ const middleware1 = createMiddleware()
+ .validator(
+ () =>
+ ({
+ input: 'a' as 'a' | 'b' | 'c',
+ }) as const,
+ )
+ .client(({ context, next }) => {
+ expectTypeOf(context).toEqualTypeOf()
+
+ const newContext = { context: 'a' } as const
+ return next({ sendContext: newContext, context: newContext })
+ })
+ .server(({ data, context, next }) => {
+ expectTypeOf(data).toEqualTypeOf<{ readonly input: 'a' | 'b' | 'c' }>()
+
+ expectTypeOf(context).toEqualTypeOf<{
+ readonly context: 'a'
+ }>()
+
+ const newContext = { context: 'b' } as const
+ return next({ sendContext: newContext, context: newContext })
+ })
+
+ const middleware2 = createMiddleware()
+ .middleware([middleware1])
+ .validator(
+ () =>
+ ({
+ input: 'b' as 'b' | 'c',
+ }) as const,
+ )
+ .client(({ context, next }) => {
+ expectTypeOf(context).toEqualTypeOf<{ readonly context: 'a' }>()
+
+ const newContext = { context: 'aa' } as const
+
+ return next({ sendContext: newContext, context: newContext })
+ })
+ .server(({ context, next }) => {
+ expectTypeOf(context).toEqualTypeOf<{ readonly context: 'aa' }>()
+
+ const newContext = { context: 'bb' } as const
+
+ return next({ sendContext: newContext, context: newContext })
+ })
+
+ createServerFn()
+ .middleware([middleware2])
+ .validator(
+ () =>
+ ({
+ input: 'c',
+ }) as const,
+ )
+ .handler(({ data, context }) => {
+ expectTypeOf(data).toEqualTypeOf<{
+ readonly input: 'c'
+ }>()
+ expectTypeOf(context).toEqualTypeOf<{ readonly context: 'bb' }>()
+ })
+})
+
+test('createServerFn where validator is a primitive', () => {
+ createServerFn({ method: 'GET' })
+ .validator(() => 'c' as const)
+ .handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: 'c'
+ signal: AbortSignal
+ response: 'data'
+ }>()
+ })
+})
+
+test('createServerFn where validator is optional if object is optional', () => {
+ const fn = createServerFn({ method: 'GET' })
+ .validator((input: 'c' | undefined) => input)
+ .handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: 'c' | undefined
+ signal: AbortSignal
+ response: 'data'
+ }>()
+ })
+
+ expectTypeOf(fn).parameter(0).toEqualTypeOf<
+ | {
+ data?: 'c' | undefined
+ headers?: HeadersInit
+ type?: 'static' | 'dynamic'
+ signal?: AbortSignal
+ }
+ | undefined
+ >()
+
+ expectTypeOf>().resolves.toEqualTypeOf()
+})
+
+test('createServerFn where data is optional if there is no validator', () => {
+ const fn = createServerFn({ method: 'GET' }).handler((options) => {
+ expectTypeOf(options).toEqualTypeOf<{
+ method: 'GET'
+ context: undefined
+ data: undefined
+ signal: AbortSignal
+ response: 'data'
+ }>()
+ })
+
+ expectTypeOf(fn).parameter(0).toEqualTypeOf<
+ | {
+ data?: undefined
+ headers?: HeadersInit
+ type?: 'static' | 'dynamic'
+ signal?: AbortSignal
+ }
+ | undefined
+ >()
+
+ expectTypeOf>().resolves.toEqualTypeOf()
+})
+
+test('createServerFn returns Date', () => {
+ const fn = createServerFn().handler(() => ({
+ dates: [new Date(), new Date()] as const,
+ }))
+
+ expectTypeOf(fn()).toEqualTypeOf>()
+})
+
+// test('createServerFn returns RSC', () => {
+// const fn = createServerFn().handler(() => ({
+// rscs: [
+// I'm an RSC
,
+// I'm an RSC
,
+// ] as const,
+// }))
+
+// expectTypeOf(fn()).toEqualTypeOf<
+// Promise<{ rscs: readonly [ReadableStream, ReadableStream] }>
+// >()
+// })
+
+test('createServerFn returns undefined', () => {
+ const fn = createServerFn().handler(() => ({
+ nothing: undefined,
+ }))
+
+ expectTypeOf(fn()).toEqualTypeOf>()
+})
+
+test('createServerFn cannot return function', () => {
+ expectTypeOf(createServerFn().handler<{ func: () => 'func' }>)
+ .parameter(0)
+ .returns.toEqualTypeOf<
+ | { func: 'Function is not serializable' }
+ | Promise<{ func: 'Function is not serializable' }>
+ >()
+})
+
+test('createServerFn cannot validate function', () => {
+ const validator = createServerFn().validator<
+ (input: { func: () => 'string' }) => { output: 'string' }
+ >
+
+ expectTypeOf(validator)
+ .parameter(0)
+ .toEqualTypeOf<
+ Constrain<
+ (input: { func: () => 'string' }) => { output: 'string' },
+ Validator<{ func: 'Function is not serializable' }, any>
+ >
+ >()
+})
+
+test('createServerFn can validate Date', () => {
+ const validator = createServerFn().validator<
+ (input: Date) => { output: 'string' }
+ >
+
+ expectTypeOf(validator)
+ .parameter(0)
+ .toEqualTypeOf<
+ Constrain<(input: Date) => { output: 'string' }, Validator>
+ >()
+})
+
+test('createServerFn can validate FormData', () => {
+ const validator = createServerFn().validator<
+ (input: FormData) => { output: 'string' }
+ >
+
+ expectTypeOf(validator)
+ .parameter(0)
+ .toEqualTypeOf<
+ Constrain<
+ (input: FormData) => { output: 'string' },
+ Validator
+ >
+ >()
+})
+
+describe('response', () => {
+ describe('data', () => {
+ test(`response: 'data' is passed into handler without response being set`, () => {
+ createServerFn().handler((options) => {
+ expectTypeOf(options.response).toEqualTypeOf<'data'>()
+ })
+ })
+
+ test(`response: 'data' is passed into handler with explicit response: 'data'`, () => {
+ createServerFn({ response: 'data' }).handler((options) => {
+ expectTypeOf(options.response).toEqualTypeOf<'data'>()
+ })
+ })
+ })
+ describe('full', () => {
+ test(`response: 'full' is passed into handler`, () => {
+ createServerFn({ response: 'full' }).handler((options) => {
+ expectTypeOf(options.response).toEqualTypeOf<'full'>()
+ })
+ })
+ })
+
+ describe('raw', () => {
+ test(`response: 'raw' is passed into handler`, () => {
+ createServerFn({ response: 'raw' }).handler((options) => {
+ expectTypeOf(options.response).toEqualTypeOf<'raw'>()
+ return null
+ })
+ })
+ })
+ test(`client receives Response when Response is returned`, () => {
+ const fn = createServerFn({ response: 'raw' }).handler(() => {
+ return new Response('Hello World')
+ })
+
+ expectTypeOf(fn()).toEqualTypeOf>()
+ })
+
+ test(`client receives Response when ReadableStream is returned`, () => {
+ const fn = createServerFn({ response: 'raw' }).handler(() => {
+ return new ReadableStream()
+ })
+
+ expectTypeOf(fn()).toEqualTypeOf>()
+ })
+
+ test(`client receives Response when string is returned`, () => {
+ const fn = createServerFn({ response: 'raw' }).handler(() => {
+ return 'hello'
+ })
+
+ expectTypeOf(fn()).toEqualTypeOf>()
+ })
+})
+
+test('createServerFn can be used as a mutation function', () => {
+ const serverFn = createServerFn()
+ .validator((data: number) => data)
+ .handler(() => 'foo')
+
+ type MutationFunction = (
+ variables: TVariables,
+ ) => Promise
+
+ // simplifeid "clone" of @tansctack/react-query's useMutation
+ const useMutation = (
+ fn: MutationFunction,
+ ) => {}
+
+ useMutation(serverFn)
+})
diff --git a/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts b/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts
new file mode 100644
index 0000000000..22caa76e3d
--- /dev/null
+++ b/packages/solid-start-client/src/tests/createServerMiddleware.test-d.ts
@@ -0,0 +1,611 @@
+import { expectTypeOf, test } from 'vitest'
+import { createMiddleware } from '../createMiddleware'
+import type { Constrain, Validator } from '@tanstack/router-core'
+
+test('createServeMiddleware removes middleware after middleware,', () => {
+ const middleware = createMiddleware()
+
+ expectTypeOf(middleware).toHaveProperty('middleware')
+ expectTypeOf(middleware).toHaveProperty('server')
+ expectTypeOf(middleware).toHaveProperty('validator')
+
+ const middlewareAfterMiddleware = middleware.middleware([])
+
+ expectTypeOf(middlewareAfterMiddleware).toHaveProperty('validator')
+ expectTypeOf(middlewareAfterMiddleware).toHaveProperty('server')
+ expectTypeOf(middlewareAfterMiddleware).not.toHaveProperty('middleware')
+
+ const middlewareAfterInput = middleware.validator(() => {})
+
+ expectTypeOf(middlewareAfterInput).toHaveProperty('server')
+ expectTypeOf(middlewareAfterInput).not.toHaveProperty('middleware')
+
+ const middlewareAfterServer = middleware.server(async (options) => {
+ expectTypeOf(options.context).toEqualTypeOf()
+ expectTypeOf(options.data).toEqualTypeOf