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 ( +
+ +
+ + {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.

} +
+

+ + + 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 ( + + ) +} 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 ( +
+
I'm a layout
+
+ +
+
+ ) +} 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()}
+
+ +
+ + ) +} 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 test +

+
+ + Link to /posts + +
+
+ + Link 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: () => ( +
+

+ redirect test with server functions (target {Route.useParams()().target} + ) +

+
+ + 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 ( +
+ +
+ + {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.

} +
+

+ + + 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 ( + <> +