Skip to content

Conversation

@birkskyum
Copy link
Member

@birkskyum birkskyum commented Nov 27, 2025

Note

Awaiting fetch handler in nitro v3

Caution

Not compatible with nitro v2 plugin

Adds Nitro v3 support for prerendering using vite.preview()

  • Add prerender-nitro.ts - prerendering implementation using vite.preview() for Nitro builds
  • Register a Nitro module via the plugin.nitro interop (Nitro v3.0.1-alpha.1 feature, so would break for any prior releases incl. v2)
  • Skip default prerendering when Nitro is detected, defer to post-Nitro-build hook

Add e2e tests for Nitro integration:

  • react-start/basic-nitro - SSR + prerendering with Nitro
  • react-start/basic-nitro-spa - SPA shell prerendering with Nitro
  • solid-start/basic-nitro - SSR + prerendering with Nitro
  • solid-start/basic-nitro-spa - SPA shell prerendering with Nitro

closes #5967

might close

Summary by CodeRabbit

  • New Features

    • New React and Solid e2e templates: Nitro-powered SPA/SSR with app shell, routing, error & not-found UIs, SEO helper, PWA manifest, and global styling.
  • Core

    • Core plugin now integrates Nitro-aware prerender support and emits Nitro build info to enable correct preview/prerender flows.
  • Tests

    • Playwright e2e suites added to verify prerendering, server/runtime responses, and client-side navigation.
  • Chores

    • Project configs, package manifests, build/test scripts, PostCSS/Vite/Nitro setup, and ignore rules added.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

Walkthrough

Adds multiple e2e example projects (React & Solid; SPA and Nitro variants) with routes, components, configs, and Playwright tests; and extends start-plugin-core to detect Nitro and support Nitro-backed prerendering alongside Vite preview.

Changes

Cohort / File(s) Summary
React Start SPA example
e2e/react-start/basic-nitro-spa/.gitignore, e2e/react-start/basic-nitro-spa/package.json, e2e/react-start/basic-nitro-spa/playwright.config.ts, e2e/react-start/basic-nitro-spa/postcss.config.mjs, e2e/react-start/basic-nitro-spa/public/site.webmanifest, e2e/react-start/basic-nitro-spa/tsconfig.json, e2e/react-start/basic-nitro-spa/vite.config.ts
New SPA project scaffolding: package manifest, Playwright config, PostCSS/Tailwind config, manifest, TS config, Vite config wired to TanStack Start and Nitro.
React Start SPA app code & tests
e2e/react-start/basic-nitro-spa/src/routes/__root.tsx, .../src/routes/index.tsx, .../src/routes/static.tsx, .../src/components/DefaultCatchBoundary.tsx, .../src/components/NotFound.tsx, .../src/router.tsx, .../src/routeTree.gen.ts, .../src/styles/app.css, .../src/utils/seo.ts, .../tests/app.spec.ts
Adds file-based routes, generated route tree/types, router factory, error/not-found components, SEO util, styles, and Playwright e2e tests for SPA prerender and client navigation.
React Start Nitro example
e2e/react-start/basic-nitro/.gitignore, e2e/react-start/basic-nitro/.prettierignore, e2e/react-start/basic-nitro/package.json, e2e/react-start/basic-nitro/playwright.config.ts, e2e/react-start/basic-nitro/postcss.config.mjs, e2e/react-start/basic-nitro/public/site.webmanifest, e2e/react-start/basic-nitro/tsconfig.json, e2e/react-start/basic-nitro/vite.config.ts
Full-stack Nitro example scaffolding with Playwright config, Vite/Nitro integration, and formatting ignores for generated files.
React Start Nitro app code & tests
e2e/react-start/basic-nitro/src/routes/__root.tsx, .../src/routes/index.tsx, .../src/routes/static.tsx, .../src/components/DefaultCatchBoundary.tsx, .../src/components/NotFound.tsx, .../src/router.tsx, .../src/routeTree.gen.ts, .../src/styles/app.css, .../src/utils/seo.ts, .../tests/app.spec.ts, .../tests/setup/global.setup.ts, .../tests/setup/global.teardown.ts
Adds Nitro-backed routes, generated route tree with typings, router factory, components, styles, SEO util, Playwright tests validating Nitro runtime and prerendered assets, plus test setup/teardown placeholders.
Solid Start SPA example
e2e/solid-start/basic-nitro-spa/.gitignore, e2e/solid-start/basic-nitro-spa/package.json, e2e/solid-start/basic-nitro-spa/playwright.config.ts, e2e/solid-start/basic-nitro-spa/postcss.config.mjs, e2e/solid-start/basic-nitro-spa/public/site.webmanifest, e2e/solid-start/basic-nitro-spa/tsconfig.json, e2e/solid-start/basic-nitro-spa/vite.config.ts
New Solid Start SPA project scaffolding: package manifest, Playwright config, PostCSS/Tailwind, TS config, and Vite + Solid plugin wiring.
Solid Start SPA app code & tests
e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx, .../src/routes/index.tsx, .../src/routes/static.tsx, .../src/components/DefaultCatchBoundary.tsx, .../src/components/NotFound.tsx, .../src/router.tsx, .../src/routeTree.gen.ts, .../src/styles/app.css, .../src/utils/seo.ts, .../tests/app.spec.ts
Adds Solid file-based routes, generated route tree/types, router factory, error/not-found components, SEO util, styles, and Playwright e2e tests for SPA prerender/navigation.
Solid Start Nitro example
e2e/solid-start/basic-nitro/.gitignore, e2e/solid-start/basic-nitro/.prettierignore, e2e/solid-start/basic-nitro/package.json, e2e/solid-start/basic-nitro/playwright.config.ts, e2e/solid-start/basic-nitro/postcss.config.mjs, e2e/solid-start/basic-nitro/public/site.webmanifest, e2e/solid-start/basic-nitro/tsconfig.json, e2e/solid-start/basic-nitro/vite.config.ts
Full-stack Solid + Nitro example scaffolding with Playwright config, Vite/Nitro plugin setup, and Prettier ignore for generated route trees.
Solid Start Nitro app code & tests
e2e/solid-start/basic-nitro/src/routes/__root.tsx, .../src/routes/index.tsx, .../src/routes/static.tsx, .../src/components/DefaultCatchBoundary.tsx, .../src/components/NotFound.tsx, .../src/router.tsx, .../src/routeTree.gen.ts, .../src/styles/app.css, .../src/utils/seo.ts, .../tests/app.spec.ts
Adds Solid Nitro routes, generated route tree with type augmentations, router factory, components, styles, SEO helper, and Playwright tests for prerendered static pages and client navigation.
Start Plugin Core — prerender & Nitro integration
packages/start-plugin-core/src/plugin.ts, packages/start-plugin-core/src/post-server-build.ts, packages/start-plugin-core/src/prerender.ts
Adds viteConfigFile to resolved config and configResolved hook; detects Nitro plugin presence; introduces Nitro-specific prerender plugin and postServerBuildForNitro; refactors postServerBuild to accept skipPrerender; updates prerender to optionally start a Nitro subprocess (startNitroServer) or use Vite preview, plus improved redirect/cleanup handling and nitro.json write-out.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Vite as Vite (build/config)
participant Plugin as tanstack-start-core plugin
participant Nitro as Nitro server (child process)
participant Prerender as Prerender worker
participant FS as Filesystem (.output)

Vite->>Plugin: configResolved (configFile)
Vite->>Plugin: build finished
Plugin->>Plugin: detect Nitro plugin (plugin.name startsWith "nitro:")
Plugin->>Prerender: invoke postServerBuildForNitro(startConfig, outputDir, configFile)
Prerender->>Nitro: startNitroServer(nitroServerPath, PORT)
Nitro-->>Prerender: ready (base URL)
Prerender->>Nitro: crawl pages (HTTP requests)
Prerender->>FS: write prerendered files into outputDir
Prerender->>Nitro: close()
Prerender-->>Plugin: prerender complete / sitemap generation

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • packages/start-plugin-core/src/prerender.ts — Nitro subprocess orchestration, waitForServerReady, redirect handling, cleanup.
    • packages/start-plugin-core/src/plugin.ts — configResolved hook, viteConfigFile propagation, Nitro detection and plugin lifecycle changes.
    • packages/start-plugin-core/src/post-server-build.ts — postServerBuildForNitro, skipPrerender propagation, nitro.json write-out.
    • Generated routeTree.gen.ts files across examples — type augmentations, consistent IDs/parent relationships.
    • Playwright webServer configs in e2e examples — port resolution, reuseExistingServer behavior, and server start commands.

Possibly related PRs

Suggested labels

ssr

Suggested reviewers

  • schiller-manuel

Poem

🐰 I hopped through routes and built a plot,
Brought Nitro warmth and prerendered lot.
React and Solid, tests all in a row,
Pages pre-made, and devtools a-glow.
Hooray — carrots for everyone, let's go! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% 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
Title check ✅ Passed The title 'nitro with vite preview' directly refers to the main purpose of the PR, which adds Nitro v3 support for prerendering using vite.preview().
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nitro-with-vite-preview

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 27, 2025

View your CI Pipeline Execution ↗ for commit b17e4b6

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

☁️ Nx Cloud last updated this comment at 2025-11-28 01:41:56 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 27, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: b17e4b6

@birkskyum birkskyum marked this pull request as ready for review November 28, 2025 00:08
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 (17)
packages/start-plugin-core/src/prerender-nitro.ts (4)

28-82: Consider extracting shared prerender configuration logic.

The prerender configuration setup (lines 28-61) duplicates logic from post-server-build.ts. Consider extracting a shared helper to reduce maintenance burden:

// In a shared utility file:
export function setupPrerenderConfig(startConfig: TanStackStartOutputConfig): void {
  // Shared prerender.enabled and SPA page logic
}

This would centralize the prerender setup logic and prevent divergence between the standard and Nitro prerender paths.


86-101: Hardcoded Nitro version may become stale.

The versions: { nitro: '3.0.0' } on line 94 is hardcoded. Consider deriving this from the actual Nitro package version or making it configurable to prevent the metadata from becoming misleading over time.


151-162: Hardcoded shutdown timeout may be insufficient or excessive.

The 3500ms wait on line 157 is arbitrary. Consider:

  1. Making this configurable via startConfig.prerender.shutdownTimeout
  2. Or implementing a more robust shutdown detection (e.g., polling for process termination)

The current approach may fail on slower systems or waste time on faster ones.


165-190: Consider failing instead of proceeding when server isn't ready.

On line 187-189, the code logs a warning but proceeds when the server doesn't become ready. This could lead to confusing downstream failures during prerendering. Consider making this a hard failure or at least configurable:

-  logger.warn(
-    `Server may not be fully ready after ${maxAttempts * delay}ms, proceeding anyway...`,
-  )
+  throw new Error(
+    `Server did not become ready after ${maxAttempts * delay}ms. Check that the Nitro build completed successfully.`,
+  )
e2e/solid-start/basic-nitro/src/styles/app.css (1)

27-29: Confirm .using-mouse focus handling to avoid a11y regressions

Removing outlines with .using-mouse * { outline: none !important; } can hide focus indicators if that class is ever present for keyboard users. Make sure the .using-mouse class is only applied for true pointer/mouse interaction, or consider narrowing the selector (e.g., to specific components) so keyboard focus remains clearly visible.

e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx (1)

1-25: Tighten children typing instead of using any

To keep TS strictness and avoid any, you can type children using Solid’s JSX types:

-import { Link } from '@tanstack/solid-router'
+import type { JSX } from 'solid-js'
+import { Link } from '@tanstack/solid-router'
 
-export function NotFound({ children }: { children?: any }) {
+export function NotFound({ children }: { children?: JSX.Element }) {

This keeps the component strongly typed without changing behavior.

e2e/react-start/basic-nitro/src/components/NotFound.tsx (1)

1-25: Use ReactNode instead of any for children

For stricter typing, you can avoid any by typing children as a React node:

-import { Link } from '@tanstack/react-router'
+import type { ReactNode } from 'react'
+import { Link } from '@tanstack/react-router'
 
-export function NotFound({ children }: { children?: any }) {
+export function NotFound({ children }: { children?: ReactNode }) {

This keeps the component API the same but aligns better with TypeScript strict mode.

e2e/solid-start/basic-nitro-spa/src/utils/seo.ts (1)

1-33: Consider filtering out metas with empty/undefined content

description and keywords are optional but their metas are always emitted, so you may end up with tags like <meta name="description" content="undefined"> if callers omit them. A small filter pass over tags (e.g. dropping entries with content == null or empty string) would keep the head cleaner, especially in tests that validate meta tags.

e2e/solid-start/basic-nitro/src/routes/__root.tsx (1)

16-94: Move HeadContent into <head> and consider adding lang on <html>

Right now <HeadContent /> is rendered inside <body>, while it’s intended to manage <head> tags. Moving it into the <head> block (alongside HydrationScript) will produce more correct HTML and better reflect recommended usage. You may also want to add a lang attribute on <html> (e.g. <html lang="en">) for accessibility/SEO completeness in this reference fixture.

e2e/solid-start/basic-nitro-spa/src/styles/app.css (1)

1-30: Be mindful of focus outlines with .using-mouse helper

The .using-mouse * { outline: none !important; } rule is fine if you reliably toggle .using-mouse only for true mouse interaction. Just ensure keyboard users never land in a state where focus outlines are suppressed, or consider scoping this more narrowly if you reuse the pattern beyond tests.

e2e/react-start/basic-nitro-spa/src/utils/seo.ts (1)

12-30: Consider filtering tags with undefined content values.

When optional parameters (description, keywords) are not provided, the tags array will include entries with content: undefined. Depending on how the meta tags are rendered, this could result in attributes like content="undefined" in the HTML.

Apply this refactor to filter out undefined values:

 export const seo = ({
   title,
   description,
   keywords,
   image,
 }: {
   title: string
   description?: string
   image?: string
   keywords?: string
 }) => {
   const tags = [
     { title },
-    { name: 'description', content: description },
-    { name: 'keywords', content: keywords },
+    ...(description ? [{ name: 'description', content: description }] : []),
+    ...(keywords ? [{ name: 'keywords', content: keywords }] : []),
     { name: 'twitter:title', content: title },
-    { name: 'twitter:description', content: description },
+    ...(description ? [{ name: 'twitter:description', content: description }] : []),
     { name: 'twitter:creator', content: '@tannerlinsley' },
     { name: 'twitter:site', content: '@tannerlinsley' },
     { name: 'og:type', content: 'website' },
     { name: 'og:title', content: title },
-    { name: 'og:description', content: description },
+    ...(description ? [{ name: 'og:description', content: description }] : []),
     ...(image
       ? [
           { name: 'twitter:image', content: image },
           { name: 'twitter:card', content: 'summary_large_image' },
           { name: 'og:image', content: image },
         ]
       : []),
   ]

   return tags
 }
e2e/react-start/basic-nitro/src/utils/seo.ts (1)

1-33: Note: Code duplication across e2e fixtures.

This SEO utility is duplicated across multiple e2e test directories (basic-nitro and basic-nitro-spa). While this keeps each test self-contained, it creates maintenance overhead if changes are needed.

The same undefined content value issue noted in e2e/react-start/basic-nitro-spa/src/utils/seo.ts applies here.

e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx (1)

31-34: Consider adding manifest link.

The project includes a site.webmanifest file, but the root route's links array doesn't reference it. While this won't break functionality, including the manifest link would complete the PWA setup.

 links: [
   { rel: 'stylesheet', href: appCss },
   { rel: 'icon', href: '/favicon.ico' },
+  { rel: 'manifest', href: '/site.webmanifest' },
 ],
e2e/react-start/basic-nitro-spa/src/routes/__root.tsx (1)

30-33: Consider adding manifest link.

Similar to the Solid Start version, this root route could reference the site.webmanifest file to complete the PWA configuration.

 links: [
   { rel: 'stylesheet', href: appCss },
   { rel: 'icon', href: '/favicon.ico' },
+  { rel: 'manifest', href: '/site.webmanifest' },
 ],
e2e/solid-start/basic-nitro/src/routes/index.tsx (1)

4-26: Index route + server function are wired correctly

The file-based route, createServerFn usage, and Route.useLoaderData() pattern all look correct for Solid Start + Nitro. If you want to trim a tiny bit of boilerplate, you could change the route definition to:

export const Route = createFileRoute('/')({
  loader: getData,
  component: Home,
})

but the current version is perfectly fine.

e2e/solid-start/basic-nitro-spa/src/routes/static.tsx (1)

4-26: Static route is well-structured for prerender tests

The /static route cleanly separates the server function (getStaticData) from the component and exposes stable selectors via data-testid for Playwright. As with the index route, you could inline loader: getStaticData instead of () => getStaticData(), but the current implementation is already clear.

e2e/react-start/basic-nitro/src/routes/__root.tsx (1)

15-92: Root route + shell are correctly wired; optional hex color tweak

The root route setup (head metadata, error/not-found components, and RootDocument shell) looks correct and consistent with other React Start examples. One tiny nit: if that color field on the manifest link is meant to be a hex color, you probably want '#ffffff' instead of '#fffff', though it won’t affect the tests.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af02265 and 8162cc6.

⛔ Files ignored due to path filters (29)
  • e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro-spa/public/favicon-16x16.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro-spa/public/favicon-32x32.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro-spa/public/favicon.ico is excluded by !**/*.ico
  • e2e/react-start/basic-nitro-spa/public/favicon.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro/public/android-chrome-192x192.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro/public/android-chrome-512x512.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro/public/apple-touch-icon.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro/public/favicon-16x16.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro/public/favicon-32x32.png is excluded by !**/*.png
  • e2e/react-start/basic-nitro/public/favicon.ico is excluded by !**/*.ico
  • e2e/react-start/basic-nitro/public/favicon.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro-spa/public/favicon.ico is excluded by !**/*.ico
  • e2e/solid-start/basic-nitro-spa/public/favicon.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro/public/android-chrome-192x192.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro/public/android-chrome-512x512.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro/public/apple-touch-icon.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro/public/favicon-16x16.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro/public/favicon-32x32.png is excluded by !**/*.png
  • e2e/solid-start/basic-nitro/public/favicon.ico is excluded by !**/*.ico
  • e2e/solid-start/basic-nitro/public/favicon.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (75)
  • e2e/react-start/basic-nitro-spa/.gitignore (1 hunks)
  • e2e/react-start/basic-nitro-spa/package.json (1 hunks)
  • e2e/react-start/basic-nitro-spa/playwright.config.ts (1 hunks)
  • e2e/react-start/basic-nitro-spa/postcss.config.mjs (1 hunks)
  • e2e/react-start/basic-nitro-spa/public/site.webmanifest (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/router.tsx (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/routes/index.tsx (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/styles/app.css (1 hunks)
  • e2e/react-start/basic-nitro-spa/src/utils/seo.ts (1 hunks)
  • e2e/react-start/basic-nitro-spa/tests/app.spec.ts (1 hunks)
  • e2e/react-start/basic-nitro-spa/tsconfig.json (1 hunks)
  • e2e/react-start/basic-nitro-spa/vite.config.ts (1 hunks)
  • e2e/react-start/basic-nitro/.gitignore (1 hunks)
  • e2e/react-start/basic-nitro/.prettierignore (1 hunks)
  • e2e/react-start/basic-nitro/package.json (1 hunks)
  • e2e/react-start/basic-nitro/playwright.config.ts (1 hunks)
  • e2e/react-start/basic-nitro/postcss.config.mjs (1 hunks)
  • e2e/react-start/basic-nitro/public/site.webmanifest (1 hunks)
  • e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx (1 hunks)
  • e2e/react-start/basic-nitro/src/components/NotFound.tsx (1 hunks)
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts (1 hunks)
  • e2e/react-start/basic-nitro/src/router.tsx (1 hunks)
  • e2e/react-start/basic-nitro/src/routes/__root.tsx (1 hunks)
  • e2e/react-start/basic-nitro/src/routes/index.tsx (1 hunks)
  • e2e/react-start/basic-nitro/src/routes/static.tsx (1 hunks)
  • e2e/react-start/basic-nitro/src/styles/app.css (1 hunks)
  • e2e/react-start/basic-nitro/src/utils/seo.ts (1 hunks)
  • e2e/react-start/basic-nitro/tests/app.spec.ts (1 hunks)
  • e2e/react-start/basic-nitro/tests/setup/global.setup.ts (1 hunks)
  • e2e/react-start/basic-nitro/tests/setup/global.teardown.ts (1 hunks)
  • e2e/react-start/basic-nitro/tsconfig.json (1 hunks)
  • e2e/react-start/basic-nitro/vite.config.ts (1 hunks)
  • e2e/solid-start/basic-nitro-spa/.gitignore (1 hunks)
  • e2e/solid-start/basic-nitro-spa/package.json (1 hunks)
  • e2e/solid-start/basic-nitro-spa/playwright.config.ts (1 hunks)
  • e2e/solid-start/basic-nitro-spa/postcss.config.mjs (1 hunks)
  • e2e/solid-start/basic-nitro-spa/public/site.webmanifest (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/router.tsx (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/routes/static.tsx (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/styles/app.css (1 hunks)
  • e2e/solid-start/basic-nitro-spa/src/utils/seo.ts (1 hunks)
  • e2e/solid-start/basic-nitro-spa/tests/app.spec.ts (1 hunks)
  • e2e/solid-start/basic-nitro-spa/tsconfig.json (1 hunks)
  • e2e/solid-start/basic-nitro-spa/vite.config.ts (1 hunks)
  • e2e/solid-start/basic-nitro/.gitignore (1 hunks)
  • e2e/solid-start/basic-nitro/.prettierignore (1 hunks)
  • e2e/solid-start/basic-nitro/package.json (1 hunks)
  • e2e/solid-start/basic-nitro/playwright.config.ts (1 hunks)
  • e2e/solid-start/basic-nitro/postcss.config.mjs (1 hunks)
  • e2e/solid-start/basic-nitro/public/site.webmanifest (1 hunks)
  • e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx (1 hunks)
  • e2e/solid-start/basic-nitro/src/components/NotFound.tsx (1 hunks)
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts (1 hunks)
  • e2e/solid-start/basic-nitro/src/router.tsx (1 hunks)
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx (1 hunks)
  • e2e/solid-start/basic-nitro/src/routes/index.tsx (1 hunks)
  • e2e/solid-start/basic-nitro/src/routes/static.tsx (1 hunks)
  • e2e/solid-start/basic-nitro/src/styles/app.css (1 hunks)
  • e2e/solid-start/basic-nitro/src/utils/seo.ts (1 hunks)
  • e2e/solid-start/basic-nitro/tests/app.spec.ts (1 hunks)
  • e2e/solid-start/basic-nitro/tsconfig.json (1 hunks)
  • e2e/solid-start/basic-nitro/vite.config.ts (1 hunks)
  • packages/start-plugin-core/src/plugin.ts (1 hunks)
  • packages/start-plugin-core/src/post-server-build.ts (2 hunks)
  • packages/start-plugin-core/src/prerender-nitro.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • e2e/react-start/basic-nitro/tests/setup/global.teardown.ts
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/tests/app.spec.ts
  • e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx
  • e2e/react-start/basic-nitro/playwright.config.ts
  • e2e/solid-start/basic-nitro/src/components/NotFound.tsx
  • e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx
  • e2e/react-start/basic-nitro/src/components/NotFound.tsx
  • e2e/react-start/basic-nitro-spa/playwright.config.ts
  • e2e/react-start/basic-nitro/src/router.tsx
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/router.tsx
  • e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
  • e2e/react-start/basic-nitro-spa/vite.config.ts
  • e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx
  • e2e/solid-start/basic-nitro-spa/src/utils/seo.ts
  • packages/start-plugin-core/src/post-server-build.ts
  • e2e/solid-start/basic-nitro-spa/vite.config.ts
  • e2e/solid-start/basic-nitro/tests/app.spec.ts
  • e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/solid-start/basic-nitro/src/utils/seo.ts
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/react-start/basic-nitro/src/utils/seo.ts
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro-spa/src/router.tsx
  • e2e/react-start/basic-nitro-spa/tests/app.spec.ts
  • e2e/solid-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routes/static.tsx
  • e2e/solid-start/basic-nitro/playwright.config.ts
  • e2e/solid-start/basic-nitro/src/router.tsx
  • packages/start-plugin-core/src/prerender-nitro.ts
  • e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx
  • e2e/react-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/playwright.config.ts
  • e2e/solid-start/basic-nitro/vite.config.ts
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/react-start/basic-nitro-spa/src/utils/seo.ts
  • e2e/solid-start/basic-nitro-spa/tests/app.spec.ts
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro/vite.config.ts
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/tests/setup/global.setup.ts
  • packages/start-plugin-core/src/plugin.ts
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Use workspace protocol for internal dependencies (workspace:*)

Files:

  • e2e/react-start/basic-nitro/package.json
  • e2e/react-start/basic-nitro-spa/package.json
  • e2e/solid-start/basic-nitro/package.json
  • e2e/solid-start/basic-nitro-spa/package.json
**/src/routes/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use file-based routing in src/routes/ directories or code-based routing with route definitions

Files:

  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
🧠 Learnings (11)
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to packages/react-router/**/*.{ts,tsx} : React Router components and hooks should use the tanstack/react-router package

Applied to files:

  • e2e/react-start/basic-nitro/package.json
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx
  • e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx
  • e2e/react-start/basic-nitro-spa/package.json
  • e2e/react-start/basic-nitro/src/components/NotFound.tsx
  • e2e/react-start/basic-nitro/src/router.tsx
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/router.tsx
  • e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro-spa/src/router.tsx
  • e2e/solid-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/router.tsx
  • e2e/react-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts
📚 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:

  • e2e/react-start/basic-nitro/package.json
  • e2e/react-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro-spa/package.json
  • e2e/react-start/basic-nitro-spa/tsconfig.json
  • e2e/react-start/basic-nitro/tsconfig.json
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/tsconfig.json
  • e2e/solid-start/basic-nitro-spa/tsconfig.json
  • e2e/solid-start/basic-nitro/package.json
  • e2e/solid-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/package.json
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to packages/solid-router/**/*.{ts,tsx} : Solid Router components and primitives should use the tanstack/solid-router package

Applied to files:

  • e2e/react-start/basic-nitro/package.json
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro-spa/package.json
  • e2e/react-start/basic-nitro/src/router.tsx
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/router.tsx
  • e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
  • e2e/solid-start/basic-nitro-spa/vite.config.ts
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro-spa/src/router.tsx
  • e2e/solid-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routes/static.tsx
  • e2e/solid-start/basic-nitro/tsconfig.json
  • e2e/solid-start/basic-nitro/src/router.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/tsconfig.json
  • e2e/solid-start/basic-nitro/package.json
  • e2e/solid-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/package.json
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts
📚 Learning: 2025-10-01T18:31:35.420Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: e2e/react-start/custom-basepath/src/routeTree.gen.ts:58-61
Timestamp: 2025-10-01T18:31:35.420Z
Learning: Do not review files named `routeTree.gen.ts` in TanStack Router repositories, as these are autogenerated files that should not be manually modified.

Applied to files:

  • e2e/react-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/router.tsx
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/src/router.tsx
  • e2e/solid-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routeTree.gen.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/react-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/tests/app.spec.ts
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/router.tsx
  • e2e/solid-start/basic-nitro/tests/app.spec.ts
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro-spa/src/router.tsx
  • e2e/react-start/basic-nitro-spa/tests/app.spec.ts
  • e2e/solid-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routes/static.tsx
  • e2e/solid-start/basic-nitro/src/router.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/tests/app.spec.ts
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to **/src/routes/**/*.{ts,tsx} : Use file-based routing in src/routes/ directories or code-based routing with route definitions

Applied to files:

  • e2e/react-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/router.tsx
  • e2e/react-start/basic-nitro/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/router.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
  • e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/react-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro-spa/src/router.tsx
  • e2e/solid-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routes/static.tsx
  • e2e/solid-start/basic-nitro/src/router.tsx
  • e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
  • e2e/react-start/basic-nitro-spa/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/.prettierignore
  • e2e/react-start/basic-nitro/src/routes/index.tsx
  • e2e/solid-start/basic-nitro/src/routeTree.gen.ts
  • e2e/solid-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routes/__root.tsx
  • e2e/react-start/basic-nitro/src/routeTree.gen.ts
📚 Learning: 2025-10-14T18:59:33.990Z
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.

Applied to files:

  • e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
📚 Learning: 2025-10-09T12:59:02.129Z
Learnt from: hokkyss
Repo: TanStack/router PR: 5418
File: e2e/react-start/custom-identifier-prefix/src/styles/app.css:19-21
Timestamp: 2025-10-09T12:59:02.129Z
Learning: In e2e test directories (paths containing `e2e/`), accessibility concerns like outline suppression patterns are less critical since the code is for testing purposes, not production use.

Applied to files:

  • e2e/react-start/basic-nitro/tests/app.spec.ts
📚 Learning: 2025-10-09T12:59:14.842Z
Learnt from: hokkyss
Repo: TanStack/router PR: 5418
File: e2e/react-start/custom-identifier-prefix/public/site.webmanifest:2-3
Timestamp: 2025-10-09T12:59:14.842Z
Learning: In e2e test fixtures (files under e2e directories), empty or placeholder values in configuration files like site.webmanifest are acceptable and should not be flagged unless the test specifically validates those fields.

Applied to files:

  • e2e/react-start/basic-nitro/tests/app.spec.ts
  • e2e/react-start/basic-nitro/playwright.config.ts
  • e2e/react-start/basic-nitro-spa/playwright.config.ts
  • e2e/react-start/basic-nitro-spa/public/site.webmanifest
  • e2e/solid-start/basic-nitro/playwright.config.ts
  • e2e/solid-start/basic-nitro/package.json
  • e2e/solid-start/basic-nitro/public/site.webmanifest
  • e2e/react-start/basic-nitro/public/site.webmanifest
  • e2e/solid-start/basic-nitro-spa/playwright.config.ts
  • e2e/solid-start/basic-nitro-spa/public/site.webmanifest
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to **/package.json : Use workspace protocol for internal dependencies (workspace:*)

Applied to files:

  • e2e/react-start/basic-nitro-spa/package.json
  • e2e/solid-start/basic-nitro-spa/package.json
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript strict mode with extensive type safety throughout the codebase

Applied to files:

  • e2e/react-start/basic-nitro-spa/tsconfig.json
  • e2e/react-start/basic-nitro/tsconfig.json
  • e2e/solid-start/basic-nitro/tsconfig.json
  • e2e/solid-start/basic-nitro-spa/tsconfig.json
🧬 Code graph analysis (17)
e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx (2)
e2e/react-start/server-routes/src/components/NotFound.tsx (1)
  • NotFound (3-25)
e2e/react-start/basic-cloudflare/src/components/NotFound.tsx (1)
  • NotFound (3-25)
e2e/react-start/basic-nitro/playwright.config.ts (1)
scripts/set-ts-version.js (1)
  • packageJson (33-33)
e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx (2)
e2e/react-start/server-routes/src/components/DefaultCatchBoundary.tsx (1)
  • DefaultCatchBoundary (10-53)
examples/react/start-basic-cloudflare/src/components/DefaultCatchBoundary.tsx (1)
  • DefaultCatchBoundary (10-53)
e2e/react-start/basic-nitro-spa/playwright.config.ts (1)
scripts/set-ts-version.js (1)
  • packageJson (33-33)
e2e/react-start/basic-nitro/src/router.tsx (2)
e2e/react-start/basic-cloudflare/src/router.tsx (1)
  • getRouter (6-16)
e2e/solid-start/basic-tsr-config/src/app/router.tsx (1)
  • createRouter (4-11)
e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx (2)
packages/router-core/src/router.ts (1)
  • state (1088-1090)
e2e/react-start/server-routes/src/components/DefaultCatchBoundary.tsx (1)
  • DefaultCatchBoundary (10-53)
e2e/solid-start/basic-nitro-spa/src/utils/seo.ts (1)
scripts/llms-generate.mjs (1)
  • title (101-101)
packages/start-plugin-core/src/post-server-build.ts (1)
packages/start-plugin-core/src/schema.ts (1)
  • TanStackStartOutputConfig (207-207)
e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx (2)
examples/solid/start-basic-nitro/src/components/DefaultCatchBoundary.tsx (1)
  • DefaultCatchBoundary (10-53)
e2e/solid-start/server-routes/src/components/DefaultCatchBoundary.tsx (1)
  • DefaultCatchBoundary (10-53)
e2e/solid-start/basic-nitro-spa/src/routes/index.tsx (2)
examples/solid/kitchen-sink/src/useMutation.tsx (1)
  • data (47-49)
packages/router-core/src/ssr/tsrScript.ts (1)
  • p (11-13)
e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx (4)
e2e/solid-start/server-functions/src/routes/__root.tsx (1)
  • RootComponent (33-47)
e2e/solid-start/custom-basepath/src/routes/__root.tsx (1)
  • RootComponent (56-114)
e2e/solid-start/serialization-adapters/src/routes/__root.tsx (1)
  • RootDocument (35-74)
e2e/solid-start/website/src/routes/__root.tsx (1)
  • RootComponent (59-73)
e2e/solid-start/basic-nitro/src/utils/seo.ts (1)
examples/solid/start-basic-nitro/src/utils/seo.ts (1)
  • title (1-33)
e2e/solid-start/basic-nitro-spa/src/router.tsx (1)
examples/solid/start-basic-nitro/src/router.tsx (1)
  • getRouter (6-16)
packages/start-plugin-core/src/prerender-nitro.ts (3)
packages/start-plugin-core/src/schema.ts (2)
  • TanStackStartOutputConfig (207-207)
  • Page (202-202)
packages/router-core/src/route.ts (1)
  • path (1553-1555)
packages/history/src/index.ts (1)
  • location (162-164)
e2e/solid-start/basic-nitro-spa/src/routes/static.tsx (1)
examples/solid/kitchen-sink/src/useMutation.tsx (1)
  • data (47-49)
e2e/solid-start/basic-nitro-spa/playwright.config.ts (1)
scripts/set-ts-version.js (1)
  • packageJson (33-33)
e2e/react-start/basic-nitro-spa/src/utils/seo.ts (1)
scripts/llms-generate.mjs (1)
  • title (101-101)

Comment on lines 113 to 133
async function localFetch(
urlPath: string,
options?: RequestInit,
maxRedirects: number = 5,
): Promise<Response> {
const url = new URL(urlPath, baseUrl)
const request = new Request(url, options)
const response = await fetch(request)

if (isRedirectResponse(response) && maxRedirects > 0) {
const location = response.headers.get('location')!
if (location.startsWith('http://localhost') || location.startsWith('/')) {
const newUrl = location.replace('http://localhost', '')
return localFetch(newUrl, options, maxRedirects - 1)
} else {
logger.warn(`Skipping redirect to external location: ${location}`)
}
}

return response
}
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 | 🟡 Minor

Redirect URL handling may not preserve port numbers correctly.

Line 125 replaces 'http://localhost' but redirects may include port numbers like 'http://localhost:3000/path'. The current logic would result in :3000/path being passed to the next localFetch call.

       if (location.startsWith('http://localhost') || location.startsWith('/')) {
-        const newUrl = location.replace('http://localhost', '')
+        // Parse the URL to properly extract the pathname
+        const redirectUrl = new URL(location, baseUrl)
+        const newUrl = redirectUrl.pathname + redirectUrl.search + redirectUrl.hash
         return localFetch(newUrl, options, maxRedirects - 1)

This ensures the path is correctly extracted regardless of the port in the redirect URL.

🤖 Prompt for AI Agents
In packages/start-plugin-core/src/prerender-nitro.ts around lines 113 to 133,
the redirect handling currently strips only the exact 'http://localhost' prefix
and will leave port numbers (e.g. ':3000/path') in the next localFetch call;
instead parse the redirect Location header into a URL and extract the pathname +
search + hash so port and origin are removed cleanly before recursing. Change
the branch that handles local redirects to detect http(s)://localhost (with
optional port), construct a URL object from location (or relative to baseUrl if
needed), build a new path using url.pathname + url.search + url.hash, and call
localFetch with that new path; keep the existing maxRedirects decrement and
warning behavior for external origins.

Comment on lines 365 to 372
} catch (error) {
if (retries < (prerenderOptions.retryCount ?? 0)) {
logger.warn(`Encountered error, retrying: ${page.path} in 500ms`)
await new Promise((resolve) =>
setTimeout(resolve, prerenderOptions.retryDelay),
)
retriesByPath.set(page.path, retries + 1)
addCrawlPageTask(page)
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 | 🟡 Minor

Log message doesn't match actual retry delay.

Line 367 logs "retrying: ${page.path} in 500ms" but line 369 uses prerenderOptions.retryDelay, which may differ from 500ms.

-          logger.warn(`Encountered error, retrying: ${page.path} in 500ms`)
+          logger.warn(`Encountered error, retrying: ${page.path} in ${prerenderOptions.retryDelay ?? 500}ms`)
🤖 Prompt for AI Agents
In packages/start-plugin-core/src/prerender-nitro.ts around lines 365 to 372,
the log message hardcodes "in 500ms" but the actual delay uses
prerenderOptions.retryDelay; update the logger to reflect the actual value (e.g.
use the retryDelay variable or prerenderOptions.retryDelay) so the message
matches the real delay, and ensure retryDelay has a sensible default when
undefined before logging.

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

🧹 Nitpick comments (2)
packages/start-plugin-core/src/prerender.ts (2)

2-2: Fix import ordering and use top-level type import.

The static analysis correctly flags that ChildProcess should be sorted before spawn and should use a top-level type-only import for consistency.

-import { spawn, type ChildProcess } from 'node:child_process'
+import type { ChildProcess } from 'node:child_process'
+import { spawn } from 'node:child_process'

317-321: Consider more robust process shutdown handling.

The fixed 500ms wait may not be sufficient for graceful shutdown, and there's no fallback if the process doesn't terminate. Consider waiting for the 'exit' event or adding a SIGKILL fallback.

             close: async () => {
-              child.kill('SIGTERM')
-              // Wait a bit for graceful shutdown
-              await new Promise((r) => setTimeout(r, 500))
+              return new Promise<void>((resolve) => {
+                const forceKillTimeout = setTimeout(() => {
+                  child.kill('SIGKILL')
+                }, 5000)
+                child.once('exit', () => {
+                  clearTimeout(forceKillTimeout)
+                  resolve()
+                })
+                child.kill('SIGTERM')
+              })
             },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8162cc6 and 6c19468.

📒 Files selected for processing (5)
  • e2e/react-start/basic-nitro-spa/tests/app.spec.ts (1 hunks)
  • e2e/solid-start/basic-nitro-spa/tests/app.spec.ts (1 hunks)
  • packages/start-plugin-core/src/plugin.ts (3 hunks)
  • packages/start-plugin-core/src/post-server-build.ts (3 hunks)
  • packages/start-plugin-core/src/prerender.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • e2e/react-start/basic-nitro-spa/tests/app.spec.ts
  • e2e/solid-start/basic-nitro-spa/tests/app.spec.ts
  • packages/start-plugin-core/src/plugin.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • packages/start-plugin-core/src/post-server-build.ts
  • packages/start-plugin-core/src/prerender.ts
🧬 Code graph analysis (1)
packages/start-plugin-core/src/prerender.ts (2)
packages/start-plugin-core/src/schema.ts (1)
  • TanStackStartOutputConfig (207-207)
packages/start-plugin-core/src/constants.ts (1)
  • VITE_ENVIRONMENT_NAMES (1-6)
🪛 ESLint
packages/start-plugin-core/src/prerender.ts

[error] 2-2: Member 'ChildProcess' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 2-2: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)

⏰ 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 (8)
packages/start-plugin-core/src/prerender.ts (5)

12-65: LGTM - Well-structured function signature and output resolution.

The updated function signature properly handles the dual-mode operation (Nitro vs Vite), with clear type definitions and appropriate error handling when neither builder nor outputDir is provided.


69-87: LGTM - Clean dual-backend abstraction.

The server startup logic properly abstracts both Nitro and Vite preview backends with a unified cleanup interface, enabling consistent resource management in the finally block.


309-330: Server readiness check accepts 4xx responses.

The check res.ok || res.status < 500 will consider the server ready even for 4xx client errors (e.g., 404, 401). If the root endpoint requires authentication or doesn't exist, this could lead to premature resolution. Consider checking only for res.ok or specific expected status codes if the root path is guaranteed to return a success response.


115-128: LGTM - Proper error handling and cleanup.

Good improvement: errors are now logged and rethrown rather than swallowed, and the cleanup function is consistently called in the finally block regardless of success or failure.


348-369: LGTM!

Clean update to accept an optional configFile parameter with proper error wrapping.

packages/start-plugin-core/src/post-server-build.ts (3)

8-42: LGTM - Good extraction of shared configuration logic.

The setupPrerenderConfig helper cleanly consolidates the prerender configuration logic that was previously duplicated, enabling reuse between postServerBuild and postServerBuildForNitro.


44-70: LGTM!

Clean refactoring with the new skipPrerender parameter enabling the caller to defer prerendering to a later stage (e.g., post-Nitro build).


72-97: LGTM - Well-structured Nitro-specific post-build handler.

The function correctly mirrors the structure of postServerBuild while using Nitro-specific parameters (outputDir, nitroServerPath) instead of the Vite builder. The reuse of setupPrerenderConfig ensures consistent behavior across both paths.

Comment on lines +288 to +299
async function startNitroServer(
serverPath: string,
): Promise<{ url: URL; close: () => Promise<void> }> {
return new Promise((resolve, reject) => {
// Find a random port
const port = 3000 + Math.floor(Math.random() * 10000)
const env = { ...process.env, PORT: String(port) }

const child: ChildProcess = spawn('node', [serverPath], {
env,
stdio: ['ignore', 'pipe', 'pipe'],
})
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:

# First, let's locate and examine the prerender.ts file
find . -type f -name "prerender.ts" | head -20

Repository: TanStack/router

Length of output: 226


🏁 Script executed:

# Get file line count and examine the function
wc -l ./packages/start-plugin-core/src/prerender.ts

Repository: TanStack/router

Length of output: 108


🏁 Script executed:

# Read the complete function starting from line 280
sed -n '280,350p' ./packages/start-plugin-core/src/prerender.ts

Repository: TanStack/router

Length of output: 1764


🏁 Script executed:

# Check if get-port is already a dependency
cat ./packages/start-plugin-core/package.json | grep -A 20 "dependencies"

Repository: TanStack/router

Length of output: 803


🏁 Script executed:

# Search for all usages of startNitroServer function
rg -n "startNitroServer" ./packages/start-plugin-core/src/prerender.ts

Repository: TanStack/router

Length of output: 165


🏁 Script executed:

# Check the context where startNitroServer is called
sed -n '60,90p' ./packages/start-plugin-core/src/prerender.ts

Repository: TanStack/router

Length of output: 953


🏁 Script executed:

# Check if there's any error handling or retry logic for port issues elsewhere
rg -n "EADDRINUSE|port.*collision|port.*in.*use" ./packages/start-plugin-core/src/ -i

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Let's look at the imports to understand what packages are available
head -30 ./packages/start-plugin-core/src/prerender.ts

Repository: TanStack/router

Length of output: 1031


🌐 Web query:

Node.js child_process port 0 OS assign available port

💡 Result:

Short answer: bind the child to port 0 (server.listen(0)) so the OS picks an available port, then read the actual port from server.address().port and communicate it back to the parent (e.g. via IPC or stdout).

Why/how (references):

  • The OS assigns an unused port when you pass 0 to listen/bind; Node exposes the chosen port in server.address().port. [1][2][3]

Minimal example (child + parent using fork + IPC):

  • child.js
    const http = require('http');
    const server = http.createServer((req,res)=>res.end('ok'));
    server.listen(0, ()=> {
    const port = server.address().port;
    if (process.send) process.send({port}); // send to parent when forked
    console.log('listening', port); // or stdout
    });

  • parent.js
    const { fork } = require('child_process');
    const child = fork('./child.js');
    child.on('message', msg => console.log('child port', msg.port));

References:

  1. Node.js child_process docs (server handle / IPC examples). [Node.js docs]. [4]
  2. StackOverflow / Node patterns: use port 0 and server.address().port to get assigned port. [3][5]
  3. (--inspect=0 note) Node can auto-assign debugger ports with =0 when launching processes. [0]

(If you want, I can paste a runnable snippet or show using spawn + stdout parsing instead of fork.)


🌐 Web query:

Nitro server startup output port logging console

💡 Result:

Do you mean Nitro (the Nuxt / nitro.build server runtime) or AWS Nitro (EC2 Nitro Enclaves)?


🌐 Web query:

nitro.build server startup console output messages

💡 Result:

Typical Nitro server startup console messages (common outputs you’ll see)

  • "Nitro vX.Y.Z" or framework banner (on some installs). [1]
  • Dev server ready / listening message, e.g. "Nitro is ready at http://localhost:3000" or "Listening on http://localhost:3000". [2]
  • Runtime preset / entry info when running built server (node .output/server/index.mjs). [1][2]
  • Environment / port info (reads NITRO_PORT / PORT, NITRO_HOST / HOST). [1]
  • Plugin/Hook logs: any console.log() from server/plugins run during startup (e.g. "Nitro plugin ..."). [3]
  • Warnings or errors about missing SSL keys, UNIX socket usage, or shutdown handling if relevant environment vars are set (NITRO_SSL_CERT / NITRO_SSL_KEY, NITRO_UNIX_SOCKET, NITRO_SHUTDOWN_*). [1]

Sources:
[1] Nitro — Node.js runtime / deploy docs.
[2] Nitro — Getting Started / dev server messages.
[3] Nitro — Plugins (plugin console output runs at startup).


🌐 Web query:

Node.js child_process stdio pipe buffer size limit blocking

💡 Result:

Short answer

  • Child stdio use OS pipes with limited, platform-specific kernel buffers; if the child writes faster than the parent consumes, the kernel pipe fills and the child will block on write. [1][2]
  • On Linux the default pipe capacity is typically 65,536 bytes (64 KiB) (can be queried/changed with F_GETPIPE_SZ / F_SETPIPE_SZ). [2]
  • In Node, spawned children’s stdout/stderr are exposed as Readable streams (with Node-level buffering controlled by stream highWaterMark, default 16 KiB) and exec()/execSync() also enforce a JS-side maxBuffer. If you don’t read the stream fast enough the kernel pipe still fills and the child will block. [1][3][4]

Practical fixes

  • Always consume the child’s stdout/stderr (attach 'data' handler or pipe it) so the kernel buffer drains. [1]
  • Use stdio: 'inherit' (shares parent stdio) or stdio: 'ignore' (drops output) if you don’t need the data. [1]
  • For very high-throughput cases: (a) stream data incrementally (pipe to a file or process), (b) tune Node stream buffers via highWaterMark where applicable, or (c) on Linux adjust pipe size with fcntl(F_SETPIPE_SZ) from native code (not exposed directly by Node core). [1][2][3]
  • If using child_process.exec(), raise maxBuffer (or switch to spawn()) to avoid JS-side buffering limits. [4]

Sources

  • Node.js child_process docs (spawn/stdio behavior, exec maxBuffer). [1]
  • Linux pipe(7) / man pages (pipe capacity, F_GET/SETPIPE_SZ). [2]
  • Node.js stream docs (default highWaterMark = 16 KiB). [3]
  • child_process.exec/execSync maxBuffer description. [4]

🏁 Script executed:

# Let's verify the exact stdio configuration and check if there are tests
grep -n "stdio" ./packages/start-plugin-core/src/prerender.ts -B 2 -A 2

Repository: TanStack/router

Length of output: 200


🏁 Script executed:

# Check if there are any tests for the prerender function
find ./packages/start-plugin-core -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "startNitroServer\|prerender" 2>/dev/null | head -5

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check tsconfig for strict mode
find ./packages/start-plugin-core -name "tsconfig.json" | xargs cat 2>/dev/null | head -50

Repository: TanStack/router

Length of output: 283


🏁 Script executed:

# Check root tsconfig
cat ./tsconfig.json | grep -A 30 "compilerOptions"

Repository: TanStack/router

Length of output: 818


Based on my verification of the review comment, I can now provide the rewritten review:


Address stdout buffer blocking and improve port collision handling.

The code has two legitimate concerns:

  1. stdout buffer blocking: Setting stdio: 'pipe' without consuming stdout can cause the child process to block if Nitro's startup output exceeds the kernel pipe buffer (~64 KiB on Linux). stderr is correctly consumed, but stdout is not. Using stdio: 'inherit' fixes this by inheriting the parent's stdout.

  2. Port collision risk: The random port selection (3000–12999) may collide with existing services, causing a confusing "startup timed out" error instead of a clear port-in-use failure. However, the suggested fix using port: 0 requires additional infrastructure to communicate the OS-assigned port back from the Nitro process (e.g., parsing startup output or using IPC), which the diff does not address. Alternatively, consider using a library like get-port or performing an availability check before spawning.

-    const port = 3000 + Math.floor(Math.random() * 10000)
+    // Use port 0 to let OS assign an available ephemeral port,
+    // then parse the actual port from Nitro's startup output
+    const port = 0
     const env = { ...process.env, PORT: String(port) }

     const child: ChildProcess = spawn('node', [serverPath], {
       env,
-      stdio: ['ignore', 'pipe', 'pipe'],
+      stdio: ['ignore', 'inherit', 'pipe'],
     })

The stdout fix is straightforward and recommended. The port collision fix needs clarification on how to retrieve the actual port Nitro binds to when using port 0.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/start-plugin-core/src/prerender.ts around lines 288-299, change the
child spawn options to avoid stdout buffer blocking by either inheriting stdout
(stdio: ['ignore','inherit','pipe']) or actively consuming child.stdout stream;
and replace the naive random port selection with a reliable approach such as
using get-port to pick an available port before spawning (or, if you choose port
0, implement a mechanism to read the OS-assigned port from Nitro startup output
or via IPC) so you fail fast on port collisions and can determine the actual
bound port.

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

♻️ Duplicate comments (1)
packages/start-plugin-core/src/prerender.ts (1)

292-350: Address stdout buffer blocking and port collision risks.

The issues from the previous review remain:

  1. stdout buffer blocking: stdio: ['ignore', 'pipe', 'pipe'] pipes stdout but it's never consumed. Only stderr is consumed (lines 343-345). If Nitro outputs significant data to stdout, the kernel pipe buffer (~64 KiB) fills and the process blocks.

  2. Port collision risk: Random port selection (3000–12999) may collide with existing services, resulting in a confusing "startup timed out" error rather than a clear port-in-use failure.

Apply this diff to fix the stdout issue (minimal change):

     const child: ChildProcess = spawn('node', [serverPath], {
       env,
-      stdio: ['ignore', 'pipe', 'pipe'],
+      stdio: ['ignore', 'inherit', 'pipe'],
     })

For port collision, consider using the get-port library (already in some Vite tooling) or implement a retry loop that catches EADDRINUSE.

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

352-397: Complex but necessary workaround for Nitro's signal handling.

The process.exit override and SIGHUP emit is a pragmatic solution to trigger Nitro's child process cleanup without exiting the parent process. A few observations:

  1. The 100ms delay (line 392) is somewhat arbitrary – if Nitro's cleanup is slow, this might not be enough.
  2. There's a brief window where process.exit is no-op'd; if other code calls it during this time, it would be silently ignored.

Consider adding a brief comment explaining why this workaround exists (Nitro's configurePreviewServer behavior) for future maintainers.

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

378-409: Consider adding type annotations for the Nitro module interface.

The nitro: any parameter and untyped access to nitro.options.output.dir, nitro.options.output.publicDir, and nitro.options.preset reduce type safety. Per the coding guidelines requiring TypeScript strict mode, consider defining a minimal interface for the Nitro options you depend on.

+// Minimal type for Nitro module interop (Nitro v3.0.1-alpha.1+)
+interface NitroModuleContext {
+  options: {
+    preset: string
+    output: {
+      dir: string
+      publicDir: string
+    }
+  }
+  hooks: {
+    hook: (name: 'compiled', fn: () => Promise<void>) => void
+  }
+}
+
 {
   name: 'tanstack-start-core:nitro-prerender',
   nitro: {
     name: 'tanstack-start-prerender',
-    setup(nitro: any) {
+    setup(nitro: NitroModuleContext) {
       nitro.hooks.hook('compiled', async () => {

This provides documentation and catches API drift at compile time. Based on coding guidelines requiring TypeScript strict mode.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14f3b45 and ca09453.

📒 Files selected for processing (3)
  • packages/start-plugin-core/src/plugin.ts (3 hunks)
  • packages/start-plugin-core/src/post-server-build.ts (3 hunks)
  • packages/start-plugin-core/src/prerender.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/start-plugin-core/src/post-server-build.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • packages/start-plugin-core/src/plugin.ts
  • packages/start-plugin-core/src/prerender.ts
🧬 Code graph analysis (2)
packages/start-plugin-core/src/plugin.ts (2)
packages/nitro-v2-vite-plugin/src/index.ts (1)
  • config (58-139)
packages/start-plugin-core/src/post-server-build.ts (3)
  • postServerBuild (76-102)
  • writeNitroBuildInfo (16-38)
  • postServerBuildForNitro (104-129)
packages/start-plugin-core/src/prerender.ts (2)
packages/start-plugin-core/src/schema.ts (1)
  • TanStackStartOutputConfig (207-207)
packages/start-plugin-core/src/constants.ts (1)
  • VITE_ENVIRONMENT_NAMES (1-6)
⏰ 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 (6)
packages/start-plugin-core/src/prerender.ts (3)

13-26: LGTM – Function signature update enables flexible invocation.

The updated signature properly supports both Vite builder-based and Nitro-based prerendering flows. The optional parameters with clear JSDoc for nitroServerPath provide good API clarity.


52-66: LGTM – Output directory resolution handles both invocation modes.

The conditional logic correctly prioritizes the explicit override, falls back to builder's client environment config, and throws a clear error when neither is available.


419-445: LGTM – Clean polling implementation for server readiness.

The function uses reasonable defaults (100ms interval, 30s timeout) and correctly treats any non-5xx response as "ready" since a 404 still indicates the server is accepting requests.

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

53-53: LGTM – Config file path tracking for Nitro integration.

Adding viteConfigFile to ResolvedStartConfig provides the necessary context for the Nitro prerender flow to invoke vite.preview() with the correct configuration.

Also applies to: 80-80


356-358: LGTM – Correct handling of configFile value.

The || undefined coercion correctly converts Vite's false (no config file) to undefined, aligning with the expected type in ResolvedStartConfig.


363-374: LGTM – Nitro detection enables deferred prerendering.

The plugin name prefix check (nitro:) is a reasonable heuristic for detecting Nitro's presence, and passing skipPrerender: true correctly defers prerendering to the Nitro module's compiled hook.

Comment on lines +88 to +91

// Wait for the server to be ready (handles Nitro's child process startup time)
await waitForServerReady(baseUrl, logger)
}
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 | 🟡 Minor

Misleading comment – This is the Vite preview path, not Nitro.

The comment says "handles Nitro's child process startup time" but this code is in the else branch where we're using Vite preview, not Nitro. The Nitro server path (lines 73-77) already handles readiness internally via polling in startNitroServer.

-    // Wait for the server to be ready (handles Nitro's child process startup time)
+    // Wait for the Vite preview server to be ready
     await waitForServerReady(baseUrl, logger)
📝 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
// Wait for the server to be ready (handles Nitro's child process startup time)
await waitForServerReady(baseUrl, logger)
}
// Wait for the Vite preview server to be ready
await waitForServerReady(baseUrl, logger)
}
🤖 Prompt for AI Agents
In packages/start-plugin-core/src/prerender.ts around lines 88 to 91, the inline
comment incorrectly states it "handles Nitro's child process startup time" while
this branch uses Vite preview; update the comment to accurately describe that
waitForServerReady is used for the Vite preview server readiness (or remove the
misleading reference to Nitro), e.g. mention Vite preview/startup latency or
remove Nitro-specific wording so comments reflect the actual code path.

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

♻️ Duplicate comments (2)
packages/start-plugin-core/src/prerender.ts (2)

89-90: Fix comment: this readiness wait is for Vite preview, not Nitro.

This branch is only executed when nitroServerPath is falsy and you’ve started a Vite preview server. The comment still refers to “Nitro’s child process startup time”, which is misleading and was already flagged in a previous review.

Something like:

// Wait for the Vite preview server (and any framework plugins) to be ready

would better reflect what this code is doing.


2-3: Nitro subprocess: stdout piping and port selection are fragile.

This block is doing the right thing conceptually (spawn Nitro, poll until ready, expose a close handle), but there are a few practical issues – some already raised in a previous review:

  1. stdout pipe can block the child process.
    You set stdio: ['ignore', 'pipe', 'pipe'] but never consume child.stdout. If Nitro writes enough to stdout during startup, the OS pipe buffer can fill and block the child, making the readiness check hang.

    Easiest fix: inherit stdout if you don’t need to capture it:

  • const child: ChildProcess = spawn('node', [serverPath], {
  •  env,
    
  •  stdio: ['ignore', 'pipe', 'pipe'],
    
  • })
  • const child: ChildProcess = spawn('node', [serverPath], {
  •  env,
    
  •  // Inherit stdout so the child can't block on a full pipe buffer
    
  •  stdio: ['ignore', 'inherit', 'pipe'],
    
  • })

2. **Random port selection risks collisions and false “readiness”.**  
Picking `3000 + Math.random() * 10000` can collide with existing services:

- If another HTTP service is already on that port, your polling `fetch("http://localhost:port/")` may see a valid response and treat an unrelated server as “Nitro is ready”.
- If Nitro fails with `EADDRINUSE`, you’ll time out after 30 seconds without a clear port‑in‑use signal.

Consider a more robust approach, e.g.:

- Use a port‑discovery helper (like `get-port`) to pick an available port before spawning Nitro, or
- Allow `PORT=0` and have Nitro (or a small wrapper) communicate the actual bound port back (e.g. via stdout structured log or IPC) so you don’t have to guess or risk collisions.

3. **Optional: react to early child exit.**  
Right now, if the Nitro process exits quickly (e.g. due to a config error), you keep polling until the 30s timeout. Adding a `child.on('exit', ...)` that rejects immediately when `!resolved` would surface those failures faster.

Points (1) and (2) are the most important to address; (3) is a nice quality‑of‑life improvement.  






Also applies to: 9-9, 292-350

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/start-plugin-core/src/prerender.ts (3)</summary><blockquote>

`16-26`: **Server setup vs try/finally – consider pulling startup into the guarded section.**

Right now Nitro/Vite server startup (and `waitForServerReady` in the preview path) happens before the `try` block, while `cleanup()` is only called from the `finally`. That means if anything throws between assigning `baseUrl/cleanup` and entering the `try` (notably `waitForServerReady` timing out), the preview server will stay running until the outer caller tears down the process.

This isn’t catastrophic for a short‑lived CLI, but for long‑lived processes/tests it’s safer to:

- Move the `if (nitroServerPath) { ... } else { ...; await waitForServerReady(...) }` block inside the `try`, or
- Add an outer `try/finally` that ensures `cleanup` is awaited whenever the server has been started.

This keeps server lifecycle management clearly tied to the guarded region where prerendering actually happens.  




Also applies to: 52-66, 70-72, 119-132

---

`10-10`: **Make `process.exit` override robust when cleaning up Nitro’s preview child.**

The Nitro‑aware `startPreviewServer` logic is clever and solves a real problem (Nitro’s `configurePreviewServer` killing the parent process on `SIGHUP`). A couple of robustness tweaks would make it safer:

- `server.close` currently does:

```ts
const originalExit = process.exit
process.exit = (() => {}) as typeof process.exit

process.emit('SIGHUP', 'SIGHUP')

process.exit = originalExit
await new Promise((resolve) => setTimeout(resolve, 100))
return originalClose()

If any handler attached to SIGHUP throws synchronously, process.exit will never be restored. Wrapping the override in a try/finally keeps the global state consistent:

-      server.close = async () => {
-        const originalExit = process.exit
-        process.exit = (() => {}) as typeof process.exit
-
-        // Emit SIGHUP to trigger Nitro's child process cleanup
-        process.emit('SIGHUP', 'SIGHUP')
-
-        // Restore process.exit
-        process.exit = originalExit
-
-        // Give the child process a moment to terminate
-        await new Promise((resolve) => setTimeout(resolve, 100))
-
-        // Now close the preview server
-        return originalClose()
-      }
+      server.close = async () => {
+        const originalExit = process.exit
+        try {
+          process.exit = (() => {}) as typeof process.exit
+
+          // Emit SIGHUP to trigger Nitro's child process cleanup
+          process.emit('SIGHUP', 'SIGHUP')
+        } finally {
+          // Always restore process.exit, even if a handler throws
+          process.exit = originalExit
+        }
+
+        // Give the child process a moment to terminate
+        await new Promise((resolve) => setTimeout(resolve, 100))
+
+        // Now close the preview server
+        return originalClose()
+      }
  • The hasNitroPlugin detection by p.name.startsWith('nitro:') is reasonable, but if Nitro ever changes its plugin names this branch will silently stop running. Non‑blocking, but you may want a short comment noting that assumption.

Overall, the preview/Nitro interop strategy looks solid; this is mainly about hardening the global process mutation.

Also applies to: 352-403


423-447: waitForServerReady looks good; consider reusing it for Nitro as well.

The readiness helper is straightforward and matches the semantics you use elsewhere (server “ready” once it responds with any status < 500). Implementation is clear and bounded by a timeout.

Given that startNitroServer has its own polling logic with similar behavior, you might consider reusing waitForServerReady there (accepting a URL instead of re‑encoding http://localhost:${port}/) to keep the readiness semantics in one place and reduce duplication. Not required, but would simplify future changes (e.g. tweaking timeouts or readiness criteria) to a single helper.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca09453 and b17e4b6.

📒 Files selected for processing (1)
  • packages/start-plugin-core/src/prerender.ts (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • packages/start-plugin-core/src/prerender.ts
⏰ 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

Comment on lines 97 to 103
async function localFetch(
path: string,
fetchPath: string,
options?: RequestInit,
maxRedirects: number = 5,
): Promise<Response> {
const url = new URL(path, baseUrl)
const url = new URL(fetchPath, baseUrl)
const request = new Request(url, options)
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

Redirect handling for http://localhost:<port>/… is broken.

In localFetch, when following redirects you do:

if (location.startsWith('http://localhost') || location.startsWith('/')) {
  const newUrl = location.replace('http://localhost', '')
  return localFetch(newUrl, options, maxRedirects - 1)
}

For a URL like http://localhost:3000/foo, replace('http://localhost', '') yields ':3000/foo', which is not a valid path. The next new URL(fetchPath, baseUrl) call then builds a wrong URL.

Suggested fix:

  • Treat absolute http://localhost[:port] redirects by parsing and reusing only the path/query/hash, and keep the host/port from baseUrl.
  • Keep the simple code path for already-relative URLs.

For example:

-    if (isRedirectResponse(response) && maxRedirects > 0) {
-      const location = response.headers.get('location')!
-      if (location.startsWith('http://localhost') || location.startsWith('/')) {
-        const newUrl = location.replace('http://localhost', '')
-        return localFetch(newUrl, options, maxRedirects - 1)
-      } else {
-        logger.warn(`Skipping redirect to external location: ${location}`)
-      }
-    }
+    if (isRedirectResponse(response) && maxRedirects > 0) {
+      const location = response.headers.get('location')!
+
+      if (location.startsWith('http://localhost')) {
+        const url = new URL(location)
+        const newPath = `${url.pathname}${url.search}${url.hash}`
+        return localFetch(newPath, options, maxRedirects - 1)
+      }
+
+      if (location.startsWith('/')) {
+        return localFetch(location, options, maxRedirects - 1)
+      }
+
+      logger.warn(`Skipping redirect to external location: ${location}`)
+    }

The new headers: prerenderOptions.headers usage looks good and preserves the ability to customize per‑request headers.

Also applies to: 106-113, 195-201

@birkskyum birkskyum marked this pull request as draft November 28, 2025 01:54
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.

nitro + spa mode breaks since v1.138.0

2 participants