diff --git a/e2e/vue-router/basic-file-based-jsx/eslint.config.js b/e2e/vue-router/basic-file-based-jsx/eslint.config.js
new file mode 100644
index 00000000000..4e8d00f1d89
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/eslint.config.js
@@ -0,0 +1,49 @@
+import js from '@eslint/js'
+import typescript from '@typescript-eslint/eslint-plugin'
+import typescriptParser from '@typescript-eslint/parser'
+import vue from 'eslint-plugin-vue'
+import vueParser from 'vue-eslint-parser'
+
+export default [
+ js.configs.recommended,
+ ...vue.configs['flat/recommended'],
+ {
+ files: ['**/*.{js,jsx,ts,tsx,vue}'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ plugins: {
+ '@typescript-eslint': typescript,
+ vue,
+ },
+ rules: {
+ // Vue specific rules
+ 'vue/multi-word-component-names': 'off',
+ 'vue/no-unused-vars': 'error',
+
+ // TypeScript rules
+ '@typescript-eslint/no-unused-vars': 'error',
+ '@typescript-eslint/no-explicit-any': 'warn',
+
+ // General rules
+ 'no-unused-vars': 'off', // Let TypeScript handle this
+ },
+ },
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ },
+ },
+ },
+]
diff --git a/e2e/vue-router/basic-file-based-jsx/index.html b/e2e/vue-router/basic-file-based-jsx/index.html
new file mode 100644
index 00000000000..21e30f16951
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-jsx/package.json b/e2e/vue-router/basic-file-based-jsx/package.json
new file mode 100644
index 00000000000..e8731b10104
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "tanstack-router-e2e-vue-basic-file-based-jsx",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && vue-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/vue-router": "workspace:^",
+ "@tanstack/vue-router-devtools": "workspace:^",
+ "@tanstack/zod-adapter": "workspace:^",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.15",
+ "vue": "^3.5.16",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.36.0",
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@typescript-eslint/eslint-plugin": "^8.44.1",
+ "@typescript-eslint/parser": "^8.44.1",
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "eslint-plugin-vue": "^9.33.0",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7",
+ "vue-eslint-parser": "^9.4.3",
+ "vue-tsc": "^3.1.5"
+ }
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/playwright.config.ts b/e2e/vue-router/basic-file-based-jsx/playwright.config.ts
new file mode 100644
index 00000000000..836ea41bd8c
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/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-file-based-jsx/postcss.config.mjs b/e2e/vue-router/basic-file-based-jsx/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/EditingAComponent.tsx b/e2e/vue-router/basic-file-based-jsx/src/components/EditingAComponent.tsx
new file mode 100644
index 00000000000..9406a081951
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/EditingAComponent.tsx
@@ -0,0 +1,41 @@
+import { ref, defineComponent } from 'vue'
+import { useBlocker, useNavigate } from '@tanstack/vue-router'
+
+export const EditingAComponent = defineComponent({
+ setup() {
+ const navigate = useNavigate()
+ const input = ref('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: ({ next }) => {
+ if (next.fullPath === '/editing-b' && input.value.length > 0) {
+ return true
+ }
+ return false
+ },
+ withResolver: true,
+ })
+
+ return () => (
+
+
Editing A
+
+
+ {blocker.value.status === 'blocked' && (
+
+ )}
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/EditingBComponent.tsx b/e2e/vue-router/basic-file-based-jsx/src/components/EditingBComponent.tsx
new file mode 100644
index 00000000000..444f94de8e3
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/EditingBComponent.tsx
@@ -0,0 +1,34 @@
+import { ref, toValue, defineComponent } from 'vue'
+import { useBlocker, useNavigate } from '@tanstack/vue-router'
+
+export const EditingBComponent = defineComponent({
+ setup() {
+ const navigate = useNavigate()
+ const input = ref('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: () => !!toValue(input),
+ withResolver: true,
+ })
+
+ return () => (
+
+
Editing B
+
+
+ {blocker.value.status === 'blocked' && (
+
+ )}
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/NotFoundComponent.vue b/e2e/vue-router/basic-file-based-jsx/src/components/NotFoundComponent.vue
new file mode 100644
index 00000000000..09ed5cf5ffd
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/NotFoundComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/NotRemountDepsComponent.tsx b/e2e/vue-router/basic-file-based-jsx/src/components/NotRemountDepsComponent.tsx
new file mode 100644
index 00000000000..bd11d54fc72
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/NotRemountDepsComponent.tsx
@@ -0,0 +1,37 @@
+import { ref, onMounted, defineComponent } from 'vue'
+import { useSearch, useNavigate } from '@tanstack/vue-router'
+
+export const NotRemountDepsComponent = defineComponent({
+ setup() {
+ // Component-scoped ref - will be recreated on component remount
+ const mounts = ref(0)
+ const search = useSearch({ from: '/notRemountDeps' })
+ const navigate = useNavigate()
+
+ onMounted(() => {
+ mounts.value++
+ })
+
+ return () => (
+
+
+
+
Search: {search.value.searchParam}
+
+ Page component mounts: {mounts.value}
+
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/PostErrorComponent.vue b/e2e/vue-router/basic-file-based-jsx/src/components/PostErrorComponent.vue
new file mode 100644
index 00000000000..9bb7514ccc5
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/PostErrorComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/RemountDepsComponent.tsx b/e2e/vue-router/basic-file-based-jsx/src/components/RemountDepsComponent.tsx
new file mode 100644
index 00000000000..ecb79aeca9c
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/RemountDepsComponent.tsx
@@ -0,0 +1,38 @@
+import { ref, onMounted, defineComponent } from 'vue'
+import { useSearch, useNavigate } from '@tanstack/vue-router'
+
+// Module-scoped ref to persist across component remounts
+const mounts = ref(0)
+
+export const RemountDepsComponent = defineComponent({
+ setup() {
+ const search = useSearch({ from: '/remountDeps' })
+ const navigate = useNavigate()
+
+ onMounted(() => {
+ mounts.value++
+ })
+
+ return () => (
+
+
+
+
Search: {search.value.searchParam}
+
+ Page component mounts: {mounts.value}
+
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/components/VueLogo.vue b/e2e/vue-router/basic-file-based-jsx/src/components/VueLogo.vue
new file mode 100644
index 00000000000..b0f134316b0
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/components/VueLogo.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ {{ countLabel }}
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-jsx/src/main.tsx b/e2e/vue-router/basic-file-based-jsx/src/main.tsx
new file mode 100644
index 00000000000..73cca4528c6
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/main.tsx
@@ -0,0 +1,29 @@
+import { RouterProvider, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+import { createApp } from 'vue'
+
+// 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-file-based-jsx/src/posts.tsx b/e2e/vue-router/basic-file-based-jsx/src/posts.tsx
new file mode 100644
index 00000000000..1b4c92b41d7
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/posts.tsx
@@ -0,0 +1,38 @@
+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}...`)
+ 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/vue-router/basic-file-based-jsx/src/routeTree.gen.ts b/e2e/vue-router/basic-file-based-jsx/src/routeTree.gen.ts
new file mode 100644
index 00000000000..01bb4b0d053
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routeTree.gen.ts
@@ -0,0 +1,513 @@
+/* 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 { lazyRouteComponent } from '@tanstack/vue-router'
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as SfcComponentRouteImport } from './routes/sfcComponent'
+import { Route as RemountDepsRouteImport } from './routes/remountDeps'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
+import { Route as EditingBRouteImport } from './routes/editing-b'
+import { Route as EditingARouteImport } from './routes/editing-a'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
+import { Route as groupInsideRouteImport } from './routes/(group)/inside'
+import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
+import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
+import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const SfcComponentRoute = SfcComponentRouteImport.update({
+ id: '/sfcComponent',
+ path: '/sfcComponent',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/sfcComponent.component.vue'),
+ 'default',
+ ),
+})
+const RemountDepsRoute = RemountDepsRouteImport.update({
+ id: '/remountDeps',
+ path: '/remountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
+ id: '/notRemountDeps',
+ path: '/notRemountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const EditingBRoute = EditingBRouteImport.update({
+ id: '/editing-b',
+ path: '/editing-b',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const EditingARoute = EditingARouteImport.update({
+ id: '/editing-a',
+ path: '/editing-a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any)
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any)
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any)
+const groupLazyinsideRoute = groupLazyinsideRouteImport.update({
+ id: '/(group)/lazyinside',
+ path: '/lazyinside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupInsideRoute = groupInsideRouteImport.update({
+ id: '/(group)/inside',
+ path: '/inside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupLayoutRoute = groupLayoutRouteImport.update({
+ id: '/(group)/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const anotherGroupOnlyrouteinsideRoute =
+ anotherGroupOnlyrouteinsideRouteImport.update({
+ id: '/(another-group)/onlyrouteinside',
+ path: '/onlyrouteinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
+ id: '/posts_/$postId/edit',
+ path: '/posts/$postId/edit',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport.update({
+ id: '/(group)/subfolder/inside',
+ path: '/subfolder/inside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({
+ id: '/insidelayout',
+ path: '/insidelayout',
+ getParentRoute: () => groupLayoutRoute,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/sfcComponent': typeof SfcComponentRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/remountDeps': typeof RemountDepsRoute
+ '/sfcComponent': typeof SfcComponentRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/_layout': typeof LayoutRouteWithChildren
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/sfcComponent': typeof SfcComponentRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/(another-group)/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/(group)/_layout': typeof groupLayoutRouteWithChildren
+ '/(group)/inside': typeof groupInsideRoute
+ '/(group)/lazyinside': typeof groupLazyinsideRoute
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts_/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/sfcComponent'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/remountDeps'
+ | '/sfcComponent'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ id:
+ | '__root__'
+ | '/'
+ | '/_layout'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/sfcComponent'
+ | '/대한민국'
+ | '/(another-group)/onlyrouteinside'
+ | '/(group)/_layout'
+ | '/(group)/inside'
+ | '/(group)/lazyinside'
+ | '/_layout/_layout-2'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/(group)/_layout/insidelayout'
+ | '/(group)/subfolder/inside'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ | '/posts_/$postId/edit'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ LayoutRoute: typeof LayoutRouteWithChildren
+ EditingARoute: typeof EditingARoute
+ EditingBRoute: typeof EditingBRoute
+ NotRemountDepsRoute: typeof NotRemountDepsRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ RemountDepsRoute: typeof RemountDepsRoute
+ SfcComponentRoute: typeof SfcComponentRoute
+ Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
+ anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
+ groupLayoutRoute: typeof groupLayoutRouteWithChildren
+ groupInsideRoute: typeof groupInsideRoute
+ groupLazyinsideRoute: typeof groupLazyinsideRoute
+ groupSubfolderInsideRoute: typeof groupSubfolderInsideRoute
+ PostsPostIdEditRoute: typeof PostsPostIdEditRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/sfcComponent': {
+ id: '/sfcComponent'
+ path: '/sfcComponent'
+ fullPath: '/sfcComponent'
+ preLoaderRoute: typeof SfcComponentRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/remountDeps': {
+ id: '/remountDeps'
+ path: '/remountDeps'
+ fullPath: '/remountDeps'
+ preLoaderRoute: typeof RemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/notRemountDeps': {
+ id: '/notRemountDeps'
+ path: '/notRemountDeps'
+ fullPath: '/notRemountDeps'
+ preLoaderRoute: typeof NotRemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-b': {
+ id: '/editing-b'
+ path: '/editing-b'
+ fullPath: '/editing-b'
+ preLoaderRoute: typeof EditingBRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-a': {
+ id: '/editing-a'
+ path: '/editing-a'
+ fullPath: '/editing-a'
+ preLoaderRoute: typeof EditingARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
+ '/(group)/lazyinside': {
+ id: '/(group)/lazyinside'
+ path: '/lazyinside'
+ fullPath: '/lazyinside'
+ preLoaderRoute: typeof groupLazyinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/inside': {
+ id: '/(group)/inside'
+ path: '/inside'
+ fullPath: '/inside'
+ preLoaderRoute: typeof groupInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout': {
+ id: '/(group)/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof groupLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(another-group)/onlyrouteinside': {
+ id: '/(another-group)/onlyrouteinside'
+ path: '/onlyrouteinside'
+ fullPath: '/onlyrouteinside'
+ preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts_/$postId/edit': {
+ id: '/posts_/$postId/edit'
+ path: '/posts/$postId/edit'
+ fullPath: '/posts/$postId/edit'
+ preLoaderRoute: typeof PostsPostIdEditRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/(group)/subfolder/inside': {
+ id: '/(group)/subfolder/inside'
+ path: '/subfolder/inside'
+ fullPath: '/subfolder/inside'
+ preLoaderRoute: typeof groupSubfolderInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout/insidelayout': {
+ id: '/(group)/_layout/insidelayout'
+ path: '/insidelayout'
+ fullPath: '/insidelayout'
+ preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
+ parentRoute: typeof groupLayoutRoute
+ }
+ }
+}
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface groupLayoutRouteChildren {
+ groupLayoutInsidelayoutRoute: typeof groupLayoutInsidelayoutRoute
+}
+
+const groupLayoutRouteChildren: groupLayoutRouteChildren = {
+ groupLayoutInsidelayoutRoute: groupLayoutInsidelayoutRoute,
+}
+
+const groupLayoutRouteWithChildren = groupLayoutRoute._addFileChildren(
+ groupLayoutRouteChildren,
+)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ LayoutRoute: LayoutRouteWithChildren,
+ EditingARoute: EditingARoute,
+ EditingBRoute: EditingBRoute,
+ NotRemountDepsRoute: NotRemountDepsRoute,
+ PostsRoute: PostsRouteWithChildren,
+ RemountDepsRoute: RemountDepsRoute,
+ SfcComponentRoute: SfcComponentRoute,
+ Char45824Char54620Char48124Char44397Route:
+ Char45824Char54620Char48124Char44397Route,
+ anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
+ groupLayoutRoute: groupLayoutRouteWithChildren,
+ groupInsideRoute: groupInsideRoute,
+ groupLazyinsideRoute: groupLazyinsideRoute,
+ groupSubfolderInsideRoute: groupSubfolderInsideRoute,
+ PostsPostIdEditRoute: PostsPostIdEditRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/(another-group)/onlyrouteinside.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/(another-group)/onlyrouteinside.tsx
new file mode 100644
index 00000000000..83036509ba4
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/(another-group)/onlyrouteinside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(another-group)/onlyrouteinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: OnlyRouteInsideComponent,
+})
+
+const routeApi = getRouteApi('/(another-group)/onlyrouteinside')
+
+function OnlyRouteInsideComponent() {
+ const searchViaHook = useSearch({ from: '/(another-group)/onlyrouteinside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/_layout.insidelayout.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/_layout.insidelayout.tsx
new file mode 100644
index 00000000000..557503a0c23
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/_layout.insidelayout.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/_layout/insidelayout')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: InsideLayoutComponent,
+})
+
+const routeApi = getRouteApi('/(group)/_layout/insidelayout')
+
+function InsideLayoutComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/_layout/insidelayout' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/_layout.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/_layout.tsx
new file mode 100644
index 00000000000..f752d719eff
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/_layout.tsx
@@ -0,0 +1,14 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/(group)/_layout')({
+ component: GroupLayoutComponent,
+})
+
+function GroupLayoutComponent() {
+ return (
+
+
Layout inside group
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/inside.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/inside.tsx
new file mode 100644
index 00000000000..bec12d8de25
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/inside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: InsideComponent,
+})
+
+const routeApi = getRouteApi('/(group)/inside')
+
+function InsideComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/inside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/lazyinside.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/lazyinside.tsx
new file mode 100644
index 00000000000..56d8d6cae85
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/lazyinside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/lazyinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: LazyInsideComponent,
+})
+
+const routeApi = getRouteApi('/(group)/lazyinside')
+
+function LazyInsideComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/lazyinside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/subfolder/inside.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/subfolder/inside.tsx
new file mode 100644
index 00000000000..b4487d163da
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/(group)/subfolder/inside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/subfolder/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: SubfolderInsideComponent,
+})
+
+const routeApi = getRouteApi('/(group)/subfolder/inside')
+
+function SubfolderInsideComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/subfolder/inside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/__root.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/__root.tsx
new file mode 100644
index 00000000000..59c9e6fa48d
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/__root.tsx
@@ -0,0 +1,104 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ createRootRoute,
+ useCanGoBack,
+ useRouter,
+ useRouterState,
+} from '@tanstack/vue-router'
+import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools'
+import NotFoundComponent from '../components/NotFoundComponent.vue'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: NotFoundComponent,
+})
+
+function RootComponent() {
+ const router = useRouter()
+ const canGoBack = useCanGoBack()
+ // test useRouterState doesn't crash client side navigation
+ const _state = useRouterState()
+
+ return (
+ <>
+
+
+
+
+ Home
+
+
+ Posts
+
+
+ Layout
+
+
+ Only Route Inside Group
+
+
+ Inside Group
+
+
+ Inside Subfolder Inside Group
+
+
+ Inside Group Inside Layout
+
+
+ Lazy Inside Group
+
+
+ unicode path
+
+
+ This Route Does Not Exist
+
+
+
+
+
+ >
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/_layout.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout.tsx
new file mode 100644
index 00000000000..90503a4acba
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 00000000000..70bcde8007c
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,24 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: Layout2Component,
+})
+
+function Layout2Component() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 00000000000..6d9d130d002
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 00000000000..9081cff17f3
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/editing-a.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/editing-a.tsx
new file mode 100644
index 00000000000..15e6804e642
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/editing-a.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { EditingAComponent } from '../components/EditingAComponent'
+
+export const Route = createFileRoute('/editing-a')({
+ component: EditingAComponent,
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/editing-b.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/editing-b.tsx
new file mode 100644
index 00000000000..85236bcaeec
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/editing-b.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { EditingBComponent } from '../components/EditingBComponent'
+
+export const Route = createFileRoute('/editing-b')({
+ component: EditingBComponent,
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/index.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/index.tsx
new file mode 100644
index 00000000000..a1a7ce5a58e
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/index.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import VueLogo from '../components/VueLogo.vue'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/notRemountDeps.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/notRemountDeps.tsx
new file mode 100644
index 00000000000..5f0874f710e
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/notRemountDeps.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { NotRemountDepsComponent } from '../components/NotRemountDepsComponent'
+
+export const Route = createFileRoute('/notRemountDeps')({
+ validateSearch: (search: Record) => ({
+ searchParam: (search.searchParam as string) || '',
+ }),
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.params
+ },
+ component: NotRemountDepsComponent,
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/posts.$postId.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..15d17387c82
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/posts.$postId.tsx
@@ -0,0 +1,24 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import PostErrorComponent from '../components/PostErrorComponent.vue'
+import { fetchPost } from '../posts'
+import type { PostType } from '../posts'
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ component: PostComponent,
+ errorComponent: PostErrorComponent,
+ notFoundComponent: () => Post not found
,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ {(post.value as PostType).title}
+
+
{(post.value as PostType).body}
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/posts.index.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..42e369c2188
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/posts.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/posts.tsx
new file mode 100644
index 00000000000..ae580dfc02a
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/posts.tsx
@@ -0,0 +1,43 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/vue-router'
+import { fetchPosts } from '../posts'
+import type { PostType } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+
+ {[
+ ...(posts.value as Array),
+ { id: 'i-do-not-exist', title: 'Non-existent Post' },
+ ].map((post) => (
+ -
+
+
{post.title.substring(0, 20)}
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/posts_.$postId.edit.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/posts_.$postId.edit.tsx
new file mode 100644
index 00000000000..57963c6b64d
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/posts_.$postId.edit.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, getRouteApi, useParams } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts_/$postId/edit')({
+ component: PostEditComponent,
+})
+
+const api = getRouteApi('/posts_/$postId/edit')
+
+function PostEditComponent() {
+ const paramsViaApi = api.useParams()
+ const paramsViaHook = useParams({ from: '/posts_/$postId/edit' })
+ const paramsViaRouteHook = api.useParams()
+
+ return (
+
+
{paramsViaHook.value.postId}
+
+ {paramsViaRouteHook.value.postId}
+
+
{paramsViaApi.value.postId}
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/remountDeps.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/remountDeps.tsx
new file mode 100644
index 00000000000..51f51e7592e
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/remountDeps.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { RemountDepsComponent } from '../components/RemountDepsComponent'
+
+export const Route = createFileRoute('/remountDeps')({
+ validateSearch: (search: Record) => ({
+ searchParam: (search.searchParam as string) || '',
+ }),
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.search
+ },
+ component: RemountDepsComponent,
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/sfcComponent.component.vue b/e2e/vue-router/basic-file-based-jsx/src/routes/sfcComponent.component.vue
new file mode 100644
index 00000000000..392d9ce9bf6
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/sfcComponent.component.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
Vue SFC!
+
+
+
diff --git a/e2e/vue-router/basic-file-based-jsx/src/routes/sfcComponent.tsx b/e2e/vue-router/basic-file-based-jsx/src/routes/sfcComponent.tsx
new file mode 100644
index 00000000000..0cbf84f07a0
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/routes/sfcComponent.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/sfcComponent')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Will be overwritten by the SFC component!
+}
diff --git "a/e2e/vue-router/basic-file-based-jsx/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/vue-router/basic-file-based-jsx/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
new file mode 100644
index 00000000000..e8c55d0ef91
--- /dev/null
+++ "b/e2e/vue-router/basic-file-based-jsx/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
@@ -0,0 +1,17 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/대한민국')({
+ component: UnicodeComponent,
+})
+
+function UnicodeComponent() {
+ return (
+
+
+ Hello "/대한민국"!
+
+
+
+
+ )
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/src/styles.css b/e2e/vue-router/basic-file-based-jsx/src/styles.css
new file mode 100644
index 00000000000..6a03b331e8e
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/src/styles.css
@@ -0,0 +1,23 @@
+@import 'tailwindcss';
+
+@source "./**/*.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-file-based-jsx/test-results/.last-run.json b/e2e/vue-router/basic-file-based-jsx/test-results/.last-run.json
new file mode 100644
index 00000000000..cbcc1fbac11
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
\ No newline at end of file
diff --git a/e2e/vue-router/basic-file-based-jsx/tests/app.spec.ts b/e2e/vue-router/basic-file-based-jsx/tests/app.spec.ts
new file mode 100644
index 00000000000..543746d64d8
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/tests/app.spec.ts
@@ -0,0 +1,289 @@
+import { expect, test } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test("useBlocker doesn't block navigation if condition is not met", async ({
+ page,
+}) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+})
+
+test('useBlocker does block navigation if condition is met', async ({
+ page,
+}) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible()
+})
+
+test('Proceeding through blocked navigation works', async ({ page }) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByRole('button', { name: 'Proceed' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+})
+
+test("legacy useBlocker doesn't block navigation if condition is not met", async ({
+ page,
+}) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+})
+
+test('legacy useBlocker does block navigation if condition is met', async ({
+ page,
+}) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible()
+})
+
+test('legacy Proceeding through blocked navigation works', async ({ page }) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByRole('button', { name: 'Proceed' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+})
+
+test('useCanGoBack correctly disables back button', async ({ page }) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goForward()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+test('useCanGoBack correctly disables back button, using router.history and window.history', async ({
+ page,
+}) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+const testCases = [
+ {
+ description: 'Navigating to a route inside a route group',
+ testId: 'link-to-route-inside-group',
+ },
+ {
+ description:
+ 'Navigating to a route inside a subfolder inside a route group ',
+ testId: 'link-to-route-inside-group-inside-subfolder',
+ },
+ {
+ description: 'Navigating to a route inside a route group inside a layout',
+ testId: 'link-to-route-inside-group-inside-layout',
+ },
+ {
+ description: 'Navigating to a lazy route inside a route group',
+ testId: 'link-to-lazy-route-inside-group',
+ },
+
+ {
+ description: 'Navigating to the only route inside a route group ',
+ testId: 'link-to-only-route-inside-group',
+ },
+]
+
+testCases.forEach(({ description, testId }) => {
+ test(description, async ({ page }) => {
+ await page.getByTestId(testId).click()
+ await expect(page.getByTestId('search-via-hook')).toContainText('world')
+ await expect(page.getByTestId('search-via-route-hook')).toContainText(
+ 'world',
+ )
+ await expect(page.getByTestId('search-via-route-api')).toContainText(
+ 'world',
+ )
+ })
+})
+
+test('navigating to an unnested route', async ({ page }) => {
+ const postId = 'hello-world'
+ page.goto(`/posts/${postId}/edit`)
+ await expect(page.getByTestId('params-via-hook')).toContainText(postId)
+ await expect(page.getByTestId('params-via-route-hook')).toContainText(postId)
+ await expect(page.getByTestId('params-via-route-api')).toContainText(postId)
+})
+
+test('Should change title on client side navigation', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+
+ await expect(page).toHaveTitle('Posts page')
+})
+
+test('Should change post navigating back and forth', async ({ page }) => {
+ await page.goto('/posts/1')
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+
+ await page.getByRole('link', { name: 'qui est esse' }).click()
+ await expect(page.getByTestId('post-title')).toContainText('qui est esse')
+
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toContainText('sunt aut facere')
+})
+
+test('Should not remount deps when remountDeps does not change ', async ({
+ page,
+}) => {
+ await page.goto('/notRemountDeps')
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+})
+
+test('Should remount deps when remountDeps does change ', async ({ page }) => {
+ await page.goto('/remountDeps')
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 2',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 3',
+ )
+})
+
+test.describe('Unicode route rendering', () => {
+ test('should render non-latin route correctly', async ({ page, baseURL }) => {
+ await page.goto('/대한민국')
+
+ await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+
+ expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ })
+})
diff --git a/e2e/vue-router/basic-file-based-jsx/tests/setup/global.setup.ts b/e2e/vue-router/basic-file-based-jsx/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/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-file-based-jsx/tests/setup/global.teardown.ts b/e2e/vue-router/basic-file-based-jsx/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/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-file-based-jsx/tsconfig.json b/e2e/vue-router/basic-file-based-jsx/tsconfig.json
new file mode 100644
index 00000000000..726b87b95ab
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "strict": true,
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "types": ["vite/client"],
+ "jsx": "preserve",
+ "jsxImportSource": "vue"
+ },
+ "include": ["src", "tests"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/vue-router/basic-file-based-jsx/vite.config.ts b/e2e/vue-router/basic-file-based-jsx/vite.config.ts
new file mode 100644
index 00000000000..570e6cff2dc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-jsx/vite.config.ts
@@ -0,0 +1,16 @@
+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,
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/eslint.config.js b/e2e/vue-router/basic-file-based-sfc/eslint.config.js
new file mode 100644
index 00000000000..4e8d00f1d89
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/eslint.config.js
@@ -0,0 +1,49 @@
+import js from '@eslint/js'
+import typescript from '@typescript-eslint/eslint-plugin'
+import typescriptParser from '@typescript-eslint/parser'
+import vue from 'eslint-plugin-vue'
+import vueParser from 'vue-eslint-parser'
+
+export default [
+ js.configs.recommended,
+ ...vue.configs['flat/recommended'],
+ {
+ files: ['**/*.{js,jsx,ts,tsx,vue}'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ plugins: {
+ '@typescript-eslint': typescript,
+ vue,
+ },
+ rules: {
+ // Vue specific rules
+ 'vue/multi-word-component-names': 'off',
+ 'vue/no-unused-vars': 'error',
+
+ // TypeScript rules
+ '@typescript-eslint/no-unused-vars': 'error',
+ '@typescript-eslint/no-explicit-any': 'warn',
+
+ // General rules
+ 'no-unused-vars': 'off', // Let TypeScript handle this
+ },
+ },
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ },
+ },
+ },
+]
diff --git a/e2e/vue-router/basic-file-based-sfc/index.html b/e2e/vue-router/basic-file-based-sfc/index.html
new file mode 100644
index 00000000000..eaa17316cdb
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/package.json b/e2e/vue-router/basic-file-based-sfc/package.json
new file mode 100644
index 00000000000..69344f374b4
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "tanstack-router-e2e-vue-basic-file-sfc",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && vue-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/vue-router": "workspace:^",
+ "@tanstack/vue-router-devtools": "workspace:^",
+ "@tanstack/zod-adapter": "workspace:^",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.15",
+ "vue": "^3.5.16",
+ "zod": "^3.24.2"
+ },
+ "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": "^2.2.0"
+ }
+}
diff --git a/e2e/vue-router/basic-file-based-sfc/playwright.config.ts b/e2e/vue-router/basic-file-based-sfc/playwright.config.ts
new file mode 100644
index 00000000000..836ea41bd8c
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/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-file-based-sfc/postcss.config.mjs b/e2e/vue-router/basic-file-based-sfc/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/EditingAComponent.tsx b/e2e/vue-router/basic-file-based-sfc/src/components/EditingAComponent.tsx
new file mode 100644
index 00000000000..9406a081951
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/EditingAComponent.tsx
@@ -0,0 +1,41 @@
+import { ref, defineComponent } from 'vue'
+import { useBlocker, useNavigate } from '@tanstack/vue-router'
+
+export const EditingAComponent = defineComponent({
+ setup() {
+ const navigate = useNavigate()
+ const input = ref('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: ({ next }) => {
+ if (next.fullPath === '/editing-b' && input.value.length > 0) {
+ return true
+ }
+ return false
+ },
+ withResolver: true,
+ })
+
+ return () => (
+
+
Editing A
+
+
+ {blocker.value.status === 'blocked' && (
+
+ )}
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/EditingBComponent.tsx b/e2e/vue-router/basic-file-based-sfc/src/components/EditingBComponent.tsx
new file mode 100644
index 00000000000..444f94de8e3
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/EditingBComponent.tsx
@@ -0,0 +1,34 @@
+import { ref, toValue, defineComponent } from 'vue'
+import { useBlocker, useNavigate } from '@tanstack/vue-router'
+
+export const EditingBComponent = defineComponent({
+ setup() {
+ const navigate = useNavigate()
+ const input = ref('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: () => !!toValue(input),
+ withResolver: true,
+ })
+
+ return () => (
+
+
Editing B
+
+
+ {blocker.value.status === 'blocked' && (
+
+ )}
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/NotFoundComponent.vue b/e2e/vue-router/basic-file-based-sfc/src/components/NotFoundComponent.vue
new file mode 100644
index 00000000000..09ed5cf5ffd
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/NotFoundComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/NotRemountDepsComponent.tsx b/e2e/vue-router/basic-file-based-sfc/src/components/NotRemountDepsComponent.tsx
new file mode 100644
index 00000000000..bd11d54fc72
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/NotRemountDepsComponent.tsx
@@ -0,0 +1,37 @@
+import { ref, onMounted, defineComponent } from 'vue'
+import { useSearch, useNavigate } from '@tanstack/vue-router'
+
+export const NotRemountDepsComponent = defineComponent({
+ setup() {
+ // Component-scoped ref - will be recreated on component remount
+ const mounts = ref(0)
+ const search = useSearch({ from: '/notRemountDeps' })
+ const navigate = useNavigate()
+
+ onMounted(() => {
+ mounts.value++
+ })
+
+ return () => (
+
+
+
+
Search: {search.value.searchParam}
+
+ Page component mounts: {mounts.value}
+
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/PostErrorComponent.vue b/e2e/vue-router/basic-file-based-sfc/src/components/PostErrorComponent.vue
new file mode 100644
index 00000000000..9bb7514ccc5
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/PostErrorComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/RemountDepsComponent.tsx b/e2e/vue-router/basic-file-based-sfc/src/components/RemountDepsComponent.tsx
new file mode 100644
index 00000000000..ecb79aeca9c
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/RemountDepsComponent.tsx
@@ -0,0 +1,38 @@
+import { ref, onMounted, defineComponent } from 'vue'
+import { useSearch, useNavigate } from '@tanstack/vue-router'
+
+// Module-scoped ref to persist across component remounts
+const mounts = ref(0)
+
+export const RemountDepsComponent = defineComponent({
+ setup() {
+ const search = useSearch({ from: '/remountDeps' })
+ const navigate = useNavigate()
+
+ onMounted(() => {
+ mounts.value++
+ })
+
+ return () => (
+
+
+
+
Search: {search.value.searchParam}
+
+ Page component mounts: {mounts.value}
+
+
+ )
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/components/VueLogo.vue b/e2e/vue-router/basic-file-based-sfc/src/components/VueLogo.vue
new file mode 100644
index 00000000000..b0f134316b0
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/components/VueLogo.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ {{ countLabel }}
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/main.ts b/e2e/vue-router/basic-file-based-sfc/src/main.ts
new file mode 100644
index 00000000000..98f9482d20b
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/main.ts
@@ -0,0 +1,30 @@
+import { createApp, h } 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) {
+ const app = createApp({
+ setup() {
+ return () => h(RouterProvider, { router })
+ },
+ })
+ app.mount('#app')
+}
diff --git a/e2e/vue-router/basic-file-based-sfc/src/posts.ts b/e2e/vue-router/basic-file-based-sfc/src/posts.ts
new file mode 100644
index 00000000000..1b4c92b41d7
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/posts.ts
@@ -0,0 +1,38 @@
+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}...`)
+ 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/vue-router/basic-file-based-sfc/src/routeTree.gen.ts b/e2e/vue-router/basic-file-based-sfc/src/routeTree.gen.ts
new file mode 100644
index 00000000000..ed2e49db39c
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routeTree.gen.ts
@@ -0,0 +1,612 @@
+/* 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 { lazyRouteComponent } from '@tanstack/vue-router'
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as RemountDepsRouteImport } from './routes/remountDeps'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
+import { Route as EditingBRouteImport } from './routes/editing-b'
+import { Route as EditingARouteImport } from './routes/editing-a'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
+import { Route as groupInsideRouteImport } from './routes/(group)/inside'
+import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
+import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
+import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/대한민국.component.vue'),
+ 'default',
+ ),
+ })
+const RemountDepsRoute = RemountDepsRouteImport.update({
+ id: '/remountDeps',
+ path: '/remountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/remountDeps.component.vue'),
+ 'default',
+ ),
+})
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts.component.vue'),
+ 'default',
+ ),
+})
+const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
+ id: '/notRemountDeps',
+ path: '/notRemountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/notRemountDeps.component.vue'),
+ 'default',
+ ),
+})
+const EditingBRoute = EditingBRouteImport.update({
+ id: '/editing-b',
+ path: '/editing-b',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/editing-b.component.vue'),
+ 'default',
+ ),
+})
+const EditingARoute = EditingARouteImport.update({
+ id: '/editing-a',
+ path: '/editing-a',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/editing-a.component.vue'),
+ 'default',
+ ),
+})
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout.component.vue'),
+ 'default',
+ ),
+})
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/index.component.vue'),
+ 'default',
+ ),
+})
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts.index.component.vue'),
+ 'default',
+ ),
+})
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts.$postId.component.vue'),
+ 'default',
+ ),
+ errorComponent: lazyRouteComponent(
+ () => import('./routes/posts.$postId.errorComponent.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2.component.vue'),
+ 'default',
+ ),
+})
+const groupLazyinsideRoute = groupLazyinsideRouteImport
+ .update({
+ id: '/(group)/lazyinside',
+ path: '/lazyinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/lazyinside.component.vue'),
+ 'default',
+ ),
+ })
+const groupInsideRoute = groupInsideRouteImport
+ .update({
+ id: '/(group)/inside',
+ path: '/inside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/inside.component.vue'),
+ 'default',
+ ),
+ })
+const groupLayoutRoute = groupLayoutRouteImport
+ .update({
+ id: '/(group)/_layout',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/_layout.component.vue'),
+ 'default',
+ ),
+ })
+const anotherGroupOnlyrouteinsideRoute = anotherGroupOnlyrouteinsideRouteImport
+ .update({
+ id: '/(another-group)/onlyrouteinside',
+ path: '/onlyrouteinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(another-group)/onlyrouteinside.component.vue'),
+ 'default',
+ ),
+ })
+const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
+ id: '/posts_/$postId/edit',
+ path: '/posts/$postId/edit',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts_.$postId.edit.component.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2/layout-b.component.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2/layout-a.component.vue'),
+ 'default',
+ ),
+})
+const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport
+ .update({
+ id: '/(group)/subfolder/inside',
+ path: '/subfolder/inside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/subfolder/inside.component.vue'),
+ 'default',
+ ),
+ })
+const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport
+ .update({
+ id: '/insidelayout',
+ path: '/insidelayout',
+ getParentRoute: () => groupLayoutRoute,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/_layout.insidelayout.component.vue'),
+ 'default',
+ ),
+ })
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/remountDeps': typeof RemountDepsRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/_layout': typeof LayoutRouteWithChildren
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/(another-group)/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/(group)/_layout': typeof groupLayoutRouteWithChildren
+ '/(group)/inside': typeof groupInsideRoute
+ '/(group)/lazyinside': typeof groupLazyinsideRoute
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts_/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/remountDeps'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ id:
+ | '__root__'
+ | '/'
+ | '/_layout'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/대한민국'
+ | '/(another-group)/onlyrouteinside'
+ | '/(group)/_layout'
+ | '/(group)/inside'
+ | '/(group)/lazyinside'
+ | '/_layout/_layout-2'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/(group)/_layout/insidelayout'
+ | '/(group)/subfolder/inside'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ | '/posts_/$postId/edit'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ LayoutRoute: typeof LayoutRouteWithChildren
+ EditingARoute: typeof EditingARoute
+ EditingBRoute: typeof EditingBRoute
+ NotRemountDepsRoute: typeof NotRemountDepsRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ RemountDepsRoute: typeof RemountDepsRoute
+ Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
+ anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
+ groupLayoutRoute: typeof groupLayoutRouteWithChildren
+ groupInsideRoute: typeof groupInsideRoute
+ groupLazyinsideRoute: typeof groupLazyinsideRoute
+ groupSubfolderInsideRoute: typeof groupSubfolderInsideRoute
+ PostsPostIdEditRoute: typeof PostsPostIdEditRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/remountDeps': {
+ id: '/remountDeps'
+ path: '/remountDeps'
+ fullPath: '/remountDeps'
+ preLoaderRoute: typeof RemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/notRemountDeps': {
+ id: '/notRemountDeps'
+ path: '/notRemountDeps'
+ fullPath: '/notRemountDeps'
+ preLoaderRoute: typeof NotRemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-b': {
+ id: '/editing-b'
+ path: '/editing-b'
+ fullPath: '/editing-b'
+ preLoaderRoute: typeof EditingBRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-a': {
+ id: '/editing-a'
+ path: '/editing-a'
+ fullPath: '/editing-a'
+ preLoaderRoute: typeof EditingARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
+ '/(group)/lazyinside': {
+ id: '/(group)/lazyinside'
+ path: '/lazyinside'
+ fullPath: '/lazyinside'
+ preLoaderRoute: typeof groupLazyinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/inside': {
+ id: '/(group)/inside'
+ path: '/inside'
+ fullPath: '/inside'
+ preLoaderRoute: typeof groupInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout': {
+ id: '/(group)/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof groupLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(another-group)/onlyrouteinside': {
+ id: '/(another-group)/onlyrouteinside'
+ path: '/onlyrouteinside'
+ fullPath: '/onlyrouteinside'
+ preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts_/$postId/edit': {
+ id: '/posts_/$postId/edit'
+ path: '/posts/$postId/edit'
+ fullPath: '/posts/$postId/edit'
+ preLoaderRoute: typeof PostsPostIdEditRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/(group)/subfolder/inside': {
+ id: '/(group)/subfolder/inside'
+ path: '/subfolder/inside'
+ fullPath: '/subfolder/inside'
+ preLoaderRoute: typeof groupSubfolderInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout/insidelayout': {
+ id: '/(group)/_layout/insidelayout'
+ path: '/insidelayout'
+ fullPath: '/insidelayout'
+ preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
+ parentRoute: typeof groupLayoutRoute
+ }
+ }
+}
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface groupLayoutRouteChildren {
+ groupLayoutInsidelayoutRoute: typeof groupLayoutInsidelayoutRoute
+}
+
+const groupLayoutRouteChildren: groupLayoutRouteChildren = {
+ groupLayoutInsidelayoutRoute: groupLayoutInsidelayoutRoute,
+}
+
+const groupLayoutRouteWithChildren = groupLayoutRoute._addFileChildren(
+ groupLayoutRouteChildren,
+)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ LayoutRoute: LayoutRouteWithChildren,
+ EditingARoute: EditingARoute,
+ EditingBRoute: EditingBRoute,
+ NotRemountDepsRoute: NotRemountDepsRoute,
+ PostsRoute: PostsRouteWithChildren,
+ RemountDepsRoute: RemountDepsRoute,
+ Char45824Char54620Char48124Char44397Route:
+ Char45824Char54620Char48124Char44397Route,
+ anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
+ groupLayoutRoute: groupLayoutRouteWithChildren,
+ groupInsideRoute: groupInsideRoute,
+ groupLazyinsideRoute: groupLazyinsideRoute,
+ groupSubfolderInsideRoute: groupSubfolderInsideRoute,
+ PostsPostIdEditRoute: PostsPostIdEditRoute,
+}
+export const routeTree = rootRouteImport
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/__root.component.vue'),
+ 'default',
+ ),
+ notFoundComponent: lazyRouteComponent(
+ () => import('./routes/__root.notFoundComponent.vue'),
+ 'default',
+ ),
+ })
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.component.vue
new file mode 100644
index 00000000000..4d998b9ff97
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.ts
new file mode 100644
index 00000000000..491bb03649f
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(another-group)/onlyrouteinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.component.vue
new file mode 100644
index 00000000000..9c2990a0293
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.component.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
Layout inside group
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.component.vue
new file mode 100644
index 00000000000..5b5d4352322
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.ts
new file mode 100644
index 00000000000..49ef8f7bb9f
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/_layout/insidelayout')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.ts
new file mode 100644
index 00000000000..ce0f6ca52a2
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/_layout.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/(group)/_layout')({})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/inside.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/inside.component.vue
new file mode 100644
index 00000000000..6542777df9b
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/inside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/inside.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/inside.ts
new file mode 100644
index 00000000000..f7b06f6a94a
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/inside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/lazyinside.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/lazyinside.component.vue
new file mode 100644
index 00000000000..8e603559378
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/lazyinside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/lazyinside.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/lazyinside.ts
new file mode 100644
index 00000000000..b66d18a7299
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/lazyinside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/lazyinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/subfolder/inside.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/subfolder/inside.component.vue
new file mode 100644
index 00000000000..516f6e1e9a5
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/subfolder/inside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/subfolder/inside.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/subfolder/inside.ts
new file mode 100644
index 00000000000..50aa8a44ad2
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/(group)/subfolder/inside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/subfolder/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/__root.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/__root.component.vue
new file mode 100644
index 00000000000..08594569105
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/__root.component.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+ Home
+
+ Posts
+ Layout
+
+ Only Route Inside Group
+
+
+ Inside Group
+
+
+ Inside Subfolder Inside Group
+
+
+ Inside Group Inside Layout
+
+
+ Lazy Inside Group
+
+ unicode path
+
+ This Route Does Not Exist
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/__root.notFoundComponent.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/__root.notFoundComponent.vue
new file mode 100644
index 00000000000..09ed5cf5ffd
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/__root.notFoundComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/__root.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/__root.ts
new file mode 100644
index 00000000000..815b2762f6b
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/__root.ts
@@ -0,0 +1,3 @@
+import { createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute()
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout.component.vue
new file mode 100644
index 00000000000..68fa9808a53
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout.component.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout.ts
new file mode 100644
index 00000000000..6767e7457a3
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout')({
+ // component is loaded from _layout.component.vue
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2.component.vue
new file mode 100644
index 00000000000..66c630f37cd
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2.component.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2.ts
new file mode 100644
index 00000000000..a03c32233bc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ // component is loaded from _layout-2.component.vue
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.component.vue
new file mode 100644
index 00000000000..6fb4e11ae29
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.component.vue
@@ -0,0 +1,3 @@
+
+ I'm layout A!
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.ts
new file mode 100644
index 00000000000..098ee2e59ca
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ // component is loaded from layout-a.component.vue
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.component.vue
new file mode 100644
index 00000000000..51c4ed6f372
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.component.vue
@@ -0,0 +1,3 @@
+
+ I'm layout B!
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.ts
new file mode 100644
index 00000000000..10e4c50303e
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ // component is loaded from layout-b.component.vue
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/editing-a.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-a.component.vue
new file mode 100644
index 00000000000..1b0e1c4a100
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-a.component.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
Editing A
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/editing-a.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-a.ts
new file mode 100644
index 00000000000..ce0b9f8b075
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-a.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/editing-a')({})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/editing-b.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-b.component.vue
new file mode 100644
index 00000000000..6e57e4244ca
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-b.component.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
Editing B
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/editing-b.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-b.ts
new file mode 100644
index 00000000000..f5158fe02c4
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/editing-b.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/editing-b')({})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/index.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/index.component.vue
new file mode 100644
index 00000000000..5606f836343
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/index.component.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
Welcome Home!
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/index.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/index.ts
new file mode 100644
index 00000000000..5948d4af8bc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/index.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ // component is loaded from index.component.vue
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/notRemountDeps.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/notRemountDeps.component.vue
new file mode 100644
index 00000000000..4ec402e3b3e
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/notRemountDeps.component.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
Search: {{ search.searchParam }}
+
+ Page component mounts: {{ mounts }}
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/notRemountDeps.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/notRemountDeps.ts
new file mode 100644
index 00000000000..1a712cd75ab
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/notRemountDeps.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/notRemountDeps')({
+ validateSearch(search: { searchParam: string }) {
+ return { searchParam: search.searchParam }
+ },
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.params
+ },
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.component.vue
new file mode 100644
index 00000000000..4c85355d592
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.component.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{ post.title }}
+
+
{{ post.body }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.errorComponent.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.errorComponent.vue
new file mode 100644
index 00000000000..d02118f6590
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.errorComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.ts
new file mode 100644
index 00000000000..3824aa7f43b
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.$postId.ts
@@ -0,0 +1,8 @@
+import { h } from 'vue'
+import { createFileRoute } from '@tanstack/vue-router'
+import { fetchPost } from '../posts'
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ notFoundComponent: () => h('p', 'Post not found'),
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.component.vue
new file mode 100644
index 00000000000..2829cbaa856
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.component.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.index.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.index.component.vue
new file mode 100644
index 00000000000..97f094befe2
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.index.component.vue
@@ -0,0 +1,3 @@
+
+ Select a post.
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.index.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.index.ts
new file mode 100644
index 00000000000..647568877c5
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.index.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts/')({
+ // component is loaded from posts.index.component.vue
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.ts
new file mode 100644
index 00000000000..a216c07cb4f
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { fetchPosts } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: fetchPosts,
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts_.$postId.edit.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/posts_.$postId.edit.component.vue
new file mode 100644
index 00000000000..2c5963c94c0
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts_.$postId.edit.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ paramsViaHook.postId }}
+
+ {{ paramsViaRouteHook.postId }}
+
+
{{ paramsViaApi.postId }}
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/posts_.$postId.edit.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/posts_.$postId.edit.ts
new file mode 100644
index 00000000000..b1b1f6029ab
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/posts_.$postId.edit.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts_/$postId/edit')({})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/remountDeps.component.vue b/e2e/vue-router/basic-file-based-sfc/src/routes/remountDeps.component.vue
new file mode 100644
index 00000000000..c5aa9417742
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/remountDeps.component.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
Search: {{ search.searchParam }}
+
+ Page component mounts: {{ mounts }}
+
+
+
diff --git a/e2e/vue-router/basic-file-based-sfc/src/routes/remountDeps.ts b/e2e/vue-router/basic-file-based-sfc/src/routes/remountDeps.ts
new file mode 100644
index 00000000000..46ce88b6c09
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/routes/remountDeps.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/remountDeps')({
+ validateSearch(search: { searchParam: string }) {
+ return { searchParam: search.searchParam }
+ },
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.search
+ },
+})
diff --git "a/e2e/vue-router/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.component.vue" "b/e2e/vue-router/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.component.vue"
new file mode 100644
index 00000000000..a520ed3cd71
--- /dev/null
+++ "b/e2e/vue-router/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.component.vue"
@@ -0,0 +1,11 @@
+
+
+
+
+
Hello "/대한민국"!
+
+
+
+
diff --git "a/e2e/vue-router/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.ts" "b/e2e/vue-router/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.ts"
new file mode 100644
index 00000000000..943e319a554
--- /dev/null
+++ "b/e2e/vue-router/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.ts"
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/대한민국')({})
diff --git a/e2e/vue-router/basic-file-based-sfc/src/styles.css b/e2e/vue-router/basic-file-based-sfc/src/styles.css
new file mode 100644
index 00000000000..237cc5590d1
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/styles.css
@@ -0,0 +1,24 @@
+@import 'tailwindcss';
+
+@source "./**/*.vue";
+@source "./**/*.ts";
+
+@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-file-based-sfc/src/vue-shims.d.ts b/e2e/vue-router/basic-file-based-sfc/src/vue-shims.d.ts
new file mode 100644
index 00000000000..2b97bd961cc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/src/vue-shims.d.ts
@@ -0,0 +1,5 @@
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/e2e/vue-router/basic-file-based-sfc/test-results/.last-run.json b/e2e/vue-router/basic-file-based-sfc/test-results/.last-run.json
new file mode 100644
index 00000000000..cbcc1fbac11
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
\ No newline at end of file
diff --git a/e2e/vue-router/basic-file-based-sfc/tests/app.spec.ts b/e2e/vue-router/basic-file-based-sfc/tests/app.spec.ts
new file mode 100644
index 00000000000..543746d64d8
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/tests/app.spec.ts
@@ -0,0 +1,289 @@
+import { expect, test } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test("useBlocker doesn't block navigation if condition is not met", async ({
+ page,
+}) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+})
+
+test('useBlocker does block navigation if condition is met', async ({
+ page,
+}) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible()
+})
+
+test('Proceeding through blocked navigation works', async ({ page }) => {
+ await page.goto('/editing-a')
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go to next step' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+
+ await page.getByRole('button', { name: 'Proceed' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+})
+
+test("legacy useBlocker doesn't block navigation if condition is not met", async ({
+ page,
+}) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+})
+
+test('legacy useBlocker does block navigation if condition is met', async ({
+ page,
+}) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await expect(page.getByRole('button', { name: 'Proceed' })).toBeVisible()
+})
+
+test('legacy Proceeding through blocked navigation works', async ({ page }) => {
+ await page.goto('/editing-b')
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByLabel('Enter your name:').fill('foo')
+
+ await page.getByRole('button', { name: 'Go back' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing B')
+
+ await page.getByRole('button', { name: 'Proceed' }).click()
+ await expect(page.getByRole('heading')).toContainText('Editing A')
+})
+
+test('useCanGoBack correctly disables back button', async ({ page }) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goForward()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+test('useCanGoBack correctly disables back button, using router.history and window.history', async ({
+ page,
+}) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+const testCases = [
+ {
+ description: 'Navigating to a route inside a route group',
+ testId: 'link-to-route-inside-group',
+ },
+ {
+ description:
+ 'Navigating to a route inside a subfolder inside a route group ',
+ testId: 'link-to-route-inside-group-inside-subfolder',
+ },
+ {
+ description: 'Navigating to a route inside a route group inside a layout',
+ testId: 'link-to-route-inside-group-inside-layout',
+ },
+ {
+ description: 'Navigating to a lazy route inside a route group',
+ testId: 'link-to-lazy-route-inside-group',
+ },
+
+ {
+ description: 'Navigating to the only route inside a route group ',
+ testId: 'link-to-only-route-inside-group',
+ },
+]
+
+testCases.forEach(({ description, testId }) => {
+ test(description, async ({ page }) => {
+ await page.getByTestId(testId).click()
+ await expect(page.getByTestId('search-via-hook')).toContainText('world')
+ await expect(page.getByTestId('search-via-route-hook')).toContainText(
+ 'world',
+ )
+ await expect(page.getByTestId('search-via-route-api')).toContainText(
+ 'world',
+ )
+ })
+})
+
+test('navigating to an unnested route', async ({ page }) => {
+ const postId = 'hello-world'
+ page.goto(`/posts/${postId}/edit`)
+ await expect(page.getByTestId('params-via-hook')).toContainText(postId)
+ await expect(page.getByTestId('params-via-route-hook')).toContainText(postId)
+ await expect(page.getByTestId('params-via-route-api')).toContainText(postId)
+})
+
+test('Should change title on client side navigation', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+
+ await expect(page).toHaveTitle('Posts page')
+})
+
+test('Should change post navigating back and forth', async ({ page }) => {
+ await page.goto('/posts/1')
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+
+ await page.getByRole('link', { name: 'qui est esse' }).click()
+ await expect(page.getByTestId('post-title')).toContainText('qui est esse')
+
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toContainText('sunt aut facere')
+})
+
+test('Should not remount deps when remountDeps does not change ', async ({
+ page,
+}) => {
+ await page.goto('/notRemountDeps')
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+})
+
+test('Should remount deps when remountDeps does change ', async ({ page }) => {
+ await page.goto('/remountDeps')
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 1',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 2',
+ )
+ await page.getByRole('button', { name: 'Regenerate search param' }).click()
+ await expect(page.getByTestId('component-mounts')).toContainText(
+ 'Page component mounts: 3',
+ )
+})
+
+test.describe('Unicode route rendering', () => {
+ test('should render non-latin route correctly', async ({ page, baseURL }) => {
+ await page.goto('/대한민국')
+
+ await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+
+ expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ })
+})
diff --git a/e2e/vue-router/basic-file-based-sfc/tests/setup/global.setup.ts b/e2e/vue-router/basic-file-based-sfc/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/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-file-based-sfc/tests/setup/global.teardown.ts b/e2e/vue-router/basic-file-based-sfc/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/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-file-based-sfc/tsconfig.json b/e2e/vue-router/basic-file-based-sfc/tsconfig.json
new file mode 100644
index 00000000000..2ee97a629f4
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "strict": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src", "tests"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/vue-router/basic-file-based-sfc/vite.config.ts b/e2e/vue-router/basic-file-based-sfc/vite.config.ts
new file mode 100644
index 00000000000..570e6cff2dc
--- /dev/null
+++ b/e2e/vue-router/basic-file-based-sfc/vite.config.ts
@@ -0,0 +1,16 @@
+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,
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/e2e/vue-router/basic/.gitignore b/e2e/vue-router/basic/.gitignore
new file mode 100644
index 00000000000..a547bf36d8d
--- /dev/null
+++ b/e2e/vue-router/basic/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/e2e/vue-router/basic/index.html b/e2e/vue-router/basic/index.html
new file mode 100644
index 00000000000..21e30f16951
--- /dev/null
+++ b/e2e/vue-router/basic/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/vue-router/basic/package.json b/e2e/vue-router/basic/package.json
new file mode 100644
index 00000000000..e1c5df5614c
--- /dev/null
+++ b/e2e/vue-router/basic/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "tanstack-router-e2e-vue-basic",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && vue-tsc --noEmit",
+ "preview": "vite preview",
+ "start": "vite",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/vue-router": "workspace:^",
+ "@tanstack/vue-router-devtools": "workspace:^",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.15",
+ "vue": "^3.5.16"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7",
+ "vue-tsc": "^2.2.0"
+ }
+}
diff --git a/e2e/vue-router/basic/playwright.config.ts b/e2e/vue-router/basic/playwright.config.ts
new file mode 100644
index 00000000000..836ea41bd8c
--- /dev/null
+++ b/e2e/vue-router/basic/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/postcss.config.mjs b/e2e/vue-router/basic/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/vue-router/basic/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/vue-router/basic/src/main.tsx b/e2e/vue-router/basic/src/main.tsx
new file mode 100644
index 00000000000..3e5ac6f1e08
--- /dev/null
+++ b/e2e/vue-router/basic/src/main.tsx
@@ -0,0 +1,461 @@
+import { createApp } from 'vue'
+import {
+ ErrorComponent,
+ Link,
+ Outlet,
+ RouterProvider,
+ createLink,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ redirect,
+} from '@tanstack/vue-router'
+import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools'
+import { NotFoundError, fetchPost, fetchPosts } from './posts'
+import './styles.css'
+import type { ErrorComponentProps } from '@tanstack/vue-router'
+
+const rootRoute = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ const SvgLink = createLink('svg')
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ View Transition
+ {' '}
+
+ View Transition types
+ {' '}
+
+ Layout
+ {' '}
+
+ This Route Does Not Exist
+ {' '}
+
+
+
+
+
+
+ >
+ )
+}
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+ )
+}
+
+export const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'posts',
+ loader: () => fetchPosts(),
+}).lazy(() => import('./posts.lazy').then((d) => d.Route))
+
+const postsIndexRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '/',
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
+
+function PostErrorComponent({ error }: ErrorComponentProps) {
+ if (error instanceof NotFoundError) {
+ return {error.message}
+ }
+
+ return
+}
+
+const postRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '$postId',
+ errorComponent: PostErrorComponent,
+ loader: ({ params }) => fetchPost(params.postId),
+ component: PostComponent,
+})
+
+function PostComponent() {
+ const post = postRoute.useLoaderData()
+
+ return (
+
+
{post.value.title}
+
+
{post.value.body}
+
+ )
+}
+
+const layoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_layout',
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
+
+const layout2Route = createRoute({
+ getParentRoute: () => layoutRoute,
+ id: '_layout-2',
+ component: Layout2Component,
+})
+
+function Layout2Component() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
+
+const layoutARoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-a',
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
+
+const layoutBRoute = createRoute({
+ getParentRoute: () => layout2Route,
+ path: '/layout-b',
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
+
+const paramsPsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/params-ps',
+})
+
+const paramsPsIndexRoute = createRoute({
+ getParentRoute: () => paramsPsRoute,
+ path: '/',
+ component: function ParamsIndex() {
+ return (
+
+
Named path params
+
+ -
+
+ /params-ps/named/$foo
+
+
+ -
+
+ /params-ps/named/{'prefix{$foo}'}
+
+
+ -
+
+ /params-ps/named/{'{$foo}suffix'}
+
+
+
+
+
Wildcard path params
+
+ -
+
+ /params-ps/wildcard/$
+
+
+ -
+
+ /params-ps/wildcard/{'prefix{$}'}
+
+
+ -
+
+ /params-ps/wildcard/{'{$}suffix'}
+
+
+
+
+ )
+ },
+})
+
+const paramsPsNamedRoute = createRoute({
+ getParentRoute: () => paramsPsRoute,
+ path: '/named',
+})
+
+const paramsPsNamedIndexRoute = createRoute({
+ getParentRoute: () => paramsPsNamedRoute,
+ path: '/',
+ beforeLoad: () => {
+ throw redirect({ to: '/params-ps' })
+ },
+})
+
+const paramsPsNamedFooRoute = createRoute({
+ getParentRoute: () => paramsPsNamedRoute,
+ path: '/$foo',
+ component: function ParamsNamedFoo() {
+ const p = paramsPsNamedFooRoute.useParams()
+ return (
+
+
ParamsNamedFoo
+
{JSON.stringify(p.value)}
+
+ )
+ },
+})
+
+const paramsPsNamedFooPrefixRoute = createRoute({
+ getParentRoute: () => paramsPsNamedRoute,
+ path: '/prefix{$foo}',
+ component: function ParamsNamedFooMarkdown() {
+ const p = paramsPsNamedFooPrefixRoute.useParams()
+ return (
+
+
ParamsNamedFooPrefix
+
{JSON.stringify(p.value)}
+
+ )
+ },
+})
+
+const paramsPsNamedFooSuffixRoute = createRoute({
+ getParentRoute: () => paramsPsNamedRoute,
+ path: '/{$foo}suffix',
+ component: function ParamsNamedFooSuffix() {
+ const p = paramsPsNamedFooSuffixRoute.useParams()
+ return (
+
+
ParamsNamedFooSuffix
+
{JSON.stringify(p.value)}
+
+ )
+ },
+})
+
+const paramsPsWildcardRoute = createRoute({
+ getParentRoute: () => paramsPsRoute,
+ path: '/wildcard',
+})
+
+const paramsPsWildcardIndexRoute = createRoute({
+ getParentRoute: () => paramsPsWildcardRoute,
+ path: '/',
+ beforeLoad: () => {
+ throw redirect({ to: '/params-ps' })
+ },
+})
+
+const paramsPsWildcardSplatRoute = createRoute({
+ getParentRoute: () => paramsPsWildcardRoute,
+ path: '$',
+ component: function ParamsWildcardSplat() {
+ const p = paramsPsWildcardSplatRoute.useParams()
+ return (
+
+
ParamsWildcardSplat
+
{JSON.stringify(p.value)}
+
+ )
+ },
+})
+
+const paramsPsWildcardSplatPrefixRoute = createRoute({
+ getParentRoute: () => paramsPsWildcardRoute,
+ path: 'prefix{$}',
+ component: function ParamsWildcardSplatPrefix() {
+ const p = paramsPsWildcardSplatPrefixRoute.useParams()
+ return (
+
+
ParamsWildcardSplatPrefix
+
{JSON.stringify(p.value)}
+
+ )
+ },
+})
+
+const paramsPsWildcardSplatSuffixRoute = createRoute({
+ getParentRoute: () => paramsPsWildcardRoute,
+ path: '{$}suffix',
+ component: function ParamsWildcardSplatSuffix() {
+ const p = paramsPsWildcardSplatSuffixRoute.useParams()
+ return (
+
+
ParamsWildcardSplatSuffix
+
{JSON.stringify(p.value)}
+
+ )
+ },
+})
+
+const routeTree = rootRoute.addChildren([
+ postsRoute.addChildren([postRoute, postsIndexRoute]),
+ layoutRoute.addChildren([
+ layout2Route.addChildren([layoutARoute, layoutBRoute]),
+ ]),
+ paramsPsRoute.addChildren([
+ paramsPsNamedRoute.addChildren([
+ paramsPsNamedFooPrefixRoute,
+ paramsPsNamedFooSuffixRoute,
+ paramsPsNamedFooRoute,
+ paramsPsNamedIndexRoute,
+ ]),
+ paramsPsWildcardRoute.addChildren([
+ paramsPsWildcardSplatRoute,
+ paramsPsWildcardSplatPrefixRoute,
+ paramsPsWildcardSplatSuffixRoute,
+ paramsPsWildcardIndexRoute,
+ ]),
+ paramsPsIndexRoute,
+ ]),
+ indexRoute,
+])
+
+// 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/src/posts.lazy.tsx b/e2e/vue-router/basic/src/posts.lazy.tsx
new file mode 100644
index 00000000000..b2df740010a
--- /dev/null
+++ b/e2e/vue-router/basic/src/posts.lazy.tsx
@@ -0,0 +1,36 @@
+import { Link, Outlet, createLazyRoute } from '@tanstack/vue-router'
+
+export const Route = createLazyRoute('/posts')({
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/vue-router/basic/src/posts.ts b/e2e/vue-router/basic/src/posts.ts
new file mode 100644
index 00000000000..e859645746b
--- /dev/null
+++ b/e2e/vue-router/basic/src/posts.ts
@@ -0,0 +1,36 @@
+import axios from 'redaxios'
+
+export class NotFoundError extends Error {}
+
+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 fetchPosts = async () => {
+ console.info('Fetching posts...')
+ return axios
+ .get>(`${queryURL}/posts`)
+ .then((r) => r.data.slice(0, 10))
+}
+
+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)
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!post) {
+ throw new NotFoundError(`Post with id "${postId}" not found!`)
+ }
+
+ return post
+}
diff --git a/e2e/vue-router/basic/src/styles.css b/e2e/vue-router/basic/src/styles.css
new file mode 100644
index 00000000000..37a1064738a
--- /dev/null
+++ b/e2e/vue-router/basic/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/test-results/.last-run.json b/e2e/vue-router/basic/test-results/.last-run.json
new file mode 100644
index 00000000000..cbcc1fbac11
--- /dev/null
+++ b/e2e/vue-router/basic/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
\ No newline at end of file
diff --git a/e2e/vue-router/basic/tests/app.spec.ts b/e2e/vue-router/basic/tests/app.spec.ts
new file mode 100644
index 00000000000..ec0e24d8a40
--- /dev/null
+++ b/e2e/vue-router/basic/tests/app.spec.ts
@@ -0,0 +1,64 @@
+import { expect, test } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from '../package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+ await page.getByRole('link', { name: 'Posts', exact: true }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+ await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+ await expect(page.locator('#app')).toContainText("I'm a layout")
+ await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+ await page.getByRole('link', { name: 'Layout A' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+ await page.getByRole('link', { name: 'Layout B' }).click()
+ await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await expect(page.getByRole('paragraph')).toContainText(
+ 'This is the notFoundComponent configured on root route',
+ )
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test('Navigating to a post page with viewTransition', async ({ page }) => {
+ await page.getByRole('link', { name: 'View Transition', exact: true }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating to a post page with viewTransition types', async ({
+ page,
+}) => {
+ await page.getByRole('link', { name: 'View Transition types' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Link in SVG does not trigger a full page reload', async ({ page }) => {
+ let fullPageLoad = false
+ page.on('domcontentloaded', () => {
+ fullPageLoad = true
+ })
+
+ await page.getByRole('link', { name: 'Open posts from SVG' }).click()
+ const url = `http://localhost:${PORT}/posts`
+ await page.waitForURL(url)
+
+ expect(fullPageLoad).toBeFalsy()
+})
diff --git a/e2e/vue-router/basic/tests/params.spec.ts b/e2e/vue-router/basic/tests/params.spec.ts
new file mode 100644
index 00000000000..ac18f39864a
--- /dev/null
+++ b/e2e/vue-router/basic/tests/params.spec.ts
@@ -0,0 +1,149 @@
+import { expect, test } from '@playwright/test'
+
+test.describe('params operations + prefix/suffix', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/params-ps')
+ })
+
+ test.describe('named params', () => {
+ const NAMED_PARAMS_PAIRS = [
+ // Test ID | Expected href
+ {
+ id: 'l-to-named-foo',
+ pathname: '/params-ps/named/foo',
+ params: { foo: 'foo' },
+ destHeadingId: 'ParamsNamedFoo',
+ },
+ {
+ id: 'l-to-named-prefixfoo',
+ pathname: '/params-ps/named/prefixfoo',
+ params: { foo: 'foo' },
+ destHeadingId: 'ParamsNamedFooPrefix',
+ },
+ {
+ id: 'l-to-named-foosuffix',
+ pathname: '/params-ps/named/foosuffix',
+ params: { foo: 'foo' },
+ destHeadingId: 'ParamsNamedFooSuffix',
+ },
+ ] satisfies Array<{
+ id: string
+ pathname: string
+ params: Record
+ destHeadingId: string
+ }>
+
+ test.describe('Link', () => {
+ NAMED_PARAMS_PAIRS.forEach(({ id, pathname }) => {
+ test(`interpolation for testid="${id}" has href="${pathname}"`, async ({
+ page,
+ }) => {
+ const link = page.getByTestId(id)
+ await expect(link).toHaveAttribute('href', pathname)
+ })
+ })
+
+ NAMED_PARAMS_PAIRS.forEach(({ id, pathname }) => {
+ test(`navigation for testid="${id}" succeeds to href="${pathname}"`, async ({
+ page,
+ }) => {
+ const link = page.getByTestId(id)
+ await link.click()
+ await page.waitForLoadState('networkidle')
+ const pagePathname = new URL(page.url()).pathname
+ expect(pagePathname).toBe(pathname)
+ })
+ })
+ })
+
+ NAMED_PARAMS_PAIRS.forEach(({ pathname, params, destHeadingId }) => {
+ test(`on first-load to "${pathname}" has correct params`, async ({
+ page,
+ }) => {
+ await page.goto(pathname)
+ await page.waitForLoadState('networkidle')
+ const pagePathname = new URL(page.url()).pathname
+ expect(pagePathname).toBe(pathname)
+
+ const headingEl = page.getByRole('heading', { name: destHeadingId })
+ await expect(headingEl).toBeVisible()
+
+ const paramsEl = page.getByTestId('params-output')
+ const paramsText = await paramsEl.innerText()
+ const paramsObj = JSON.parse(paramsText)
+ expect(paramsObj).toEqual(params)
+ })
+ })
+ })
+
+ test.describe('wildcard param', () => {
+ const WILDCARD_PARAM_PAIRS = [
+ // Test ID | Expected href
+ {
+ id: 'l-to-wildcard-foo',
+ pathname: '/params-ps/wildcard/foo',
+ params: { '*': 'foo', _splat: 'foo' },
+ destHeadingId: 'ParamsWildcardSplat',
+ },
+ {
+ id: 'l-to-wildcard-prefixfoo',
+ pathname: '/params-ps/wildcard/prefixfoo',
+ params: { '*': 'foo', _splat: 'foo' },
+ destHeadingId: 'ParamsWildcardSplatPrefix',
+ },
+ {
+ id: 'l-to-wildcard-foosuffix',
+ pathname: '/params-ps/wildcard/foosuffix',
+ params: { '*': 'foo', _splat: 'foo' },
+ destHeadingId: 'ParamsWildcardSplatSuffix',
+ },
+ ] satisfies Array<{
+ id: string
+ pathname: string
+ params: Record
+ destHeadingId: string
+ }>
+
+ test.describe('Link', () => {
+ WILDCARD_PARAM_PAIRS.forEach(({ id, pathname }) => {
+ test(`interpolation for testid="${id}" has href="${pathname}"`, async ({
+ page,
+ }) => {
+ const link = page.getByTestId(id)
+ await expect(link).toHaveAttribute('href', pathname)
+ })
+ })
+
+ WILDCARD_PARAM_PAIRS.forEach(({ id, pathname }) => {
+ test(`navigation for testid="${id}" succeeds to href="${pathname}"`, async ({
+ page,
+ }) => {
+ const link = page.getByTestId(id)
+ await link.click()
+ await page.waitForLoadState('networkidle')
+ const pagePathname = new URL(page.url()).pathname
+ expect(pagePathname).toBe(pathname)
+ })
+ })
+ })
+
+ WILDCARD_PARAM_PAIRS.forEach(({ pathname, params, destHeadingId }) => {
+ test(`on first-load to "${pathname}" has correct params`, async ({
+ page,
+ }) => {
+ await page.goto(pathname)
+ await page.waitForLoadState('networkidle')
+ const pagePathname = new URL(page.url()).pathname
+ expect(pagePathname).toBe(pathname)
+
+ const headingEl = page.getByRole('heading', { name: destHeadingId })
+ await expect(headingEl).toBeVisible()
+
+ const paramsEl = page.getByTestId('params-output')
+ const paramsText = await paramsEl.innerText()
+ const paramsObj = JSON.parse(paramsText)
+ expect(paramsObj).toEqual(params)
+ })
+ })
+ })
+})
diff --git a/e2e/vue-router/basic/tests/setup/global.setup.ts b/e2e/vue-router/basic/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/vue-router/basic/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/tests/setup/global.teardown.ts b/e2e/vue-router/basic/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/vue-router/basic/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/tsconfig.json b/e2e/vue-router/basic/tsconfig.json
new file mode 100644
index 00000000000..402411ef2b3
--- /dev/null
+++ b/e2e/vue-router/basic/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "strict": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/vue-router/basic/vite.config.js b/e2e/vue-router/basic/vite.config.js
new file mode 100644
index 00000000000..20045a7b4f7
--- /dev/null
+++ b/e2e/vue-router/basic/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vueJsx()],
+})
diff --git a/examples/vue/basic-file-based-jsx/eslint.config.js b/examples/vue/basic-file-based-jsx/eslint.config.js
new file mode 100644
index 00000000000..4e8d00f1d89
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/eslint.config.js
@@ -0,0 +1,49 @@
+import js from '@eslint/js'
+import typescript from '@typescript-eslint/eslint-plugin'
+import typescriptParser from '@typescript-eslint/parser'
+import vue from 'eslint-plugin-vue'
+import vueParser from 'vue-eslint-parser'
+
+export default [
+ js.configs.recommended,
+ ...vue.configs['flat/recommended'],
+ {
+ files: ['**/*.{js,jsx,ts,tsx,vue}'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ plugins: {
+ '@typescript-eslint': typescript,
+ vue,
+ },
+ rules: {
+ // Vue specific rules
+ 'vue/multi-word-component-names': 'off',
+ 'vue/no-unused-vars': 'error',
+
+ // TypeScript rules
+ '@typescript-eslint/no-unused-vars': 'error',
+ '@typescript-eslint/no-explicit-any': 'warn',
+
+ // General rules
+ 'no-unused-vars': 'off', // Let TypeScript handle this
+ },
+ },
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ },
+ },
+ },
+]
diff --git a/examples/vue/basic-file-based-jsx/index.html b/examples/vue/basic-file-based-jsx/index.html
new file mode 100644
index 00000000000..21e30f16951
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-jsx/package.json b/examples/vue/basic-file-based-jsx/package.json
new file mode 100644
index 00000000000..797612d2172
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "tanstack-router-vue-example-basic-file-based-jsx",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "build": "vite build && vue-tsc --noEmit",
+ "preview": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-plugin": "^1.139.14",
+ "@tanstack/vue-router": "workspace:*",
+ "@tanstack/vue-router-devtools": "workspace:*",
+ "@tanstack/zod-adapter": "^1.139.14",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.15",
+ "vue": "^3.5.16",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.36.0",
+ "@typescript-eslint/eslint-plugin": "^8.44.1",
+ "@typescript-eslint/parser": "^8.44.1",
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "eslint-plugin-vue": "^9.33.0",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7",
+ "vue-eslint-parser": "^9.4.3",
+ "vue-tsc": "^3.1.5"
+ }
+}
diff --git a/examples/vue/basic-file-based-jsx/postcss.config.mjs b/examples/vue/basic-file-based-jsx/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/examples/vue/basic-file-based-jsx/src/components/EditingAComponent.tsx b/examples/vue/basic-file-based-jsx/src/components/EditingAComponent.tsx
new file mode 100644
index 00000000000..9406a081951
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/EditingAComponent.tsx
@@ -0,0 +1,41 @@
+import { ref, defineComponent } from 'vue'
+import { useBlocker, useNavigate } from '@tanstack/vue-router'
+
+export const EditingAComponent = defineComponent({
+ setup() {
+ const navigate = useNavigate()
+ const input = ref('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: ({ next }) => {
+ if (next.fullPath === '/editing-b' && input.value.length > 0) {
+ return true
+ }
+ return false
+ },
+ withResolver: true,
+ })
+
+ return () => (
+
+
Editing A
+
+
+ {blocker.value.status === 'blocked' && (
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/vue/basic-file-based-jsx/src/components/EditingBComponent.tsx b/examples/vue/basic-file-based-jsx/src/components/EditingBComponent.tsx
new file mode 100644
index 00000000000..444f94de8e3
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/EditingBComponent.tsx
@@ -0,0 +1,34 @@
+import { ref, toValue, defineComponent } from 'vue'
+import { useBlocker, useNavigate } from '@tanstack/vue-router'
+
+export const EditingBComponent = defineComponent({
+ setup() {
+ const navigate = useNavigate()
+ const input = ref('')
+
+ const blocker = useBlocker({
+ shouldBlockFn: () => !!toValue(input),
+ withResolver: true,
+ })
+
+ return () => (
+
+
Editing B
+
+
+ {blocker.value.status === 'blocked' && (
+
+ )}
+
+ )
+ },
+})
diff --git a/examples/vue/basic-file-based-jsx/src/components/NotFoundComponent.vue b/examples/vue/basic-file-based-jsx/src/components/NotFoundComponent.vue
new file mode 100644
index 00000000000..09ed5cf5ffd
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/NotFoundComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+
diff --git a/examples/vue/basic-file-based-jsx/src/components/NotRemountDepsComponent.tsx b/examples/vue/basic-file-based-jsx/src/components/NotRemountDepsComponent.tsx
new file mode 100644
index 00000000000..bd11d54fc72
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/NotRemountDepsComponent.tsx
@@ -0,0 +1,37 @@
+import { ref, onMounted, defineComponent } from 'vue'
+import { useSearch, useNavigate } from '@tanstack/vue-router'
+
+export const NotRemountDepsComponent = defineComponent({
+ setup() {
+ // Component-scoped ref - will be recreated on component remount
+ const mounts = ref(0)
+ const search = useSearch({ from: '/notRemountDeps' })
+ const navigate = useNavigate()
+
+ onMounted(() => {
+ mounts.value++
+ })
+
+ return () => (
+
+
+
+
Search: {search.value.searchParam}
+
+ Page component mounts: {mounts.value}
+
+
+ )
+ },
+})
diff --git a/examples/vue/basic-file-based-jsx/src/components/PostErrorComponent.vue b/examples/vue/basic-file-based-jsx/src/components/PostErrorComponent.vue
new file mode 100644
index 00000000000..9bb7514ccc5
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/PostErrorComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-jsx/src/components/RemountDepsComponent.tsx b/examples/vue/basic-file-based-jsx/src/components/RemountDepsComponent.tsx
new file mode 100644
index 00000000000..ecb79aeca9c
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/RemountDepsComponent.tsx
@@ -0,0 +1,38 @@
+import { ref, onMounted, defineComponent } from 'vue'
+import { useSearch, useNavigate } from '@tanstack/vue-router'
+
+// Module-scoped ref to persist across component remounts
+const mounts = ref(0)
+
+export const RemountDepsComponent = defineComponent({
+ setup() {
+ const search = useSearch({ from: '/remountDeps' })
+ const navigate = useNavigate()
+
+ onMounted(() => {
+ mounts.value++
+ })
+
+ return () => (
+
+
+
+
Search: {search.value.searchParam}
+
+ Page component mounts: {mounts.value}
+
+
+ )
+ },
+})
diff --git a/examples/vue/basic-file-based-jsx/src/components/VueLogo.vue b/examples/vue/basic-file-based-jsx/src/components/VueLogo.vue
new file mode 100644
index 00000000000..b0f134316b0
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/components/VueLogo.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ {{ countLabel }}
+
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-jsx/src/main.tsx b/examples/vue/basic-file-based-jsx/src/main.tsx
new file mode 100644
index 00000000000..73cca4528c6
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/main.tsx
@@ -0,0 +1,29 @@
+import { RouterProvider, createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+import { createApp } from 'vue'
+
+// 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/examples/vue/basic-file-based-jsx/src/posts.tsx b/examples/vue/basic-file-based-jsx/src/posts.tsx
new file mode 100644
index 00000000000..1b4c92b41d7
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/posts.tsx
@@ -0,0 +1,38 @@
+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}...`)
+ 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/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts b/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts
new file mode 100644
index 00000000000..01bb4b0d053
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts
@@ -0,0 +1,513 @@
+/* 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 { lazyRouteComponent } from '@tanstack/vue-router'
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as SfcComponentRouteImport } from './routes/sfcComponent'
+import { Route as RemountDepsRouteImport } from './routes/remountDeps'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
+import { Route as EditingBRouteImport } from './routes/editing-b'
+import { Route as EditingARouteImport } from './routes/editing-a'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
+import { Route as groupInsideRouteImport } from './routes/(group)/inside'
+import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
+import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
+import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const SfcComponentRoute = SfcComponentRouteImport.update({
+ id: '/sfcComponent',
+ path: '/sfcComponent',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/sfcComponent.component.vue'),
+ 'default',
+ ),
+})
+const RemountDepsRoute = RemountDepsRouteImport.update({
+ id: '/remountDeps',
+ path: '/remountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
+ id: '/notRemountDeps',
+ path: '/notRemountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const EditingBRoute = EditingBRouteImport.update({
+ id: '/editing-b',
+ path: '/editing-b',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const EditingARoute = EditingARouteImport.update({
+ id: '/editing-a',
+ path: '/editing-a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any)
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any)
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any)
+const groupLazyinsideRoute = groupLazyinsideRouteImport.update({
+ id: '/(group)/lazyinside',
+ path: '/lazyinside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupInsideRoute = groupInsideRouteImport.update({
+ id: '/(group)/inside',
+ path: '/inside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupLayoutRoute = groupLayoutRouteImport.update({
+ id: '/(group)/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const anotherGroupOnlyrouteinsideRoute =
+ anotherGroupOnlyrouteinsideRouteImport.update({
+ id: '/(another-group)/onlyrouteinside',
+ path: '/onlyrouteinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
+ id: '/posts_/$postId/edit',
+ path: '/posts/$postId/edit',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
+const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport.update({
+ id: '/(group)/subfolder/inside',
+ path: '/subfolder/inside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({
+ id: '/insidelayout',
+ path: '/insidelayout',
+ getParentRoute: () => groupLayoutRoute,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/sfcComponent': typeof SfcComponentRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/remountDeps': typeof RemountDepsRoute
+ '/sfcComponent': typeof SfcComponentRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/_layout': typeof LayoutRouteWithChildren
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/sfcComponent': typeof SfcComponentRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/(another-group)/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/(group)/_layout': typeof groupLayoutRouteWithChildren
+ '/(group)/inside': typeof groupInsideRoute
+ '/(group)/lazyinside': typeof groupLazyinsideRoute
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts_/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/sfcComponent'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/remountDeps'
+ | '/sfcComponent'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ id:
+ | '__root__'
+ | '/'
+ | '/_layout'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/sfcComponent'
+ | '/대한민국'
+ | '/(another-group)/onlyrouteinside'
+ | '/(group)/_layout'
+ | '/(group)/inside'
+ | '/(group)/lazyinside'
+ | '/_layout/_layout-2'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/(group)/_layout/insidelayout'
+ | '/(group)/subfolder/inside'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ | '/posts_/$postId/edit'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ LayoutRoute: typeof LayoutRouteWithChildren
+ EditingARoute: typeof EditingARoute
+ EditingBRoute: typeof EditingBRoute
+ NotRemountDepsRoute: typeof NotRemountDepsRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ RemountDepsRoute: typeof RemountDepsRoute
+ SfcComponentRoute: typeof SfcComponentRoute
+ Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
+ anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
+ groupLayoutRoute: typeof groupLayoutRouteWithChildren
+ groupInsideRoute: typeof groupInsideRoute
+ groupLazyinsideRoute: typeof groupLazyinsideRoute
+ groupSubfolderInsideRoute: typeof groupSubfolderInsideRoute
+ PostsPostIdEditRoute: typeof PostsPostIdEditRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/sfcComponent': {
+ id: '/sfcComponent'
+ path: '/sfcComponent'
+ fullPath: '/sfcComponent'
+ preLoaderRoute: typeof SfcComponentRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/remountDeps': {
+ id: '/remountDeps'
+ path: '/remountDeps'
+ fullPath: '/remountDeps'
+ preLoaderRoute: typeof RemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/notRemountDeps': {
+ id: '/notRemountDeps'
+ path: '/notRemountDeps'
+ fullPath: '/notRemountDeps'
+ preLoaderRoute: typeof NotRemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-b': {
+ id: '/editing-b'
+ path: '/editing-b'
+ fullPath: '/editing-b'
+ preLoaderRoute: typeof EditingBRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-a': {
+ id: '/editing-a'
+ path: '/editing-a'
+ fullPath: '/editing-a'
+ preLoaderRoute: typeof EditingARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
+ '/(group)/lazyinside': {
+ id: '/(group)/lazyinside'
+ path: '/lazyinside'
+ fullPath: '/lazyinside'
+ preLoaderRoute: typeof groupLazyinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/inside': {
+ id: '/(group)/inside'
+ path: '/inside'
+ fullPath: '/inside'
+ preLoaderRoute: typeof groupInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout': {
+ id: '/(group)/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof groupLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(another-group)/onlyrouteinside': {
+ id: '/(another-group)/onlyrouteinside'
+ path: '/onlyrouteinside'
+ fullPath: '/onlyrouteinside'
+ preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts_/$postId/edit': {
+ id: '/posts_/$postId/edit'
+ path: '/posts/$postId/edit'
+ fullPath: '/posts/$postId/edit'
+ preLoaderRoute: typeof PostsPostIdEditRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/(group)/subfolder/inside': {
+ id: '/(group)/subfolder/inside'
+ path: '/subfolder/inside'
+ fullPath: '/subfolder/inside'
+ preLoaderRoute: typeof groupSubfolderInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout/insidelayout': {
+ id: '/(group)/_layout/insidelayout'
+ path: '/insidelayout'
+ fullPath: '/insidelayout'
+ preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
+ parentRoute: typeof groupLayoutRoute
+ }
+ }
+}
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface groupLayoutRouteChildren {
+ groupLayoutInsidelayoutRoute: typeof groupLayoutInsidelayoutRoute
+}
+
+const groupLayoutRouteChildren: groupLayoutRouteChildren = {
+ groupLayoutInsidelayoutRoute: groupLayoutInsidelayoutRoute,
+}
+
+const groupLayoutRouteWithChildren = groupLayoutRoute._addFileChildren(
+ groupLayoutRouteChildren,
+)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ LayoutRoute: LayoutRouteWithChildren,
+ EditingARoute: EditingARoute,
+ EditingBRoute: EditingBRoute,
+ NotRemountDepsRoute: NotRemountDepsRoute,
+ PostsRoute: PostsRouteWithChildren,
+ RemountDepsRoute: RemountDepsRoute,
+ SfcComponentRoute: SfcComponentRoute,
+ Char45824Char54620Char48124Char44397Route:
+ Char45824Char54620Char48124Char44397Route,
+ anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
+ groupLayoutRoute: groupLayoutRouteWithChildren,
+ groupInsideRoute: groupInsideRoute,
+ groupLazyinsideRoute: groupLazyinsideRoute,
+ groupSubfolderInsideRoute: groupSubfolderInsideRoute,
+ PostsPostIdEditRoute: PostsPostIdEditRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/examples/vue/basic-file-based-jsx/src/routes/(another-group)/onlyrouteinside.tsx b/examples/vue/basic-file-based-jsx/src/routes/(another-group)/onlyrouteinside.tsx
new file mode 100644
index 00000000000..83036509ba4
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/(another-group)/onlyrouteinside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(another-group)/onlyrouteinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: OnlyRouteInsideComponent,
+})
+
+const routeApi = getRouteApi('/(another-group)/onlyrouteinside')
+
+function OnlyRouteInsideComponent() {
+ const searchViaHook = useSearch({ from: '/(another-group)/onlyrouteinside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/(group)/_layout.insidelayout.tsx b/examples/vue/basic-file-based-jsx/src/routes/(group)/_layout.insidelayout.tsx
new file mode 100644
index 00000000000..557503a0c23
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/(group)/_layout.insidelayout.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/_layout/insidelayout')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: InsideLayoutComponent,
+})
+
+const routeApi = getRouteApi('/(group)/_layout/insidelayout')
+
+function InsideLayoutComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/_layout/insidelayout' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/(group)/_layout.tsx b/examples/vue/basic-file-based-jsx/src/routes/(group)/_layout.tsx
new file mode 100644
index 00000000000..f752d719eff
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/(group)/_layout.tsx
@@ -0,0 +1,14 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/(group)/_layout')({
+ component: GroupLayoutComponent,
+})
+
+function GroupLayoutComponent() {
+ return (
+
+
Layout inside group
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/(group)/inside.tsx b/examples/vue/basic-file-based-jsx/src/routes/(group)/inside.tsx
new file mode 100644
index 00000000000..bec12d8de25
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/(group)/inside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: InsideComponent,
+})
+
+const routeApi = getRouteApi('/(group)/inside')
+
+function InsideComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/inside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/(group)/lazyinside.tsx b/examples/vue/basic-file-based-jsx/src/routes/(group)/lazyinside.tsx
new file mode 100644
index 00000000000..56d8d6cae85
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/(group)/lazyinside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/lazyinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: LazyInsideComponent,
+})
+
+const routeApi = getRouteApi('/(group)/lazyinside')
+
+function LazyInsideComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/lazyinside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/(group)/subfolder/inside.tsx b/examples/vue/basic-file-based-jsx/src/routes/(group)/subfolder/inside.tsx
new file mode 100644
index 00000000000..b4487d163da
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/(group)/subfolder/inside.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute, getRouteApi, useSearch } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/subfolder/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+ component: SubfolderInsideComponent,
+})
+
+const routeApi = getRouteApi('/(group)/subfolder/inside')
+
+function SubfolderInsideComponent() {
+ const searchViaHook = useSearch({ from: '/(group)/subfolder/inside' })
+ const searchViaRouteHook = routeApi.useSearch()
+ const searchViaRouteApi = routeApi.useSearch()
+
+ return (
+
+
{searchViaHook.value.hello}
+
+ {searchViaRouteHook.value.hello}
+
+
+ {searchViaRouteApi.value.hello}
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/__root.tsx b/examples/vue/basic-file-based-jsx/src/routes/__root.tsx
new file mode 100644
index 00000000000..59c9e6fa48d
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/__root.tsx
@@ -0,0 +1,104 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ createRootRoute,
+ useCanGoBack,
+ useRouter,
+ useRouterState,
+} from '@tanstack/vue-router'
+import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools'
+import NotFoundComponent from '../components/NotFoundComponent.vue'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: NotFoundComponent,
+})
+
+function RootComponent() {
+ const router = useRouter()
+ const canGoBack = useCanGoBack()
+ // test useRouterState doesn't crash client side navigation
+ const _state = useRouterState()
+
+ return (
+ <>
+
+
+
+
+ Home
+
+
+ Posts
+
+
+ Layout
+
+
+ Only Route Inside Group
+
+
+ Inside Group
+
+
+ Inside Subfolder Inside Group
+
+
+ Inside Group Inside Layout
+
+
+ Lazy Inside Group
+
+
+ unicode path
+
+
+ This Route Does Not Exist
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/_layout.tsx b/examples/vue/basic-file-based-jsx/src/routes/_layout.tsx
new file mode 100644
index 00000000000..90503a4acba
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2.tsx b/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 00000000000..70bcde8007c
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,24 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ component: Layout2Component,
+})
+
+function Layout2Component() {
+ return (
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-a.tsx b/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 00000000000..6d9d130d002
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm layout A!
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-b.tsx b/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 00000000000..9081cff17f3
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm layout B!
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/editing-a.tsx b/examples/vue/basic-file-based-jsx/src/routes/editing-a.tsx
new file mode 100644
index 00000000000..15e6804e642
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/editing-a.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { EditingAComponent } from '../components/EditingAComponent'
+
+export const Route = createFileRoute('/editing-a')({
+ component: EditingAComponent,
+})
diff --git a/examples/vue/basic-file-based-jsx/src/routes/editing-b.tsx b/examples/vue/basic-file-based-jsx/src/routes/editing-b.tsx
new file mode 100644
index 00000000000..85236bcaeec
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/editing-b.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { EditingBComponent } from '../components/EditingBComponent'
+
+export const Route = createFileRoute('/editing-b')({
+ component: EditingBComponent,
+})
diff --git a/examples/vue/basic-file-based-jsx/src/routes/index.tsx b/examples/vue/basic-file-based-jsx/src/routes/index.tsx
new file mode 100644
index 00000000000..a1a7ce5a58e
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/index.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import VueLogo from '../components/VueLogo.vue'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/notRemountDeps.tsx b/examples/vue/basic-file-based-jsx/src/routes/notRemountDeps.tsx
new file mode 100644
index 00000000000..5f0874f710e
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/notRemountDeps.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { NotRemountDepsComponent } from '../components/NotRemountDepsComponent'
+
+export const Route = createFileRoute('/notRemountDeps')({
+ validateSearch: (search: Record) => ({
+ searchParam: (search.searchParam as string) || '',
+ }),
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.params
+ },
+ component: NotRemountDepsComponent,
+})
diff --git a/examples/vue/basic-file-based-jsx/src/routes/posts.$postId.tsx b/examples/vue/basic-file-based-jsx/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..15d17387c82
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/posts.$postId.tsx
@@ -0,0 +1,24 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import PostErrorComponent from '../components/PostErrorComponent.vue'
+import { fetchPost } from '../posts'
+import type { PostType } from '../posts'
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ component: PostComponent,
+ errorComponent: PostErrorComponent,
+ notFoundComponent: () => Post not found
,
+})
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ {(post.value as PostType).title}
+
+
{(post.value as PostType).body}
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/posts.index.tsx b/examples/vue/basic-file-based-jsx/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..42e369c2188
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/posts.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/posts.tsx b/examples/vue/basic-file-based-jsx/src/routes/posts.tsx
new file mode 100644
index 00000000000..ae580dfc02a
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/posts.tsx
@@ -0,0 +1,43 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/vue-router'
+import { fetchPosts } from '../posts'
+import type { PostType } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: fetchPosts,
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+
+ {[
+ ...(posts.value as Array),
+ { id: 'i-do-not-exist', title: 'Non-existent Post' },
+ ].map((post) => (
+ -
+
+
{post.title.substring(0, 20)}
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/posts_.$postId.edit.tsx b/examples/vue/basic-file-based-jsx/src/routes/posts_.$postId.edit.tsx
new file mode 100644
index 00000000000..57963c6b64d
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/posts_.$postId.edit.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, getRouteApi, useParams } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts_/$postId/edit')({
+ component: PostEditComponent,
+})
+
+const api = getRouteApi('/posts_/$postId/edit')
+
+function PostEditComponent() {
+ const paramsViaApi = api.useParams()
+ const paramsViaHook = useParams({ from: '/posts_/$postId/edit' })
+ const paramsViaRouteHook = api.useParams()
+
+ return (
+
+
{paramsViaHook.value.postId}
+
+ {paramsViaRouteHook.value.postId}
+
+
{paramsViaApi.value.postId}
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/routes/remountDeps.tsx b/examples/vue/basic-file-based-jsx/src/routes/remountDeps.tsx
new file mode 100644
index 00000000000..51f51e7592e
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/remountDeps.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { RemountDepsComponent } from '../components/RemountDepsComponent'
+
+export const Route = createFileRoute('/remountDeps')({
+ validateSearch: (search: Record) => ({
+ searchParam: (search.searchParam as string) || '',
+ }),
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.search
+ },
+ component: RemountDepsComponent,
+})
diff --git a/examples/vue/basic-file-based-jsx/src/routes/sfcComponent.component.vue b/examples/vue/basic-file-based-jsx/src/routes/sfcComponent.component.vue
new file mode 100644
index 00000000000..392d9ce9bf6
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/sfcComponent.component.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
Vue SFC!
+
+
+
diff --git a/examples/vue/basic-file-based-jsx/src/routes/sfcComponent.tsx b/examples/vue/basic-file-based-jsx/src/routes/sfcComponent.tsx
new file mode 100644
index 00000000000..0cbf84f07a0
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/routes/sfcComponent.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/sfcComponent')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Will be overwritten by the SFC component!
+}
diff --git "a/examples/vue/basic-file-based-jsx/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/examples/vue/basic-file-based-jsx/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
new file mode 100644
index 00000000000..e8c55d0ef91
--- /dev/null
+++ "b/examples/vue/basic-file-based-jsx/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
@@ -0,0 +1,17 @@
+import { Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/대한민국')({
+ component: UnicodeComponent,
+})
+
+function UnicodeComponent() {
+ return (
+
+
+ Hello "/대한민국"!
+
+
+
+
+ )
+}
diff --git a/examples/vue/basic-file-based-jsx/src/styles.css b/examples/vue/basic-file-based-jsx/src/styles.css
new file mode 100644
index 00000000000..6a03b331e8e
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/src/styles.css
@@ -0,0 +1,23 @@
+@import 'tailwindcss';
+
+@source "./**/*.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/examples/vue/basic-file-based-jsx/tsconfig.json b/examples/vue/basic-file-based-jsx/tsconfig.json
new file mode 100644
index 00000000000..6613f951304
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "strict": true,
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "types": ["vite/client"],
+ "jsx": "preserve",
+ "jsxImportSource": "vue"
+ },
+ "include": ["src/**/*", "src/**/*.vue", "tests"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/examples/vue/basic-file-based-jsx/vite.config.ts b/examples/vue/basic-file-based-jsx/vite.config.ts
new file mode 100644
index 00000000000..570e6cff2dc
--- /dev/null
+++ b/examples/vue/basic-file-based-jsx/vite.config.ts
@@ -0,0 +1,16 @@
+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,
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/examples/vue/basic-file-based-sfc/eslint.config.js b/examples/vue/basic-file-based-sfc/eslint.config.js
new file mode 100644
index 00000000000..4e8d00f1d89
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/eslint.config.js
@@ -0,0 +1,49 @@
+import js from '@eslint/js'
+import typescript from '@typescript-eslint/eslint-plugin'
+import typescriptParser from '@typescript-eslint/parser'
+import vue from 'eslint-plugin-vue'
+import vueParser from 'vue-eslint-parser'
+
+export default [
+ js.configs.recommended,
+ ...vue.configs['flat/recommended'],
+ {
+ files: ['**/*.{js,jsx,ts,tsx,vue}'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ plugins: {
+ '@typescript-eslint': typescript,
+ vue,
+ },
+ rules: {
+ // Vue specific rules
+ 'vue/multi-word-component-names': 'off',
+ 'vue/no-unused-vars': 'error',
+
+ // TypeScript rules
+ '@typescript-eslint/no-unused-vars': 'error',
+ '@typescript-eslint/no-explicit-any': 'warn',
+
+ // General rules
+ 'no-unused-vars': 'off', // Let TypeScript handle this
+ },
+ },
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ },
+ },
+ },
+]
diff --git a/examples/vue/basic-file-based-sfc/index.html b/examples/vue/basic-file-based-sfc/index.html
new file mode 100644
index 00000000000..eaa17316cdb
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/package.json b/examples/vue/basic-file-based-sfc/package.json
new file mode 100644
index 00000000000..6c17f126637
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "tanstack-router-vue-example-basic-file-based-sfc",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "build": "vite build && vue-tsc --noEmit",
+ "preview": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-plugin": "^1.139.14",
+ "@tanstack/vue-router": "workspace:*",
+ "@tanstack/vue-router-devtools": "workspace:*",
+ "@tanstack/zod-adapter": "^1.139.14",
+ "postcss": "^8.5.1",
+ "redaxios": "^0.5.1",
+ "tailwindcss": "^4.1.15",
+ "vue": "^3.5.16",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7",
+ "vue-tsc": "^2.2.0"
+ }
+}
diff --git a/examples/vue/basic-file-based-sfc/postcss.config.mjs b/examples/vue/basic-file-based-sfc/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/examples/vue/basic-file-based-sfc/src/components/VueLogo.vue b/examples/vue/basic-file-based-sfc/src/components/VueLogo.vue
new file mode 100644
index 00000000000..b0f134316b0
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/components/VueLogo.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ {{ countLabel }}
+
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/main.ts b/examples/vue/basic-file-based-sfc/src/main.ts
new file mode 100644
index 00000000000..98f9482d20b
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/main.ts
@@ -0,0 +1,30 @@
+import { createApp, h } 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) {
+ const app = createApp({
+ setup() {
+ return () => h(RouterProvider, { router })
+ },
+ })
+ app.mount('#app')
+}
diff --git a/examples/vue/basic-file-based-sfc/src/posts.ts b/examples/vue/basic-file-based-sfc/src/posts.ts
new file mode 100644
index 00000000000..1b4c92b41d7
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/posts.ts
@@ -0,0 +1,38 @@
+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}...`)
+ 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/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts b/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts
new file mode 100644
index 00000000000..ed2e49db39c
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts
@@ -0,0 +1,612 @@
+/* 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 { lazyRouteComponent } from '@tanstack/vue-router'
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as RemountDepsRouteImport } from './routes/remountDeps'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
+import { Route as EditingBRouteImport } from './routes/editing-b'
+import { Route as EditingARouteImport } from './routes/editing-a'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
+import { Route as groupInsideRouteImport } from './routes/(group)/inside'
+import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
+import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
+import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/대한민국.component.vue'),
+ 'default',
+ ),
+ })
+const RemountDepsRoute = RemountDepsRouteImport.update({
+ id: '/remountDeps',
+ path: '/remountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/remountDeps.component.vue'),
+ 'default',
+ ),
+})
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts.component.vue'),
+ 'default',
+ ),
+})
+const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
+ id: '/notRemountDeps',
+ path: '/notRemountDeps',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/notRemountDeps.component.vue'),
+ 'default',
+ ),
+})
+const EditingBRoute = EditingBRouteImport.update({
+ id: '/editing-b',
+ path: '/editing-b',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/editing-b.component.vue'),
+ 'default',
+ ),
+})
+const EditingARoute = EditingARouteImport.update({
+ id: '/editing-a',
+ path: '/editing-a',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/editing-a.component.vue'),
+ 'default',
+ ),
+})
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout.component.vue'),
+ 'default',
+ ),
+})
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/index.component.vue'),
+ 'default',
+ ),
+})
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts.index.component.vue'),
+ 'default',
+ ),
+})
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts.$postId.component.vue'),
+ 'default',
+ ),
+ errorComponent: lazyRouteComponent(
+ () => import('./routes/posts.$postId.errorComponent.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2.component.vue'),
+ 'default',
+ ),
+})
+const groupLazyinsideRoute = groupLazyinsideRouteImport
+ .update({
+ id: '/(group)/lazyinside',
+ path: '/lazyinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/lazyinside.component.vue'),
+ 'default',
+ ),
+ })
+const groupInsideRoute = groupInsideRouteImport
+ .update({
+ id: '/(group)/inside',
+ path: '/inside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/inside.component.vue'),
+ 'default',
+ ),
+ })
+const groupLayoutRoute = groupLayoutRouteImport
+ .update({
+ id: '/(group)/_layout',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/_layout.component.vue'),
+ 'default',
+ ),
+ })
+const anotherGroupOnlyrouteinsideRoute = anotherGroupOnlyrouteinsideRouteImport
+ .update({
+ id: '/(another-group)/onlyrouteinside',
+ path: '/onlyrouteinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(another-group)/onlyrouteinside.component.vue'),
+ 'default',
+ ),
+ })
+const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
+ id: '/posts_/$postId/edit',
+ path: '/posts/$postId/edit',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts_.$postId.edit.component.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2/layout-b.component.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2/layout-a.component.vue'),
+ 'default',
+ ),
+})
+const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport
+ .update({
+ id: '/(group)/subfolder/inside',
+ path: '/subfolder/inside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/subfolder/inside.component.vue'),
+ 'default',
+ ),
+ })
+const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport
+ .update({
+ id: '/insidelayout',
+ path: '/insidelayout',
+ getParentRoute: () => groupLayoutRoute,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/_layout.insidelayout.component.vue'),
+ 'default',
+ ),
+ })
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/remountDeps': typeof RemountDepsRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/inside': typeof groupInsideRoute
+ '/lazyinside': typeof groupLazyinsideRoute
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts': typeof PostsIndexRoute
+ '/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/layout-a': typeof LayoutLayout2LayoutARoute
+ '/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/_layout': typeof LayoutRouteWithChildren
+ '/editing-a': typeof EditingARoute
+ '/editing-b': typeof EditingBRoute
+ '/notRemountDeps': typeof NotRemountDepsRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/remountDeps': typeof RemountDepsRoute
+ '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/(another-group)/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
+ '/(group)/_layout': typeof groupLayoutRouteWithChildren
+ '/(group)/inside': typeof groupInsideRoute
+ '/(group)/lazyinside': typeof groupLazyinsideRoute
+ '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute
+ '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
+ '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+ '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/posts_/$postId/edit': typeof PostsPostIdEditRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/remountDeps'
+ | '/대한민국'
+ | '/onlyrouteinside'
+ | '/inside'
+ | '/lazyinside'
+ | '/posts/$postId'
+ | '/posts'
+ | '/insidelayout'
+ | '/subfolder/inside'
+ | '/layout-a'
+ | '/layout-b'
+ | '/posts/$postId/edit'
+ id:
+ | '__root__'
+ | '/'
+ | '/_layout'
+ | '/editing-a'
+ | '/editing-b'
+ | '/notRemountDeps'
+ | '/posts'
+ | '/remountDeps'
+ | '/대한민국'
+ | '/(another-group)/onlyrouteinside'
+ | '/(group)/_layout'
+ | '/(group)/inside'
+ | '/(group)/lazyinside'
+ | '/_layout/_layout-2'
+ | '/posts/$postId'
+ | '/posts/'
+ | '/(group)/_layout/insidelayout'
+ | '/(group)/subfolder/inside'
+ | '/_layout/_layout-2/layout-a'
+ | '/_layout/_layout-2/layout-b'
+ | '/posts_/$postId/edit'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ LayoutRoute: typeof LayoutRouteWithChildren
+ EditingARoute: typeof EditingARoute
+ EditingBRoute: typeof EditingBRoute
+ NotRemountDepsRoute: typeof NotRemountDepsRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ RemountDepsRoute: typeof RemountDepsRoute
+ Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
+ anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
+ groupLayoutRoute: typeof groupLayoutRouteWithChildren
+ groupInsideRoute: typeof groupInsideRoute
+ groupLazyinsideRoute: typeof groupLazyinsideRoute
+ groupSubfolderInsideRoute: typeof groupSubfolderInsideRoute
+ PostsPostIdEditRoute: typeof PostsPostIdEditRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/remountDeps': {
+ id: '/remountDeps'
+ path: '/remountDeps'
+ fullPath: '/remountDeps'
+ preLoaderRoute: typeof RemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/notRemountDeps': {
+ id: '/notRemountDeps'
+ path: '/notRemountDeps'
+ fullPath: '/notRemountDeps'
+ preLoaderRoute: typeof NotRemountDepsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-b': {
+ id: '/editing-b'
+ path: '/editing-b'
+ fullPath: '/editing-b'
+ preLoaderRoute: typeof EditingBRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-a': {
+ id: '/editing-a'
+ path: '/editing-a'
+ fullPath: '/editing-a'
+ preLoaderRoute: typeof EditingARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
+ '/(group)/lazyinside': {
+ id: '/(group)/lazyinside'
+ path: '/lazyinside'
+ fullPath: '/lazyinside'
+ preLoaderRoute: typeof groupLazyinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/inside': {
+ id: '/(group)/inside'
+ path: '/inside'
+ fullPath: '/inside'
+ preLoaderRoute: typeof groupInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout': {
+ id: '/(group)/_layout'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof groupLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(another-group)/onlyrouteinside': {
+ id: '/(another-group)/onlyrouteinside'
+ path: '/onlyrouteinside'
+ fullPath: '/onlyrouteinside'
+ preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts_/$postId/edit': {
+ id: '/posts_/$postId/edit'
+ path: '/posts/$postId/edit'
+ fullPath: '/posts/$postId/edit'
+ preLoaderRoute: typeof PostsPostIdEditRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2/layout-b': {
+ id: '/_layout/_layout-2/layout-b'
+ path: '/layout-b'
+ fullPath: '/layout-b'
+ preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
+ '/(group)/subfolder/inside': {
+ id: '/(group)/subfolder/inside'
+ path: '/subfolder/inside'
+ fullPath: '/subfolder/inside'
+ preLoaderRoute: typeof groupSubfolderInsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout/insidelayout': {
+ id: '/(group)/_layout/insidelayout'
+ path: '/insidelayout'
+ fullPath: '/insidelayout'
+ preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
+ parentRoute: typeof groupLayoutRoute
+ }
+ }
+}
+
+interface LayoutLayout2RouteChildren {
+ LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+ LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+ LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+ LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+ LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+ LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+ LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+ LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface groupLayoutRouteChildren {
+ groupLayoutInsidelayoutRoute: typeof groupLayoutInsidelayoutRoute
+}
+
+const groupLayoutRouteChildren: groupLayoutRouteChildren = {
+ groupLayoutInsidelayoutRoute: groupLayoutInsidelayoutRoute,
+}
+
+const groupLayoutRouteWithChildren = groupLayoutRoute._addFileChildren(
+ groupLayoutRouteChildren,
+)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ LayoutRoute: LayoutRouteWithChildren,
+ EditingARoute: EditingARoute,
+ EditingBRoute: EditingBRoute,
+ NotRemountDepsRoute: NotRemountDepsRoute,
+ PostsRoute: PostsRouteWithChildren,
+ RemountDepsRoute: RemountDepsRoute,
+ Char45824Char54620Char48124Char44397Route:
+ Char45824Char54620Char48124Char44397Route,
+ anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
+ groupLayoutRoute: groupLayoutRouteWithChildren,
+ groupInsideRoute: groupInsideRoute,
+ groupLazyinsideRoute: groupLazyinsideRoute,
+ groupSubfolderInsideRoute: groupSubfolderInsideRoute,
+ PostsPostIdEditRoute: PostsPostIdEditRoute,
+}
+export const routeTree = rootRouteImport
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/__root.component.vue'),
+ 'default',
+ ),
+ notFoundComponent: lazyRouteComponent(
+ () => import('./routes/__root.notFoundComponent.vue'),
+ 'default',
+ ),
+ })
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.component.vue b/examples/vue/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.component.vue
new file mode 100644
index 00000000000..4d998b9ff97
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.ts b/examples/vue/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.ts
new file mode 100644
index 00000000000..491bb03649f
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(another-group)/onlyrouteinside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(another-group)/onlyrouteinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.component.vue b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.component.vue
new file mode 100644
index 00000000000..9c2990a0293
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.component.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
Layout inside group
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.component.vue b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.component.vue
new file mode 100644
index 00000000000..5b5d4352322
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.ts b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.ts
new file mode 100644
index 00000000000..49ef8f7bb9f
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.insidelayout.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/_layout/insidelayout')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.ts b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.ts
new file mode 100644
index 00000000000..ce0f6ca52a2
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/_layout.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/(group)/_layout')({})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/inside.component.vue b/examples/vue/basic-file-based-sfc/src/routes/(group)/inside.component.vue
new file mode 100644
index 00000000000..6542777df9b
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/inside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/inside.ts b/examples/vue/basic-file-based-sfc/src/routes/(group)/inside.ts
new file mode 100644
index 00000000000..f7b06f6a94a
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/inside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/lazyinside.component.vue b/examples/vue/basic-file-based-sfc/src/routes/(group)/lazyinside.component.vue
new file mode 100644
index 00000000000..8e603559378
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/lazyinside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/lazyinside.ts b/examples/vue/basic-file-based-sfc/src/routes/(group)/lazyinside.ts
new file mode 100644
index 00000000000..b66d18a7299
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/lazyinside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/lazyinside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/subfolder/inside.component.vue b/examples/vue/basic-file-based-sfc/src/routes/(group)/subfolder/inside.component.vue
new file mode 100644
index 00000000000..516f6e1e9a5
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/subfolder/inside.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ searchViaHook.hello }}
+
+ {{ searchViaRouteHook.hello }}
+
+
{{ searchViaRouteApi.hello }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/(group)/subfolder/inside.ts b/examples/vue/basic-file-based-sfc/src/routes/(group)/subfolder/inside.ts
new file mode 100644
index 00000000000..50aa8a44ad2
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/(group)/subfolder/inside.ts
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(group)/subfolder/inside')({
+ validateSearch: zodValidator(z.object({ hello: z.string().optional() })),
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/__root.component.vue b/examples/vue/basic-file-based-sfc/src/routes/__root.component.vue
new file mode 100644
index 00000000000..08594569105
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/__root.component.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+ Home
+
+ Posts
+ Layout
+
+ Only Route Inside Group
+
+
+ Inside Group
+
+
+ Inside Subfolder Inside Group
+
+
+ Inside Group Inside Layout
+
+
+ Lazy Inside Group
+
+ unicode path
+
+ This Route Does Not Exist
+
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/__root.notFoundComponent.vue b/examples/vue/basic-file-based-sfc/src/routes/__root.notFoundComponent.vue
new file mode 100644
index 00000000000..09ed5cf5ffd
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/__root.notFoundComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/__root.ts b/examples/vue/basic-file-based-sfc/src/routes/__root.ts
new file mode 100644
index 00000000000..815b2762f6b
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/__root.ts
@@ -0,0 +1,3 @@
+import { createRootRoute } from '@tanstack/vue-router'
+
+export const Route = createRootRoute()
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout.component.vue b/examples/vue/basic-file-based-sfc/src/routes/_layout.component.vue
new file mode 100644
index 00000000000..68fa9808a53
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout.component.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout.ts b/examples/vue/basic-file-based-sfc/src/routes/_layout.ts
new file mode 100644
index 00000000000..6767e7457a3
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout')({
+ // component is loaded from _layout.component.vue
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2.component.vue b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2.component.vue
new file mode 100644
index 00000000000..66c630f37cd
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2.component.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
I'm a nested layout
+
+
+ Layout A
+
+
+ Layout B
+
+
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2.ts b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2.ts
new file mode 100644
index 00000000000..a03c32233bc
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+ // component is loaded from _layout-2.component.vue
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.component.vue b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.component.vue
new file mode 100644
index 00000000000..6fb4e11ae29
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.component.vue
@@ -0,0 +1,3 @@
+
+ I'm layout A!
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.ts b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.ts
new file mode 100644
index 00000000000..098ee2e59ca
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-a.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+ // component is loaded from layout-a.component.vue
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.component.vue b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.component.vue
new file mode 100644
index 00000000000..51c4ed6f372
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.component.vue
@@ -0,0 +1,3 @@
+
+ I'm layout B!
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.ts b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.ts
new file mode 100644
index 00000000000..10e4c50303e
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/_layout/_layout-2/layout-b.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+ // component is loaded from layout-b.component.vue
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/editing-a.component.vue b/examples/vue/basic-file-based-sfc/src/routes/editing-a.component.vue
new file mode 100644
index 00000000000..1b0e1c4a100
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/editing-a.component.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
Editing A
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/editing-a.ts b/examples/vue/basic-file-based-sfc/src/routes/editing-a.ts
new file mode 100644
index 00000000000..ce0b9f8b075
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/editing-a.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/editing-a')({})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/editing-b.component.vue b/examples/vue/basic-file-based-sfc/src/routes/editing-b.component.vue
new file mode 100644
index 00000000000..6e57e4244ca
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/editing-b.component.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
Editing B
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/editing-b.ts b/examples/vue/basic-file-based-sfc/src/routes/editing-b.ts
new file mode 100644
index 00000000000..f5158fe02c4
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/editing-b.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/editing-b')({})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/index.component.vue b/examples/vue/basic-file-based-sfc/src/routes/index.component.vue
new file mode 100644
index 00000000000..392d9ce9bf6
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/index.component.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
Vue SFC!
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/index.ts b/examples/vue/basic-file-based-sfc/src/routes/index.ts
new file mode 100644
index 00000000000..5948d4af8bc
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/index.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ // component is loaded from index.component.vue
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/notRemountDeps.component.vue b/examples/vue/basic-file-based-sfc/src/routes/notRemountDeps.component.vue
new file mode 100644
index 00000000000..4ec402e3b3e
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/notRemountDeps.component.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
Search: {{ search.searchParam }}
+
+ Page component mounts: {{ mounts }}
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/notRemountDeps.ts b/examples/vue/basic-file-based-sfc/src/routes/notRemountDeps.ts
new file mode 100644
index 00000000000..1a712cd75ab
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/notRemountDeps.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/notRemountDeps')({
+ validateSearch(search: { searchParam: string }) {
+ return { searchParam: search.searchParam }
+ },
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.params
+ },
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.component.vue b/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.component.vue
new file mode 100644
index 00000000000..4c85355d592
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.component.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{ post.title }}
+
+
{{ post.body }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.errorComponent.vue b/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.errorComponent.vue
new file mode 100644
index 00000000000..d02118f6590
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.errorComponent.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.ts b/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.ts
new file mode 100644
index 00000000000..3824aa7f43b
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.$postId.ts
@@ -0,0 +1,8 @@
+import { h } from 'vue'
+import { createFileRoute } from '@tanstack/vue-router'
+import { fetchPost } from '../posts'
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost(postId),
+ notFoundComponent: () => h('p', 'Post not found'),
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.component.vue b/examples/vue/basic-file-based-sfc/src/routes/posts.component.vue
new file mode 100644
index 00000000000..2829cbaa856
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.component.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.index.component.vue b/examples/vue/basic-file-based-sfc/src/routes/posts.index.component.vue
new file mode 100644
index 00000000000..97f094befe2
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.index.component.vue
@@ -0,0 +1,3 @@
+
+ Select a post.
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.index.ts b/examples/vue/basic-file-based-sfc/src/routes/posts.index.ts
new file mode 100644
index 00000000000..647568877c5
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.index.ts
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts/')({
+ // component is loaded from posts.index.component.vue
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts.ts b/examples/vue/basic-file-based-sfc/src/routes/posts.ts
new file mode 100644
index 00000000000..a216c07cb4f
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { fetchPosts } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: fetchPosts,
+})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts_.$postId.edit.component.vue b/examples/vue/basic-file-based-sfc/src/routes/posts_.$postId.edit.component.vue
new file mode 100644
index 00000000000..2c5963c94c0
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts_.$postId.edit.component.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
{{ paramsViaHook.postId }}
+
+ {{ paramsViaRouteHook.postId }}
+
+
{{ paramsViaApi.postId }}
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/posts_.$postId.edit.ts b/examples/vue/basic-file-based-sfc/src/routes/posts_.$postId.edit.ts
new file mode 100644
index 00000000000..b1b1f6029ab
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/posts_.$postId.edit.ts
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/posts_/$postId/edit')({})
diff --git a/examples/vue/basic-file-based-sfc/src/routes/remountDeps.component.vue b/examples/vue/basic-file-based-sfc/src/routes/remountDeps.component.vue
new file mode 100644
index 00000000000..c5aa9417742
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/remountDeps.component.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
Search: {{ search.searchParam }}
+
+ Page component mounts: {{ mounts }}
+
+
+
diff --git a/examples/vue/basic-file-based-sfc/src/routes/remountDeps.ts b/examples/vue/basic-file-based-sfc/src/routes/remountDeps.ts
new file mode 100644
index 00000000000..46ce88b6c09
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/routes/remountDeps.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/remountDeps')({
+ validateSearch(search: { searchParam: string }) {
+ return { searchParam: search.searchParam }
+ },
+ loaderDeps(opts) {
+ return opts.search
+ },
+ remountDeps(opts) {
+ return opts.search
+ },
+})
diff --git "a/examples/vue/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.component.vue" "b/examples/vue/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.component.vue"
new file mode 100644
index 00000000000..a520ed3cd71
--- /dev/null
+++ "b/examples/vue/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.component.vue"
@@ -0,0 +1,11 @@
+
+
+
+
+
Hello "/대한민국"!
+
+
+
+
diff --git "a/examples/vue/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.ts" "b/examples/vue/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.ts"
new file mode 100644
index 00000000000..943e319a554
--- /dev/null
+++ "b/examples/vue/basic-file-based-sfc/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.ts"
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/대한민국')({})
diff --git a/examples/vue/basic-file-based-sfc/src/styles.css b/examples/vue/basic-file-based-sfc/src/styles.css
new file mode 100644
index 00000000000..237cc5590d1
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/styles.css
@@ -0,0 +1,24 @@
+@import 'tailwindcss';
+
+@source "./**/*.vue";
+@source "./**/*.ts";
+
+@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/examples/vue/basic-file-based-sfc/src/vue-shims.d.ts b/examples/vue/basic-file-based-sfc/src/vue-shims.d.ts
new file mode 100644
index 00000000000..2b97bd961cc
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/src/vue-shims.d.ts
@@ -0,0 +1,5 @@
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/examples/vue/basic-file-based-sfc/tsconfig.json b/examples/vue/basic-file-based-sfc/tsconfig.json
new file mode 100644
index 00000000000..0356ff8bae5
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "strict": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src/**/*", "src/**/*.vue", "tests"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/examples/vue/basic-file-based-sfc/vite.config.ts b/examples/vue/basic-file-based-sfc/vite.config.ts
new file mode 100644
index 00000000000..570e6cff2dc
--- /dev/null
+++ b/examples/vue/basic-file-based-sfc/vite.config.ts
@@ -0,0 +1,16 @@
+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,
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/examples/vue/basic/.gitignore b/examples/vue/basic/.gitignore
new file mode 100644
index 00000000000..8354e4d50d5
--- /dev/null
+++ b/examples/vue/basic/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
\ No newline at end of file
diff --git a/examples/vue/basic/README.md b/examples/vue/basic/README.md
new file mode 100644
index 00000000000..115199d292c
--- /dev/null
+++ b/examples/vue/basic/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm start` or `yarn start`
diff --git a/examples/vue/basic/eslint.config.js b/examples/vue/basic/eslint.config.js
new file mode 100644
index 00000000000..4e8d00f1d89
--- /dev/null
+++ b/examples/vue/basic/eslint.config.js
@@ -0,0 +1,49 @@
+import js from '@eslint/js'
+import typescript from '@typescript-eslint/eslint-plugin'
+import typescriptParser from '@typescript-eslint/parser'
+import vue from 'eslint-plugin-vue'
+import vueParser from 'vue-eslint-parser'
+
+export default [
+ js.configs.recommended,
+ ...vue.configs['flat/recommended'],
+ {
+ files: ['**/*.{js,jsx,ts,tsx,vue}'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ plugins: {
+ '@typescript-eslint': typescript,
+ vue,
+ },
+ rules: {
+ // Vue specific rules
+ 'vue/multi-word-component-names': 'off',
+ 'vue/no-unused-vars': 'error',
+
+ // TypeScript rules
+ '@typescript-eslint/no-unused-vars': 'error',
+ '@typescript-eslint/no-explicit-any': 'warn',
+
+ // General rules
+ 'no-unused-vars': 'off', // Let TypeScript handle this
+ },
+ },
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parser: vueParser,
+ parserOptions: {
+ parser: typescriptParser,
+ },
+ },
+ },
+]
diff --git a/examples/vue/basic/index.html b/examples/vue/basic/index.html
new file mode 100644
index 00000000000..9b6335c0ac1
--- /dev/null
+++ b/examples/vue/basic/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/examples/vue/basic/package.json b/examples/vue/basic/package.json
new file mode 100644
index 00000000000..3b946cdfa9c
--- /dev/null
+++ b/examples/vue/basic/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "tanstack-router-vue-example-basic-jsx",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3000",
+ "build": "vite build && vue-tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tanstack/vue-router": "workspace:*",
+ "@tanstack/vue-router-devtools": "workspace:*",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "vue": "^3.5.13",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17"
+ },
+ "devDependencies": {
+ "typescript": "^5.7.2",
+ "vite": "^6.1.0",
+ "vue-tsc": "^2.2.0",
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2"
+ }
+}
diff --git a/examples/vue/basic/postcss.config.mjs b/examples/vue/basic/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/examples/vue/basic/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/vue/basic/src/components/VueLogo.vue b/examples/vue/basic/src/components/VueLogo.vue
new file mode 100644
index 00000000000..b0f134316b0
--- /dev/null
+++ b/examples/vue/basic/src/components/VueLogo.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ {{ countLabel }}
+
+
+
+
+
+
diff --git a/examples/vue/basic/src/main.tsx b/examples/vue/basic/src/main.tsx
new file mode 100644
index 00000000000..dc12871db0d
--- /dev/null
+++ b/examples/vue/basic/src/main.tsx
@@ -0,0 +1,239 @@
+import { createApp } from 'vue'
+import {
+ ErrorComponent,
+ Link,
+ Outlet,
+ RouterProvider,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '@tanstack/vue-router'
+import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools'
+import { NotFoundError, fetchPost, fetchPosts } from './posts'
+import VueLogo from './components/VueLogo.vue'
+import type { ErrorComponentProps } from '@tanstack/vue-router'
+import './styles.css'
+
+const rootRoute = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Pathless Layout
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+
+
+ >
+ )
+}
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+
+ )
+}
+
+export const postsLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'posts',
+ loader: () => fetchPosts(),
+}).lazy(() => import('./posts.lazy').then((d) => d.Route))
+
+const postsIndexRoute = createRoute({
+ getParentRoute: () => postsLayoutRoute,
+ path: '/',
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
+
+const postRoute = createRoute({
+ getParentRoute: () => postsLayoutRoute,
+ path: '$postId',
+ errorComponent: PostErrorComponent,
+ loader: ({ params }) => fetchPost(params.postId),
+ component: PostComponent,
+})
+
+function PostErrorComponent({ error }: ErrorComponentProps) {
+ if (error instanceof NotFoundError) {
+ return {error.message}
+ }
+
+ return
+}
+
+function PostComponent() {
+ const post = postRoute.useLoaderData()
+
+ return (
+
+
{post.value.title}
+
+
{post.value.body}
+
+ )
+}
+
+const pathlessLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_pathlessLayout',
+ component: PathlessLayoutComponent,
+})
+
+function PathlessLayoutComponent() {
+ return (
+
+
I'm a pathless layout
+
+
+
+
+ )
+}
+
+const nestedPathlessLayout2Route = createRoute({
+ getParentRoute: () => pathlessLayoutRoute,
+ id: '_nestedPathlessLayout',
+ component: PathlessLayout2Component,
+})
+
+function PathlessLayout2Component() {
+ return (
+
+
I'm a nested pathless layout
+
+
+ Go to Route A
+
+
+ Go to Route B
+
+
+
+
+
+
+ )
+}
+
+const pathlessLayoutARoute = createRoute({
+ getParentRoute: () => nestedPathlessLayout2Route,
+ path: '/route-a',
+ component: PathlessLayoutAComponent,
+})
+
+function PathlessLayoutAComponent() {
+ return I'm route A!
+}
+
+const pathlessLayoutBRoute = createRoute({
+ getParentRoute: () => nestedPathlessLayout2Route,
+ path: '/route-b',
+ component: PathlessLayoutBComponent,
+})
+
+function PathlessLayoutBComponent() {
+ return I'm route B!
+}
+
+const routeTree = rootRoute.addChildren([
+ postsLayoutRoute.addChildren([postRoute, postsIndexRoute]),
+ pathlessLayoutRoute.addChildren([
+ nestedPathlessLayout2Route.addChildren([
+ pathlessLayoutARoute,
+ pathlessLayoutBRoute,
+ ]),
+ ]),
+ indexRoute,
+])
+
+// 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/examples/vue/basic/src/posts.lazy.tsx b/examples/vue/basic/src/posts.lazy.tsx
new file mode 100644
index 00000000000..1fd64723e53
--- /dev/null
+++ b/examples/vue/basic/src/posts.lazy.tsx
@@ -0,0 +1,36 @@
+import { Link, Outlet, createLazyRoute } from '@tanstack/vue-router'
+
+export const Route = createLazyRoute('/posts')({
+ component: PostsLayoutComponent,
+})
+
+function PostsLayoutComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/examples/vue/basic/src/posts.ts b/examples/vue/basic/src/posts.ts
new file mode 100644
index 00000000000..54d62e57886
--- /dev/null
+++ b/examples/vue/basic/src/posts.ts
@@ -0,0 +1,32 @@
+import axios from 'redaxios'
+
+export class NotFoundError extends Error {}
+
+type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+export const fetchPosts = async () => {
+ console.info('Fetching posts...')
+ await new Promise((r) => setTimeout(r, 500))
+ return axios
+ .get>('https://jsonplaceholder.typicode.com/posts')
+ .then((r) => r.data.slice(0, 10))
+}
+
+export const fetchPost = async (postId: string) => {
+ console.info(`Fetching post with id ${postId}...`)
+ await new Promise((r) => setTimeout(r, 500))
+ const post = await axios
+ .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+ .then((r) => r.data)
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!post) {
+ throw new NotFoundError(`Post with id "${postId}" not found!`)
+ }
+
+ return post
+}
diff --git a/examples/vue/basic/src/styles.css b/examples/vue/basic/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/examples/vue/basic/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/examples/vue/basic/tailwind.config.mjs b/examples/vue/basic/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/examples/vue/basic/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/examples/vue/basic/tsconfig.dev.json b/examples/vue/basic/tsconfig.dev.json
new file mode 100644
index 00000000000..285a09b0dcf
--- /dev/null
+++ b/examples/vue/basic/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/examples/vue/basic/tsconfig.json b/examples/vue/basic/tsconfig.json
new file mode 100644
index 00000000000..92ac5febf8b
--- /dev/null
+++ b/examples/vue/basic/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/vue/basic/vite.config.js b/examples/vue/basic/vite.config.js
new file mode 100644
index 00000000000..33d651a84c5
--- /dev/null
+++ b/examples/vue/basic/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue(), vueJsx()],
+})
diff --git a/labeler-config.yml b/labeler-config.yml
index bf86504942f..299b1902635 100644
--- a/labeler-config.yml
+++ b/labeler-config.yml
@@ -100,6 +100,15 @@
'package: virtual-file-routes':
- changed-files:
- any-glob-to-any-file: 'packages/virtual-file-routes/**/*'
+'package: vue-router':
+ - changed-files:
+ - any-glob-to-any-file: 'packages/vue-router/**/*'
+'package: vue-router-devtools':
+ - changed-files:
+ - any-glob-to-any-file: 'packages/vue-router-devtools/**/*'
+'package: vue-router-ssr-query':
+ - changed-files:
+ - any-glob-to-any-file: 'packages/vue-router-ssr-query/**/*'
'package: zod-adapter':
- changed-files:
- any-glob-to-any-file: 'packages/zod-adapter/**/*'
diff --git a/package.json b/package.json
index 0eda9829eb0..cb69a7977f2 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,7 @@
"@tanstack/router-ssr-query-core": "workspace:*",
"@tanstack/react-router-ssr-query": "workspace:*",
"@tanstack/solid-router-ssr-query": "workspace:*",
+ "@tanstack/vue-router-ssr-query": "workspace:*",
"@tanstack/zod-adapter": "workspace:*",
"@tanstack/valibot-adapter": "workspace:*",
"@tanstack/arktype-adapter": "workspace:*",
@@ -113,6 +114,8 @@
"@tanstack/start-client-core": "workspace:*",
"@tanstack/start-server-core": "workspace:*",
"@tanstack/start-storage-context": "workspace:*",
+ "@tanstack/vue-router": "workspace:*",
+ "@tanstack/vue-router-devtools": "workspace:*",
"@tanstack/eslint-plugin-router": "workspace:*",
"@tanstack/server-functions-plugin": "workspace:*",
"@tanstack/directive-functions-plugin": "workspace:*",
diff --git a/packages/router-generator/src/config.ts b/packages/router-generator/src/config.ts
index 5c663a5593c..d44575435f7 100644
--- a/packages/router-generator/src/config.ts
+++ b/packages/router-generator/src/config.ts
@@ -5,7 +5,7 @@ import { virtualRootRouteSchema } from './filesystem/virtual/config'
import type { GeneratorPlugin } from './plugin/types'
export const baseConfigSchema = z.object({
- target: z.enum(['react', 'solid']).optional().default('react'),
+ target: z.enum(['react', 'solid', 'vue']).optional().default('react'),
virtualRouteConfig: virtualRootRouteSchema.or(z.string()).optional(),
routeFilePrefix: z.string().optional(),
routeFileIgnorePrefix: z.string().optional().default('-'),
diff --git a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts
index 892e0095347..4adcf6d7cdd 100644
--- a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts
+++ b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts
@@ -17,7 +17,7 @@ import type {
import type { FsRouteType, GetRouteNodesResult, RouteNode } from '../../types'
import type { Config } from '../../config'
-const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx)/
+const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx|vue)/
const virtualConfigFileRegExp = /__virtual\.[mc]?[jt]s$/
export function isVirtualConfigFile(fileName: string): boolean {
@@ -129,7 +129,7 @@ export async function getRouteNodes(
if (dirent.isDirectory()) {
await recurse(relativePath)
- } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) {
+ } else if (fullPath.match(/\.(tsx|ts|jsx|js|vue)$/)) {
const filePath = replaceBackslash(path.join(dir, dirent.name))
const filePathNoExt = removeExt(filePath)
const {
@@ -170,31 +170,37 @@ export async function getRouteNodes(
routeType = 'pathless_layout'
}
- ;(
- [
- ['component', 'component'],
- ['errorComponent', 'errorComponent'],
- ['pendingComponent', 'pendingComponent'],
- ['loader', 'loader'],
- ] satisfies Array<[FsRouteType, string]>
- ).forEach(([matcher, type]) => {
- if (routeType === matcher) {
- logger.warn(
- `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
- )
- }
- })
+ // Only show deprecation warning for .tsx/.ts files, not .vue files
+ // Vue files using .component.vue is the Vue-native way
+ const isVueFile = filePath.endsWith('.vue')
+ if (!isVueFile) {
+ ;(
+ [
+ ['component', 'component'],
+ ['errorComponent', 'errorComponent'],
+ ['notFoundComponent', 'notFoundComponent'],
+ ['pendingComponent', 'pendingComponent'],
+ ['loader', 'loader'],
+ ] satisfies Array<[FsRouteType, string]>
+ ).forEach(([matcher, type]) => {
+ if (routeType === matcher) {
+ logger.warn(
+ `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
+ )
+ }
+ })
+ }
routePath = routePath.replace(
new RegExp(
- `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
+ `/(component|errorComponent|notFoundComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
),
'',
)
originalRoutePath = originalRoutePath.replace(
new RegExp(
- `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
+ `/(component|errorComponent|notFoundComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
),
'',
)
@@ -234,7 +240,20 @@ export async function getRouteNodes(
await recurse('./')
- const rootRouteNode = routeNodes.find((d) => d.routePath === `/${rootPathId}`)
+ // Find the root route node - prefer the actual route file over component/loader files
+ const rootRouteNode =
+ routeNodes.find(
+ (d) =>
+ d.routePath === `/${rootPathId}` &&
+ ![
+ 'component',
+ 'errorComponent',
+ 'notFoundComponent',
+ 'pendingComponent',
+ 'loader',
+ 'lazy',
+ ].includes(d._fsRouteType),
+ ) ?? routeNodes.find((d) => d.routePath === `/${rootPathId}`)
if (rootRouteNode) {
rootRouteNode._fsRouteType = '__root'
rootRouteNode.variableName = 'root'
@@ -270,6 +289,7 @@ export function getRouteMeta(
| 'component'
| 'pendingComponent'
| 'errorComponent'
+ | 'notFoundComponent'
>
variableName: string
} {
@@ -293,6 +313,9 @@ export function getRouteMeta(
} else if (routePath.endsWith('/errorComponent')) {
// error component routes, i.e. `/foo.errorComponent.tsx`
fsRouteType = 'errorComponent'
+ } else if (routePath.endsWith('/notFoundComponent')) {
+ // not found component routes, i.e. `/foo.notFoundComponent.tsx`
+ fsRouteType = 'notFoundComponent'
}
const variableName = routePathToVariable(routePath)
diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts
index 54a16e265b6..baf9dd89a2e 100644
--- a/packages/router-generator/src/generator.ts
+++ b/packages/router-generator/src/generator.ts
@@ -353,7 +353,7 @@ export class Generator {
: -1,
(d) =>
d.filePath.match(
- /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/,
+ /[./](component|errorComponent|notFoundComponent|pendingComponent|loader|lazy)[.]/,
)
? 1
: -1,
@@ -363,7 +363,20 @@ export class Generator {
: 1,
(d) => (d.routePath?.endsWith('/') ? -1 : 1),
(d) => d.routePath,
- ]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ''))
+ ]).filter((d) => {
+ // Exclude the root route itself, but keep component/loader pieces for the root
+ if (d.routePath === `/${rootPathId}`) {
+ return [
+ 'component',
+ 'errorComponent',
+ 'notFoundComponent',
+ 'pendingComponent',
+ 'loader',
+ 'lazy',
+ ].includes(d._fsRouteType)
+ }
+ return true
+ })
const routeFileAllResult = await Promise.allSettled(
preRouteNodes
@@ -562,6 +575,31 @@ export class Generator {
source: this.targetTemplate.fullPkg,
})
}
+ // Add lazyRouteComponent import if there are component pieces
+ const hasComponentPieces = sortedRouteNodes.some(
+ (node) =>
+ acc.routePiecesByPath[node.routePath!]?.component ||
+ acc.routePiecesByPath[node.routePath!]?.errorComponent ||
+ acc.routePiecesByPath[node.routePath!]?.notFoundComponent ||
+ acc.routePiecesByPath[node.routePath!]?.pendingComponent,
+ )
+ // Add lazyFn import if there are loader pieces
+ const hasLoaderPieces = sortedRouteNodes.some(
+ (node) => acc.routePiecesByPath[node.routePath!]?.loader,
+ )
+ if (hasComponentPieces || hasLoaderPieces) {
+ const runtimeImport: ImportDeclaration = {
+ specifiers: [],
+ source: this.targetTemplate.fullPkg,
+ }
+ if (hasComponentPieces) {
+ runtimeImport.specifiers.push({ imported: 'lazyRouteComponent' })
+ }
+ if (hasLoaderPieces) {
+ runtimeImport.specifiers.push({ imported: 'lazyFn' })
+ }
+ imports.push(runtimeImport)
+ }
if (config.verboseFileRoutes === false) {
const typeImport: ImportDeclaration = {
specifiers: [],
@@ -602,6 +640,8 @@ export class Generator {
const componentNode = acc.routePiecesByPath[node.routePath!]?.component
const errorComponentNode =
acc.routePiecesByPath[node.routePath!]?.errorComponent
+ const notFoundComponentNode =
+ acc.routePiecesByPath[node.routePath!]?.notFoundComponent
const pendingComponentNode =
acc.routePiecesByPath[node.routePath!]?.pendingComponent
const lazyComponentNode = acc.routePiecesByPath[node.routePath!]?.lazy
@@ -628,50 +668,147 @@ export class Generator {
),
)}'), 'loader') })`
: '',
- componentNode || errorComponentNode || pendingComponentNode
+ componentNode ||
+ errorComponentNode ||
+ notFoundComponentNode ||
+ pendingComponentNode
? `.update({
${(
[
['component', componentNode],
['errorComponent', errorComponentNode],
+ ['notFoundComponent', notFoundComponentNode],
['pendingComponent', pendingComponentNode],
] as const
)
.filter((d) => d[1])
.map((d) => {
+ // For .vue files, use 'default' as the export name since Vue SFCs export default
+ const isVueFile = d[1]!.filePath.endsWith('.vue')
+ const exportName = isVueFile ? 'default' : d[0]
+ // Keep .vue extension for Vue files since Vite requires it
+ const importPath = replaceBackslash(
+ isVueFile
+ ? path.relative(
+ path.dirname(config.generatedRouteTree),
+ path.resolve(
+ config.routesDirectory,
+ d[1]!.filePath,
+ ),
+ )
+ : removeExt(
+ path.relative(
+ path.dirname(config.generatedRouteTree),
+ path.resolve(
+ config.routesDirectory,
+ d[1]!.filePath,
+ ),
+ ),
+ config.addExtensions,
+ ),
+ )
return `${
d[0]
- }: lazyRouteComponent(() => import('./${replaceBackslash(
- removeExt(
- path.relative(
- path.dirname(config.generatedRouteTree),
- path.resolve(config.routesDirectory, d[1]!.filePath),
- ),
- config.addExtensions,
- ),
- )}'), '${d[0]}')`
+ }: lazyRouteComponent(() => import('./${importPath}'), '${exportName}')`
})
.join('\n,')}
})`
: '',
lazyComponentNode
- ? `.lazy(() => import('./${replaceBackslash(
- removeExt(
- path.relative(
- path.dirname(config.generatedRouteTree),
- path.resolve(
- config.routesDirectory,
- lazyComponentNode.filePath,
- ),
- ),
- config.addExtensions,
- ),
- )}').then((d) => d.Route))`
+ ? (() => {
+ // For .vue files, use 'default' export since Vue SFCs export default
+ const isVueFile = lazyComponentNode.filePath.endsWith('.vue')
+ const exportAccessor = isVueFile ? 'd.default' : 'd.Route'
+ // Keep .vue extension for Vue files since Vite requires it
+ const importPath = replaceBackslash(
+ isVueFile
+ ? path.relative(
+ path.dirname(config.generatedRouteTree),
+ path.resolve(
+ config.routesDirectory,
+ lazyComponentNode.filePath,
+ ),
+ )
+ : removeExt(
+ path.relative(
+ path.dirname(config.generatedRouteTree),
+ path.resolve(
+ config.routesDirectory,
+ lazyComponentNode.filePath,
+ ),
+ ),
+ config.addExtensions,
+ ),
+ )
+ return `.lazy(() => import('./${importPath}').then((d) => ${exportAccessor}))`
+ })()
: '',
].join(''),
].join('\n\n')
})
+ // Generate update for root route if it has component pieces
+ const rootRoutePath = `/${rootPathId}`
+ const rootComponentNode = acc.routePiecesByPath[rootRoutePath]?.component
+ const rootErrorComponentNode =
+ acc.routePiecesByPath[rootRoutePath]?.errorComponent
+ const rootNotFoundComponentNode =
+ acc.routePiecesByPath[rootRoutePath]?.notFoundComponent
+ const rootPendingComponentNode =
+ acc.routePiecesByPath[rootRoutePath]?.pendingComponent
+
+ let rootRouteUpdate = ''
+ if (
+ rootComponentNode ||
+ rootErrorComponentNode ||
+ rootNotFoundComponentNode ||
+ rootPendingComponentNode
+ ) {
+ rootRouteUpdate = `const rootRouteWithChildren = rootRouteImport${
+ rootComponentNode ||
+ rootErrorComponentNode ||
+ rootNotFoundComponentNode ||
+ rootPendingComponentNode
+ ? `.update({
+ ${(
+ [
+ ['component', rootComponentNode],
+ ['errorComponent', rootErrorComponentNode],
+ ['notFoundComponent', rootNotFoundComponentNode],
+ ['pendingComponent', rootPendingComponentNode],
+ ] as const
+ )
+ .filter((d) => d[1])
+ .map((d) => {
+ // For .vue files, use 'default' as the export name since Vue SFCs export default
+ const isVueFile = d[1]!.filePath.endsWith('.vue')
+ const exportName = isVueFile ? 'default' : d[0]
+ // Keep .vue extension for Vue files since Vite requires it
+ const importPath = replaceBackslash(
+ isVueFile
+ ? path.relative(
+ path.dirname(config.generatedRouteTree),
+ path.resolve(config.routesDirectory, d[1]!.filePath),
+ )
+ : removeExt(
+ path.relative(
+ path.dirname(config.generatedRouteTree),
+ path.resolve(
+ config.routesDirectory,
+ d[1]!.filePath,
+ ),
+ ),
+ config.addExtensions,
+ ),
+ )
+ return `${d[0]}: lazyRouteComponent(() => import('./${importPath}'), '${exportName}')`
+ })
+ .join('\n,')}
+ })`
+ : ''
+ }._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes()`}`
+ }
+
let fileRoutesByPathInterface = ''
let fileRoutesByFullPath = ''
@@ -741,7 +878,12 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
)
.join(',')}
}`,
- `export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes()`}`,
+ rootRouteUpdate
+ ? rootRouteUpdate.replace(
+ 'const rootRouteWithChildren = ',
+ 'export const routeTree = ',
+ )
+ : `export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)${config.disableTypes ? '' : `._addFileTypes()`}`,
].join('\n')
checkRouteFullPathUniqueness(
@@ -894,6 +1036,7 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
'component',
'pendingComponent',
'errorComponent',
+ 'notFoundComponent',
'loader',
] satisfies Array
).every((d) => d !== node._fsRouteType)
@@ -915,32 +1058,39 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
return null
}
}
- // transform the file
- const transformResult = await transform({
- source: updatedCacheEntry.fileContent,
- ctx: {
- target: this.config.target,
- routeId: escapedRoutePath,
- lazy: node._fsRouteType === 'lazy',
- verboseFileRoutes: !(this.config.verboseFileRoutes === false),
- },
- node,
- })
- if (transformResult.result === 'no-route-export') {
- this.logger.warn(
- `Route file "${node.fullPath}" does not contain any route piece. This is likely a mistake.`,
- )
- return null
- }
- if (transformResult.result === 'error') {
- throw new Error(
- `Error transforming route file ${node.fullPath}: ${transformResult.error}`,
- )
- }
- if (transformResult.result === 'modified') {
- updatedCacheEntry.fileContent = transformResult.output
- shouldWriteRouteFile = true
+ // Check if this is a Vue component file
+ // Vue SFC files (.vue) don't need transformation as they can't have a Route export
+ const isVueFile = node.filePath.endsWith('.vue')
+
+ if (!isVueFile) {
+ // transform the file
+ const transformResult = await transform({
+ source: updatedCacheEntry.fileContent,
+ ctx: {
+ target: this.config.target,
+ routeId: escapedRoutePath,
+ lazy: node._fsRouteType === 'lazy',
+ verboseFileRoutes: !(this.config.verboseFileRoutes === false),
+ },
+ node,
+ })
+
+ if (transformResult.result === 'no-route-export') {
+ this.logger.warn(
+ `Route file "${node.fullPath}" does not contain any route piece. This is likely a mistake.`,
+ )
+ return null
+ }
+ if (transformResult.result === 'error') {
+ throw new Error(
+ `Error transforming route file ${node.fullPath}: ${transformResult.error}`,
+ )
+ }
+ if (transformResult.result === 'modified') {
+ updatedCacheEntry.fileContent = transformResult.output
+ shouldWriteRouteFile = true
+ }
}
for (const plugin of this.plugins) {
@@ -1262,6 +1412,7 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
'component',
'pendingComponent',
'errorComponent',
+ 'notFoundComponent',
] satisfies Array
).some((d) => d === node._fsRouteType)
) {
@@ -1275,16 +1426,19 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
? 'loader'
: node._fsRouteType === 'errorComponent'
? 'errorComponent'
- : node._fsRouteType === 'pendingComponent'
- ? 'pendingComponent'
- : 'component'
+ : node._fsRouteType === 'notFoundComponent'
+ ? 'notFoundComponent'
+ : node._fsRouteType === 'pendingComponent'
+ ? 'pendingComponent'
+ : 'component'
] = node
const anchorRoute = acc.routeNodes.find(
(d) => d.routePath === node.routePath,
)
- if (!anchorRoute) {
+ // Don't create virtual routes for root route component pieces - the root route is handled separately
+ if (!anchorRoute && node.routePath !== `/${rootPathId}`) {
this.handleNode(
{
...node,
diff --git a/packages/router-generator/src/template.ts b/packages/router-generator/src/template.ts
index 5204cee26f8..69031f689cb 100644
--- a/packages/router-generator/src/template.ts
+++ b/packages/router-generator/src/template.ts
@@ -167,6 +167,71 @@ export function getTargetTemplate(config: Config): TargetTemplate {
? 'export const Route = createLazyFileRoute('
: `export const Route = createLazyFileRoute('${routePath}')(`,
+ tsrExportEnd: () => ');',
+ },
+ },
+ }
+ case 'vue':
+ return {
+ fullPkg: '@tanstack/vue-router',
+ subPkg: 'vue-router',
+ rootRoute: {
+ template: () =>
+ [
+ 'import { h } from "vue"\n',
+ '%%tsrImports%%',
+ '\n\n',
+ '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n',
+ 'function RootComponent() { return h("div", {}, ["Hello \\"%%tsrPath%%\\"!", h(Outlet)]) };\n',
+ ].join(''),
+ imports: {
+ tsrImports: () =>
+ "import { Outlet, createRootRoute } from '@tanstack/vue-router';",
+ tsrExportStart: () => 'export const Route = createRootRoute(',
+ tsrExportEnd: () => ');',
+ },
+ },
+ route: {
+ template: () =>
+ [
+ 'import { h } from "vue"\n',
+ '%%tsrImports%%',
+ '\n\n',
+ '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n',
+ 'function RouteComponent() { return h("div", {}, "Hello \\"%%tsrPath%%\\"!") };\n',
+ ].join(''),
+ imports: {
+ tsrImports: () =>
+ config.verboseFileRoutes === false
+ ? ''
+ : "import { createFileRoute } from '@tanstack/vue-router';",
+ tsrExportStart: (routePath) =>
+ config.verboseFileRoutes === false
+ ? 'export const Route = createFileRoute('
+ : `export const Route = createFileRoute('${routePath}')(`,
+ tsrExportEnd: () => ');',
+ },
+ },
+ lazyRoute: {
+ template: () =>
+ [
+ 'import { h } from "vue"\n',
+ '%%tsrImports%%',
+ '\n\n',
+ '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n',
+ 'function RouteComponent() { return h("div", {}, "Hello \\"%%tsrPath%%\\"!") };\n',
+ ].join(''),
+ imports: {
+ tsrImports: () =>
+ config.verboseFileRoutes === false
+ ? ''
+ : "import { createLazyFileRoute } from '@tanstack/vue-router';",
+
+ tsrExportStart: (routePath) =>
+ config.verboseFileRoutes === false
+ ? 'export const Route = createLazyFileRoute('
+ : `export const Route = createLazyFileRoute('${routePath}')(`,
+
tsrExportEnd: () => ');',
},
},
diff --git a/packages/router-generator/src/types.ts b/packages/router-generator/src/types.ts
index 1b9a74b2540..997d5ff7a20 100644
--- a/packages/router-generator/src/types.ts
+++ b/packages/router-generator/src/types.ts
@@ -33,10 +33,12 @@ export type FsRouteType =
| 'component' // @deprecated
| 'pendingComponent' // @deprecated
| 'errorComponent' // @deprecated
+ | 'notFoundComponent' // @deprecated
export type RouteSubNode = {
component?: RouteNode
errorComponent?: RouteNode
+ notFoundComponent?: RouteNode
pendingComponent?: RouteNode
loader?: RouteNode
lazy?: RouteNode
diff --git a/packages/router-plugin/src/core/code-splitter/framework-options.ts b/packages/router-plugin/src/core/code-splitter/framework-options.ts
index 860178b1f1e..0a2a9e8230a 100644
--- a/packages/router-plugin/src/core/code-splitter/framework-options.ts
+++ b/packages/router-plugin/src/core/code-splitter/framework-options.ts
@@ -31,6 +31,16 @@ export function getFrameworkOptions(framework: string): FrameworkOptions {
},
}
break
+ case 'vue':
+ frameworkOptions = {
+ package: '@tanstack/vue-router',
+ idents: {
+ createFileRoute: 'createFileRoute',
+ lazyFn: 'lazyFn',
+ lazyRouteComponent: 'lazyRouteComponent',
+ },
+ }
+ break
default:
throw new Error(
`[getFrameworkOptions] - Unsupported framework: ${framework}`,
diff --git a/packages/vue-router-devtools/README.md b/packages/vue-router-devtools/README.md
new file mode 100644
index 00000000000..f88118caa59
--- /dev/null
+++ b/packages/vue-router-devtools/README.md
@@ -0,0 +1,5 @@
+
+
+# TanStack Vue Router Devtools
+
+See https://tanstack.com/router/latest/docs/framework/vue/devtools
diff --git a/packages/vue-router-devtools/eslint.config.js b/packages/vue-router-devtools/eslint.config.js
new file mode 100644
index 00000000000..bd7118fa1a8
--- /dev/null
+++ b/packages/vue-router-devtools/eslint.config.js
@@ -0,0 +1,20 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+export default [
+ ...rootConfig,
+ {
+ files: ['**/*.{ts,tsx}'],
+ },
+ {
+ plugins: {},
+ rules: {},
+ },
+ {
+ files: ['**/__tests__/**'],
+ rules: {
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ },
+ },
+]
diff --git a/packages/vue-router-devtools/package.json b/packages/vue-router-devtools/package.json
new file mode 100644
index 00000000000..9c0a38cdc67
--- /dev/null
+++ b/packages/vue-router-devtools/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "@tanstack/vue-router-devtools",
+ "version": "1.114.25",
+ "description": "Modern and scalable routing for Vue applications",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TanStack/router.git",
+ "directory": "packages/vue-router-devtools"
+ },
+ "homepage": "https://tanstack.com/router",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "vue",
+ "location",
+ "router",
+ "routing",
+ "async",
+ "async router",
+ "typescript"
+ ],
+ "scripts": {
+ "clean": "rimraf ./dist && rimraf ./coverage",
+ "test:eslint": "eslint ./src",
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+ "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+ "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
+ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
+ "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
+ "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
+ "test:types:ts59": "tsc",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "./dist/esm/index.d.ts",
+ "main": "./dist/cjs/index.cjs",
+ "module": "./dist/esm/index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "dependencies": {
+ "@tanstack/router-devtools-core": "workspace:*",
+ "vite": "^7.1.7"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "vue": "^3.5.13"
+ },
+ "peerDependencies": {
+ "@tanstack/vue-router": "workspace:^",
+ "@tanstack/router-core": "workspace:^",
+ "vue": "^3.5.13"
+ },
+ "peerDependenciesMeta": {
+ "@tanstack/router-core": {
+ "optional": true
+ }
+ }
+}
diff --git a/packages/vue-router-devtools/src/TanStackRouterDevtools.tsx b/packages/vue-router-devtools/src/TanStackRouterDevtools.tsx
new file mode 100644
index 00000000000..75c04dfb56b
--- /dev/null
+++ b/packages/vue-router-devtools/src/TanStackRouterDevtools.tsx
@@ -0,0 +1,125 @@
+import { TanStackRouterDevtoolsCore } from '@tanstack/router-devtools-core'
+import { defineComponent, h, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useRouter, useRouterState } from '@tanstack/vue-router'
+import type { AnyRouter } from '@tanstack/vue-router'
+
+export interface TanStackRouterDevtoolsOptions {
+ /**
+ * Set this true if you want the dev tools to default to being open
+ */
+ initialIsOpen?: boolean
+ /**
+ * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc.
+ */
+ panelProps?: Record
+ /**
+ * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
+ */
+ closeButtonProps?: Record
+ /**
+ * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
+ */
+ toggleButtonProps?: Record
+ /**
+ * The position of the TanStack Router logo to open and close the devtools panel.
+ * Defaults to 'bottom-left'.
+ */
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
+ /**
+ * Use this to render the devtools inside a different type of container element for a11y purposes.
+ * Any string which corresponds to a valid intrinsic JSX element is allowed.
+ * Defaults to 'footer'.
+ */
+ containerElement?: string | any
+ /**
+ * The router instance to use for the devtools.
+ */
+ router?: AnyRouter
+ /**
+ * Use this to attach the devtool's styles to specific element in the DOM.
+ */
+ shadowDOMTarget?: ShadowRoot
+}
+
+export const TanStackRouterDevtools = /* #__PURE__ */ defineComponent({
+ name: 'TanStackRouterDevtools',
+ props: [
+ 'initialIsOpen',
+ 'panelProps',
+ 'closeButtonProps',
+ 'toggleButtonProps',
+ 'position',
+ 'containerElement',
+ 'router',
+ 'shadowDOMTarget',
+ ] as unknown as undefined,
+ setup(props: TanStackRouterDevtoolsOptions) {
+ const devToolRef = ref(null)
+
+ const hookRouter = useRouter({ warn: false })
+ const activeRouter = props.router ?? hookRouter
+
+ const activeRouterState = useRouterState({ router: activeRouter })
+
+ const devtools = new TanStackRouterDevtoolsCore({
+ initialIsOpen: props.initialIsOpen,
+ panelProps: props.panelProps,
+ closeButtonProps: props.closeButtonProps,
+ toggleButtonProps: props.toggleButtonProps,
+ position: props.position,
+ containerElement: props.containerElement,
+ shadowDOMTarget: props.shadowDOMTarget,
+ router: activeRouter,
+ routerState: activeRouterState.value,
+ })
+
+ // Update devtools when router changes
+ watch(
+ () => activeRouter,
+ (router) => {
+ devtools.setRouter(router)
+ },
+ )
+
+ // Update devtools when router state changes
+ watch(activeRouterState, (routerState) => {
+ devtools.setRouterState(routerState)
+ })
+
+ // Update devtools when options change
+ watch(
+ () => [
+ props.initialIsOpen,
+ props.panelProps,
+ props.closeButtonProps,
+ props.toggleButtonProps,
+ props.position,
+ props.containerElement,
+ props.shadowDOMTarget,
+ ],
+ () => {
+ devtools.setOptions({
+ initialIsOpen: props.initialIsOpen,
+ panelProps: props.panelProps,
+ closeButtonProps: props.closeButtonProps,
+ toggleButtonProps: props.toggleButtonProps,
+ position: props.position,
+ containerElement: props.containerElement,
+ shadowDOMTarget: props.shadowDOMTarget,
+ })
+ },
+ )
+
+ onMounted(() => {
+ if (devToolRef.value) {
+ devtools.mount(devToolRef.value)
+ }
+ })
+
+ onUnmounted(() => {
+ devtools.unmount()
+ })
+
+ return () => h('div', { ref: devToolRef })
+ },
+})
diff --git a/packages/vue-router-devtools/src/TanStackRouterDevtoolsPanel.tsx b/packages/vue-router-devtools/src/TanStackRouterDevtoolsPanel.tsx
new file mode 100644
index 00000000000..222b41c33b9
--- /dev/null
+++ b/packages/vue-router-devtools/src/TanStackRouterDevtoolsPanel.tsx
@@ -0,0 +1,104 @@
+import { TanStackRouterDevtoolsPanelCore } from '@tanstack/router-devtools-core'
+import { defineComponent, h, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useRouter, useRouterState } from '@tanstack/vue-router'
+import type { AnyRouter } from '@tanstack/vue-router'
+
+export interface TanStackRouterDevtoolsPanelOptions {
+ /**
+ * The standard style object used to style a component with inline styles
+ */
+ style?: any
+ /**
+ * The standard class property used to style a component with classes
+ */
+ className?: string
+ /**
+ * A boolean variable indicating whether the panel is open or closed
+ */
+ isOpen?: boolean
+ /**
+ * A function that toggles the open and close state of the panel
+ */
+ setIsOpen?: (isOpen: boolean) => void
+ /**
+ * Handles the opening and closing the devtools panel
+ */
+ handleDragStart?: (e: any) => void
+ /**
+ * The router instance to use for the devtools.
+ */
+ router?: AnyRouter
+ /**
+ * Use this to attach the devtool's styles to specific element in the DOM.
+ */
+ shadowDOMTarget?: ShadowRoot
+}
+
+export const TanStackRouterDevtoolsPanel = /* #__PURE__ */ defineComponent({
+ name: 'TanStackRouterDevtoolsPanel',
+ props: [
+ 'style',
+ 'className',
+ 'isOpen',
+ 'setIsOpen',
+ 'handleDragStart',
+ 'router',
+ 'shadowDOMTarget',
+ ] as unknown as undefined,
+ setup(props: TanStackRouterDevtoolsPanelOptions) {
+ const devToolRef = ref(null)
+
+ const hookRouter = useRouter({ warn: false })
+ const activeRouter = props.router ?? hookRouter
+
+ const activeRouterState = useRouterState({ router: activeRouter })
+
+ const devtools = new TanStackRouterDevtoolsPanelCore({
+ style: props.style,
+ className: props.className,
+ isOpen: props.isOpen,
+ setIsOpen: props.setIsOpen,
+ handleDragStart: props.handleDragStart,
+ shadowDOMTarget: props.shadowDOMTarget,
+ router: activeRouter,
+ routerState: activeRouterState.value,
+ })
+
+ // Update devtools when router changes
+ watch(
+ () => activeRouter,
+ (router) => {
+ devtools.setRouter(router)
+ },
+ )
+
+ // Update devtools when router state changes
+ watch(activeRouterState, (routerState) => {
+ devtools.setRouterState(routerState)
+ })
+
+ // Update devtools when options change
+ watch(
+ () => [props.className, props.style, props.shadowDOMTarget],
+ () => {
+ devtools.setOptions({
+ className: props.className,
+ style: props.style,
+ shadowDOMTarget: props.shadowDOMTarget,
+ })
+ },
+ )
+
+ onMounted(() => {
+ if (devToolRef.value) {
+ devtools.mount(devToolRef.value)
+ }
+ })
+
+ onUnmounted(() => {
+ devtools.unmount()
+ })
+
+ return () => h('div', { ref: devToolRef })
+ },
+})
diff --git a/packages/vue-router-devtools/src/index.tsx b/packages/vue-router-devtools/src/index.tsx
new file mode 100644
index 00000000000..098e03b9f46
--- /dev/null
+++ b/packages/vue-router-devtools/src/index.tsx
@@ -0,0 +1,27 @@
+import { defineComponent } from 'vue'
+import { TanStackRouterDevtools as DevToolsComponent } from './TanStackRouterDevtools'
+import { TanStackRouterDevtoolsPanel as DevToolsPanelComponent } from './TanStackRouterDevtoolsPanel'
+
+// Re-export types
+export type { TanStackRouterDevtoolsOptions } from './TanStackRouterDevtools'
+export type { TanStackRouterDevtoolsPanelOptions } from './TanStackRouterDevtoolsPanel'
+
+// Create a null component for production
+const NullComponent = /* #__PURE__ */ defineComponent({
+ name: 'NullTanStackRouterDevtools',
+ setup() {
+ return () => null
+ },
+})
+
+export const TanStackRouterDevtools =
+ process.env.NODE_ENV !== 'development' ? NullComponent : DevToolsComponent
+
+export const TanStackRouterDevtoolsInProd = DevToolsComponent
+
+export const TanStackRouterDevtoolsPanel =
+ process.env.NODE_ENV !== 'development'
+ ? NullComponent
+ : DevToolsPanelComponent
+
+export const TanStackRouterDevtoolsPanelInProd = DevToolsPanelComponent
diff --git a/packages/vue-router-devtools/tsconfig.json b/packages/vue-router-devtools/tsconfig.json
new file mode 100644
index 00000000000..9ff17fad2b1
--- /dev/null
+++ b/packages/vue-router-devtools/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "vue"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/packages/vue-router-devtools/vite.config.ts b/packages/vue-router-devtools/vite.config.ts
new file mode 100644
index 00000000000..3b32a63c521
--- /dev/null
+++ b/packages/vue-router-devtools/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+import type { UserConfig } from 'vitest/config'
+
+const config = defineConfig({
+ plugins: [vueJsx()] as UserConfig['plugins'],
+})
+
+export default mergeConfig(
+ config,
+ tanstackViteConfig({
+ entry: './src/index.tsx',
+ srcDir: './src',
+ }),
+)
diff --git a/packages/vue-router-ssr-query/README.md b/packages/vue-router-ssr-query/README.md
new file mode 100644
index 00000000000..a0c0b4928cf
--- /dev/null
+++ b/packages/vue-router-ssr-query/README.md
@@ -0,0 +1,23 @@
+# @tanstack/vue-router-ssr-query
+
+SSR Query integration for TanStack Vue Router.
+
+## Installation
+
+```bash
+npm install @tanstack/vue-router-ssr-query
+```
+
+## Usage
+
+```ts
+import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query'
+import { QueryClient } from '@tanstack/vue-query'
+
+const queryClient = new QueryClient()
+
+setupRouterSsrQueryIntegration({
+ router,
+ queryClient,
+})
+```
diff --git a/packages/vue-router-ssr-query/eslint.config.ts b/packages/vue-router-ssr-query/eslint.config.ts
new file mode 100644
index 00000000000..7644cf8e41b
--- /dev/null
+++ b/packages/vue-router-ssr-query/eslint.config.ts
@@ -0,0 +1,13 @@
+import rootConfig from '../../eslint.config.js'
+
+export default [
+ ...rootConfig,
+ {
+ files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
+ plugins: {},
+ rules: {
+ 'unused-imports/no-unused-vars': 'off',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ },
+ },
+]
diff --git a/packages/vue-router-ssr-query/package.json b/packages/vue-router-ssr-query/package.json
new file mode 100644
index 00000000000..ff099d9067c
--- /dev/null
+++ b/packages/vue-router-ssr-query/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "@tanstack/vue-router-ssr-query",
+ "version": "1.0.0",
+ "description": "Modern and scalable routing for Vue applications",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TanStack/router.git",
+ "directory": "packages/vue-router-ssr-query"
+ },
+ "homepage": "https://tanstack.com/router",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "vue",
+ "location",
+ "router",
+ "routing",
+ "async",
+ "async router",
+ "typescript"
+ ],
+ "scripts": {
+ "clean": "rimraf ./dist && rimraf ./coverage",
+ "test:eslint": "eslint ./src",
+ "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+ "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+ "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
+ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
+ "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
+ "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
+ "test:types:ts59": "tsc",
+ "test:unit": "exit 0; vitest",
+ "test:unit:dev": "pnpm run test:unit --watch",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "main": "dist/cjs/index.cjs",
+ "module": "dist/esm/index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "dependencies": {
+ "@tanstack/router-ssr-query-core": "workspace:*"
+ },
+ "devDependencies": {
+ "@tanstack/vue-query": "^5.92.0",
+ "@tanstack/vue-router": "workspace:*",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "vue": "^3.5.25"
+ },
+ "peerDependencies": {
+ "@tanstack/query-core": ">=5.90.0",
+ "@tanstack/vue-query": ">=5.90.0",
+ "@tanstack/vue-router": ">=1.127.0",
+ "vue": "^3.3.0"
+ }
+}
diff --git a/packages/vue-router-ssr-query/src/index.tsx b/packages/vue-router-ssr-query/src/index.tsx
new file mode 100644
index 00000000000..6a0a81e27cb
--- /dev/null
+++ b/packages/vue-router-ssr-query/src/index.tsx
@@ -0,0 +1,46 @@
+import * as Vue from 'vue'
+import { setupCoreRouterSsrQueryIntegration } from '@tanstack/router-ssr-query-core'
+import type { RouterSsrQueryOptions } from '@tanstack/router-ssr-query-core'
+import type { AnyRouter } from '@tanstack/vue-router'
+import type { QueryClient } from '@tanstack/query-core'
+
+// Vue Query uses this string as the injection key
+const VUE_QUERY_CLIENT = 'VUE_QUERY_CLIENT'
+
+export type Options =
+ RouterSsrQueryOptions & {
+ wrapQueryClient?: boolean
+ }
+
+export function setupRouterSsrQueryIntegration(
+ opts: Options,
+) {
+ setupCoreRouterSsrQueryIntegration(opts)
+
+ if (opts.wrapQueryClient === false) {
+ return
+ }
+
+ const OGWrap =
+ opts.router.options.Wrap || ((props: { children: any }) => props.children)
+
+ opts.router.options.Wrap = (props) => {
+ return Vue.h(QueryClientProvider, { client: opts.queryClient }, () =>
+ Vue.h(OGWrap, null, () => props.children),
+ )
+ }
+}
+
+const QueryClientProvider = Vue.defineComponent({
+ name: 'QueryClientProvider',
+ props: {
+ client: {
+ type: Object as () => QueryClient,
+ required: true,
+ },
+ },
+ setup(props, { slots }) {
+ Vue.provide(VUE_QUERY_CLIENT, props.client)
+ return () => slots.default?.()
+ },
+})
diff --git a/packages/vue-router-ssr-query/tsconfig.json b/packages/vue-router-ssr-query/tsconfig.json
new file mode 100644
index 00000000000..6c6fc6320bd
--- /dev/null
+++ b/packages/vue-router-ssr-query/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "vue"
+ },
+ "include": ["src", "tests", "vite.config.ts", "eslint.config.ts"]
+}
diff --git a/packages/vue-router-ssr-query/vite.config.ts b/packages/vue-router-ssr-query/vite.config.ts
new file mode 100644
index 00000000000..4c70baca683
--- /dev/null
+++ b/packages/vue-router-ssr-query/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+import packageJson from './package.json'
+import type { UserConfig } from 'vitest/config'
+
+const config = defineConfig({
+ plugins: [vueJsx()] as UserConfig['plugins'],
+ test: {
+ name: packageJson.name,
+ dir: './tests',
+ watch: false,
+ environment: 'jsdom',
+ typecheck: { enabled: true },
+ },
+})
+
+export default mergeConfig(
+ config,
+ tanstackViteConfig({
+ entry: './src/index.tsx',
+ srcDir: './src',
+ }),
+)
diff --git a/packages/vue-router/README.md b/packages/vue-router/README.md
new file mode 100644
index 00000000000..2d8023df94d
--- /dev/null
+++ b/packages/vue-router/README.md
@@ -0,0 +1,66 @@
+
+
+# TanStack Solid Router
+
+
+
+🤖 Type-safe router w/ built-in caching & URL state management for Solid!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
+
+## File-Based Routing Conventions
+
+| Suffix/Pattern | Purpose |
+| ----------------------------------- | -------------------------------------------------------- |
+| `.route.ts` | Route configuration (loader, validateSearch, head, etc.) |
+| `.component.vue` | The component rendered for the route |
+| `.errorComponent.vue` | Error boundary component for the route |
+| `.notFoundComponent.vue` | Not found component for the route |
+| `.lazy.ts` | Lazy-loaded route configuration |
+| `_layout` prefix | Layout routes that wrap child routes |
+| `_` suffix (e.g., `posts_.$postId`) | Unnested routes (break out of parent layout) |
+| `(groupName)` directory | Route groups (organizational, don't affect URL) |
+| `$param` | Dynamic route parameters |
+
+### Examples from e2e/basic-file-routes/ project
+
+```
+src/routes/
+├── __root.ts # Root route config
+├── __root.component.vue # Root layout component
+├── __root.notFoundComponent.vue # Global not found component
+├── index.route.ts # "/" route config
+├── index.component.vue # "/" component
+├── posts.route.ts # "/posts" route config
+├── posts.component.vue # "/posts" layout component
+├── posts.index.component.vue # "/posts" index component
+├── posts.$postId.route.ts # "/posts/:postId" route config
+├── posts.$postId.component.vue # "/posts/:postId" component
+├── posts.$postId.errorComponent.vue # Error boundary for post
+├── posts_.$postId.edit.route.ts # "/posts/:postId/edit" (unnested)
+├── (group)/ # Route group (no URL impact)
+│ ├── _layout.route.ts # Layout for group
+│ ├── _layout.component.vue
+│ └── inside.component.vue # "/inside"
+└── 대한민국.component.vue # Unicode routes supported
+```
diff --git a/packages/vue-router/eslint.config.ts b/packages/vue-router/eslint.config.ts
new file mode 100644
index 00000000000..7644cf8e41b
--- /dev/null
+++ b/packages/vue-router/eslint.config.ts
@@ -0,0 +1,13 @@
+import rootConfig from '../../eslint.config.js'
+
+export default [
+ ...rootConfig,
+ {
+ files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
+ plugins: {},
+ rules: {
+ 'unused-imports/no-unused-vars': 'off',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ },
+ },
+]
diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json
new file mode 100644
index 00000000000..45c2d4de148
--- /dev/null
+++ b/packages/vue-router/package.json
@@ -0,0 +1,85 @@
+{
+ "name": "@tanstack/vue-router",
+ "version": "1.0.0",
+ "description": "Modern and scalable routing for Vue applications",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TanStack/router.git",
+ "directory": "packages/vue-router"
+ },
+ "homepage": "https://tanstack.com/router",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "vue",
+ "location",
+ "router",
+ "routing",
+ "async",
+ "async router",
+ "typescript"
+ ],
+ "scripts": {
+ "clean": "rimraf ./dist && rimraf ./coverage",
+ "test": "pnpm run test:unit",
+ "test:eslint": "eslint",
+ "test:types": "pnpm run test:unit .test-d.ts",
+ "test:unit": "vitest",
+ "test:unit:ui": "vitest --ui --watch",
+ "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests",
+ "test:perf": "vitest bench",
+ "test:perf:dev": "pnpm run test:perf --watch --hideSkippedTests",
+ "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
+ "build": "vite build && tsc -p tsconfig.build.json"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "main": "dist/cjs/index.cjs",
+ "module": "dist/esm/index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist",
+ "src"
+ ],
+ "engines": {
+ "node": ">=12"
+ },
+ "dependencies": {
+ "@tanstack/history": "workspace:*",
+ "@tanstack/router-core": "workspace:*",
+ "@tanstack/vue-store": "^0.8.0",
+ "jsesc": "^3.0.2",
+ "tiny-invariant": "^1.3.3",
+ "tiny-warning": "^1.0.3"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/vue": "^8.1.0",
+ "@types/jsesc": "^3.0.3",
+ "@vitejs/plugin-vue": "^5.2.3",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "combinate": "^1.1.11",
+ "vue": "^3.5.25",
+ "zod": "^3.23.8"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+}
diff --git a/packages/vue-router/src/Asset.tsx b/packages/vue-router/src/Asset.tsx
new file mode 100644
index 00000000000..6c620ba41f1
--- /dev/null
+++ b/packages/vue-router/src/Asset.tsx
@@ -0,0 +1,23 @@
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export function Asset({ tag, attrs, children }: RouterManagedTag): any {
+ switch (tag) {
+ case 'title':
+ return {children}
+ case 'meta':
+ return
+ case 'link':
+ return
+ case 'style':
+ return
+ case 'script':
+ if ((attrs as any) && (attrs as any).src) {
+ return
+ }
+ if (typeof children === 'string')
+ return
+ return null
+ default:
+ return null
+ }
+}
diff --git a/packages/vue-router/src/CatchBoundary.tsx b/packages/vue-router/src/CatchBoundary.tsx
new file mode 100644
index 00000000000..6b7b1d4633e
--- /dev/null
+++ b/packages/vue-router/src/CatchBoundary.tsx
@@ -0,0 +1,186 @@
+import * as Vue from 'vue'
+import type { ErrorRouteComponent } from './route'
+
+// Define the error component props interface
+interface ErrorComponentProps {
+ error: Error
+ reset: () => void
+}
+
+// Create a Vue error boundary component
+const VueErrorBoundary = Vue.defineComponent({
+ name: 'VueErrorBoundary',
+ props: {
+ onError: Function,
+ resetKey: [String, Number],
+ },
+ emits: ['catch'],
+ setup(props, { slots }) {
+ const error = Vue.ref(null)
+ const resetFn = Vue.ref<(() => void) | null>(null)
+
+ const reset = () => {
+ error.value = null
+ }
+
+ // Watch for changes in the reset key
+ Vue.watch(
+ () => props.resetKey,
+ (newKey, oldKey) => {
+ if (newKey !== oldKey && error.value) {
+ reset()
+ }
+ },
+ )
+
+ // Capture errors from child components
+ Vue.onErrorCaptured((err: Error) => {
+ // If the error is a Promise (thrown for Suspense), don't treat it as an error
+ // Just ignore it - Suspense will handle it
+ if (
+ err instanceof Promise ||
+ (err && typeof (err as any).then === 'function')
+ ) {
+ return false // Prevent from propagating as an error, but don't set error state
+ }
+
+ error.value = err
+ resetFn.value = reset
+
+ // Call the onError callback if provided
+ if (props.onError) {
+ props.onError(err)
+ }
+
+ // Prevent the error from propagating further
+ return false
+ })
+
+ return () => {
+ // If there's an error, render the fallback
+ if (error.value && slots.fallback) {
+ return slots.fallback({
+ error: error.value,
+ reset,
+ })
+ }
+
+ // Otherwise render the default slot
+ return slots.default && slots.default()
+ }
+ },
+})
+
+// Main CatchBoundary component
+export function CatchBoundary(props: {
+ getResetKey: () => number | string
+ children: Vue.VNode
+ errorComponent?: ErrorRouteComponent
+ onCatch?: (error: Error) => void
+}) {
+ // Create a component to use in the template
+ const CatchBoundaryWrapper = Vue.defineComponent({
+ name: 'CatchBoundaryWrapper',
+ inheritAttrs: false,
+ setup() {
+ const resetKey = Vue.computed(() => props.getResetKey())
+
+ return () => {
+ // Always use our default component as a safe fallback
+ const defaultErrorComponent = ErrorComponent
+
+ return Vue.h(
+ VueErrorBoundary,
+ {
+ resetKey: resetKey.value,
+ onError: props.onCatch,
+ },
+ {
+ default: () => props.children,
+ fallback: ({ error, reset }: ErrorComponentProps) => {
+ // Safely render the error component - either the provided one or the default
+ if (props.errorComponent) {
+ // Use the provided error component
+ return Vue.h(props.errorComponent, { error, reset })
+ } else {
+ // Use the default error component
+ return Vue.h(defaultErrorComponent, { error, reset })
+ }
+ },
+ },
+ )
+ }
+ },
+ })
+
+ return Vue.h(CatchBoundaryWrapper)
+}
+
+// Error component
+export const ErrorComponent = Vue.defineComponent({
+ name: 'ErrorComponent',
+ props: {
+ error: Object,
+ reset: Function,
+ },
+ setup(props) {
+ const show = Vue.ref(process.env.NODE_ENV !== 'production')
+
+ const toggleShow = () => {
+ show.value = !show.value
+ }
+
+ return () =>
+ Vue.h('div', { style: { padding: '.5rem', maxWidth: '100%' } }, [
+ Vue.h(
+ 'div',
+ { style: { display: 'flex', alignItems: 'center', gap: '.5rem' } },
+ [
+ Vue.h(
+ 'strong',
+ { style: { fontSize: '1rem' } },
+ 'Something went wrong!',
+ ),
+ Vue.h(
+ 'button',
+ {
+ style: {
+ appearance: 'none',
+ fontSize: '.6em',
+ border: '1px solid currentColor',
+ padding: '.1rem .2rem',
+ fontWeight: 'bold',
+ borderRadius: '.25rem',
+ },
+ onClick: toggleShow,
+ },
+ show.value ? 'Hide Error' : 'Show Error',
+ ),
+ ],
+ ),
+ Vue.h('div', { style: { height: '.25rem' } }),
+ show.value
+ ? Vue.h('div', {}, [
+ Vue.h(
+ 'pre',
+ {
+ style: {
+ fontSize: '.7em',
+ border: '1px solid red',
+ borderRadius: '.25rem',
+ padding: '.3rem',
+ color: 'red',
+ overflow: 'auto',
+ },
+ },
+ [
+ props.error?.message
+ ? Vue.h('code', {}, props.error.message)
+ : null,
+ ],
+ ),
+ ])
+ : null,
+ ])
+ },
+})
diff --git a/packages/vue-router/src/ClientOnly.tsx b/packages/vue-router/src/ClientOnly.tsx
new file mode 100644
index 00000000000..f3d2cbc414d
--- /dev/null
+++ b/packages/vue-router/src/ClientOnly.tsx
@@ -0,0 +1,75 @@
+import * as Vue from 'vue'
+
+export interface ClientOnlyProps {
+ /**
+ * The children to render when the JS is loaded.
+ */
+ children?: Vue.VNode
+ /**
+ * The fallback component to render if the JS is not yet loaded.
+ */
+ fallback?: Vue.VNode
+}
+
+/**
+ * Render the children only after the JS has loaded client-side. Use an optional
+ * fallback component if the JS is not yet loaded.
+ *
+ * @example
+ * Render a Chart component if JS loads, renders a simple FakeChart
+ * component server-side or if there is no JS. The FakeChart can have only the
+ * UI without the behavior or be a loading spinner or skeleton.
+ *
+ * ```tsx
+ * return (
+ * }>
+ *
+ *
+ * )
+ * ```
+ */
+export const ClientOnly = Vue.defineComponent({
+ name: 'ClientOnly',
+ props: {
+ fallback: {
+ type: Object as Vue.PropType,
+ default: null,
+ },
+ },
+ setup(props, { slots }) {
+ const hydrated = useHydrated()
+ return () => {
+ if (hydrated.value) {
+ return slots.default?.()
+ }
+ return props.fallback ?? null
+ }
+ },
+})
+
+/**
+ * Return a boolean indicating if the JS has been hydrated already.
+ * When doing Server-Side Rendering, the result will always be false.
+ * When doing Client-Side Rendering, the result will always be false on the
+ * first render and true from then on. Even if a new component renders it will
+ * always start with true.
+ *
+ * @example
+ * ```tsx
+ * // Disable a button that needs JS to work.
+ * const hydrated = useHydrated()
+ * return (
+ *
+ * )
+ * ```
+ * @returns True if the JS has been hydrated already, false otherwise.
+ */
+export function useHydrated(): Vue.Ref {
+ const hydrated = Vue.ref(false)
+ Vue.onMounted(() => {
+ hydrated.value = true
+ })
+ return hydrated
+}
diff --git a/packages/vue-router/src/HeadContent.tsx b/packages/vue-router/src/HeadContent.tsx
new file mode 100644
index 00000000000..3aa7fc36a94
--- /dev/null
+++ b/packages/vue-router/src/HeadContent.tsx
@@ -0,0 +1,159 @@
+import * as Vue from 'vue'
+
+import { Asset } from './Asset'
+import { useRouter } from './useRouter'
+import { useRouterState } from './useRouterState'
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export const useTags = () => {
+ const router = useRouter()
+
+ const routeMeta = useRouterState({
+ select: (state) => {
+ return state.matches.map((match) => match.meta!).filter(Boolean)
+ },
+ })
+
+ const meta: Vue.Ref> = Vue.computed(() => {
+ const resultMeta: Array = []
+ const metaByAttribute: Record = {}
+ let title: RouterManagedTag | undefined
+ ;[...routeMeta.value].reverse().forEach((metas) => {
+ ;[...metas].reverse().forEach((m) => {
+ if (!m) return
+
+ if (m.title) {
+ if (!title) {
+ title = {
+ tag: 'title',
+ children: m.title,
+ }
+ }
+ } else {
+ const attribute = m.name ?? m.property
+ if (attribute) {
+ if (metaByAttribute[attribute]) {
+ return
+ } else {
+ metaByAttribute[attribute] = true
+ }
+ }
+
+ resultMeta.push({
+ tag: 'meta',
+ attrs: {
+ ...m,
+ },
+ })
+ }
+ })
+ })
+
+ if (title) {
+ resultMeta.push(title)
+ }
+
+ resultMeta.reverse()
+
+ return resultMeta
+ })
+
+ const links = useRouterState({
+ select: (state) =>
+ state.matches
+ .map((match) => match.links!)
+ .filter(Boolean)
+ .flat(1)
+ .map((link) => ({
+ tag: 'link',
+ attrs: {
+ ...link,
+ },
+ })) as Array,
+ })
+
+ const preloadMeta = useRouterState({
+ select: (state) => {
+ const preloadMeta: Array = []
+
+ state.matches
+ .map((match) => router.looseRoutesById[match.routeId]!)
+ .forEach((route) =>
+ router.ssr?.manifest?.routes[route.id]?.preloads
+ ?.filter(Boolean)
+ .forEach((preload) => {
+ preloadMeta.push({
+ tag: 'link',
+ attrs: {
+ rel: 'modulepreload',
+ href: preload,
+ },
+ })
+ }),
+ )
+
+ return preloadMeta
+ },
+ })
+
+ const headScripts = useRouterState({
+ select: (state) =>
+ (
+ state.matches
+ .map((match) => match.headScripts!)
+ .flat(1)
+ .filter(Boolean) as Array
+ ).map(({ children, ...script }) => ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ },
+ children,
+ })),
+ })
+
+ return () =>
+ uniqBy(
+ [
+ ...meta.value,
+ ...preloadMeta.value,
+ ...links.value,
+ ...headScripts.value,
+ ] as Array,
+ (d) => {
+ return JSON.stringify(d)
+ },
+ )
+}
+
+/**
+ * @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route.
+ * It should be rendered in the `` of your document.
+ */
+export const HeadContent = Vue.defineComponent({
+ name: 'HeadContent',
+ setup() {
+ const tags = useTags()
+
+ return () => {
+ return tags().map((tag) =>
+ Vue.h(Asset, {
+ ...tag,
+ key: `tsr-meta-${JSON.stringify(tag)}`,
+ }),
+ )
+ }
+ },
+})
+
+function uniqBy(arr: Array, fn: (item: T) => string) {
+ const seen = new Set()
+ return arr.filter((item) => {
+ const key = fn(item)
+ if (seen.has(key)) {
+ return false
+ }
+ seen.add(key)
+ return true
+ })
+}
diff --git a/packages/vue-router/src/Match.tsx b/packages/vue-router/src/Match.tsx
new file mode 100644
index 00000000000..a7f7e48a9ce
--- /dev/null
+++ b/packages/vue-router/src/Match.tsx
@@ -0,0 +1,415 @@
+import * as Vue from 'vue'
+import invariant from 'tiny-invariant'
+import warning from 'tiny-warning'
+import {
+ createControlledPromise,
+ getLocationChangeInfo,
+ isNotFound,
+ isRedirect,
+ rootRouteId,
+} from '@tanstack/router-core'
+import { CatchBoundary, ErrorComponent } from './CatchBoundary'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import { CatchNotFound } from './not-found'
+import { matchContext } from './matchContext'
+import { renderRouteNotFound } from './renderRouteNotFound'
+import { ScrollRestoration } from './scroll-restoration'
+import type { VNode } from 'vue'
+import type { AnyRoute } from '@tanstack/router-core'
+
+export const Match = Vue.defineComponent({
+ name: 'Match',
+ props: {
+ matchId: {
+ type: String,
+ required: true,
+ },
+ },
+ setup(props) {
+ const router = useRouter()
+ const routeId = useRouterState({
+ select: (s) => {
+ return s.matches.find((d) => d.id === props.matchId)?.routeId as string
+ },
+ })
+
+ invariant(
+ routeId.value,
+ `Could not find routeId for matchId "${props.matchId}". Please file an issue!`,
+ )
+
+ const route = Vue.computed(() => router.routesById[routeId.value])
+
+ const PendingComponent = Vue.computed(
+ () =>
+ route.value?.options?.pendingComponent ??
+ router?.options?.defaultPendingComponent,
+ )
+
+ const routeErrorComponent = Vue.computed(
+ () =>
+ route.value?.options?.errorComponent ??
+ router?.options?.defaultErrorComponent,
+ )
+
+ const routeOnCatch = Vue.computed(
+ () => route.value?.options?.onCatch ?? router?.options?.defaultOnCatch,
+ )
+
+ const routeNotFoundComponent = Vue.computed(() =>
+ route.value?.isRoot
+ ? // If it's the root route, use the globalNotFound option, with fallback to the notFoundRoute's component
+ (route.value?.options?.notFoundComponent ??
+ router?.options?.notFoundRoute?.options?.component)
+ : route.value?.options?.notFoundComponent,
+ )
+
+ const resetKey = useRouterState({
+ select: (s) => s.loadedAt,
+ })
+
+ const parentRouteId = useRouterState({
+ select: (s) => {
+ const index = s.matches.findIndex((d) => d.id === props.matchId)
+ return s.matches[index - 1]?.routeId as string
+ },
+ })
+
+ // Create a ref for the current matchId that we can provide to child components
+ const matchIdRef = Vue.ref(props.matchId)
+
+ // When props.matchId changes, update the ref
+ Vue.watch(
+ () => props.matchId,
+ (newMatchId) => {
+ matchIdRef.value = newMatchId
+ },
+ { immediate: true },
+ )
+
+ // Provide the matchId to child components
+ Vue.provide(matchContext, matchIdRef)
+
+ return (): VNode => {
+ // Determine which components to render
+ let content: VNode = Vue.h(MatchInner, { matchId: props.matchId })
+
+ // Wrap in NotFound boundary if needed
+ if (routeNotFoundComponent.value) {
+ content = Vue.h(CatchNotFound, {
+ fallback: (error: any) => {
+ // If the current not found handler doesn't exist or it has a
+ // route ID which doesn't match the current route, rethrow the error
+ if (
+ !routeNotFoundComponent.value ||
+ (error.routeId && error.routeId !== routeId.value) ||
+ (!error.routeId && route.value && !route.value.isRoot)
+ )
+ throw error
+
+ return Vue.h(routeNotFoundComponent.value, error)
+ },
+ children: content,
+ })
+ }
+
+ // Wrap in error boundary if needed
+ if (routeErrorComponent.value) {
+ content = CatchBoundary({
+ getResetKey: () => resetKey.value,
+ errorComponent: routeErrorComponent.value || ErrorComponent,
+ onCatch: (error: Error) => {
+ // Forward not found errors (we don't want to show the error component for these)
+ if (isNotFound(error)) throw error
+ warning(false, `Error in route match: ${props.matchId}`)
+ routeOnCatch.value?.(error)
+ },
+ children: content,
+ })
+ }
+
+ // Wrap in suspense if needed
+ // Root routes should also wrap in Suspense if they have a pendingComponent
+ const needsSuspense =
+ route.value &&
+ (route.value?.options?.wrapInSuspense ??
+ PendingComponent.value ??
+ false)
+
+ if (needsSuspense) {
+ content = Vue.h(
+ Vue.Suspense,
+ {
+ fallback: PendingComponent.value
+ ? Vue.h(PendingComponent.value)
+ : null,
+ },
+ {
+ default: () => content,
+ },
+ )
+ }
+
+ // Add scroll restoration if needed
+ const withScrollRestoration: Array = [
+ content,
+ parentRouteId.value === rootRouteId && router.options.scrollRestoration
+ ? Vue.h(Vue.Fragment, null, [
+ Vue.h(OnRendered),
+ Vue.h(ScrollRestoration),
+ ])
+ : null,
+ ].filter(Boolean) as Array
+
+ return Vue.h(Vue.Fragment, null, withScrollRestoration)
+ }
+ },
+})
+
+// On Rendered can't happen above the root layout because it actually
+// renders a dummy dom element to track the rendered state of the app.
+// We render a script tag with a key that changes based on the current
+// location state.key. Also, because it's below the root layout, it
+// allows us to fire onRendered events even after a hydration mismatch
+// error that occurred above the root layout (like bad head/link tags,
+// which is common).
+const OnRendered = Vue.defineComponent({
+ name: 'OnRendered',
+ setup() {
+ const router = useRouter()
+
+ const location = useRouterState({
+ select: (s) => {
+ return s.resolvedLocation?.state.key
+ },
+ })
+
+ Vue.watchEffect(() => {
+ if (location.value) {
+ router.emit({
+ type: 'onRendered',
+ ...getLocationChangeInfo(router.state),
+ })
+ }
+ })
+
+ return () => null
+ },
+})
+
+export const MatchInner = Vue.defineComponent({
+ name: 'MatchInner',
+ props: {
+ matchId: {
+ type: String,
+ required: true,
+ },
+ },
+ setup(props) {
+ const router = useRouter()
+
+ // { match, key, routeId } =
+ const matchState = useRouterState({
+ select: (s) => {
+ const match = s.matches.find((d) => d.id === props.matchId)
+
+ // During navigation transitions, matches can be temporarily removed
+ if (!match) {
+ return null
+ }
+
+ const routeId = match.routeId as string
+
+ const remountFn =
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
+ router.options.defaultRemountDeps
+ const remountDeps = remountFn?.({
+ routeId,
+ loaderDeps: match.loaderDeps,
+ params: match._strictParams,
+ search: match._strictSearch,
+ })
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
+
+ return {
+ key,
+ routeId,
+ match: {
+ id: match.id,
+ status: match.status,
+ error: match.error,
+ },
+ }
+ },
+ })
+
+ const route = Vue.computed(() => {
+ if (!matchState.value) return null
+ return router.routesById[matchState.value.routeId]!
+ })
+
+ const match = Vue.computed(() => matchState.value?.match)
+
+ const out = Vue.computed((): VNode | null => {
+ if (!route.value) return null
+ const Comp =
+ route.value.options.component ?? router.options.defaultComponent
+ if (Comp) {
+ return Vue.h(Comp)
+ }
+ return Vue.h(Outlet)
+ })
+
+ return (): VNode | null => {
+ // If match doesn't exist, return null (component is being unmounted or not ready)
+ if (!matchState.value || !match.value || !route.value) {
+ return null
+ }
+
+ // Handle different match statuses
+ if (match.value.status === 'notFound') {
+ invariant(isNotFound(match.value.error), 'Expected a notFound error')
+ return renderRouteNotFound(router, route.value, match.value.error)
+ }
+
+ if (match.value.status === 'redirected') {
+ invariant(isRedirect(match.value.error), 'Expected a redirect error')
+ throw router.getMatch(match.value.id)?._nonReactive.loadPromise
+ }
+
+ if (match.value.status === 'error') {
+ // Check if this route or any parent has an error component
+ const RouteErrorComponent =
+ route.value.options.errorComponent ??
+ router.options.defaultErrorComponent
+
+ // If this route has an error component, render it directly
+ // This is more reliable than relying on Vue's error boundary
+ if (RouteErrorComponent) {
+ return Vue.h(RouteErrorComponent, {
+ error: match.value.error,
+ reset: () => {
+ router.invalidate()
+ },
+ info: {
+ componentStack: '',
+ },
+ })
+ }
+
+ // If there's no error component for this route, throw the error
+ // so it can bubble up to the nearest parent with an error component
+ throw match.value.error
+ }
+
+ if (match.value.status === 'pending') {
+ const pendingMinMs =
+ route.value.options.pendingMinMs ?? router.options.defaultPendingMinMs
+
+ const routerMatch = router.getMatch(match.value.id)
+ if (
+ pendingMinMs &&
+ routerMatch &&
+ !routerMatch._nonReactive.minPendingPromise
+ ) {
+ // Create a promise that will resolve after the minPendingMs
+ if (!router.isServer) {
+ const minPendingPromise = createControlledPromise()
+
+ routerMatch._nonReactive.minPendingPromise = minPendingPromise
+
+ setTimeout(() => {
+ minPendingPromise.resolve()
+ // We've handled the minPendingPromise, so we can delete it
+ routerMatch._nonReactive.minPendingPromise = undefined
+ }, pendingMinMs)
+ }
+ }
+
+ // In Vue, we render the pending component directly instead of throwing a promise
+ // because Vue's Suspense doesn't catch thrown promises like React does
+ const PendingComponent =
+ route.value.options.pendingComponent ??
+ router.options.defaultPendingComponent
+
+ if (PendingComponent) {
+ return Vue.h(PendingComponent)
+ }
+
+ // If no pending component, return null while loading
+ return null
+ }
+
+ // Success status - render the component
+ return out.value
+ }
+ },
+})
+
+export const Outlet = Vue.defineComponent({
+ name: 'Outlet',
+ setup() {
+ const router = useRouter()
+ const matchId = Vue.inject(matchContext)
+ const safeMatchId = Vue.computed(() => matchId?.value || '')
+
+ const routeId = useRouterState({
+ select: (s) =>
+ s.matches.find((d) => d.id === safeMatchId.value)?.routeId as string,
+ })
+
+ const route = Vue.computed(() => router.routesById[routeId.value]!)
+
+ const parentGlobalNotFound = useRouterState({
+ select: (s) => {
+ const matches = s.matches
+ const parentMatch = matches.find((d) => d.id === safeMatchId.value)
+
+ // During navigation transitions, parent match can be temporarily removed
+ // Return false to avoid errors - the component will handle this gracefully
+ if (!parentMatch) {
+ return false
+ }
+
+ return parentMatch.globalNotFound
+ },
+ })
+
+ const childMatchId = useRouterState({
+ select: (s) => {
+ const matches = s.matches
+ const index = matches.findIndex((d) => d.id === safeMatchId.value)
+ return matches[index + 1]?.id
+ },
+ })
+
+ return (): VNode | null => {
+ if (parentGlobalNotFound.value) {
+ return renderRouteNotFound(router, route.value, undefined)
+ }
+
+ if (!childMatchId.value) {
+ return null
+ }
+
+ const nextMatch = Vue.h(Match, { matchId: childMatchId.value })
+
+ if (safeMatchId.value === rootRouteId) {
+ return Vue.h(
+ Vue.Suspense,
+ {
+ fallback: router.options.defaultPendingComponent
+ ? Vue.h(router.options.defaultPendingComponent)
+ : null,
+ },
+ {
+ default: () => nextMatch,
+ },
+ )
+ }
+
+ return nextMatch
+ }
+ },
+})
diff --git a/packages/vue-router/src/Matches.tsx b/packages/vue-router/src/Matches.tsx
new file mode 100644
index 00000000000..6f4d66e4cf6
--- /dev/null
+++ b/packages/vue-router/src/Matches.tsx
@@ -0,0 +1,349 @@
+import * as Vue from 'vue'
+import warning from 'tiny-warning'
+import { CatchBoundary } from './CatchBoundary'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import { Transitioner } from './Transitioner'
+import { matchContext } from './matchContext'
+import { Match } from './Match'
+import type {
+ AnyRouter,
+ DeepPartial,
+ ErrorComponentProps,
+ MakeOptionalPathParams,
+ MakeOptionalSearchParams,
+ MakeRouteMatchUnion,
+ MaskOptions,
+ MatchRouteOptions,
+ NoInfer,
+ RegisteredRouter,
+ ResolveRelativePath,
+ ResolveRoute,
+ RouteByPath,
+ RouterState,
+ ToSubOptionsProps,
+} from '@tanstack/router-core'
+
+// Define a type for the error component function
+type ErrorRouteComponentType = (props: ErrorComponentProps) => Vue.VNode
+
+declare module '@tanstack/router-core' {
+ export interface RouteMatchExtensions {
+ meta?: Array
+ links?: Array
+ scripts?: Array
+ headScripts?: Array
+ }
+}
+
+// Create a component that renders both the Transitioner and MatchesInner
+const MatchesContent = Vue.defineComponent({
+ name: 'MatchesContent',
+ setup() {
+ return () =>
+ Vue.h(Vue.Fragment, null, [Vue.h(Transitioner), Vue.h(MatchesInner)])
+ },
+})
+
+export const Matches = Vue.defineComponent({
+ name: 'Matches',
+ setup() {
+ const router = useRouter()
+
+ return () => {
+ const pendingElement = router?.options?.defaultPendingComponent
+ ? Vue.h(router.options.defaultPendingComponent)
+ : null
+
+ // Do not render a root Suspense during SSR or hydrating from SSR
+ const inner =
+ router?.isServer || (typeof document !== 'undefined' && router?.ssr)
+ ? Vue.h(MatchesContent)
+ : Vue.h(
+ Vue.Suspense,
+ { fallback: pendingElement },
+ {
+ default: () => Vue.h(MatchesContent),
+ },
+ )
+
+ return router?.options?.InnerWrap
+ ? Vue.h(router.options.InnerWrap, null, { default: () => inner })
+ : inner
+ }
+ },
+})
+
+// Create a simple error component function that matches ErrorRouteComponent
+const errorComponentFn: ErrorRouteComponentType = (
+ props: ErrorComponentProps,
+) => {
+ return Vue.h('div', { class: 'error' }, [
+ Vue.h('h1', null, 'Error'),
+ Vue.h('p', null, props.error.message || String(props.error)),
+ Vue.h('button', { onClick: props.reset }, 'Try Again'),
+ ])
+}
+
+const MatchesInner = Vue.defineComponent({
+ name: 'MatchesInner',
+ setup() {
+ const router = useRouter()
+
+ const matchId = useRouterState({
+ select: (s) => {
+ return s.matches[0]?.id
+ },
+ })
+
+ const resetKey = useRouterState({
+ select: (s) => s.loadedAt,
+ })
+
+ // Create a ref for the match id to provide
+ const matchIdRef = Vue.computed(() => matchId.value)
+
+ // Provide the matchId for child components using the InjectionKey
+ Vue.provide(matchContext, matchIdRef)
+
+ return () => {
+ // Generate a placeholder element if matchId.value is not present
+ const childElement = matchId.value
+ ? Vue.h(Match, { matchId: matchId.value })
+ : Vue.h('div')
+
+ // If disableGlobalCatchBoundary is true, don't wrap in CatchBoundary
+ if (router.options.disableGlobalCatchBoundary) {
+ return childElement
+ }
+
+ return Vue.h(CatchBoundary, {
+ getResetKey: () => resetKey.value,
+ errorComponent: errorComponentFn,
+ onCatch: (error: Error) => {
+ warning(
+ false,
+ `The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
+ )
+ warning(false, error.message || error.toString())
+ },
+ children: childElement,
+ })
+ }
+ },
+})
+
+export type UseMatchRouteOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = undefined,
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '',
+> = ToSubOptionsProps &
+ DeepPartial> &
+ DeepPartial> &
+ MaskOptions &
+ MatchRouteOptions
+
+export function useMatchRoute() {
+ const router = useRouter()
+
+ // Track state changes to trigger re-computation
+ // Use multiple state values like React does for complete reactivity
+ const routerState = useRouterState({
+ select: (s) => ({
+ locationHref: s.location.href,
+ resolvedLocationHref: s.resolvedLocation?.href,
+ status: s.status,
+ }),
+ })
+
+ return <
+ const TFrom extends string = string,
+ const TTo extends string | undefined = undefined,
+ const TMaskFrom extends string = TFrom,
+ const TMaskTo extends string = '',
+ >(
+ opts: UseMatchRouteOptions,
+ ): Vue.Ref<
+ false | ResolveRoute['types']['allParams']
+ > => {
+ const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts
+
+ const matchRoute = Vue.computed(() => {
+ // Access routerState to establish dependency
+
+ routerState.value
+ return router.matchRoute(rest as any, {
+ pending,
+ caseSensitive,
+ fuzzy,
+ includeSearch,
+ })
+ })
+
+ return matchRoute
+ }
+}
+
+export type MakeMatchRouteOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = undefined,
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '',
+> = UseMatchRouteOptions & {
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
+ children?:
+ | ((
+ params?: RouteByPath<
+ TRouter['routeTree'],
+ ResolveRelativePath>
+ >['types']['allParams'],
+ ) => Vue.VNode)
+ | Vue.VNode
+}
+
+// Create a type for the MatchRoute component that includes the generics
+export interface MatchRouteComponentType {
+ <
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = undefined,
+ >(
+ props: MakeMatchRouteOptions,
+ ): Vue.VNode
+ new (): {
+ $props: {
+ from?: string
+ to?: string
+ fuzzy?: boolean
+ caseSensitive?: boolean
+ includeSearch?: boolean
+ pending?: boolean
+ }
+ }
+}
+
+export const MatchRoute = Vue.defineComponent({
+ name: 'MatchRoute',
+ props: {
+ // Define props to match MakeMatchRouteOptions
+ from: {
+ type: String,
+ required: false,
+ },
+ to: {
+ type: String,
+ required: false,
+ },
+ fuzzy: {
+ type: Boolean,
+ required: false,
+ },
+ caseSensitive: {
+ type: Boolean,
+ required: false,
+ },
+ includeSearch: {
+ type: Boolean,
+ required: false,
+ },
+ pending: {
+ type: Boolean,
+ required: false,
+ },
+ },
+ setup(props, { slots }) {
+ const status = useRouterState({
+ select: (s) => s.status,
+ })
+
+ return () => {
+ if (!status.value) return null
+
+ const matchRoute = useMatchRoute()
+ const params = matchRoute(props as any).value as boolean
+
+ // Create a component that renders the slot in a reactive manner
+ if (!params || !slots.default) {
+ return null
+ }
+
+ // For function slots, pass the params
+ if (typeof slots.default === 'function') {
+ // Use h to create a wrapper component that will call the slot function
+ return Vue.h(Vue.Fragment, null, slots.default(params))
+ }
+
+ // For normal slots, just render them
+ return Vue.h(Vue.Fragment, null, slots.default)
+ }
+ },
+}) as unknown as MatchRouteComponentType
+
+export interface UseMatchesBaseOptions {
+ select?: (matches: Array>) => TSelected
+}
+
+export type UseMatchesResult<
+ TRouter extends AnyRouter,
+ TSelected,
+> = unknown extends TSelected ? Array> : TSelected
+
+export function useMatches<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TSelected = unknown,
+>(
+ opts?: UseMatchesBaseOptions,
+): Vue.Ref> {
+ return useRouterState({
+ select: (state: RouterState) => {
+ const matches = state?.matches || []
+ return opts?.select
+ ? opts.select(matches as Array>)
+ : matches
+ },
+ } as any) as Vue.Ref>
+}
+
+export function useParentMatches<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TSelected = unknown,
+>(
+ opts?: UseMatchesBaseOptions,
+): Vue.Ref> {
+ // Use matchContext with proper type
+ const contextMatchId = Vue.inject>(matchContext)
+ const safeMatchId = Vue.computed(() => contextMatchId?.value || '')
+
+ return useMatches({
+ select: (matches: Array>) => {
+ matches = matches.slice(
+ 0,
+ matches.findIndex((d) => d.id === safeMatchId.value),
+ )
+ return opts?.select ? opts.select(matches) : matches
+ },
+ } as any)
+}
+
+export function useChildMatches<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TSelected = unknown,
+>(
+ opts?: UseMatchesBaseOptions,
+): Vue.Ref> {
+ // Use matchContext with proper type
+ const contextMatchId = Vue.inject>(matchContext)
+ const safeMatchId = Vue.computed(() => contextMatchId?.value || '')
+
+ return useMatches({
+ select: (matches: Array>) => {
+ matches = matches.slice(
+ matches.findIndex((d) => d.id === safeMatchId.value) + 1,
+ )
+ return opts?.select ? opts.select(matches) : matches
+ },
+ } as any)
+}
diff --git a/packages/vue-router/src/RouterProvider.tsx b/packages/vue-router/src/RouterProvider.tsx
new file mode 100644
index 00000000000..cf95d658470
--- /dev/null
+++ b/packages/vue-router/src/RouterProvider.tsx
@@ -0,0 +1,117 @@
+import * as Vue from 'vue'
+import { Matches } from './Matches'
+import { provideRouter } from './routerContext'
+import type {
+ AnyRouter,
+ RegisteredRouter,
+ RouterOptions,
+} from '@tanstack/router-core'
+
+// Component that provides router context and renders children
+export const RouterContextProvider = Vue.defineComponent({
+ name: 'RouterContextProvider',
+ props: {
+ router: {
+ type: Object,
+ required: true,
+ },
+ // Rest of router options will be passed as attrs
+ },
+ setup(props, { attrs, slots }) {
+ const router = props.router as AnyRouter
+ const restAttrs = attrs as Record
+
+ // Allow the router to update options on the router instance
+ router.update({
+ ...router.options,
+ ...restAttrs,
+ context: {
+ ...router.options.context,
+ ...((restAttrs.context as Record) || {}),
+ },
+ } as any)
+
+ // Provide router to all child components
+ provideRouter(router)
+
+ return () => {
+ // Get child content
+ const childContent = slots.default?.()
+
+ // If a Wrap component is specified in router options, use it
+ if (router.options.Wrap) {
+ const WrapComponent = router.options.Wrap
+ return Vue.h(WrapComponent, null, () => childContent)
+ }
+
+ // Otherwise just return the child content
+ return childContent
+ }
+ },
+})
+
+// The main router provider component that includes matches
+export const RouterProvider = Vue.defineComponent({
+ name: 'RouterProvider',
+ props: {
+ router: {
+ type: Object,
+ required: true,
+ },
+ // Rest of router options will be passed as attrs
+ },
+ setup(props, { attrs }) {
+ const restAttrs = attrs as Record
+
+ return () => {
+ return Vue.h(
+ RouterContextProvider,
+ {
+ router: props.router,
+ ...restAttrs,
+ },
+ {
+ default: () => Vue.h(Matches),
+ },
+ )
+ }
+ },
+}) as unknown as {
+ (
+ props: {
+ router: TRouter
+ routeTree?: TRouter['routeTree']
+ } & Record,
+ ): Vue.VNode
+ new (): {
+ $props: {
+ router: AnyRouter
+ routeTree?: AnyRouter['routeTree']
+ }
+ }
+}
+
+export type RouterProps<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TDehydrated extends Record = Record,
+> = Omit<
+ RouterOptions<
+ TRouter['routeTree'],
+ NonNullable,
+ false,
+ TRouter['history'],
+ TDehydrated
+ >,
+ 'context'
+> & {
+ router: TRouter
+ context?: Partial<
+ RouterOptions<
+ TRouter['routeTree'],
+ NonNullable,
+ false,
+ TRouter['history'],
+ TDehydrated
+ >['context']
+ >
+}
diff --git a/packages/vue-router/src/SafeFragment.tsx b/packages/vue-router/src/SafeFragment.tsx
new file mode 100644
index 00000000000..a045c467a37
--- /dev/null
+++ b/packages/vue-router/src/SafeFragment.tsx
@@ -0,0 +1,10 @@
+import * as Vue from 'vue'
+
+export const SafeFragment = Vue.defineComponent({
+ name: 'SafeFragment',
+ setup(_, { slots }) {
+ return () => {
+ return Vue.h(Vue.Fragment, null, slots.default?.())
+ }
+ },
+})
diff --git a/packages/vue-router/src/ScriptOnce.tsx b/packages/vue-router/src/ScriptOnce.tsx
new file mode 100644
index 00000000000..e85cb9c060f
--- /dev/null
+++ b/packages/vue-router/src/ScriptOnce.tsx
@@ -0,0 +1,30 @@
+import jsesc from 'jsesc'
+
+export function ScriptOnce({
+ children,
+ log,
+}: {
+ children: string
+ log?: boolean
+ sync?: boolean
+}) {
+ if (typeof document !== 'undefined') {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/vue-router/src/Scripts.tsx b/packages/vue-router/src/Scripts.tsx
new file mode 100644
index 00000000000..054e2a4ede8
--- /dev/null
+++ b/packages/vue-router/src/Scripts.tsx
@@ -0,0 +1,65 @@
+import { Asset } from './Asset'
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import type { RouterManagedTag } from '@tanstack/router-core'
+
+export const Scripts = () => {
+ const router = useRouter()
+
+ const assetScripts = useRouterState({
+ select: (state) => {
+ const assetScripts: Array = []
+ const manifest = router.ssr?.manifest
+
+ if (!manifest) {
+ return []
+ }
+
+ state.matches
+ .map((match) => router.looseRoutesById[match.routeId]!)
+ .forEach((route) =>
+ manifest.routes[route.id]?.assets
+ ?.filter((d) => d.tag === 'script')
+ .forEach((asset) => {
+ assetScripts.push({
+ tag: 'script',
+ attrs: asset.attrs,
+ children: asset.children,
+ } as any)
+ }),
+ )
+
+ return assetScripts
+ },
+ })
+
+ const scripts = useRouterState({
+ select: (state) => ({
+ scripts: (
+ state.matches
+ .map((match) => match.scripts!)
+ .flat(1)
+ .filter(Boolean) as Array
+ ).map(({ children, ...script }) => ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ },
+ children,
+ })),
+ }),
+ })
+
+ const allScripts = [
+ ...scripts.value.scripts,
+ ...assetScripts.value,
+ ] as Array
+
+ return (
+ <>
+ {allScripts.map((asset, i) => (
+
+ ))}
+ >
+ )
+}
diff --git a/packages/vue-router/src/ScrollRestoration.tsx b/packages/vue-router/src/ScrollRestoration.tsx
new file mode 100644
index 00000000000..71d472723de
--- /dev/null
+++ b/packages/vue-router/src/ScrollRestoration.tsx
@@ -0,0 +1,69 @@
+import {
+ defaultGetScrollRestorationKey,
+ getCssSelector,
+ scrollRestorationCache,
+ setupScrollRestoration,
+} from '@tanstack/router-core'
+import { useRouter } from './useRouter'
+import type {
+ ParsedLocation,
+ ScrollRestorationEntry,
+ ScrollRestorationOptions,
+} from '@tanstack/router-core'
+
+function useScrollRestoration() {
+ const router = useRouter()
+ setupScrollRestoration(router, true)
+}
+
+/**
+ * @deprecated use createRouter's `scrollRestoration` option instead
+ */
+export function ScrollRestoration(_props: ScrollRestorationOptions) {
+ useScrollRestoration()
+
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(
+ "The ScrollRestoration component is deprecated. Use createRouter's `scrollRestoration` option instead.",
+ )
+ }
+
+ return null
+}
+
+export function useElementScrollRestoration(
+ options: (
+ | {
+ id: string
+ getElement?: () => Window | Element | undefined | null
+ }
+ | {
+ id?: string
+ getElement: () => Window | Element | undefined | null
+ }
+ ) & {
+ getKey?: (location: ParsedLocation) => string
+ },
+): ScrollRestorationEntry | undefined {
+ useScrollRestoration()
+
+ const router = useRouter()
+ const getKey = options.getKey || defaultGetScrollRestorationKey
+
+ let elementSelector = ''
+
+ if (options.id) {
+ elementSelector = `[data-scroll-restoration-id="${options.id}"]`
+ } else {
+ const element = options.getElement?.()
+ if (!element) {
+ return
+ }
+ elementSelector =
+ element instanceof Window ? 'window' : getCssSelector(element)
+ }
+
+ const restoreKey = getKey(router.latestLocation)
+ const byKey = scrollRestorationCache?.state[restoreKey]
+ return byKey?.[elementSelector]
+}
diff --git a/packages/vue-router/src/Transitioner.tsx b/packages/vue-router/src/Transitioner.tsx
new file mode 100644
index 00000000000..8e6a67a6361
--- /dev/null
+++ b/packages/vue-router/src/Transitioner.tsx
@@ -0,0 +1,213 @@
+import * as Vue from 'vue'
+import {
+ getLocationChangeInfo,
+ handleHashScroll,
+ trimPathRight,
+} from '@tanstack/router-core'
+import { useRouter } from './useRouter'
+import { useRouterState } from './useRouterState'
+import { usePrevious } from './utils'
+
+export const Transitioner = Vue.defineComponent({
+ name: 'Transitioner',
+ setup() {
+ const router = useRouter()
+ let mountLoadForRouter = { router, mounted: false }
+
+ if (router.isServer) {
+ return () => null
+ }
+
+ const isLoading = useRouterState({
+ select: ({ isLoading }) => isLoading,
+ })
+
+ // Track if we're in a transition - using a ref to track async transitions
+ const isTransitioning = Vue.ref(false)
+
+ // Track pending state changes
+ const hasPendingMatches = useRouterState({
+ select: (s) => s.matches.some((d) => d.status === 'pending'),
+ })
+
+ const previousIsLoading = usePrevious(() => isLoading.value)
+
+ const isAnyPending = Vue.computed(
+ () => isLoading.value || isTransitioning.value || hasPendingMatches.value,
+ )
+ const previousIsAnyPending = usePrevious(() => isAnyPending.value)
+
+ const isPagePending = Vue.computed(
+ () => isLoading.value || hasPendingMatches.value,
+ )
+ const previousIsPagePending = usePrevious(() => isPagePending.value)
+
+ // Implement startTransition similar to React/Solid
+ // Vue doesn't have a native useTransition like React 18, so we simulate it
+ // We also update the router state's isTransitioning flag so useMatch can check it
+ router.startTransition = (fn: () => void | Promise) => {
+ isTransitioning.value = true
+ // Also update the router state so useMatch knows we're transitioning
+ try {
+ router.__store.setState((s) => ({ ...s, isTransitioning: true }))
+ } catch {
+ // Ignore errors if component is unmounted
+ }
+
+ // Helper to end the transition
+ const endTransition = () => {
+ // Use nextTick to ensure Vue has processed all reactive updates
+ Vue.nextTick(() => {
+ try {
+ isTransitioning.value = false
+ router.__store.setState((s) => ({ ...s, isTransitioning: false }))
+ } catch {
+ // Ignore errors if component is unmounted
+ }
+ })
+ }
+
+ // Execute the function synchronously
+ // The function internally may call startViewTransition which schedules async work
+ // via document.startViewTransition, but we don't need to wait for it here
+ // because Vue's reactivity will trigger re-renders when state changes
+ fn()
+
+ // End the transition on next tick to allow Vue to process reactive updates
+ endTransition()
+ }
+
+ // For Vue, we need to completely override startViewTransition because Vue's
+ // async rendering doesn't work well with the View Transitions API's requirement
+ // for synchronous DOM updates. The browser expects the DOM to be updated
+ // when the callback promise resolves, but Vue updates asynchronously.
+ //
+ // Our approach: Skip the actual view transition animation but still update state.
+ // This ensures navigation works correctly even without the visual transition.
+ // In the future, we could explore using viewTransition.captured like vue-view-transitions does.
+ router.startViewTransition = (fn: () => Promise) => {
+ // Just run the callback directly without wrapping in document.startViewTransition
+ // This ensures the state updates happen and Vue can render them normally
+ fn()
+ }
+
+ // Subscribe to location changes
+ // and try to load the new location
+ let unsubscribe: (() => void) | undefined
+
+ Vue.onMounted(() => {
+ unsubscribe = router.history.subscribe(router.load)
+
+ const nextLocation = router.buildLocation({
+ to: router.latestLocation.pathname,
+ search: true,
+ params: true,
+ hash: true,
+ state: true,
+ _includeValidateSearch: true,
+ })
+
+ if (
+ trimPathRight(router.latestLocation.href) !==
+ trimPathRight(nextLocation.href)
+ ) {
+ router.commitLocation({ ...nextLocation, replace: true })
+ }
+ })
+
+ // Track if component is mounted to prevent updates after unmount
+ const isMounted = Vue.ref(false)
+
+ Vue.onMounted(() => {
+ isMounted.value = true
+ })
+
+ Vue.onUnmounted(() => {
+ isMounted.value = false
+ if (unsubscribe) {
+ unsubscribe()
+ }
+ })
+
+ // Try to load the initial location
+ Vue.onMounted(() => {
+ if (
+ (typeof window !== 'undefined' && router.ssr) ||
+ (mountLoadForRouter.router === router && mountLoadForRouter.mounted)
+ ) {
+ return
+ }
+ mountLoadForRouter = { router, mounted: true }
+ const tryLoad = async () => {
+ try {
+ await router.load()
+ } catch (err) {
+ console.error(err)
+ }
+ }
+ tryLoad()
+ })
+
+ // Setup watchers for emitting events
+ // All watchers check isMounted to prevent updates after unmount
+ Vue.watch(
+ () => isLoading.value,
+ (newValue) => {
+ if (!isMounted.value) return
+ try {
+ if (previousIsLoading.value.previous && !newValue) {
+ router.emit({
+ type: 'onLoad',
+ ...getLocationChangeInfo(router.state),
+ })
+ }
+ } catch {
+ // Ignore errors if component is unmounted
+ }
+ },
+ )
+
+ Vue.watch(isPagePending, (newValue) => {
+ if (!isMounted.value) return
+ try {
+ // emit onBeforeRouteMount
+ if (previousIsPagePending.value.previous && !newValue) {
+ router.emit({
+ type: 'onBeforeRouteMount',
+ ...getLocationChangeInfo(router.state),
+ })
+ }
+ } catch {
+ // Ignore errors if component is unmounted
+ }
+ })
+
+ Vue.watch(isAnyPending, (newValue) => {
+ if (!isMounted.value) return
+ try {
+ // The router was pending and now it's not
+ if (previousIsAnyPending.value.previous && !newValue) {
+ const changeInfo = getLocationChangeInfo(router.state)
+ router.emit({
+ type: 'onResolved',
+ ...changeInfo,
+ })
+
+ router.__store.setState((s) => ({
+ ...s,
+ status: 'idle',
+ resolvedLocation: s.location,
+ }))
+
+ if (changeInfo.hrefChanged) {
+ handleHashScroll(router)
+ }
+ }
+ } catch {
+ // Ignore errors if component is unmounted
+ }
+ })
+
+ return () => null
+ },
+})
diff --git a/packages/vue-router/src/awaited.tsx b/packages/vue-router/src/awaited.tsx
new file mode 100644
index 00000000000..95f02559e37
--- /dev/null
+++ b/packages/vue-router/src/awaited.tsx
@@ -0,0 +1,54 @@
+import * as Vue from 'vue'
+
+import { TSR_DEFERRED_PROMISE, defer } from '@tanstack/router-core'
+import type { DeferredPromise } from '@tanstack/router-core'
+
+export type AwaitOptions = {
+ promise: Promise
+}
+
+export function useAwaited({
+ promise: _promise,
+}: AwaitOptions): [T, DeferredPromise] {
+ const promise = defer(_promise)
+
+ if (promise[TSR_DEFERRED_PROMISE].status === 'pending') {
+ throw promise
+ }
+
+ if (promise[TSR_DEFERRED_PROMISE].status === 'error') {
+ throw promise[TSR_DEFERRED_PROMISE].error
+ }
+
+ return [promise[TSR_DEFERRED_PROMISE].data, promise]
+}
+
+export function Await(
+ props: AwaitOptions & {
+ fallback?: Vue.VNode
+ children: (result: T) => Vue.VNode
+ },
+) {
+ const data = Vue.ref(null)
+ const error = Vue.ref(null)
+ const pending = Vue.ref(true)
+
+ Vue.watchEffect(async () => {
+ pending.value = true
+ try {
+ data.value = await props.promise
+ } catch (err) {
+ error.value = err as Error
+ } finally {
+ pending.value = false
+ }
+ })
+
+ const inner = Vue.computed(() => {
+ if (error.value) throw error.value
+ if (pending.value) return props.fallback
+ return props.children(data.value as T)
+ })
+
+ return () => inner.value
+}
diff --git a/packages/vue-router/src/fileRoute.ts b/packages/vue-router/src/fileRoute.ts
new file mode 100644
index 00000000000..3cce0d4ca3e
--- /dev/null
+++ b/packages/vue-router/src/fileRoute.ts
@@ -0,0 +1,271 @@
+import warning from 'tiny-warning'
+import { createRoute } from './route'
+
+import { useMatch } from './useMatch'
+import { useLoaderDeps } from './useLoaderDeps'
+import { useLoaderData } from './useLoaderData'
+import { useSearch } from './useSearch'
+import { useParams } from './useParams'
+import { useNavigate } from './useNavigate'
+import { useRouter } from './useRouter'
+import type { UseParamsRoute } from './useParams'
+import type { UseMatchRoute } from './useMatch'
+import type { UseSearchRoute } from './useSearch'
+import type {
+ AnyContext,
+ AnyRoute,
+ AnyRouter,
+ Constrain,
+ ConstrainLiteral,
+ FileBaseRouteOptions,
+ FileRoutesByPath,
+ LazyRouteOptions,
+ Register,
+ RegisteredRouter,
+ ResolveParams,
+ Route,
+ RouteById,
+ RouteConstraints,
+ RouteIds,
+ RouteLoaderFn,
+ UpdatableRouteOptions,
+ UseNavigateResult,
+} from '@tanstack/router-core'
+import type { UseLoaderDepsRoute } from './useLoaderDeps'
+import type { UseLoaderDataRoute } from './useLoaderData'
+import type { UseRouteContextRoute } from './useRouteContext'
+
+export function createFileRoute<
+ TFilePath extends keyof FileRoutesByPath,
+ TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
+ TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
+ TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
+ TFullPath extends
+ RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
+>(
+ path?: TFilePath,
+): FileRoute['createRoute'] {
+ if (typeof path === 'object') {
+ return new FileRoute(path, {
+ silent: true,
+ }).createRoute(path) as any
+ }
+ return new FileRoute(path, {
+ silent: true,
+ }).createRoute
+}
+
+/**
+ @deprecated It's no longer recommended to use the `FileRoute` class directly.
+ Instead, use `createFileRoute('/path/to/file')(options)` to create a file route.
+*/
+export class FileRoute<
+ TFilePath extends keyof FileRoutesByPath,
+ TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'],
+ TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'],
+ TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'],
+ TFullPath extends
+ RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'],
+> {
+ silent?: boolean
+
+ constructor(
+ public path?: TFilePath,
+ _opts?: { silent: boolean },
+ ) {
+ this.silent = _opts?.silent
+ }
+
+ createRoute = <
+ TRegister = Register,
+ TSearchValidator = undefined,
+ TParams = ResolveParams,
+ TRouteContextFn = AnyContext,
+ TBeforeLoadFn = AnyContext,
+ TLoaderDeps extends Record = {},
+ TLoaderFn = undefined,
+ TChildren = unknown,
+ TSSR = unknown,
+ TMiddlewares = unknown,
+ THandlers = undefined,
+ >(
+ options?: FileBaseRouteOptions<
+ TRegister,
+ TParentRoute,
+ TId,
+ TPath,
+ TSearchValidator,
+ TParams,
+ TLoaderDeps,
+ TLoaderFn,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ AnyContext,
+ TSSR,
+ TMiddlewares,
+ THandlers
+ > &
+ UpdatableRouteOptions<
+ TParentRoute,
+ TId,
+ TFullPath,
+ TParams,
+ TSearchValidator,
+ TLoaderFn,
+ TLoaderDeps,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn
+ >,
+ ): Route<
+ TRegister,
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TFilePath,
+ TId,
+ TSearchValidator,
+ TParams,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ unknown,
+ TSSR,
+ TMiddlewares,
+ THandlers
+ > => {
+ warning(
+ this.silent,
+ 'FileRoute is deprecated and will be removed in the next major version. Use the createFileRoute(path)(options) function instead.',
+ )
+ const route = createRoute(options as any)
+ ;(route as any).isRoot = false
+ return route as any
+ }
+}
+
+/**
+ @deprecated It's recommended not to split loaders into separate files.
+ Instead, place the loader function in the the main route file, inside the
+ `createFileRoute('/path/to/file)(options)` options.
+*/
+export function FileRouteLoader<
+ TFilePath extends keyof FileRoutesByPath,
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
+>(
+ _path: TFilePath,
+): (
+ loaderFn: Constrain<
+ TLoaderFn,
+ RouteLoaderFn<
+ Register,
+ TRoute['parentRoute'],
+ TRoute['types']['id'],
+ TRoute['types']['params'],
+ TRoute['types']['loaderDeps'],
+ TRoute['types']['routerContext'],
+ TRoute['types']['routeContextFn'],
+ TRoute['types']['beforeLoadFn']
+ >
+ >,
+) => TLoaderFn {
+ warning(
+ false,
+ `FileRouteLoader is deprecated and will be removed in the next major version. Please place the loader function in the the main route file, inside the \`createFileRoute('/path/to/file')(options)\` options`,
+ )
+ return (loaderFn) => loaderFn as any
+}
+
+declare module '@tanstack/router-core' {
+ export interface LazyRoute {
+ useMatch: UseMatchRoute
+ useRouteContext: UseRouteContextRoute
+ useSearch: UseSearchRoute
+ useParams: UseParamsRoute
+ useLoaderDeps: UseLoaderDepsRoute
+ useLoaderData: UseLoaderDataRoute
+ useNavigate: () => UseNavigateResult
+ }
+}
+
+export class LazyRoute {
+ options: {
+ id: string
+ } & LazyRouteOptions
+
+ constructor(
+ opts: {
+ id: string
+ } & LazyRouteOptions,
+ ) {
+ this.options = opts
+ }
+
+ useMatch: UseMatchRoute = (opts) => {
+ return useMatch({
+ select: opts?.select,
+ from: this.options.id,
+ } as any) as any
+ }
+
+ useRouteContext: UseRouteContextRoute = (opts) => {
+ return useMatch({
+ from: this.options.id,
+ select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
+ }) as any
+ }
+
+ useSearch: UseSearchRoute = (opts) => {
+ return useSearch({
+ select: opts?.select,
+ from: this.options.id,
+ } as any) as any
+ }
+
+ useParams: UseParamsRoute = (opts) => {
+ return useParams({
+ select: opts?.select,
+ from: this.options.id,
+ } as any) as any
+ }
+
+ useLoaderDeps: UseLoaderDepsRoute = (opts) => {
+ return useLoaderDeps({ ...opts, from: this.options.id } as any)
+ }
+
+ useLoaderData: UseLoaderDataRoute = (opts) => {
+ return useLoaderData({ ...opts, from: this.options.id } as any)
+ }
+
+ useNavigate = (): UseNavigateResult => {
+ const router = useRouter()
+ return useNavigate({ from: router.routesById[this.options.id].fullPath })
+ }
+}
+
+export function createLazyRoute<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TId extends string = string,
+ TRoute extends AnyRoute = RouteById,
+>(id: ConstrainLiteral>) {
+ return (opts: LazyRouteOptions) => {
+ return new LazyRoute({
+ id: id,
+ ...opts,
+ })
+ }
+}
+export function createLazyFileRoute<
+ TFilePath extends keyof FileRoutesByPath,
+ TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
+>(id: TFilePath): (opts: LazyRouteOptions) => LazyRoute {
+ if (typeof id === 'object') {
+ return new LazyRoute(id) as any
+ }
+
+ return (opts: LazyRouteOptions) => new LazyRoute({ id, ...opts })
+}
diff --git a/packages/vue-router/src/history.ts b/packages/vue-router/src/history.ts
new file mode 100644
index 00000000000..2271af692cd
--- /dev/null
+++ b/packages/vue-router/src/history.ts
@@ -0,0 +1,9 @@
+import type { HistoryLocation } from '@tanstack/history'
+
+declare module '@tanstack/history' {
+ interface HistoryState {
+ __tempLocation?: HistoryLocation
+ __tempKey?: string
+ __hashScrollIntoViewOptions?: boolean | ScrollIntoViewOptions
+ }
+}
diff --git a/packages/vue-router/src/index.tsx b/packages/vue-router/src/index.tsx
new file mode 100644
index 00000000000..28e9f290384
--- /dev/null
+++ b/packages/vue-router/src/index.tsx
@@ -0,0 +1,346 @@
+export {
+ defer,
+ TSR_DEFERRED_PROMISE,
+ isMatch,
+ joinPaths,
+ cleanPath,
+ trimPathLeft,
+ trimPathRight,
+ trimPath,
+ resolvePath,
+ interpolatePath,
+ rootRouteId,
+ defaultSerializeError,
+ defaultParseSearch,
+ defaultStringifySearch,
+ parseSearchWith,
+ stringifySearchWith,
+ functionalUpdate,
+ replaceEqualDeep,
+ isPlainObject,
+ isPlainArray,
+ deepEqual,
+ createControlledPromise,
+ retainSearchParams,
+ stripSearchParams,
+ createSerializationAdapter,
+} from '@tanstack/router-core'
+
+export type {
+ AnyRoute,
+ DeferredPromiseState,
+ DeferredPromise,
+ ParsedLocation,
+ RemoveTrailingSlashes,
+ RemoveLeadingSlashes,
+ ActiveOptions,
+ ResolveRelativePath,
+ RootRouteId,
+ AnyPathParams,
+ ResolveParams,
+ ResolveOptionalParams,
+ ResolveRequiredParams,
+ SearchSchemaInput,
+ AnyContext,
+ RouteContext,
+ PreloadableObj,
+ RoutePathOptions,
+ StaticDataRouteOption,
+ RoutePathOptionsIntersection,
+ UpdatableStaticRouteOption,
+ MetaDescriptor,
+ RouteLinkEntry,
+ ParseParamsFn,
+ SearchFilter,
+ ResolveId,
+ InferFullSearchSchema,
+ InferFullSearchSchemaInput,
+ ErrorRouteProps,
+ ErrorComponentProps,
+ NotFoundRouteProps,
+ TrimPath,
+ TrimPathLeft,
+ TrimPathRight,
+ StringifyParamsFn,
+ ParamsOptions,
+ InferAllParams,
+ InferAllContext,
+ LooseReturnType,
+ LooseAsyncReturnType,
+ ContextReturnType,
+ ContextAsyncReturnType,
+ ResolveLoaderData,
+ ResolveRouteContext,
+ SearchSerializer,
+ SearchParser,
+ TrailingSlashOption,
+ Manifest,
+ RouterManagedTag,
+ ControlledPromise,
+ Constrain,
+ Expand,
+ MergeAll,
+ Assign,
+ IntersectAssign,
+ ResolveValidatorInput,
+ ResolveValidatorOutput,
+ Register,
+ AnyValidator,
+ DefaultValidator,
+ ValidatorFn,
+ AnySchema,
+ AnyValidatorAdapter,
+ AnyValidatorFn,
+ AnyValidatorObj,
+ ResolveValidatorInputFn,
+ ResolveValidatorOutputFn,
+ ResolveSearchValidatorInput,
+ ResolveSearchValidatorInputFn,
+ Validator,
+ ValidatorAdapter,
+ ValidatorObj,
+ FileRoutesByPath,
+ RouteById,
+ RootRouteOptions,
+ CreateFileRoute,
+ SerializationAdapter,
+ AnySerializationAdapter,
+} from '@tanstack/router-core'
+
+export {
+ createHistory,
+ createBrowserHistory,
+ createHashHistory,
+ createMemoryHistory,
+} from '@tanstack/history'
+
+export type {
+ BlockerFn,
+ HistoryLocation,
+ RouterHistory,
+ ParsedPath,
+ HistoryState,
+} from '@tanstack/history'
+
+export { useAwaited, Await } from './awaited'
+export type { AwaitOptions } from './awaited'
+
+export { CatchBoundary, ErrorComponent } from './CatchBoundary'
+
+export {
+ FileRoute,
+ createFileRoute,
+ FileRouteLoader,
+ LazyRoute,
+ createLazyRoute,
+ createLazyFileRoute,
+} from './fileRoute'
+
+export * from './history'
+
+export { lazyRouteComponent } from './lazyRouteComponent'
+
+export { useLinkProps, createLink, Link, linkOptions } from './link'
+export type {
+ InferDescendantToPaths,
+ RelativeToPath,
+ RelativeToParentPath,
+ RelativeToCurrentPath,
+ AbsoluteToPath,
+ RelativeToPathAutoComplete,
+ NavigateOptions,
+ ToOptions,
+ ToMaskOptions,
+ ToSubOptions,
+ ResolveRoute,
+ SearchParamOptions,
+ PathParamOptions,
+ ToPathOption,
+ LinkOptions,
+ MakeOptionalPathParams,
+ FileRouteTypes,
+ RouteContextParameter,
+ BeforeLoadContextParameter,
+ ResolveAllContext,
+ ResolveAllParamsFromParent,
+ ResolveFullSearchSchema,
+ ResolveFullSearchSchemaInput,
+ RouteIds,
+ NavigateFn,
+ BuildLocationFn,
+ FullSearchSchemaOption,
+ MakeRemountDepsOptionsUnion,
+ RemountDepsOptions,
+ ResolveFullPath,
+ AnyRouteWithContext,
+ AnyRouterWithContext,
+ CommitLocationOptions,
+ MatchLocation,
+ UseNavigateResult,
+ AnyRedirect,
+ Redirect,
+ RedirectOptions,
+ ResolvedRedirect,
+ MakeRouteMatch,
+ MakeRouteMatchUnion,
+ RouteMatch,
+ AnyRouteMatch,
+ RouteContextFn,
+ RouteContextOptions,
+ BeforeLoadContextOptions,
+ ContextOptions,
+ RouteOptions,
+ FileBaseRouteOptions,
+ BaseRouteOptions,
+ UpdatableRouteOptions,
+ RouteLoaderFn,
+ LoaderFnContext,
+ LazyRouteOptions,
+ AnyRouter,
+ RegisteredRouter,
+ RouterContextOptions,
+ ControllablePromise,
+ InjectedHtmlEntry,
+ RouterOptions,
+ RouterState,
+ ListenerFn,
+ BuildNextOptions,
+ RouterConstructorOptions,
+ RouterEvents,
+ RouterEvent,
+ RouterListener,
+ RouteConstraints,
+ RouteMask,
+ MatchRouteOptions,
+ CreateLazyFileRoute,
+} from '@tanstack/router-core'
+export type {
+ UseLinkPropsOptions,
+ ActiveLinkOptions,
+ LinkProps,
+ LinkComponent,
+ LinkComponentProps,
+ CreateLinkProps,
+} from './link'
+
+export {
+ Matches,
+ useMatchRoute,
+ MatchRoute,
+ useMatches,
+ useParentMatches,
+ useChildMatches,
+} from './Matches'
+
+export type { UseMatchRouteOptions, MakeMatchRouteOptions } from './Matches'
+
+export { matchContext } from './matchContext'
+export { Match, Outlet } from './Match'
+
+export { useMatch } from './useMatch'
+export { useLoaderDeps } from './useLoaderDeps'
+export { useLoaderData } from './useLoaderData'
+
+export { redirect, isRedirect, createRouterConfig } from '@tanstack/router-core'
+
+export {
+ RouteApi,
+ getRouteApi,
+ Route,
+ createRoute,
+ RootRoute,
+ rootRouteWithContext,
+ createRootRoute,
+ createRootRouteWithContext,
+ createRouteMask,
+ NotFoundRoute,
+} from './route'
+export type {
+ AnyRootRoute,
+ VueNode,
+ SyncRouteComponent,
+ AsyncRouteComponent,
+ RouteComponent,
+ ErrorRouteComponent,
+ NotFoundRouteComponent,
+} from './route'
+
+export { createRouter, Router } from './router'
+
+export {
+ componentTypes,
+ lazyFn,
+ SearchParamError,
+ PathParamError,
+ getInitialRouterState,
+} from '@tanstack/router-core'
+
+export { RouterProvider, RouterContextProvider } from './RouterProvider'
+export type { RouterProps } from './RouterProvider'
+
+export {
+ useElementScrollRestoration,
+ ScrollRestoration,
+} from './ScrollRestoration'
+
+export type { UseBlockerOpts, ShouldBlockFn } from './useBlocker'
+export { useBlocker, Block } from './useBlocker'
+
+export { useNavigate, Navigate } from './useNavigate'
+
+export { useParams } from './useParams'
+export { useSearch } from './useSearch'
+
+export {
+ getRouterContext, // SSR
+} from './routerContext'
+
+export { useRouteContext } from './useRouteContext'
+export { useRouter } from './useRouter'
+export { useRouterState } from './useRouterState'
+export { useLocation } from './useLocation'
+export { useCanGoBack } from './useCanGoBack'
+
+export { useLayoutEffect } from './utils'
+
+export { CatchNotFound, DefaultGlobalNotFound } from './not-found'
+export { notFound, isNotFound } from '@tanstack/router-core'
+export type { NotFoundError } from '@tanstack/router-core'
+
+export type {
+ ValidateLinkOptions,
+ ValidateUseSearchOptions,
+ ValidateUseParamsOptions,
+ ValidateLinkOptionsArray,
+} from './typePrimitives'
+
+export type {
+ ValidateFromPath,
+ ValidateToPath,
+ ValidateSearch,
+ ValidateParams,
+ InferFrom,
+ InferTo,
+ InferMaskTo,
+ InferMaskFrom,
+ ValidateNavigateOptions,
+ ValidateNavigateOptionsArray,
+ ValidateRedirectOptions,
+ ValidateRedirectOptionsArray,
+ ValidateId,
+ InferStrict,
+ InferShouldThrow,
+ InferSelected,
+ ValidateUseSearchResult,
+ ValidateUseParamsResult,
+} from '@tanstack/router-core'
+
+export { ScriptOnce } from './ScriptOnce'
+export { Asset } from './Asset'
+export { HeadContent } from './HeadContent'
+export { Scripts } from './Scripts'
+export { composeRewrites } from '@tanstack/router-core'
+export type {
+ LocationRewrite,
+ LocationRewriteFunction,
+} from '@tanstack/router-core'
diff --git a/packages/vue-router/src/lazyRouteComponent.tsx b/packages/vue-router/src/lazyRouteComponent.tsx
new file mode 100644
index 00000000000..061892bc374
--- /dev/null
+++ b/packages/vue-router/src/lazyRouteComponent.tsx
@@ -0,0 +1,173 @@
+import * as Vue from 'vue'
+import { Outlet } from './Match'
+import type { AsyncRouteComponent } from './route'
+
+// If the load fails due to module not found, it may mean a new version of
+// the build was deployed and the user's browser is still using an old version.
+// If this happens, the old version in the user's browser would have an outdated
+// URL to the lazy module.
+// In that case, we want to attempt one window refresh to get the latest.
+function isModuleNotFoundError(error: any): boolean {
+ // chrome: "Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
+ // firefox: "error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
+ // safari: "Importing a module script failed."
+ if (typeof error?.message !== 'string') return false
+ return (
+ error.message.startsWith('Failed to fetch dynamically imported module') ||
+ error.message.startsWith('error loading dynamically imported module') ||
+ error.message.startsWith('Importing a module script failed')
+ )
+}
+
+export function ClientOnly(props: { children?: any; fallback?: Vue.VNode }) {
+ const hydrated = useHydrated()
+
+ return () => {
+ if (hydrated.value) {
+ return props.children
+ }
+ return props.fallback || null
+ }
+}
+
+export function useHydrated() {
+ // Only hydrate on client-side, never on server
+ const hydrated = Vue.ref(false)
+
+ // If on server, return false
+ if (typeof window === 'undefined') {
+ return Vue.computed(() => false)
+ }
+
+ // On client, set to true once mounted
+ Vue.onMounted(() => {
+ hydrated.value = true
+ })
+
+ return hydrated
+}
+
+export function lazyRouteComponent<
+ T extends Record,
+ TKey extends keyof T = 'default',
+>(
+ importer: () => Promise,
+ exportName?: TKey,
+ ssr?: () => boolean,
+): T[TKey] extends (props: infer TProps) => any
+ ? AsyncRouteComponent
+ : never {
+ let loadPromise: Promise | undefined
+ let comp: T[TKey] | T['default'] | null = null
+ let error: any = null
+ let attemptedReload = false
+
+ const load = () => {
+ // If we're on the server and SSR is disabled for this component
+ if (typeof document === 'undefined' && ssr?.() === false) {
+ comp = (() => null) as any
+ return Promise.resolve(comp)
+ }
+
+ // Use existing promise or create new one
+ if (!loadPromise) {
+ loadPromise = importer()
+ .then((res) => {
+ loadPromise = undefined
+ comp = res[exportName ?? 'default']
+ return comp
+ })
+ .catch((err) => {
+ error = err
+ loadPromise = undefined
+
+ // If it's a module not found error, we'll try to handle it in the component
+ if (isModuleNotFoundError(error)) {
+ return null
+ }
+
+ throw err
+ })
+ }
+
+ return loadPromise
+ }
+
+ // Create a lazy component wrapper using defineComponent so it works in Vue SFC templates
+ const lazyComp = Vue.defineComponent({
+ name: 'LazyRouteComponent',
+ setup(props: any) {
+ // Create refs to track component state
+ const component = Vue.ref(comp)
+ const errorState = Vue.ref(error)
+ const loading = Vue.ref(!component.value && !errorState.value)
+
+ // Setup effect to load the component when this component is used
+ Vue.onMounted(() => {
+ if (!component.value && !errorState.value) {
+ loading.value = true
+
+ load()
+ .then((result) => {
+ component.value = result
+ loading.value = false
+ })
+ .catch((err) => {
+ errorState.value = err
+ loading.value = false
+ })
+ }
+ })
+
+ // Handle module not found error with reload attempt
+ if (
+ errorState.value &&
+ isModuleNotFoundError(errorState.value) &&
+ !attemptedReload
+ ) {
+ if (
+ typeof window !== 'undefined' &&
+ typeof sessionStorage !== 'undefined'
+ ) {
+ // Try to reload once on module not found error
+ const storageKey = `tanstack_router_reload:${errorState.value.message}`
+ if (!sessionStorage.getItem(storageKey)) {
+ sessionStorage.setItem(storageKey, '1')
+ attemptedReload = true
+ window.location.reload()
+ return () => null // Return empty while reloading
+ }
+ }
+ }
+
+ // If we have a non-module-not-found error, throw it
+ if (errorState.value && !isModuleNotFoundError(errorState.value)) {
+ throw errorState.value
+ }
+
+ // Return a render function
+ return () => {
+ // If we're still loading or don't have a component yet, use a suspense pattern
+ if (loading.value || !component.value) {
+ return Vue.h('div', null) // Empty div while loading
+ }
+
+ // If SSR is disabled for this component
+ if (ssr?.() === false) {
+ return Vue.h(ClientOnly, {
+ fallback: Vue.h(Outlet),
+ children: Vue.h(component.value, props),
+ })
+ }
+
+ // Regular render with the loaded component
+ return Vue.h(component.value, props)
+ }
+ },
+ })
+
+ // Add preload method
+ ;(lazyComp as any).preload = load
+
+ return lazyComp as any
+}
diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx
new file mode 100644
index 00000000000..85a668ad55f
--- /dev/null
+++ b/packages/vue-router/src/link.tsx
@@ -0,0 +1,765 @@
+import * as Vue from 'vue'
+import {
+ deepEqual,
+ exactPathTest,
+ preloadWarning,
+ removeTrailingSlash,
+} from '@tanstack/router-core'
+
+import { useRouterState } from './useRouterState'
+import { useRouter } from './useRouter'
+import { useIntersectionObserver } from './utils'
+import { useMatches } from './Matches'
+
+import type {
+ AnyRouter,
+ Constrain,
+ LinkCurrentTargetElement,
+ LinkOptions,
+ RegisteredRouter,
+ RoutePaths,
+} from '@tanstack/router-core'
+import type {
+ ValidateLinkOptions,
+ ValidateLinkOptionsArray,
+} from './typePrimitives'
+
+// Type definitions to replace missing Vue JSX types
+type EventHandler = (e: TEvent) => void
+interface HTMLAttributes {
+ class?: string
+ style?: Record
+ onClick?: EventHandler
+ onFocus?: EventHandler
+ // Vue 3's h() function expects lowercase event names after 'on' prefix
+ onMouseenter?: EventHandler
+ onMouseleave?: EventHandler
+ onMouseover?: EventHandler
+ onMouseout?: EventHandler
+ onTouchstart?: EventHandler
+ // Also accept the camelCase versions for external API compatibility
+ onMouseEnter?: EventHandler
+ onMouseLeave?: EventHandler
+ onMouseOver?: EventHandler
+ onMouseOut?: EventHandler
+ onTouchStart?: EventHandler
+ [key: string]: any
+}
+
+interface StyledProps {
+ class?: string
+ style?: Record
+ [key: string]: any
+}
+
+export function useLinkProps<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends RoutePaths | string = string,
+ TTo extends string = '',
+ TMaskFrom extends RoutePaths | string = TFrom,
+ TMaskTo extends string = '',
+>(
+ options: UseLinkPropsOptions,
+): HTMLAttributes {
+ const router = useRouter()
+ const isTransitioning = Vue.ref(false)
+ let hasRenderFetched = false
+
+ // Ensure router is defined before proceeding
+ if (!router) {
+ console.warn('useRouter must be used inside a component!')
+ return {}
+ }
+
+ // Determine if the link is external or internal
+ const type = Vue.computed(() => {
+ try {
+ new URL(`${options.to}`)
+ return 'external'
+ } catch {
+ return 'internal'
+ }
+ })
+
+ const currentSearch = useRouterState({
+ select: (s) => s.location.searchStr,
+ })
+
+ // when `from` is not supplied, use the leaf route of the current matches as the `from` location
+ const from = useMatches({
+ select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
+ })
+
+ const _options = Vue.computed(() => ({
+ ...options,
+ from: from.value,
+ }))
+
+ const next = Vue.computed(() => {
+ // Depend on search to rebuild when search changes
+ currentSearch.value
+ return router.buildLocation(_options.value as any)
+ })
+
+ const preload = Vue.computed(() => {
+ if (_options.value.reloadDocument) {
+ return false
+ }
+ return options.preload ?? router.options.defaultPreload
+ })
+
+ const preloadDelay = Vue.computed(
+ () => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0,
+ )
+
+ const isActive = useRouterState({
+ select: (s) => {
+ const activeOptions = options.activeOptions
+ if (activeOptions?.exact) {
+ const testExact = exactPathTest(
+ s.location.pathname,
+ next.value.pathname,
+ router.basepath,
+ )
+ if (!testExact) {
+ return false
+ }
+ } else {
+ const currentPathSplit = removeTrailingSlash(
+ s.location.pathname,
+ router.basepath,
+ ).split('/')
+ const nextPathSplit = removeTrailingSlash(
+ next.value?.pathname,
+ router.basepath,
+ )?.split('/')
+
+ const pathIsFuzzyEqual = nextPathSplit?.every(
+ (d, i) => d === currentPathSplit[i],
+ )
+ if (!pathIsFuzzyEqual) {
+ return false
+ }
+ }
+
+ if (activeOptions?.includeSearch ?? true) {
+ const searchTest = deepEqual(s.location.search, next.value.search, {
+ partial: !activeOptions?.exact,
+ ignoreUndefined: !activeOptions?.explicitUndefined,
+ })
+ if (!searchTest) {
+ return false
+ }
+ }
+
+ if (activeOptions?.includeHash) {
+ return s.location.hash === next.value.hash
+ }
+ return true
+ },
+ })
+
+ const doPreload = () =>
+ router.preloadRoute(_options.value as any).catch((err: any) => {
+ console.warn(err)
+ console.warn(preloadWarning)
+ })
+
+ const preloadViewportIoCallback = (
+ entry: IntersectionObserverEntry | undefined,
+ ) => {
+ if (entry?.isIntersecting) {
+ doPreload()
+ }
+ }
+
+ const ref = Vue.ref(null)
+
+ useIntersectionObserver(
+ ref,
+ preloadViewportIoCallback,
+ { rootMargin: '100px' },
+ { disabled: () => !!options.disabled || !(preload.value === 'viewport') },
+ )
+
+ Vue.effect(() => {
+ if (hasRenderFetched) {
+ return
+ }
+ if (!options.disabled && preload.value === 'render') {
+ doPreload()
+ hasRenderFetched = true
+ }
+ })
+
+ // Create safe props that can be spread
+ const getPropsSafeToSpread = () => {
+ const result: Record = {}
+ for (const key in options) {
+ if (
+ ![
+ 'activeProps',
+ 'inactiveProps',
+ 'activeOptions',
+ 'to',
+ 'preload',
+ 'preloadDelay',
+ 'hashScrollIntoView',
+ 'replace',
+ 'startTransition',
+ 'resetScroll',
+ 'viewTransition',
+ 'children',
+ 'target',
+ 'disabled',
+ 'style',
+ 'class',
+ 'onClick',
+ 'onFocus',
+ 'onMouseEnter',
+ 'onMouseLeave',
+ 'onMouseOver',
+ 'onMouseOut',
+ 'onTouchStart',
+ 'ignoreBlocker',
+ 'params',
+ 'search',
+ 'hash',
+ 'state',
+ 'mask',
+ 'reloadDocument',
+ '_asChild',
+ 'from',
+ 'additionalProps',
+ ].includes(key)
+ ) {
+ result[key] = options[key]
+ }
+ }
+ return result
+ }
+
+ if (type.value === 'external') {
+ // External links just have simple props
+ const externalProps: HTMLAttributes = {
+ ...getPropsSafeToSpread(),
+ ref,
+ href: options.to,
+ target: options.target,
+ disabled: options.disabled,
+ style: options.style,
+ class: options.class,
+ onClick: options.onClick,
+ onFocus: options.onFocus,
+ onMouseEnter: options.onMouseEnter,
+ onMouseLeave: options.onMouseLeave,
+ onMouseOver: options.onMouseOver,
+ onMouseOut: options.onMouseOut,
+ onTouchStart: options.onTouchStart,
+ }
+
+ // Remove undefined values
+ Object.keys(externalProps).forEach((key) => {
+ if (externalProps[key] === undefined) {
+ delete externalProps[key]
+ }
+ })
+
+ return externalProps
+ }
+
+ // The click handler
+ const handleClick = (e: MouseEvent): void => {
+ // Check actual element's target attribute as fallback
+ const elementTarget = (
+ e.currentTarget as HTMLAnchorElement | SVGAElement
+ )?.getAttribute('target')
+ const effectiveTarget =
+ options.target !== undefined ? options.target : elementTarget
+
+ if (
+ !options.disabled &&
+ !isCtrlEvent(e) &&
+ !e.defaultPrevented &&
+ (!effectiveTarget || effectiveTarget === '_self') &&
+ e.button === 0
+ ) {
+ // Don't prevent default or handle navigation if reloadDocument is true
+ if (_options.value.reloadDocument) {
+ return
+ }
+
+ e.preventDefault()
+
+ isTransitioning.value = true
+
+ const unsub = router.subscribe('onResolved', () => {
+ unsub()
+ isTransitioning.value = false
+ })
+
+ // All is well? Navigate!
+ router.navigate({
+ ..._options.value,
+ replace: options.replace,
+ resetScroll: options.resetScroll,
+ hashScrollIntoView: options.hashScrollIntoView,
+ startTransition: options.startTransition,
+ viewTransition: options.viewTransition,
+ ignoreBlocker: options.ignoreBlocker,
+ } as any)
+ }
+ }
+
+ // The focus handler
+ const handleFocus = (_: FocusEvent) => {
+ if (options.disabled) return
+ if (preload.value) {
+ doPreload()
+ }
+ }
+
+ const handleTouchStart = (_: TouchEvent) => {
+ if (options.disabled) return
+ if (preload.value) {
+ doPreload()
+ }
+ }
+
+ const handleEnter = (e: MouseEvent) => {
+ if (options.disabled) return
+ // Use currentTarget (the element with the handler) instead of target (which may be a child)
+ const eventTarget = (e.currentTarget ||
+ e.target ||
+ {}) as LinkCurrentTargetElement
+
+ if (preload.value) {
+ if (eventTarget.preloadTimeout) {
+ return
+ }
+
+ eventTarget.preloadTimeout = setTimeout(() => {
+ eventTarget.preloadTimeout = null
+ doPreload()
+ }, preloadDelay.value)
+ }
+ }
+
+ const handleLeave = (e: MouseEvent) => {
+ if (options.disabled) return
+ // Use currentTarget (the element with the handler) instead of target (which may be a child)
+ const eventTarget = (e.currentTarget ||
+ e.target ||
+ {}) as LinkCurrentTargetElement
+
+ if (eventTarget.preloadTimeout) {
+ clearTimeout(eventTarget.preloadTimeout)
+ eventTarget.preloadTimeout = null
+ }
+ }
+
+ // Helper to compose event handlers - with explicit return type and better type handling
+ function composeEventHandlers(
+ handlers: Array | undefined>,
+ ): (e: T) => void {
+ return (event: T) => {
+ for (const handler of handlers) {
+ if (handler) {
+ handler(event)
+ }
+ }
+ }
+ }
+
+ // Get the active and inactive props
+ const resolvedActiveProps = Vue.computed(() => {
+ const activeProps = options.activeProps || (() => ({ class: 'active' }))
+ const props = isActive.value
+ ? typeof activeProps === 'function'
+ ? activeProps()
+ : activeProps
+ : {}
+
+ return props || { class: undefined, style: undefined }
+ })
+
+ const resolvedInactiveProps = Vue.computed(() => {
+ const inactiveProps = options.inactiveProps || (() => ({}))
+ const props = isActive.value
+ ? {}
+ : typeof inactiveProps === 'function'
+ ? inactiveProps()
+ : inactiveProps
+
+ return props || { class: undefined, style: undefined }
+ })
+
+ const resolvedClassName = Vue.computed(() => {
+ const classes = [
+ options.class,
+ resolvedActiveProps.value?.class,
+ resolvedInactiveProps.value?.class,
+ ].filter(Boolean)
+ return classes.length ? classes.join(' ') : undefined
+ })
+
+ const resolvedStyle = Vue.computed(() => {
+ const result: Record = {}
+
+ // Merge styles from all sources
+ if (options.style) {
+ Object.assign(result, options.style)
+ }
+
+ if (resolvedActiveProps.value?.style) {
+ Object.assign(result, resolvedActiveProps.value.style)
+ }
+
+ if (resolvedInactiveProps.value?.style) {
+ Object.assign(result, resolvedInactiveProps.value.style)
+ }
+
+ return Object.keys(result).length > 0 ? result : undefined
+ })
+
+ const href = Vue.computed(() => {
+ if (options.disabled) {
+ return undefined
+ }
+ const nextLocation = next.value
+ const maskedLocation = nextLocation?.maskedLocation
+
+ let hrefValue: string
+ if (maskedLocation) {
+ hrefValue = maskedLocation.url
+ } else {
+ hrefValue = nextLocation?.url
+ }
+
+ // Handle origin stripping like Solid does
+ if (router.origin && hrefValue?.startsWith(router.origin)) {
+ hrefValue = router.history.createHref(
+ hrefValue.replace(router.origin, ''),
+ )
+ }
+
+ return hrefValue
+ })
+
+ // Create a reactive proxy that reads computed values on access
+ // This allows the returned object to stay reactive when used in templates
+ // Use shallowReactive to preserve the ref object without unwrapping it
+ const reactiveProps: HTMLAttributes = Vue.shallowReactive({
+ ...getPropsSafeToSpread(),
+ href: undefined as string | undefined,
+ ref,
+ onClick: composeEventHandlers([
+ options.onClick,
+ handleClick,
+ ]) as any,
+ onFocus: composeEventHandlers([
+ options.onFocus,
+ handleFocus,
+ ]) as any,
+ onMouseenter: composeEventHandlers([
+ options.onMouseEnter,
+ handleEnter,
+ ]) as any,
+ onMouseover: composeEventHandlers([
+ options.onMouseOver,
+ handleEnter,
+ ]) as any,
+ onMouseleave: composeEventHandlers([
+ options.onMouseLeave,
+ handleLeave,
+ ]) as any,
+ onMouseout: composeEventHandlers([
+ options.onMouseOut,
+ handleLeave,
+ ]) as any,
+ onTouchstart: composeEventHandlers([
+ options.onTouchStart,
+ handleTouchStart,
+ ]) as any,
+ disabled: !!options.disabled,
+ target: options.target,
+ })
+
+ // Watch computed values and update reactive props
+ Vue.watchEffect(() => {
+ // Update from resolved active/inactive props
+ const activeP = resolvedActiveProps.value
+ const inactiveP = resolvedInactiveProps.value
+
+ // Update href
+ reactiveProps.href = href.value
+
+ // Update style
+ if (resolvedStyle.value) {
+ reactiveProps.style = resolvedStyle.value
+ } else {
+ delete reactiveProps.style
+ }
+
+ // Update class
+ if (resolvedClassName.value) {
+ reactiveProps.class = resolvedClassName.value
+ } else {
+ delete reactiveProps.class
+ }
+
+ // Update disabled props
+ if (options.disabled) {
+ reactiveProps.role = 'link'
+ reactiveProps['aria-disabled'] = true
+ } else {
+ delete reactiveProps.role
+ delete reactiveProps['aria-disabled']
+ }
+
+ // Update active status
+ if (isActive.value) {
+ reactiveProps['data-status'] = 'active'
+ reactiveProps['aria-current'] = 'page'
+ } else {
+ delete reactiveProps['data-status']
+ delete reactiveProps['aria-current']
+ }
+
+ // Update transitioning status
+ if (isTransitioning.value) {
+ reactiveProps['data-transitioning'] = 'transitioning'
+ } else {
+ delete reactiveProps['data-transitioning']
+ }
+
+ // Merge active/inactive props (excluding class and style which are handled above)
+ for (const key of Object.keys(activeP)) {
+ if (key !== 'class' && key !== 'style') {
+ reactiveProps[key] = activeP[key]
+ }
+ }
+ for (const key of Object.keys(inactiveP)) {
+ if (key !== 'class' && key !== 'style') {
+ reactiveProps[key] = inactiveP[key]
+ }
+ }
+ })
+
+ return reactiveProps
+}
+
+// Type definitions
+export type UseLinkPropsOptions<
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends RoutePaths | string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends RoutePaths | string = TFrom,
+ TMaskTo extends string = '.',
+> = ActiveLinkOptions<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
+ HTMLAttributes
+
+export type ActiveLinkOptions<
+ TComp = 'a',
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '.',
+> = LinkOptions &
+ ActiveLinkOptionProps
+
+type ActiveLinkProps = Partial<
+ HTMLAttributes & {
+ [key: `data-${string}`]: unknown
+ }
+>
+
+export interface ActiveLinkOptionProps {
+ /**
+ * A function that returns additional props for the `active` state of this link.
+ * These props override other props passed to the link (`style`'s are merged, `class`'s are concatenated)
+ */
+ activeProps?: ActiveLinkProps | (() => ActiveLinkProps)
+ /**
+ * A function that returns additional props for the `inactive` state of this link.
+ * These props override other props passed to the link (`style`'s are merged, `class`'s are concatenated)
+ */
+ inactiveProps?: ActiveLinkProps | (() => ActiveLinkProps)
+}
+
+export type LinkProps<
+ TComp = 'a',
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '.',
+> = ActiveLinkOptions &
+ LinkPropsChildren
+
+export interface LinkPropsChildren {
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
+ children?:
+ | Vue.VNode
+ | ((state: { isActive: boolean; isTransitioning: boolean }) => Vue.VNode)
+}
+
+type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap
+ ? Omit
+ : TComp extends Vue.Component
+ ? Record
+ : Record
+
+export type LinkComponentProps<
+ TComp = 'a',
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends string = TFrom,
+ TMaskTo extends string = '.',
+> = LinkComponentVueProps &
+ LinkProps
+
+export type CreateLinkProps = LinkProps<
+ any,
+ any,
+ string,
+ string,
+ string,
+ string
+>
+
+export type LinkComponent = <
+ TRouter extends AnyRouter = RegisteredRouter,
+ const TFrom extends string = string,
+ const TTo extends string | undefined = undefined,
+ const TMaskFrom extends string = TFrom,
+ const TMaskTo extends string = '',
+>(
+ props: LinkComponentProps,
+) => Vue.VNode
+
+export function createLink(
+ Comp: Constrain Vue.VNode>,
+): LinkComponent {
+ return Vue.defineComponent({
+ name: 'CreatedLink',
+ inheritAttrs: false,
+ setup(_, { attrs, slots }) {
+ return () => Vue.h(Link, { ...attrs, _asChild: Comp }, slots)
+ },
+ }) as any
+}
+
+const LinkImpl = Vue.defineComponent({
+ name: 'Link',
+ inheritAttrs: false,
+ props: [
+ '_asChild',
+ 'to',
+ 'preload',
+ 'preloadDelay',
+ 'activeProps',
+ 'inactiveProps',
+ 'activeOptions',
+ 'from',
+ 'search',
+ 'params',
+ 'hash',
+ 'state',
+ 'mask',
+ 'reloadDocument',
+ 'disabled',
+ 'additionalProps',
+ 'viewTransition',
+ 'resetScroll',
+ 'startTransition',
+ 'hashScrollIntoView',
+ 'replace',
+ 'ignoreBlocker',
+ 'target',
+ ],
+ setup(props, { attrs, slots }) {
+ // Call useLinkProps ONCE during setup with combined props and attrs
+ // The returned object includes computed values that update reactively
+ const allProps = { ...props, ...attrs }
+ const linkProps = useLinkProps(allProps as any)
+
+ return () => {
+ const Component = props._asChild || 'a'
+
+ const isActive = linkProps['data-status'] === 'active'
+ const isTransitioning =
+ linkProps['data-transitioning'] === 'transitioning'
+
+ // Create the slot content or empty array if no default slot
+ const slotContent = slots.default
+ ? slots.default({
+ isActive,
+ isTransitioning,
+ })
+ : []
+
+ // Special handling for SVG links - wrap an inside the SVG
+ if (Component === 'svg') {
+ // Create props without class for svg link
+ const svgLinkProps = { ...linkProps }
+ delete (svgLinkProps as any).class
+ return Vue.h('svg', {}, [Vue.h('a', svgLinkProps, slotContent)])
+ }
+
+ // For custom functional components (non-string), pass children as a prop
+ // since they may expect children as a prop like in Solid
+ if (typeof Component !== 'string') {
+ return Vue.h(
+ Component,
+ { ...linkProps, children: slotContent },
+ slotContent,
+ )
+ }
+
+ // Return the component with props and children
+ return Vue.h(Component, linkProps, slotContent)
+ }
+ },
+})
+
+/**
+ * Link component with proper TypeScript generics support
+ */
+export const Link = LinkImpl as unknown as {
+ <
+ TRouter extends AnyRouter = RegisteredRouter,
+ TFrom extends RoutePaths | string = string,
+ TTo extends string | undefined = '.',
+ TMaskFrom extends RoutePaths | string = TFrom,
+ TMaskTo extends string = '.',
+ >(
+ props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
+ ): Vue.VNode
+}
+
+function isCtrlEvent(e: MouseEvent) {
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
+}
+
+export type LinkOptionsFnOptions<
+ TOptions,
+ TComp,
+ TRouter extends AnyRouter = RegisteredRouter,
+> =
+ TOptions extends ReadonlyArray
+ ? ValidateLinkOptionsArray
+ : ValidateLinkOptions
+
+export type LinkOptionsFn = <
+ const TOptions,
+ TRouter extends AnyRouter = RegisteredRouter,
+>(
+ options: LinkOptionsFnOptions,
+) => TOptions
+
+export const linkOptions: LinkOptionsFn<'a'> = (options) => {
+ return options as any
+}
diff --git a/packages/vue-router/src/matchContext.tsx b/packages/vue-router/src/matchContext.tsx
new file mode 100644
index 00000000000..ff1271bf528
--- /dev/null
+++ b/packages/vue-router/src/matchContext.tsx
@@ -0,0 +1,41 @@
+import * as Vue from 'vue'
+
+// Create a typed injection key with support for undefined values
+// This is the primary match context used throughout the router
+export const matchContext = Symbol('TanStackRouterMatch') as Vue.InjectionKey<
+ Vue.Ref
+>
+
+// Dummy match context for when we want to look up by explicit 'from' route
+export const dummyMatchContext = Symbol(
+ 'TanStackRouterDummyMatch',
+) as Vue.InjectionKey>
+
+/**
+ * Provides a match ID to child components
+ */
+export function provideMatch(matchId: string | undefined) {
+ Vue.provide(matchContext, Vue.ref(matchId))
+}
+
+/**
+ * Retrieves the match ID from the component tree
+ */
+export function injectMatch(): Vue.Ref {
+ return Vue.inject(matchContext, Vue.ref(undefined))
+}
+
+/**
+ * Provides a dummy match ID to child components
+ */
+export function provideDummyMatch(matchId: string | undefined) {
+ Vue.provide(dummyMatchContext, Vue.ref(matchId))
+}
+
+/**
+ * Retrieves the dummy match ID from the component tree
+ * This only exists so we can conditionally inject a value when we are not interested in the nearest match
+ */
+export function injectDummyMatch(): Vue.Ref {
+ return Vue.inject(dummyMatchContext, Vue.ref(undefined))
+}
diff --git a/packages/vue-router/src/not-found.tsx b/packages/vue-router/src/not-found.tsx
new file mode 100644
index 00000000000..d8ce39debe2
--- /dev/null
+++ b/packages/vue-router/src/not-found.tsx
@@ -0,0 +1,55 @@
+import * as Vue from 'vue'
+import { isNotFound } from '@tanstack/router-core'
+import { CatchBoundary } from './CatchBoundary'
+import { useRouterState } from './useRouterState'
+import type { ErrorComponentProps, NotFoundError } from '@tanstack/router-core'
+
+export function CatchNotFound(props: {
+ fallback?: (error: NotFoundError) => Vue.VNode
+ onCatch?: (error: Error) => void
+ children: Vue.VNode
+}) {
+ // TODO: Some way for the user to programmatically reset the not-found boundary?
+ const resetKey = useRouterState({
+ select: (s) => `not-found-${s.location.pathname}-${s.status}`,
+ })
+
+ // Create a function that returns a VNode to match the SyncRouteComponent signature
+ const errorComponentFn = (componentProps: ErrorComponentProps) => {
+ const error = componentProps.error
+
+ if (isNotFound(error)) {
+ // If a fallback is provided, use it
+ if (props.fallback) {
+ return props.fallback(error as NotFoundError)
+ }
+ // Otherwise return a default not found message
+ return Vue.h('p', null, 'Not Found')
+ } else {
+ // Re-throw non-NotFound errors
+ throw error
+ }
+ }
+
+ return Vue.h(CatchBoundary, {
+ getResetKey: () => resetKey.value,
+ onCatch: (error: Error) => {
+ if (isNotFound(error)) {
+ if (props.onCatch) {
+ props.onCatch(error)
+ }
+ } else {
+ throw error
+ }
+ },
+ errorComponent: errorComponentFn,
+ children: props.children,
+ })
+}
+
+export const DefaultGlobalNotFound = Vue.defineComponent({
+ name: 'DefaultGlobalNotFound',
+ setup() {
+ return () => Vue.h('p', null, 'Not Found')
+ },
+})
diff --git a/packages/vue-router/src/renderRouteNotFound.tsx b/packages/vue-router/src/renderRouteNotFound.tsx
new file mode 100644
index 00000000000..63ec147f7e8
--- /dev/null
+++ b/packages/vue-router/src/renderRouteNotFound.tsx
@@ -0,0 +1,35 @@
+import * as Vue from 'vue'
+import warning from 'tiny-warning'
+import { DefaultGlobalNotFound } from './not-found'
+import type { AnyRoute, AnyRouter } from '@tanstack/router-core'
+
+/**
+ * Renders a not found component for a route when no matching route is found.
+ *
+ * @param router - The router instance containing the route configuration
+ * @param route - The route that triggered the not found state
+ * @param data - Additional data to pass to the not found component
+ * @returns The rendered not found component or a default fallback component
+ */
+export function renderRouteNotFound(
+ router: AnyRouter,
+ route: AnyRoute,
+ data: any,
+): Vue.VNode {
+ if (!route.options.notFoundComponent) {
+ if (router.options.defaultNotFoundComponent) {
+ return Vue.h(router.options.defaultNotFoundComponent as any, data)
+ }
+
+ if (process.env.NODE_ENV === 'development') {
+ warning(
+ route.options.notFoundComponent,
+ `A notFoundError was encountered on the route with ID "${route.id}", but a notFoundComponent option was not configured, nor was a router level defaultNotFoundComponent configured. Consider configuring at least one of these to avoid TanStack Router's overly generic defaultNotFoundComponent (Not Found
)`,
+ )
+ }
+
+ return Vue.h(DefaultGlobalNotFound)
+ }
+
+ return Vue.h(route.options.notFoundComponent as any, data)
+}
diff --git a/packages/vue-router/src/route.ts b/packages/vue-router/src/route.ts
new file mode 100644
index 00000000000..458d70659d9
--- /dev/null
+++ b/packages/vue-router/src/route.ts
@@ -0,0 +1,658 @@
+import {
+ BaseRootRoute,
+ BaseRoute,
+ BaseRouteApi,
+ notFound,
+} from '@tanstack/router-core'
+import { useLoaderData } from './useLoaderData'
+import { useLoaderDeps } from './useLoaderDeps'
+import { useParams } from './useParams'
+import { useSearch } from './useSearch'
+import { useNavigate } from './useNavigate'
+import { useMatch } from './useMatch'
+import { useRouter } from './useRouter'
+import type {
+ AnyContext,
+ AnyRoute,
+ AnyRouter,
+ ConstrainLiteral,
+ ErrorComponentProps,
+ NotFoundError,
+ NotFoundRouteProps,
+ Register,
+ RegisteredRouter,
+ ResolveFullPath,
+ ResolveId,
+ ResolveParams,
+ RootRoute as RootRouteCore,
+ RootRouteId,
+ RootRouteOptions,
+ RouteConstraints,
+ Route as RouteCore,
+ RouteIds,
+ RouteMask,
+ RouteOptions,
+ RouteTypesById,
+ RouterCore,
+ ToMaskOptions,
+ UseNavigateResult,
+} from '@tanstack/router-core'
+import type { UseLoaderDataRoute } from './useLoaderData'
+import type { UseMatchRoute } from './useMatch'
+import type { UseLoaderDepsRoute } from './useLoaderDeps'
+import type { UseParamsRoute } from './useParams'
+import type { UseSearchRoute } from './useSearch'
+import type * as Vue from 'vue'
+import type { UseRouteContextRoute } from './useRouteContext'
+
+// Structural type for Vue SFC components (.vue files)
+// Uses structural matching to accept Vue components without breaking
+// TypeScript inference for inline function components
+type VueSFC = {
+ readonly __name?: string
+ setup?: (...args: Array) => any
+ render?: Function
+}
+
+declare module '@tanstack/router-core' {
+ export interface UpdatableRouteOptionsExtensions {
+ component?: RouteComponent | VueSFC
+ errorComponent?: false | null | undefined | ErrorRouteComponent | VueSFC
+ notFoundComponent?: NotFoundRouteComponent | VueSFC
+ pendingComponent?: RouteComponent | VueSFC
+ }
+
+ export interface RouteExtensions<
+ in out TId extends string,
+ in out TFullPath extends string,
+ > {
+ useMatch: UseMatchRoute
+ useRouteContext: UseRouteContextRoute
+ useSearch: UseSearchRoute
+ useParams: UseParamsRoute
+ useLoaderDeps: UseLoaderDepsRoute
+ useLoaderData: UseLoaderDataRoute
+ useNavigate: () => UseNavigateResult
+ }
+}
+
+export function getRouteApi<
+ const TId,
+ TRouter extends AnyRouter = RegisteredRouter,
+>(id: ConstrainLiteral>) {
+ return new RouteApi({ id })
+}
+
+export class RouteApi<
+ TId,
+ TRouter extends AnyRouter = RegisteredRouter,
+> extends BaseRouteApi {
+ /**
+ * @deprecated Use the `getRouteApi` function instead.
+ */
+ constructor({ id }: { id: TId }) {
+ super({ id })
+ }
+
+ useMatch: UseMatchRoute = (opts) => {
+ return useMatch({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useRouteContext: UseRouteContextRoute = (opts) => {
+ return useMatch({
+ from: this.id as any,
+ select: (d) => (opts?.select ? opts.select(d.context) : d.context),
+ }) as any
+ }
+
+ useSearch: UseSearchRoute = (opts) => {
+ return useSearch({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useParams: UseParamsRoute = (opts) => {
+ return useParams({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useLoaderDeps: UseLoaderDepsRoute = (opts) => {
+ return useLoaderDeps({ ...opts, from: this.id, strict: false } as any)
+ }
+
+ useLoaderData: UseLoaderDataRoute = (opts) => {
+ return useLoaderData({ ...opts, from: this.id, strict: false } as any)
+ }
+
+ useNavigate = (): UseNavigateResult<
+ RouteTypesById['fullPath']
+ > => {
+ const router = useRouter()
+ return useNavigate({ from: router.routesById[this.id as string].fullPath })
+ }
+
+ notFound = (opts?: NotFoundError) => {
+ return notFound({ routeId: this.id as string, ...opts })
+ }
+}
+
+export class Route<
+ in out TRegister = unknown,
+ in out TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute,
+ in out TPath extends RouteConstraints['TPath'] = '/',
+ in out TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath<
+ TParentRoute,
+ TPath
+ >,
+ in out TCustomId extends RouteConstraints['TCustomId'] = string,
+ in out TId extends RouteConstraints['TId'] = ResolveId<
+ TParentRoute,
+ TCustomId,
+ TPath
+ >,
+ in out TSearchValidator = undefined,
+ in out TParams = ResolveParams,
+ in out TRouterContext = AnyContext,
+ in out TRouteContextFn = AnyContext,
+ in out TBeforeLoadFn = AnyContext,
+ in out TLoaderDeps extends Record = {},
+ in out TLoaderFn = undefined,
+ in out TChildren = unknown,
+ in out TFileRouteTypes = unknown,
+ in out TSSR = unknown,
+ in out TMiddlewares = unknown,
+ in out THandlers = undefined,
+ >
+ extends BaseRoute<
+ TRegister,
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes,
+ TSSR,
+ TMiddlewares,
+ THandlers
+ >
+ implements
+ RouteCore<
+ TRegister,
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes,
+ TSSR,
+ TMiddlewares,
+ THandlers
+ >
+{
+ /**
+ * @deprecated Use the `createRoute` function instead.
+ */
+ constructor(
+ options?: RouteOptions<
+ TRegister,
+ TParentRoute,
+ TId,
+ TCustomId,
+ TFullPath,
+ TPath,
+ TSearchValidator,
+ TParams,
+ TLoaderDeps,
+ TLoaderFn,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TSSR,
+ TMiddlewares,
+ THandlers
+ >,
+ ) {
+ super(options)
+ }
+
+ useMatch: UseMatchRoute = (opts) => {
+ return useMatch({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useRouteContext: UseRouteContextRoute = (opts?) => {
+ return useMatch({
+ ...opts,
+ from: this.id,
+ select: (d) => (opts?.select ? opts.select(d.context) : d.context),
+ }) as any
+ }
+
+ useSearch: UseSearchRoute = (opts) => {
+ return useSearch({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useParams: UseParamsRoute = (opts) => {
+ return useParams({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useLoaderDeps: UseLoaderDepsRoute = (opts) => {
+ return useLoaderDeps({ ...opts, from: this.id } as any)
+ }
+
+ useLoaderData: UseLoaderDataRoute = (opts) => {
+ return useLoaderData({ ...opts, from: this.id } as any)
+ }
+
+ useNavigate = (): UseNavigateResult => {
+ return useNavigate({ from: this.fullPath })
+ }
+}
+
+export function createRoute<
+ TRegister = unknown,
+ TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute,
+ TPath extends RouteConstraints['TPath'] = '/',
+ TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath<
+ TParentRoute,
+ TPath
+ >,
+ TCustomId extends RouteConstraints['TCustomId'] = string,
+ TId extends RouteConstraints['TId'] = ResolveId<
+ TParentRoute,
+ TCustomId,
+ TPath
+ >,
+ TSearchValidator = undefined,
+ TParams = ResolveParams,
+ TRouteContextFn = AnyContext,
+ TBeforeLoadFn = AnyContext,
+ TLoaderDeps extends Record = {},
+ TLoaderFn = undefined,
+ TChildren = unknown,
+ TSSR = unknown,
+ THandlers = undefined,
+>(
+ options: RouteOptions<
+ TRegister,
+ TParentRoute,
+ TId,
+ TCustomId,
+ TFullPath,
+ TPath,
+ TSearchValidator,
+ TParams,
+ TLoaderDeps,
+ TLoaderFn,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TSSR,
+ THandlers
+ >,
+): Route<
+ TRegister,
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ unknown,
+ TSSR,
+ THandlers
+> {
+ return new Route<
+ TRegister,
+ TParentRoute,
+ TPath,
+ TFullPath,
+ TCustomId,
+ TId,
+ TSearchValidator,
+ TParams,
+ AnyContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ unknown,
+ TSSR,
+ THandlers
+ >(options)
+}
+
+export type AnyRootRoute = RootRoute<
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any
+>
+
+export function createRootRouteWithContext() {
+ return <
+ TRegister = Register,
+ TRouteContextFn = AnyContext,
+ TBeforeLoadFn = AnyContext,
+ TSearchValidator = undefined,
+ TLoaderDeps extends Record = {},
+ TLoaderFn = undefined,
+ TSSR = unknown,
+ THandlers = undefined,
+ >(
+ options?: RootRouteOptions<
+ TRegister,
+ TSearchValidator,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TSSR,
+ THandlers
+ >,
+ ) => {
+ return createRootRoute<
+ TRegister,
+ TSearchValidator,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TSSR,
+ THandlers
+ >(options as any)
+ }
+}
+
+/**
+ * @deprecated Use the `createRootRouteWithContext` function instead.
+ */
+export const rootRouteWithContext = createRootRouteWithContext
+
+export class RootRoute<
+ in out TRegister = Register,
+ in out TSearchValidator = undefined,
+ in out TRouterContext = {},
+ in out TRouteContextFn = AnyContext,
+ in out TBeforeLoadFn = AnyContext,
+ in out TLoaderDeps extends Record = {},
+ in out TLoaderFn = undefined,
+ in out TChildren = unknown,
+ in out TFileRouteTypes = unknown,
+ in out TSSR = unknown,
+ in out THandlers = undefined,
+ >
+ extends BaseRootRoute<
+ TRegister,
+ TSearchValidator,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes,
+ TSSR,
+ THandlers
+ >
+ implements
+ RootRouteCore<
+ TRegister,
+ TSearchValidator,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TChildren,
+ TFileRouteTypes,
+ TSSR,
+ THandlers
+ >
+{
+ /**
+ * @deprecated `RootRoute` is now an internal implementation detail. Use `createRootRoute()` instead.
+ */
+ constructor(
+ options?: RootRouteOptions<
+ TRegister,
+ TSearchValidator,
+ TRouterContext,
+ TRouteContextFn,
+ TBeforeLoadFn,
+ TLoaderDeps,
+ TLoaderFn,
+ TSSR,
+ THandlers
+ >,
+ ) {
+ super(options)
+ }
+
+ useMatch: UseMatchRoute = (opts) => {
+ return useMatch({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useRouteContext: UseRouteContextRoute = (opts) => {
+ return useMatch({
+ ...opts,
+ from: this.id,
+ select: (d) => (opts?.select ? opts.select(d.context) : d.context),
+ }) as any
+ }
+
+ useSearch: UseSearchRoute = (opts) => {
+ return useSearch({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useParams: UseParamsRoute = (opts) => {
+ return useParams({
+ select: opts?.select,
+ from: this.id,
+ } as any) as any
+ }
+
+ useLoaderDeps: UseLoaderDepsRoute = (opts) => {
+ return useLoaderDeps({ ...opts, from: this.id } as any)
+ }
+
+ useLoaderData: UseLoaderDataRoute = (opts) => {
+ return useLoaderData({ ...opts, from: this.id } as any)
+ }
+
+ useNavigate = (): UseNavigateResult<'/'> => {
+ return useNavigate({ from: this.fullPath })
+ }
+}
+
+export function createRouteMask<
+ TRouteTree extends AnyRoute,
+ TFrom extends string,
+ TTo extends string,
+>(
+ opts: {
+ routeTree: TRouteTree
+ } & ToMaskOptions, TFrom, TTo>,
+): RouteMask {
+ return opts as any
+}
+
+export type VueNode = Vue.VNode
+
+export type SyncRouteComponent = (props: TProps) => Vue.VNode
+
+export type AsyncRouteComponent = SyncRouteComponent & {
+ preload?: () => Promise
+}
+
+export type RouteComponent = AsyncRouteComponent
+
+export type ErrorRouteComponent = RouteComponent
+
+export type NotFoundRouteComponent = SyncRouteComponent