diff --git a/e2e/react-start/rsc-hmr/.gitignore b/e2e/react-start/rsc-hmr/.gitignore new file mode 100644 index 0000000000..3116396262 --- /dev/null +++ b/e2e/react-start/rsc-hmr/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +dist-vite-* +dist-rsbuild-* +test-results +playwright-report +port*.txt diff --git a/e2e/react-start/rsc-hmr/package.json b/e2e/react-start/rsc-hmr/package.json new file mode 100644 index 0000000000..1f2a2160bf --- /dev/null +++ b/e2e/react-start/rsc-hmr/package.json @@ -0,0 +1,61 @@ +{ + "name": "tanstack-react-start-e2e-rsc-hmr", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "pnpm dev:vite --port 3000", + "dev:e2e": "pnpm dev:vite", + "dev:vite": "vite dev", + "dev:rsbuild": "rsbuild dev", + "build": "pnpm build:vite", + "build:vite": "vite build && tsc --noEmit", + "build:rsbuild": "rsbuild build && tsc --noEmit", + "test:e2e": "MODE=dev playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.61.0", + "@rsbuild/core": "^2.0.11", + "@rsbuild/plugin-react": "^2.0.0", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-rsc": "^0.5.30", + "react-server-dom-rspack": "^0.0.2", + "typescript": "npm:@typescript/typescript6@^6.0.2", + "vite": "^8.0.14" + }, + "nx": { + "targets": { + "test:e2e": { + "parallelism": false + }, + "test:e2e--vite-ssr": { + "parallelism": false + }, + "test:e2e--rsbuild-ssr": { + "parallelism": false + } + }, + "metadata": { + "playwrightModes": [ + { + "toolchain": "vite", + "mode": "ssr" + }, + { + "toolchain": "rsbuild", + "mode": "ssr" + } + ] + } + } +} diff --git a/e2e/react-start/rsc-hmr/playwright.config.ts b/e2e/react-start/rsc-hmr/playwright.config.ts new file mode 100644 index 0000000000..c159aad328 --- /dev/null +++ b/e2e/react-start/rsc-hmr/playwright.config.ts @@ -0,0 +1,50 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' +const e2ePortKey = + process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +const distDir = process.env.E2E_DIST_DIR ?? 'dist' +const viteBundledDev = process.env.E2E_VITE_BUNDLED_DEV === 'true' +const PORT = await getTestServerPort(e2ePortKey) +const baseURL = `http://localhost:${PORT}` +const devCommand = + toolchain === 'rsbuild' ? 'pnpm dev:rsbuild' : 'pnpm dev:vite' + +export default defineConfig({ + testDir: './tests', + workers: 1, + reporter: [['line']], + + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + + use: { + baseURL, + }, + + webServer: { + command: `${devCommand} --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + env: { + VITE_NODE_ENV: 'test', + PORT: String(PORT), + E2E_TOOLCHAIN: toolchain, + E2E_DIST_DIR: distDir, + E2E_PORT_KEY: e2ePortKey, + E2E_VITE_BUNDLED_DEV: String(viteBundledDev), + }, + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}) diff --git a/e2e/react-start/rsc-hmr/rsbuild.config.ts b/e2e/react-start/rsc-hmr/rsbuild.config.ts new file mode 100644 index 0000000000..85eb2a91e5 --- /dev/null +++ b/e2e/react-start/rsc-hmr/rsbuild.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' +import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist' + +export default defineConfig({ + plugins: [ + pluginReact({ splitChunks: false }), + tanstackStart({ + rsc: { + enabled: true, + }, + }), + ], + output: { + distPath: { + root: outDir, + }, + }, + server: { + port: 3000, + }, +}) diff --git a/e2e/react-start/rsc-hmr/src/routeTree.gen.ts b/e2e/react-start/rsc-hmr/src/routeTree.gen.ts new file mode 100644 index 0000000000..6c692bc76c --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routeTree.gen.ts @@ -0,0 +1,127 @@ +/* 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 IndexRouteImport } from './routes/index' +import { Route as CoLocatedRouteImport } from './routes/co-located' +import { Route as CoLocatedCssModuleRouteImport } from './routes/co-located-css-module' +import { Route as SeparateFileRouteImport } from './routes/separate-file' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const CoLocatedRoute = CoLocatedRouteImport.update({ + id: '/co-located', + path: '/co-located', + getParentRoute: () => rootRouteImport, +} as any) +const CoLocatedCssModuleRoute = CoLocatedCssModuleRouteImport.update({ + id: '/co-located-css-module', + path: '/co-located-css-module', + getParentRoute: () => rootRouteImport, +} as any) +const SeparateFileRoute = SeparateFileRouteImport.update({ + id: '/separate-file', + path: '/separate-file', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/co-located': typeof CoLocatedRoute + '/co-located-css-module': typeof CoLocatedCssModuleRoute + '/separate-file': typeof SeparateFileRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/co-located': typeof CoLocatedRoute + '/co-located-css-module': typeof CoLocatedCssModuleRoute + '/separate-file': typeof SeparateFileRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/co-located': typeof CoLocatedRoute + '/co-located-css-module': typeof CoLocatedCssModuleRoute + '/separate-file': typeof SeparateFileRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/co-located' | '/co-located-css-module' | '/separate-file' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/co-located' | '/co-located-css-module' | '/separate-file' + id: + | '__root__' + | '/' + | '/co-located' + | '/co-located-css-module' + | '/separate-file' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CoLocatedRoute: typeof CoLocatedRoute + CoLocatedCssModuleRoute: typeof CoLocatedCssModuleRoute + SeparateFileRoute: typeof SeparateFileRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/co-located': { + id: '/co-located' + path: '/co-located' + fullPath: '/co-located' + preLoaderRoute: typeof CoLocatedRouteImport + parentRoute: typeof rootRouteImport + } + '/co-located-css-module': { + id: '/co-located-css-module' + path: '/co-located-css-module' + fullPath: '/co-located-css-module' + preLoaderRoute: typeof CoLocatedCssModuleRouteImport + parentRoute: typeof rootRouteImport + } + '/separate-file': { + id: '/separate-file' + path: '/separate-file' + fullPath: '/separate-file' + preLoaderRoute: typeof SeparateFileRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CoLocatedRoute: CoLocatedRoute, + CoLocatedCssModuleRoute: CoLocatedCssModuleRoute, + SeparateFileRoute: SeparateFileRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/react-start/rsc-hmr/src/router.tsx b/e2e/react-start/rsc-hmr/src/router.tsx new file mode 100644 index 0000000000..f67ec4b71f --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/router.tsx @@ -0,0 +1,10 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: true, + }) +} diff --git a/e2e/react-start/rsc-hmr/src/routes/-separateFile.tsx b/e2e/react-start/rsc-hmr/src/routes/-separateFile.tsx new file mode 100644 index 0000000000..25b71526b9 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/-separateFile.tsx @@ -0,0 +1,10 @@ +import { createServerFn } from '@tanstack/react-start' +import { renderServerComponent } from '@tanstack/react-start/rsc' + +export const getSeparateFileContent = createServerFn({ + method: 'GET', +}).handler(async () => { + return renderServerComponent( +

server-rendered content

, + ) +}) diff --git a/e2e/react-start/rsc-hmr/src/routes/__root.tsx b/e2e/react-start/rsc-hmr/src/routes/__root.tsx new file mode 100644 index 0000000000..db80ebce32 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/__root.tsx @@ -0,0 +1,52 @@ +/// +import { + ClientOnly, + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + +

hydrated

+
+ + + + + + ) +} diff --git a/e2e/react-start/rsc-hmr/src/routes/co-located-css-module.tsx b/e2e/react-start/rsc-hmr/src/routes/co-located-css-module.tsx new file mode 100644 index 0000000000..bf9106d581 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/co-located-css-module.tsx @@ -0,0 +1,45 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' +import { renderServerComponent } from '@tanstack/react-start/rsc' +import { useState } from 'react' +import styles from './coLocatedCssModule.module.css' + +// Same co-location as ./co-located.tsx, plus a CSS module import. The upstream +// guard decides whether to suppress client HMR by classifying importers as CSS +// vs non-CSS, so a route that has both kinds of importer is the case most likely +// to regress -- in either direction: the component must still Fast Refresh, and +// a CSS-only edit must still hot-swap without a full reload. +const getServerContent = createServerFn({ method: 'GET' }).handler(async () => { + return renderServerComponent( +

server-rendered content

, + ) +}) + +export const Route = createFileRoute('/co-located-css-module')({ + loader: () => getServerContent(), + component: CoLocatedCssModuleComponent, +}) + +function CoLocatedCssModuleComponent() { + const Server = Route.useLoaderData() + const [count, setCount] = useState(0) + + return ( +
+

+ css-module-baseline +

+ +

Count: {count}

+ + + {Server} +
+ ) +} diff --git a/e2e/react-start/rsc-hmr/src/routes/co-located.tsx b/e2e/react-start/rsc-hmr/src/routes/co-located.tsx new file mode 100644 index 0000000000..0e1bbe9ff8 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/co-located.tsx @@ -0,0 +1,41 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' +import { renderServerComponent } from '@tanstack/react-start/rsc' +import { useState } from 'react' + +// The server function is deliberately co-located with the route component in +// this file. That puts the file in the rsc module graph while the component +// below is still a genuine client module, which is the combination that used to +// lose client HMR entirely (see vitejs/vite-plugin-react#1248). +const getServerContent = createServerFn({ method: 'GET' }).handler(async () => { + return renderServerComponent( +

server-rendered content

, + ) +}) + +export const Route = createFileRoute('/co-located')({ + loader: () => getServerContent(), + component: CoLocatedComponent, +}) + +function CoLocatedComponent() { + const Server = Route.useLoaderData() + const [count, setCount] = useState(0) + + return ( +
+

co-located-baseline

+ +

Count: {count}

+ + + {Server} +
+ ) +} diff --git a/e2e/react-start/rsc-hmr/src/routes/coLocatedCssModule.module.css b/e2e/react-start/rsc-hmr/src/routes/coLocatedCssModule.module.css new file mode 100644 index 0000000000..f3997ffe94 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/coLocatedCssModule.module.css @@ -0,0 +1,3 @@ +.marker { + color: rgb(10, 20, 200); +} diff --git a/e2e/react-start/rsc-hmr/src/routes/index.tsx b/e2e/react-start/rsc-hmr/src/routes/index.tsx new file mode 100644 index 0000000000..bdab705bb0 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return

RSC HMR fixtures

+} diff --git a/e2e/react-start/rsc-hmr/src/routes/separate-file.tsx b/e2e/react-start/rsc-hmr/src/routes/separate-file.tsx new file mode 100644 index 0000000000..352ff3df77 --- /dev/null +++ b/e2e/react-start/rsc-hmr/src/routes/separate-file.tsx @@ -0,0 +1,37 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' +import { getSeparateFileContent } from './-separateFile' + +// Control: identical to ./co-located.tsx except the server function lives in its +// own module. This was the documented workaround, and it kept working throughout, +// so it guards against a fix that only ever exercises the co-located path. +// +// The extracted module deliberately avoids a `.server.` suffix: import protection +// treats those as server-only and replaces them with a mock on the client, which +// breaks the route rather than exercising the workaround. +export const Route = createFileRoute('/separate-file')({ + loader: () => getSeparateFileContent(), + component: SeparateFileComponent, +}) + +function SeparateFileComponent() { + const Server = Route.useLoaderData() + const [count, setCount] = useState(0) + + return ( +
+

separate-file-baseline

+ +

Count: {count}

+ + + {Server} +
+ ) +} diff --git a/e2e/react-start/rsc-hmr/tests/rsc-hmr.spec.ts b/e2e/react-start/rsc-hmr/tests/rsc-hmr.spec.ts new file mode 100644 index 0000000000..6550c798fe --- /dev/null +++ b/e2e/react-start/rsc-hmr/tests/rsc-hmr.spec.ts @@ -0,0 +1,184 @@ +import { expect } from '@playwright/test' +import { createHmrFileEditor, test } from '@tanstack/router-e2e-utils' +import type { Page } from '@playwright/test' + +// HMR assertions get a generous ceiling: on a loaded CI box the client and rsc +// compilers finish on different ticks, so the DOM can lag the file write by +// seconds even when everything is working. +const hmrExpect = expect.configure({ timeout: 20_000 }) + +const MARKERS = { + coLocated: 'co-located-baseline', + cssModule: 'css-module-baseline', + separateFile: 'separate-file-baseline', +} as const + +const CSS_BASELINE_COLOR = 'rgb(10, 20, 200)' + +const routeFileEditor = createHmrFileEditor({ + rootDir: new URL('..', import.meta.url).pathname, + files: { + coLocated: 'src/routes/co-located.tsx', + cssModuleRoute: 'src/routes/co-located-css-module.tsx', + cssModuleStyles: 'src/routes/coLocatedCssModule.module.css', + separateFile: 'src/routes/separate-file.tsx', + }, + // A run killed mid-edit leaves a fixture dirty, which would otherwise cascade + // into every later test. Rewrite each file back to its baseline on capture so + // a crashed previous run is self-healing rather than poisonous. + normalizeSource: (fileKey, source) => { + switch (fileKey) { + case 'coLocated': + return source.replace(/co-located-[\w-]+ { + ;(window as any).__hmrReloadSentinel = 'alive' + }) +} + +async function expectNoReloadHappened(page: Page) { + await expect + .poll(() => page.evaluate(() => (window as any).__hmrReloadSentinel), { + timeout: 5_000, + }) + .toBe('alive') +} + +async function gotoAndWaitForHydration(page: Page, path: string) { + await page.goto(path, { waitUntil: 'domcontentloaded' }) + await page.getByTestId('hydrated').waitFor({ state: 'visible' }) +} + +test.beforeEach(async () => { + await routeFileEditor.capturePromise + await routeFileEditor.restoreFiles() +}) + +test.afterEach(async () => { + await routeFileEditor.capturePromise + await routeFileEditor.restoreFiles() +}) + +test.describe('RSC route component HMR with a co-located server function', () => { + test('Fast Refreshes a co-located route component and preserves state', async ({ + page, + }) => { + await gotoAndWaitForHydration(page, '/co-located') + await expect(page.getByTestId('co-located-server-content')).toHaveText( + 'server-rendered content', + ) + await seedClientState(page, 'co-located') + + await routeFileEditor.replaceText( + 'coLocated', + MARKERS.coLocated, + 'co-located-updated', + ) + + await hmrExpect(page.getByTestId('co-located-marker')).toHaveText( + 'co-located-updated', + ) + await expect(page.getByTestId('co-located-count')).toHaveText('Count: 2') + await expectNoReloadHappened(page) + }) + + test('Fast Refreshes when the route also imports a CSS module', async ({ + page, + }) => { + await gotoAndWaitForHydration(page, '/co-located-css-module') + await seedClientState(page, 'css-module') + + await routeFileEditor.replaceText( + 'cssModuleRoute', + MARKERS.cssModule, + 'css-module-updated', + ) + + await hmrExpect(page.getByTestId('css-module-marker')).toHaveText( + 'css-module-updated', + ) + await expect(page.getByTestId('css-module-count')).toHaveText('Count: 2') + await expectNoReloadHappened(page) + }) + + test('hot-swaps a CSS-module-only edit without reloading the page', async ({ + page, + }) => { + await gotoAndWaitForHydration(page, '/co-located-css-module') + await expect(page.getByTestId('css-module-marker')).toHaveCSS( + 'color', + CSS_BASELINE_COLOR, + ) + await seedClientState(page, 'css-module') + + await routeFileEditor.replaceText( + 'cssModuleStyles', + `color: ${CSS_BASELINE_COLOR};`, + 'color: rgb(220, 30, 40);', + ) + + await hmrExpect(page.getByTestId('css-module-marker')).toHaveCSS( + 'color', + 'rgb(220, 30, 40)', + ) + await expect(page.getByTestId('css-module-count')).toHaveText('Count: 2') + await expectNoReloadHappened(page) + }) + + test('Fast Refreshes a route whose server function lives in its own file', async ({ + page, + }) => { + await gotoAndWaitForHydration(page, '/separate-file') + await expect(page.getByTestId('separate-file-server-content')).toHaveText( + 'server-rendered content', + ) + await seedClientState(page, 'separate-file') + + await routeFileEditor.replaceText( + 'separateFile', + MARKERS.separateFile, + 'separate-file-updated', + ) + + await hmrExpect(page.getByTestId('separate-file-marker')).toHaveText( + 'separate-file-updated', + ) + await expect(page.getByTestId('separate-file-count')).toHaveText('Count: 2') + await expectNoReloadHappened(page) + }) +}) diff --git a/e2e/react-start/rsc-hmr/tests/setup/global.setup.ts b/e2e/react-start/rsc-hmr/tests/setup/global.setup.ts new file mode 100644 index 0000000000..fb9aad22ae --- /dev/null +++ b/e2e/react-start/rsc-hmr/tests/setup/global.setup.ts @@ -0,0 +1,27 @@ +import { + e2eStartDummyServer, + getTestServerPort, + preOptimizeDevServer, + waitForServer, +} from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +function getE2EPortKey() { + const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' + return process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +} + +export default async function setup() { + const e2ePortKey = getE2EPortKey() + + await e2eStartDummyServer(e2ePortKey) + + const port = await getTestServerPort(e2ePortKey) + const baseURL = `http://localhost:${port}` + + await waitForServer(baseURL) + await preOptimizeDevServer({ + baseURL, + readyTestId: 'hydrated', + }) +} diff --git a/e2e/react-start/rsc-hmr/tests/setup/global.teardown.ts b/e2e/react-start/rsc-hmr/tests/setup/global.teardown.ts new file mode 100644 index 0000000000..e17856dd82 --- /dev/null +++ b/e2e/react-start/rsc-hmr/tests/setup/global.teardown.ts @@ -0,0 +1,11 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +function getE2EPortKey() { + const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' + return process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +} + +export default async function teardown() { + await e2eStopDummyServer(getE2EPortKey()) +} diff --git a/e2e/react-start/rsc-hmr/tsconfig.json b/e2e/react-start/rsc-hmr/tsconfig.json new file mode 100644 index 0000000000..cef9369516 --- /dev/null +++ b/e2e/react-start/rsc-hmr/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/react-start/rsc-hmr/vite.config.ts b/e2e/react-start/rsc-hmr/vite.config.ts new file mode 100644 index 0000000000..c0ae311a5d --- /dev/null +++ b/e2e/react-start/rsc-hmr/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' +import rsc from '@vitejs/plugin-rsc' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist' + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + build: { + outDir, + }, + server: { + port: 3000, + }, + plugins: [ + tanstackStart({ + rsc: { + enabled: true, + }, + }), + rsc(), + viteReact(), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be5b8a29f5..044e2f0dda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2791,6 +2791,58 @@ importers: specifier: ^8.0.14 version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + e2e/react-start/rsc-hmr: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@playwright/test': + specifier: ^1.61.0 + version: 1.61.1 + '@rsbuild/core': + specifier: ^2.0.11 + version: 2.0.14 + '@rsbuild/plugin-react': + specifier: ^2.0.0 + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 25.0.9 + version: 25.0.9 + '@types/react': + specifier: ^19.2.8 + version: 19.2.9 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitejs/plugin-rsc': + specifier: ^0.5.30 + version: 0.5.30(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + react-server-dom-rspack: + specifier: ^0.0.2 + version: 0.0.2(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + typescript: + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + e2e/react-start/rsc-query: dependencies: '@tanstack/react-query': @@ -40422,6 +40474,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + react-server-dom-rspack@0.0.2(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@rspack/core': 2.0.8(@swc/helpers@0.5.23) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-style-singleton@2.2.3(@types/react@19.2.9)(react@19.2.3): dependencies: get-nonce: 1.0.1 @@ -42419,7 +42477,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.21.6 - es-module-lexer: 2.0.0 + es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1