Skip to content

Conversation

@ScriptedAlchemy
Copy link

@ScriptedAlchemy ScriptedAlchemy commented Feb 9, 2026

Summary

  • Add rsbuild support across Start packages and plugin core with parity for compile/runtime behavior.
  • Add rsbuild integration for React, Solid, and Vue Start packages plus e2e coverage updates.
  • Keep CI workflow aligned after review by reverting unnecessary PR workflow audit changes on this branch.

Test plan

  • pnpm test:eslint
  • pnpm test:types
  • pnpm test:unit
  • Run relevant e2e suites for Start rsbuild changes

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Optional RsBuild bundler support with new plugin exports for React, Solid, Vue and a unified bundler runner for dev/build/preview.
  • Improvements

    • Better server-function manifest, redirect and error handling.
    • Tighter static asset serving and improved SSR router typing.
    • Automatic route auto-import/code-splitting support.
    • Replaced Tailwind with explicit plain CSS utilities.
  • Chores

    • Updated build entries, package exports, dev deps, and test snapshots.

cursoragent and others added 9 commits February 8, 2026 04:30
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…on-refinement-1b7e

Branch implementation refinement
Restore the PR GitHub Actions workflow to the main-branch behavior because the quality-audit PR changes were not needed for feat/rsbuild.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Adds RsBuild integration across Start: new rsbuild plugins, loaders, prerender/post-build tooling, server-fn manifest/resolver, bundler runner replacing direct Vite scripts, framework rsbuild plugin entries/exports, virtual module ID changes, CSS/test adjustments, and multiple package metadata/entry updates.

Changes

Cohort / File(s) Summary
E2E React start
e2e/react-start/basic/package.json, e2e/react-start/basic/rsbuild.config.ts, e2e/react-start/basic/scripts/run-bundler.mjs, e2e/react-start/basic/server.js, e2e/react-start/basic/src/..., e2e/react-start/basic/src/styles/app.css
Replace direct Vite scripts with node scripts/run-bundler.mjs, add rsbuild deps, add rsbuild config, adjust express static options, change CSS import and replace Tailwind with plain CSS utilities.
Start-plugin-core — RsBuild core & entries
packages/start-plugin-core/src/rsbuild/plugin.ts, packages/start-plugin-core/src/rsbuild/index.ts, packages/start-plugin-core/package.json, packages/start-plugin-core/vite.config.ts
Add TanStackStartRsbuildPluginCore, rsbuild entry aggregator, rsbuild exports in package.json, add unplugin dep and peer @rsbuild/core, expand vite entries to include rsbuild modules.
Start-plugin-core — Compiler / loaders / manifest / prerender
packages/start-plugin-core/src/rsbuild/*.ts (start-compiler-loader.ts, start-compiler-plugin.ts, start-manifest-plugin.ts, prerender.ts, post-server-build.ts, route-tree-loader.ts, route-tree-state.ts, injected-head-scripts-plugin.ts, start-storage-context-stub.ts)
Introduce StartCompiler loader, server-fn manifest generation & resolver, start manifest virtual module, full prerender pipeline, route-tree generation/state, injected head-scripts virtual module, and post-server-build orchestration.
Framework rsbuild plugins & package exports
packages/react-start/src/plugin/rsbuild.ts, packages/solid-start/src/plugin/rsbuild.ts, packages/vue-start/src/plugin/rsbuild.ts, packages/*-start/package.json
Add framework-specific tanstackStart rsbuild plugin modules and add ./plugin/rsbuild package exports plus optional peer @rsbuild/core entries for React/Solid/Vue and start-plugin-core.
Router plugin integrations
packages/router-plugin/src/rspack.ts, packages/router-plugin/src/vite.ts, packages/router-plugin/src/webpack.ts
Add auto-import plugin wiring (rspack/webpack), expose auto-import plugin and add public alias exports for generator/code-splitter/auto-import.
Start-server & virtual modules
packages/start-server-core/src/server-functions-handler.ts, packages/start-server-core/src/virtual-modules.ts, packages/start-server-core/src/tanstack-start.d.ts, packages/start-server-core/src/router-manifest.ts
Wrap actions via executableAction, add response-like/redirect helpers and serialized redirect handling; remove :v suffix from virtual module IDs and update type module declarations.
Start-client RPC & fetcher
packages/start-client-core/src/client-rpc/serverFnFetcher.ts
Add isResponseLike and parseRedirectFallback, centralize redirect/not-found detection across framed/ndjson/json paths.
Server-fn compiler & createServerFn handling
packages/start-plugin-core/src/start-compiler-plugin/handleCreateServerFn.ts, packages/start-plugin-core/src/start-compiler-plugin/types.ts, packages/start-plugin-core/tests/...
Add provider import tracking, emit/importer metadata (importerChunkIds/importerModuleId/importerPath), register handlers via globalThis.__tssServerFnHandlers, and update test snapshots.
Router core
packages/router-core/src/router.ts
Change commitLocation wrapper from async to synchronous wrapper; add Promise.resolve() inside startViewTransition inner wrapper.
Misc / config / lint
packages/react-router/eslint.config.ts, packages/start-plugin-core/src/index.ts, packages/start-plugin-core/src/start-compiler-plugin/compiler.ts, assorted package.json edits
Add ESLint override, export TanStackStartRsbuildPluginCore from start-plugin-core index, switch Babel import to namespace import, and small package.json/script updates.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer
    participant Runner as run-bundler.mjs
    participant Bundler as RsBuild/Vite
    participant PluginCore as TanStackStartRsbuildPluginCore
    participant Compiler as StartCompiler (loader)
    participant Server as Dev/Prod Server

    Dev->>Runner: npm run dev / build / preview
    Runner->>Bundler: select bundler (BUNDLER env) and forward command
    Bundler->>PluginCore: load rsbuild plugins
    PluginCore->>Compiler: invoke start-compiler-loader / route-tree-loader
    Compiler->>PluginCore: emit server-fn metadata & transformed modules
    PluginCore->>Bundler: produce virtual modules (manifest, server-fn resolver, injected scripts)
    alt build + prerender
        PluginCore->>Server: start built server artifact for prerender
        Server->>PluginCore: serve routes => prerender writes HTML
    end
    Bundler-->>Dev: build/dev complete (artifacts + manifest)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐰 I hopped a patch through build and test,
Replaced raw scripts with one small jest.
Plugins, prerender, manifests spun,
Server-fn handlers lined up, one by one.
Cheers — the Start garden grows in sun.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.97% 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 title 'feat: add rsbuild support for TanStack Start' accurately captures the main objective of this large changeset, which adds comprehensive rsbuild integration across multiple Start packages (React, Solid, Vue) and the plugin core infrastructure.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


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 Feb 9, 2026

View your CI Pipeline Execution ↗ for commit 4ccdde4

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

☁️ Nx Cloud last updated this comment at 2026-02-10 00:42:27 UTC

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

Caution

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

⚠️ Outside diff range comments (1)
packages/start-server-core/src/server-functions-handler.ts (1)

167-183: 🛠️ Refactor suggestion | 🟠 Major

Redundant redirect checks and dead code in isRedirectResponse.

Two issues in this block:

  1. getRedirectOptions(unwrapped) is computed at Line 167, but if it returns falsy, getRedirectOptions(unwrapped) is computed again at Line 177. Cache the result from Line 167.

  2. Boolean(getRedirectOptions(unwrapped)) at Line 177 is dead code: getRedirectOptions returns undefined when !isRedirect(value), so this expression can only be true when isRedirect(unwrapped) is already true. The condition simplifies to just isRedirect(unwrapped).

Proposed simplification
       const redirectOptions = getRedirectOptions(unwrapped)
       if (redirectOptions) {
         return Response.json(
           { ...redirectOptions, isSerializedRedirect: true },
           { headers: getResponseHeaders(unwrapped) },
         )
       }

       if (isResponseLike(unwrapped)) {
-        const isRedirectResponse =
-          isRedirect(unwrapped) || Boolean(getRedirectOptions(unwrapped))
-        if (isRedirectResponse) {
+        if (isRedirect(unwrapped)) {
           return unwrapped
         }
🤖 Fix all issues with AI agents
In @.github/workflows/autofix.yml:
- Around line 12-14: The workflow currently grants broad runner token
permissions (permissions: contents: write and pull-requests: write); change the
workflow-level permissions block to set contents: read (remove contents: write
and pull-requests: write) so the GITHUB_TOKEN only has read access; leave write
actions (pushing fixes/commenting) to the autofix.ci GitHub App and ensure the
permissions block references the symbol permissions with contents: read to
enforce least privilege.

In `@e2e/react-start/basic/rsbuild.config.ts`:
- Around line 1-7: Reorder the import statements so Node.js built-ins come
first: move the imports for node:path and node:url (path and fileURLToPath)
before external package imports like { defineConfig } from '@rsbuild/core', {
pluginReact } from '@rsbuild/plugin-react', and { tanstackStart } from
'@tanstack/react-start/plugin/rsbuild'; ensure symbols referenced in this file
(defineConfig, pluginReact, path, fileURLToPath, tanstackStart, isSpaMode,
isPrerender) are preserved and only the import order is changed to satisfy the
Node built-in-before-external rule.

In `@e2e/react-start/basic/scripts/run-bundler.mjs`:
- Around line 21-35: The run function's Promise currently listens only for the
child's 'close' event and can hang if spawn emits an 'error'; update the run
implementation (the run function that calls spawn and the child variable) to
attach a child.on('error', ...) handler that rejects the Promise with the error
(ensuring no double-resolve/reject race with the existing 'close' handler) and
cleanly propagates spawn errors to callers.

In `@packages/react-start/package.json`:
- Around line 109-119: The peerDependencies entry for "vite" is not marked
optional in peerDependenciesMeta, causing unnecessary warnings; update the
package.json so that under "peerDependenciesMeta" you add an object for "vite"
with "optional": true (mirroring how "@rsbuild/core" is marked) so the "vite"
peer dependency becomes optional for consumers — locate the "peerDependencies"
and "peerDependenciesMeta" blocks in packages/react-start/package.json and add
"vite": { "optional": true } to the meta object.

In `@packages/router-plugin/src/rspack.ts`:
- Around line 70-80: The public API surface is inconsistent across bundlers;
make webpack.ts and vite.ts match rspack.ts by exporting the same set of
symbols: add the function variants tanstackRouterAutoImport,
tanstackRouterCodeSplitter, tanstackRouterGenerator and the class-style
auto-import export (e.g., TanStackRouterWebpackAutoImport) to webpack.ts (and if
missing in vite.ts add corresponding TanStackRouterViteAutoImport or the chosen
class names), or alternatively remove the extra class/function duplicates from
rspack.ts—pick one consistent pattern and apply it across
TanStackRouterRspack/TanStackRouterWebpack/TanStackRouterVite plus their
Generator, CodeSplitter, AutoImport and function helpers so all bundles export
the same identifiers (e.g., TanStackRouter{Bundler},
TanStackRouterGenerator{Bundler}, TanStackRouterCodeSplitter{Bundler},
TanStackRouterAutoImport{Bundler}, tanstackRouterGenerator,
tanstackRouterCodeSplitter, tanstackRouterAutoImport, tanstackRouter).

In `@packages/start-client-core/src/client-rpc/serverFnFetcher.ts`:
- Around line 38-50: isResponseLike currently claims value is Response but only
duck-types status and headers.get; change it to narrow to a minimal interface
(e.g., ResponseLike with the exact members your downstream code uses such as
json, text, status, headers) or add explicit checks for the methods used before
narrowing so downstream calls (in serverFnFetcher where isResponseLike is
consumed at lines referenced) are safe; update the function signature from
"value is Response" to "value is ResponseLike" (or similar) and include checks
that headers.get, json, and text are functions if those are invoked later.

In `@packages/start-plugin-core/package.json`:
- Around line 96-104: Update the package.json peerDependenciesMeta so that the
"vite" peer dependency is marked optional like "@rsbuild/core": add a "vite": {
"optional": true } entry under "peerDependenciesMeta" to prevent peer dependency
warnings for users who only use rsbuild; ensure the JSON object includes this
key alongside the existing "@rsbuild/core" entry.

In `@packages/start-plugin-core/src/rsbuild/plugin.ts`:
- Around line 716-725: The code hardcodes outputFilename = 'server.js' which
breaks if rspack's output.filename changes; update the logic in plugin.ts around
where serverBuild/serverEntryPath are set to derive the actual emitted filename
from the build output (e.g., inspect the rspack build stats/compilation assets
or the configured output.filename pattern) or accept a configurable option for
the server entry filename; specifically, use the rspack compilation output
mapping to find the emitted server entry file in serverOutputDir (referencing
serverBuild, serverEntryPath, serverOutputDir, and outputFilename) and import
that resolved filename instead of assuming 'server.js'.
- Around line 598-606: The forEach callback currently uses a concise arrow (fn:
any) => fn(middlewares, context) which implicitly returns fn's value; change it
to a block-bodied function so it does not return a value (e.g., (fn: any) => {
fn(middlewares, context) }) or replace the forEach with an explicit loop; update
the invocation around existingSetupMiddlewares (the branch handling
Array.isArray(existingSetupMiddlewares)) to call each fn with middlewares and
context without returning values before calling setupMiddlewares(middlewares,
context).

In `@packages/start-plugin-core/src/rsbuild/post-server-build.ts`:
- Around line 38-50: The spread of startConfig.spa.prerender.headers can be
undefined and causes a TypeError; replace the optional-chaining or raw spread
with a nullish-coalescing fallback so the spread always gets an object—i.e.,
change ...(startConfig.spa.prerender.headers) to
...(startConfig.spa.prerender.headers ?? {}) in the startConfig.pages.push block
(the code that sets prerender.headers and adds HEADERS.TSS_SHELL) and make the
identical change in the other occurrence where startConfig.spa.prerender.headers
is spread.

In `@packages/start-plugin-core/src/rsbuild/prerender.ts`:
- Around line 233-241: The retry path is broken because addCrawlPageTask(page)
returns early due to seen.has(page.path); in the catch block inside the
prerender retry logic, remove page.path from the seen set before incrementing
retriesByPath and re-queueing so addCrawlPageTask can enqueue it again;
specifically, update the catch handling around addCrawlPageTask, referencing
seen, retriesByPath, addCrawlPageTask, and
prerenderOptions.retryCount/retryDelay to remove the path from seen, increment
retriesByPath, wait retryDelay, then call addCrawlPageTask(page).
- Around line 234-237: The retry log message is hardcoded to "in 500ms" but the
code uses prerenderOptions.retryDelay (which may be undefined); update the retry
logic in the block that checks retries < (prerenderOptions.retryCount ?? 0) so
the logger.warn prints the actual delay value (use a resolvedDelay =
prerenderOptions.retryDelay ?? 500 to mirror the intended default) and pass that
resolvedDelay into setTimeout; refer to the variables logger.warn,
prerenderOptions.retryDelay, and the retries check to locate and fix the code.

In `@packages/start-plugin-core/src/rsbuild/start-compiler-loader.ts`:
- Around line 28-29: The module-level Map compilers and object serverFnsById are
never cleared and can retain stale state across rebuilds; update the build
lifecycle (e.g., inside beforeRun or the start of each build cycle where
beforeRun currently clears the disk manifest) to explicitly clear these
in-memory caches: call compilers.clear() to reset the Map and reassign or delete
all keys from serverFnsById (or set serverFnsById = {} if scope allows) before
each build, ensuring any envName-based cached StartCompiler entries and
accumulated server function entries are removed between rebuilds.

In `@packages/start-plugin-core/src/rsbuild/start-compiler-plugin.ts`:
- Around line 174-185: The getManifest() function currently sets the
module-level cached variable to {} inside the catch, causing transient read
errors to be cached permanently; change the logic so cached is only assigned on
successful read/JSON.parse (using fs.readFileSync(manifestPath, 'utf-8') and
JSON.parse(raw)) and do not set cached in the catch block—simply return an empty
object (or undefined) on error so subsequent calls will retry reading the file;
reference getManifest, cached, manifestPath, fs.readFileSync and JSON.parse to
locate and update the code.
- Around line 76-93: In generateManifestModule, id and fn.functionName are
interpolated into a single-quoted JS literal unescaped; replace those raw
interpolations with safe JSON.stringify usage (e.g., JSON.stringify(id) for the
property key and JSON.stringify(fn.functionName) for the functionName value) so
any quotes/backslashes are escaped like fn.extractedFilename currently is; keep
the existing importer and conditional isClientReferenced logic
(includeClientReferencedCheck and fn.isClientReferenced) unchanged while
switching the string construction to use the JSON-encoded values.

In `@packages/start-plugin-core/src/rsbuild/start-router-plugin.ts`:
- Around line 71-79: The function resolveLoaderPath is duplicated; extract it
into a single shared utility module (e.g., create and export resolveLoaderPath
from a new util file) and replace the duplicate implementations by importing
that exported resolveLoaderPath in both locations that currently define it (the
copy in start-router-plugin.ts and the other copy). Ensure the new util
preserves the same logic (uses fileURLToPath(import.meta.url), path.resolve, and
fs.existsSync) and update the imports in the modules that referenced the local
function to import the shared symbol instead.
- Around line 17-69: moduleDeclaration logic is duplicated across
moduleDeclaration (in start-router-plugin.ts), buildRouteTreeModuleDeclaration
(in plugin.ts), and the equivalent in start-router-plugin/plugin.ts; extract
this into a single shared utility function (e.g., createModuleDeclaration) that
accepts the same parameters (startFilePath, routerFilePath, corePluginOpts,
generatedRouteTreePath) and reuse it from all three locations. Implement the
helper to contain getImportPath and the string-building logic currently inside
moduleDeclaration, export it, then replace the existing inline functions by
importing and calling the new helper from each file (ensure callers still
receive the same returned string shape and update any referenced names like
getRouter/startInstance/createStart to match the helper’s implementation).

In `@packages/start-server-core/src/server-functions-handler.ts`:
- Around line 398-411: The duck-typing in isResponseLike currently only asserts
headers?.get exists but later code (in handlers that call isResponseLike) calls
headers.set; update isResponseLike to also verify that the headers object
exposes a set function (e.g., check typeof headers?.set === 'function') so
objects that only implement get won't be treated as Response-like, and keep the
rest of the existing guards (status and headers presence) intact; locate the
guard in function isResponseLike and add the set check to the returned
predicate.
- Around line 324-340: The catch block redundantly calls
getRedirectOptions(error) twice; compute const redirectOptions =
getRedirectOptions(error) once at the start of the isResponseLike(error) branch
and reuse it for the server-fn serialized redirect branch and for computing
isRedirectResponse (use isRedirect(error) || Boolean(redirectOptions)); then
remove the duplicate getRedirectOptions call so the logic in the isResponseLike
catch path (the redirectOptions && isServerFn serialized Response,
isRedirectResponse check, and setting X_TSS_RAW_RESPONSE) all use the single
redirectOptions variable.
🧹 Nitpick comments (22)
packages/router-plugin/src/rspack.ts (1)

47-49: Missing JSDoc on TanStackRouterAutoImportRspack.

TanStackRouterGeneratorRspack and TanStackRouterCodeSplitterRspack both have @example JSDoc blocks showing usage in an rsbuild config. Consider adding one here for consistency.

📝 Suggested JSDoc
+/**
+ * `@example`
+ * ```ts
+ * export default defineConfig({
+ *   // ...
+ *   tools: {
+ *     rspack: {
+ *       plugins: [TanStackRouterAutoImportRspack()],
+ *     },
+ *   },
+ * })
+ * ```
+ */
 const TanStackRouterAutoImportRspack = /* `#__PURE__` */ createRspackPlugin(
   unpluginRouteAutoImportFactory,
 )
e2e/react-start/basic/src/styles/app.css (1)

26-28: Blanket outline: none !important removes keyboard focus indicators.

Even in an e2e test app, removing all outlines via .using-mouse * can mask accessibility regressions if e2e tests ever assert focus behavior. Consider using :focus-visible instead so keyboard focus indicators remain intact while hiding outlines for mouse clicks:

:focus:not(:focus-visible) {
  outline: none;
}
packages/start-plugin-core/src/start-compiler-plugin/handleCreateServerFn.ts (1)

491-530: Minor: globalHandlers AST node created but not reused in initHandlers.

Line 492 creates globalHandlers as a MemberExpression, but initHandlers (Lines 496-512) builds its own duplicate MemberExpression nodes for globalThis.__tssServerFnHandlers. Consider reusing globalHandlers via t.cloneNode() to reduce AST duplication.

Proposed simplification
     const globalHandlers = t.memberExpression(
       t.identifier('globalThis'),
       t.identifier('__tssServerFnHandlers'),
     )
     const initHandlers = t.expressionStatement(
       t.assignmentExpression(
         '=',
-        t.memberExpression(
-          t.identifier('globalThis'),
-          t.identifier('__tssServerFnHandlers'),
-        ),
+        t.cloneNode(globalHandlers),
         t.logicalExpression(
           '||',
-          t.memberExpression(
-            t.identifier('globalThis'),
-            t.identifier('__tssServerFnHandlers'),
-          ),
+          t.cloneNode(globalHandlers),
           t.arrayExpression([]),
         ),
       ),
     )
packages/start-plugin-core/src/rsbuild/start-compiler-plugin.ts (4)

104-155: Substantial code duplication between generateManifestModule and generateManifestModuleFromFile.

The getServerFnById function body (fallback resolution with fnModule, action, serverFnMeta, globalThis.__tssServerFnHandlers) is duplicated nearly verbatim between the two generators (Lines 107–154 and Lines 188–248). Any future bug fix or behavior change would need to be applied in both places.

Consider extracting the shared resolution logic into a helper string or a separate runtime utility that both code paths can reference.

Also applies to: 188-248


511-520: Static analysis flagged ReDoS risk from variable in regex — low risk here but worth sanitizing.

handlerVar (Line 515) is derived from a prior regex match constraining it to [A-Za-z_$][\w$]* (valid JS identifiers), so the practical ReDoS risk is minimal. However, defensively escaping the variable is low-cost and eliminates the static analysis warning.

♻️ Escape handlerVar before regex construction
+           const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
            const exportMatch = scope.match(
-             new RegExp(`([A-Za-z_$][\\w$]*):\\(\\)=>${handlerVar}`)
+             new RegExp(`([A-Za-z_$][\\w$]*):\\(\\)=>${escapeRegex(handlerVar)}`)
            )

353-362: Asset content scanning for every unmatched server function may be slow on large builds.

findAssetMatch (Lines 353–362) reads and scans potentially every JS asset for each server function that doesn't have a direct module-graph match. This is O(N × M) where N = unmatched server fns and M = asset count, and each iteration may trigger filesystem reads (Line 346–348 fallback).

Consider caching asset content reads (they're already partially used via getAssetContent) or building an inverted index of tokens → asset names once per compilation.


39-42: Extensive use of any types across rspack/webpack interop layer.

CompilationModule, the various function parameters (compiler: any, compilation: any, module: any, chunk: any), and helper functions all rely on any. While this is understandable given the untyped nature of rspack/webpack plugin APIs, consider using @rspack/core types or at minimum defining minimal structural interfaces for the accessed properties to catch shape mismatches at compile time.

packages/start-plugin-core/src/rsbuild/prerender.ts (2)

42-48: validateAndNormalizePrerenderPages is called twice on the same pages.

Lines 45–48 normalize startConfig.pages and then Lines 122–125 inside prerenderPages normalize them again. The second call is redundant since the pages are already normalized and mutated in-place via startConfig.pages.

♻️ Remove the duplicate call inside prerenderPages
     const routerBasePath = joinURL('/', startConfig.router.basepath ?? '')
-
-    const routerBaseUrl = new URL(routerBasePath, 'http://localhost')
-    startConfig.pages = validateAndNormalizePrerenderPages(
-      startConfig.pages,
-      routerBaseUrl,
-    )
-
     startConfig.pages.forEach((page) => addCrawlPageTask(page))

Also applies to: 119-125


97-110: extractLinks won't capture links with explicit basepath or protocol-relative URLs.

The regex on Line 104 only matches href values starting with / or ./. Links written as relative paths without ./ (e.g., href="subpage") or with the full origin (e.g., href="http://localhost/foo") won't be crawled. This may be acceptable for the intended use case, but it's worth noting.

packages/solid-start/src/plugin/rsbuild.ts (1)

6-9: Consider importing RsbuildPlugin from @rsbuild/core instead of defining a local type with any.

The local type erases the setup API parameter type. Since @rsbuild/core is already declared as a peer dependency, importing the canonical type would provide compile-time safety for plugin implementations.

That said, this avoids a hard import dependency at the cost of type safety, which may be intentional to keep the module loadable without @rsbuild/core installed.

Suggested improvement
-type RsbuildPlugin = {
-  name: string
-  setup: (api: any) => void
-}
+import type { RsbuildPlugin } from '@rsbuild/core'

As per coding guidelines, "Use TypeScript strict mode with extensive type safety for all code" (**/*.{ts,tsx}).

packages/vue-start/src/plugin/rsbuild.ts (1)

6-9: Consider sharing the RsbuildPlugin type across framework plugins.

This identical type definition is duplicated in react-start, solid-start, and vue-start. Consider exporting it from @tanstack/start-plugin-core/rsbuild (or colocating it there) to avoid drift. Also, setup: (api: any) => void loses type safety — if @rsbuild/core exposes a proper RsbuildPlugin type, importing it (behind the optional peer dep) would be preferable.

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

1-3: Vite-named exports from an rsbuild barrel may cause confusion.

VITE_ENVIRONMENT_NAMES and resolveViteId carry Vite-specific naming but are exported from the rsbuild entry point. If these are intentionally shared across bundler backends, consider renaming to bundler-agnostic names (e.g., ENVIRONMENT_NAMES, resolveModuleId) to avoid confusion for consumers.

packages/start-plugin-core/src/rsbuild/route-tree-loader.ts (1)

3-8: Tighten loader context and options typing to comply with strict mode guidelines.

The this: any and routerConfig?: any bypass TypeScript strict mode. LoaderContext is available from @rspack/core (v1.2.2), and Config is available from @tanstack/router-generator. Update the types to improve type safety per the strict mode requirement:

Suggested improvement
+import type { Config } from '@tanstack/router-generator'
+import type { LoaderContext } from '@rspack/core'
 import { getClientRouteTreeContent } from './route-tree-state'
 
-export default function routeTreeLoader(this: any) {
+export default function routeTreeLoader(this: LoaderContext) {
   const callback = this.async()
-  const options = this.getOptions() as {
-    routerConfig?: any
+  const options = this.getOptions() as {
+    routerConfig?: Config
     root?: string
   }
packages/start-plugin-core/src/rsbuild/start-manifest-plugin.ts (2)

246-277: Rspack plugin uses any for compiler and stats — loses type safety.

The apply(compiler: any) and stats: any parameters bypass TypeScript's type system entirely. While rspack types may not be readily available, consider importing Compiler and Stats types from @rspack/core if they're a dependency, or at minimum add a local interface for the subset of API used.

As per coding guidelines, TypeScript strict mode with extensive type safety should be used.


279-311: Virtual module caches manifest permanently after first read — no invalidation path.

The generated virtual module (lines 296-305) uses a module-scoped cached variable. Once the manifest is read successfully, subsequent calls always return the stale cached value, even if the manifest file on disk changes. This is likely acceptable for production builds, but during development or watch-mode rebuilds this could serve outdated data.

If this is intentionally production-only, a brief comment documenting that assumption would help future maintainers.

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

98-122: Confusing control flow in getRouteTreeFileFooter — conditional assignment is immediately overwritten.

Lines 105-111 conditionally assign routeTreeFileFooter from the original config, but line 112 unconditionally reassigns it. The original footer content is preserved via the spread on line 119, so the logic is functionally correct, but the intermediate assignment to the outer routeTreeFileFooter variable is misleading.

A clearer approach would use a local variable for the parsed original footer:

♻️ Suggested clarification
   function getRouteTreeFileFooter() {
     if (routeTreeFileFooter) {
       return routeTreeFileFooter
     }
     const { startConfig, resolvedStartConfig } = getConfig()
     const ogRouteTreeFileFooter = startConfig.router.routeTreeFileFooter
-    if (ogRouteTreeFileFooter) {
-      if (Array.isArray(ogRouteTreeFileFooter)) {
-        routeTreeFileFooter = ogRouteTreeFileFooter
-      } else {
-        routeTreeFileFooter = ogRouteTreeFileFooter()
-      }
-    }
-    routeTreeFileFooter = [
+    let existingFooter: Array<string> = []
+    if (ogRouteTreeFileFooter) {
+      existingFooter = Array.isArray(ogRouteTreeFileFooter)
+        ? ogRouteTreeFileFooter
+        : ogRouteTreeFileFooter()
+    }
+    routeTreeFileFooter = [
       moduleDeclaration({
         generatedRouteTreePath: getGeneratedRouteTreePath(),
         corePluginOpts,
         startFilePath: resolvedStartConfig.startFilePath,
         routerFilePath: resolvedStartConfig.routerFilePath,
       }),
-      ...(routeTreeFileFooter ?? []),
+      ...existingFooter,
     ]
     return routeTreeFileFooter
   }
packages/start-plugin-core/src/rsbuild/plugin.ts (3)

30-33: RsbuildPlugin type uses api: any — consider narrowing.

While the full rsbuild API type may be complex, defining at least the methods used (modifyRsbuildConfig, onAfterBuild, onAfterStartProdServer, context) as a partial interface would provide better type safety across this file.

As per coding guidelines, TypeScript strict mode with extensive type safety should be used.


100-119: mergeRspackConfig shallow-merges only plugins, module.rules, and resolve.alias — other nested keys may be silently dropped.

If base has keys under module other than rules (e.g., parser, generator), the spread ...next?.module will overwrite them. The same applies to resolve keys beyond alias (e.g., extensions, mainFields).

This is acceptable if the merge is only used for known, controlled configurations within this file, but it could cause subtle breakage if user-provided rsbuild configs include additional nested keys.

♻️ More robust merge for resolve
     resolve: {
       ...base?.resolve,
       ...next?.resolve,
       alias: {
         ...(base?.resolve?.alias ?? {}),
         ...(next?.resolve?.alias ?? {}),
       },
+      extensions: [
+        ...(base?.resolve?.extensions ?? []),
+        ...(next?.resolve?.extensions ?? []),
+      ],
     },

406-418: Hardcoded monorepo path packages/start-client-core/dist/esm only resolves in the monorepo workspace.

Line 406-408 builds a path relative to root that assumes the TanStack monorepo structure. For published packages consumed by end users, this path won't exist. The existsSync guard (line 415) prevents a crash, and the regex fallback (line 411, pushed on line 418) provides the actual matching — so this is safe. But the dead code path is confusing.

Consider adding a comment explaining this is a monorepo dev-time optimization, or remove the existsSync branch entirely and rely solely on the regex.

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

31-38: Sync I/O in appendServerFnsToManifest blocks the event loop.

fs.mkdirSync and fs.appendFileSync are used inside what is ultimately called from an async loader. While this is typically acceptable in build tooling, it could become a bottleneck with many server functions. The mkdirSync call on every append is also redundant after the first call.

♻️ Optional: cache directory creation
+const createdDirs = new Set<string>()
 const appendServerFnsToManifest = (
   manifestPath: string,
   data: Record<string, ServerFn>,
 ) => {
   if (!manifestPath || Object.keys(data).length === 0) return
-  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
+  const dir = path.dirname(manifestPath)
+  if (!createdDirs.has(dir)) {
+    fs.mkdirSync(dir, { recursive: true })
+    createdDirs.add(dir)
+  }
   fs.appendFileSync(manifestPath, `${JSON.stringify(data)}\n`)
 }

42-54: getTransformCodeFilterForEnv recomputes patterns on every call.

KindDetectionPatterns and LookupKindsPerEnv are presumably static. This function is called for every file processed by the loader (via shouldTransformCode). Consider caching the result per env:

♻️ Simple memoization
+const transformCodeFilterCache = new Map<string, Array<RegExp>>()
 function getTransformCodeFilterForEnv(env: 'client' | 'server'): Array<RegExp> {
+  const cached = transformCodeFilterCache.get(env)
+  if (cached) return cached
   const validKinds = LookupKindsPerEnv[env]
   const patterns: Array<RegExp> = []
   for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array<
     [keyof typeof KindDetectionPatterns, RegExp]
   >) {
     if (validKinds.has(kind)) {
       patterns.push(pattern)
     }
   }
+  transformCodeFilterCache.set(env, patterns)
   return patterns
 }

114-151: resolveId Promise may never settle if resolver throws synchronously.

If the resolver function (line 133) throws synchronously instead of calling the callback, the Promise will hang indefinitely. Wrapping the resolver call in a try/catch inside the Promise executor would make this more resilient:

🛡️ Suggested defensive fix
   return new Promise((resolve) => {
     const resolver =
       loaderContext.getResolve?.({
         conditionNames: ['import', 'module', 'default'],
       }) ?? loaderContext.resolve

-    resolver(resolveContext, source, (err: Error | null, result?: string) => {
+    try {
+      resolver(resolveContext, source, (err: Error | null, result?: string) => {
-      if (!err && result) {
-        resolve(cleanId(result))
-        return
-      }
-      try {
-        const resolved = require.resolve(source, {
-          paths: [
-            baseContext,
-            loaderContext.rootContext || loaderContext.context,
-          ].filter(Boolean),
-        })
-        resolve(cleanId(resolved))
-      } catch {
-        resolve(null)
-      }
-    })
+        if (!err && result) {
+          resolve(cleanId(result))
+          return
+        }
+        try {
+          const resolved = require.resolve(source, {
+            paths: [
+              baseContext,
+              loaderContext.rootContext || loaderContext.context,
+            ].filter(Boolean),
+          })
+          resolve(cleanId(resolved))
+        } catch {
+          resolve(null)
+        }
+      })
+    } catch {
+      resolve(null)
+    }
   })

Comment on lines +17 to +69
function moduleDeclaration({
startFilePath,
routerFilePath,
corePluginOpts,
generatedRouteTreePath,
}: {
startFilePath: string | undefined
routerFilePath: string
corePluginOpts: TanStackStartVitePluginCoreOptions
generatedRouteTreePath: string
}): string {
function getImportPath(absolutePath: string) {
let relativePath = path.relative(
path.dirname(generatedRouteTreePath),
absolutePath,
)

if (!relativePath.startsWith('.')) {
relativePath = './' + relativePath
}

relativePath = relativePath.split(path.sep).join('/')
return relativePath
}

const result: Array<string> = [
`import type { getRouter } from '${getImportPath(routerFilePath)}'`,
]
if (startFilePath) {
result.push(
`import type { startInstance } from '${getImportPath(startFilePath)}'`,
)
} else {
result.push(
`import type { createStart } from '@tanstack/${corePluginOpts.framework}-start'`,
)
}
result.push(
`declare module '@tanstack/${corePluginOpts.framework}-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>`,
)
if (startFilePath) {
result.push(
` config: Awaited<ReturnType<typeof startInstance.getOptions>>`,
)
}
result.push(` }
}`)

return result.join('\n')
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the plugin.ts file in the repository
fd -t f "plugin.ts" packages/start-plugin-core

Repository: TanStack/router

Length of output: 966


🏁 Script executed:

#!/bin/bash
# Extract the moduleDeclaration function from start-router-plugin.ts
cat -n packages/start-plugin-core/src/rsbuild/start-router-plugin.ts | sed -n '17,69p'

Repository: TanStack/router

Length of output: 1750


🏁 Script executed:

#!/bin/bash
# Verify the file path and check for buildRouteTreeModuleDeclaration
fd -t f "plugin.ts" packages/start-plugin-core -x cat -n {} | grep -A 50 "buildRouteTreeModuleDeclaration"

Repository: TanStack/router

Length of output: 4143


🏁 Script executed:

#!/bin/bash
# Check which plugin.ts file contains buildRouteTreeModuleDeclaration
grep -l "buildRouteTreeModuleDeclaration" packages/start-plugin-core/src/**/*.ts

Repository: TanStack/router

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Get context on where moduleDeclaration is used in start-router-plugin.ts
rg "moduleDeclaration" packages/start-plugin-core/src/rsbuild/start-router-plugin.ts -B 2 -A 2

Repository: TanStack/router

Length of output: 329


🏁 Script executed:

#!/bin/bash
# Get context on where buildRouteTreeModuleDeclaration is used in plugin.ts
rg "buildRouteTreeModuleDeclaration" packages/start-plugin-core/src/start-router-plugin/plugin.ts -B 2 -A 2

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check the correct file with buildRouteTreeModuleDeclaration
rg "buildRouteTreeModuleDeclaration" packages/start-plugin-core/src/rsbuild/plugin.ts -B 2 -A 2

Repository: TanStack/router

Length of output: 425


🏁 Script executed:

#!/bin/bash
# Get the full content of both files to understand the context
echo "=== start-router-plugin.ts (lines 1-80) ==="
head -80 packages/start-plugin-core/src/rsbuild/start-router-plugin.ts

echo ""
echo "=== rsbuild/plugin.ts (lines 40-95) ==="
sed -n '40,95p' packages/start-plugin-core/src/rsbuild/plugin.ts

Repository: TanStack/router

Length of output: 4004


🏁 Script executed:

#!/bin/bash
# Check where moduleDeclaration is called in start-router-plugin.ts
rg "moduleDeclaration\(" packages/start-plugin-core/src/rsbuild/start-router-plugin.ts -B 5 -A 2

Repository: TanStack/router

Length of output: 610


🏁 Script executed:

#!/bin/bash
# Check where buildRouteTreeModuleDeclaration is called in plugin.ts
rg "buildRouteTreeModuleDeclaration\(" packages/start-plugin-core/src/rsbuild/plugin.ts -B 2 -A 5

Repository: TanStack/router

Length of output: 605


🏁 Script executed:

#!/bin/bash
# Check if there are any other similar functions in the codebase
rg "declare module '@tanstack/.*-start'" packages/start-plugin-core/src --type ts -l

Repository: TanStack/router

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Check if buildRouteTreeModuleDeclaration is exported or could be imported
head -20 packages/start-plugin-core/src/rsbuild/plugin.ts

Repository: TanStack/router

Length of output: 810


🏁 Script executed:

#!/bin/bash
# Check if there's a third duplicate in start-router-plugin/plugin.ts
rg "function.*ModuleDeclaration|function moduleDeclaration" packages/start-plugin-core/src/start-router-plugin/plugin.ts -B 2 -A 30

Repository: TanStack/router

Length of output: 888


🏁 Script executed:

#!/bin/bash
# Check if buildRouteTreeModuleDeclaration is exported from plugin.ts
grep -A 2 "export.*buildRouteTreeModuleDeclaration\|export.*moduleDeclaration" packages/start-plugin-core/src/rsbuild/plugin.ts

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check file sizes and structure of all three files to understand the context better
echo "=== start-router-plugin.ts structure ==="
wc -l packages/start-plugin-core/src/rsbuild/start-router-plugin.ts
grep "^function\|^export\|^const.*=" packages/start-plugin-core/src/rsbuild/start-router-plugin.ts | head -20

echo ""
echo "=== rsbuild/plugin.ts structure ==="
wc -l packages/start-plugin-core/src/rsbuild/plugin.ts
grep "^function\|^export\|^const.*=" packages/start-plugin-core/src/rsbuild/plugin.ts | head -20

echo ""
echo "=== start-router-plugin/plugin.ts structure ==="
wc -l packages/start-plugin-core/src/start-router-plugin/plugin.ts
grep "^function\|^export\|^const.*=" packages/start-plugin-core/src/start-router-plugin/plugin.ts | head -20

Repository: TanStack/router

Length of output: 1044


🏁 Script executed:

#!/bin/bash
# Verify the exact differences in parameter handling between the two functions
echo "=== moduleDeclaration parameter signature ==="
sed -n '17,27p' packages/start-plugin-core/src/rsbuild/start-router-plugin.ts

echo ""
echo "=== buildRouteTreeModuleDeclaration parameter signature ==="
sed -n '44,49p' packages/start-plugin-core/src/rsbuild/plugin.ts

Repository: TanStack/router

Length of output: 595


🏁 Script executed:

#!/bin/bash
# Get the moduleDeclaration function from start-router-plugin/plugin.ts to confirm triple duplication
sed -n '/^function moduleDeclaration/,/^}/p' packages/start-plugin-core/src/start-router-plugin/plugin.ts | head -60

Repository: TanStack/router

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Get line numbers of moduleDeclaration in start-router-plugin/plugin.ts
rg -n "^function moduleDeclaration" packages/start-plugin-core/src/start-router-plugin/plugin.ts

Repository: TanStack/router

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Get the full moduleDeclaration from start-router-plugin/plugin.ts
sed -n '44,100p' packages/start-plugin-core/src/start-router-plugin/plugin.ts

Repository: TanStack/router

Length of output: 1636


🏁 Script executed:

#!/bin/bash
# Confirm all three functions generate identical logic - compare key parts
echo "=== Checking all three module declaration functions ==="
echo "1. start-router-plugin.ts (rsbuild):"
sed -n '42,69p' packages/start-plugin-core/src/rsbuild/start-router-plugin.ts | tail -20

echo ""
echo "2. rsbuild/plugin.ts:"
sed -n '61,88p' packages/start-plugin-core/src/rsbuild/plugin.ts | tail -20

echo ""
echo "3. start-router-plugin/plugin.ts:"
sed -n '57,85p' packages/start-plugin-core/src/start-router-plugin/plugin.ts | tail -20

Repository: TanStack/router

Length of output: 1666


Extract duplicated module declaration logic into a shared utility.

The moduleDeclaration function here is duplicated across three locations in the codebase:

  • packages/start-plugin-core/src/rsbuild/start-router-plugin.ts (lines 17-69)
  • packages/start-plugin-core/src/rsbuild/plugin.ts as buildRouteTreeModuleDeclaration (lines 44-88)
  • packages/start-plugin-core/src/start-router-plugin/plugin.ts (lines 32-81)

All three generate identical TypeScript module augmentation declarations with the same logic for computing import paths, building conditional imports, and generating the Register interface. Create a shared utility function (e.g., in a common helpers file) and import it from all three locations.

🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/start-router-plugin.ts` around lines
17 - 69, moduleDeclaration logic is duplicated across moduleDeclaration (in
start-router-plugin.ts), buildRouteTreeModuleDeclaration (in plugin.ts), and the
equivalent in start-router-plugin/plugin.ts; extract this into a single shared
utility function (e.g., createModuleDeclaration) that accepts the same
parameters (startFilePath, routerFilePath, corePluginOpts,
generatedRouteTreePath) and reuse it from all three locations. Implement the
helper to contain getImportPath and the string-building logic currently inside
moduleDeclaration, export it, then replace the existing inline functions by
importing and calling the new helper from each file (ensure callers still
receive the same returned string shape and update any referenced names like
getRouter/startInstance/createStart to match the helper’s implementation).

Comment on lines +71 to +79
function resolveLoaderPath(relativePath: string) {
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const basePath = path.resolve(currentDir, relativePath)
const jsPath = `${basePath}.js`
const tsPath = `${basePath}.ts`
if (fs.existsSync(jsPath)) return jsPath
if (fs.existsSync(tsPath)) return tsPath
return jsPath
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicate: resolveLoaderPath is also duplicated in plugin.ts (lines 173-181).

Identical implementation in both files. Extract to a shared utility.

🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/start-router-plugin.ts` around lines
71 - 79, The function resolveLoaderPath is duplicated; extract it into a single
shared utility module (e.g., create and export resolveLoaderPath from a new util
file) and replace the duplicate implementations by importing that exported
resolveLoaderPath in both locations that currently define it (the copy in
start-router-plugin.ts and the other copy). Ensure the new util preserves the
same logic (uses fileURLToPath(import.meta.url), path.resolve, and
fs.existsSync) and update the imports in the modules that referenced the local
function to import the shared symbol instead.

Re-run CI after an external pkg.pr.new Cloudflare worker error in the Preview publish step.

Co-authored-by: Cursor <cursoragent@cursor.com>
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 9, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 4ccdde4

ScriptedAlchemy and others added 2 commits February 9, 2026 13:27
Resolve branch-introduced review findings by hardening response/redirect handling, fixing prerender retry behavior, tightening workflow and peer dependency metadata, and improving rsbuild/webpack/vite plugin consistency.

Co-authored-by: Cursor <cursoragent@cursor.com>
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: 7

Caution

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

⚠️ Outside diff range comments (1)
packages/start-client-core/src/client-rpc/serverFnFetcher.ts (1)

332-339: ⚠️ Potential issue | 🟠 Major

Type mismatch: processServerFnResponse parameter expects Response but receives ResponseLike.

At line 276, processServerFnResponse is called with response typed as ResponseLike (line 221), but the function parameter (line 337) is typed as Response. Since ResponseLike is Pick<Response, 'status' | 'ok' | 'headers' | 'body' | 'json' | 'text'>, it's a structural subset and not assignable to the full Response type under strict TypeScript mode.

Update the parameter type to ResponseLike for consistency:

Proposed fix
 async function processServerFnResponse({
   response,
   onMessage,
   onError,
 }: {
-  response: Response
+  response: ResponseLike
   onMessage: (msg: any) => any
   onError?: (msg: string, error?: any) => void
 }) {
🤖 Fix all issues with AI agents
In `@packages/start-plugin-core/src/rsbuild/plugin.ts`:
- Around line 386-394: The synchronous deletion using
fs.rmSync(generatedRouteTreePath) can race with concurrent generators; instead,
perform a safe, non-racing removal: attempt an atomic rename of
generatedRouteTreePath to a temp/backup name (e.g., generatedRouteTreePath +
'.stale') inside a try/catch and only unlink the backup afterwards, or at
minimum replace fs.rmSync with an unlink/rename wrapped in try/catch that
ignores ENOENT/EBUSY errors; update the code around generatedRouteTreePath /
existingTree / registerDeclaration to use this safe rename-then-remove or
try/catch-unlink pattern rather than calling fs.rmSync directly.
- Around line 558-590: The dev middleware inside setupMiddlewares mutates
req.url by assigning joinURL(resolvedStartConfig.viteAppBase, req.url ?? '/'),
which can break downstream middleware; instead create a new request object with
the modified URL (e.g., construct a new Request/NodeRequest using the composed
URL) and pass that to serverBuild.fetch while leaving the original req.url
untouched, updating the NodeRequest construction site (where NodeRequest({ req,
res }) is created) to use the new URL and returning sendNodeResponse(res,
webRes) as before; apply the same non-mutating change to the analogous
production middleware to keep behavior consistent.

In `@packages/start-plugin-core/src/rsbuild/prerender.ts`:
- Around line 86-95: The catch in the prerendering block swallows errors from
prerenderPages (and upstream addCrawlPageTask/queue.start), so update the catch
in the async block around prerenderPages to log the error via
logger.error(error) and then rethrow the error so callers (e.g.,
postServerBuildRsbuild) observe the failure; keep the existing logging but add a
`throw error` after it to propagate the failure.
- Around line 186-189: The code computing isSpaShell risks a TypeError because
it uses optional chaining on startConfig.spa but then accesses
.prerender.outputPath without guarding against prerender being undefined; update
the check in the isSpaShell assignment to safely access prerender (e.g., use
startConfig.spa?.prerender?.outputPath or check startConfig.spa?.prerender
before reading outputPath) so comparison with cleanPagePath is only performed
when outputPath is defined, preserving the existing isSpaShell logic and
variable names.
- Line 50: The code sets process.env.TSS_PRERENDERING = 'true' but never resets
it and also swallows errors; modify the prerender flow (the function that sets
process.env.TSS_PRERENDERING in
packages/start-plugin-core/src/rsbuild/prerender.ts) to ensure the env var is
restored (delete or reset to previous value) in a finally block after
prerendering completes, and change the catch block to rethrow the caught error
so failures are propagated.

In `@packages/start-plugin-core/src/rsbuild/start-compiler-loader.ts`:
- Around line 163-182: The loadModule function currently short-circuits relative
ids (cleaned.startsWith('.') or '/') and calls fsp.readFile with cleaned, which
reads from process.cwd(); instead, always call resolveId(loaderContext, cleaned)
to get a proper module path (use ((await resolveId(loaderContext, cleaned)) ??
cleaned) only as a fallback for already-absolute paths), then skip if the
resolved result is falsy or contains '\0', and finally use the resolvedPath for
fsp.readFile and compiler.ingestModule; update the logic around cleanId,
resolveId, resolvedPath, fsp.readFile, and compiler.ingestModule accordingly so
relative imports are resolved relative to the importer rather than the CWD.

In `@packages/start-plugin-core/src/rsbuild/start-compiler-plugin.ts`:
- Around line 254-270: The plugin currently only hooks compiler.hooks.beforeRun
so resetServerFnCompilerState() and the tempManifestPath removal (fsp.rm) won’t
run in watch mode; update createServerFnManifestRspackPlugin to also tapPromise
into compiler.hooks.watchRun (using the same callback) so both
resetServerFnCompilerState() and await fsp.rm(tempManifestPath, { force: true })
run for watch builds as well, referencing the existing beforeRun handler logic
to avoid duplicating behavior.
🧹 Nitpick comments (15)
packages/router-plugin/src/vite.ts (1)

37-37: Consider adding a @deprecated notice on tanStackRouterCodeSplitter.

The new tanstackRouterCodeSplitter alias aligns naming with the rest of the exports (tanstackRouter, tanstackRouterGenerator, tanstackRouterAutoImport) — nice cleanup. However, unlike TanStackRouterVite (Line 51-52), the old tanStackRouterCodeSplitter doesn't carry a @deprecated JSDoc. Adding one would guide consumers toward the consistent name.

Suggested change
+/**
+ * `@deprecated` Use `tanstackRouterCodeSplitter` instead.
+ */
 const tanStackRouterCodeSplitter = createVitePlugin(
   unpluginRouterCodeSplitterFactory,
 )

Also applies to: 60-61

packages/router-plugin/src/webpack.ts (1)

36-38: Missing JSDoc @example block for consistency with sibling plugins.

TanStackRouterGeneratorWebpack (line 10) and TanStackRouterCodeSplitterWebpack (line 23) both have @example JSDoc blocks showing usage. TanStackRouterAutoImportWebpack is missing one.

📝 Suggested JSDoc addition
+/**
+ * `@example`
+ * ```ts
+ * export default {
+ *   // ...
+ *   plugins: [TanStackRouterAutoImportWebpack()],
+ * }
+ * ```
+ */
 const TanStackRouterAutoImportWebpack = /* `#__PURE__` */ createWebpackPlugin(
   unpluginRouteAutoImportFactory,
 )
packages/start-client-core/src/client-rpc/serverFnFetcher.ts (1)

71-85: redirect(payload as any) bypasses type checking on the redirect payload.

If the shape of payload doesn't match what redirect() expects beyond isSerializedRedirect, this will silently produce a malformed redirect. Consider narrowing to at least validate that payload has href or the required redirect fields before calling redirect().

packages/start-plugin-core/src/rsbuild/start-compiler-loader.ts (4)

120-161: Promise may hang if the resolver callback is never invoked.

If resolver(...) neither throws nor invokes its callback (e.g., a buggy or custom resolver in the rspack configuration), this promise will never settle, silently stalling the build. Consider wrapping it with a timeout or an AbortSignal-based guard.

🛡️ Suggested defensive timeout
   return new Promise((resolve) => {
+    const timeout = setTimeout(() => {
+      resolve(null)
+    }, 10_000)
     const resolver =
       loaderContext.getResolve?.({
         conditionNames: ['import', 'module', 'default'],
       }) ?? loaderContext.resolve
 
     try {
       resolver(resolveContext, source, (err: Error | null, result?: string) => {
+        clearTimeout(timeout)
         if (!err && result) {
           resolve(cleanId(result))
           return
         }
         try {
           const resolved = require.resolve(source, {
             paths: [
               baseContext,
               loaderContext.rootContext || loaderContext.context,
             ].filter(Boolean),
           })
           resolve(cleanId(resolved))
         } catch {
           resolve(null)
         }
       })
     } catch {
+      clearTimeout(timeout)
       resolve(null)
     }
   })

48-60: getTransformCodeFilterForEnv allocates a new array on every loader call.

shouldTransformCodegetTransformCodeFilterForEnv is called for every file processed by the loader. Since LookupKindsPerEnv and KindDetectionPatterns are module-level constants, the result per env is always identical. Memoizing by env avoids repeated iteration and allocation.

♻️ Optional: memoize by env
+const transformCodeFilterCache = new Map<string, Array<RegExp>>()
+
 function getTransformCodeFilterForEnv(env: 'client' | 'server'): Array<RegExp> {
+  const cached = transformCodeFilterCache.get(env)
+  if (cached) return cached
   const validKinds = LookupKindsPerEnv[env]
   const patterns: Array<RegExp> = []
   for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array<
     [keyof typeof KindDetectionPatterns, RegExp]
   >) {
     if (validKinds.has(kind)) {
       patterns.push(pattern)
     }
   }
+  transformCodeFilterCache.set(env, patterns)
   return patterns
 }

Also applies to: 115-118


201-233: Compiler instance is cached by envName with first-call options — subsequent calls with different options are silently ignored.

If a loader call for the same envName arrives with a different env, root, or framework (e.g., due to misconfiguration), the cached compiler from the first invocation is silently reused. This is likely fine in practice, but there's no validation or assertion guarding this invariant.

Consider adding a debug-mode assertion (e.g., checking compiler.options.env === env) after the cache hit to catch misconfiguration early.


184-186: Apply proper types from @rsbuild/core to improve type safety for the loader context.

The this: any typing at line 184 and loaderContext: any parameters (lines 121, 165) bypass TypeScript checks entirely. Since @rsbuild/core is a peer dependency and re-exports the Rspack namespace, use Rspack.LoaderContext<LoaderOptions> for proper typing:

Example fix
import type { Rspack } from '@rsbuild/core'

async function resolveId(
  loaderContext: Rspack.LoaderContext<LoaderOptions>,
  source: string,
  importer?: string,
): Promise<string | null> {
  // ...
}

export default function startCompilerLoader(
  this: Rspack.LoaderContext<LoaderOptions>,
  code: string,
  map: any,
) {
  // ...
}

This aligns with the coding guideline requirement for "TypeScript strict mode with extensive type safety for all code."

packages/start-plugin-core/src/rsbuild/start-compiler-plugin.ts (2)

328-365: Consider caching asset content to avoid repeated I/O in the manifest resolution loop.

getAssetContent is called per-server-function per-asset in the fallback paths of findAssetMatch, findExportName, and findModuleIdByFunctionId. For a project with N server functions and M assets, this can result in O(N×M) redundant reads of the same assets. Since this runs inside a single afterEmit callback, asset content is immutable at that point.

♻️ Proposed fix — add a simple content cache
+         const assetContentCache = new Map<string, string | undefined>()
          const getAssetContent = async (assetName: string) => {
+           if (assetContentCache.has(assetName)) {
+             return assetContentCache.get(assetName)
+           }
            const assetFromCompilation =
              ...
            try {
              const assetPath = path.join(opts.serverOutputDir, assetName)
-             return await fsp.readFile(assetPath, 'utf-8')
+             const content = await fsp.readFile(assetPath, 'utf-8')
+             assetContentCache.set(assetName, content)
+             return content
            } catch {
+             assetContentCache.set(assetName, undefined)
              return undefined
            }
          }

Apply the same caching pattern at the function level, setting assetContentCache for all code paths before returning.


42-45: Loose CompilationModule type with index signature.

The [key: string]: any index signature means module.id returns any instead of string | number | undefined, defeating type safety for property access. Consider using a more specific intersection or a mapped type if feasible, though this is understandable for rspack interop.

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

406-418: Hardcoded monorepo-relative path for start-client-core.

path.resolve(root, 'packages/start-client-core/dist/esm') on line 406 only resolves in the TanStack Router monorepo. For consumer projects, this path will never exist. The existsSync guard (line 415) prevents a runtime error, and the regex on line 410–411 covers the installed-package case, so this isn't a bug — but the hardcoded path is dead code outside the monorepo and could confuse future maintainers.

Consider adding a brief comment explaining this is a monorepo development convenience, or deriving the path from require.resolve / import.meta.resolve.


100-119: mergeRspackConfig silently drops nested properties from base when next defines the same top-level key.

The shallow spread ...next at line 103 replaces all base keys not explicitly deep-merged. Currently only plugins, module.rules, and resolve.alias are deep-merged. If both base and next supply output or experiments, the base values are completely lost.

This appears intentional for the current usage (the plugin sets complete configs), but it's a latent footgun for future callers. A brief JSDoc noting the shallow-merge behavior would help.


30-33: Local RsbuildPlugin type uses any for the api parameter.

This loses all type safety on the rsbuild API calls throughout the file. @rsbuild/core is available as a dependency (>=1.0.0) and should provide proper types. Consider importing the real RsbuildPlugin type. If not suitable, at minimum type the most-used API methods (modifyRsbuildConfig, onAfterBuild, onAfterStartProdServer, context) as a lightweight interface.

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

119-125: Duplicate page validation — pages are validated twice.

routerBasePath, routerBaseUrl, and validateAndNormalizePrerenderPages are computed and applied identically on lines 42–48 (in prerender()) and again on lines 119–125 (in prerenderPages()). The second pass is redundant since startConfig.pages was already normalized before prerenderPages is called.

♻️ Remove the redundant validation
   async function prerenderPages({ outputDir }: { outputDir: string }) {
     const seen = new Set<string>()
     const prerendered = new Set<string>()
     const retriesByPath = new Map<string, number>()
     const concurrency = startConfig.prerender?.concurrency ?? os.cpus().length
     logger.info(`Concurrency: ${concurrency}`)
     const queue = new Queue({ concurrency })
-    const routerBasePath = joinURL('/', startConfig.router.basepath ?? '')
-
-    const routerBaseUrl = new URL(routerBasePath, 'http://localhost')
-    startConfig.pages = validateAndNormalizePrerenderPages(
-      startConfig.pages,
-      routerBaseUrl,
-    )

Then reference the outer routerBasePath variable inside prerenderPages. Currently it's re-declared (line 119 shadows line 42).


127-127: Biome: forEach callback implicitly returns a value.

addCrawlPageTask returns void, so this is functionally harmless, but using a block body satisfies the linter.

♻️ Proposed fix
-    startConfig.pages.forEach((page) => addCrawlPageTask(page))
+    startConfig.pages.forEach((page) => {
+      addCrawlPageTask(page)
+    })

97-110: Link extraction regex doesn't handle self-closing anchors or single-quoted/unquoted attributes correctly beyond the basic case.

The regex /<a[^>]+href=["']([^"']+)["'][^>]*>/g conflates single and double quotes in its character class — [^"'] matches up to whichever quote type appears first rather than the one that opened the attribute. For example, <a href="it's"> would incorrectly truncate at the apostrophe.

This is a minor edge case for prerender crawling, and HTML from SSR is likely well-formed, but worth noting.

Comment on lines +386 to +394
if (fs.existsSync(generatedRouteTreePath)) {
const existingTree = fs.readFileSync(
generatedRouteTreePath,
'utf-8',
)
if (!existingTree.includes(registerDeclaration)) {
fs.rmSync(generatedRouteTreePath)
}
}
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

Deleting the generated route tree file could race with other processes.

fs.rmSync(generatedRouteTreePath) on line 392 deletes the route tree file synchronously if it lacks the register declaration. If the router plugin or a watcher is concurrently regenerating this file, the delete could cause a race condition or a confusing error.

🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/plugin.ts` around lines 386 - 394, The
synchronous deletion using fs.rmSync(generatedRouteTreePath) can race with
concurrent generators; instead, perform a safe, non-racing removal: attempt an
atomic rename of generatedRouteTreePath to a temp/backup name (e.g.,
generatedRouteTreePath + '.stale') inside a try/catch and only unlink the backup
afterwards, or at minimum replace fs.rmSync with an unlink/rename wrapped in
try/catch that ignores ENOENT/EBUSY errors; update the code around
generatedRouteTreePath / existingTree / registerDeclaration to use this safe
rename-then-remove or try/catch-unlink pattern rather than calling fs.rmSync
directly.

Comment on lines +558 to +590
const setupMiddlewares = (
middlewares: Array<any>,
context: { environments?: Record<string, any> },
) => {
if (startConfig.vite?.installDevServerMiddleware === false) {
return
}
const serverEnv =
context.environments?.[VITE_ENVIRONMENT_NAMES.server]
middlewares.push(async (req: any, res: any, next: any) => {
if (res.headersSent || res.writableEnded) {
return next()
}
if (!serverEnv?.loadBundle) {
return next()
}
try {
const serverBundle = await serverEnv.loadBundle()
const serverBuild = serverBundle?.default ?? serverBundle
if (!serverBuild?.fetch) {
return next()
}
req.url = joinURL(
resolvedStartConfig.viteAppBase,
req.url ?? '/',
)
const webReq = new NodeRequest({ req, res })
const webRes = await serverBuild.fetch(webReq)
return sendNodeResponse(res, webRes)
} catch (error) {
return next(error)
}
})
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

Dev middleware mutates req.url — may break downstream middleware.

Line 580–583 modifies req.url in place by prepending viteAppBase. If this middleware is followed by other middleware that also reads req.url, the prepended base path could cause unexpected routing. The production middleware on line 736 has the same pattern.

Consider creating a new request with the modified URL instead of mutating, or documenting that this middleware must be last in the chain.

🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/plugin.ts` around lines 558 - 590, The
dev middleware inside setupMiddlewares mutates req.url by assigning
joinURL(resolvedStartConfig.viteAppBase, req.url ?? '/'), which can break
downstream middleware; instead create a new request object with the modified URL
(e.g., construct a new Request/NodeRequest using the composed URL) and pass that
to serverBuild.fetch while leaving the original req.url untouched, updating the
NodeRequest construction site (where NodeRequest({ req, res }) is created) to
use the new URL and returning sendNodeResponse(res, webRes) as before; apply the
same non-mutating change to the analogous production middleware to keep behavior
consistent.

routerBaseUrl,
)

process.env.TSS_PRERENDERING = 'true'
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

process.env.TSS_PRERENDERING is set but never cleaned up.

If the Node process continues after prerendering (e.g., in a long-lived build orchestrator), this env var persists and could affect subsequent operations.

🛡️ Proposed fix — clean up after prerendering
+  try {
     const pages = await prerenderPages({ outputDir: clientOutputDir })

     logger.info(`Prerendered ${pages.length} pages:`)
     pages.forEach((page) => {
       logger.info(`- ${page}`)
     })
-  } catch (error) {
-    logger.error(error)
+  } finally {
+    delete process.env.TSS_PRERENDERING
   }

Note: this also relates to the error-swallowing issue — the catch should rethrow.

🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/prerender.ts` at line 50, The code
sets process.env.TSS_PRERENDERING = 'true' but never resets it and also swallows
errors; modify the prerender flow (the function that sets
process.env.TSS_PRERENDERING in
packages/start-plugin-core/src/rsbuild/prerender.ts) to ensure the env var is
restored (delete or reset to previous value) in a finally block after
prerendering completes, and change the catch block to rethrow the caught error
so failures are propagated.

Comment on lines +86 to +95
try {
const pages = await prerenderPages({ outputDir: clientOutputDir })

logger.info(`Prerendered ${pages.length} pages:`)
pages.forEach((page) => {
logger.info(`- ${page}`)
})
} catch (error) {
logger.error(error)
}
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

Prerender errors are silently swallowed — build will appear to succeed even when prerendering fails.

The outer catch on lines 93–95 logs the error but does not rethrow. Even when failOnError is true inside addCrawlPageTask (line 244), the thrown error propagates up through the queue to prerenderPages, then to queue.start() (line 129), and is caught here — silently swallowed. The caller (postServerBuildRsbuild) will never know prerendering failed.

🐛 Proposed fix — rethrow after logging
   } catch (error) {
     logger.error(error)
+    throw error
   }
📝 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
try {
const pages = await prerenderPages({ outputDir: clientOutputDir })
logger.info(`Prerendered ${pages.length} pages:`)
pages.forEach((page) => {
logger.info(`- ${page}`)
})
} catch (error) {
logger.error(error)
}
try {
const pages = await prerenderPages({ outputDir: clientOutputDir })
logger.info(`Prerendered ${pages.length} pages:`)
pages.forEach((page) => {
logger.info(`- ${page}`)
})
} catch (error) {
logger.error(error)
throw error
}
🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/prerender.ts` around lines 86 - 95,
The catch in the prerendering block swallows errors from prerenderPages (and
upstream addCrawlPageTask/queue.start), so update the catch in the async block
around prerenderPages to log the error via logger.error(error) and then rethrow
the error so callers (e.g., postServerBuildRsbuild) observe the failure; keep
the existing logging but add a `throw error` after it to propagate the failure.

Comment on lines +186 to +189

const isSpaShell =
startConfig.spa?.prerender.outputPath === cleanPagePath

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

Potential TypeError on startConfig.spa?.prerender.outputPath if prerender is undefined.

Line 188 accesses .outputPath on startConfig.spa.prerender using optional chaining on spa but not on prerender. If spa exists but spa.prerender is undefined, this throws a TypeError. Same pattern as the issue in post-server-build.ts.

🛡️ Proposed fix
-          const isSpaShell =
-            startConfig.spa?.prerender.outputPath === cleanPagePath
+          const isSpaShell =
+            startConfig.spa?.prerender?.outputPath === cleanPagePath
📝 Committable suggestion

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

Suggested change
const isSpaShell =
startConfig.spa?.prerender.outputPath === cleanPagePath
const isSpaShell =
startConfig.spa?.prerender?.outputPath === cleanPagePath
🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/prerender.ts` around lines 186 - 189,
The code computing isSpaShell risks a TypeError because it uses optional
chaining on startConfig.spa but then accesses .prerender.outputPath without
guarding against prerender being undefined; update the check in the isSpaShell
assignment to safely access prerender (e.g., use
startConfig.spa?.prerender?.outputPath or check startConfig.spa?.prerender
before reading outputPath) so comparison with cleanPagePath is only performed
when outputPath is defined, preserving the existing isSpaShell logic and
variable names.

Comment on lines +163 to +182
async function loadModule(
compiler: StartCompiler,
loaderContext: any,
id: string,
) {
const cleaned = cleanId(id)
const resolvedPath =
cleaned.startsWith('.') || cleaned.startsWith('/')
? cleaned
: ((await resolveId(loaderContext, cleaned)) ?? cleaned)

if (resolvedPath.includes('\0')) return

try {
const code = await fsp.readFile(resolvedPath, 'utf-8')
compiler.ingestModule({ code, id: resolvedPath })
} catch {
// ignore missing files
}
}
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

Relative-path short-circuit in loadModule may read from the wrong directory.

Lines 170–171: if cleaned starts with ./, it bypasses resolveId and is passed directly to fsp.readFile. This would resolve relative to process.cwd(), not relative to the importing module. If the StartCompiler ever calls loadModule with a relative (non-resolved) id, the read will fail or load the wrong file.

If loadModule is only ever called with already-resolved absolute paths, this is safe — but the startsWith('.') branch creates a silent trap for future callers.

🛡️ Suggested fix: always resolve relative paths
 async function loadModule(
   compiler: StartCompiler,
   loaderContext: any,
   id: string,
 ) {
   const cleaned = cleanId(id)
-  const resolvedPath =
-    cleaned.startsWith('.') || cleaned.startsWith('/')
-      ? cleaned
-      : ((await resolveId(loaderContext, cleaned)) ?? cleaned)
+  const resolvedPath = cleaned.startsWith('/')
+    ? cleaned
+    : ((await resolveId(loaderContext, cleaned)) ?? cleaned)
 
   if (resolvedPath.includes('\0')) return
📝 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
async function loadModule(
compiler: StartCompiler,
loaderContext: any,
id: string,
) {
const cleaned = cleanId(id)
const resolvedPath =
cleaned.startsWith('.') || cleaned.startsWith('/')
? cleaned
: ((await resolveId(loaderContext, cleaned)) ?? cleaned)
if (resolvedPath.includes('\0')) return
try {
const code = await fsp.readFile(resolvedPath, 'utf-8')
compiler.ingestModule({ code, id: resolvedPath })
} catch {
// ignore missing files
}
}
async function loadModule(
compiler: StartCompiler,
loaderContext: any,
id: string,
) {
const cleaned = cleanId(id)
const resolvedPath = cleaned.startsWith('/')
? cleaned
: ((await resolveId(loaderContext, cleaned)) ?? cleaned)
if (resolvedPath.includes('\0')) return
try {
const code = await fsp.readFile(resolvedPath, 'utf-8')
compiler.ingestModule({ code, id: resolvedPath })
} catch {
// ignore missing files
}
}
🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/start-compiler-loader.ts` around lines
163 - 182, The loadModule function currently short-circuits relative ids
(cleaned.startsWith('.') or '/') and calls fsp.readFile with cleaned, which
reads from process.cwd(); instead, always call resolveId(loaderContext, cleaned)
to get a proper module path (use ((await resolveId(loaderContext, cleaned)) ??
cleaned) only as a fallback for already-absolute paths), then skip if the
resolved result is falsy or contains '\0', and finally use the resolvedPath for
fsp.readFile and compiler.ingestModule; update the logic around cleanId,
resolveId, resolvedPath, fsp.readFile, and compiler.ingestModule accordingly so
relative imports are resolved relative to the importer rather than the CWD.

Comment on lines +254 to +270
export function createServerFnManifestRspackPlugin(opts: {
serverOutputDir: string
}) {
const tempManifestPath = path.join(
opts.serverOutputDir,
SERVER_FN_MANIFEST_TEMP_FILE,
)

return {
apply(compiler: any) {
compiler.hooks.beforeRun.tapPromise(
'tanstack-start:server-fn-manifest',
async () => {
resetServerFnCompilerState()
await fsp.rm(tempManifestPath, { force: true })
},
)
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

beforeRun hook won't fire for watch mode — state may leak across rebuilds.

compiler.hooks.beforeRun only fires for non-watch builds. In watch mode (e.g., rspack --watch or rsbuild dev), the watchRun hook fires instead. If this plugin is ever used in dev/watch mode, resetServerFnCompilerState() and the temp manifest cleanup won't run, potentially causing stale server function data to persist across rebuilds.

🛡️ Proposed fix — also tap into watchRun
      compiler.hooks.beforeRun.tapPromise(
        'tanstack-start:server-fn-manifest',
        async () => {
          resetServerFnCompilerState()
          await fsp.rm(tempManifestPath, { force: true })
        },
      )
+     compiler.hooks.watchRun.tapPromise(
+       'tanstack-start:server-fn-manifest',
+       async () => {
+         resetServerFnCompilerState()
+         await fsp.rm(tempManifestPath, { force: true })
+       },
+     )
🤖 Prompt for AI Agents
In `@packages/start-plugin-core/src/rsbuild/start-compiler-plugin.ts` around lines
254 - 270, The plugin currently only hooks compiler.hooks.beforeRun so
resetServerFnCompilerState() and the tempManifestPath removal (fsp.rm) won’t run
in watch mode; update createServerFnManifestRspackPlugin to also tapPromise into
compiler.hooks.watchRun (using the same callback) so both
resetServerFnCompilerState() and await fsp.rm(tempManifestPath, { force: true })
run for watch builds as well, referencing the existing beforeRun handler logic
to avoid duplicating behavior.

Use a Response type guard-compatible response variable while keeping stricter runtime checks so start-client-core type and eslint checks pass in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
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

🤖 Fix all issues with AI agents
In `@packages/start-client-core/src/client-rpc/serverFnFetcher.ts`:
- Around line 67-81: The parseRedirectFallback function uses an unsafe "as any"
cast before calling redirect; replace that with a proper cast to the redirect
input type (e.g., RedirectOptions) so TypeScript can check fields while still
allowing optional properties to be omitted—specifically change the call in
parseRedirectFallback from redirect(payload as any) to redirect(payload as
RedirectOptions) (or the exact exported type used by redirect), keeping the
existing existence checks and not adding extra validation.

Comment on lines +67 to +81
function parseRedirectFallback(payload: unknown) {
if (!payload || typeof payload !== 'object') {
return undefined
}
if (!('isSerializedRedirect' in payload)) {
return undefined
}
if (
(payload as { isSerializedRedirect?: boolean }).isSerializedRedirect !==
true
) {
return undefined
}
return redirect(payload as any)
}
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

🧩 Analysis chain

🏁 Script executed:

# Search for the redirect function definition in the router-core package
fd -e ts -e tsx 'router' --type f | xargs grep -l "export.*function redirect" | head -20

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Find the redirect function in the codebase
rg "export\s+(function|const)\s+redirect" -t ts -t tsx -A 5

Repository: TanStack/router

Length of output: 86


🏁 Script executed:

# Check the imports in the serverFnFetcher file to understand where redirect comes from
fd -e ts -e tsx 'serverFnFetcher' --type f -exec cat {} \;

Repository: TanStack/router

Length of output: 246


🏁 Script executed:

# First, find the serverFnFetcher.ts file
fd 'serverFnFetcher' -t f

Repository: TanStack/router

Length of output: 119


🏁 Script executed:

# Search for redirect function export using rg with correct flags
rg "export\s+(function|const)\s+redirect" --type ts -A 5

Repository: TanStack/router

Length of output: 518


🏁 Script executed:

# Alternative: look for redirect in package structure
find . -name "*.ts" -type f -exec grep -l "export.*function redirect" {} \; 2>/dev/null | head -10

Repository: TanStack/router

Length of output: 182


🏁 Script executed:

# Read the redirect.ts file to understand the full function signature
cat -n packages/router-core/src/redirect.ts

Repository: TanStack/router

Length of output: 8122


🏁 Script executed:

# Read the serverFnFetcher.ts file to understand context
cat -n packages/start-client-core/src/client-rpc/serverFnFetcher.ts

Repository: TanStack/router

Length of output: 16063


Improve type safety of the as any cast — no field validation needed.

The as any cast does bypass type checking. However, all fields in RedirectOptions are optional, and the redirect() function handles missing fields gracefully (e.g., setting a default statusCode, checking if href exists before using it). The concern about malformed redirects is unfounded.

For better type safety without overvalidating, cast to the proper type instead:

Better approach
 function parseRedirectFallback(payload: unknown) {
   if (!payload || typeof payload !== 'object') {
     return undefined
   }
   if (!('isSerializedRedirect' in payload)) {
     return undefined
   }
   if (
     (payload as { isSerializedRedirect?: boolean }).isSerializedRedirect !==
     true
   ) {
     return undefined
   }
-  return redirect(payload as any)
+  return redirect(payload as Parameters<typeof redirect>[0])
 }
🤖 Prompt for AI Agents
In `@packages/start-client-core/src/client-rpc/serverFnFetcher.ts` around lines 67
- 81, The parseRedirectFallback function uses an unsafe "as any" cast before
calling redirect; replace that with a proper cast to the redirect input type
(e.g., RedirectOptions) so TypeScript can check fields while still allowing
optional properties to be omitted—specifically change the call in
parseRedirectFallback from redirect(payload as any) to redirect(payload as
RedirectOptions) (or the exact exported type used by redirect), keeping the
existing existence checks and not adding extra validation.

Remove unnecessary optional chaining in the response-like header guard so start-server-core eslint passes in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
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