diff --git a/e2e/react-router/basepath-file-based/.gitignore b/e2e/react-router/basepath-file-based/.gitignore new file mode 100644 index 00000000000..4d2da67b504 --- /dev/null +++ b/e2e/react-router/basepath-file-based/.gitignore @@ -0,0 +1,11 @@ +node_modules +.DS_Store +dist +dist-hash +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-router/basepath-file-based/index.html b/e2e/react-router/basepath-file-based/index.html new file mode 100644 index 00000000000..21e30f16951 --- /dev/null +++ b/e2e/react-router/basepath-file-based/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/e2e/react-router/basepath-file-based/package.json b/e2e/react-router/basepath-file-based/package.json new file mode 100644 index 00000000000..bfe2ec82f2f --- /dev/null +++ b/e2e/react-router/basepath-file-based/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-e2e-react-basepath-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-router-devtools": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^7.1.7" + } +} diff --git a/e2e/react-router/basepath-file-based/playwright.config.ts b/e2e/react-router/basepath-file-based/playwright.config.ts new file mode 100644 index 00000000000..4dc2271f01e --- /dev/null +++ b/e2e/react-router/basepath-file-based/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test' +import { + getDummyServerPort, + getTestServerPort, +} from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const EXTERNAL_PORT = await getDummyServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && VITE_SERVER_PORT=${PORT} pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-router/basepath-file-based/src/main.tsx b/e2e/react-router/basepath-file-based/src/main.tsx new file mode 100644 index 00000000000..28f985de688 --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/main.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, + basepath: '/app', +}) + +// Register things for typesafety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement) + root.render() +} diff --git a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts new file mode 100644 index 00000000000..59499d9fbf3 --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' + id: '__root__' | '/' | '/about' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/react-router/basepath-file-based/src/routes/__root.tsx b/e2e/react-router/basepath-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..9c657c7d5b4 --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/routes/__root.tsx @@ -0,0 +1,3 @@ +import { createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute() diff --git a/e2e/react-router/basepath-file-based/src/routes/about.tsx b/e2e/react-router/basepath-file-based/src/routes/about.tsx new file mode 100644 index 00000000000..2d0dba2d6fb --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/routes/about.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/about')({ + component: RouteComponent, +}) + +function RouteComponent() { + const navigate = Route.useNavigate() + + return ( +
+ +
+ ) +} diff --git a/e2e/react-router/basepath-file-based/src/routes/index.tsx b/e2e/react-router/basepath-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..9975e63698d --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/routes/index.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: App, +}) + +function App() { + const navigate = Route.useNavigate() + + return ( +
+ +
+ ) +} diff --git a/e2e/react-router/basepath-file-based/tests/reload-document.test.ts b/e2e/react-router/basepath-file-based/tests/reload-document.test.ts new file mode 100644 index 00000000000..3e60f3bedcb --- /dev/null +++ b/e2e/react-router/basepath-file-based/tests/reload-document.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test' + +test('navigate() respects basepath for when reloadDocument=true', async ({ + page, +}) => { + await page.goto(`/app/`) + await expect(page.getByTestId(`home-component`)).toBeInViewport() + + const aboutBtn = page.getByTestId(`to-about-btn`) + await aboutBtn.click() + await page.waitForURL('/app/about') + await expect(page.getByTestId(`about-component`)).toBeInViewport() + + const homeBtn = page.getByTestId(`to-home-btn`) + await homeBtn.click() + await page.waitForURL('/app/') + await expect(page.getByTestId(`home-component`)).toBeInViewport() +}) diff --git a/e2e/react-router/basepath-file-based/tests/setup/global.setup.ts b/e2e/react-router/basepath-file-based/tests/setup/global.setup.ts new file mode 100644 index 00000000000..3593d10ab90 --- /dev/null +++ b/e2e/react-router/basepath-file-based/tests/setup/global.setup.ts @@ -0,0 +1,6 @@ +import { e2eStartDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function setup() { + await e2eStartDummyServer(packageJson.name) +} diff --git a/e2e/react-router/basepath-file-based/tests/setup/global.teardown.ts b/e2e/react-router/basepath-file-based/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..62fd79911cc --- /dev/null +++ b/e2e/react-router/basepath-file-based/tests/setup/global.teardown.ts @@ -0,0 +1,6 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function teardown() { + await e2eStopDummyServer(packageJson.name) +} diff --git a/e2e/react-router/basepath-file-based/tsconfig.json b/e2e/react-router/basepath-file-based/tsconfig.json new file mode 100644 index 00000000000..82cf0bcd2c9 --- /dev/null +++ b/e2e/react-router/basepath-file-based/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true, + "types": ["vite/client"] + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/react-router/basepath-file-based/vite.config.js b/e2e/react-router/basepath-file-based/vite.config.js new file mode 100644 index 00000000000..784341f8bc4 --- /dev/null +++ b/e2e/react-router/basepath-file-based/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: '/app/', + plugins: [tanstackRouter({ target: 'react' }), react()], +}) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 46c52c9ec38..78b5e11608b 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1916,7 +1916,7 @@ export class RouterCore< if (reloadDocument) { if (!href) { const location = this.buildLocation({ to, ...rest } as any) - href = location.href + href = location.url } if (rest.replace) { window.location.replace(href) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d040a1b385c..a3dd14049b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,43 @@ importers: specifier: ^4.5.4 version: 4.5.4(@types/node@22.10.2)(rollup@4.52.2)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/react-router/basepath-file-based: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-router-devtools': + specifier: workspace:^ + version: link:../../../packages/react-router-devtools + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + react: + specifier: ^19.0.0 + version: 19.0.0 + react-dom: + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) + devDependencies: + '@playwright/test': + specifier: ^1.52.0 + version: 1.52.0 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/react': + specifier: ^19.0.8 + version: 19.0.8 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.0.3(@types/react@19.0.8) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0) + e2e/react-router/basic: dependencies: '@tanstack/react-router': @@ -25645,28 +25682,28 @@ snapshots: pluralize@8.0.0: {} - postcss-import@15.1.0(postcss@8.5.3): + postcss-import@15.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.3 + postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.3): + postcss-js@4.0.1(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.3 + postcss: 8.5.6 - postcss-load-config@4.0.2(postcss@8.5.3): + postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 yaml: 2.7.0 optionalDependencies: - postcss: 8.5.3 + postcss: 8.5.6 - postcss-nested@6.2.0(postcss@8.5.3): + postcss-nested@6.2.0(postcss@8.5.6): dependencies: - postcss: 8.5.3 + postcss: 8.5.6 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -26581,11 +26618,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.3 - postcss-import: 15.1.0(postcss@8.5.3) - postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3) - postcss-nested: 6.2.0(postcss@8.5.3) + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0