diff --git a/e2e/solid-start/virtual-routes/.gitignore b/e2e/solid-start/virtual-routes/.gitignore new file mode 100644 index 00000000000..a79d5cf1299 --- /dev/null +++ b/e2e/solid-start/virtual-routes/.gitignore @@ -0,0 +1,20 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output + +/build/ +/api/ +/server/build +/public/build +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/virtual-routes/.prettierignore b/e2e/solid-start/virtual-routes/.prettierignore new file mode 100644 index 00000000000..2be5eaa6ece --- /dev/null +++ b/e2e/solid-start/virtual-routes/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/virtual-routes/package.json b/e2e/solid-start/virtual-routes/package.json new file mode 100644 index 00000000000..c8b3b971e59 --- /dev/null +++ b/e2e/solid-start/virtual-routes/package.json @@ -0,0 +1,37 @@ +{ + "name": "tanstack-solid-start-e2e-virtual-routes", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "start": "pnpx srvx --prod -s ../client dist/server/server.js", + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-router-devtools": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "@tanstack/virtual-file-routes": "workspace:^", + "solid-js": "^1.9.5", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vite": "^7.1.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "vite-plugin-solid": "^2.11.9", + "autoprefixer": "^10.4.20", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "srvx": "^0.8.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/virtual-routes/playwright.config.ts b/e2e/solid-start/virtual-routes/playwright.config.ts new file mode 100644 index 00000000000..b0c365f8bd1 --- /dev/null +++ b/e2e/solid-start/virtual-routes/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig, devices } from '@playwright/test' +import { + getDummyServerPort, + getTestServerPort, +} from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const EXTERNAL_PORT = await getDummyServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/virtual-routes/postcss.config.mjs b/e2e/solid-start/virtual-routes/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-start/virtual-routes/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-start/virtual-routes/public/android-chrome-192x192.png b/e2e/solid-start/virtual-routes/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/android-chrome-192x192.png differ diff --git a/e2e/solid-start/virtual-routes/public/android-chrome-512x512.png b/e2e/solid-start/virtual-routes/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/android-chrome-512x512.png differ diff --git a/e2e/solid-start/virtual-routes/public/apple-touch-icon.png b/e2e/solid-start/virtual-routes/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/apple-touch-icon.png differ diff --git a/e2e/solid-start/virtual-routes/public/favicon-16x16.png b/e2e/solid-start/virtual-routes/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/favicon-16x16.png differ diff --git a/e2e/solid-start/virtual-routes/public/favicon-32x32.png b/e2e/solid-start/virtual-routes/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/favicon-32x32.png differ diff --git a/e2e/solid-start/virtual-routes/public/favicon.ico b/e2e/solid-start/virtual-routes/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/favicon.ico differ diff --git a/e2e/solid-start/virtual-routes/public/favicon.png b/e2e/solid-start/virtual-routes/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/solid-start/virtual-routes/public/favicon.png differ diff --git a/e2e/solid-start/virtual-routes/public/script.js b/e2e/solid-start/virtual-routes/public/script.js new file mode 100644 index 00000000000..897477e7d0a --- /dev/null +++ b/e2e/solid-start/virtual-routes/public/script.js @@ -0,0 +1,2 @@ +console.log('SCRIPT_1 loaded') +window.SCRIPT_1 = true diff --git a/e2e/solid-start/virtual-routes/public/script2.js b/e2e/solid-start/virtual-routes/public/script2.js new file mode 100644 index 00000000000..819af30daf9 --- /dev/null +++ b/e2e/solid-start/virtual-routes/public/script2.js @@ -0,0 +1,2 @@ +console.log('SCRIPT_2 loaded') +window.SCRIPT_2 = true diff --git a/e2e/solid-start/virtual-routes/public/site.webmanifest b/e2e/solid-start/virtual-routes/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/e2e/solid-start/virtual-routes/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/solid-start/virtual-routes/routes.ts b/e2e/solid-start/virtual-routes/routes.ts new file mode 100644 index 00000000000..ab17b8f58b4 --- /dev/null +++ b/e2e/solid-start/virtual-routes/routes.ts @@ -0,0 +1,24 @@ +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('layout/second-layout.tsx', [ + route('route-without-file', [ + route('/layout-a', 'a.tsx'), + route('/layout-b', 'b.tsx'), + ]), + ]), + ]), + physical('/classic', 'file-based-subtree'), +]) diff --git a/e2e/solid-start/virtual-routes/src/posts.tsx b/e2e/solid-start/virtual-routes/src/posts.tsx new file mode 100644 index 00000000000..909620abf96 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/posts.tsx @@ -0,0 +1,38 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +let queryURL = 'https://jsonplaceholder.typicode.com' + +if (import.meta.env.VITE_NODE_ENV === 'test') { + queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}` +} + +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(`${queryURL}/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>(`${queryURL}/posts`) + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-start/virtual-routes/src/routeTree.gen.ts b/e2e/solid-start/virtual-routes/src/routeTree.gen.ts new file mode 100644 index 00000000000..6b10324a1ff --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/routeTree.gen.ts @@ -0,0 +1,326 @@ +/* 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 { Route as rootRouteImport } from './routes/root' +import { Route as postsPostsRouteImport } from './routes/posts/posts' +import { Route as layoutFirstLayoutRouteImport } from './routes/layout/first-layout' +import { Route as homeRouteImport } from './routes/home' +import { Route as postsPostsDetailRouteImport } from './routes/posts/posts-detail' +import { Route as layoutSecondLayoutRouteImport } from './routes/layout/second-layout' +import { Route as postsPostsHomeRouteImport } from './routes/posts/posts-home' +import { Route as ClassicHelloRouteRouteImport } from './routes/file-based-subtree/hello/route' +import { Route as ClassicHelloIndexRouteImport } from './routes/file-based-subtree/hello/index' +import { Route as ClassicHelloWorldRouteImport } from './routes/file-based-subtree/hello/world' +import { Route as ClassicHelloUniverseRouteImport } from './routes/file-based-subtree/hello/universe' +import { Route as bRouteImport } from './routes/b' +import { Route as aRouteImport } from './routes/a' + +const postsPostsRoute = postsPostsRouteImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRouteImport, +} as any) +const layoutFirstLayoutRoute = layoutFirstLayoutRouteImport.update({ + id: '/_first', + getParentRoute: () => rootRouteImport, +} as any) +const homeRoute = homeRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const postsPostsDetailRoute = postsPostsDetailRouteImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => postsPostsRoute, +} as any) +const layoutSecondLayoutRoute = layoutSecondLayoutRouteImport.update({ + id: '/_second-layout', + getParentRoute: () => layoutFirstLayoutRoute, +} as any) +const postsPostsHomeRoute = postsPostsHomeRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => postsPostsRoute, +} as any) +const ClassicHelloRouteRoute = ClassicHelloRouteRouteImport.update({ + id: '/classic/hello', + path: '/classic/hello', + getParentRoute: () => rootRouteImport, +} as any) +const ClassicHelloIndexRoute = ClassicHelloIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ClassicHelloRouteRoute, +} as any) +const ClassicHelloWorldRoute = ClassicHelloWorldRouteImport.update({ + id: '/world', + path: '/world', + getParentRoute: () => ClassicHelloRouteRoute, +} as any) +const ClassicHelloUniverseRoute = ClassicHelloUniverseRouteImport.update({ + id: '/universe', + path: '/universe', + getParentRoute: () => ClassicHelloRouteRoute, +} as any) +const bRoute = bRouteImport.update({ + id: '/route-without-file/layout-b', + path: '/route-without-file/layout-b', + getParentRoute: () => layoutSecondLayoutRoute, +} as any) +const aRoute = aRouteImport.update({ + id: '/route-without-file/layout-a', + path: '/route-without-file/layout-a', + getParentRoute: () => layoutSecondLayoutRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof homeRoute + '/posts': typeof postsPostsRouteWithChildren + '/classic/hello': typeof ClassicHelloRouteRouteWithChildren + '/posts/': typeof postsPostsHomeRoute + '/posts/$postId': typeof postsPostsDetailRoute + '/classic/hello/universe': typeof ClassicHelloUniverseRoute + '/classic/hello/world': typeof ClassicHelloWorldRoute + '/classic/hello/': typeof ClassicHelloIndexRoute + '/route-without-file/layout-a': typeof aRoute + '/route-without-file/layout-b': typeof bRoute +} +export interface FileRoutesByTo { + '/': typeof homeRoute + '/posts': typeof postsPostsHomeRoute + '/posts/$postId': typeof postsPostsDetailRoute + '/classic/hello/universe': typeof ClassicHelloUniverseRoute + '/classic/hello/world': typeof ClassicHelloWorldRoute + '/classic/hello': typeof ClassicHelloIndexRoute + '/route-without-file/layout-a': typeof aRoute + '/route-without-file/layout-b': typeof bRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof homeRoute + '/_first': typeof layoutFirstLayoutRouteWithChildren + '/posts': typeof postsPostsRouteWithChildren + '/classic/hello': typeof ClassicHelloRouteRouteWithChildren + '/posts/': typeof postsPostsHomeRoute + '/_first/_second-layout': typeof layoutSecondLayoutRouteWithChildren + '/posts/$postId': typeof postsPostsDetailRoute + '/classic/hello/universe': typeof ClassicHelloUniverseRoute + '/classic/hello/world': typeof ClassicHelloWorldRoute + '/classic/hello/': typeof ClassicHelloIndexRoute + '/_first/_second-layout/route-without-file/layout-a': typeof aRoute + '/_first/_second-layout/route-without-file/layout-b': typeof bRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/posts' + | '/classic/hello' + | '/posts/' + | '/posts/$postId' + | '/classic/hello/universe' + | '/classic/hello/world' + | '/classic/hello/' + | '/route-without-file/layout-a' + | '/route-without-file/layout-b' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/posts' + | '/posts/$postId' + | '/classic/hello/universe' + | '/classic/hello/world' + | '/classic/hello' + | '/route-without-file/layout-a' + | '/route-without-file/layout-b' + id: + | '__root__' + | '/' + | '/_first' + | '/posts' + | '/classic/hello' + | '/posts/' + | '/_first/_second-layout' + | '/posts/$postId' + | '/classic/hello/universe' + | '/classic/hello/world' + | '/classic/hello/' + | '/_first/_second-layout/route-without-file/layout-a' + | '/_first/_second-layout/route-without-file/layout-b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + homeRoute: typeof homeRoute + layoutFirstLayoutRoute: typeof layoutFirstLayoutRouteWithChildren + postsPostsRoute: typeof postsPostsRouteWithChildren + ClassicHelloRouteRoute: typeof ClassicHelloRouteRouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof postsPostsRouteImport + parentRoute: typeof rootRouteImport + } + '/_first': { + id: '/_first' + path: '' + fullPath: '' + preLoaderRoute: typeof layoutFirstLayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof homeRouteImport + parentRoute: typeof rootRouteImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof postsPostsDetailRouteImport + parentRoute: typeof postsPostsRoute + } + '/_first/_second-layout': { + id: '/_first/_second-layout' + path: '' + fullPath: '' + preLoaderRoute: typeof layoutSecondLayoutRouteImport + parentRoute: typeof layoutFirstLayoutRoute + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof postsPostsHomeRouteImport + parentRoute: typeof postsPostsRoute + } + '/classic/hello': { + id: '/classic/hello' + path: '/classic/hello' + fullPath: '/classic/hello' + preLoaderRoute: typeof ClassicHelloRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/classic/hello/': { + id: '/classic/hello/' + path: '/' + fullPath: '/classic/hello/' + preLoaderRoute: typeof ClassicHelloIndexRouteImport + parentRoute: typeof ClassicHelloRouteRoute + } + '/classic/hello/world': { + id: '/classic/hello/world' + path: '/world' + fullPath: '/classic/hello/world' + preLoaderRoute: typeof ClassicHelloWorldRouteImport + parentRoute: typeof ClassicHelloRouteRoute + } + '/classic/hello/universe': { + id: '/classic/hello/universe' + path: '/universe' + fullPath: '/classic/hello/universe' + preLoaderRoute: typeof ClassicHelloUniverseRouteImport + parentRoute: typeof ClassicHelloRouteRoute + } + '/_first/_second-layout/route-without-file/layout-b': { + id: '/_first/_second-layout/route-without-file/layout-b' + path: '/route-without-file/layout-b' + fullPath: '/route-without-file/layout-b' + preLoaderRoute: typeof bRouteImport + parentRoute: typeof layoutSecondLayoutRoute + } + '/_first/_second-layout/route-without-file/layout-a': { + id: '/_first/_second-layout/route-without-file/layout-a' + path: '/route-without-file/layout-a' + fullPath: '/route-without-file/layout-a' + preLoaderRoute: typeof aRouteImport + parentRoute: typeof layoutSecondLayoutRoute + } + } +} + +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) + +const rootRouteChildren: RootRouteChildren = { + homeRoute: homeRoute, + layoutFirstLayoutRoute: layoutFirstLayoutRouteWithChildren, + postsPostsRoute: postsPostsRouteWithChildren, + ClassicHelloRouteRoute: ClassicHelloRouteRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/solid-start/virtual-routes/src/router.tsx b/e2e/solid-start/virtual-routes/src/router.tsx new file mode 100644 index 00000000000..d17e2881da0 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/router.tsx @@ -0,0 +1,12 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, + }) + + return router +} diff --git a/e2e/solid-start/virtual-routes/src/routes/a.tsx b/e2e/solid-start/virtual-routes/src/routes/a.tsx new file mode 100644 index 00000000000..b34bf9e000a --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/routes/a.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute( + '/_first/_second-layout/route-without-file/layout-a', +)({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/solid-start/virtual-routes/src/routes/b.tsx b/e2e/solid-start/virtual-routes/src/routes/b.tsx new file mode 100644 index 00000000000..06d73e222c4 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/routes/b.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute( + '/_first/_second-layout/route-without-file/layout-b', +)({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/solid-start/virtual-routes/src/routes/file-based-subtree/hello/index.tsx b/e2e/solid-start/virtual-routes/src/routes/file-based-subtree/hello/index.tsx new file mode 100644 index 00000000000..f7ff5379165 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/file-based-subtree/hello/route.tsx b/e2e/solid-start/virtual-routes/src/routes/file-based-subtree/hello/route.tsx new file mode 100644 index 00000000000..f4f30d84256 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/file-based-subtree/hello/universe.tsx b/e2e/solid-start/virtual-routes/src/routes/file-based-subtree/hello/universe.tsx new file mode 100644 index 00000000000..2a6bf16c377 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/file-based-subtree/hello/world.tsx b/e2e/solid-start/virtual-routes/src/routes/file-based-subtree/hello/world.tsx new file mode 100644 index 00000000000..03edc7f484a --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/home.tsx b/e2e/solid-start/virtual-routes/src/routes/home.tsx new file mode 100644 index 00000000000..bdfb4c76768 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/layout/first-layout.tsx b/e2e/solid-start/virtual-routes/src/routes/layout/first-layout.tsx new file mode 100644 index 00000000000..5c77421bb29 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/layout/second-layout.tsx b/e2e/solid-start/virtual-routes/src/routes/layout/second-layout.tsx new file mode 100644 index 00000000000..245a25bafc8 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/routes/layout/second-layout.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_first/_second-layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/solid-start/virtual-routes/src/routes/posts/posts-detail.tsx b/e2e/solid-start/virtual-routes/src/routes/posts/posts-detail.tsx new file mode 100644 index 00000000000..7cf90547d73 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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 const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + notFoundComponent: () => { + return

Post not found

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

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-start/virtual-routes/src/routes/posts/posts-home.tsx b/e2e/solid-start/virtual-routes/src/routes/posts/posts-home.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/posts/posts.tsx b/e2e/solid-start/virtual-routes/src/routes/posts/posts.tsx new file mode 100644 index 00000000000..9ae6dfc747d --- /dev/null +++ b/e2e/solid-start/virtual-routes/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-start/virtual-routes/src/routes/root.tsx b/e2e/solid-start/virtual-routes/src/routes/root.tsx new file mode 100644 index 00000000000..ef7b0745f16 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/routes/root.tsx @@ -0,0 +1,97 @@ +/// +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' +import type { JSX } from 'solid-js' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, + head: () => { + return { + links: [{ rel: 'stylesheet', href: appCss }], + } + }, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: { children: JSX.Element }) { + return ( + + + + + +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Subtree + {' '} + + This Route Does Not Exist + +
+
+ {children} + {/* Start rendering router matches */} + + + + + ) +} diff --git a/e2e/solid-start/virtual-routes/src/styles/app.css b/e2e/solid-start/virtual-routes/src/styles/app.css new file mode 100644 index 00000000000..c53c8706654 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/virtual-routes/src/utils/posts.tsx b/e2e/solid-start/virtual-routes/src/utils/posts.tsx new file mode 100644 index 00000000000..83a03ea163b --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/utils/posts.tsx @@ -0,0 +1,42 @@ +import { notFound } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +let queryURL = 'https://jsonplaceholder.typicode.com' + +if (import.meta.env.VITE_NODE_ENV === 'test') { + queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}` +} + +export const fetchPost = createServerFn({ method: 'GET' }) + .inputValidator((postId: string) => postId) + .handler(async ({ data: postId }) => { + console.info(`Fetching post with id ${postId}...`) + const post = await axios + .get(`${queryURL}/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + console.error(err) + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post + }) + +export const fetchPosts = createServerFn({ method: 'GET' }).handler( + async () => { + console.info('Fetching posts...') + return axios + .get>(`${queryURL}/posts`) + .then((r) => r.data.slice(0, 10)) + }, +) diff --git a/e2e/solid-start/virtual-routes/src/utils/seo.ts b/e2e/solid-start/virtual-routes/src/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/e2e/solid-start/virtual-routes/src/utils/users.tsx b/e2e/solid-start/virtual-routes/src/utils/users.tsx new file mode 100644 index 00000000000..46be4b15804 --- /dev/null +++ b/e2e/solid-start/virtual-routes/src/utils/users.tsx @@ -0,0 +1,9 @@ +export type User = { + id: number + name: string + email: string +} + +const PORT = process.env.VITE_SERVER_PORT || 3000 + +export const DEPLOY_URL = `http://localhost:${PORT}` diff --git a/e2e/solid-start/virtual-routes/tailwind.config.mjs b/e2e/solid-start/virtual-routes/tailwind.config.mjs new file mode 100644 index 00000000000..e49f4eb776e --- /dev/null +++ b/e2e/solid-start/virtual-routes/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/solid-start/virtual-routes/tests/app.spec.ts b/e2e/solid-start/virtual-routes/tests/app.spec.ts new file mode 100644 index 00000000000..3182febfa01 --- /dev/null +++ b/e2e/solid-start/virtual-routes/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('body')).toContainText("I'm a layout") + await expect(page.locator('body')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('body')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('body')).toContainText("I'm layout B!") +}) + +test('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-start/virtual-routes/tests/setup/global.setup.ts b/e2e/solid-start/virtual-routes/tests/setup/global.setup.ts new file mode 100644 index 00000000000..3593d10ab90 --- /dev/null +++ b/e2e/solid-start/virtual-routes/tests/setup/global.setup.ts @@ -0,0 +1,6 @@ +import { e2eStartDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function setup() { + await e2eStartDummyServer(packageJson.name) +} diff --git a/e2e/solid-start/virtual-routes/tests/setup/global.teardown.ts b/e2e/solid-start/virtual-routes/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..62fd79911cc --- /dev/null +++ b/e2e/solid-start/virtual-routes/tests/setup/global.teardown.ts @@ -0,0 +1,6 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function teardown() { + await e2eStopDummyServer(packageJson.name) +} diff --git a/e2e/solid-start/virtual-routes/tsconfig.json b/e2e/solid-start/virtual-routes/tsconfig.json new file mode 100644 index 00000000000..d53f9138f5a --- /dev/null +++ b/e2e/solid-start/virtual-routes/tsconfig.json @@ -0,0 +1,24 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true, + "types": ["vite/client"] + } +} diff --git a/e2e/solid-start/virtual-routes/vite.config.ts b/e2e/solid-start/virtual-routes/vite.config.ts new file mode 100644 index 00000000000..10090cbf513 --- /dev/null +++ b/e2e/solid-start/virtual-routes/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + router: { + virtualRouteConfig: './routes.ts', + }, + }), + viteSolid({ ssr: true }), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ffbd80cfc3..3fee224ac09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3064,6 +3064,70 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.3)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/solid-start/virtual-routes: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-router-devtools': + specifier: workspace:^ + version: link:../../../packages/solid-router-devtools + '@tanstack/solid-start': + specifier: workspace:* + version: link:../../../packages/solid-start + '@tanstack/virtual-file-routes': + specifier: workspace:* + version: link:../../../packages/virtual-file-routes + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: ^1.9.5 + version: 1.9.5 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.52.0 + version: 1.52.0 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.6) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.6 + srvx: + specifier: ^0.8.6 + version: 0.8.15 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite-plugin-solid: + specifier: ^2.11.9 + version: 2.11.9(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/solid-start/website: dependencies: '@tanstack/solid-router': @@ -16844,6 +16908,16 @@ packages: '@testing-library/jest-dom': optional: true + vite-plugin-solid@2.11.9: + resolution: {integrity: sha512-bTA6p+bspXZsuulSd2y6aTzegF8xGaJYcq1Uyh/mv+W4DQtzCgL9nN6n2fsTaxp/dMk+ZHHKgGndlNeooqHLKw==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: ^7.1.7 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -26960,6 +27034,21 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-solid@2.11.9(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): + dependencies: + '@babel/core': 7.28.4 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.3(@babel/core@7.28.4) + merge-anything: 5.1.7 + solid-js: 1.9.5 + solid-refresh: 0.6.3(solid-js@1.9.5) + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + optionalDependencies: + '@testing-library/jest-dom': 6.6.3 + transitivePeerDependencies: + - supports-color + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: debug: 4.4.0