diff --git a/e2e/react-router/rspack-basic-file-based/.gitignore b/e2e/react-router/rspack-basic-file-based/.gitignore new file mode 100644 index 0000000000..fbb2bd0293 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/.gitignore @@ -0,0 +1,20 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# E2E +src/routeTree.gen.ts +test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-router/rspack-basic-file-based/README.md b/e2e/react-router/rspack-basic-file-based/README.md new file mode 100644 index 0000000000..93f18812e1 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `pnpm install` +- `pnpm dev` diff --git a/e2e/react-router/rspack-basic-file-based/package.json b/e2e/react-router/rspack-basic-file-based/package.json new file mode 100644 index 0000000000..68c438f770 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/package.json @@ -0,0 +1,28 @@ +{ + "name": "tanstack-router-e2e-react-rspack-basic-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "rsbuild dev --port 3000", + "build": "rsbuild build && tsc --noEmit", + "preview": "rsbuild preview", + "start": "rsbuild preview", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/router-devtools": "workspace:^", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "redaxios": "^0.5.1" + }, + "devDependencies": { + "@playwright/test": "^1.49.0", + "@rsbuild/core": "1.1.9", + "@rsbuild/plugin-react": "1.1.0", + "@tanstack/router-plugin": "workspace:^", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.6.2" + } +} diff --git a/e2e/react-router/rspack-basic-file-based/playwright.config.ts b/e2e/react-router/rspack-basic-file-based/playwright.config.ts new file mode 100644 index 0000000000..4ecf327e3c --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '../../utils.js' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `PUBLIC_SERVER_PORT=${PORT} pnpm build && PUBLIC_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-router/rspack-basic-file-based/rsbuild.config.ts b/e2e/react-router/rspack-basic-file-based/rsbuild.config.ts new file mode 100644 index 0000000000..23217fbbc1 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/rsbuild.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' +import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack' + +export default defineConfig({ + plugins: [pluginReact()], + html: { + tags: [ + { + tag: 'script', + attrs: { src: 'https://cdn.tailwindcss.com' }, + }, + ], + }, + tools: { + rspack: { + plugins: [TanStackRouterRspack()], + }, + }, +}) diff --git a/e2e/react-router/rspack-basic-file-based/src/app.tsx b/e2e/react-router/rspack-basic-file-based/src/app.tsx new file mode 100644 index 0000000000..5bace65b50 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/app.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { RouterProvider, createRouter } from '@tanstack/react-router' + +import { routeTree } from './routeTree.gen' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', +}) + +// Register things for typesafety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} +const App = () => { + return +} + +export default App diff --git a/e2e/react-router/rspack-basic-file-based/src/env.d.ts b/e2e/react-router/rspack-basic-file-based/src/env.d.ts new file mode 100644 index 0000000000..b0ac762b09 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/e2e/react-router/rspack-basic-file-based/src/index.tsx b/e2e/react-router/rspack-basic-file-based/src/index.tsx new file mode 100644 index 0000000000..f56cadf11c --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/index.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './app' + +const rootEl = document.getElementById('root') + +if (rootEl) { + const root = ReactDOM.createRoot(rootEl) + root.render( + + + , + ) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/posts.tsx b/e2e/react-router/rspack-basic-file-based/src/posts.tsx new file mode 100644 index 0000000000..3ccf1ff421 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/react-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +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) + .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>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/__root.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/__root.tsx new file mode 100644 index 0000000000..6b57d1e239 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/__root.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + + + ) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/_layout.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/_layout.tsx new file mode 100644 index 0000000000..02ddbb1cd9 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx new file mode 100644 index 0000000000..3b7dbf2903 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 0000000000..61e19b4d9f --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 0000000000..cceed1fb9a --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/index.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/index.tsx new file mode 100644 index 0000000000..eac82a9174 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/index.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/posts.$postId.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 0000000000..cded91ef96 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import { ErrorComponent, createFileRoute } from '@tanstack/react-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

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

{post.title}

+
{post.body}
+
+ ) +} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/posts.index.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/posts.index.tsx new file mode 100644 index 0000000000..056433ca0a --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/posts.index.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/react-router/rspack-basic-file-based/src/routes/posts.tsx b/e2e/react-router/rspack-basic-file-based/src/routes/posts.tsx new file mode 100644 index 0000000000..c7a09ed7f8 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/src/routes/posts.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/react-router/rspack-basic-file-based/tests/app.spec.ts b/e2e/react-router/rspack-basic-file-based/tests/app.spec.ts new file mode 100644 index 0000000000..10beff655f --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#root')).toContainText("I'm a layout") + await expect(page.locator('#root')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#root')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#root')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/react-router/rspack-basic-file-based/tsconfig.json b/e2e/react-router/rspack-basic-file-based/tsconfig.json new file mode 100644 index 0000000000..76f578eb54 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "allowJs": true + }, + "include": ["src", "playwright.config.ts", "tests"], + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/react-router/rspack-basic-file-based/tsr.config.json b/e2e/react-router/rspack-basic-file-based/tsr.config.json new file mode 100644 index 0000000000..15b57e5ea3 --- /dev/null +++ b/e2e/react-router/rspack-basic-file-based/tsr.config.json @@ -0,0 +1,4 @@ +{ + "routesDirectory": "./src/routes", + "generatedRouteTree": "./src/routeTree.gen.ts" +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/.gitignore b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/.gitignore new file mode 100644 index 0000000000..fbb2bd0293 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/.gitignore @@ -0,0 +1,20 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# E2E +src/routeTree.gen.ts +test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/README.md b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/README.md new file mode 100644 index 0000000000..93f18812e1 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `pnpm install` +- `pnpm dev` diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json new file mode 100644 index 0000000000..a880b34a05 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json @@ -0,0 +1,29 @@ +{ + "name": "tanstack-router-e2e-react-rspack-basic-virtual-named-export-config-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "rsbuild dev --port 3000", + "build": "rsbuild build && tsc --noEmit", + "preview": "rsbuild preview", + "start": "rsbuild preview", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/router-devtools": "workspace:^", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "redaxios": "^0.5.1" + }, + "devDependencies": { + "@playwright/test": "^1.49.0", + "@rsbuild/core": "1.1.9", + "@rsbuild/plugin-react": "1.1.0", + "@tanstack/router-plugin": "workspace:^", + "@tanstack/virtual-file-routes": "workspace:^", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.6.2" + } +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts new file mode 100644 index 0000000000..4ecf327e3c --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '../../utils.js' +import packageJson from './package.json' with { type: 'json' } + +const PORT = derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `PUBLIC_SERVER_PORT=${PORT} pnpm build && PUBLIC_SERVER_PORT=${PORT} pnpm start --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/routes.ts b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/routes.ts new file mode 100644 index 0000000000..6c2c144ec5 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/routes.ts @@ -0,0 +1,22 @@ +import { + index, + layout, + physical, + rootRoute, + route, +} from '@tanstack/virtual-file-routes' + +export const routes = rootRoute('root.tsx', [ + index('home.tsx'), + route('/posts', 'posts/posts.tsx', [ + index('posts/posts-home.tsx'), + route('$postId', 'posts/posts-detail.tsx'), + ]), + layout('first', 'layout/first-layout.tsx', [ + layout('second', 'layout/second-layout.tsx', [ + route('/layout-a', 'a.tsx'), + route('/layout-b', 'b.tsx'), + ]), + ]), + physical('/classic', 'file-based-subtree'), +]) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts new file mode 100644 index 0000000000..23217fbbc1 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/rsbuild.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' +import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack' + +export default defineConfig({ + plugins: [pluginReact()], + html: { + tags: [ + { + tag: 'script', + attrs: { src: 'https://cdn.tailwindcss.com' }, + }, + ], + }, + tools: { + rspack: { + plugins: [TanStackRouterRspack()], + }, + }, +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx new file mode 100644 index 0000000000..5bace65b50 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/app.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { RouterProvider, createRouter } from '@tanstack/react-router' + +import { routeTree } from './routeTree.gen' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', +}) + +// Register things for typesafety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} +const App = () => { + return +} + +export default App diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts new file mode 100644 index 0000000000..b0ac762b09 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx new file mode 100644 index 0000000000..f56cadf11c --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/index.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './app' + +const rootEl = document.getElementById('root') + +if (rootEl) { + const root = ReactDOM.createRoot(rootEl) + root.render( + + + , + ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx new file mode 100644 index 0000000000..3ccf1ff421 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/react-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +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) + .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>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx new file mode 100644 index 0000000000..6cccd02950 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_first/_second/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx new file mode 100644 index 0000000000..98bb842612 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_first/_second/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx new file mode 100644 index 0000000000..c7417e5eeb --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/classic/hello/')({ + component: () =>
This is the index
, +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx new file mode 100644 index 0000000000..566efc8777 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/route.tsx @@ -0,0 +1,27 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/classic/hello')({ + component: () => ( +
+ Hello! +
{' '} + + say hello to the universe + {' '} + + say hello to the world + + +
+ ), +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx new file mode 100644 index 0000000000..e00c47d74b --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/universe.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/classic/hello/universe')({ + component: () =>
Hello /classic/hello/universe!
, +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx new file mode 100644 index 0000000000..9783557342 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/file-based-subtree/hello/world.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/classic/hello/world')({ + component: () =>
Hello /classic/hello/world!
, +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx new file mode 100644 index 0000000000..eac82a9174 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/home.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx new file mode 100644 index 0000000000..d39e206f2d --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/first-layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_first')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx new file mode 100644 index 0000000000..ef178a6e16 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/layout/second-layout.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_first/_second')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx new file mode 100644 index 0000000000..948d52d6d6 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-detail.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import { ErrorComponent, createFileRoute } from '@tanstack/react-router' +import { fetchPost } from '../../posts' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + notFoundComponent: () => { + return

Post not found

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

{post.title}

+
{post.body}
+
+ ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx new file mode 100644 index 0000000000..056433ca0a --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts-home.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx new file mode 100644 index 0000000000..a2ab1ee388 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/posts/posts.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '../../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx new file mode 100644 index 0000000000..05ac9527f3 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/src/routes/root.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Layout + {' '} + + Subtree + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + + + ) +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts new file mode 100644 index 0000000000..10beff655f --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tests/app.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Navigating to a post page', async ({ page }) => { + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) + +test('Navigating nested layouts', async ({ page }) => { + await page.getByRole('link', { name: 'Layout', exact: true }).click() + + await expect(page.locator('#root')).toContainText("I'm a layout") + await expect(page.locator('#root')).toContainText("I'm a nested layout") + + await page.getByRole('link', { name: 'Layout A' }).click() + await expect(page.locator('#root')).toContainText("I'm layout A!") + + await page.getByRole('link', { name: 'Layout B' }).click() + await expect(page.locator('#root')).toContainText("I'm layout B!") +}) + +test('Navigating to a not-found route', async ({ page }) => { + await page.getByRole('link', { name: 'This Route Does Not Exist' }).click() + await expect(page.getByRole('paragraph')).toContainText( + 'This is the notFoundComponent configured on root route', + ) + await page.getByRole('link', { name: 'Start Over' }).click() + await expect(page.getByRole('heading')).toContainText('Welcome Home!') +}) diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json new file mode 100644 index 0000000000..7dc6d51cf9 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "useDefineForClassFields": true, + "allowJs": true + }, + "include": ["src", "playwright.config.ts", "tests", "./routes.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json new file mode 100644 index 0000000000..2759341ba2 --- /dev/null +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/tsr.config.json @@ -0,0 +1,5 @@ +{ + "routesDirectory": "./src/routes", + "generatedRouteTree": "./src/routeTree.gen.ts", + "virtualRouteConfig": "./routes.ts" +} diff --git a/e2e/utils.js b/e2e/utils.js index d0090dde51..a1626c67f2 100644 --- a/e2e/utils.js +++ b/e2e/utils.js @@ -16,5 +16,8 @@ export function derivePort(input, min = 5600, max = 65535) { // Map hash value to the port range const port = min + (hashInt % (max - min + 1)) + + console.info(`Mapped "${input}" to port ${port}`) + return port } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0196c5a305..b2b0582227 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,6 +475,89 @@ importers: specifier: ^6.0.3 version: 6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + e2e/react-router/rspack-basic-file-based: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../../../packages/router-devtools + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + devDependencies: + '@playwright/test': + specifier: ^1.49.0 + version: 1.49.0 + '@rsbuild/core': + specifier: 1.1.9 + version: 1.1.9 + '@rsbuild/plugin-react': + specifier: 1.1.0 + version: 1.1.0(@rsbuild/core@1.1.9) + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + '@types/react': + specifier: ^18.3.3 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.1 + typescript: + specifier: ^5.6.2 + version: 5.7.2 + + e2e/react-router/rspack-basic-virtual-named-export-config-file-based: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../../../packages/router-devtools + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + devDependencies: + '@playwright/test': + specifier: ^1.49.0 + version: 1.49.0 + '@rsbuild/core': + specifier: 1.1.9 + version: 1.1.9 + '@rsbuild/plugin-react': + specifier: 1.1.0 + version: 1.1.0(@rsbuild/core@1.1.9) + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + '@tanstack/virtual-file-routes': + specifier: workspace:* + version: link:../../../packages/virtual-file-routes + '@types/react': + specifier: ^18.3.3 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.1 + typescript: + specifier: ^5.6.2 + version: 5.7.2 + e2e/react-router/scroll-restoration-sandbox-vite: dependencies: '@tanstack/react-router':