Skip to content

Conversation

@edmundhung
Copy link
Contributor

@edmundhung edmundhung commented Nov 19, 2025

This adds vite preview support with a new preview server plugin that loads the built server bundle and serves SSR requests. Both the react-start and solid-start basic e2e projects have been updated to re-run their test suites in preview mode.

Summary by CodeRabbit

  • New Features

    • Added preview-server integration to enable running built apps in preview mode.
    • Added a runtime preview mode toggle for e2e runs.
  • Tests

    • Added preview-mode e2e test scripts and included preview validation in test orchestration.
    • Test configs updated to support preview-specific ports and execution paths.
  • Chores

    • Added a small runtime helper to detect preview mode from the environment.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

Adds a preview test mode across React and Solid e2e suites with new scripts and preview-aware Playwright ports; introduces an isPreview test utility; broadens Vite config types in output-directory utilities; and adds a new preview-server Vite plugin that lazily loads and proxies the server build during preview.

Changes

Cohort / File(s) Summary
E2E React Start - Package Scripts
e2e/react-start/basic/package.json
Added preview script (vite preview), added test:e2e:preview, and updated test:e2e to include preview tests
E2E React Start - Playwright Config
e2e/react-start/basic/playwright.config.ts
Imported isPreview, added previewModeCommand, and append _preview suffix to PORT when isPreview is true; getCommand() selects preview command accordingly
E2E React Start - Tests
e2e/react-start/basic/tests/redirect.spec.ts, e2e/react-start/basic/tests/utils/isPreview.ts
Added isPreview utility (exports boolean from process.env.MODE === 'preview'); tests append _preview suffix to port/package name when preview mode is active
E2E Solid Start - Package Scripts
e2e/solid-start/basic/package.json
Added preview script (vite preview), added test:e2e:preview, and updated test:e2e to include preview tests
E2E Solid Start - Playwright Config
e2e/solid-start/basic/playwright.config.ts
Imported isPreview, added previewModeCommand, and append _preview suffix to PORT when isPreview is true; getCommand() selects preview command accordingly
E2E Solid Start - Tests
e2e/solid-start/basic/tests/redirect.spec.ts, e2e/solid-start/basic/tests/utils/isPreview.ts
Added isPreview utility; tests append _preview suffix to port/package name when preview mode is active
Core Plugin - Type Broadening
packages/start-plugin-core/src/output-directory.ts
Broadened parameter types: getClientOutputDirectory, getServerOutputDirectory, and internal getOutputDirectory now accept `vite.UserConfig
Core Plugin - Plugin Registration
packages/start-plugin-core/src/plugin.ts
Imported and registered new previewServerPlugin() in core plugin array (placed before capture-bundle stage)
Core Plugin - Preview Server Plugin
packages/start-plugin-core/src/preview-server-plugin/plugin.ts
New plugin exported previewServerPlugin() that registers post-order middleware, lazily imports server build on first request, caches it, proxies requests via serverBuild.fetch(), writes headers/status, sends Node responses, and forwards errors to next()

Sequence Diagram

sequenceDiagram
    participant Client
    participant VitePreview as Vite Preview Server
    participant PreviewPlugin as Preview Server Plugin
    participant ServerBuild as Server Build (dynamic)

    Client->>VitePreview: HTTP Request
    VitePreview->>PreviewPlugin: Invoke middleware (post-order)

    alt Cache miss (first request)
        PreviewPlugin->>PreviewPlugin: Resolve server entry path from output dir
        PreviewPlugin->>ServerBuild: Dynamic import(server entry)
        ServerBuild-->>PreviewPlugin: Return module (cached)
    end

    PreviewPlugin->>ServerBuild: Call fetch(NodeRequest)
    ServerBuild-->>PreviewPlugin: Return WebResponse
    PreviewPlugin->>VitePreview: Write headers & status, send response
    VitePreview-->>Client: HTTP Response

    note right of PreviewPlugin: On error -> call next() to forward
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Heterogeneous changes across e2e infra and core plugin increase review scope.
  • Preview server plugin requires careful review for concurrency, dynamic import caching, request/response fidelity, and middleware ordering.
  • Playwright/port-suffix changes are lower-risk but should be validated end-to-end.

Areas requiring extra attention:

  • packages/start-plugin-core/src/preview-server-plugin/plugin.ts — concurrency on first-request dynamic import, correct path resolution, header handling, and error forwarding to next().
  • packages/start-plugin-core/src/plugin.ts — plugin insertion point relative to capture-bundle and other middleware.
  • Playwright config and tests — ensure port-suffix logic matches how ports are written/read by test harness and preview server.

Possibly related PRs

Suggested labels

ssr, package: react-start, package: solid-start

Suggested reviewers

  • schiller-manuel

Poem

🐰 I hopped in quick to add preview delight,
Lazy-loads on first bounce, proxies through the night;
Tests now wear a _preview crown, ports sing in line,
Types stretch a bit, plugins sit just fine —
A tiny rabbit cheer for this new preview time! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(start-plugin-core): vite preview support' accurately and concisely describes the main change: adding Vite preview support to the start-plugin-core package.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4153eb6 and 5b7b4bf.

📒 Files selected for processing (2)
  • e2e/react-start/basic/package.json (1 hunks)
  • e2e/solid-start/basic/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/react-start/basic/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
e2e/solid-start/basic/package.json (3)

12-12: Simple addition of preview script.

The vite preview command is a standard Vite feature for serving the production build locally. This is correct.


20-20: New preview test script follows established pattern.

The test:e2e:preview script correctly:

  • Clears port lock files (rm -rf port*.txt)
  • Sets MODE=preview environment variable for downstream test utilities to detect preview mode
  • Follows the same structure as test:e2e:spaMode, test:e2e:ssrMode, and test:e2e:prerender

21-21: Updated test:e2e script chains all test modes sequentially.

The updated script now runs all four test scenarios in sequence (spaMode → ssrMode → prerender → preview). This provides comprehensive coverage across all deployment modes.

Note: The full test suite will now take longer to complete (4 sequential Playwright test runs). Ensure this is acceptable for your CI/CD pipeline timing and consider whether parallel execution of independent test modes is viable in the future if needed.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Nov 19, 2025

View your CI Pipeline Execution ↗ for commit 5b7b4bf

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 7m 18s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 16s View ↗

☁️ Nx Cloud last updated this comment at 2025-11-20 00:40:43 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 19, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5910

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5910

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5910

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5910

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5910

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5910

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5910

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5910

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5910

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5910

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5910

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5910

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5910

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5910

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5910

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5910

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5910

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5910

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5910

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5910

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5910

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5910

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5910

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@5910

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5910

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5910

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5910

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5910

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5910

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5910

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5910

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5910

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5910

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5910

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5910

commit: 5b7b4bf

Comment on lines +49 to +53
// Temporary workaround
// Vite preview's compression middleware doesn't support flattened array headers that srvx sets
// Call writeHead() before srvx to avoid corruption
res.setHeaders(webRes.headers)
res.writeHead(webRes.status, webRes.statusText)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context:

Vite preview server uses @polka/compression to handle compression. This middleware overwrites the res.writeHead method, which expects headers to be an object and uses a for...in loop to iterate over them.

However, srvx passes headers as a flattened array to writeHead(). When the middleware's for...in loop runs through an array, it iterates over array indices ('0', '1', '2', ...) instead of the actual header names.

This causes header corruption where a header like:

Content-Type: text/html; charset=utf-8

Becomes:

0: Content-Type
1: text/html; charset=utf-8

The workaround is to call res.writeHead() directly before sendNodeResponse(), which sets res.headersSent = true and causes sendNodeResponse() to skip header writing entirely.

I will try to submit a patch to either Vite or @polka/compression, but we will likely want to keep this workaround for compatibility with earlier versions of Vite.

@edmundhung edmundhung marked this pull request as ready for review November 19, 2025 21:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 292b82a and 4153eb6.

📒 Files selected for processing (11)
  • e2e/react-start/basic/package.json (1 hunks)
  • e2e/react-start/basic/playwright.config.ts (2 hunks)
  • e2e/react-start/basic/tests/redirect.spec.ts (1 hunks)
  • e2e/react-start/basic/tests/utils/isPreview.ts (1 hunks)
  • e2e/solid-start/basic/package.json (1 hunks)
  • e2e/solid-start/basic/playwright.config.ts (2 hunks)
  • e2e/solid-start/basic/tests/redirect.spec.ts (1 hunks)
  • e2e/solid-start/basic/tests/utils/isPreview.ts (1 hunks)
  • packages/start-plugin-core/src/output-directory.ts (1 hunks)
  • packages/start-plugin-core/src/plugin.ts (2 hunks)
  • packages/start-plugin-core/src/preview-server-plugin/plugin.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-02T16:16:24.898Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5732
File: packages/start-client-core/src/client/hydrateStart.ts:6-9
Timestamp: 2025-11-02T16:16:24.898Z
Learning: In packages/start-client-core/src/client/hydrateStart.ts, the `import/no-duplicates` ESLint disable is necessary for imports from `#tanstack-router-entry` and `#tanstack-start-entry` because both aliases resolve to the same placeholder file (`fake-start-entry.js`) in package.json during static analysis, even though they resolve to different files at runtime.

Applied to files:

  • packages/start-plugin-core/src/plugin.ts
  • e2e/solid-start/basic/tests/redirect.spec.ts
  • e2e/react-start/basic/tests/redirect.spec.ts
  • e2e/solid-start/basic/playwright.config.ts
  • e2e/react-start/basic/playwright.config.ts
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • e2e/solid-start/basic/tests/redirect.spec.ts
  • e2e/react-start/basic/tests/redirect.spec.ts
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • e2e/solid-start/basic/tests/redirect.spec.ts
  • e2e/react-start/basic/tests/redirect.spec.ts
  • e2e/solid-start/basic/playwright.config.ts
  • e2e/react-start/basic/playwright.config.ts
🧬 Code graph analysis (9)
e2e/solid-start/basic/tests/utils/isPreview.ts (1)
e2e/react-start/basic/tests/utils/isPreview.ts (1)
  • isPreview (1-1)
packages/start-plugin-core/src/output-directory.ts (1)
packages/start-plugin-core/src/constants.ts (1)
  • VITE_ENVIRONMENT_NAMES (1-6)
packages/start-plugin-core/src/plugin.ts (1)
packages/start-plugin-core/src/preview-server-plugin/plugin.ts (1)
  • previewServerPlugin (8-64)
e2e/solid-start/basic/tests/redirect.spec.ts (2)
e2e/e2e-utils/src/derivePort.ts (1)
  • getTestServerPort (25-27)
e2e/solid-start/basic/tests/utils/isPreview.ts (1)
  • isPreview (1-1)
e2e/react-start/basic/tests/redirect.spec.ts (3)
e2e/e2e-utils/src/derivePort.ts (1)
  • getTestServerPort (25-27)
e2e/react-start/basic/tests/utils/isSpaMode.ts (1)
  • isSpaMode (1-1)
e2e/react-start/basic/tests/utils/isPreview.ts (1)
  • isPreview (1-1)
e2e/react-start/basic/tests/utils/isPreview.ts (1)
e2e/solid-start/basic/tests/utils/isPreview.ts (1)
  • isPreview (1-1)
packages/start-plugin-core/src/preview-server-plugin/plugin.ts (2)
packages/start-plugin-core/src/constants.ts (1)
  • VITE_ENVIRONMENT_NAMES (1-6)
packages/start-plugin-core/src/output-directory.ts (1)
  • getServerOutputDirectory (12-16)
e2e/solid-start/basic/playwright.config.ts (5)
e2e/react-start/server-functions/playwright.config.ts (1)
  • PORT (5-5)
e2e/e2e-utils/src/derivePort.ts (1)
  • getTestServerPort (25-27)
e2e/solid-start/basic/tests/utils/isSpaMode.ts (1)
  • isSpaMode (1-1)
e2e/solid-start/basic/tests/utils/isPreview.ts (1)
  • isPreview (1-1)
e2e/solid-start/basic/tests/utils/isPrerender.ts (1)
  • isPrerender (1-1)
e2e/react-start/basic/playwright.config.ts (4)
e2e/e2e-utils/src/derivePort.ts (1)
  • getTestServerPort (25-27)
e2e/react-start/basic/tests/utils/isSpaMode.ts (1)
  • isSpaMode (1-1)
e2e/react-start/basic/tests/utils/isPreview.ts (1)
  • isPreview (1-1)
e2e/react-start/basic/tests/utils/isPrerender.ts (1)
  • isPrerender (1-1)
🔇 Additional comments (10)
e2e/react-start/basic/tests/redirect.spec.ts (1)

10-18: Preview port key wiring looks consistent

Using isPreview to append the _preview suffix to the getTestServerPort key mirrors the Playwright config and cleanly separates preview ports from SSR/SPA runs. Looks good.

e2e/solid-start/basic/package.json (1)

12-21: Preview scripts align with new test mode

The preview and test:e2e:preview scripts, plus the updated test:e2e chain, match the MODE-based setup and existing patterns for spa/prerender/SSR. No changes needed.

e2e/react-start/basic/tests/utils/isPreview.ts (1)

1-1: MODE-based isPreview flag is straightforward

isPreview mirrors the existing isSpaMode pattern and cleanly expresses preview mode from MODE. Looks good as-is.

e2e/solid-start/basic/tests/utils/isPreview.ts (1)

1-1: Solid isPreview helper matches React counterpart

Same MODE === 'preview' check as in the React tests, keeping the mode handling consistent between suites.

e2e/solid-start/basic/tests/redirect.spec.ts (1)

11-18: Preview-aware port derivation matches Solid config

Including isPreview in the getTestServerPort key ensures the redirect tests hit the same preview-specific port that the Playwright config uses. Looks correct.

e2e/solid-start/basic/playwright.config.ts (1)

8-33: Preview mode integration into Solid Playwright config looks sound

The isPreview flag, _preview-suffixed port key, and previewModeCommand branch in getCommand() fit the existing spa/prerender/SSR pattern and stay in sync with the test expectations. Logging the preview state should also help debug CI runs.

e2e/react-start/basic/playwright.config.ts (1)

8-33: React Playwright preview support is consistent and minimal

The isPreview import, _preview port key, and previewModeCommand hook neatly into the existing mode switch without affecting spa/prerender/SSR behavior. This looks correct and aligned with the tests.

e2e/react-start/basic/package.json (1)

12-21: React preview scripts mirror Solid and fit the new flow

The new preview and test:e2e:preview scripts, plus the extended test:e2e chain, line up with the React Playwright config and isPreview helper. All good.

packages/start-plugin-core/src/plugin.ts (1)

14-14: Preview plugin wiring looks solid.

Importing and registering previewServerPlugin() ahead of the bundle capture keeps the lifecycle intact for preview runs. 👍

Also applies to: 403-403

packages/start-plugin-core/src/output-directory.ts (1)

6-20: Accepting ResolvedConfig here is helpful.

The widened signatures let runtime hooks share the same helper without duplicating logic—looks good.

Comment on lines +52 to +55
res.setHeaders(webRes.headers)
res.writeHead(webRes.status, webRes.statusText)

return sendNodeResponse(res, webRes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard res.setHeaders for older Node runtimes.

During vite preview, many projects still run on Node 18.14 or earlier. On those versions ServerResponse#setHeaders is undefined, so these lines throw and the preview server never responds. Please gate this by checking typeof res.setHeaders === 'function' and fall back to iterating webRes.headers with res.setHeader(...) when it isn’t available.

🤖 Prompt for AI Agents
In packages/start-plugin-core/src/preview-server-plugin/plugin.ts around lines
52 to 55, res.setHeaders may be undefined on older Node versions (e.g., Node
18.14) which causes a crash; guard that call by checking if typeof
res.setHeaders === 'function' and call it when available, otherwise iterate over
webRes.headers and call res.setHeader(headerName, headerValue) for each entry
(normalize headerValue to a string or join arrays with ',') before calling
res.writeHead and returning sendNodeResponse.

Copy link
Member

@SeanCassiere SeanCassiere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it!

We should probably update all the e2e sandboxes to use the preview command, but that might make more sense to ship once this PR lands in, just to avoid huge-PR-syndrome.

@SeanCassiere SeanCassiere changed the title feat: vite preview support feat(start-plugin-core): vite preview support Nov 19, 2025
@birkskyum
Copy link
Member

birkskyum commented Nov 20, 2025

Tested this out, and it seems to work as expected

Copy link
Member

@birkskyum birkskyum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried it out a bit, and it sure does appear to work. Great to get support for this!

@birkskyum birkskyum merged commit 0dcc20e into TanStack:main Nov 20, 2025
6 checks passed
@edmundhung
Copy link
Contributor Author

Thanks for the review @birkskyum @SeanCassiere!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants