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 @@ + + + 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 @@ + + + + + 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 ( +
+
I'm a layout
+
+ +
+
+ ) +} 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 @@ + + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + 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 @@ + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 + {' '} +
+ + Link in SVG + + + + +
+
+ + + + ) +} + +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 ( +
+
I'm a layout
+
+ +
+
+ ) +} + +const layout2Route = createRoute({ + getParentRoute: () => layoutRoute, + id: '_layout-2', + component: Layout2Component, +}) + +function Layout2Component() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} + +const layoutARoute = createRoute({ + getParentRoute: () => layout2Route, + path: '/layout-a', + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} + +const layoutBRoute = createRoute({ + getParentRoute: () => layout2Route, + path: '/layout-b', + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} + +const 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 ( +
+
    + {[ + ...posts.value, + { id: 'i-do-not-exist', title: 'Non-existent Post' }, + ].map((post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + })} +
+ +
+ ) +} 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 @@ + + + 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 @@ + + + + + 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 ( +
+
I'm a layout
+
+ +
+
+ ) +} 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + 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 @@ + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + + + 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 ( +
+
    + {[ + ...posts.value, + { id: 'i-do-not-exist', title: 'Non-existent Post' }, + ].map((post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + })} +
+ +
+ ) +} 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 + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for Solid! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +## 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