Skip to content

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Jan 11, 2026

Summary by CodeRabbit

  • New Features

    • Added explicit development entry points for dev builds.
    • Dev-mode HeadContent now removes development-only styles after hydration.
  • Refactor

    • Consolidated head-tag generation into shared utilities for consistency across frameworks.
  • Tests

    • Updated SSR test expectations to no longer include the dev stylesheet link in rendered output.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Refactors head-tag generation into new headContentUtils modules, adds dev-specific HeadContent entry points for React/Solid/Vue that clean up dev styles after hydration, and threads matchedRoutes into server manifest/handler to inject per-route dev-styles in development.

Changes

Cohort / File(s) Summary
Package export maps
packages/react-router/package.json, packages/solid-router/package.json, packages/vue-router/package.json
Added "development" export condition pointing to dev build files (e.g., ./dist/esm/index.dev.js) for top-level package exports.
Vite entry points
packages/.../vite.config.ts
packages/react-router/vite.config.ts, packages/solid-router/vite.config.ts, packages/vue-router/vite.config.ts
Added ./src/index.dev.tsx to Vite entry arrays to emit development entry bundles.
Development index entries
packages/react-router/src/index.dev.tsx, packages/solid-router/src/index.dev.tsx, packages/vue-router/src/index.dev.tsx
New files re-export public API and override HeadContent with dev-specific implementations.
Dev HeadContent components
packages/react-router/src/HeadContent.dev.tsx, packages/solid-router/src/HeadContent.dev.tsx, packages/vue-router/src/HeadContent.dev.tsx
New framework-specific dev HeadContent that filters/removes dev-styles after hydration and renders assets via Asset.
Head content utilities
packages/react-router/src/headContentUtils.tsx, packages/solid-router/src/headContentUtils.tsx, packages/vue-router/src/headContentUtils.tsx
New useTags and uniqBy utilities that aggregate/dedupe head tags (meta, link, script, style, title), apply CSP nonce, handle JSON‑LD, and include SSR manifest assets/preloads.
Simplified HeadContent
packages/react-router/src/HeadContent.tsx, packages/solid-router/src/HeadContent.tsx, packages/vue-router/src/HeadContent.tsx
Removed in-file tag-building and dev-style handling; delegate to headContentUtils.useTags and render Assets only.
Index exports
packages/react-router/src/index.tsx, packages/solid-router/src/index.tsx, packages/vue-router/src/index.tsx
Added useTags export from ./headContentUtils (moved export source where applicable).
SSR useTags import updates
packages/solid-router/src/ssr/RouterServer.tsx, packages/vue-router/src/ssr/RouterServer.tsx
Updated imports to consume useTags from headContentUtils instead of HeadContent.
Server manifest: dev-styles injection
packages/start-server-core/src/router-manifest.ts
getStartManifest(matchedRoutes?) now accepts matchedRoutes and injects a dev-styles link (with marker attr) built from matched route IDs when TSS_DEV_SERVER is true.
Handler: matchedRoutes propagation
packages/start-server-core/src/createStartHandler.ts
getManifest and executeRouter signatures accept optional matchedRoutes; middleware chain threads matchedRoutes into manifest generation.
Tests
packages/react-router/tests/Scripts.test.tsx
Updated SSR test expectation to omit the dev-styles stylesheet link from output.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Handler as StartHandler
    participant Manifest as RouterManifest

    Client->>Server: HTTP request
    Server->>Handler: route resolution -> matchedRoutes
    Handler->>Manifest: getStartManifest(matchedRoutes)
    alt TSS_DEV_SERVER == true
        Manifest->>Manifest: compute matched route IDs
        Manifest->>Manifest: build dev-styles URL
        Manifest->>Handler: include dev-styles link in manifest
    else
        Manifest->>Handler: return manifest without dev-styles
    end
    Handler->>Server: render SSR HTML (includes manifest assets)
    Server->>Client: SSR HTML served
    Client->>Client: hydrate
    Client->>Client: HeadContent.dev detects hydration complete
    Client->>Client: remove dev-styles link elements from DOM
    Client->>Client: filter dev-styles from future head rendering
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • birkskyum

Poem

🐰
Dev styles danced upon the page,
Routes and tags all turned the page,
Three routers trimmed and tidy now,
A rabbit pats the refactor brow —
Hop, hydrate, and clear away! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% 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 'fix: cleanly separate dev styles from prod runtime' accurately reflects the main objective across all modified packages—extracting development style handling into separate dev entry points and utility modules to isolate it from production code.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@nx-cloud
Copy link

nx-cloud bot commented Jan 11, 2026

View your CI Pipeline Execution ↗ for commit 5b75bfc

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

☁️ Nx Cloud last updated this comment at 2026-01-11 02:25:25 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 11, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6356

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6356

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 5b75bfc

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/vue-router/src/HeadContent.tsx (1)

12-22: Replace JSON.stringify(tag) keys with stable identifiers; nonce and other volatile SSR fields cause key instability across renders.

Using JSON.stringify(tag) as a key includes volatile fields like nonce (from router.options.ssr?.nonce in Scripts.tsx lines 30, 51, 74, 81) that change per request. This breaks Vue's key stability and causes unnecessary remounting and potential hydration mismatches.

Suggested fix:

  • Extract stable identifiers from tag attributes (href, src, name, property)
  • Explicitly exclude volatile fields like nonce
  • Fall back to a compact serialization for tags without stable fields
Proposed fix (omit `nonce` and keep keys small/stable)
 export const HeadContent = Vue.defineComponent({
   name: 'HeadContent',
   setup() {
     const tags = useTags()
 
     return () => {
       return tags().map((tag) =>
         Vue.h(Asset, {
           ...tag,
-          key: `tsr-meta-${JSON.stringify(tag)}`,
+          key: `tsr-head-${tag.tag}-${stableTagKey(tag)}`,
         }),
       )
     }
   },
 })
+
+function stableTagKey(tag: Record<string, any>) {
+  const attrs = tag?.attrs ? { ...tag.attrs } : undefined
+  if (attrs && 'nonce' in attrs) delete attrs.nonce
+  // Prefer common identity fields; fall back to a compact serialization.
+  return (
+    attrs?.href ??
+    attrs?.src ??
+    attrs?.name ??
+    attrs?.property ??
+    (tag.children ? String(tag.children).slice(0, 64) : JSON.stringify({ ...tag, attrs }))
+  )
+}
🤖 Fix all issues with AI agents
In @packages/react-router/src/headContentUtils.tsx:
- Around line 164-169: The style tag objects are returning a top-level nonce
property which mismatches RouterManagedTag and other tag shapes; update the
mapping in the style tag creation (the map over ({ children, ...attrs })) to
place nonce inside attrs (e.g., add nonce into attrs before returning) so the
returned object is { tag: 'style', attrs, children } with attrs.nonce set,
matching meta/link/script tag structure and the RouterManagedTag type.
🧹 Nitpick comments (7)
packages/react-router/src/headContentUtils.tsx (2)

62-68: Nonce on meta tags is unconventional.

Meta tags typically don't require or use CSP nonces. The nonce is usually only needed for script and style elements. Adding nonce to meta tag attributes may be unnecessary and could clutter the HTML output.

Consider removing nonce from meta tags
          resultMeta.push({
            tag: 'meta',
            attrs: {
              ...m,
-             nonce,
            },
          })

128-129: Type assertion for structuralSharing indicates API typing gap.

The as any assertion on structuralSharing: true suggests a mismatch between the expected type and the actual usage. This pattern is repeated across multiple useRouterState calls (lines 128, 154, 170, 188).

Consider opening an issue to fix the typing in useRouterState options to properly accept boolean for structuralSharing, eliminating the need for as any.

packages/react-router/src/HeadContent.dev.tsx (1)

34-46: Consider more efficient key generation.

The current approach using JSON.stringify(tag) for keys works but could be inefficient. Consider using a hash function or constructing a simpler key from tag properties (e.g., ${tag.tag}-${tag.attrs?.name || tag.attrs?.property || ''}).

However, given that head tags are typically small in number, the current implementation is acceptable.

♻️ Example of more efficient key generation
-        <Asset {...tag} key={`tsr-meta-${JSON.stringify(tag)}`} nonce={nonce} />
+        <Asset {...tag} key={`tsr-meta-${tag.tag}-${tag.attrs?.name || tag.attrs?.property || tag.attrs?.rel || ''}-${tag.children || ''}`} nonce={nonce} />
packages/solid-router/src/headContentUtils.tsx (4)

14-18: Tighten typing: avoid match.meta! + filter(Boolean) + later ! indexing (strict TS + runtime safety).

Proposed refactor (keeps behavior, removes non-null assertions)
   const routeMeta = useRouterState({
     select: (state) => {
-      return state.matches.map((match) => match.meta!).filter(Boolean)
+      return state.matches.map((match) => match.meta ?? []).filter((m) => m.length)
     },
   })

Also applies to: 24-27


92-123: links: consider flatMap + type-narrowing instead of filter(Boolean).flat(1) (cleaner strict TS).


150-183: styles / headScripts: the as Array<RouterManagedTag> casts likely hide mismatches; prefer satisfies at construction sites.


199-209: uniqBy: add an explicit return type for stricter TS signal (and easier public API reading).

Proposed tweak
-export function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
+export function uniqBy<T>(arr: Array<T>, fn: (item: T) => string): Array<T> {
   const seen = new Set<string>()
   return arr.filter((item) => {
     const key = fn(item)
     if (seen.has(key)) {
       return false
     }
     seen.add(key)
     return true
   })
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8883ca0 and 1ff0508.

📒 Files selected for processing (26)
  • packages/react-router/package.json
  • packages/react-router/src/HeadContent.dev.tsx
  • packages/react-router/src/HeadContent.tsx
  • packages/react-router/src/headContentUtils.tsx
  • packages/react-router/src/index.dev.tsx
  • packages/react-router/src/index.tsx
  • packages/react-router/tests/Scripts.test.tsx
  • packages/react-router/vite.config.ts
  • packages/solid-router/package.json
  • packages/solid-router/src/HeadContent.dev.tsx
  • packages/solid-router/src/HeadContent.tsx
  • packages/solid-router/src/headContentUtils.tsx
  • packages/solid-router/src/index.dev.tsx
  • packages/solid-router/src/index.tsx
  • packages/solid-router/src/ssr/RouterServer.tsx
  • packages/solid-router/vite.config.ts
  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-server-core/src/router-manifest.ts
  • packages/vue-router/package.json
  • packages/vue-router/src/HeadContent.dev.tsx
  • packages/vue-router/src/HeadContent.tsx
  • packages/vue-router/src/headContentUtils.tsx
  • packages/vue-router/src/index.dev.tsx
  • packages/vue-router/src/index.tsx
  • packages/vue-router/src/ssr/RouterServer.tsx
  • packages/vue-router/vite.config.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/vue-router/src/ssr/RouterServer.tsx
  • packages/vue-router/src/index.tsx
  • packages/vue-router/src/HeadContent.dev.tsx
  • packages/react-router/tests/Scripts.test.tsx
  • packages/react-router/src/index.tsx
  • packages/react-router/src/HeadContent.dev.tsx
  • packages/solid-router/src/HeadContent.dev.tsx
  • packages/solid-router/src/headContentUtils.tsx
  • packages/solid-router/src/index.dev.tsx
  • packages/start-server-core/src/router-manifest.ts
  • packages/vue-router/vite.config.ts
  • packages/vue-router/src/headContentUtils.tsx
  • packages/vue-router/src/index.dev.tsx
  • packages/react-router/src/index.dev.tsx
  • packages/vue-router/src/HeadContent.tsx
  • packages/react-router/src/headContentUtils.tsx
  • packages/solid-router/src/ssr/RouterServer.tsx
  • packages/react-router/vite.config.ts
  • packages/solid-router/vite.config.ts
  • packages/react-router/src/HeadContent.tsx
  • packages/start-server-core/src/createStartHandler.ts
  • packages/solid-router/src/HeadContent.tsx
  • packages/solid-router/src/index.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/vue-router/src/ssr/RouterServer.tsx
  • packages/vue-router/src/index.tsx
  • packages/vue-router/src/HeadContent.dev.tsx
  • packages/react-router/tests/Scripts.test.tsx
  • packages/react-router/src/index.tsx
  • packages/react-router/src/HeadContent.dev.tsx
  • packages/solid-router/src/HeadContent.dev.tsx
  • packages/solid-router/src/headContentUtils.tsx
  • packages/solid-router/src/index.dev.tsx
  • packages/start-server-core/src/router-manifest.ts
  • packages/vue-router/vite.config.ts
  • packages/vue-router/src/headContentUtils.tsx
  • packages/vue-router/src/index.dev.tsx
  • packages/react-router/src/index.dev.tsx
  • packages/vue-router/src/HeadContent.tsx
  • packages/react-router/src/headContentUtils.tsx
  • packages/solid-router/src/ssr/RouterServer.tsx
  • packages/react-router/vite.config.ts
  • packages/solid-router/vite.config.ts
  • packages/react-router/src/HeadContent.tsx
  • packages/start-server-core/src/createStartHandler.ts
  • packages/solid-router/src/HeadContent.tsx
  • packages/solid-router/src/index.tsx
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Use workspace protocol workspace:* for internal dependencies in package.json files

Files:

  • packages/solid-router/package.json
  • packages/vue-router/package.json
  • packages/react-router/package.json
🧠 Learnings (10)
📚 Learning: 2025-11-02T16:16:24.898Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5732
File: packages/start-client-core/src/client/hydrateStart.ts:6-9
Timestamp: 2025-11-02T16:16:24.898Z
Learning: In packages/start-client-core/src/client/hydrateStart.ts, the `import/no-duplicates` ESLint disable is necessary for imports from `#tanstack-router-entry` and `#tanstack-start-entry` because both aliases resolve to the same placeholder file (`fake-start-entry.js`) in package.json during static analysis, even though they resolve to different files at runtime.

Applied to files:

  • packages/solid-router/package.json
  • packages/react-router/tests/Scripts.test.tsx
  • packages/vue-router/package.json
  • packages/react-router/package.json
  • packages/solid-router/src/index.dev.tsx
  • packages/start-server-core/src/router-manifest.ts
  • packages/vue-router/vite.config.ts
  • packages/vue-router/src/index.dev.tsx
  • packages/react-router/src/index.dev.tsx
  • packages/solid-router/src/ssr/RouterServer.tsx
  • packages/react-router/vite.config.ts
  • packages/solid-router/vite.config.ts
  • packages/start-server-core/src/createStartHandler.ts
  • packages/solid-router/src/index.tsx
📚 Learning: 2025-12-06T15:03:07.223Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript strict mode with extensive type safety for all code

Applied to files:

  • packages/solid-router/package.json
📚 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:

  • packages/react-router/tests/Scripts.test.tsx
  • packages/start-server-core/src/router-manifest.ts
  • packages/start-server-core/src/createStartHandler.ts
📚 Learning: 2025-12-21T12:52:35.231Z
Learnt from: Sheraff
Repo: TanStack/router PR: 6171
File: packages/router-core/src/new-process-route-tree.ts:898-898
Timestamp: 2025-12-21T12:52:35.231Z
Learning: In `packages/router-core/src/new-process-route-tree.ts`, the matching logic intentionally allows paths without trailing slashes to match index routes with trailing slashes (e.g., `/a` can match `/a/` route), but not vice-versa (e.g., `/a/` cannot match `/a` layout route). This is implemented via the condition `!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX` and is a deliberate design decision to provide better UX by being permissive with missing trailing slashes.

Applied to files:

  • packages/solid-router/src/index.dev.tsx
  • packages/start-server-core/src/router-manifest.ts
  • packages/vue-router/src/index.dev.tsx
  • packages/start-server-core/src/createStartHandler.ts
📚 Learning: 2025-12-17T02:17:55.086Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6120
File: packages/router-generator/src/generator.ts:654-657
Timestamp: 2025-12-17T02:17:55.086Z
Learning: In `packages/router-generator/src/generator.ts`, pathless_layout routes must receive a `path` property when they have a `cleanedPath`, even though they are non-path routes. This is necessary because child routes inherit the path from their parent, and without this property, child routes would not have the correct full path at runtime.

Applied to files:

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

Applied to files:

  • packages/solid-router/src/index.dev.tsx
  • packages/start-server-core/src/router-manifest.ts
  • packages/solid-router/src/ssr/RouterServer.tsx
  • packages/solid-router/src/index.tsx
📚 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:

  • packages/start-server-core/src/router-manifest.ts
  • packages/start-server-core/src/createStartHandler.ts
📚 Learning: 2025-12-06T15:03:07.223Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Applies to **/*.{js,ts,tsx} : Implement ESLint rules for router best practices using the ESLint plugin router

Applied to files:

  • packages/start-server-core/src/router-manifest.ts
  • packages/solid-router/src/ssr/RouterServer.tsx
  • packages/react-router/vite.config.ts
📚 Learning: 2025-12-06T15:03:07.223Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Separate framework-agnostic core logic from React/Solid bindings

Applied to files:

  • packages/solid-router/src/ssr/RouterServer.tsx
📚 Learning: 2025-12-24T22:47:44.320Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6211
File: e2e/react-start/i18n-paraglide/src/server.ts:6-6
Timestamp: 2025-12-24T22:47:44.320Z
Learning: In TanStack Router projects using `inlang/paraglide-js`, the callback passed to `paraglideMiddleware` should use `() => handler.fetch(req)` (referencing the outer `req`) instead of `({ request }) => handler.fetch(request)`. This is intentional because the router needs the untouched URL to perform its own rewrite logic with `deLocalizeUrl`/`localizeUrl`. The middleware's processed request would delocalize the URL and interfere with the router's rewrite handling.

Applied to files:

  • packages/start-server-core/src/createStartHandler.ts
🧬 Code graph analysis (3)
packages/start-server-core/src/router-manifest.ts (1)
packages/virtual-file-routes/src/api.ts (2)
  • route (66-84)
  • rootRoute (10-19)
packages/react-router/src/headContentUtils.tsx (3)
packages/react-router/src/index.tsx (1)
  • RouterManagedTag (78-78)
packages/vue-router/src/headContentUtils.tsx (1)
  • uniqBy (166-176)
packages/router-core/src/router.ts (1)
  • state (1104-1106)
packages/start-server-core/src/createStartHandler.ts (1)
packages/router-core/src/route.ts (1)
  • AnyRoute (778-797)
⏰ 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 (39)
packages/start-server-core/src/router-manifest.ts (4)

4-5: LGTM: Good use of pre-computed constant.

Pre-computing the basepath from the environment variable avoids repeated lookups and improves runtime performance.


12-18: LGTM: Well-documented API change.

The optional matchedRoutes parameter is properly typed with ReadonlyArray, and the documentation clearly explains its dev-mode purpose.


1-2: All imported utilities (buildDevStylesUrl, AnyRoute, RouterManagedTag) are properly exported from @tanstack/router-core and are available for use in this package.


27-38: Dev-styles cleanup is properly implemented across all frameworks.

The data-tanstack-router-dev-styles attribute is correctly handled in all three dev implementations:

  • React, Solid, and Vue each filter out dev styles after hydration
  • All include fallback cleanup effects that remove orphaned dev-style link elements from the DOM when hydration completes
  • Consistent pattern across frameworks ensures reliable cleanup in all scenarios
packages/start-server-core/src/createStartHandler.ts (5)

85-95: LGTM: Dev-mode caching disabled for route-specific styles.

The conditional caching strategy is appropriate:

  • Dev mode: Fresh manifest on each request to include route-specific dev styles
  • Production: Cached manifest (dev styles not needed)

The performance impact of disabling caching in dev mode is acceptable since it only affects development builds.


321-324: LGTM: Signature properly extends executeRouter.

The optional matchedRoutes parameter maintains backward compatibility while enabling dev-styles injection.


340-340: LGTM: Correctly propagates matchedRoutes.

The matchedRoutes parameter is properly passed to getManifest, completing the data flow for dev-styles injection.


488-491: LGTM: Type definition matches implementation.

The executeRouter parameter type is correctly updated to include the optional matchedRoutes parameter, consistent with the function implementation.


555-558: LGTM: Completes the dev-styles data flow.

The comment clearly explains the purpose, and matchedRoutes correctly flows from router.getMatchedRoutes(pathname) through to executeRouter, enabling route-scoped dev-styles injection.

packages/solid-router/vite.config.ts (1)

39-44: LGTM! Development entry point added consistently.

The addition of './src/index.dev.tsx' to the entry array aligns with the PR's objective to separate dev-specific code from production builds. This change is consistent with the parallel updates in React and Vue router packages.

packages/vue-router/package.json (1)

46-46: LGTM! Development export properly configured.

The addition of the development conditional export follows Node.js module resolution conventions correctly. When NODE_ENV or conditions include "development", bundlers will resolve to the dev build, enabling development-specific features like dev-style cleanup.

packages/react-router/package.json (1)

53-53: LGTM! Development exports configured for both module formats.

The addition of development conditional exports for both ESM and CJS formats ensures that development builds are available regardless of the consuming project's module system. This follows Node.js conventions and properly supports the PR's goal of separating dev and production runtimes.

Also applies to: 58-58

packages/solid-router/package.json (1)

51-51: LGTM! Development exports cover all distribution formats.

The development conditional exports are properly configured across all three distribution formats:

  • solid condition for JSX source (preserving reactivity)
  • import for ESM consumers
  • require for CJS consumers

This comprehensive coverage ensures dev-specific behavior works correctly regardless of how the package is consumed.

Also applies to: 56-56, 61-61

packages/solid-router/src/HeadContent.dev.tsx (2)

33-44: Well-structured hydration-aware filtering.

The memo correctly implements the filtering strategy:

  • Pre-hydration: all tags (including dev styles) render to match server HTML
  • Post-hydration: dev styles are filtered out from the reactive tree

The combination of the DOM cleanup effect (lines 24-30) and this filtered rendering ensures dev styles are removed both from Solid's reactive tree and from any orphaned DOM elements. This dual approach handles both normal cases and hydration mismatches effectively.


7-7: DEV_STYLES_ATTR constant is consistent across the codebase.

The DEV_STYLES_ATTR constant at line 7 ('data-tanstack-router-dev-styles') matches the attribute name used in server-side manifest generation, and is used consistently in cleanup logic and tag filtering across all framework implementations (React, Vue, Solid).

packages/react-router/src/HeadContent.tsx (1)

11-21: LGTM! Clean separation of concerns.

The refactored HeadContent component is now a thin renderer that delegates tag construction to useTags. The implementation is correct and follows good separation principles.

One minor observation: using JSON.stringify(tag) for React keys works but is computed on every render. Since useTags already deduplicates tags, this should be fine for typical head content sizes.

packages/react-router/src/headContentUtils.tsx (1)

205-215: LGTM! Consistent utility implementation.

The uniqBy function matches the Vue implementation exactly, ensuring consistent behavior across framework variants.

packages/vue-router/src/ssr/RouterServer.tsx (1)

3-3: LGTM! Import path updated correctly.

The import source change from ../HeadContent to ../headContentUtils aligns with the refactoring to centralize tag generation utilities.

packages/vue-router/src/index.tsx (1)

342-342: LGTM! Public API export added correctly.

The useTags hook is now exported from the package's public API, enabling consumers to access head tag data programmatically if needed.

packages/solid-router/src/ssr/RouterServer.tsx (1)

10-10: LGTM! Import path updated correctly.

The import source change from ../HeadContent to ../headContentUtils aligns with the refactoring to centralize tag generation utilities across all framework variants (React, Vue, Solid).

packages/vue-router/vite.config.ts (1)

27-32: LGTM! Development entry point added.

The new ./src/index.dev.tsx entry point enables separate bundling of development-specific code (like dev styles cleanup), aligning with the PR's objective to cleanly separate dev styles from production runtime.

packages/react-router/src/index.tsx (1)

346-346: LGTM! Public API export added correctly.

The useTags hook is now exported from the React router package's public API, enabling advanced consumers to access head tag data programmatically. This mirrors the same change in Vue router for cross-framework consistency.

packages/react-router/vite.config.ts (1)

22-27: LGTM! Clean addition of development entry point.

The addition of ./src/index.dev.tsx to the Vite entry points aligns well with the PR's objective to separate dev styles from production runtime, and mirrors the pattern used in solid-router and vue-router packages.

packages/react-router/src/index.dev.tsx (1)

1-6: LGTM! Clean re-export pattern for development builds.

The development entry point cleanly overrides HeadContent while preserving all other exports. The comments clearly document the purpose, making this easy to understand and maintain.

packages/solid-router/src/index.dev.tsx (1)

1-6: LGTM! Consistent dev entry point pattern across frameworks.

This follows the same clean re-export pattern as the React Router variant, ensuring consistent developer experience across TanStack Router's framework implementations.

packages/react-router/tests/Scripts.test.tsx (1)

220-220: LGTM! Test correctly reflects dev-style separation.

The updated test expectation correctly validates that dev stylesheet link tags are omitted from SSR/production output, aligning with the PR's objective to cleanly separate development styles from production runtime.

packages/vue-router/src/HeadContent.dev.tsx (1)

1-42: LGTM! Well-structured dev-time cleanup with defensive fallback.

The component correctly:

  • Renders all tags (including dev styles) during SSR and initial hydration
  • Filters dev styles from the virtual DOM after hydration via the hydrated check
  • Provides fallback DOM cleanup in onMounted as a safety net for edge cases

The optional chaining on line 31 ensures type safety, and the pattern is consistent with the production HeadContent implementation referenced in the code snippets.

packages/vue-router/src/index.dev.tsx (1)

1-6: LGTM! Clean dev/prod separation pattern.

The development entry point correctly re-exports all production exports and overrides HeadContent with the dev variant. This pattern aligns with the broader refactoring across React and Solid routers.

packages/react-router/src/HeadContent.dev.tsx (3)

1-7: LGTM! Clean imports and constant definition.

The imports are well-organized and the DEV_STYLES_ATTR constant provides a single source of truth for the dev styles attribute name.


9-17: LGTM! Well-documented dev variant.

The JSDoc clearly explains the development-specific behavior, including dev styles filtering and hydration cleanup.


18-32: LGTM! Correct hydration cleanup logic.

The useEffect correctly waits for hydration to complete before removing orphaned dev styles link elements from the DOM. The guard condition prevents premature execution.

packages/solid-router/src/index.tsx (1)

349-350: LGTM! Clean export reorganization.

The relocation of useTags from HeadContent to the dedicated headContentUtils module improves modularity. Since both exports remain available from the package root, this is a non-breaking refactoring that aligns with the changes in React and Vue routers.

packages/vue-router/src/headContentUtils.tsx (4)

1-7: LGTM! Clean imports.

All necessary dependencies are imported correctly.


8-74: LGTM! Robust meta tag building with proper deduplication.

The meta building logic correctly:

  • Implements last-wins semantics for titles by processing routes in reverse
  • Handles JSON-LD with proper HTML escaping and error handling
  • Deduplicates meta tags by name or property attribute

The try-catch for invalid JSON-LD objects prevents runtime errors from malformed data.


76-149: LGTM! Comprehensive tag collection from multiple sources.

The code correctly collects and transforms:

  • Route-defined links
  • Manifest-based modulepreload links
  • Head scripts with proper attribute/children separation
  • Manifest assets filtered to link tags

The satisfies RouterManagedTag annotation ensures type safety.


151-176: LGTM! Correct Vue Composition API pattern.

The function correctly returns a computed function that:

  1. Accesses .value on Vue refs
  2. Combines tags from all sources in the correct order
  3. Deduplicates using uniqBy with JSON.stringify

The uniqBy implementation efficiently uses a Set for O(n) deduplication.

Note: The function return pattern is appropriate for Vue's composition API, where the calling code will invoke this function to get the current tag list.

packages/solid-router/src/HeadContent.tsx (1)

1-20: LGTM! Excellent refactoring that improves separation of concerns.

The simplified HeadContent implementation correctly:

  • Delegates tag generation to the centralized useTags utility
  • Uses Solid's For component to efficiently render tags
  • Maintains the MetaProvider wrapper for proper portal behavior

This refactoring reduces code duplication and makes the head content logic reusable across production and dev builds.

packages/vue-router/src/HeadContent.tsx (1)

4-4: Good simplification: centralizing tag construction in headContentUtils is the right direction.

packages/solid-router/src/headContentUtils.tsx (1)

38-52: No changes needed; current implementation is correct. The escapeHtml() function uses unicode escape sequences (\u003c for <, etc.), which are valid JSON per RFC 7159 and do not modify JSON-LD semantics. When JSON is parsed, these escape sequences are correctly interpreted back to their original characters. This behavior is explicitly tested in packages/router-core/tests/utils.test.ts (line 696–709) with the test "should handle script tag injection attempt in JSON," confirming that escaped JSON-LD round-trips correctly and prevents XSS injection. The same pattern is used across all router packages (React, Vue, Solid) with no reported issues.

Comment on lines +184 to +197
return () =>
uniqBy(
[
...meta(),
...preloadLinks(),
...links(),
...styles(),
...headScripts(),
] as Array<RouterManagedTag>,
(d) => {
return JSON.stringify(d)
},
)
}
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:

#!/bin/bash
# Check if SSR nonce is expected to be present client-side (to avoid SSR/client tag diffs).
rg -n "ssr\?\?.nonce|ssr\.nonce|csp-nonce" -S packages/solid-router/src

Repository: TanStack/router

Length of output: 236


🏁 Script executed:

# Also check the context of the function to understand the full picture
cat -n packages/solid-router/src/headContentUtils.tsx | head -220 | tail -80

Repository: TanStack/router

Length of output: 2367


🏁 Script executed:

# Check if Solid.createMemo is available/imported
rg -n "createMemo|Solid\." packages/solid-router/src/headContentUtils.tsx | head -20

Repository: TanStack/router

Length of output: 142


🏁 Script executed:

# Check if uniqBy is being used elsewhere and understand the perf implications
rg -n "uniqBy|JSON\.stringify" packages/solid-router/src/headContentUtils.tsx

Repository: TanStack/router

Length of output: 247


Strip nonce from dedup key and memoize the tag deduplication.

Using JSON.stringify(d) directly as the dedup key causes tags to be considered duplicates only when nonce values match. During SSR, the nonce is set from router.options.ssr?.nonce, but on the client it may differ or be removed, causing false duplicates on hydration. Additionally, calling JSON.stringify for every item on every dedup is unnecessary.

Create a stable tagKey function that excludes the nonce before stringifying, and wrap the dedup in Solid.createMemo() to prevent recomputation:

Proposed fix
 export const useTags = () => {
   const router = useRouter()
   const nonce = router.options.ssr?.nonce
+  const tagKey = (d: RouterManagedTag) => {
+    const attrs = d.attrs ? { ...d.attrs } : undefined
+    if (attrs && 'nonce' in attrs) delete (attrs as any).nonce
+    return JSON.stringify({ ...d, attrs })
+  }
 
   // ... meta/links/preloadLinks/styles/headScripts
 
-  return () =>
-    uniqBy(
-      [
-        ...meta(),
-        ...preloadLinks(),
-        ...links(),
-        ...styles(),
-        ...headScripts(),
-      ] as Array<RouterManagedTag>,
-      (d) => {
-        return JSON.stringify(d)
-      },
-    )
+  return Solid.createMemo(() =>
+    uniqBy(
+      [...meta(), ...preloadLinks(), ...links(), ...styles(), ...headScripts()],
+      tagKey,
+    ),
+  )
 }

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

🧹 Nitpick comments (3)
packages/react-router/src/headContentUtils.tsx (3)

11-18: Avoid match.meta! for strict TS; prefer a type guard.
The non-null assertion weakens safety and can hide real undefined cases (and violates the “strict type safety” guideline).

Proposed diff
   const routeMeta = useRouterState({
     select: (state) => {
-      return state.matches.map((match) => match.meta!).filter(Boolean)
+      return state.matches
+        .map((match) => match.meta)
+        .filter((m): m is NonNullable<typeof m> => Boolean(m))
     },
   })

91-129: Remove as any on structuralSharing (or fix the option type).
structuralSharing: true as any defeats strict typing and can hide an invalid option name at compile-time.

If useRouterState supports this option, prefer structuralSharing: true. If it doesn’t, either drop it here or extend useRouterState’s options type to include it (so this file stays type-safe). As per coding guidelines, strict TS should avoid any.

Also applies to: 131-156, 157-174, 175-192


193-205: Dedupe via JSON.stringify is fragile if tags ever contain non-JSON data.
If any RouterManagedTag fields can contain non-serializable values (e.g. Symbols/BigInt/circular references/React elements), JSON.stringify can throw and break rendering. If the type guarantees “JSON-only”, consider documenting/enforcing that at the type boundary; otherwise, consider a safer key (e.g., stable stringify of a normalized subset like tag + attrs + children with sorted keys).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff0508 and 5b75bfc.

📒 Files selected for processing (1)
  • packages/react-router/src/headContentUtils.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/react-router/src/headContentUtils.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/react-router/src/headContentUtils.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Applies to **/*.{js,ts,tsx} : Implement ESLint rules for router best practices using the ESLint plugin router
🧬 Code graph analysis (1)
packages/react-router/src/headContentUtils.tsx (2)
packages/solid-router/src/headContentUtils.tsx (2)
  • useTags (11-197)
  • uniqBy (199-209)
packages/vue-router/src/headContentUtils.tsx (2)
  • useTags (8-164)
  • uniqBy (166-176)
⏰ 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 (2)
packages/react-router/src/headContentUtils.tsx (2)

207-217: uniqBy implementation looks good.
Simple, linear-time, and generic.


20-90: Remove this comment; the JSON-LD escaping approach is correct and necessary.

The code properly uses escapeHtml() with dangerouslySetInnerHTML. The escapeHtml function applies Unicode escapes (e.g., \u003c for <, \u003e for >), not HTML entities. Since dangerouslySetInnerHTML sets innerHTML directly without parsing Unicode escape sequences, these are preserved as literal characters in the JSON. When the script executes and the JSON is parsed, the runtime correctly interprets \u003c as <. This prevents XSS by ensuring special characters cannot break out of the script tag while preserving JSON integrity—no double-escaping occurs.

Likely an incorrect or invalid review comment.

@schiller-manuel schiller-manuel merged commit 99bb8d2 into main Jan 11, 2026
6 checks passed
@schiller-manuel schiller-manuel deleted the dev-styles-refactor branch January 11, 2026 02:28
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.

2 participants