diff --git a/e2e/vue-router/basic-virtual-file-based/.gitignore b/e2e/vue-router/basic-virtual-file-based/.gitignore
new file mode 100644
index 0000000000..4d2da67b50
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/.gitignore
@@ -0,0 +1,11 @@
+node_modules
+.DS_Store
+dist
+dist-hash
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/vue-router/basic-virtual-file-based/index.html b/e2e/vue-router/basic-virtual-file-based/index.html
new file mode 100644
index 0000000000..21e30f1695
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-virtual-file-based/package.json b/e2e/vue-router/basic-virtual-file-based/package.json
new file mode 100644
index 0000000000..bf094e7d01
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "tanstack-router-e2e-vue-basic-virtual-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "vite",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-plugin": "workspace:^",
+ "@tanstack/virtual-file-routes": "workspace:^",
+ "@tanstack/vue-query": "^5.90.0",
+ "@tanstack/vue-query-devtools": "^6.1.2",
+ "@tanstack/vue-router": "workspace:^",
+ "@tanstack/vue-router-devtools": "workspace:^",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.17",
+ "vue": "^3.5.16"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7",
+ "vue-tsc": "^3.1.5"
+ }
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/playwright.config.ts b/e2e/vue-router/basic-virtual-file-based/playwright.config.ts
new file mode 100644
index 0000000000..836ea41bd8
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/playwright.config.ts
@@ -0,0 +1,41 @@
+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} VITE_SERVER_PORT=${PORT} pnpm build && VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_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/vue-router/basic-virtual-file-based/postcss.config.mjs b/e2e/vue-router/basic-virtual-file-based/postcss.config.mjs
new file mode 100644
index 0000000000..a7f73a2d1d
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/routes.ts b/e2e/vue-router/basic-virtual-file-based/routes.ts
new file mode 100644
index 0000000000..6c2c144ec5
--- /dev/null
+++ b/e2e/vue-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/vue-router/basic-virtual-file-based/src/main.tsx b/e2e/vue-router/basic-virtual-file-based/src/main.tsx
new file mode 100644
index 0000000000..03cf6bd9ef
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/main.tsx
@@ -0,0 +1,29 @@
+import { createApp } from 'vue'
+import { RouterProvider, createRouter } from '@tanstack/vue-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/vue-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ createApp({
+ setup() {
+ return () =>
+ },
+ }).mount('#app')
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/posts.tsx b/e2e/vue-router/basic-virtual-file-based/src/posts.tsx
new file mode 100644
index 0000000000..8576a3799a
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/posts.tsx
@@ -0,0 +1,36 @@
+import { notFound } from '@tanstack/vue-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}...`)
+ 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...')
+ return axios
+ .get>(`${queryURL}/posts`)
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routeTree.gen.ts b/e2e/vue-router/basic-virtual-file-based/src/routeTree.gen.ts
new file mode 100644
index 0000000000..49926e7792
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routeTree.gen.ts
@@ -0,0 +1,428 @@
+/* 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 type { CreateFileRoute, FileRoutesByPath } from '@tanstack/vue-router'
+
+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',
+ 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: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => layoutSecondLayoutRoute,
+} as any)
+const aRoute = aRouteImport.update({
+ id: '/layout-a',
+ path: '/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
+ '/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
+ '/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 rootRouteImport
+ '/': 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
+}
+
+declare module '@tanstack/vue-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': {
+ id: '/_first/_second'
+ 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-b': {
+ id: '/_first/_second/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof bRouteImport
+ parentRoute: typeof layoutSecondLayoutRoute
+ }
+ '/_first/_second/layout-a': {
+ id: '/_first/_second/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof aRouteImport
+ parentRoute: typeof layoutSecondLayoutRoute
+ }
+ }
+}
+
+declare module './routes/home' {
+ const createFileRoute: CreateFileRoute<
+ '/',
+ FileRoutesByPath['/']['parentRoute'],
+ FileRoutesByPath['/']['id'],
+ FileRoutesByPath['/']['path'],
+ FileRoutesByPath['/']['fullPath']
+ >
+}
+declare module './routes/layout/first-layout' {
+ const createFileRoute: CreateFileRoute<
+ '/_first',
+ FileRoutesByPath['/_first']['parentRoute'],
+ FileRoutesByPath['/_first']['id'],
+ FileRoutesByPath['/_first']['path'],
+ FileRoutesByPath['/_first']['fullPath']
+ >
+}
+declare module './routes/posts/posts' {
+ const createFileRoute: CreateFileRoute<
+ '/posts',
+ FileRoutesByPath['/posts']['parentRoute'],
+ FileRoutesByPath['/posts']['id'],
+ FileRoutesByPath['/posts']['path'],
+ FileRoutesByPath['/posts']['fullPath']
+ >
+}
+declare module './routes/file-based-subtree/hello/route' {
+ const createFileRoute: CreateFileRoute<
+ '/classic/hello',
+ FileRoutesByPath['/classic/hello']['parentRoute'],
+ FileRoutesByPath['/classic/hello']['id'],
+ FileRoutesByPath['/classic/hello']['path'],
+ FileRoutesByPath['/classic/hello']['fullPath']
+ >
+}
+declare module './routes/posts/posts-home' {
+ const createFileRoute: CreateFileRoute<
+ '/posts/',
+ FileRoutesByPath['/posts/']['parentRoute'],
+ FileRoutesByPath['/posts/']['id'],
+ FileRoutesByPath['/posts/']['path'],
+ FileRoutesByPath['/posts/']['fullPath']
+ >
+}
+declare module './routes/layout/second-layout' {
+ const createFileRoute: CreateFileRoute<
+ '/_first/_second',
+ FileRoutesByPath['/_first/_second']['parentRoute'],
+ FileRoutesByPath['/_first/_second']['id'],
+ FileRoutesByPath['/_first/_second']['path'],
+ FileRoutesByPath['/_first/_second']['fullPath']
+ >
+}
+declare module './routes/posts/posts-detail' {
+ const createFileRoute: CreateFileRoute<
+ '/posts/$postId',
+ FileRoutesByPath['/posts/$postId']['parentRoute'],
+ FileRoutesByPath['/posts/$postId']['id'],
+ FileRoutesByPath['/posts/$postId']['path'],
+ FileRoutesByPath['/posts/$postId']['fullPath']
+ >
+}
+declare module './routes/a' {
+ const createFileRoute: CreateFileRoute<
+ '/_first/_second/layout-a',
+ FileRoutesByPath['/_first/_second/layout-a']['parentRoute'],
+ FileRoutesByPath['/_first/_second/layout-a']['id'],
+ FileRoutesByPath['/_first/_second/layout-a']['path'],
+ FileRoutesByPath['/_first/_second/layout-a']['fullPath']
+ >
+}
+declare module './routes/b' {
+ const createFileRoute: CreateFileRoute<
+ '/_first/_second/layout-b',
+ FileRoutesByPath['/_first/_second/layout-b']['parentRoute'],
+ FileRoutesByPath['/_first/_second/layout-b']['id'],
+ FileRoutesByPath['/_first/_second/layout-b']['path'],
+ FileRoutesByPath['/_first/_second/layout-b']['fullPath']
+ >
+}
+declare module './routes/file-based-subtree/hello/universe' {
+ const createFileRoute: CreateFileRoute<
+ '/classic/hello/universe',
+ FileRoutesByPath['/classic/hello/universe']['parentRoute'],
+ FileRoutesByPath['/classic/hello/universe']['id'],
+ FileRoutesByPath['/classic/hello/universe']['path'],
+ FileRoutesByPath['/classic/hello/universe']['fullPath']
+ >
+}
+declare module './routes/file-based-subtree/hello/world' {
+ const createFileRoute: CreateFileRoute<
+ '/classic/hello/world',
+ FileRoutesByPath['/classic/hello/world']['parentRoute'],
+ FileRoutesByPath['/classic/hello/world']['id'],
+ FileRoutesByPath['/classic/hello/world']['path'],
+ FileRoutesByPath['/classic/hello/world']['fullPath']
+ >
+}
+declare module './routes/file-based-subtree/hello/index' {
+ const createFileRoute: CreateFileRoute<
+ '/classic/hello/',
+ FileRoutesByPath['/classic/hello/']['parentRoute'],
+ FileRoutesByPath['/classic/hello/']['id'],
+ FileRoutesByPath['/classic/hello/']['path'],
+ FileRoutesByPath['/classic/hello/']['fullPath']
+ >
+}
+
+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()
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/a.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/a.tsx
new file mode 100644
index 0000000000..a190b24202
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/a.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/b.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/b.tsx
new file mode 100644
index 0000000000..505f8f6fbf
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/b.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx
new file mode 100644
index 0000000000..f86335e291
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/index.tsx
@@ -0,0 +1,3 @@
+export const Route = createFileRoute({
+ component: () => This is the index
,
+})
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx
new file mode 100644
index 0000000000..f07d58fd3e
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/route.tsx
@@ -0,0 +1,27 @@
+import { Link, Outlet } from '@tanstack/vue-router'
+
+export const Route = createFileRoute({
+ component: () => (
+
+ Hello!
+
{' '}
+
+ say hello to the universe
+ {' '}
+
+ say hello to the world
+
+
+
+ ),
+})
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx
new file mode 100644
index 0000000000..20b07c41cc
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/universe.tsx
@@ -0,0 +1,3 @@
+export const Route = createFileRoute({
+ component: () => Hello /classic/hello/universe!
,
+})
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx
new file mode 100644
index 0000000000..4af11357a2
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/file-based-subtree/hello/world.tsx
@@ -0,0 +1,3 @@
+export const Route = createFileRoute({
+ component: () => Hello /classic/hello/world!
,
+})
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/home.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/home.tsx
new file mode 100644
index 0000000000..510db79b73
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/home.tsx
@@ -0,0 +1,11 @@
+export const Route = createFileRoute({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx
new file mode 100644
index 0000000000..9e66c18e33
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/layout/first-layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet } from '@tanstack/vue-router'
+
+export const Route = createFileRoute({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx
new file mode 100644
index 0000000000..acb6d2c8ae
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/layout/second-layout.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet } from '@tanstack/vue-router'
+
+export const Route = createFileRoute({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx
new file mode 100644
index 0000000000..9b416bcf82
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts-detail.tsx
@@ -0,0 +1,27 @@
+import { ErrorComponent } from '@tanstack/vue-router'
+import { fetchPost } from '../../posts'
+import type { ErrorComponentProps } from '@tanstack/vue-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute({
+ 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.value?.title}
+
{post.value?.body}
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx
new file mode 100644
index 0000000000..13529228bb
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts-home.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts.tsx
new file mode 100644
index 0000000000..5942bac97e
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/posts/posts.tsx
@@ -0,0 +1,39 @@
+import { Link, Outlet } from '@tanstack/vue-router'
+import { fetchPosts } from '../../posts'
+
+export const Route = createFileRoute({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/src/routes/root.tsx b/e2e/vue-router/basic-virtual-file-based/src/routes/root.tsx
new file mode 100644
index 0000000000..6b1294dac7
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/routes/root.tsx
@@ -0,0 +1,75 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ createRootRoute,
+} from '@tanstack/vue-router'
+import { TanStackRouterDevtools } from '@tanstack/vue-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/vue-router/basic-virtual-file-based/src/styles.css b/e2e/vue-router/basic-virtual-file-based/src/styles.css
new file mode 100644
index 0000000000..37a1064738
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/src/styles.css
@@ -0,0 +1,21 @@
+@import 'tailwindcss';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+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/vue-router/basic-virtual-file-based/tests/app.spec.ts b/e2e/vue-router/basic-virtual-file-based/tests/app.spec.ts
new file mode 100644
index 0000000000..3e5a69ccaa
--- /dev/null
+++ b/e2e/vue-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/vue-router/basic-virtual-file-based/tests/setup/global.setup.ts b/e2e/vue-router/basic-virtual-file-based/tests/setup/global.setup.ts
new file mode 100644
index 0000000000..3593d10ab9
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/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/vue-router/basic-virtual-file-based/tests/setup/global.teardown.ts b/e2e/vue-router/basic-virtual-file-based/tests/setup/global.teardown.ts
new file mode 100644
index 0000000000..62fd79911c
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/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/vue-router/basic-virtual-file-based/tsconfig.json b/e2e/vue-router/basic-virtual-file-based/tsconfig.json
new file mode 100644
index 0000000000..3ed4d1e8fc
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "types": ["vite/client"]
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/vue-router/basic-virtual-file-based/vite.config.ts b/e2e/vue-router/basic-virtual-file-based/vite.config.ts
new file mode 100644
index 0000000000..fba56c6ce5
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-file-based/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
+import { routes } from './routes'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ tanstackRouter({
+ target: 'vue',
+ autoCodeSplitting: true,
+ verboseFileRoutes: false,
+ virtualRouteConfig: routes,
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/.gitignore b/e2e/vue-router/basic-virtual-named-export-config-file-based/.gitignore
new file mode 100644
index 0000000000..47bb41d43a
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/.gitignore
@@ -0,0 +1,13 @@
+node_modules
+.DS_Store
+dist
+dist-hash
+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/vue-router/basic-virtual-named-export-config-file-based/index.html b/e2e/vue-router/basic-virtual-named-export-config-file-based/index.html
new file mode 100644
index 0000000000..21e30f1695
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/package.json b/e2e/vue-router/basic-virtual-named-export-config-file-based/package.json
new file mode 100644
index 0000000000..1bb1009339
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "tanstack-router-e2e-vue-basic-virtual-named-export-config-file-based",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "vite",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-plugin": "workspace:^",
+ "@tanstack/virtual-file-routes": "workspace:^",
+ "@tanstack/vue-query": "^5.90.0",
+ "@tanstack/vue-query-devtools": "^6.1.2",
+ "@tanstack/vue-router": "workspace:^",
+ "@tanstack/vue-router-devtools": "workspace:^",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.17",
+ "vue": "^3.5.16"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7",
+ "vue-tsc": "^3.1.5"
+ }
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/playwright.config.ts b/e2e/vue-router/basic-virtual-named-export-config-file-based/playwright.config.ts
new file mode 100644
index 0000000000..836ea41bd8
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/playwright.config.ts
@@ -0,0 +1,41 @@
+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} VITE_SERVER_PORT=${PORT} pnpm build && VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_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/vue-router/basic-virtual-named-export-config-file-based/postcss.config.mjs b/e2e/vue-router/basic-virtual-named-export-config-file-based/postcss.config.mjs
new file mode 100644
index 0000000000..a7f73a2d1d
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/routes.ts b/e2e/vue-router/basic-virtual-named-export-config-file-based/routes.ts
new file mode 100644
index 0000000000..6c2c144ec5
--- /dev/null
+++ b/e2e/vue-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/vue-router/basic-virtual-named-export-config-file-based/src/main.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/main.tsx
new file mode 100644
index 0000000000..03cf6bd9ef
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/main.tsx
@@ -0,0 +1,29 @@
+import { createApp } from 'vue'
+import { RouterProvider, createRouter } from '@tanstack/vue-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/vue-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+ createApp({
+ setup() {
+ return () =>
+ },
+ }).mount('#app')
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/posts.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/posts.tsx
new file mode 100644
index 0000000000..8576a3799a
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/posts.tsx
@@ -0,0 +1,36 @@
+import { notFound } from '@tanstack/vue-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}...`)
+ 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...')
+ return axios
+ .get>(`${queryURL}/posts`)
+ .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx
new file mode 100644
index 0000000000..a190b24202
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/a.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx
new file mode 100644
index 0000000000..505f8f6fbf
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/b.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx
new file mode 100644
index 0000000000..f86335e291
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx
@@ -0,0 +1,3 @@
+export const Route = createFileRoute({
+ component: () => This is the index
,
+})
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx
new file mode 100644
index 0000000000..f07d58fd3e
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx
@@ -0,0 +1,27 @@
+import { Link, Outlet } from '@tanstack/vue-router'
+
+export const Route = createFileRoute({
+ component: () => (
+
+ Hello!
+
{' '}
+
+ say hello to the universe
+ {' '}
+
+ say hello to the world
+
+
+
+ ),
+})
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx
new file mode 100644
index 0000000000..20b07c41cc
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx
@@ -0,0 +1,3 @@
+export const Route = createFileRoute({
+ component: () => Hello /classic/hello/universe!
,
+})
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx
new file mode 100644
index 0000000000..4af11357a2
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx
@@ -0,0 +1,3 @@
+export const Route = createFileRoute({
+ component: () => Hello /classic/hello/world!
,
+})
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx
new file mode 100644
index 0000000000..510db79b73
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/home.tsx
@@ -0,0 +1,11 @@
+export const Route = createFileRoute({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx
new file mode 100644
index 0000000000..9e66c18e33
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet } from '@tanstack/vue-router'
+
+export const Route = createFileRoute({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx
new file mode 100644
index 0000000000..acb6d2c8ae
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet } from '@tanstack/vue-router'
+
+export const Route = createFileRoute({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx
new file mode 100644
index 0000000000..9b416bcf82
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx
@@ -0,0 +1,27 @@
+import { ErrorComponent } from '@tanstack/vue-router'
+import { fetchPost } from '../../posts'
+import type { ErrorComponentProps } from '@tanstack/vue-router'
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+export const Route = createFileRoute({
+ 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.value?.title}
+
{post.value?.body}
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx
new file mode 100644
index 0000000000..13529228bb
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx
@@ -0,0 +1,7 @@
+export const Route = createFileRoute({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx
new file mode 100644
index 0000000000..5942bac97e
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx
@@ -0,0 +1,39 @@
+import { Link, Outlet } from '@tanstack/vue-router'
+import { fetchPosts } from '../../posts'
+
+export const Route = createFileRoute({
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx
new file mode 100644
index 0000000000..6b1294dac7
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/routes/root.tsx
@@ -0,0 +1,75 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ createRootRoute,
+} from '@tanstack/vue-router'
+import { TanStackRouterDevtools } from '@tanstack/vue-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/vue-router/basic-virtual-named-export-config-file-based/src/styles.css b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/styles.css
new file mode 100644
index 0000000000..4fea791308
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/src/styles.css
@@ -0,0 +1,23 @@
+@import 'tailwindcss';
+
+@source './**/*.{js,jsx,ts,tsx}';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+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/vue-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts b/e2e/vue-router/basic-virtual-named-export-config-file-based/tests/app.spec.ts
new file mode 100644
index 0000000000..3e5a69ccaa
--- /dev/null
+++ b/e2e/vue-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/vue-router/basic-virtual-named-export-config-file-based/tests/setup/global.setup.ts b/e2e/vue-router/basic-virtual-named-export-config-file-based/tests/setup/global.setup.ts
new file mode 100644
index 0000000000..3593d10ab9
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/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/vue-router/basic-virtual-named-export-config-file-based/tests/setup/global.teardown.ts b/e2e/vue-router/basic-virtual-named-export-config-file-based/tests/setup/global.teardown.ts
new file mode 100644
index 0000000000..62fd79911c
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/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/vue-router/basic-virtual-named-export-config-file-based/tsconfig.json b/e2e/vue-router/basic-virtual-named-export-config-file-based/tsconfig.json
new file mode 100644
index 0000000000..3ed4d1e8fc
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "types": ["vite/client"]
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/vue-router/basic-virtual-named-export-config-file-based/vite.config.ts b/e2e/vue-router/basic-virtual-named-export-config-file-based/vite.config.ts
new file mode 100644
index 0000000000..dc5c9283f5
--- /dev/null
+++ b/e2e/vue-router/basic-virtual-named-export-config-file-based/vite.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ tanstackRouter({
+ target: 'vue',
+ autoCodeSplitting: true,
+ verboseFileRoutes: false,
+ virtualRouteConfig: './routes.ts',
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8bc6b940bd..0794615d03 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3963,6 +3963,122 @@ importers:
specifier: ^3.1.5
version: 3.1.5(typescript@5.8.3)
+ e2e/vue-router/basic-virtual-file-based:
+ dependencies:
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.15
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ '@tanstack/virtual-file-routes':
+ specifier: workspace:*
+ version: link:../../../packages/virtual-file-routes
+ '@tanstack/vue-query':
+ specifier: ^5.90.0
+ version: 5.92.0(vue@3.5.25(typescript@5.8.3))
+ '@tanstack/vue-query-devtools':
+ specifier: ^6.1.2
+ version: 6.1.2(@tanstack/vue-query@5.92.0(vue@3.5.25(typescript@5.8.3)))(vue@3.5.25(typescript@5.8.3))
+ '@tanstack/vue-router':
+ specifier: workspace:*
+ version: link:../../../packages/vue-router
+ '@tanstack/vue-router-devtools':
+ specifier: workspace:*
+ version: link:../../../packages/vue-router-devtools
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ tailwindcss:
+ specifier: ^4.1.17
+ version: 4.1.17
+ vue:
+ specifier: ^3.5.16
+ version: 3.5.25(typescript@5.8.3)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@vitejs/plugin-vue':
+ specifier: ^5.2.3
+ version: 5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.8.3))
+ '@vitejs/plugin-vue-jsx':
+ specifier: ^4.1.2
+ version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.8.3))
+ typescript:
+ specifier: ~5.8.3
+ version: 5.8.3
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vue-tsc:
+ specifier: ^3.1.5
+ version: 3.1.5(typescript@5.8.3)
+
+ e2e/vue-router/basic-virtual-named-export-config-file-based:
+ dependencies:
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.15
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ '@tanstack/virtual-file-routes':
+ specifier: workspace:*
+ version: link:../../../packages/virtual-file-routes
+ '@tanstack/vue-query':
+ specifier: ^5.90.0
+ version: 5.92.0(vue@3.5.25(typescript@5.8.3))
+ '@tanstack/vue-query-devtools':
+ specifier: ^6.1.2
+ version: 6.1.2(@tanstack/vue-query@5.92.0(vue@3.5.25(typescript@5.8.3)))(vue@3.5.25(typescript@5.8.3))
+ '@tanstack/vue-router':
+ specifier: workspace:*
+ version: link:../../../packages/vue-router
+ '@tanstack/vue-router-devtools':
+ specifier: workspace:*
+ version: link:../../../packages/vue-router-devtools
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ tailwindcss:
+ specifier: ^4.1.17
+ version: 4.1.17
+ vue:
+ specifier: ^3.5.16
+ version: 3.5.25(typescript@5.8.3)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@vitejs/plugin-vue':
+ specifier: ^5.2.3
+ version: 5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.8.3))
+ '@vitejs/plugin-vue-jsx':
+ specifier: ^4.1.2
+ version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.8.3))
+ typescript:
+ specifier: ~5.8.3
+ version: 5.8.3
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vue-tsc:
+ specifier: ^3.1.5
+ version: 3.1.5(typescript@5.8.3)
+
e2e/vue-router/basic-vue-query:
dependencies:
'@tailwindcss/postcss':