Skip to content

feat: allow to configure dev ssr style injection#6797

Merged
schiller-manuel merged 1 commit intomainfrom
config-dev-ssr-styles
Mar 1, 2026
Merged

feat: allow to configure dev ssr style injection#6797
schiller-manuel merged 1 commit intomainfrom
config-dev-ssr-styles

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Mar 1, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added configurable SSR styles support for development mode with options to enable/disable and set custom basepath.
    • Expanded support for testing SSR styles across different runtime configurations.
  • Tests

    • Added comprehensive end-to-end tests validating SSR styles behavior across multiple modes.
    • Included tests ensuring CSS renders correctly in development environments.

@nx-cloud
Copy link

nx-cloud bot commented Mar 1, 2026

View your CI Pipeline Execution ↗ for commit f7623fb

Command Status Duration Result
nx run tanstack-router-e2e-bundle-size:build --... ✅ Succeeded 1m 27s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-01 23:43:05 UTC

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

This PR introduces configurable dev SSR styles support with a new e2e test workspace, schema extensions, and conditional middleware registration. Changes enable toggling dev styles injection via configuration flags and custom basepath support during development.

Changes

Cohort / File(s) Summary
New E2E SSR Styles Workspace
e2e/react-start/dev-ssr-styles/...
Complete React SSR workspace with route definitions, stylesheet imports, Vite/Playwright configuration, environment variables, package scripts for testing across default/Nitro/custom-basepath modes, and e2e tests validating SSR style behavior across three configuration scenarios.
E2E Setup/Teardown Infrastructure
e2e/react-start/split-base-and-basepath/playwright.config.ts, e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts, e2e/react-start/split-base-and-basepath/tests/setup/global.teardown.ts
Adds global lifecycle hooks to Playwright config with server startup/warmup and graceful teardown utilities; refactors CSS import in root route from URL-based to side-effect; updates test assertions to validate stylesheet collection uniformly.
Core Plugin Configuration
packages/start-plugin-core/src/schema.ts, packages/start-plugin-core/src/plugin.ts
Extends TanStackStartOptionsSchema with optional dev.ssrStyles object (enabled/basepath); adds environment replacements TSS_DEV_SSR_STYLES_ENABLED and TSS_DEV_SSR_STYLES_BASEPATH; wires devSsrStylesEnabled flag through devServerPlugin invocation.
Dev Server Middleware
packages/start-plugin-core/src/dev-server-plugin/plugin.ts
Adds devSsrStylesEnabled parameter to guard conditional registration of CSS styles middleware; middleware remains functionally unchanged but is installed only when flag is true.
Router Manifest Updates
packages/start-server-core/src/router-manifest.ts
Replaces ROUTER_BASEPATH with DEV_SSR_STYLES_BASEPATH in dev-styles URL construction; refines conditional dev-styles injection to check both TSS_DEV_SERVER and new TSS_DEV_SSR_STYLES_ENABLED flag.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

package: start-plugin-core, package: start-server-core, ssr

Suggested reviewers

  • nlynzaad
  • birkskyum

Poem

🐰 A new test workspace hops in with style,
Config flags and basepaths all the while,
Dev server warmth and CSS delight,
SSR styles now perfectly aligned just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: allow to configure dev ssr style injection' accurately describes the main changes, which introduce configuration options for dev SSR style injection across multiple files and packages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch config-dev-ssr-styles

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

@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Bundle Size Benchmarks

  • Commit: 5f7e9e872bb0
  • Measured at: 2026-03-01T23:23:26.655Z
  • Baseline source: history:5f7e9e872bb0
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 86.58 KiB 0 B (0.00%) 272.45 KiB 75.22 KiB ▁▇▇████████
react-router.full 89.61 KiB 0 B (0.00%) 282.78 KiB 77.90 KiB ▁▇▇████████
solid-router.minimal 35.88 KiB 0 B (0.00%) 107.56 KiB 32.26 KiB ▁▆▆████████
solid-router.full 40.21 KiB 0 B (0.00%) 120.61 KiB 36.13 KiB ▁▆▆████████
vue-router.minimal 51.75 KiB 0 B (0.00%) 147.54 KiB 46.50 KiB ▁▆▆████████
vue-router.full 56.55 KiB 0 B (0.00%) 163.12 KiB 50.86 KiB ▁▅▅████████
react-start.minimal 99.11 KiB 0 B (0.00%) 311.58 KiB 85.68 KiB ▁▅▅████████
react-start.full 102.50 KiB 0 B (0.00%) 321.39 KiB 88.63 KiB ▁▅▅▇▇▇▇▇███
solid-start.minimal 48.19 KiB 0 B (0.00%) 145.13 KiB 42.67 KiB ▁▆▆████████
solid-start.full 53.68 KiB 0 B (0.00%) 161.08 KiB 47.37 KiB ▁▆▆▇▇▇▇▇███

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 1, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@6797

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@6797

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@6797

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@6797

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@6797

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@6797

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@6797

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@6797

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@6797

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@6797

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@6797

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@6797

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@6797

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@6797

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@6797

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@6797

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@6797

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@6797

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@6797

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@6797

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@6797

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@6797

commit: e80b39f

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: 2

🧹 Nitpick comments (3)
packages/start-plugin-core/src/plugin.ts (1)

423-426: Use parsed config as the single source for devSsrStylesEnabled.

Line 425 reads from startPluginOpts, while Lines 313-314 already use parsed startConfig. Keeping both on parsed config prevents future drift if parsing/defaulting behavior changes.

♻️ Suggested single-source wiring
// packages/start-plugin-core/src/plugin.ts
 devServerPlugin({
   getConfig,
-  devSsrStylesEnabled: startPluginOpts?.dev?.ssrStyles?.enabled ?? true,
+  getDevSsrStylesEnabled: () => getConfig().startConfig.dev.ssrStyles.enabled,
 }),
// packages/start-plugin-core/src/dev-server-plugin/plugin.ts
 export function devServerPlugin({
   getConfig,
-  devSsrStylesEnabled,
+  getDevSsrStylesEnabled,
 }: {
   getConfig: GetConfigFn
-  devSsrStylesEnabled: boolean
+  getDevSsrStylesEnabled: () => boolean
 }): PluginOption {

-        if (devSsrStylesEnabled) {
+        if (getDevSsrStylesEnabled()) {
           // ...
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/start-plugin-core/src/plugin.ts` around lines 423 - 426, The
devServerPlugin call currently reads devSsrStylesEnabled from startPluginOpts
causing divergence from the parsed startConfig used elsewhere; change the
argument to source devSsrStylesEnabled from the parsed startConfig (the same
object used at lines where startConfig is referenced) so devServerPlugin({
getConfig, devSsrStylesEnabled: /* from startConfig */ }) uses startConfig's
value instead of startPluginOpts, keeping startConfig as the single source of
truth for SSR styles defaults; update the devServerPlugin invocation to
reference startConfig and remove reliance on startPluginOpts for this flag.
e2e/react-start/dev-ssr-styles/playwright.config.ts (1)

12-21: Consider extracting getPortKey to a shared helper.

This key builder is duplicated with e2e/react-start/dev-ssr-styles/tests/setup/global.setup.ts (Lines 9-18). A shared helper reduces drift between config/setup/teardown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/dev-ssr-styles/playwright.config.ts` around lines 12 - 21,
The getPortKey function is duplicated; extract it into a shared helper (e.g.,
export function getPortKey(packageJson, ssrStylesMode, useNitro) in a new/shared
module) and replace the local implementations in both getPortKey
(playwright.config.ts) and the duplicate in global.setup.ts to import and call
that helper; ensure the helper accepts the same inputs (packageJson.name,
ssrStylesMode, useNitro) and preserves the existing key-building logic and
export it for reuse so both files import the single source of truth.
packages/start-plugin-core/src/dev-server-plugin/plugin.ts (1)

87-89: Remove the any cast for routes manifest access.

Line 87 bypasses strict typing with (globalThis as any), which makes this path easier to break silently.

♻️ Suggested type-safe replacement
-              const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
-                | Record<string, { filePath: string; children?: Array<string> }>
-                | undefined
+              const routesManifest = globalThis.TSS_ROUTES_MANIFEST
// Add an ambient declaration (preferred in a shared types file)
declare global {
  // eslint-disable-next-line no-var
  var TSS_ROUTES_MANIFEST:
    | Record<string, { filePath: string; children?: Array<string> }>
    | undefined
}

As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/start-plugin-core/src/dev-server-plugin/plugin.ts` around lines 87 -
89, Replace the unsafe cast by declaring a proper ambient global type for
TSS_ROUTES_MANIFEST and then access it without (globalThis as any); add a
declaration like "declare global { var TSS_ROUTES_MANIFEST: Record<string, {
filePath: string; children?: string[] }> | undefined }" in a shared types file
(or project-wide d.ts), remove the "(globalThis as any)" cast and read the
manifest via "const routesManifest = globalThis.TSS_ROUTES_MANIFEST" so the code
using routesManifest has correct compile-time types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/react-start/dev-ssr-styles/vite.config.ts`:
- Around line 7-16: Add an explicit return type for getSsrStylesConfig (e.g., a
union type representing the three possible config shapes) and implement a
never-branch guard that throws if ssrStylesMode is an unhandled SsrStylesMode
value; specifically update function getSsrStylesConfig to declare its return
type, keep the existing cases for 'disabled', 'custom-basepath', and 'default',
then add a final default case that calls a helper like
assertUnreachable(ssrStylesMode) or throws with the unknown value to ensure
future SsrStylesMode additions do not result in undefined returns.

In `@e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts`:
- Around line 30-65: The try/finally currently begins after creating context and
page so if chromium.launch(), browser.newContext(), or context.newPage() throws
those resources may leak; move the browser/newContext/newPage calls inside the
try and use nested try/finally blocks (one surrounding context creation to
ensure context.close() runs, and an inner one surrounding page creation to
ensure page-related waits/actions and then page.close()/context.close()), i.e.,
wrap chromium.launch()/browser in the outer try, create context inside that try
and ensure context.close() in its finally, then create page inside a nested try
and ensure page cleanup in its finally; reference the variables browser,
context, page and the warmup logic so the cleanup always runs even on
construction failure.

---

Nitpick comments:
In `@e2e/react-start/dev-ssr-styles/playwright.config.ts`:
- Around line 12-21: The getPortKey function is duplicated; extract it into a
shared helper (e.g., export function getPortKey(packageJson, ssrStylesMode,
useNitro) in a new/shared module) and replace the local implementations in both
getPortKey (playwright.config.ts) and the duplicate in global.setup.ts to import
and call that helper; ensure the helper accepts the same inputs
(packageJson.name, ssrStylesMode, useNitro) and preserves the existing
key-building logic and export it for reuse so both files import the single
source of truth.

In `@packages/start-plugin-core/src/dev-server-plugin/plugin.ts`:
- Around line 87-89: Replace the unsafe cast by declaring a proper ambient
global type for TSS_ROUTES_MANIFEST and then access it without (globalThis as
any); add a declaration like "declare global { var TSS_ROUTES_MANIFEST:
Record<string, { filePath: string; children?: string[] }> | undefined }" in a
shared types file (or project-wide d.ts), remove the "(globalThis as any)" cast
and read the manifest via "const routesManifest =
globalThis.TSS_ROUTES_MANIFEST" so the code using routesManifest has correct
compile-time types.

In `@packages/start-plugin-core/src/plugin.ts`:
- Around line 423-426: The devServerPlugin call currently reads
devSsrStylesEnabled from startPluginOpts causing divergence from the parsed
startConfig used elsewhere; change the argument to source devSsrStylesEnabled
from the parsed startConfig (the same object used at lines where startConfig is
referenced) so devServerPlugin({ getConfig, devSsrStylesEnabled: /* from
startConfig */ }) uses startConfig's value instead of startPluginOpts, keeping
startConfig as the single source of truth for SSR styles defaults; update the
devServerPlugin invocation to reference startConfig and remove reliance on
startPluginOpts for this flag.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f7e9e8 and f7623fb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • e2e/react-start/dev-ssr-styles/.gitignore
  • e2e/react-start/dev-ssr-styles/.prettierignore
  • e2e/react-start/dev-ssr-styles/env.ts
  • e2e/react-start/dev-ssr-styles/package.json
  • e2e/react-start/dev-ssr-styles/playwright.config.ts
  • e2e/react-start/dev-ssr-styles/src/routeTree.gen.ts
  • e2e/react-start/dev-ssr-styles/src/router.tsx
  • e2e/react-start/dev-ssr-styles/src/routes/__root.tsx
  • e2e/react-start/dev-ssr-styles/src/routes/index.tsx
  • e2e/react-start/dev-ssr-styles/src/styles/app.css
  • e2e/react-start/dev-ssr-styles/tests/app.spec.ts
  • e2e/react-start/dev-ssr-styles/tests/setup/global.setup.ts
  • e2e/react-start/dev-ssr-styles/tests/setup/global.teardown.ts
  • e2e/react-start/dev-ssr-styles/tsconfig.json
  • e2e/react-start/dev-ssr-styles/vite.config.ts
  • e2e/react-start/split-base-and-basepath/playwright.config.ts
  • e2e/react-start/split-base-and-basepath/src/routes/__root.tsx
  • e2e/react-start/split-base-and-basepath/tests/app.spec.ts
  • e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts
  • e2e/react-start/split-base-and-basepath/tests/setup/global.teardown.ts
  • packages/start-plugin-core/src/dev-server-plugin/plugin.ts
  • packages/start-plugin-core/src/plugin.ts
  • packages/start-plugin-core/src/schema.ts
  • packages/start-server-core/src/router-manifest.ts

Comment on lines +7 to +16
function getSsrStylesConfig() {
switch (ssrStylesMode) {
case 'disabled':
return { enabled: false }
case 'custom-basepath':
return { enabled: true, basepath: '/custom-styles/' }
case 'default':
return {} // use defaults (enabled=true, basepath=vite base)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current switch coverage vs SsrStylesMode definition
rg -n -C3 'SsrStylesMode|ssrStylesModeSchema|function getSsrStylesConfig|switch \(ssrStylesMode\)|case ' \
  e2e/react-start/dev-ssr-styles/env.ts \
  e2e/react-start/dev-ssr-styles/vite.config.ts

Repository: TanStack/router

Length of output: 1857


🏁 Script executed:

# Also check TypeScript configuration to verify strict mode
find . -name "tsconfig.json" -o -name "tsconfig*.json" | head -5 | xargs grep -l "strict"

Repository: TanStack/router

Length of output: 266


🏁 Script executed:

# Check the full content of the vite.config.ts file
cat e2e/react-start/dev-ssr-styles/vite.config.ts

Repository: TanStack/router

Length of output: 1244


Make getSsrStylesConfig exhaustively typed.

Add an explicit return type + never guard so future SsrStylesMode additions can't silently return undefined.

♻️ Proposed refactor
+function assertNever(value: never): never {
+  throw new Error(`Unhandled ssrStylesMode: ${String(value)}`)
+}
+
-function getSsrStylesConfig() {
+function getSsrStylesConfig(): { enabled?: boolean; basepath?: string } {
   switch (ssrStylesMode) {
     case 'disabled':
       return { enabled: false }
     case 'custom-basepath':
       return { enabled: true, basepath: '/custom-styles/' }
     case 'default':
       return {} // use defaults (enabled=true, basepath=vite base)
+    default:
+      return assertNever(ssrStylesMode)
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getSsrStylesConfig() {
switch (ssrStylesMode) {
case 'disabled':
return { enabled: false }
case 'custom-basepath':
return { enabled: true, basepath: '/custom-styles/' }
case 'default':
return {} // use defaults (enabled=true, basepath=vite base)
}
}
function assertNever(value: never): never {
throw new Error(`Unhandled ssrStylesMode: ${String(value)}`)
}
function getSsrStylesConfig(): { enabled?: boolean; basepath?: string } {
switch (ssrStylesMode) {
case 'disabled':
return { enabled: false }
case 'custom-basepath':
return { enabled: true, basepath: '/custom-styles/' }
case 'default':
return {} // use defaults (enabled=true, basepath=vite base)
default:
return assertNever(ssrStylesMode)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/dev-ssr-styles/vite.config.ts` around lines 7 - 16, Add an
explicit return type for getSsrStylesConfig (e.g., a union type representing the
three possible config shapes) and implement a never-branch guard that throws if
ssrStylesMode is an unhandled SsrStylesMode value; specifically update function
getSsrStylesConfig to declare its return type, keep the existing cases for
'disabled', 'custom-basepath', and 'default', then add a final default case that
calls a helper like assertUnreachable(ssrStylesMode) or throws with the unknown
value to ensure future SsrStylesMode additions do not result in undefined
returns.

Comment on lines +30 to +65
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()

try {
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('home-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

// Exercise client-side navigation so Vite discovers all deps
await page.getByTestId('link-about').click()
await page.waitForURL('**/about')
await page.getByTestId('about-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

await page.getByTestId('link-home').click()
await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
await page.getByTestId('home-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')

// Ensure we end in a stable state. Vite's optimize step triggers a reload;
// this waits until no further navigations happen for a short window.
for (let i = 0; i < 40; i++) {
const currentUrl = page.url()
await page.waitForTimeout(250)
if (page.url() === currentUrl) {
await page.waitForTimeout(250)
if (page.url() === currentUrl) return
}
}

throw new Error('Dev server did not reach a stable URL after warmup')
} finally {
await context.close()
await browser.close()
}
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

🧩 Analysis chain

🏁 Script executed:

cat -n e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts | head -80

Repository: TanStack/router

Length of output: 3145


Move cleanup boundaries to cover resource creation.

try/finally starts after newContext() and newPage(). If either throws, cleanup is skipped, causing resource leaks. Move both creation calls inside the try block with nested try/finally blocks to ensure each resource is cleaned up at the appropriate level.

🔧 Proposed fix
 async function preOptimizeDevServer(baseURL: string) {
   const browser = await chromium.launch()
-  const context = await browser.newContext()
-  const page = await context.newPage()
-
   try {
+    const context = await browser.newContext()
+    try {
+      const page = await context.newPage()
+
+      await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
+      await page.getByTestId('home-heading').waitFor({ state: 'visible' })
+      await page.waitForLoadState('networkidle')
+
+      // Exercise client-side navigation so Vite discovers all deps
+      await page.getByTestId('link-about').click()
+      await page.waitForURL('**/about')
+      await page.getByTestId('about-heading').waitFor({ state: 'visible' })
+      await page.waitForLoadState('networkidle')
+
+      await page.getByTestId('link-home').click()
+      await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
+      await page.getByTestId('home-heading').waitFor({ state: 'visible' })
+      await page.waitForLoadState('networkidle')
+
+      // Ensure we end in a stable state. Vite's optimize step triggers a reload;
+      // this waits until no further navigations happen for a short window.
+      for (let i = 0; i < 40; i++) {
+        const currentUrl = page.url()
+        await page.waitForTimeout(250)
+        if (page.url() === currentUrl) {
+          await page.waitForTimeout(250)
+          if (page.url() === currentUrl) return
+        }
+      }
+
+      throw new Error('Dev server did not reach a stable URL after warmup')
+    } finally {
+      await context.close()
+    }
-    await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
-    await page.getByTestId('home-heading').waitFor({ state: 'visible' })
-    await page.waitForLoadState('networkidle')
-
-    // Exercise client-side navigation so Vite discovers all deps
-    await page.getByTestId('link-about').click()
-    await page.waitForURL('**/about')
-    await page.getByTestId('about-heading').waitFor({ state: 'visible' })
-    await page.waitForLoadState('networkidle')
-
-    await page.getByTestId('link-home').click()
-    await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
-    await page.getByTestId('home-heading').waitFor({ state: 'visible' })
-    await page.waitForLoadState('networkidle')
-
-    // Ensure we end in a stable state. Vite's optimize step triggers a reload;
-    // this waits until no further navigations happen for a short window.
-    for (let i = 0; i < 40; i++) {
-      const currentUrl = page.url()
-      await page.waitForTimeout(250)
-      if (page.url() === currentUrl) {
-        await page.waitForTimeout(250)
-        if (page.url() === currentUrl) return
-      }
-    }
-
-    throw new Error('Dev server did not reach a stable URL after warmup')
   } finally {
-    await context.close()
     await browser.close()
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
try {
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('home-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
// Exercise client-side navigation so Vite discovers all deps
await page.getByTestId('link-about').click()
await page.waitForURL('**/about')
await page.getByTestId('about-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.getByTestId('link-home').click()
await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
await page.getByTestId('home-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
// Ensure we end in a stable state. Vite's optimize step triggers a reload;
// this waits until no further navigations happen for a short window.
for (let i = 0; i < 40; i++) {
const currentUrl = page.url()
await page.waitForTimeout(250)
if (page.url() === currentUrl) {
await page.waitForTimeout(250)
if (page.url() === currentUrl) return
}
}
throw new Error('Dev server did not reach a stable URL after warmup')
} finally {
await context.close()
await browser.close()
}
const browser = await chromium.launch()
try {
const context = await browser.newContext()
try {
const page = await context.newPage()
await page.goto(`${baseURL}/`, { waitUntil: 'domcontentloaded' })
await page.getByTestId('home-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
// Exercise client-side navigation so Vite discovers all deps
await page.getByTestId('link-about').click()
await page.waitForURL('**/about')
await page.getByTestId('about-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.getByTestId('link-home').click()
await page.waitForURL(/\/([^/]*)(\/)?($|\?)/)
await page.getByTestId('home-heading').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
// Ensure we end in a stable state. Vite's optimize step triggers a reload;
// this waits until no further navigations happen for a short window.
for (let i = 0; i < 40; i++) {
const currentUrl = page.url()
await page.waitForTimeout(250)
if (page.url() === currentUrl) {
await page.waitForTimeout(250)
if (page.url() === currentUrl) return
}
}
throw new Error('Dev server did not reach a stable URL after warmup')
} finally {
await context.close()
}
} finally {
await browser.close()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/split-base-and-basepath/tests/setup/global.setup.ts` around
lines 30 - 65, The try/finally currently begins after creating context and page
so if chromium.launch(), browser.newContext(), or context.newPage() throws those
resources may leak; move the browser/newContext/newPage calls inside the try and
use nested try/finally blocks (one surrounding context creation to ensure
context.close() runs, and an inner one surrounding page creation to ensure
page-related waits/actions and then page.close()/context.close()), i.e., wrap
chromium.launch()/browser in the outer try, create context inside that try and
ensure context.close() in its finally, then create page inside a nested try and
ensure page cleanup in its finally; reference the variables browser, context,
page and the warmup logic so the cleanup always runs even on construction
failure.

@schiller-manuel schiller-manuel merged commit 816d0d1 into main Mar 1, 2026
8 checks passed
@schiller-manuel schiller-manuel deleted the config-dev-ssr-styles branch March 1, 2026 23:45
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.

1 participant