diff --git a/.github/labeler.yml b/.github/labeler.yml index adb8101911c..9134329a053 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -32,6 +32,8 @@ - 'packages/router-vite-plugin/**/*' 'package: server-functions-plugin': - 'packages/server-functions-plugin/**/*' +'package: solid-router': + - 'packages/solid-router/**/*' 'package: start': - 'packages/start/**/*' 'package: start-api-routes': diff --git a/e2e/solid-router/basic-esbuild-file-based/.gitignore b/e2e/solid-router/basic-esbuild-file-based/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/basic-esbuild-file-based/index.html b/e2e/solid-router/basic-esbuild-file-based/index.html new file mode 100644 index 00000000000..b67448091c2 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/index.html @@ -0,0 +1,24 @@ + + + + + + Vite App + + + + +
+ + + diff --git a/e2e/solid-router/basic-esbuild-file-based/package.json b/e2e/solid-router/basic-esbuild-file-based/package.json new file mode 100644 index 00000000000..41a61af0ea6 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/package.json @@ -0,0 +1,26 @@ +{ + "name": "tanstack-router-e2e-solid-basic-esbuild-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "esbuild src/main.tsx --jsx=preserve --jsx-import-source=solid-js --serve=5601 --bundle --outfile=dist/main.js --watch --servedir=.", + "build": "esbuild src/main.tsx --jsx=preserve --jsx-import-source=solid-js --bundle --outfile=dist/main.js && tsc --noEmit", + "serve": "esbuild src/main.tsx --jsx=preserve --jsx-import-source=solid-js --bundle --outfile=dist/main.js --servedir=.", + "start": "dev", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/router-plugin": "workspace:^", + "@tanstack/solid-router": "workspace:^", + "@tanstack/zod-adapter": "workspace:^", + "redaxios": "^0.5.1", + "solid-js": "^1.9.4", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "esbuild": "^0.25.0", + "esbuild-plugin-solid": "^0.6.0" + } +} diff --git a/e2e/solid-router/basic-esbuild-file-based/playwright.config.ts b/e2e/solid-router/basic-esbuild-file-based/playwright.config.ts new file mode 100644 index 00000000000..99e8771d481 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `pnpm run build && pnpm run serve --serve=${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-esbuild-file-based/src/esbuild.config.js b/e2e/solid-router/basic-esbuild-file-based/src/esbuild.config.js new file mode 100644 index 00000000000..a361b1d4f1b --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/esbuild.config.js @@ -0,0 +1,13 @@ +import { TanStackRouterEsbuild } from '@tanstack/router-plugin/esbuild' +import { solidPlugin } from 'esbuild-plugin-solid' + +export default { + // ... + plugins: [ + TanStackRouterEsbuild({ + target: 'solid', + autoCodeSplitting: true, + }), + solidPlugin(), + ], +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/main.tsx b/e2e/solid-router/basic-esbuild-file-based/src/main.tsx new file mode 100644 index 00000000000..0eebff3ea94 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/main.tsx @@ -0,0 +1,24 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/posts.tsx b/e2e/solid-router/basic-esbuild-file-based/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-esbuild-file-based/src/routeTree.gen.ts new file mode 100644 index 00000000000..4d5b165a87c --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routeTree.gen.ts @@ -0,0 +1,424 @@ +/* prettier-ignore-start */ + +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file is auto-generated by TanStack Router + +import { createFileRoute } from '@tanstack/solid-router' + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as PostsImport } from './routes/posts' +import { Route as LayoutImport } from './routes/_layout' +import { Route as IndexImport } from './routes/index' +import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as PostsPostIdImport } from './routes/posts.$postId' +import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as groupLazyinsideImport } from './routes/(group)/lazyinside' +import { Route as groupLayoutImport } from './routes/(group)/_layout' +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 groupLayoutInsideImport } from './routes/(group)/_layout.inside' + +// Create Virtual Routes + +const groupImport = createFileRoute('/(group)')() + +// Create/Update Routes + +const groupRoute = groupImport.update({ + id: '/(group)', + getParentRoute: () => rootRoute, +} as any) + +const PostsRoute = PostsImport.update({ + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const LayoutRoute = LayoutImport.update({ + id: '/_layout', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const PostsIndexRoute = PostsIndexImport.update({ + path: '/', + getParentRoute: () => PostsRoute, +} as any) + +const PostsPostIdRoute = PostsPostIdImport.update({ + path: '/$postId', + getParentRoute: () => PostsRoute, +} as any) + +const LayoutLayout2Route = LayoutLayout2Import.update({ + id: '/_layout-2', + getParentRoute: () => LayoutRoute, +} as any) + +const groupLazyinsideRoute = groupLazyinsideImport + .update({ + path: '/lazyinside', + getParentRoute: () => groupRoute, + } as any) + .lazy(() => import('./routes/(group)/lazyinside.lazy').then((d) => d.Route)) + +const groupLayoutRoute = groupLayoutImport.update({ + id: '/_layout', + getParentRoute: () => groupRoute, +} as any) + +const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({ + path: '/layout-b', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ + path: '/layout-a', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const groupLayoutInsideRoute = groupLayoutInsideImport.update({ + path: '/inside', + getParentRoute: () => groupLayoutRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsImport + parentRoute: typeof rootRoute + } + '/(group)': { + id: '/(group)' + path: '/' + fullPath: '/' + preLoaderRoute: typeof groupImport + parentRoute: typeof rootRoute + } + '/(group)/_layout': { + id: '/(group)/_layout' + path: '/' + fullPath: '/' + preLoaderRoute: typeof groupLayoutImport + parentRoute: typeof groupRoute + } + '/(group)/lazyinside': { + id: '/(group)/lazyinside' + path: '/lazyinside' + fullPath: '/lazyinside' + preLoaderRoute: typeof groupLazyinsideImport + parentRoute: typeof groupImport + } + '/_layout/_layout-2': { + id: '/_layout/_layout-2' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutLayout2Import + parentRoute: typeof LayoutImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdImport + parentRoute: typeof PostsImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexImport + parentRoute: typeof PostsImport + } + '/(group)/_layout/inside': { + id: '/(group)/_layout/inside' + path: '/inside' + fullPath: '/inside' + preLoaderRoute: typeof groupLayoutInsideImport + parentRoute: typeof groupLayoutImport + } + '/_layout/_layout-2/layout-a': { + id: '/_layout/_layout-2/layout-a' + path: '/layout-a' + fullPath: '/layout-a' + preLoaderRoute: typeof LayoutLayout2LayoutAImport + parentRoute: typeof LayoutLayout2Import + } + '/_layout/_layout-2/layout-b': { + id: '/_layout/_layout-2/layout-b' + path: '/layout-b' + fullPath: '/layout-b' + preLoaderRoute: typeof LayoutLayout2LayoutBImport + parentRoute: typeof LayoutLayout2Import + } + } +} + +// Create and export the route tree + +interface LayoutLayout2RouteChildren { + LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute + LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute +} + +const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = { + LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute, +} + +const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren( + LayoutLayout2RouteChildren, +) + +interface LayoutRouteChildren { + LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutLayout2Route: LayoutLayout2RouteWithChildren, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface PostsRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteChildren: PostsRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) + +interface groupLayoutRouteChildren { + groupLayoutInsideRoute: typeof groupLayoutInsideRoute +} + +const groupLayoutRouteChildren: groupLayoutRouteChildren = { + groupLayoutInsideRoute: groupLayoutInsideRoute, +} + +const groupLayoutRouteWithChildren = groupLayoutRoute._addFileChildren( + groupLayoutRouteChildren, +) + +interface groupRouteChildren { + groupLayoutRoute: typeof groupLayoutRouteWithChildren + groupLazyinsideRoute: typeof groupLazyinsideRoute +} + +const groupRouteChildren: groupRouteChildren = { + groupLayoutRoute: groupLayoutRouteWithChildren, + groupLazyinsideRoute: groupLazyinsideRoute, +} + +const groupRouteWithChildren = groupRoute._addFileChildren(groupRouteChildren) + +export interface FileRoutesByFullPath { + '/': typeof groupLayoutRouteWithChildren + '': typeof LayoutLayout2RouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/lazyinside': typeof groupLazyinsideRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/inside': typeof groupLayoutInsideRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesByTo { + '/': typeof groupLayoutRouteWithChildren + '': typeof LayoutLayout2RouteWithChildren + '/lazyinside': typeof groupLazyinsideRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute + '/inside': typeof groupLayoutInsideRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/(group)': typeof groupRouteWithChildren + '/(group)/_layout': typeof groupLayoutRouteWithChildren + '/(group)/lazyinside': typeof groupLazyinsideRoute + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/(group)/_layout/inside': typeof groupLayoutInsideRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/posts' + | '/lazyinside' + | '/posts/$postId' + | '/posts/' + | '/inside' + | '/layout-a' + | '/layout-b' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/lazyinside' + | '/posts/$postId' + | '/posts' + | '/inside' + | '/layout-a' + | '/layout-b' + id: + | '__root__' + | '/' + | '/_layout' + | '/posts' + | '/(group)' + | '/(group)/_layout' + | '/(group)/lazyinside' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/posts/' + | '/(group)/_layout/inside' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren + groupRoute: typeof groupRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, + groupRoute: groupRouteWithChildren, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* prettier-ignore-end */ + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/_layout", + "/posts", + "/(group)" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/_layout": { + "filePath": "_layout.tsx", + "children": [ + "/_layout/_layout-2" + ] + }, + "/posts": { + "filePath": "posts.tsx", + "children": [ + "/posts/$postId", + "/posts/" + ] + }, + "/(group)": { + "filePath": "(group)", + "children": [ + "/(group)/_layout", + "/(group)/lazyinside" + ] + }, + "/(group)/_layout": { + "filePath": "(group)/_layout.tsx", + "parent": "/(group)", + "children": [ + "/(group)/_layout/inside" + ] + }, + "/(group)/lazyinside": { + "filePath": "(group)/lazyinside.tsx", + "parent": "/(group)" + }, + "/_layout/_layout-2": { + "filePath": "_layout/_layout-2.tsx", + "parent": "/_layout", + "children": [ + "/_layout/_layout-2/layout-a", + "/_layout/_layout-2/layout-b" + ] + }, + "/posts/$postId": { + "filePath": "posts.$postId.tsx", + "parent": "/posts" + }, + "/posts/": { + "filePath": "posts.index.tsx", + "parent": "/posts" + }, + "/(group)/_layout/inside": { + "filePath": "(group)/_layout.inside.tsx", + "parent": "/(group)/_layout" + }, + "/_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" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/_layout.inside.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/_layout.inside.tsx new file mode 100644 index 00000000000..f41c3f376f5 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/_layout.inside.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +const routeApi = getRouteApi('/(group)/_layout/inside') + +export const Route = createFileRoute('/(group)/_layout/inside')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), + component: () => { + const searchViaHook = useSearch({ from: '/(group)/_layout/inside' }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/_layout.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/_layout.tsx new file mode 100644 index 00000000000..660cfb70e67 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/_layout.tsx @@ -0,0 +1,10 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(group)/_layout')({ + component: () => ( + <> +
/(group)/_layout!
+ + + ), +}) diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/lazyinside.lazy.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/lazyinside.lazy.tsx new file mode 100644 index 00000000000..a68febeaddf --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/lazyinside.lazy.tsx @@ -0,0 +1,26 @@ +import { + createLazyFileRoute, + getRouteApi, + useSearch, +} from '@tanstack/solid-router' + +const routeApi = getRouteApi('/(group)/lazyinside') + +export const Route = createLazyFileRoute('/(group)/lazyinside')({ + component: () => { + const searchViaHook = useSearch({ from: '/(group)/lazyinside' }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/lazyinside.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/lazyinside.tsx new file mode 100644 index 00000000000..dcaf7b07270 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/(group)/lazyinside.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +export const Route = createFileRoute('/(group)/lazyinside')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), +}) diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/__root.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..fe8c40d9fa8 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/__root.tsx @@ -0,0 +1,81 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Inside Group + {' '} + + Lazy Inside Group + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + {/* */} + + ) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout.tsx new file mode 100644 index 00000000000..d43b4ef5f5e --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2.tsx new file mode 100644 index 00000000000..7a5a3623a03 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 00000000000..b69951b2465 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 00000000000..30dbcce90fa --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/index.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..55f8871d03f --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,27 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.index.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.index.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.tsx b/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.tsx new file mode 100644 index 00000000000..11a999f50aa --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/src/routes/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/basic-esbuild-file-based/tests/app.spec.ts b/e2e/solid-router/basic-esbuild-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..f9f8e888605 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/tests/app.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test.skip('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test.skip('Navigating nested layouts', async ({ page }) => { + await page.goto('/') + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test.skip('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) + +test.skip('Navigating to a route inside a route group', async ({ page }) => { + await page.getByTestId('link-to-route-inside-group').click() + await expect(page.getByTestId('search-via-hook')).toContainText('world') + await expect(page.getByTestId('search-via-route-hook')).toContainText('world') + await expect(page.getByTestId('search-via-route-api')).toContainText('world') +}) + +test.skip('Navigating to a lazy route inside a route group', async ({ + page, +}) => { + await page.getByTestId('link-to-lazy-route-inside-group').click() + await expect(page.getByTestId('search-via-hook')).toContainText('world') + await expect(page.getByTestId('search-via-route-hook')).toContainText('world') + await expect(page.getByTestId('search-via-route-api')).toContainText('world') +}) diff --git a/e2e/solid-router/basic-esbuild-file-based/tsconfig.json b/e2e/solid-router/basic-esbuild-file-based/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-esbuild-file-based/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/.gitignore b/e2e/solid-router/basic-file-based-code-splitting/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/basic-file-based-code-splitting/index.html b/e2e/solid-router/basic-file-based-code-splitting/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-file-based-code-splitting/package.json b/e2e/solid-router/basic-file-based-code-splitting/package.json new file mode 100644 index 00000000000..65021e57d02 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-e2e-solid-basic-file-based-code-splitting", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/router-plugin": "workspace:^", + "@tanstack/solid-router": "workspace:^", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "solid-js": "^1.9.4", + "tailwindcss": "^3.4.17", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite": "^6.1.0", + "vite-plugin-solid": "^2.11.2" + } +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/playwright.config.ts b/e2e/solid-router/basic-file-based-code-splitting/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-file-based-code-splitting/postcss.config.mjs b/e2e/solid-router/basic-file-based-code-splitting/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/main.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/main.tsx new file mode 100644 index 00000000000..8128384193c --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/main.tsx @@ -0,0 +1,25 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/posts.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routeTree.gen.ts b/e2e/solid-router/basic-file-based-code-splitting/src/routeTree.gen.ts new file mode 100644 index 00000000000..b7e80e980bd --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routeTree.gen.ts @@ -0,0 +1,293 @@ +/* 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 WithoutLoaderImport } from './routes/without-loader' +import { Route as ViewportTestImport } from './routes/viewport-test' +import { Route as PostsImport } from './routes/posts' +import { Route as LayoutImport } from './routes/_layout' +import { Route as IndexImport } from './routes/index' +import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as PostsPostIdImport } from './routes/posts.$postId' +import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b' +import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a' + +// Create/Update Routes + +const WithoutLoaderRoute = WithoutLoaderImport.update({ + id: '/without-loader', + path: '/without-loader', + getParentRoute: () => rootRoute, +} as any) + +const ViewportTestRoute = ViewportTestImport.update({ + id: '/viewport-test', + path: '/viewport-test', + getParentRoute: () => rootRoute, +} as any) + +const PostsRoute = PostsImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const LayoutRoute = LayoutImport.update({ + id: '/_layout', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const PostsIndexRoute = PostsIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRoute, +} as any) + +const PostsPostIdRoute = PostsPostIdImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => PostsRoute, +} as any) + +const LayoutLayout2Route = LayoutLayout2Import.update({ + id: '/_layout-2', + getParentRoute: () => LayoutRoute, +} as any) + +const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({ + id: '/layout-b', + path: '/layout-b', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ + id: '/layout-a', + path: '/layout-a', + getParentRoute: () => LayoutLayout2Route, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsImport + parentRoute: typeof rootRoute + } + '/viewport-test': { + id: '/viewport-test' + path: '/viewport-test' + fullPath: '/viewport-test' + preLoaderRoute: typeof ViewportTestImport + parentRoute: typeof rootRoute + } + '/without-loader': { + id: '/without-loader' + path: '/without-loader' + fullPath: '/without-loader' + preLoaderRoute: typeof WithoutLoaderImport + parentRoute: typeof rootRoute + } + '/_layout/_layout-2': { + id: '/_layout/_layout-2' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutLayout2Import + parentRoute: typeof LayoutImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdImport + parentRoute: typeof PostsImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexImport + parentRoute: typeof PostsImport + } + '/_layout/_layout-2/layout-a': { + id: '/_layout/_layout-2/layout-a' + path: '/layout-a' + fullPath: '/layout-a' + preLoaderRoute: typeof LayoutLayout2LayoutAImport + parentRoute: typeof LayoutLayout2Import + } + '/_layout/_layout-2/layout-b': { + id: '/_layout/_layout-2/layout-b' + path: '/layout-b' + fullPath: '/layout-b' + preLoaderRoute: typeof LayoutLayout2LayoutBImport + parentRoute: typeof LayoutLayout2Import + } + } +} + +// Create and export the route tree + +interface LayoutLayout2RouteChildren { + LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute + LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute +} + +const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = { + LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute, +} + +const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren( + LayoutLayout2RouteChildren, +) + +interface LayoutRouteChildren { + LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutLayout2Route: LayoutLayout2RouteWithChildren, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface PostsRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteChildren: PostsRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/viewport-test': typeof ViewportTestRoute + '/without-loader': typeof WithoutLoaderRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/viewport-test': typeof ViewportTestRoute + '/without-loader': typeof WithoutLoaderRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/viewport-test': typeof ViewportTestRoute + '/without-loader': typeof WithoutLoaderRoute + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/posts' + | '/viewport-test' + | '/without-loader' + | '/posts/$postId' + | '/posts/' + | '/layout-a' + | '/layout-b' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/viewport-test' + | '/without-loader' + | '/posts/$postId' + | '/posts' + | '/layout-a' + | '/layout-b' + id: + | '__root__' + | '/' + | '/_layout' + | '/posts' + | '/viewport-test' + | '/without-loader' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/posts/' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren + ViewportTestRoute: typeof ViewportTestRoute + WithoutLoaderRoute: typeof WithoutLoaderRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, + ViewportTestRoute: ViewportTestRoute, + WithoutLoaderRoute: WithoutLoaderRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/__root.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/__root.tsx new file mode 100644 index 00000000000..8222611c99f --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/__root.tsx @@ -0,0 +1,79 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + without-loader + {' '} + + This Route Does Not Exist + +
+
+
+ + viewport-test + + + {/* Start rendering router matches */} + {/* */} + + ) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout.tsx new file mode 100644 index 00000000000..d43b4ef5f5e --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2.tsx new file mode 100644 index 00000000000..7a5a3623a03 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 00000000000..b69951b2465 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 00000000000..30dbcce90fa --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/index.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/index.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.$postId.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..105f08d64e1 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.$postId.tsx @@ -0,0 +1,31 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, + codeSplitGroupings: [ + ['component'], + ['pendingComponent', 'errorComponent', 'notFoundComponent'], + ], +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.index.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.index.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.tsx new file mode 100644 index 00000000000..11a999f50aa --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/viewport-test.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/viewport-test.tsx new file mode 100644 index 00000000000..22dd8fd282c --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/viewport-test.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/viewport-test')({ + component: () =>
Hello /viewport-test!
, +}) diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/routes/without-loader.tsx b/e2e/solid-router/basic-file-based-code-splitting/src/routes/without-loader.tsx new file mode 100644 index 00000000000..1506595204d --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/routes/without-loader.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/without-loader')({ + component: () =>
Hello /without-loader!
, +}) diff --git a/e2e/solid-router/basic-file-based-code-splitting/src/styles.css b/e2e/solid-router/basic-file-based-code-splitting/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/tailwind.config.mjs b/e2e/solid-router/basic-file-based-code-splitting/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/tests/app.spec.ts b/e2e/solid-router/basic-file-based-code-splitting/tests/app.spec.ts new file mode 100644 index 00000000000..3e5a69ccaae --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/basic-file-based-code-splitting/tests/preload.spec.ts b/e2e/solid-router/basic-file-based-code-splitting/tests/preload.spec.ts new file mode 100644 index 00000000000..0bc3265760a --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/tests/preload.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('hovering a link with preload=intent to a route without a loader should preload route', async ({ + page, +}) => { + await page.waitForLoadState('networkidle') + + const requestPromise = new Promise((resolve) => { + page.on('request', (request) => { + resolve(request.url()) + }) + }) + + await page.getByRole('link', { name: 'without-loader' }).hover() + const url = await requestPromise + const expectedString = + process.env.NODE_ENV === 'development' + ? 'without-loader.tsx?tsr-split' + : '/assets/without-loader' + expect(url).toContain(expectedString) +}) + +test('scrolling into viewport a link with preload=viewport to a route should preload route', async ({ + page, +}) => { + await page.waitForLoadState('networkidle') + + const [request] = await Promise.all([ + page.waitForRequest(() => true), + page.getByRole('link', { name: 'viewport-test' }).scrollIntoViewIfNeeded(), + ]) + + const expectedString = + process.env.NODE_ENV === 'development' + ? 'viewport-test.tsx?tsr-split' + : '/assets/viewport-test' + expect(request.url()).toEqual(expect.stringContaining(expectedString)) +}) diff --git a/e2e/solid-router/basic-file-based-code-splitting/tsconfig.json b/e2e/solid-router/basic-file-based-code-splitting/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-file-based-code-splitting/vite.config.ts b/e2e/solid-router/basic-file-based-code-splitting/vite.config.ts new file mode 100644 index 00000000000..df642f32229 --- /dev/null +++ b/e2e/solid-router/basic-file-based-code-splitting/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + TanStackRouterVite({ + target: 'solid', + autoCodeSplitting: true, + codeSplittingOptions: { + splitBehavior: ({ routeId }) => { + if (routeId === '/posts') { + return [ + ['loader'], + ['component'], + ['pendingComponent', 'notFoundComponent', 'errorComponent'], + ] + } + }, + }, + }), + solid(), + ], +}) diff --git a/e2e/solid-router/basic-file-based/.gitignore b/e2e/solid-router/basic-file-based/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/basic-file-based/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/basic-file-based/index.html b/e2e/solid-router/basic-file-based/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-file-based/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-file-based/package.json b/e2e/solid-router/basic-file-based/package.json new file mode 100644 index 00000000000..edfe47e4d84 --- /dev/null +++ b/e2e/solid-router/basic-file-based/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-router-e2e-solid-basic-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/zod-adapter": "workspace:^", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "solid-js": "^1.9.4", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "combinate": "^1.1.11", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic-file-based/playwright.config.ts b/e2e/solid-router/basic-file-based/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-file-based/postcss.config.mjs b/e2e/solid-router/basic-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-file-based/src/main.tsx b/e2e/solid-router/basic-file-based/src/main.tsx new file mode 100644 index 00000000000..fc12c765703 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/main.tsx @@ -0,0 +1,25 @@ +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic-file-based/src/posts.tsx b/e2e/solid-router/basic-file-based/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts new file mode 100644 index 00000000000..45a5a6df76c --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts @@ -0,0 +1,692 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { createFileRoute } from '@tanstack/solid-router' + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as PostsImport } from './routes/posts' +import { Route as EditingBImport } from './routes/editing-b' +import { Route as EditingAImport } from './routes/editing-a' +import { Route as AnchorImport } from './routes/anchor' +import { Route as LayoutImport } from './routes/_layout' +import { Route as IndexImport } from './routes/index' +import { Route as RedirectIndexImport } from './routes/redirect/index' +import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as RedirectTargetImport } from './routes/redirect/$target' +import { Route as PostsPostIdImport } from './routes/posts.$postId' +import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as groupLazyinsideImport } from './routes/(group)/lazyinside' +import { Route as groupInsideImport } from './routes/(group)/inside' +import { Route as groupLayoutImport } from './routes/(group)/_layout' +import { Route as anotherGroupOnlyrouteinsideImport } from './routes/(another-group)/onlyrouteinside' +import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index' +import { Route as RedirectPreloadThirdImport } from './routes/redirect/preload/third' +import { Route as RedirectPreloadSecondImport } from './routes/redirect/preload/second' +import { Route as RedirectPreloadFirstImport } from './routes/redirect/preload/first' +import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader' +import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad' +import { Route as PostsPostIdEditImport } from './routes/posts_.$postId.edit' +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 groupSubfolderInsideImport } from './routes/(group)/subfolder/inside' +import { Route as groupLayoutInsidelayoutImport } from './routes/(group)/_layout.insidelayout' + +// Create Virtual Routes + +const groupImport = createFileRoute('/(group)')() + +// Create/Update Routes + +const groupRoute = groupImport.update({ + id: '/(group)', + getParentRoute: () => rootRoute, +} as any) + +const PostsRoute = PostsImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const EditingBRoute = EditingBImport.update({ + id: '/editing-b', + path: '/editing-b', + getParentRoute: () => rootRoute, +} as any) + +const EditingARoute = EditingAImport.update({ + id: '/editing-a', + path: '/editing-a', + getParentRoute: () => rootRoute, +} as any) + +const AnchorRoute = AnchorImport.update({ + id: '/anchor', + path: '/anchor', + getParentRoute: () => rootRoute, +} as any) + +const LayoutRoute = LayoutImport.update({ + id: '/_layout', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const RedirectIndexRoute = RedirectIndexImport.update({ + id: '/redirect/', + path: '/redirect/', + getParentRoute: () => rootRoute, +} as any) + +const PostsIndexRoute = PostsIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRoute, +} 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 LayoutLayout2Route = LayoutLayout2Import.update({ + id: '/_layout-2', + getParentRoute: () => LayoutRoute, +} as any) + +const groupLazyinsideRoute = groupLazyinsideImport + .update({ + id: '/lazyinside', + path: '/lazyinside', + getParentRoute: () => groupRoute, + } as any) + .lazy(() => import('./routes/(group)/lazyinside.lazy').then((d) => d.Route)) + +const groupInsideRoute = groupInsideImport.update({ + id: '/inside', + path: '/inside', + getParentRoute: () => groupRoute, +} as any) + +const groupLayoutRoute = groupLayoutImport.update({ + id: '/_layout', + getParentRoute: () => groupRoute, +} as any) + +const anotherGroupOnlyrouteinsideRoute = + anotherGroupOnlyrouteinsideImport.update({ + id: '/(another-group)/onlyrouteinside', + path: '/onlyrouteinside', + getParentRoute: () => rootRoute, + } as any) + +const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectPreloadThirdRoute = RedirectPreloadThirdImport.update({ + id: '/redirect/preload/third', + path: '/redirect/preload/third', + getParentRoute: () => rootRoute, +} as any) + +const RedirectPreloadSecondRoute = RedirectPreloadSecondImport.update({ + id: '/redirect/preload/second', + path: '/redirect/preload/second', + getParentRoute: () => rootRoute, +} as any) + +const RedirectPreloadFirstRoute = RedirectPreloadFirstImport.update({ + id: '/redirect/preload/first', + path: '/redirect/preload/first', + getParentRoute: () => rootRoute, +} 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 PostsPostIdEditRoute = PostsPostIdEditImport.update({ + id: '/posts_/$postId/edit', + path: '/posts/$postId/edit', + 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 groupSubfolderInsideRoute = groupSubfolderInsideImport.update({ + id: '/subfolder/inside', + path: '/subfolder/inside', + getParentRoute: () => groupRoute, +} as any) + +const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutImport.update({ + id: '/insidelayout', + path: '/insidelayout', + getParentRoute: () => groupLayoutRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport + parentRoute: typeof rootRoute + } + '/anchor': { + id: '/anchor' + path: '/anchor' + fullPath: '/anchor' + preLoaderRoute: typeof AnchorImport + parentRoute: typeof rootRoute + } + '/editing-a': { + id: '/editing-a' + path: '/editing-a' + fullPath: '/editing-a' + preLoaderRoute: typeof EditingAImport + parentRoute: typeof rootRoute + } + '/editing-b': { + id: '/editing-b' + path: '/editing-b' + fullPath: '/editing-b' + preLoaderRoute: typeof EditingBImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsImport + parentRoute: typeof rootRoute + } + '/(another-group)/onlyrouteinside': { + id: '/(another-group)/onlyrouteinside' + path: '/onlyrouteinside' + fullPath: '/onlyrouteinside' + preLoaderRoute: typeof anotherGroupOnlyrouteinsideImport + parentRoute: typeof rootRoute + } + '/(group)': { + id: '/(group)' + path: '/' + fullPath: '/' + preLoaderRoute: typeof groupImport + parentRoute: typeof rootRoute + } + '/(group)/_layout': { + id: '/(group)/_layout' + path: '/' + fullPath: '/' + preLoaderRoute: typeof groupLayoutImport + parentRoute: typeof groupRoute + } + '/(group)/inside': { + id: '/(group)/inside' + path: '/inside' + fullPath: '/inside' + preLoaderRoute: typeof groupInsideImport + parentRoute: typeof groupImport + } + '/(group)/lazyinside': { + id: '/(group)/lazyinside' + path: '/lazyinside' + fullPath: '/lazyinside' + preLoaderRoute: typeof groupLazyinsideImport + parentRoute: typeof groupImport + } + '/_layout/_layout-2': { + id: '/_layout/_layout-2' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutLayout2Import + parentRoute: typeof LayoutImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdImport + parentRoute: typeof PostsImport + } + '/redirect/$target': { + id: '/redirect/$target' + path: '/redirect/$target' + fullPath: '/redirect/$target' + preLoaderRoute: typeof RedirectTargetImport + parentRoute: typeof rootRoute + } + '/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 + } + '/(group)/_layout/insidelayout': { + id: '/(group)/_layout/insidelayout' + path: '/insidelayout' + fullPath: '/insidelayout' + preLoaderRoute: typeof groupLayoutInsidelayoutImport + parentRoute: typeof groupLayoutImport + } + '/(group)/subfolder/inside': { + id: '/(group)/subfolder/inside' + path: '/subfolder/inside' + fullPath: '/subfolder/inside' + preLoaderRoute: typeof groupSubfolderInsideImport + parentRoute: typeof groupImport + } + '/_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/edit': { + id: '/posts_/$postId/edit' + path: '/posts/$postId/edit' + fullPath: '/posts/$postId/edit' + preLoaderRoute: typeof PostsPostIdEditImport + 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/preload/first': { + id: '/redirect/preload/first' + path: '/redirect/preload/first' + fullPath: '/redirect/preload/first' + preLoaderRoute: typeof RedirectPreloadFirstImport + parentRoute: typeof rootRoute + } + '/redirect/preload/second': { + id: '/redirect/preload/second' + path: '/redirect/preload/second' + fullPath: '/redirect/preload/second' + preLoaderRoute: typeof RedirectPreloadSecondImport + parentRoute: typeof rootRoute + } + '/redirect/preload/third': { + id: '/redirect/preload/third' + path: '/redirect/preload/third' + fullPath: '/redirect/preload/third' + preLoaderRoute: typeof RedirectPreloadThirdImport + parentRoute: typeof rootRoute + } + '/redirect/$target/': { + id: '/redirect/$target/' + path: '/' + fullPath: '/redirect/$target/' + preLoaderRoute: typeof RedirectTargetIndexImport + parentRoute: typeof RedirectTargetImport + } + } +} + +// Create and export the route tree + +interface LayoutLayout2RouteChildren { + LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute + LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute +} + +const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = { + LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute, +} + +const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren( + LayoutLayout2RouteChildren, +) + +interface LayoutRouteChildren { + LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutLayout2Route: LayoutLayout2RouteWithChildren, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface PostsRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteChildren: PostsRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) + +interface groupLayoutRouteChildren { + groupLayoutInsidelayoutRoute: typeof groupLayoutInsidelayoutRoute +} + +const groupLayoutRouteChildren: groupLayoutRouteChildren = { + groupLayoutInsidelayoutRoute: groupLayoutInsidelayoutRoute, +} + +const groupLayoutRouteWithChildren = groupLayoutRoute._addFileChildren( + groupLayoutRouteChildren, +) + +interface groupRouteChildren { + groupLayoutRoute: typeof groupLayoutRouteWithChildren + groupInsideRoute: typeof groupInsideRoute + groupLazyinsideRoute: typeof groupLazyinsideRoute + groupSubfolderInsideRoute: typeof groupSubfolderInsideRoute +} + +const groupRouteChildren: groupRouteChildren = { + groupLayoutRoute: groupLayoutRouteWithChildren, + groupInsideRoute: groupInsideRoute, + groupLazyinsideRoute: groupLazyinsideRoute, + groupSubfolderInsideRoute: groupSubfolderInsideRoute, +} + +const groupRouteWithChildren = groupRoute._addFileChildren(groupRouteChildren) + +interface RedirectTargetRouteChildren { + RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute + RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute + RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute +} + +const RedirectTargetRouteChildren: RedirectTargetRouteChildren = { + RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute, + RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute, + RedirectTargetIndexRoute: RedirectTargetIndexRoute, +} + +const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren( + RedirectTargetRouteChildren, +) + +export interface FileRoutesByFullPath { + '/': typeof groupLayoutRouteWithChildren + '': typeof LayoutLayout2RouteWithChildren + '/anchor': typeof AnchorRoute + '/editing-a': typeof EditingARoute + '/editing-b': typeof EditingBRoute + '/posts': typeof PostsRouteWithChildren + '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute + '/inside': typeof groupInsideRoute + '/lazyinside': typeof groupLazyinsideRoute + '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren + '/posts/': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute + '/insidelayout': typeof groupLayoutInsidelayoutRoute + '/subfolder/inside': typeof groupSubfolderInsideRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts/$postId/edit': typeof PostsPostIdEditRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/preload/first': typeof RedirectPreloadFirstRoute + '/redirect/preload/second': typeof RedirectPreloadSecondRoute + '/redirect/preload/third': typeof RedirectPreloadThirdRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute +} + +export interface FileRoutesByTo { + '/': typeof groupLayoutRouteWithChildren + '': typeof LayoutLayout2RouteWithChildren + '/anchor': typeof AnchorRoute + '/editing-a': typeof EditingARoute + '/editing-b': typeof EditingBRoute + '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute + '/inside': typeof groupInsideRoute + '/lazyinside': typeof groupLazyinsideRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute + '/insidelayout': typeof groupLayoutInsidelayoutRoute + '/subfolder/inside': typeof groupSubfolderInsideRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts/$postId/edit': typeof PostsPostIdEditRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/preload/first': typeof RedirectPreloadFirstRoute + '/redirect/preload/second': typeof RedirectPreloadSecondRoute + '/redirect/preload/third': typeof RedirectPreloadThirdRoute + '/redirect/$target': typeof RedirectTargetIndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/anchor': typeof AnchorRoute + '/editing-a': typeof EditingARoute + '/editing-b': typeof EditingBRoute + '/posts': typeof PostsRouteWithChildren + '/(another-group)/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute + '/(group)': typeof groupRouteWithChildren + '/(group)/_layout': typeof groupLayoutRouteWithChildren + '/(group)/inside': typeof groupInsideRoute + '/(group)/lazyinside': typeof groupLazyinsideRoute + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren + '/posts/': typeof PostsIndexRoute + '/redirect/': typeof RedirectIndexRoute + '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute + '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts_/$postId/edit': typeof PostsPostIdEditRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/preload/first': typeof RedirectPreloadFirstRoute + '/redirect/preload/second': typeof RedirectPreloadSecondRoute + '/redirect/preload/third': typeof RedirectPreloadThirdRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/anchor' + | '/editing-a' + | '/editing-b' + | '/posts' + | '/onlyrouteinside' + | '/inside' + | '/lazyinside' + | '/posts/$postId' + | '/redirect/$target' + | '/posts/' + | '/redirect' + | '/insidelayout' + | '/subfolder/inside' + | '/layout-a' + | '/layout-b' + | '/posts/$postId/edit' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/preload/first' + | '/redirect/preload/second' + | '/redirect/preload/third' + | '/redirect/$target/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/anchor' + | '/editing-a' + | '/editing-b' + | '/onlyrouteinside' + | '/inside' + | '/lazyinside' + | '/posts/$postId' + | '/posts' + | '/redirect' + | '/insidelayout' + | '/subfolder/inside' + | '/layout-a' + | '/layout-b' + | '/posts/$postId/edit' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/preload/first' + | '/redirect/preload/second' + | '/redirect/preload/third' + | '/redirect/$target' + id: + | '__root__' + | '/' + | '/_layout' + | '/anchor' + | '/editing-a' + | '/editing-b' + | '/posts' + | '/(another-group)/onlyrouteinside' + | '/(group)' + | '/(group)/_layout' + | '/(group)/inside' + | '/(group)/lazyinside' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/redirect/$target' + | '/posts/' + | '/redirect/' + | '/(group)/_layout/insidelayout' + | '/(group)/subfolder/inside' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + | '/posts_/$postId/edit' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/preload/first' + | '/redirect/preload/second' + | '/redirect/preload/third' + | '/redirect/$target/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + AnchorRoute: typeof AnchorRoute + EditingARoute: typeof EditingARoute + EditingBRoute: typeof EditingBRoute + PostsRoute: typeof PostsRouteWithChildren + anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute + groupRoute: typeof groupRouteWithChildren + RedirectTargetRoute: typeof RedirectTargetRouteWithChildren + RedirectIndexRoute: typeof RedirectIndexRoute + PostsPostIdEditRoute: typeof PostsPostIdEditRoute + RedirectPreloadFirstRoute: typeof RedirectPreloadFirstRoute + RedirectPreloadSecondRoute: typeof RedirectPreloadSecondRoute + RedirectPreloadThirdRoute: typeof RedirectPreloadThirdRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + AnchorRoute: AnchorRoute, + EditingARoute: EditingARoute, + EditingBRoute: EditingBRoute, + PostsRoute: PostsRouteWithChildren, + anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute, + groupRoute: groupRouteWithChildren, + RedirectTargetRoute: RedirectTargetRouteWithChildren, + RedirectIndexRoute: RedirectIndexRoute, + PostsPostIdEditRoute: PostsPostIdEditRoute, + RedirectPreloadFirstRoute: RedirectPreloadFirstRoute, + RedirectPreloadSecondRoute: RedirectPreloadSecondRoute, + RedirectPreloadThirdRoute: RedirectPreloadThirdRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-router/basic-file-based/src/routes/(another-group)/onlyrouteinside.tsx b/e2e/solid-router/basic-file-based/src/routes/(another-group)/onlyrouteinside.tsx new file mode 100644 index 00000000000..5dfc82fc334 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(another-group)/onlyrouteinside.tsx @@ -0,0 +1,27 @@ +import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +const routeApi = getRouteApi('/(another-group)/onlyrouteinside') + +export const Route = createFileRoute('/(another-group)/onlyrouteinside')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), + component: () => { + const searchViaHook = useSearch({ + from: '/(another-group)/onlyrouteinside', + }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.insidelayout.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.insidelayout.tsx new file mode 100644 index 00000000000..dd06459a357 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.insidelayout.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +const routeApi = getRouteApi('/(group)/_layout/insidelayout') + +export const Route = createFileRoute('/(group)/_layout/insidelayout')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), + component: () => { + const searchViaHook = useSearch({ from: '/(group)/_layout/insidelayout' }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.tsx new file mode 100644 index 00000000000..660cfb70e67 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(group)/_layout.tsx @@ -0,0 +1,10 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(group)/_layout')({ + component: () => ( + <> +
/(group)/_layout!
+ + + ), +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/inside.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/inside.tsx new file mode 100644 index 00000000000..76ed0e8a07c --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(group)/inside.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +const routeApi = getRouteApi('/(group)/inside') + +export const Route = createFileRoute('/(group)/inside')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), + component: () => { + const searchViaHook = useSearch({ from: '/(group)/inside' }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.lazy.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.lazy.tsx new file mode 100644 index 00000000000..a68febeaddf --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.lazy.tsx @@ -0,0 +1,26 @@ +import { + createLazyFileRoute, + getRouteApi, + useSearch, +} from '@tanstack/solid-router' + +const routeApi = getRouteApi('/(group)/lazyinside') + +export const Route = createLazyFileRoute('/(group)/lazyinside')({ + component: () => { + const searchViaHook = useSearch({ from: '/(group)/lazyinside' }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.tsx new file mode 100644 index 00000000000..dcaf7b07270 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(group)/lazyinside.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +export const Route = createFileRoute('/(group)/lazyinside')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/(group)/subfolder/inside.tsx b/e2e/solid-router/basic-file-based/src/routes/(group)/subfolder/inside.tsx new file mode 100644 index 00000000000..9e963d170c6 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/(group)/subfolder/inside.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, getRouteApi, useSearch } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +const routeApi = getRouteApi('/(group)/subfolder/inside') + +export const Route = createFileRoute('/(group)/subfolder/inside')({ + validateSearch: zodValidator(z.object({ hello: z.string().optional() })), + component: () => { + const searchViaHook = useSearch({ from: '/(group)/subfolder/inside' }) + const searchViaRouteHook = Route.useSearch() + const searchViaRouteApi = routeApi.useSearch() + return ( + <> +
{searchViaHook().hello}
+
+ {searchViaRouteHook().hello} +
+
+ {searchViaRouteApi().hello} +
+ + ) + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/__root.tsx b/e2e/solid-router/basic-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..8dca5f31041 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/__root.tsx @@ -0,0 +1,136 @@ +import { + Link, + Outlet, + createRootRoute, + useCanGoBack, + useRouter, +} from '@tanstack/solid-router' +// // import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + const router = useRouter() + const canGoBack = useCanGoBack() + + return ( + <> +
+ {' '} + + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Only Route Inside Group + {' '} + + Inside Group + {' '} + + Inside Subfolder Inside Group + {' '} + + Inside Group Inside Layout + {' '} + + Lazy Inside Group + {' '} + + redirect + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + {/* {/* */} + + ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout.tsx new file mode 100644 index 00000000000..d43b4ef5f5e --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2.tsx new file mode 100644 index 00000000000..7a5a3623a03 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 00000000000..b69951b2465 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 00000000000..30dbcce90fa --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/basic-file-based/src/routes/anchor.tsx b/e2e/solid-router/basic-file-based/src/routes/anchor.tsx new file mode 100644 index 00000000000..77c0e5a6de4 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/anchor.tsx @@ -0,0 +1,226 @@ +import { + Link, + createFileRoute, + useLocation, + useNavigate, +} from '@tanstack/solid-router' +import { + createEffect, + createRenderEffect, + createSignal, + onCleanup, +} from 'solid-js' + +export const Route = createFileRoute('/anchor')({ + component: AnchorComponent, +}) + +const anchors: Array<{ + id: string + title: string + hashScrollIntoView?: boolean | ScrollIntoViewOptions +}> = [ + { + id: 'default-anchor', + title: 'Default Anchor', + }, + { + id: 'false-anchor', + title: 'No Scroll Into View', + hashScrollIntoView: false, + }, + { + id: 'smooth-scroll', + title: 'Smooth Scroll', + hashScrollIntoView: { behavior: 'smooth' }, + }, +] as const + +function AnchorSection({ id, title }: { id: string; title: string }) { + const [hasShown, setHasShown] = createSignal(false) + let elementRef: HTMLHeadingElement | null = null + + createEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (!hasShown() && entry.isIntersecting) { + setHasShown(true) + } + }, + { threshold: 0.01 }, + ) + + const currentRef = elementRef + if (currentRef) { + observer.observe(currentRef) + } + + onCleanup(() => { + if (currentRef) { + observer.unobserve(currentRef) + } + }) + }) + + return ( +
+

+ {`${title} ${hasShown() ? '(shown)' : ''}`} +

+
+ ) +} + +function AnchorComponent() { + const navigate = useNavigate() + const location = useLocation() + const [withScroll, setWithScroll] = createSignal(true) + + return ( +
+ +
+
{ + event.preventDefault() + event.stopPropagation() + const formData = new FormData(event.target as HTMLFormElement) + + const toHash = formData.get('hash') as string + + if (!toHash) { + return + } + + const hashScrollIntoView = withScroll() + ? ({ + behavior: formData.get('scrollBehavior') as ScrollBehavior, + block: formData.get('scrollBlock') as ScrollLogicalPosition, + inline: formData.get('scrollInline') as ScrollLogicalPosition, + } satisfies ScrollIntoViewOptions) + : false + + navigate({ hash: toHash, hashScrollIntoView }) + }} + > +

Scroll with navigate

+
+ +
+ +
+
+ {withScroll() ? ( + <> +
+ +
+ +
+ +
+ +
+ +
+ + ) : null} +
+ +
+
+ + {anchors.map((anchor) => ( + + ))} +
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/editing-a.tsx b/e2e/solid-router/basic-file-based/src/routes/editing-a.tsx new file mode 100644 index 00000000000..e8931690250 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/editing-a.tsx @@ -0,0 +1,45 @@ +import { createFileRoute, useBlocker } from '@tanstack/solid-router' +import { createSignal } from 'solid-js' + +export const Route = createFileRoute('/editing-a')({ + component: RouteComponent, +}) + +function RouteComponent() { + const navigate = Route.useNavigate() + const [input, setInput] = createSignal('') + + const blocker = useBlocker({ + shouldBlockFn: ({ next }) => { + if (next.fullPath === '/editing-b' && input().length > 0) { + return true + } + return false + }, + withResolver: true, + }) + + return ( +
+

Editing A

+ + + {blocker().status === 'blocked' && ( + + )} +
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/editing-b.tsx b/e2e/solid-router/basic-file-based/src/routes/editing-b.tsx new file mode 100644 index 00000000000..3dc2d1655f3 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/editing-b.tsx @@ -0,0 +1,41 @@ +import { createFileRoute, useBlocker } from '@tanstack/solid-router' +import { createEffect, createSignal, createMemo } from 'solid-js' + +export const Route = createFileRoute('/editing-b')({ + component: RouteComponent, +}) + +function RouteComponent() { + const navigate = Route.useNavigate() + const [input, setInput] = createSignal('') + + const blocker = createMemo(() => + useBlocker({ + condition: input(), + }), + ) + + return ( +
+

Editing B

+ + + {blocker()().status === 'blocked' && ( + + )} +
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/index.tsx b/e2e/solid-router/basic-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/basic-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..a48895f3391 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,29 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

+ {post().title} +

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/posts.index.tsx b/e2e/solid-router/basic-file-based/src/routes/posts.index.tsx new file mode 100644 index 00000000000..c7d8cfe19c7 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/basic-file-based/src/routes/posts.tsx b/e2e/solid-router/basic-file-based/src/routes/posts.tsx new file mode 100644 index 00000000000..fd42972ee9a --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/posts_.$postId.edit.tsx b/e2e/solid-router/basic-file-based/src/routes/posts_.$postId.edit.tsx new file mode 100644 index 00000000000..f38f369b7a5 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/posts_.$postId.edit.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, getRouteApi, useParams } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts_/$postId/edit')({ + component: PostEditPage, +}) + +const api = getRouteApi('/posts_/$postId/edit') + +function PostEditPage() { + const paramsViaApi = api.useParams() + const paramsViaHook = useParams({ from: '/posts_/$postId/edit' }) + const paramsViaRouteHook = Route.useParams() + + return ( + <> +
{paramsViaHook().postId}
+
+ {paramsViaRouteHook().postId} +
+
{paramsViaApi().postId}
+ + ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target.tsx new file mode 100644 index 00000000000..525dd9da254 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target.tsx @@ -0,0 +1,21 @@ +import { createFileRoute, retainSearchParams } from '@tanstack/solid-router' +import z from 'zod' + +export const Route = createFileRoute('/redirect/$target')({ + params: { + parse: (p) => + z + .object({ + target: z.union([z.literal('internal'), z.literal('external')]), + }) + .parse(p), + }, + validateSearch: z.object({ + reloadDocument: z.boolean().optional(), + preload: z.literal(false).optional(), + externalHost: z.string().optional(), + }), + search: { + middlewares: [retainSearchParams(['externalHost'])], + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target/index.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/index.tsx new file mode 100644 index 00000000000..a44008e3c6b --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/index.tsx @@ -0,0 +1,65 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/')({ + component: () => { + const preload = Route.useSearch({ select: (s) => s.preload }) + return ( +
+
+ + via-beforeLoad + +
+
+ + via-beforeLoad (reloadDocument=true) + +
+
+ + via-loader + +
+
+ + via-loader (reloadDocument=true) + +
+
+ ) + }, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx new file mode 100644 index 00000000000..c88cc079864 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({ + beforeLoad: ({ + params: { target }, + search: { reloadDocument, externalHost }, + }) => { + switch (target) { + case 'internal': + throw redirect({ to: '/posts', reloadDocument }) + case 'external': + const href = externalHost ?? 'http://example.com' + throw redirect({ href }) + } + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx new file mode 100644 index 00000000000..5c059717c5c --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx @@ -0,0 +1,18 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/$target/via-loader')({ + loaderDeps: ({ search: { reloadDocument, externalHost } }) => ({ + reloadDocument, + externalHost, + }), + loader: ({ params: { target }, deps: { externalHost, reloadDocument } }) => { + switch (target) { + case 'internal': + throw redirect({ to: '/posts', reloadDocument }) + case 'external': + const href = externalHost ?? 'http://example.com' + throw redirect({ href }) + } + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/index.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/index.tsx new file mode 100644 index 00000000000..043f305e574 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/index.tsx @@ -0,0 +1,28 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/')({ + component: () => ( +
+ + internal + {' '} + + external + +
+ ), +}) diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/preload/first.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/first.tsx new file mode 100644 index 00000000000..68752e5a238 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/first.tsx @@ -0,0 +1,23 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/preload/first')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+ + go to second + +
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/preload/second.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/second.tsx new file mode 100644 index 00000000000..0ecd74ef80c --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/second.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/preload/second')({ + loader: async () => { + await new Promise((r) => setTimeout(r, 1000)) + throw redirect({ from: Route.fullPath, to: '../third' }) + }, + component: RouteComponent, + pendingComponent: () =>

second pending

, +}) + +function RouteComponent() { + return
Hello "/redirect/preload/second"!
+} diff --git a/e2e/solid-router/basic-file-based/src/routes/redirect/preload/third.tsx b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/third.tsx new file mode 100644 index 00000000000..e831276d29a --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/redirect/preload/third.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/redirect/preload/third')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/redirect/preload/third"!
+} diff --git a/e2e/solid-router/basic-file-based/src/styles.css b/e2e/solid-router/basic-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-file-based/tailwind.config.mjs b/e2e/solid-router/basic-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-file-based/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..fd2627c9378 --- /dev/null +++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts @@ -0,0 +1,255 @@ +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) + +test("useBlocker doesn't block navigation if condition is not met", async ({ + page, +}) => { + await page.goto('/editing-a') + await expect(page.getByRole('heading')).toContainText('Editing A') + + await page.getByRole('button', { name: 'Go to next step' }).click() + await expect(page.getByRole('heading')).toContainText('Editing B') +}) + +test('useBlocker does block navigation if condition is met', async ({ + page, +}) => { + await page.goto('/editing-a') + await expect(page.getByRole('heading')).toContainText('Editing A') + + await page.getByLabel('Enter your name:').fill('foo') + + await page.getByRole('button', { name: 'Go to next step' }).click() + await expect(page.getByRole('heading')).toContainText('Editing A') + + await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible() +}) + +test('Proceeding through blocked navigation works', async ({ page }) => { + await page.goto('/editing-a') + await expect(page.getByRole('heading')).toContainText('Editing A') + + await page.getByLabel('Enter your name:').fill('foo') + + await page.getByRole('button', { name: 'Go to next step' }).click() + await expect(page.getByRole('heading')).toContainText('Editing A') + + await page.getByRole('button', { name: 'Proceed' }).click() + await expect(page.getByRole('heading')).toContainText('Editing B') +}) + +test("legacy useBlocker doesn't block navigation if condition is not met", async ({ + page, +}) => { + await page.goto('/editing-b') + await expect(page.getByRole('heading')).toContainText('Editing B') + + await page.getByRole('button', { name: 'Go back' }).click() + await expect(page.getByRole('heading')).toContainText('Editing A') +}) + +test('legacy useBlocker does block navigation if condition is met', async ({ + page, +}) => { + await page.goto('/editing-b') + await expect(page.getByRole('heading')).toContainText('Editing B') + + await page.getByLabel('Enter your name:').fill('foo') + + await page.getByRole('button', { name: 'Go back' }).click() + await expect(page.getByRole('heading')).toContainText('Editing B') + + await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible() +}) + +test('legacy Proceeding through blocked navigation works', async ({ page }) => { + await page.goto('/editing-b') + await expect(page.getByRole('heading')).toContainText('Editing B') + + await page.getByLabel('Enter your name:').fill('foo') + + await page.getByRole('button', { name: 'Go back' }).click() + await expect(page.getByRole('heading')).toContainText('Editing B') + + await page.getByRole('button', { name: 'Proceed' }).click() + await expect(page.getByRole('heading')).toContainText('Editing A') +}) + +test('useCanGoBack correctly disables back button', async ({ page }) => { + const getBackButtonDisabled = async () => { + const backButton = page.getByTestId('back-button') + const isDisabled = (await backButton.getAttribute('disabled')) !== null + return isDisabled + } + + expect(await getBackButtonDisabled()).toBe(true) + + await page.getByRole('link', { name: 'Posts' }).click() + await expect(page.getByTestId('posts-links')).toBeInViewport() + expect(await getBackButtonDisabled()).toBe(false) + + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByTestId('post-title')).toBeInViewport() + expect(await getBackButtonDisabled()).toBe(false) + + await page.reload() + expect(await getBackButtonDisabled()).toBe(false) + + await page.goBack() + expect(await getBackButtonDisabled()).toBe(false) + + await page.goForward() + expect(await getBackButtonDisabled()).toBe(false) + + await page.goBack() + expect(await getBackButtonDisabled()).toBe(false) + + await page.goBack() + expect(await getBackButtonDisabled()).toBe(true) + + await page.reload() + expect(await getBackButtonDisabled()).toBe(true) +}) + +test('useCanGoBack correctly disables back button, using router.history and window.history', async ({ + page, +}) => { + const getBackButtonDisabled = async () => { + const backButton = page.getByTestId('back-button') + const isDisabled = (await backButton.getAttribute('disabled')) !== null + return isDisabled + } + + await page.getByRole('link', { name: 'Posts' }).click() + await expect(page.getByTestId('posts-links')).toBeInViewport() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByTestId('post-title')).toBeInViewport() + await page.getByTestId('back-button').click() + expect(await getBackButtonDisabled()).toBe(false) + + await page.reload() + expect(await getBackButtonDisabled()).toBe(false) + + await page.getByTestId('back-button').click() + expect(await getBackButtonDisabled()).toBe(true) + + await page.evaluate('window.history.forward()') + expect(await getBackButtonDisabled()).toBe(false) + + await page.evaluate('window.history.forward()') + expect(await getBackButtonDisabled()).toBe(false) + + await page.evaluate('window.history.back()') + expect(await getBackButtonDisabled()).toBe(false) + + await page.evaluate('window.history.back()') + expect(await getBackButtonDisabled()).toBe(true) + + await page.reload() + expect(await getBackButtonDisabled()).toBe(true) +}) + +const testCases = [ + { + description: 'Navigating to a route inside a route group', + testId: 'link-to-route-inside-group', + }, + { + description: + 'Navigating to a route inside a subfolder inside a route group ', + testId: 'link-to-route-inside-group-inside-subfolder', + }, + { + description: 'Navigating to a route inside a route group inside a layout', + testId: 'link-to-route-inside-group-inside-layout', + }, + { + description: 'Navigating to a lazy route inside a route group', + testId: 'link-to-lazy-route-inside-group', + }, + + { + description: 'Navigating to the only route inside a route group ', + testId: 'link-to-only-route-inside-group', + }, +] + +testCases.forEach(({ description, testId }) => { + test(description, async ({ page }) => { + await page.getByTestId(testId).click() + await expect(page.getByTestId('search-via-hook')).toContainText('world') + await expect(page.getByTestId('search-via-route-hook')).toContainText( + 'world', + ) + await expect(page.getByTestId('search-via-route-api')).toContainText( + 'world', + ) + }) +}) + +test('navigating to an unnested route', async ({ page }) => { + const postId = 'hello-world' + page.goto(`/posts/${postId}/edit`) + await expect(page.getByTestId('params-via-hook')).toContainText(postId) + await expect(page.getByTestId('params-via-route-hook')).toContainText(postId) + await expect(page.getByTestId('params-via-route-api')).toContainText(postId) +}) + +async function getRenderCount(page: Page) { + const renderCount = parseInt( + await page.getByTestId('render-count').innerText(), + ) + return renderCount +} +async function structuralSharingTest(page: Page, enabled: boolean) { + page.goto(`/structural-sharing/${enabled}/?foo=f1&bar=b1`) + await expect(page.getByTestId('enabled')).toHaveText(JSON.stringify(enabled)) + + async function checkSearch({ foo, bar }: { foo: string; bar: string }) { + expect(page.url().endsWith(`?foo=${foo}&bar=${bar}`)).toBe(true) + const expectedSearch = JSON.stringify({ values: [foo, bar] }) + await expect(page.getByTestId('search-via-hook')).toHaveText(expectedSearch) + await expect(page.getByTestId('search-via-route-hook')).toHaveText( + expectedSearch, + ) + await expect(page.getByTestId('search-via-route-api-hook')).toHaveText( + expectedSearch, + ) + } + + await checkSearch({ bar: 'b1', foo: 'f1' }) + await page.getByTestId('link').click() + await checkSearch({ bar: 'b2', foo: 'f2' }) +} diff --git a/e2e/solid-router/basic-file-based/tests/redirect.spec.ts b/e2e/solid-router/basic-file-based/tests/redirect.spec.ts new file mode 100644 index 00000000000..7026b047019 --- /dev/null +++ b/e2e/solid-router/basic-file-based/tests/redirect.spec.ts @@ -0,0 +1,137 @@ +import { expect, test } from '@playwright/test' +import combinateImport from 'combinate' +import { derivePort, localDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../package.json' with { type: 'json' } +import { Server } from 'node:http' +import queryString from 'node:querystring' + +// somehow playwright does not correctly import default exports +const combinate = (combinateImport as any).default as typeof combinateImport + +const PORT = derivePort(packageJson.name) +const EXTERNAL_HOST_PORT = derivePort(`${packageJson.name}-external`) + +test.describe('redirects', () => { + let server: Server + test.beforeAll(async () => { + server = await localDummyServer(EXTERNAL_HOST_PORT) + }) + test.afterAll(async () => { + server.close() + }) + + const internalNavigationTestMatrix = combinate({ + thrower: ['beforeLoad', 'loader'] as const, + reloadDocument: [false, true] as const, + preload: [false, true] as const, + }) + + internalNavigationTestMatrix.forEach( + ({ thrower, reloadDocument, preload }) => { + test(`internal target, navigation: thrower: ${thrower}, reloadDocument: ${reloadDocument}, preload: ${preload}`, async ({ + page, + }) => { + await page.waitForLoadState('networkidle') + await page.goto( + `/redirect/internal${preload === false ? '?preload=false' : ''}`, + ) + const link = page.getByTestId( + `via-${thrower}${reloadDocument ? '-reloadDocument' : ''}`, + ) + + await page.waitForLoadState('networkidle') + let requestHappened = false + + const requestPromise = new Promise((resolve) => { + page.on('request', (request) => { + if ( + request.url() === 'https://jsonplaceholder.typicode.com/posts' + ) { + requestHappened = true + resolve() + } + }) + }) + await link.focus() + + const expectRequestHappened = preload && !reloadDocument + const timeoutPromise = new Promise((resolve) => + setTimeout(resolve, expectRequestHappened ? 5000 : 500), + ) + await Promise.race([requestPromise, timeoutPromise]) + expect(requestHappened).toBe(expectRequestHappened) + await link.click() + let fullPageLoad = false + page.on('domcontentloaded', () => { + fullPageLoad = true + }) + + const url = `http://localhost:${PORT}/posts` + + await page.waitForURL(url) + expect(page.url()).toBe(url) + await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport() + expect(fullPageLoad).toBe(reloadDocument) + }) + }, + ) + + const internalDirectVisitTestMatrix = combinate({ + thrower: ['beforeLoad', 'loader'] as const, + reloadDocument: [false, true] as const, + }) + + internalDirectVisitTestMatrix.forEach(({ thrower, reloadDocument }) => { + test(`internal target, direct visit: thrower: ${thrower}, reloadDocument: ${reloadDocument}`, async ({ + page, + }) => { + await page.waitForLoadState('networkidle') + + await page.goto(`/redirect/internal/via-${thrower}`) + + const url = `http://localhost:${PORT}/posts` + + await page.waitForURL(url) + expect(page.url()).toBe(url) + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('PostsIndexComponent')).toBeInViewport() + }) + }) + + const externalTestMatrix = combinate({ + scenario: ['navigate', 'direct_visit'] as const, + thrower: ['beforeLoad', 'loader'] as const, + }) + + externalTestMatrix.forEach(({ scenario, thrower }) => { + test(`external target: scenario: ${scenario}, thrower: ${thrower}`, async ({ + page, + }) => { + await page.waitForLoadState('networkidle') + + let q = queryString.stringify({ + externalHost: `http://localhost:${EXTERNAL_HOST_PORT}/`, + }) + if (scenario === 'navigate') { + await page.goto(`/redirect/external?${q}`) + await page.getByTestId(`via-${thrower}`).click() + } else { + await page.goto(`/redirect/external/via-${thrower}?${q}`) + } + + const url = `http://localhost:${EXTERNAL_HOST_PORT}/` + + await page.waitForURL(url) + expect(page.url()).toBe(url) + }) + }) + + test('regression test for #3097', async ({ page }) => { + await page.goto(`/redirect/preload/first`) + const link = page.getByTestId(`link`) + await link.focus() + await link.click() + await page.waitForURL('/redirect/preload/third') + await expect(page.getByTestId(`third`)).toBeInViewport() + }) +}) diff --git a/e2e/solid-router/basic-file-based/tests/scroll-into-view.spec.ts b/e2e/solid-router/basic-file-based/tests/scroll-into-view.spec.ts new file mode 100644 index 00000000000..d1af86adfb2 --- /dev/null +++ b/e2e/solid-router/basic-file-based/tests/scroll-into-view.spec.ts @@ -0,0 +1,213 @@ +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +const anchors = { + defaultAnchor: 'default-anchor', + noScrollIntoView: 'false-anchor', + smoothScroll: 'smooth-scroll', +} as const + +const formTestIds = { + targetAnchor: 'hash-select', + scrollIntoView: 'with-scroll', + behaviorSelect: 'behavior-select', + blockSelect: 'block-select', + inlineSelect: 'inline-select', + navigateButton: 'navigate-button', +} + +const shownSuffix = '(shown)' + +const activeClass = 'font-bold active' + +function getAnchorTarget(page: Page, anchor: string) { + return page.getByTestId(`heading-${anchor}`) +} + +function getAnchorLink(page: Page, anchor: string) { + return page.getByTestId(`link-${anchor}`) +} + +test.beforeEach(async ({ page }) => { + await page.goto('/anchor') +}) + +// Testing the `Link` component with the `hashScrollIntoView` prop +test('Navigating via anchor `Link` with default hash scrolling behavior', async ({ + page, +}) => { + await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText( + shownSuffix, + ) + + await getAnchorLink(page, anchors.defaultAnchor).click() + + await expect(getAnchorTarget(page, anchors.defaultAnchor)).toBeVisible() + await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText( + shownSuffix, + ) + + await expect(getAnchorLink(page, anchors.defaultAnchor)).toHaveClass( + activeClass, + ) +}) + +test('Navigating via anchor `Link` with hash scrolling disabled', async ({ + page, +}) => { + const initialScrollPosition = await page.evaluate(() => window.scrollY) + + await expect( + getAnchorTarget(page, anchors.noScrollIntoView), + ).not.toContainText(shownSuffix) + + await getAnchorLink(page, anchors.noScrollIntoView).click() + + // The active anchor should have updated + await expect(getAnchorLink(page, anchors.noScrollIntoView)).toHaveClass( + activeClass, + ) + + // The anchor should not have been visible, because the scroll should not have been activated + await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText( + shownSuffix, + ) + + // Expect the same scroll position as before + expect(await page.evaluate(() => window.scrollY)).toBe(initialScrollPosition) +}) + +test('Navigating via anchor `Link` with smooth hash scrolling behavior', async ({ + page, +}) => { + await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText( + shownSuffix, + ) + + await getAnchorLink(page, anchors.smoothScroll).click() + await expect(getAnchorTarget(page, anchors.smoothScroll)).toBeVisible() + + // Smooth scrolling should activate the IntersectionObserver on all headings, making them all render "(shown)" + await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText( + shownSuffix, + ) + await expect(getAnchorTarget(page, anchors.noScrollIntoView)).toContainText( + shownSuffix, + ) + await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText( + shownSuffix, + ) + + await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass( + activeClass, + ) +}) + +// Testing the `useNavigate` hook with the `hashScrollIntoView` option +test('Navigating via `useNavigate` with instant scroll behavior', async ({ + page, +}) => { + await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText( + shownSuffix, + ) + + // Scroll to the last anchor instantly, should not activate Intersection Observers for the other anchors + await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll') + await page.getByTestId(formTestIds.scrollIntoView).check() + await page.getByTestId(formTestIds.behaviorSelect).selectOption('instant') + await page.getByTestId(formTestIds.blockSelect).selectOption('start') + await page.getByTestId(formTestIds.inlineSelect).selectOption('nearest') + await page.getByTestId(formTestIds.navigateButton).click() + + await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText( + shownSuffix, + ) + + await expect( + getAnchorTarget(page, anchors.noScrollIntoView), + ).not.toContainText(shownSuffix) + + await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText( + shownSuffix, + ) + + await expect(getAnchorLink(page, anchors.smoothScroll)).toBeVisible() + + await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass( + activeClass, + ) +}) + +test('Navigating via `useNavigate` with scrollIntoView disabled', async ({ + page, +}) => { + const initialScrollPosition = await page.evaluate(() => window.scrollY) + + await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText( + shownSuffix, + ) + + await expect( + getAnchorTarget(page, anchors.noScrollIntoView), + ).not.toContainText(shownSuffix) + + await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText( + shownSuffix, + ) + + // Navigate to the last anchor, but with scrollIntoView disabled should not activate Intersection Observers for any anchors + await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll') + await page.getByTestId(formTestIds.scrollIntoView).uncheck() + await page.getByTestId(formTestIds.navigateButton).click() + + await expect(getAnchorTarget(page, anchors.defaultAnchor)).not.toContainText( + shownSuffix, + ) + + await expect( + getAnchorTarget(page, anchors.noScrollIntoView), + ).not.toContainText(shownSuffix) + + await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText( + shownSuffix, + ) + + await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass( + activeClass, + ) + + // Expect the same scroll position as before + expect(await page.evaluate(() => window.scrollY)).toBe(initialScrollPosition) +}) + +test('Navigating via `useNavigate` with smooth scroll behavior', async ({ + page, +}) => { + await expect(getAnchorTarget(page, anchors.smoothScroll)).not.toContainText( + shownSuffix, + ) + + // Scroll to the last anchor smoothly, should activate Intersection Observers for the other anchors, making them all render "(shown)" + await page.getByTestId(formTestIds.targetAnchor).selectOption('Smooth Scroll') + await page.getByTestId(formTestIds.scrollIntoView).check() + await page.getByTestId(formTestIds.behaviorSelect).selectOption('smooth') + await page.getByTestId(formTestIds.blockSelect).selectOption('start') + await page.getByTestId(formTestIds.inlineSelect).selectOption('nearest') + await page.getByTestId(formTestIds.navigateButton).click() + + await expect(getAnchorTarget(page, anchors.defaultAnchor)).toContainText( + shownSuffix, + ) + + await expect(getAnchorTarget(page, anchors.noScrollIntoView)).toContainText( + shownSuffix, + ) + + await expect(getAnchorTarget(page, anchors.smoothScroll)).toContainText( + shownSuffix, + ) + + await expect(getAnchorLink(page, anchors.smoothScroll)).toHaveClass( + activeClass, + ) +}) diff --git a/e2e/solid-router/basic-file-based/tsconfig.json b/e2e/solid-router/basic-file-based/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-file-based/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-file-based/vite.config.js b/e2e/solid-router/basic-file-based/vite.config.js new file mode 100644 index 00000000000..0d2f08b695a --- /dev/null +++ b/e2e/solid-router/basic-file-based/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [TanStackRouterVite({ target: 'solid' }), solid()], +}) diff --git a/e2e/solid-router/basic-scroll-restoration/.gitignore b/e2e/solid-router/basic-scroll-restoration/.gitignore new file mode 100644 index 00000000000..8354e4d50d5 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/e2e/solid-router/basic-scroll-restoration/index.html b/e2e/solid-router/basic-scroll-restoration/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-scroll-restoration/package.json b/e2e/solid-router/basic-scroll-restoration/package.json new file mode 100644 index 00000000000..95387e1a701 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-e2e-solid-basic-scroll-restoration", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-virtual": "^3.13.0", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic-scroll-restoration/playwright.config.ts b/e2e/solid-router/basic-scroll-restoration/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-scroll-restoration/postcss.config.mjs b/e2e/solid-router/basic-scroll-restoration/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-scroll-restoration/src/main.tsx b/e2e/solid-router/basic-scroll-restoration/src/main.tsx new file mode 100644 index 00000000000..f1a9dae0f9d --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/src/main.tsx @@ -0,0 +1,229 @@ +import { render } from 'solid-js/web' +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, + useElementScrollRestoration, +} from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { createVirtualizer } from '@tanstack/solid-virtual' +import './styles.css' +import { createRenderEffect } from 'solid-js' + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + About + + + About (No Reset) + + + By-Element + +
+ + {/* */} + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: IndexComponent, +}) + +function IndexComponent() { + createRenderEffect(() => { + window.invokeOrders.push('index-useLayoutEffect') + }, []) + + return ( +
+

+ Welcome Home! +

+
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ Home Item {i + 1} +
+ ))} +
+
+ ) +} + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: AboutComponent, +}) + +function AboutComponent() { + createRenderEffect(() => { + window.invokeOrders.push('about-useLayoutEffect') + }, []) + return ( +
+

Hello from About!

+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ About Item {i + 1} +
+ ))} +
+
+ ) +} + +const byElementRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/by-element', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: ByElementComponent, +}) + +function ByElementComponent() { + // We need a unique ID for manual scroll restoration on a specific element + // It should be as unique as possible for this element across your app + const scrollRestorationId = 'myVirtualizedContent' + + // We use that ID to get the scroll entry for this element + const scrollEntry = useElementScrollRestoration({ + id: scrollRestorationId, + }) + + // Let's use TanStack Virtual to virtualize some content! + let virtualizerParentRef: any = null + const virtualizer = createVirtualizer({ + count: 10000, + getScrollElement: () => virtualizerParentRef?.current, + estimateSize: () => 100, + // We pass the scrollY from the scroll restoration entry to the virtualizer + // as the initial offset + initialOffset: scrollEntry?.scrollY, + }) + + return ( +
+
Hello from By-Element!
+
+
+
+ First Regular List Item +
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ Regular List Item {i + 1} +
+ ))} +
+
+ {Array.from({ length: 2 }).map((_, i) => ( +
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ About Item {i + 1} +
+ ))} +
+
+ ))} +
+
Virtualized
+
+
+ {virtualizer.getVirtualItems().map((item) => ( +
+
+ Virtualized Item {item.index + 1} +
+
+ ))} +
+
+
+
+
+
+ ) +} + +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + byElementRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, + getScrollRestorationKey: (location) => location.pathname, +}) + +declare global { + interface Window { + invokeOrders: Array + } +} +window.invokeOrders = [] +router.subscribe('onBeforeRouteMount', (event) => { + window.invokeOrders.push(event.type) +}) + +router.subscribe('onResolved', (event) => { + window.invokeOrders.push(event.type) +}) + +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic-scroll-restoration/src/styles.css b/e2e/solid-router/basic-scroll-restoration/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-scroll-restoration/tailwind.config.mjs b/e2e/solid-router/basic-scroll-restoration/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-scroll-restoration/tests/app.spec.ts b/e2e/solid-router/basic-scroll-restoration/tests/app.spec.ts new file mode 100644 index 00000000000..d23c1fb7ce2 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/tests/app.spec.ts @@ -0,0 +1,82 @@ +import { expect, test } from '@playwright/test' + +test('restore scroll positions by page, home pages top message should not display on navigating back', async ({ + page, +}) => { + // Step 1: Navigate to the home page + await page.goto('/') + + await expect(page.locator('#greeting')).toContainText('Welcome Home!') + await expect(page.locator('#top-message')).toBeInViewport() + + // Step 2: Scroll to a position that hides the top + const targetScrollPosition = 1000 + await page.evaluate( + (scrollPos: number) => window.scrollTo(0, scrollPos), + targetScrollPosition, + ) + + // Verify initial scroll position + const scrollPosition = await page.evaluate(() => window.scrollY) + expect(scrollPosition).toBe(targetScrollPosition) + + await expect(page.locator('#top-message')).not.toBeInViewport() + + // Step 3: Navigate to the about page + await page.getByRole('link', { name: 'About', exact: true }).click() + await expect(page.locator('#greeting')).toContainText('Hello from About!') + + // Step 4: Go back to the home page and immediately check the message + await page.goBack() + + // Wait for the home page to have rendered + await page.waitForSelector('#greeting') + await page.waitForTimeout(1000) + await expect(page.locator('#top-message')).not.toBeInViewport() + + // Confirm the scroll position was restored correctly + const restoredScrollPosition = await page.evaluate(() => window.scrollY) + expect(restoredScrollPosition).toBe(targetScrollPosition) +}) + +test('restore scroll positions by element, first regular list item should not display on navigating back', async ({ + page, +}) => { + // Step 1: Navigate to the by-element page + await page.goto('/by-element') + + // Step 2: Scroll to a position that hides the first list item in regular list + const targetScrollPosition = 1000 + await page.waitForSelector('#RegularList') + await expect(page.locator('#first-regular-list-item')).toBeInViewport() + + await page.evaluate( + (scrollPos: number) => + document.querySelector('#RegularList')!.scrollTo(0, scrollPos), + targetScrollPosition, + ) + + // Verify initial scroll position + const scrollPosition = await page.evaluate( + () => document.querySelector('#RegularList')!.scrollTop, + ) + expect(scrollPosition).toBe(targetScrollPosition) + + await expect(page.locator('#first-regular-list-item')).not.toBeInViewport() + + // Step 3: Navigate to the about page + await page.getByRole('link', { name: 'About', exact: true }).click() + await expect(page.locator('#greeting')).toContainText('Hello from About!') + + // Step 4: Go back to the by-element page and immediately check the message + await page.goBack() + + // TODO: For some reason, this only works in headed mode. + // When someone can explain that to me, I'll fix this test. + + // Confirm the scroll position was restored correctly + // const restoredScrollPosition = await page.evaluate( + // () => document.querySelector('#RegularList')!.scrollTop, + // ) + // expect(restoredScrollPosition).toBe(targetScrollPosition) +}) diff --git a/e2e/solid-router/basic-scroll-restoration/tests/router-events.spec.ts b/e2e/solid-router/basic-scroll-restoration/tests/router-events.spec.ts new file mode 100644 index 00000000000..23cbd154514 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/tests/router-events.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test' + +test('after a navigation, should have emitted "onBeforeRouteMount","onResolved" and useRenderEffect setup in the correct order', async ({ + page, +}) => { + // Navigate to the Home page + await page.goto('/') + await expect(page.locator('#greeting')).toContainText('Welcome Home!') + + let orders = await page.evaluate(() => window.invokeOrders) + + expectItemOrder(orders, 'onBeforeRouteMount', 'onResolved') + expectItemOrder(orders, 'onBeforeRouteMount', 'index-useLayoutEffect') + + // Clear the invokeOrders array + orders = await page.evaluate(() => { + window.invokeOrders = [] + return window.invokeOrders + }) + + // Navigate to the About page + await page.getByRole('link', { name: 'About', exact: true }).click() + await expect(page.locator('#greeting')).toContainText('Hello from About!') + + orders = await page.evaluate(() => window.invokeOrders) + + expectItemOrder(orders, 'onBeforeRouteMount', 'onResolved') + expectItemOrder(orders, 'onBeforeRouteMount', 'about-useLayoutEffect') +}) + +function expectItemOrder( + array: Array, + firstItem: TItem, + secondItem: TItem, +) { + const firstIndex = array.findIndex((item) => item === firstItem) + const secondIndex = array.findIndex((item) => item === secondItem) + + if (firstIndex === -1 || secondIndex === -1) { + throw new Error('One or both items were not found in the array ' + array) + } + + expect(firstIndex).toBeLessThan(secondIndex) +} diff --git a/e2e/solid-router/basic-scroll-restoration/tsconfig.json b/e2e/solid-router/basic-scroll-restoration/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-scroll-restoration/vite.config.js b/e2e/solid-router/basic-scroll-restoration/vite.config.js new file mode 100644 index 00000000000..05041cc6d83 --- /dev/null +++ b/e2e/solid-router/basic-scroll-restoration/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [solid()], +}) diff --git a/e2e/solid-router/basic-solid-query-file-based/.gitignore b/e2e/solid-router/basic-solid-query-file-based/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/basic-solid-query-file-based/index.html b/e2e/solid-router/basic-solid-query-file-based/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-solid-query-file-based/package.json b/e2e/solid-router/basic-solid-query-file-based/package.json new file mode 100644 index 00000000000..93c299cecb6 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-router-e2e-solid-basic-solid-query-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-query": "^5.66.0", + "@tanstack/solid-query-devtools": "^5.66.0", + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic-solid-query-file-based/playwright.config.ts b/e2e/solid-router/basic-solid-query-file-based/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-solid-query-file-based/postcss.config.mjs b/e2e/solid-router/basic-solid-query-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/main.tsx b/e2e/solid-router/basic-solid-query-file-based/src/main.tsx new file mode 100644 index 00000000000..7f1d2165f16 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/main.tsx @@ -0,0 +1,40 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' +import { routeTree } from './routeTree.gen' +import './styles.css' + +export const queryClient = new QueryClient() + +// Set up a Router instance +const router = createRouter({ + routeTree, + context: { + queryClient, + }, + scrollRestoration: true, + defaultPreload: 'intent', + // Since we're using React Query, we don't want loader calls to ever be stale + // This will ensure that the loader is always called when the route is preloaded or visited + defaultPreloadStaleTime: 0, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render( + () => ( + + + + ), + rootElement, + ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/postQueryOptions.tsx b/e2e/solid-router/basic-solid-query-file-based/src/postQueryOptions.tsx new file mode 100644 index 00000000000..fe118d7bdea --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/postQueryOptions.tsx @@ -0,0 +1,8 @@ +import { queryOptions } from '@tanstack/solid-query' +import { fetchPost } from './posts' + +export const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', { postId }], + queryFn: () => fetchPost(postId), + }) diff --git a/e2e/solid-router/basic-solid-query-file-based/src/posts.tsx b/e2e/solid-router/basic-solid-query-file-based/src/posts.tsx new file mode 100644 index 00000000000..d551659b92e --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/posts.tsx @@ -0,0 +1,33 @@ +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export class PostNotFoundError extends Error {} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw new PostNotFoundError(`Post with id "${postId}" not found!`) + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/postsQueryOptions.tsx b/e2e/solid-router/basic-solid-query-file-based/src/postsQueryOptions.tsx new file mode 100644 index 00000000000..ce59b8a0bb4 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/postsQueryOptions.tsx @@ -0,0 +1,7 @@ +import { queryOptions } from '@tanstack/solid-query' +import { fetchPosts } from './posts' + +export const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: () => fetchPosts(), +}) diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-solid-query-file-based/src/routeTree.gen.ts new file mode 100644 index 00000000000..586c996b9bf --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routeTree.gen.ts @@ -0,0 +1,243 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as PostsImport } from './routes/posts' +import { Route as LayoutImport } from './routes/_layout' +import { Route as IndexImport } from './routes/index' +import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as PostsPostIdImport } from './routes/posts.$postId' +import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b' +import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a' + +// Create/Update Routes + +const PostsRoute = PostsImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const LayoutRoute = LayoutImport.update({ + id: '/_layout', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const PostsIndexRoute = PostsIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRoute, +} as any) + +const PostsPostIdRoute = PostsPostIdImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => PostsRoute, +} as any) + +const LayoutLayout2Route = LayoutLayout2Import.update({ + id: '/_layout-2', + getParentRoute: () => LayoutRoute, +} as any) + +const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({ + id: '/layout-b', + path: '/layout-b', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ + id: '/layout-a', + path: '/layout-a', + getParentRoute: () => LayoutLayout2Route, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsImport + parentRoute: typeof rootRoute + } + '/_layout/_layout-2': { + id: '/_layout/_layout-2' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutLayout2Import + parentRoute: typeof LayoutImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdImport + parentRoute: typeof PostsImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexImport + parentRoute: typeof PostsImport + } + '/_layout/_layout-2/layout-a': { + id: '/_layout/_layout-2/layout-a' + path: '/layout-a' + fullPath: '/layout-a' + preLoaderRoute: typeof LayoutLayout2LayoutAImport + parentRoute: typeof LayoutLayout2Import + } + '/_layout/_layout-2/layout-b': { + id: '/_layout/_layout-2/layout-b' + path: '/layout-b' + fullPath: '/layout-b' + preLoaderRoute: typeof LayoutLayout2LayoutBImport + parentRoute: typeof LayoutLayout2Import + } + } +} + +// Create and export the route tree + +interface LayoutLayout2RouteChildren { + LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute + LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute +} + +const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = { + LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute, +} + +const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren( + LayoutLayout2RouteChildren, +) + +interface LayoutRouteChildren { + LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutLayout2Route: LayoutLayout2RouteWithChildren, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface PostsRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteChildren: PostsRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/posts': typeof PostsRouteWithChildren + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/posts' + | '/posts/$postId' + | '/posts/' + | '/layout-a' + | '/layout-b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b' + id: + | '__root__' + | '/' + | '/_layout' + | '/posts' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/posts/' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + PostsRoute: typeof PostsRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + PostsRoute: PostsRouteWithChildren, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/__root.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..f52862bfbf7 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import { + Link, + Outlet, + createRootRouteWithContext, +} from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' +import type { QueryClient } from '@tanstack/solid-query' + +export const Route = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + This Route Does Not Exist + +
+
+ + + {/* */} + + ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout.tsx new file mode 100644 index 00000000000..d43b4ef5f5e --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2.tsx new file mode 100644 index 00000000000..7a5a3623a03 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 00000000000..b69951b2465 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 00000000000..30dbcce90fa --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/index.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..ee729333659 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,58 @@ +import { + ErrorComponent, + createFileRoute, + useRouter, +} from '@tanstack/solid-router' +import { createQuery } from '@tanstack/solid-query' +import { PostNotFoundError } from '../posts' +import { postQueryOptions } from '../postQueryOptions' +import type { ErrorComponentProps } from '@tanstack/solid-router' +import { createEffect, createMemo } from 'solid-js' +import { queryClient } from '../main' + +export function PostErrorComponent({ error, reset }: ErrorComponentProps) { + const router = useRouter() + if (error instanceof PostNotFoundError) { + return
{error.message}
+ } + + createEffect(() => { + reset() + queryClient.resetQueries() + }) + + return ( +
+ + +
+ ) +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ context: { queryClient }, params: { postId } }) => { + return queryClient.ensureQueryData(postQueryOptions(postId)) + }, + errorComponent: PostErrorComponent, + component: PostComponent, +}) + +function PostComponent() { + const params = Route.useParams() + const post = createMemo(() => + createQuery(() => postQueryOptions(params().postId)), + ) + + return ( +
+

{post()?.data?.title}

+
{post()?.data?.body}
+
+ ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.index.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.index.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.tsx new file mode 100644 index 00000000000..8ab5195a348 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/posts.tsx @@ -0,0 +1,48 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { createQuery } from '@tanstack/solid-query' +import { postsQueryOptions } from '../postsQueryOptions' +import { createMemo } from 'solid-js' + +export const Route = createFileRoute('/posts')({ + loader: ({ context: { queryClient } }) => + queryClient.ensureQueryData(postsQueryOptions), + component: PostsComponent, +}) + +function PostsComponent() { + const postsQuery = createQuery(() => postsQueryOptions) + const posts = createMemo(() => { + if (postsQuery.data) { + return postsQuery.data + } else { + return [] + } + }) + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/basic-solid-query-file-based/src/styles.css b/e2e/solid-router/basic-solid-query-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-solid-query-file-based/tailwind.config.mjs b/e2e/solid-router/basic-solid-query-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-solid-query-file-based/tests/app.spec.ts b/e2e/solid-router/basic-solid-query-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..3e5a69ccaae --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/basic-solid-query-file-based/tsconfig.json b/e2e/solid-router/basic-solid-query-file-based/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-solid-query-file-based/vite.config.js b/e2e/solid-router/basic-solid-query-file-based/vite.config.js new file mode 100644 index 00000000000..0d2f08b695a --- /dev/null +++ b/e2e/solid-router/basic-solid-query-file-based/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [TanStackRouterVite({ target: 'solid' }), solid()], +}) diff --git a/e2e/solid-router/basic-solid-query/.gitignore b/e2e/solid-router/basic-solid-query/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/basic-solid-query/index.html b/e2e/solid-router/basic-solid-query/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-solid-query/package.json b/e2e/solid-router/basic-solid-query/package.json new file mode 100644 index 00000000000..b98a7681fe0 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/package.json @@ -0,0 +1,29 @@ +{ + "name": "tanstack-router-e2e-solid-solid-query", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-query": "^5.66.0", + "@tanstack/solid-query-devtools": "^5.66.0", + "@tanstack/solid-router": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic-solid-query/playwright.config.ts b/e2e/solid-router/basic-solid-query/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-solid-query/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-solid-query/postcss.config.mjs b/e2e/solid-router/basic-solid-query/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-solid-query/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-solid-query/src/main.tsx b/e2e/solid-router/basic-solid-query/src/main.tsx new file mode 100644 index 00000000000..c7bbbabcdf0 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/src/main.tsx @@ -0,0 +1,275 @@ +import { render } from 'solid-js/web' +import { + ErrorComponent, + Link, + Outlet, + RouterProvider, + createRootRouteWithContext, + createRoute, + createRouter, + useRouter, +} from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' +import { + QueryClient, + QueryClientProvider, + createQuery, +} from '@tanstack/solid-query' +import { NotFoundError, postQueryOptions, postsQueryOptions } from './posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' +import './styles.css' +import { createEffect, createMemo } from 'solid-js' + +const rootRoute = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + This Route Does Not Exist + +
+
+ + + {/* */} + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexRouteComponent, +}) + +function IndexRouteComponent() { + return ( +
+

Welcome Home!

+
+ ) +} + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: ({ context: { queryClient } }) => + queryClient.ensureQueryData(postsQueryOptions), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '/', + component: PostsIndexRouteComponent, +}) + +function PostsIndexRouteComponent() { + return
Select a post.
+} + +function PostErrorComponent({ error, reset }: ErrorComponentProps) { + const router = useRouter() + if (error instanceof NotFoundError) { + return
{error.message}
+ } + createEffect(() => { + reset() + queryClient.resetQueries() + }) + + return ( +
+ + +
+ ) +} + +const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ context: { queryClient }, params: { postId } }) => + queryClient.ensureQueryData(postQueryOptions(postId)), + component: PostRouteComponent, +}) + +function PostRouteComponent() { + const params = postRoute.useParams() + const postQuery = createQuery(() => postQueryOptions(params().postId)) + const post = createMemo(() => postQuery.data) + + return ( +
+

{post()?.title}

+
{post()?.body}
+
+ ) +} + +const layoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_layout', + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} + +const layout2Route = createRoute({ + getParentRoute: () => layoutRoute, + id: '_layout-2', + component: Layout2Component, +}) + +function Layout2Component() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} + +const layoutARoute = createRoute({ + getParentRoute: () => layout2Route, + path: '/layout-a', + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} + +const layoutBRoute = createRoute({ + getParentRoute: () => layout2Route, + path: '/layout-b', + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} + +const routeTree = rootRoute.addChildren([ + postsRoute.addChildren([postRoute, postsIndexRoute]), + layoutRoute.addChildren([ + layout2Route.addChildren([layoutARoute, layoutBRoute]), + ]), + indexRoute, +]) + +const queryClient = new QueryClient() + +// Set up a Router instance +const router = createRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + // Since we're using React Query, we don't want loader calls to ever be stale + // This will ensure that the loader is always called when the route is preloaded or visited + defaultPreloadStaleTime: 0, + context: { + queryClient, + }, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render( + () => ( + + + + ), + rootElement, + ) +} diff --git a/e2e/solid-router/basic-solid-query/src/posts.lazy.tsx b/e2e/solid-router/basic-solid-query/src/posts.lazy.tsx new file mode 100644 index 00000000000..ce21b15cc69 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/src/posts.lazy.tsx @@ -0,0 +1,49 @@ +import { Link, Outlet, createLazyRoute } from '@tanstack/solid-router' +import { createQuery } from '@tanstack/solid-query' +import { postsQueryOptions } from './posts' +import { createMemo, Suspense } from 'solid-js' + +export const Route = createLazyRoute('/posts')({ + component: PostsComponent, +}) + +function PostsComponent() { + const postsQuery = createQuery(() => postsQueryOptions) + + const posts = createMemo(() => { + if (postsQuery.data) { + return postsQuery.data + } else { + return [] + } + }) + + return ( +
+
    + + {[ + ...posts(), + { id: 'i-do-not-exist', title: 'Non-existent Post' }, + ].map((post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + })} +
    +
+ +
+ ) +} diff --git a/e2e/solid-router/basic-solid-query/src/posts.ts b/e2e/solid-router/basic-solid-query/src/posts.ts new file mode 100644 index 00000000000..cc07134ab2d --- /dev/null +++ b/e2e/solid-router/basic-solid-query/src/posts.ts @@ -0,0 +1,44 @@ +import axios from 'redaxios' +import { queryOptions } from '@tanstack/solid-query' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} + +export const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', { postId }], + queryFn: () => fetchPost(postId), + }) + +export const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: () => fetchPosts(), +}) diff --git a/e2e/solid-router/basic-solid-query/src/styles.css b/e2e/solid-router/basic-solid-query/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-solid-query/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-solid-query/tailwind.config.mjs b/e2e/solid-router/basic-solid-query/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-solid-query/tests/app.spec.ts b/e2e/solid-router/basic-solid-query/tests/app.spec.ts new file mode 100644 index 00000000000..3e5a69ccaae --- /dev/null +++ b/e2e/solid-router/basic-solid-query/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/basic-solid-query/tsconfig.json b/e2e/solid-router/basic-solid-query/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-solid-query/vite.config.js b/e2e/solid-router/basic-solid-query/vite.config.js new file mode 100644 index 00000000000..05041cc6d83 --- /dev/null +++ b/e2e/solid-router/basic-solid-query/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [solid()], +}) diff --git a/e2e/solid-router/basic-virtual-file-based/.gitignore b/e2e/solid-router/basic-virtual-file-based/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/basic-virtual-file-based/index.html b/e2e/solid-router/basic-virtual-file-based/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-virtual-file-based/package.json b/e2e/solid-router/basic-virtual-file-based/package.json new file mode 100644 index 00000000000..c97d54dbf14 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/package.json @@ -0,0 +1,30 @@ +{ + "name": "tanstack-router-e2e-solid-basic-virtual-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/virtual-file-routes": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic-virtual-file-based/playwright.config.ts b/e2e/solid-router/basic-virtual-file-based/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-virtual-file-based/postcss.config.mjs b/e2e/solid-router/basic-virtual-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-virtual-file-based/routes.ts b/e2e/solid-router/basic-virtual-file-based/routes.ts new file mode 100644 index 00000000000..6c2c144ec5a --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/routes.ts @@ -0,0 +1,22 @@ +import { + index, + layout, + physical, + rootRoute, + route, +} from '@tanstack/virtual-file-routes' + +export const routes = rootRoute('root.tsx', [ + index('home.tsx'), + route('/posts', 'posts/posts.tsx', [ + index('posts/posts-home.tsx'), + route('$postId', 'posts/posts-detail.tsx'), + ]), + layout('first', 'layout/first-layout.tsx', [ + layout('second', 'layout/second-layout.tsx', [ + route('/layout-a', 'a.tsx'), + route('/layout-b', 'b.tsx'), + ]), + ]), + physical('/classic', 'file-based-subtree'), +]) diff --git a/e2e/solid-router/basic-virtual-file-based/src/main.tsx b/e2e/solid-router/basic-virtual-file-based/src/main.tsx new file mode 100644 index 00000000000..8128384193c --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/main.tsx @@ -0,0 +1,25 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/posts.tsx b/e2e/solid-router/basic-virtual-file-based/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-virtual-file-based/src/routeTree.gen.ts new file mode 100644 index 00000000000..fc2a2b44e11 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routeTree.gen.ts @@ -0,0 +1,345 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/root' +import { Route as postsPostsImport } from './routes/posts/posts' +import { Route as layoutFirstLayoutImport } from './routes/layout/first-layout' +import { Route as homeImport } from './routes/home' +import { Route as postsPostsDetailImport } from './routes/posts/posts-detail' +import { Route as layoutSecondLayoutImport } from './routes/layout/second-layout' +import { Route as postsPostsHomeImport } from './routes/posts/posts-home' +import { Route as ClassicHelloRouteImport } from './routes/file-based-subtree/hello/route' +import { Route as ClassicHelloIndexImport } from './routes/file-based-subtree/hello/index' +import { Route as ClassicHelloWorldImport } from './routes/file-based-subtree/hello/world' +import { Route as ClassicHelloUniverseImport } from './routes/file-based-subtree/hello/universe' +import { Route as bImport } from './routes/b' +import { Route as aImport } from './routes/a' + +// Create/Update Routes + +const postsPostsRoute = postsPostsImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const layoutFirstLayoutRoute = layoutFirstLayoutImport.update({ + id: '/_first', + getParentRoute: () => rootRoute, +} as any) + +const homeRoute = homeImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const postsPostsDetailRoute = postsPostsDetailImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => postsPostsRoute, +} as any) + +const layoutSecondLayoutRoute = layoutSecondLayoutImport.update({ + id: '/_second', + getParentRoute: () => layoutFirstLayoutRoute, +} as any) + +const postsPostsHomeRoute = postsPostsHomeImport.update({ + id: '/', + path: '/', + getParentRoute: () => postsPostsRoute, +} as any) + +const ClassicHelloRouteRoute = ClassicHelloRouteImport.update({ + id: '/classic/hello', + path: '/classic/hello', + getParentRoute: () => rootRoute, +} as any) + +const ClassicHelloIndexRoute = ClassicHelloIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => ClassicHelloRouteRoute, +} as any) + +const ClassicHelloWorldRoute = ClassicHelloWorldImport.update({ + id: '/world', + path: '/world', + getParentRoute: () => ClassicHelloRouteRoute, +} as any) + +const ClassicHelloUniverseRoute = ClassicHelloUniverseImport.update({ + id: '/universe', + path: '/universe', + getParentRoute: () => ClassicHelloRouteRoute, +} as any) + +const bRoute = bImport.update({ + id: '/layout-b', + path: '/layout-b', + getParentRoute: () => layoutSecondLayoutRoute, +} as any) + +const aRoute = aImport.update({ + id: '/layout-a', + path: '/layout-a', + getParentRoute: () => layoutSecondLayoutRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof homeImport + parentRoute: typeof rootRoute + } + '/_first': { + id: '/_first' + path: '' + fullPath: '' + preLoaderRoute: typeof layoutFirstLayoutImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof postsPostsImport + parentRoute: typeof rootRoute + } + '/classic/hello': { + id: '/classic/hello' + path: '/classic/hello' + fullPath: '/classic/hello' + preLoaderRoute: typeof ClassicHelloRouteImport + parentRoute: typeof rootRoute + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof postsPostsHomeImport + parentRoute: typeof postsPostsImport + } + '/_first/_second': { + id: '/_first/_second' + path: '' + fullPath: '' + preLoaderRoute: typeof layoutSecondLayoutImport + parentRoute: typeof layoutFirstLayoutImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof postsPostsDetailImport + parentRoute: typeof postsPostsImport + } + '/_first/_second/layout-a': { + id: '/_first/_second/layout-a' + path: '/layout-a' + fullPath: '/layout-a' + preLoaderRoute: typeof aImport + parentRoute: typeof layoutSecondLayoutImport + } + '/_first/_second/layout-b': { + id: '/_first/_second/layout-b' + path: '/layout-b' + fullPath: '/layout-b' + preLoaderRoute: typeof bImport + parentRoute: typeof layoutSecondLayoutImport + } + '/classic/hello/universe': { + id: '/classic/hello/universe' + path: '/universe' + fullPath: '/classic/hello/universe' + preLoaderRoute: typeof ClassicHelloUniverseImport + parentRoute: typeof ClassicHelloRouteImport + } + '/classic/hello/world': { + id: '/classic/hello/world' + path: '/world' + fullPath: '/classic/hello/world' + preLoaderRoute: typeof ClassicHelloWorldImport + parentRoute: typeof ClassicHelloRouteImport + } + '/classic/hello/': { + id: '/classic/hello/' + path: '/' + fullPath: '/classic/hello/' + preLoaderRoute: typeof ClassicHelloIndexImport + parentRoute: typeof ClassicHelloRouteImport + } + } +} + +// Create and export the route tree + +interface layoutSecondLayoutRouteChildren { + aRoute: typeof aRoute + bRoute: typeof bRoute +} + +const layoutSecondLayoutRouteChildren: layoutSecondLayoutRouteChildren = { + aRoute: aRoute, + bRoute: bRoute, +} + +const layoutSecondLayoutRouteWithChildren = + layoutSecondLayoutRoute._addFileChildren(layoutSecondLayoutRouteChildren) + +interface layoutFirstLayoutRouteChildren { + layoutSecondLayoutRoute: typeof layoutSecondLayoutRouteWithChildren +} + +const layoutFirstLayoutRouteChildren: layoutFirstLayoutRouteChildren = { + layoutSecondLayoutRoute: layoutSecondLayoutRouteWithChildren, +} + +const layoutFirstLayoutRouteWithChildren = + layoutFirstLayoutRoute._addFileChildren(layoutFirstLayoutRouteChildren) + +interface postsPostsRouteChildren { + postsPostsHomeRoute: typeof postsPostsHomeRoute + postsPostsDetailRoute: typeof postsPostsDetailRoute +} + +const postsPostsRouteChildren: postsPostsRouteChildren = { + postsPostsHomeRoute: postsPostsHomeRoute, + postsPostsDetailRoute: postsPostsDetailRoute, +} + +const postsPostsRouteWithChildren = postsPostsRoute._addFileChildren( + postsPostsRouteChildren, +) + +interface ClassicHelloRouteRouteChildren { + ClassicHelloUniverseRoute: typeof ClassicHelloUniverseRoute + ClassicHelloWorldRoute: typeof ClassicHelloWorldRoute + ClassicHelloIndexRoute: typeof ClassicHelloIndexRoute +} + +const ClassicHelloRouteRouteChildren: ClassicHelloRouteRouteChildren = { + ClassicHelloUniverseRoute: ClassicHelloUniverseRoute, + ClassicHelloWorldRoute: ClassicHelloWorldRoute, + ClassicHelloIndexRoute: ClassicHelloIndexRoute, +} + +const ClassicHelloRouteRouteWithChildren = + ClassicHelloRouteRoute._addFileChildren(ClassicHelloRouteRouteChildren) + +export interface FileRoutesByFullPath { + '/': typeof homeRoute + '': typeof layoutSecondLayoutRouteWithChildren + '/posts': typeof postsPostsRouteWithChildren + '/classic/hello': typeof ClassicHelloRouteRouteWithChildren + '/posts/': typeof postsPostsHomeRoute + '/posts/$postId': typeof postsPostsDetailRoute + '/layout-a': typeof aRoute + '/layout-b': typeof bRoute + '/classic/hello/universe': typeof ClassicHelloUniverseRoute + '/classic/hello/world': typeof ClassicHelloWorldRoute + '/classic/hello/': typeof ClassicHelloIndexRoute +} + +export interface FileRoutesByTo { + '/': typeof homeRoute + '': typeof layoutSecondLayoutRouteWithChildren + '/posts': typeof postsPostsHomeRoute + '/posts/$postId': typeof postsPostsDetailRoute + '/layout-a': typeof aRoute + '/layout-b': typeof bRoute + '/classic/hello/universe': typeof ClassicHelloUniverseRoute + '/classic/hello/world': typeof ClassicHelloWorldRoute + '/classic/hello': typeof ClassicHelloIndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof homeRoute + '/_first': typeof layoutFirstLayoutRouteWithChildren + '/posts': typeof postsPostsRouteWithChildren + '/classic/hello': typeof ClassicHelloRouteRouteWithChildren + '/posts/': typeof postsPostsHomeRoute + '/_first/_second': typeof layoutSecondLayoutRouteWithChildren + '/posts/$postId': typeof postsPostsDetailRoute + '/_first/_second/layout-a': typeof aRoute + '/_first/_second/layout-b': typeof bRoute + '/classic/hello/universe': typeof ClassicHelloUniverseRoute + '/classic/hello/world': typeof ClassicHelloWorldRoute + '/classic/hello/': typeof ClassicHelloIndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/posts' + | '/classic/hello' + | '/posts/' + | '/posts/$postId' + | '/layout-a' + | '/layout-b' + | '/classic/hello/universe' + | '/classic/hello/world' + | '/classic/hello/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/posts' + | '/posts/$postId' + | '/layout-a' + | '/layout-b' + | '/classic/hello/universe' + | '/classic/hello/world' + | '/classic/hello' + id: + | '__root__' + | '/' + | '/_first' + | '/posts' + | '/classic/hello' + | '/posts/' + | '/_first/_second' + | '/posts/$postId' + | '/_first/_second/layout-a' + | '/_first/_second/layout-b' + | '/classic/hello/universe' + | '/classic/hello/world' + | '/classic/hello/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + homeRoute: typeof homeRoute + layoutFirstLayoutRoute: typeof layoutFirstLayoutRouteWithChildren + postsPostsRoute: typeof postsPostsRouteWithChildren + ClassicHelloRouteRoute: typeof ClassicHelloRouteRouteWithChildren +} + +const rootRouteChildren: RootRouteChildren = { + homeRoute: homeRoute, + layoutFirstLayoutRoute: layoutFirstLayoutRouteWithChildren, + postsPostsRoute: postsPostsRouteWithChildren, + ClassicHelloRouteRoute: ClassicHelloRouteRouteWithChildren, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/a.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/a.tsx new file mode 100644 index 00000000000..055cba1e6f6 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/b.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/b.tsx new file mode 100644 index 00000000000..c5bb8051af9 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx new file mode 100644 index 00000000000..f7ff5379165 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/')({ + component: () =>
This is the index
, +}) diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx new file mode 100644 index 00000000000..f4f30d84256 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello')({ + component: () => ( +
+ Hello! +
{' '} + + say hello to the universe + {' '} + + say hello to the world + + +
+ ), +}) diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx new file mode 100644 index 00000000000..2a6bf16c377 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/universe')({ + component: () =>
Hello /classic/hello/universe!
, +}) diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx new file mode 100644 index 00000000000..03edc7f484a --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/world')({ + component: () =>
Hello /classic/hello/world!
, +}) diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/home.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx new file mode 100644 index 00000000000..5c77421bb29 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx new file mode 100644 index 00000000000..9ab147de5e9 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx new file mode 100644 index 00000000000..990f473ae8a --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx @@ -0,0 +1,27 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts.tsx new file mode 100644 index 00000000000..9ae6dfc747d --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/posts/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/routes/root.tsx b/e2e/solid-router/basic-virtual-file-based/src/routes/root.tsx new file mode 100644 index 00000000000..95685a9cd3b --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/routes/root.tsx @@ -0,0 +1,69 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Subtree + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + {/* */} + + ) +} diff --git a/e2e/solid-router/basic-virtual-file-based/src/styles.css b/e2e/solid-router/basic-virtual-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-virtual-file-based/tailwind.config.mjs b/e2e/solid-router/basic-virtual-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-virtual-file-based/tests/app.spec.ts b/e2e/solid-router/basic-virtual-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..3e5a69ccaae --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/basic-virtual-file-based/tsconfig.json b/e2e/solid-router/basic-virtual-file-based/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-virtual-file-based/vite.config.ts b/e2e/solid-router/basic-virtual-file-based/vite.config.ts new file mode 100644 index 00000000000..2b456a8eaa1 --- /dev/null +++ b/e2e/solid-router/basic-virtual-file-based/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' +import { routes } from './routes' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + TanStackRouterVite({ target: 'solid', virtualRouteConfig: routes }), + solid(), + ], +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/.gitignore b/e2e/solid-router/basic-virtual-named-export-config-file-based/.gitignore new file mode 100644 index 00000000000..ea2b6bb1fc7 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/.gitignore @@ -0,0 +1,12 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +src/routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/index.html b/e2e/solid-router/basic-virtual-named-export-config-file-based/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/package.json b/e2e/solid-router/basic-virtual-named-export-config-file-based/package.json new file mode 100644 index 00000000000..82d97318b3a --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/package.json @@ -0,0 +1,30 @@ +{ + "name": "tanstack-router-e2e-solid-basic-virtual-named-export-config-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/virtual-file-routes": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/playwright.config.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/postcss.config.mjs b/e2e/solid-router/basic-virtual-named-export-config-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/routes.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/routes.ts new file mode 100644 index 00000000000..6c2c144ec5a --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/routes.ts @@ -0,0 +1,22 @@ +import { + index, + layout, + physical, + rootRoute, + route, +} from '@tanstack/virtual-file-routes' + +export const routes = rootRoute('root.tsx', [ + index('home.tsx'), + route('/posts', 'posts/posts.tsx', [ + index('posts/posts-home.tsx'), + route('$postId', 'posts/posts-detail.tsx'), + ]), + layout('first', 'layout/first-layout.tsx', [ + layout('second', 'layout/second-layout.tsx', [ + route('/layout-a', 'a.tsx'), + route('/layout-b', 'b.tsx'), + ]), + ]), + physical('/classic', 'file-based-subtree'), +]) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/main.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/main.tsx new file mode 100644 index 00000000000..8128384193c --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/main.tsx @@ -0,0 +1,25 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/posts.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx new file mode 100644 index 00000000000..055cba1e6f6 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx new file mode 100644 index 00000000000..c5bb8051af9 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx new file mode 100644 index 00000000000..f7ff5379165 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/')({ + component: () =>
This is the index
, +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx new file mode 100644 index 00000000000..f4f30d84256 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello')({ + component: () => ( +
+ Hello! +
{' '} + + say hello to the universe + {' '} + + say hello to the world + + +
+ ), +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx new file mode 100644 index 00000000000..2a6bf16c377 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/universe')({ + component: () =>
Hello /classic/hello/universe!
, +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx new file mode 100644 index 00000000000..03edc7f484a --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/world')({ + component: () =>
Hello /classic/hello/world!
, +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx new file mode 100644 index 00000000000..5c77421bb29 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx new file mode 100644 index 00000000000..9ab147de5e9 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx new file mode 100644 index 00000000000..990f473ae8a --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx @@ -0,0 +1,27 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx new file mode 100644 index 00000000000..9ae6dfc747d --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx new file mode 100644 index 00000000000..95685a9cd3b --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx @@ -0,0 +1,69 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Subtree + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + {/* */} + + ) +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/src/styles.css b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/tailwind.config.mjs b/e2e/solid-router/basic-virtual-named-export-config-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..3e5a69ccaae --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/tsconfig.json b/e2e/solid-router/basic-virtual-named-export-config-file-based/tsconfig.json new file mode 100644 index 00000000000..98f80160757 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic-virtual-named-export-config-file-based/vite.config.ts b/e2e/solid-router/basic-virtual-named-export-config-file-based/vite.config.ts new file mode 100644 index 00000000000..c20765965a7 --- /dev/null +++ b/e2e/solid-router/basic-virtual-named-export-config-file-based/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + TanStackRouterVite({ target: 'solid', virtualRouteConfig: './routes.ts' }), + solid(), + ], +}) diff --git a/e2e/solid-router/basic/.gitignore b/e2e/solid-router/basic/.gitignore new file mode 100644 index 00000000000..8354e4d50d5 --- /dev/null +++ b/e2e/solid-router/basic/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/e2e/solid-router/basic/index.html b/e2e/solid-router/basic/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/basic/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/basic/package.json b/e2e/solid-router/basic/package.json new file mode 100644 index 00000000000..a39bbb9c796 --- /dev/null +++ b/e2e/solid-router/basic/package.json @@ -0,0 +1,27 @@ +{ + "name": "tanstack-router-e2e-solid-basic", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/basic/playwright.config.ts b/e2e/solid-router/basic/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/basic/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/basic/postcss.config.mjs b/e2e/solid-router/basic/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/basic/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/basic/src/main.tsx b/e2e/solid-router/basic/src/main.tsx new file mode 100644 index 00000000000..f9d53db711b --- /dev/null +++ b/e2e/solid-router/basic/src/main.tsx @@ -0,0 +1,235 @@ +import { render } from 'solid-js/web' +import { + ErrorComponent, + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { NotFoundError, fetchPost, fetchPosts } from './posts' +import './styles.css' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + View Transition + {' '} + + View Transition types + {' '} + + Layout + {' '} + + This Route Does Not Exist + +
+ + {/* */} + + ) +} +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+

Welcome Home!

+
+ ) +} + +export const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '/', + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} + +function PostErrorComponent({ error }: ErrorComponentProps) { + if (error instanceof NotFoundError) { + return
{error.message}
+ } + + return +} + +const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ params }) => fetchPost(params.postId), + component: PostComponent, +}) + +function PostComponent() { + const post = postRoute.useLoaderData() + + return ( +
+

{post().title}

+
+
{post().body}
+
+ ) +} + +const layoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_layout', + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} + +const layout2Route = createRoute({ + getParentRoute: () => layoutRoute, + id: '_layout-2', + component: Layout2Component, +}) + +function Layout2Component() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} + +const layoutARoute = createRoute({ + getParentRoute: () => layout2Route, + path: '/layout-a', + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} + +const layoutBRoute = createRoute({ + getParentRoute: () => layout2Route, + path: '/layout-b', + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} + +const routeTree = rootRoute.addChildren([ + postsRoute.addChildren([postRoute, postsIndexRoute]), + layoutRoute.addChildren([ + layout2Route.addChildren([layoutARoute, layoutBRoute]), + ]), + indexRoute, +]) + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/basic/src/posts.lazy.tsx b/e2e/solid-router/basic/src/posts.lazy.tsx new file mode 100644 index 00000000000..5277a5979a8 --- /dev/null +++ b/e2e/solid-router/basic/src/posts.lazy.tsx @@ -0,0 +1,35 @@ +import { Link, Outlet, createLazyRoute } from '@tanstack/solid-router' + +export const Route = createLazyRoute('/posts')({ + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+ +
+ ) +} diff --git a/e2e/solid-router/basic/src/posts.ts b/e2e/solid-router/basic/src/posts.ts new file mode 100644 index 00000000000..54d62e57886 --- /dev/null +++ b/e2e/solid-router/basic/src/posts.ts @@ -0,0 +1,32 @@ +import axios from 'redaxios' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} diff --git a/e2e/solid-router/basic/src/styles.css b/e2e/solid-router/basic/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/basic/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/basic/tailwind.config.mjs b/e2e/solid-router/basic/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/basic/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/basic/tests/app.spec.ts b/e2e/solid-router/basic/tests/app.spec.ts new file mode 100644 index 00000000000..5ccdafa513b --- /dev/null +++ b/e2e/solid-router/basic/tests/app.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts', exact: true }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#app')).toContainText("I'm a layout") + await expect(page.locator('#app')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#app')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#app')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) + +test('Navigating to a post page with viewTransition', async ({ page }) => { + await page.getByRole('link', { name: 'View Transition', exact: true }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating to a post page with viewTransition types', async ({ + page, +}) => { + await page.getByRole('link', { name: 'View Transition types' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) diff --git a/e2e/solid-router/basic/tsconfig.json b/e2e/solid-router/basic/tsconfig.json new file mode 100644 index 00000000000..e011e786b59 --- /dev/null +++ b/e2e/solid-router/basic/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/basic/vite.config.js b/e2e/solid-router/basic/vite.config.js new file mode 100644 index 00000000000..05041cc6d83 --- /dev/null +++ b/e2e/solid-router/basic/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [solid()], +}) diff --git a/e2e/solid-router/rspack-basic-file-based/.gitignore b/e2e/solid-router/rspack-basic-file-based/.gitignore new file mode 100644 index 00000000000..fbb2bd02932 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/.gitignore @@ -0,0 +1,20 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# E2E +src/routeTree.gen.ts +test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/rspack-basic-file-based/README.md b/e2e/solid-router/rspack-basic-file-based/README.md new file mode 100644 index 00000000000..93f18812e1c --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `pnpm install` +- `pnpm dev` diff --git a/e2e/solid-router/rspack-basic-file-based/package.json b/e2e/solid-router/rspack-basic-file-based/package.json new file mode 100644 index 00000000000..ff73441f2ca --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/package.json @@ -0,0 +1,29 @@ +{ + "name": "tanstack-router-e2e-solid-rspack-basic-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "rsbuild dev --port 3000", + "build": "rsbuild build && tsc --noEmit", + "preview": "rsbuild preview", + "start": "rsbuild preview", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@rsbuild/core": "^1.2.4", + "@rsbuild/plugin-babel": "^1.0.3", + "@rsbuild/plugin-solid": "^1.0.4", + "@tanstack/router-plugin": "workspace:^", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2" + } +} diff --git a/e2e/solid-router/rspack-basic-file-based/playwright.config.ts b/e2e/solid-router/rspack-basic-file-based/playwright.config.ts new file mode 100644 index 00000000000..5989adb1c7b --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `PUBLIC_SERVER_PORT=${PORT} pnpm build && PUBLIC_SERVER_PORT=${PORT} pnpm preview --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/rspack-basic-file-based/postcss.config.mjs b/e2e/solid-router/rspack-basic-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts b/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts new file mode 100644 index 00000000000..93cd6b0f54f --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/rsbuild.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginSolid } from '@rsbuild/plugin-solid' +import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack' +import { pluginBabel } from '@rsbuild/plugin-babel' + +export default defineConfig({ + plugins: [ + pluginBabel({ + include: /\.(?:jsx|tsx)$/, + }), + pluginSolid(), + ], + tools: { + rspack: { + plugins: [TanStackRouterRspack({ target: 'solid' })], + }, + }, +}) diff --git a/e2e/solid-router/rspack-basic-file-based/src/app.tsx b/e2e/solid-router/rspack-basic-file-based/src/app.tsx new file mode 100644 index 00000000000..0981f90396a --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/app.tsx @@ -0,0 +1,23 @@ +import { RouterProvider, createRouter } from '@tanstack/solid-router' + +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} +const App = () => { + return +} + +export default App diff --git a/e2e/solid-router/rspack-basic-file-based/src/env.d.ts b/e2e/solid-router/rspack-basic-file-based/src/env.d.ts new file mode 100644 index 00000000000..b0ac762b091 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/e2e/solid-router/rspack-basic-file-based/src/index.tsx b/e2e/solid-router/rspack-basic-file-based/src/index.tsx new file mode 100644 index 00000000000..3403ff7cbfc --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/index.tsx @@ -0,0 +1,8 @@ +import { render } from 'solid-js/web' +import App from './app' + +const rootEl = document.getElementById('root') + +if (rootEl) { + render(() => , rootEl) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/posts.tsx b/e2e/solid-router/rspack-basic-file-based/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/__root.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..eb544e319f7 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + {/* */} + + ) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx new file mode 100644 index 00000000000..d43b4ef5f5e --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx new file mode 100644 index 00000000000..7a5a3623a03 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 00000000000..b69951b2465 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 00000000000..30dbcce90fa --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.$postId.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..55f8871d03f --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,27 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.index.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.index.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx new file mode 100644 index 00000000000..11a999f50aa --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/routes/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/rspack-basic-file-based/src/styles.css b/e2e/solid-router/rspack-basic-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/rspack-basic-file-based/tailwind.config.mjs b/e2e/solid-router/rspack-basic-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/rspack-basic-file-based/tests/app.spec.ts b/e2e/solid-router/rspack-basic-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..10beff655f0 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#root')).toContainText("I'm a layout") + await expect(page.locator('#root')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#root')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#root')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/rspack-basic-file-based/tsconfig.json b/e2e/solid-router/rspack-basic-file-based/tsconfig.json new file mode 100644 index 00000000000..e4741a7cfe8 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "allowJs": true + }, + "include": ["src", "playwright.config.ts", "tests"], + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/rspack-basic-file-based/tsr.config.json b/e2e/solid-router/rspack-basic-file-based/tsr.config.json new file mode 100644 index 00000000000..15b57e5ea33 --- /dev/null +++ b/e2e/solid-router/rspack-basic-file-based/tsr.config.json @@ -0,0 +1,4 @@ +{ + "routesDirectory": "./src/routes", + "generatedRouteTree": "./src/routeTree.gen.ts" +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/.gitignore b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/.gitignore new file mode 100644 index 00000000000..fbb2bd02932 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/.gitignore @@ -0,0 +1,20 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# E2E +src/routeTree.gen.ts +test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/README.md b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/README.md new file mode 100644 index 00000000000..93f18812e1c --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `pnpm install` +- `pnpm dev` diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json new file mode 100644 index 00000000000..0861b672d44 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json @@ -0,0 +1,30 @@ +{ + "name": "tanstack-router-e2e-solid-rspack-basic-virtual-named-export-config-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "rsbuild dev --port 3000", + "build": "rsbuild build && tsc --noEmit", + "preview": "rsbuild preview", + "start": "rsbuild preview", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@rsbuild/core": "^1.2.4", + "@rsbuild/plugin-babel": "^1.0.3", + "@rsbuild/plugin-solid": "^1.0.4", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/virtual-file-routes": "workspace:^", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2" + } +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts new file mode 100644 index 00000000000..912664a64e0 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `PUBLIC_SERVER_PORT=${PORT} pnpm build && PUBLIC_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/postcss.config.mjs b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/routes.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/routes.ts new file mode 100644 index 00000000000..6c2c144ec5a --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/routes.ts @@ -0,0 +1,22 @@ +import { + index, + layout, + physical, + rootRoute, + route, +} from '@tanstack/virtual-file-routes' + +export const routes = rootRoute('root.tsx', [ + index('home.tsx'), + route('/posts', 'posts/posts.tsx', [ + index('posts/posts-home.tsx'), + route('$postId', 'posts/posts-detail.tsx'), + ]), + layout('first', 'layout/first-layout.tsx', [ + layout('second', 'layout/second-layout.tsx', [ + route('/layout-a', 'a.tsx'), + route('/layout-b', 'b.tsx'), + ]), + ]), + physical('/classic', 'file-based-subtree'), +]) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts new file mode 100644 index 00000000000..93cd6b0f54f --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginSolid } from '@rsbuild/plugin-solid' +import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack' +import { pluginBabel } from '@rsbuild/plugin-babel' + +export default defineConfig({ + plugins: [ + pluginBabel({ + include: /\.(?:jsx|tsx)$/, + }), + pluginSolid(), + ], + tools: { + rspack: { + plugins: [TanStackRouterRspack({ target: 'solid' })], + }, + }, +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx new file mode 100644 index 00000000000..0981f90396a --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx @@ -0,0 +1,23 @@ +import { RouterProvider, createRouter } from '@tanstack/solid-router' + +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} +const App = () => { + return +} + +export default App diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts new file mode 100644 index 00000000000..b0ac762b091 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx new file mode 100644 index 00000000000..3403ff7cbfc --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx @@ -0,0 +1,8 @@ +import { render } from 'solid-js/web' +import App from './app' + +const rootEl = document.getElementById('root') + +if (rootEl) { + render(() => , rootEl) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx new file mode 100644 index 00000000000..a42cb8d24e7 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx new file mode 100644 index 00000000000..055cba1e6f6 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx new file mode 100644 index 00000000000..c5bb8051af9 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx new file mode 100644 index 00000000000..f7ff5379165 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/')({ + component: () =>
This is the index
, +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx new file mode 100644 index 00000000000..f4f30d84256 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello')({ + component: () => ( +
+ Hello! +
{' '} + + say hello to the universe + {' '} + + say hello to the world + + +
+ ), +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx new file mode 100644 index 00000000000..2a6bf16c377 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/universe')({ + component: () =>
Hello /classic/hello/universe!
, +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx new file mode 100644 index 00000000000..03edc7f484a --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/classic/hello/world')({ + component: () =>
Hello /classic/hello/world!
, +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx new file mode 100644 index 00000000000..5c77421bb29 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx new file mode 100644 index 00000000000..9ab147de5e9 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx new file mode 100644 index 00000000000..990f473ae8a --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx @@ -0,0 +1,27 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx new file mode 100644 index 00000000000..9ae6dfc747d --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx new file mode 100644 index 00000000000..95685a9cd3b --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx @@ -0,0 +1,69 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Subtree + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + {/* */} + + ) +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/styles.css b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tailwind.config.mjs b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts new file mode 100644 index 00000000000..10beff655f0 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#root')).toContainText("I'm a layout") + await expect(page.locator('#root')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#root')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#root')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json new file mode 100644 index 00000000000..88b60cea21f --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "allowJs": true + }, + "include": ["src", "playwright.config.ts", "tests", "./routes.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json new file mode 100644 index 00000000000..2759341ba21 --- /dev/null +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json @@ -0,0 +1,5 @@ +{ + "routesDirectory": "./src/routes", + "generatedRouteTree": "./src/routeTree.gen.ts", + "virtualRouteConfig": "./routes.ts" +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/.gitignore b/e2e/solid-router/scroll-restoration-sandbox-vite/.gitignore new file mode 100644 index 00000000000..7baa77502b8 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +test-results diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/README.md b/e2e/solid-router/scroll-restoration-sandbox-vite/README.md new file mode 100644 index 00000000000..31385a50056 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/README.md @@ -0,0 +1,16 @@ +# Scroll Restoration Testing Sandbox with Vite + +To run this example: + +- `npm install` +- `npm start` + +This sandbox is for testing the scroll restoration behavior. + +## Setup + +- Create your files in `src/routes` directory. +- Make sure you update the arrays in the following files with the expected routes + - `tests/app.spec.ts` > routes array + - `src/routes/__root.tsx` > Nav component, routes array + - `src/routes/index.tsx` > Navigation test suite, routes array diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/index.html b/e2e/solid-router/scroll-restoration-sandbox-vite/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/package.json b/e2e/solid-router/scroll-restoration-sandbox-vite/package.json new file mode 100644 index 00000000000..eaf3d8beaf9 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-router-e2e-solid-scroll-restoration-sandbox-vite", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/router-devtools": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/zod-adapter": "workspace:^", + "solid-js": "^1.9.4", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.1" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.2", + "vite": "^6.1.0" + } +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/playwright.config.ts b/e2e/solid-router/scroll-restoration-sandbox-vite/playwright.config.ts new file mode 100644 index 00000000000..2eeb79844fc --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/postcss.config.mjs b/e2e/solid-router/scroll-restoration-sandbox-vite/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/main.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/main.tsx new file mode 100644 index 00000000000..65b505e952c --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/main.tsx @@ -0,0 +1,23 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/posts.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/posts.tsx new file mode 100644 index 00000000000..56007aa0f6d --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/posts.tsx @@ -0,0 +1,37 @@ +import axios from 'redaxios' + +export function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)) +} + +export type PostType = { + id: string + title: string + body: string +} + +export class PostNotFoundError extends Error {} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw new PostNotFoundError(`Post with id "${postId}" not found!`) + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts new file mode 100644 index 00000000000..969ebba8d6d --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routeTree.gen.ts @@ -0,0 +1,199 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { createFileRoute } from '@tanstack/solid-router' + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as IndexImport } from './routes/index' +import { Route as testsPageWithSearchImport } from './routes/(tests)/page-with-search' +import { Route as testsNormalPageImport } from './routes/(tests)/normal-page' +import { Route as testsLazyWithLoaderPageImport } from './routes/(tests)/lazy-with-loader-page' +import { Route as testsLazyPageImport } from './routes/(tests)/lazy-page' + +// Create Virtual Routes + +const testsVirtualPageLazyImport = createFileRoute('/(tests)/virtual-page')() + +// Create/Update Routes + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const testsVirtualPageLazyRoute = testsVirtualPageLazyImport + .update({ + id: '/(tests)/virtual-page', + path: '/virtual-page', + getParentRoute: () => rootRoute, + } as any) + .lazy(() => import('./routes/(tests)/virtual-page.lazy').then((d) => d.Route)) + +const testsPageWithSearchRoute = testsPageWithSearchImport.update({ + id: '/(tests)/page-with-search', + path: '/page-with-search', + getParentRoute: () => rootRoute, +} as any) + +const testsNormalPageRoute = testsNormalPageImport.update({ + id: '/(tests)/normal-page', + path: '/normal-page', + getParentRoute: () => rootRoute, +} as any) + +const testsLazyWithLoaderPageRoute = testsLazyWithLoaderPageImport + .update({ + id: '/(tests)/lazy-with-loader-page', + path: '/lazy-with-loader-page', + getParentRoute: () => rootRoute, + } as any) + .lazy(() => + import('./routes/(tests)/lazy-with-loader-page.lazy').then((d) => d.Route), + ) + +const testsLazyPageRoute = testsLazyPageImport + .update({ + id: '/(tests)/lazy-page', + path: '/lazy-page', + getParentRoute: () => rootRoute, + } as any) + .lazy(() => import('./routes/(tests)/lazy-page.lazy').then((d) => d.Route)) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/(tests)/lazy-page': { + id: '/(tests)/lazy-page' + path: '/lazy-page' + fullPath: '/lazy-page' + preLoaderRoute: typeof testsLazyPageImport + parentRoute: typeof rootRoute + } + '/(tests)/lazy-with-loader-page': { + id: '/(tests)/lazy-with-loader-page' + path: '/lazy-with-loader-page' + fullPath: '/lazy-with-loader-page' + preLoaderRoute: typeof testsLazyWithLoaderPageImport + parentRoute: typeof rootRoute + } + '/(tests)/normal-page': { + id: '/(tests)/normal-page' + path: '/normal-page' + fullPath: '/normal-page' + preLoaderRoute: typeof testsNormalPageImport + parentRoute: typeof rootRoute + } + '/(tests)/page-with-search': { + id: '/(tests)/page-with-search' + path: '/page-with-search' + fullPath: '/page-with-search' + preLoaderRoute: typeof testsPageWithSearchImport + parentRoute: typeof rootRoute + } + '/(tests)/virtual-page': { + id: '/(tests)/virtual-page' + path: '/virtual-page' + fullPath: '/virtual-page' + preLoaderRoute: typeof testsVirtualPageLazyImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/lazy-page': typeof testsLazyPageRoute + '/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute + '/normal-page': typeof testsNormalPageRoute + '/page-with-search': typeof testsPageWithSearchRoute + '/virtual-page': typeof testsVirtualPageLazyRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/lazy-page': typeof testsLazyPageRoute + '/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute + '/normal-page': typeof testsNormalPageRoute + '/page-with-search': typeof testsPageWithSearchRoute + '/virtual-page': typeof testsVirtualPageLazyRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/(tests)/lazy-page': typeof testsLazyPageRoute + '/(tests)/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute + '/(tests)/normal-page': typeof testsNormalPageRoute + '/(tests)/page-with-search': typeof testsPageWithSearchRoute + '/(tests)/virtual-page': typeof testsVirtualPageLazyRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/lazy-page' + | '/lazy-with-loader-page' + | '/normal-page' + | '/page-with-search' + | '/virtual-page' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/lazy-page' + | '/lazy-with-loader-page' + | '/normal-page' + | '/page-with-search' + | '/virtual-page' + id: + | '__root__' + | '/' + | '/(tests)/lazy-page' + | '/(tests)/lazy-with-loader-page' + | '/(tests)/normal-page' + | '/(tests)/page-with-search' + | '/(tests)/virtual-page' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + testsLazyPageRoute: typeof testsLazyPageRoute + testsLazyWithLoaderPageRoute: typeof testsLazyWithLoaderPageRoute + testsNormalPageRoute: typeof testsNormalPageRoute + testsPageWithSearchRoute: typeof testsPageWithSearchRoute + testsVirtualPageLazyRoute: typeof testsVirtualPageLazyRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + testsLazyPageRoute: testsLazyPageRoute, + testsLazyWithLoaderPageRoute: testsLazyWithLoaderPageRoute, + testsNormalPageRoute: testsNormalPageRoute, + testsPageWithSearchRoute: testsPageWithSearchRoute, + testsVirtualPageLazyRoute: testsVirtualPageLazyRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.lazy.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.lazy.tsx new file mode 100644 index 00000000000..2e7b3306d21 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.lazy.tsx @@ -0,0 +1,16 @@ +import { createLazyFileRoute } from '@tanstack/solid-router' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createLazyFileRoute('/(tests)/lazy-page')({ + component: Component, +}) + +function Component() { + return ( +
+

lazy-page

+
+ +
+ ) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.tsx new file mode 100644 index 00000000000..042a0537656 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-page.tsx @@ -0,0 +1,3 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(tests)/lazy-page')({}) diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.lazy.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.lazy.tsx new file mode 100644 index 00000000000..2079777df0e --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.lazy.tsx @@ -0,0 +1,16 @@ +import { createLazyFileRoute } from '@tanstack/solid-router' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createLazyFileRoute('/(tests)/lazy-with-loader-page')({ + component: Component, +}) + +function Component() { + return ( +
+

lazy-with-loader-page

+
+ +
+ ) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.tsx new file mode 100644 index 00000000000..6f2cf280a47 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/lazy-with-loader-page.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { sleep } from '../../posts' + +export const Route = createFileRoute('/(tests)/lazy-with-loader-page')({ + loader: async () => { + await sleep(1000) + return { foo: 'bar' } + }, +}) diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/normal-page.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/normal-page.tsx new file mode 100644 index 00000000000..6151049c357 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/normal-page.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createFileRoute('/(tests)/normal-page')({ + component: Component, +}) + +function Component() { + return ( +
+

normal-page

+
+ +
+ ) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/page-with-search.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/page-with-search.tsx new file mode 100644 index 00000000000..4d5953ed67c --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/page-with-search.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createFileRoute('/(tests)/page-with-search')({ + validateSearch: zodValidator(z.object({ where: z.string() })), + component: Component, +}) + +function Component() { + return ( +
+

page-with-search

+
+ +
+ ) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/virtual-page.lazy.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/virtual-page.lazy.tsx new file mode 100644 index 00000000000..8c6c8ef3530 --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/(tests)/virtual-page.lazy.tsx @@ -0,0 +1,16 @@ +import { createLazyFileRoute } from '@tanstack/solid-router' +import { ScrollBlock } from '../-components/scroll-block' + +export const Route = createLazyFileRoute('/(tests)/virtual-page')({ + component: Component, +}) + +function Component() { + return ( +
+

virtual-page

+
+ +
+ ) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/-components/scroll-block.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/-components/scroll-block.tsx new file mode 100644 index 00000000000..0539293f92e --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/-components/scroll-block.tsx @@ -0,0 +1,16 @@ +export const atTheTopId = 'at-the-top' +export const atTheBottomId = 'at-the-bottom' + +export function ScrollBlock({ number = 100 }: { number?: number }) { + return ( + <> +
+ {Array.from({ length: number }).map((_, i) => ( +
{i}
+ ))} +
+ At the bottom +
+ + ) +} diff --git a/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/__root.tsx b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/__root.tsx new file mode 100644 index 00000000000..60d576d62be --- /dev/null +++ b/e2e/solid-router/scroll-restoration-sandbox-vite/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + linkOptions, +} from '@tanstack/solid-router' +import { Dynamic } from 'solid-js/web' +// import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> +