Fix/server fn cross file manifest#7752
Conversation
Updates the @playwright/test version in the pnpm workspace catalog and in every e2e package.json to ^1.61.0. The previously pinned version intermittently stalled/hung during e2e runs; upgrading resolves the stalling issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s in the production manifest When SSR is the server-function provider (the Vite default), a server function reachable only from server-only code — a middleware `.server()` body or another server function's handler — could be omitted from the generated server-function manifest, causing "Server function info not found" at runtime in production builds while dev worked. The server-function resolver now completes discovery of the reachable server module graph before snapshotting the manifest, including provider modules so handler-nested and external-package server functions are covered. Adds e2e coverage in the server-functions app: cross-file middleware, nested handler, a multi-level cross-file chain, and a separate-package fixture. Closes TanStack#7213. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR fixes server-function manifest generation for server-only reachability, adds regression fixtures/routes/tests for cross-file discovery cases, and updates ChangesServer function manifest discovery fix
Playwright Dependency Version Bump
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Vite
participant ResolverModule
participant ensureServerModuleGraphTransformed
participant ctx.load
Vite->>ResolverModule: load(id)
ResolverModule->>ensureServerModuleGraphTransformed: crawl(id, root, serverFnsById)
ensureServerModuleGraphTransformed->>ctx.load: load app graph with resolveDependencies
ctx.load-->>ensureServerModuleGraphTransformed: imported module ids
ensureServerModuleGraphTransformed->>ctx.load: load provider modules for discovered server fns
ctx.load-->>ensureServerModuleGraphTransformed: additional server fns
ensureServerModuleGraphTransformed-->>ResolverModule: populated serverFnsById
ResolverModule-->>Vite: generateServerFnResolverModule
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
e2e/react-start/server-functions/tests/server-functions.spec.ts (1)
732-735: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAvoid
networkidlein new regression tests.All four new tests wait via
page.waitForLoadState('networkidle')before interacting. Playwright's own docs mark this state as'networkidle' - DISCOURAGED ... Don't use this method for testing, rely on web assertions to assess readiness instead. Given this PR's sibling commit bumps Playwright specifically to fix "intermittent e2e test stalling," introducing morenetworkidlewaits works against that goal.♻️ Suggested approach
- await page.goto('/middleware/serverfn-in-middleware') - - await page.waitForLoadState('networkidle') - - await page.getByTestId('test-serverfn-in-middleware-btn').click() + await page.goto('/middleware/serverfn-in-middleware') + await page.getByTestId('test-serverfn-in-middleware-btn').click()Playwright's locator-based assertions (
toContainText,toHaveText, etc.) already used later in each test auto-wait/retry, so the explicitnetworkidlewait is likely unnecessary; the click itself auto-waits for the button to be actionable.Please confirm whether this repo's Playwright test suite has an established convention of using
networkidleelsewhere (if so, this is consistent with existing style and lower priority to change).Also applies to: 761-762, 787-788, 807-808
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@e2e/react-start/server-functions/tests/server-functions.spec.ts` around lines 732 - 735, The new regression tests are using an explicit page.waitForLoadState('networkidle') before interactions, which should be removed or replaced. Update the affected server-functions Playwright specs to rely on the existing locator assertions and Playwright’s built-in auto-waiting around page.goto, click, and toHaveText/toContainText instead of waiting for network idle. Apply the same change in the related tests using this pattern so the suite follows the recommended readiness convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts`:
- Around line 309-314: The loop in plugin.ts uses one-line `if` bodies that
violate the brace requirement. Update the `for...of` blocks that iterate over
`info.importedIds` and `info.dynamicallyImportedIds` in `plugin.ts` so each `if
(!visited.has(dep))` uses curly braces before calling `queue.push(dep)`. Keep
the existing `info`/`visited`/`queue` logic unchanged, just add braces to both
conditional statements.
- Around line 239-256: The root check in shouldWalkForServerFns is too loose and
can match sibling directories outside the app. Update the
cleaned.startsWith(root) guard to enforce a path boundary, using the root path
symbol in shouldWalkForServerFns so only files under the exact root are walked.
Prefer comparing against root with a trailing separator or using a relative-path
check, and keep the existing node_modules, virtual-module, and
TRANSFORM_ID_REGEX filtering unchanged.
---
Nitpick comments:
In `@e2e/react-start/server-functions/tests/server-functions.spec.ts`:
- Around line 732-735: The new regression tests are using an explicit
page.waitForLoadState('networkidle') before interactions, which should be
removed or replaced. Update the affected server-functions Playwright specs to
rely on the existing locator assertions and Playwright’s built-in auto-waiting
around page.goto, click, and toHaveText/toContainText instead of waiting for
network idle. Apply the same change in the related tests using this pattern so
the suite follows the recommended readiness convention.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 99cd7fc7-fa21-4bee-b177-215dbde3d1c4
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (131)
.changeset/fix-server-fn-manifest-cross-file.mde2e/react-router/basepath-file-based/package.jsone2e/react-router/basic-esbuild-file-based/package.jsone2e/react-router/basic-file-based-code-splitting/package.jsone2e/react-router/basic-file-based/package.jsone2e/react-router/basic-react-query-file-based/package.jsone2e/react-router/basic-react-query/package.jsone2e/react-router/basic-scroll-restoration/package.jsone2e/react-router/basic-virtual-file-based/package.jsone2e/react-router/basic-virtual-named-export-config-file-based/package.jsone2e/react-router/basic/package.jsone2e/react-router/escaped-special-strings/package.jsone2e/react-router/generator-cli-only/package.jsone2e/react-router/i18n-paraglide/package.jsone2e/react-router/js-only-file-based/package.jsone2e/react-router/match-params/package.jsone2e/react-router/rspack-basic-file-based/package.jsone2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.jsone2e/react-router/scroll-restoration-sandbox-vite/package.jsone2e/react-router/sentry-integration/package.jsone2e/react-router/view-transitions/package.jsone2e/react-start/basic-auth/package.jsone2e/react-start/basic-cloudflare/package.jsone2e/react-start/basic-react-query/package.jsone2e/react-start/basic/package.jsone2e/react-start/clerk-basic/package.jsone2e/react-start/csp/package.jsone2e/react-start/css-inline/package.jsone2e/react-start/css-modules/package.jsone2e/react-start/custom-basepath/package.jsone2e/react-start/custom-server-rsbuild/package.jsone2e/react-start/dev-ssr-styles/package.jsone2e/react-start/early-hints/package.jsone2e/react-start/hmr/package.jsone2e/react-start/i18n-paraglide/package.jsone2e/react-start/import-protection-custom-config/package.jsone2e/react-start/import-protection/package.jsone2e/react-start/query-integration/package.jsone2e/react-start/rsc-deferred-hydration/package.jsone2e/react-start/rsc-query/package.jsone2e/react-start/rsc-rsbuild/package.jsone2e/react-start/rsc/package.jsone2e/react-start/scroll-restoration/package.jsone2e/react-start/server-functions-global-middleware/package.jsone2e/react-start/server-functions-shared-lib/package.jsone2e/react-start/server-functions-shared-lib/src/index.tse2e/react-start/server-functions-shared-lib/src/internal.tse2e/react-start/server-functions/package.jsone2e/react-start/server-functions/src/functions/chainLevelB.tse2e/react-start/server-functions/src/functions/chainLevelC.tse2e/react-start/server-functions/src/functions/serverFnCalledByMiddleware.tse2e/react-start/server-functions/src/middleware/serverFnCallingMiddleware.tse2e/react-start/server-functions/src/routeTree.gen.tse2e/react-start/server-functions/src/routes/middleware/index.tsxe2e/react-start/server-functions/src/routes/middleware/serverfn-in-middleware.tsxe2e/react-start/server-functions/src/routes/nested-server-fns.tsxe2e/react-start/server-functions/src/routes/shared-package-server-fn.tsxe2e/react-start/server-functions/tests/server-functions.spec.tse2e/react-start/server-routes-global-middleware/package.jsone2e/react-start/server-routes/package.jsone2e/react-start/split-base-and-basepath/package.jsone2e/react-start/start-manifest/package.jsone2e/react-start/static-server-functions/package.jsone2e/react-start/streaming-ssr/package.jsone2e/react-start/transform-asset-urls/package.jsone2e/react-start/virtual-routes/package.jsone2e/react-start/website/package.jsone2e/solid-router/basepath-file-based/package.jsone2e/solid-router/basic-esbuild-file-based/package.jsone2e/solid-router/basic-file-based-code-splitting/package.jsone2e/solid-router/basic-file-based/package.jsone2e/solid-router/basic-scroll-restoration/package.jsone2e/solid-router/basic-solid-query-file-based/package.jsone2e/solid-router/basic-solid-query/package.jsone2e/solid-router/basic-virtual-file-based/package.jsone2e/solid-router/basic-virtual-named-export-config-file-based/package.jsone2e/solid-router/basic/package.jsone2e/solid-router/generator-cli-only/package.jsone2e/solid-router/js-only-file-based/package.jsone2e/solid-router/rspack-basic-file-based/package.jsone2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.jsone2e/solid-router/scroll-restoration-sandbox-vite/package.jsone2e/solid-router/sentry-integration/package.jsone2e/solid-router/view-transitions/package.jsone2e/solid-start/basic-auth/package.jsone2e/solid-start/basic-cloudflare/package.jsone2e/solid-start/basic-solid-query/package.jsone2e/solid-start/basic/package.jsone2e/solid-start/csp/package.jsone2e/solid-start/css-modules/package.jsone2e/solid-start/custom-basepath/package.jsone2e/solid-start/query-integration/package.jsone2e/solid-start/scroll-restoration/package.jsone2e/solid-start/server-functions/package.jsone2e/solid-start/server-routes/package.jsone2e/solid-start/solid-query-layout-suspense/package.jsone2e/solid-start/start-manifest/package.jsone2e/solid-start/virtual-routes/package.jsone2e/solid-start/website/package.jsone2e/vue-router/basepath-file-based/package.jsone2e/vue-router/basic-esbuild-file-based/package.jsone2e/vue-router/basic-file-based-jsx/package.jsone2e/vue-router/basic-file-based-sfc/package.jsone2e/vue-router/basic-scroll-restoration/package.jsone2e/vue-router/basic-virtual-file-based/package.jsone2e/vue-router/basic-virtual-named-export-config-file-based/package.jsone2e/vue-router/basic-vue-query-file-based/package.jsone2e/vue-router/basic-vue-query/package.jsone2e/vue-router/basic/package.jsone2e/vue-router/generator-cli-only/package.jsone2e/vue-router/js-only-file-based/package.jsone2e/vue-router/rspack-basic-file-based/package.jsone2e/vue-router/rspack-basic-virtual-named-export-config-file-based/package.jsone2e/vue-router/scroll-restoration-sandbox-vite/package.jsone2e/vue-router/sentry-integration/package.jsone2e/vue-router/view-transitions/package.jsone2e/vue-start/basic-auth/package.jsone2e/vue-start/basic-cloudflare/package.jsone2e/vue-start/basic-vue-query/package.jsone2e/vue-start/basic/package.jsone2e/vue-start/css-modules/package.jsone2e/vue-start/custom-basepath/package.jsone2e/vue-start/query-integration/package.jsone2e/vue-start/scroll-restoration/package.jsone2e/vue-start/server-functions/package.jsone2e/vue-start/server-routes/package.jsone2e/vue-start/start-manifest/package.jsone2e/vue-start/virtual-routes/package.jsone2e/vue-start/website/package.jsonpackages/start-plugin-core/src/vite/start-compiler-plugin/plugin.tspnpm-workspace.yaml
| for (const dep of info.importedIds) { | ||
| if (!visited.has(dep)) queue.push(dep) | ||
| } | ||
| for (const dep of info.dynamicallyImportedIds) { | ||
| if (!visited.has(dep)) queue.push(dep) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Missing curly braces on one-line if bodies.
Lines 310 and 313 use one-line if bodies without braces, which the coding guidelines explicitly disallow.
As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Always use curly braces for if, else, loops, and similar control statements. Never write one-line bodies like if (foo) x = 1."
🔧 Proposed fix
for (const dep of info.importedIds) {
- if (!visited.has(dep)) queue.push(dep)
+ if (!visited.has(dep)) {
+ queue.push(dep)
+ }
}
for (const dep of info.dynamicallyImportedIds) {
- if (!visited.has(dep)) queue.push(dep)
+ if (!visited.has(dep)) {
+ queue.push(dep)
+ }
}📝 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.
| for (const dep of info.importedIds) { | |
| if (!visited.has(dep)) queue.push(dep) | |
| } | |
| for (const dep of info.dynamicallyImportedIds) { | |
| if (!visited.has(dep)) queue.push(dep) | |
| } | |
| for (const dep of info.importedIds) { | |
| if (!visited.has(dep)) { | |
| queue.push(dep) | |
| } | |
| } | |
| for (const dep of info.dynamicallyImportedIds) { | |
| if (!visited.has(dep)) { | |
| queue.push(dep) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts` around
lines 309 - 314, The loop in plugin.ts uses one-line `if` bodies that violate
the brace requirement. Update the `for...of` blocks that iterate over
`info.importedIds` and `info.dynamicallyImportedIds` in `plugin.ts` so each `if
(!visited.has(dep))` uses curly braces before calling `queue.push(dep)`. Keep
the existing `info`/`visited`/`queue` logic unchanged, just add braces to both
conditional statements.
Source: Coding guidelines
- Enforce a path boundary in the crawl's root check so a sibling directory
sharing a name prefix (e.g. `<root>-other`) is not treated as under the root.
This also makes the separate-package e2e fixture exercise the provider-module
phase as intended rather than matching via the loose prefix.
- Guard virtual modules on the raw id (`id.startsWith('\0')`) before `cleanId`
strips the NUL prefix, matching the convention used elsewhere; the previous
post-`cleanId` `includes('\0')` check never matched.
- Use curly braces on the two one-line `if` bodies, per AGENTS.md.
- Extract the crawl (`shouldWalkForServerFns` / `ensureServerModuleGraphTransformed`)
into `server-fn-graph-crawl.ts` so the pure boundary logic can be unit-tested
without importing the full plugin (which pulls in server-only entry imports).
- Add unit tests for `shouldWalkForServerFns` covering the sibling-prefix
boundary, NUL-prefixed virtual ids, node_modules / non-transformable
exclusions, and query-string handling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix: server functions missing from the production manifest (TanStack Start, issue #7213)
Repository: fork of
TanStack/routerBranch:
fix/server-fn-cross-file-manifestCommits:
8b53769ee4(Playwright bump) ·d33b368de7(the fix)Upstream issue: #7213
1. Executive summary
TanStack Start lets you write server functions — functions that run on the server and are callable from the browser over HTTP (an RPC). In production builds, a whole category of server functions was silently left out of the server's lookup table ("the manifest"), so calling them crashed at runtime with:
Crucially, this only happened in production builds, not in development — so it was the kind of bug that passes local testing and breaks in production.
The affected category: any server function that is only ever reached from server-side code — for example, one called inside a middleware, or called by another server function. Organising code into separate files (a completely normal practice) reliably triggered it.
The fix makes the build finish discovering these server functions before it writes out the manifest. It is a one-file change to the build tooling (
@tanstack/start-plugin-core), shipped as a patch. We validated it with new automated tests and the full existing test suite across all three supported frameworks (React, Solid, Vue). Build-time impact is negligible (single-digit milliseconds, within measurement noise). There is no security or API change.2. Background
A server function is declared with
createServerFn().handler(...). At build time the tooling splits each one into two pieces: a small client stub (shipped to the browser, makes an HTTP call) and the real implementation (kept on the server). The server keeps a manifest — a map from a function's ID to its implementation — so that when a request arrives (or when server code calls the function in-process), it can find and run the right implementation. If a function is missing from that manifest, any attempt to resolve it throwsServer function info not found.3. The bug
Symptom. Production builds throw
Server function info not found for <id>at runtime. Development works fine. Single-file setups work fine. The error appears once server functions and the code that uses them are split across files.What triggers it. A server function that is reached only through server-only code. Two common shapes:
.server()handler calls a server function defined in another file.Why it matters. Both shapes are ordinary, encouraged patterns (shared middleware, shared server-side helpers, code organised into modules). The failure is production-only, so it evades local development and typically surfaces only after deploy. The error message is opaque (an internal ID, no source location).
Where the error originates.
packages/start-plugin-core/src/start-compiler/server-fn-resolver-module.ts:53— the generated resolver throws when an ID is absent from the manifest.4. Root cause
Three facts combine into a timing race.
(a) A server function is registered only when its file is compiled.
The build registers a server function into the shared registry (
serverFnsById) as a side effect of compiling the file that defines it — seepackages/start-plugin-core/src/start-compiler/handleCreateServerFn.ts:350and:476. If a definition file never gets compiled before the manifest is written, the function is absent.(b) The client build cannot see server-only functions.
The browser build strips out server-only code: a middleware's
.server()body is removed (packages/start-plugin-core/src/start-compiler/handleCreateMiddleware.ts:68), and a server function's handler body is replaced by a client stub. Dead-code elimination then drops the now-unused imports. So a function reached only from that stripped code never enters the client build's module graph, and is never registered during the client pass.(c) The manifest is generated once, mid-build, possibly too early.
The manifest lives in a "resolver" virtual module that is generated exactly once, the first time the server runtime imports it —
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts:750(theload()hook) callinggenerateServerFnResolverModule(...)at:792. The build order is: client environment fully, then server environment (packages/start-plugin-core/src/vite/planning.ts:204then:208). The resolver is imported early in the server build — before the server graph has transitively compiled every server-only definition file.Net effect: the manifest ends up containing essentially "every server function the client references." Server-only functions are systematically excluded, because the client can't see them (b) and the server writes the manifest before it has finished finding them (c).
Why dev works. In development there is no pre-baked manifest; the dev resolver decodes the target from the request and imports it on demand, so nothing needs to be discovered ahead of time.
Why it's specific to the default setup. The bug is confined to the configuration where the SSR environment is also the server-function "provider" (
ssrIsProvider, the Vite default). In the alternative setup (a separate provider environment, e.g. RSC), that environment builds last (planning.ts:211+), so discovery is already complete by the time it writes the manifest. We confirmed the referencersbuildbundler is unaffected — it regenerates the resolver after the graph is fully built, which Vite did not do.Figure 1 — why the function goes missing:
flowchart TB A["Server function in its own file,<br/>reached only from server-only code"] subgraph CB["Client build — runs first, in full"] C1["Strips server-only code, so this function<br/>is never compiled or registered"] end subgraph SB["Server build — runs second"] S1["Manifest generated once, early —<br/>before this function's file is compiled"] end A --> C1 A --> S1 C1 --> MISS["Function absent from the manifest"] S1 --> MISS MISS --> ERR["Production runtime error:<br/>Server function info not found"] classDef err fill:#fdecea,stroke:#d1352c,color:#611000 class ERR err5. The fix
Idea: before the server build writes the manifest, force the reachable server module graph to finish compiling, so discovery is complete.
All changes are in
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts.Gate (only runs where the bug exists).
plugin.ts:781— the discovery step runs only whenssrIsProvider. For the separate-provider/RSC case it is skipped, because that environment already builds after discovery completes. This also avoids doing redundant work (and avoids running during RSC's throwaway "scan" build pass).Two-phase discovery —
ensureServerModuleGraphTransformed(...)atplugin.ts:270, awaited at:782:Phase 1 — bounded walk of the app's own source (
plugin.ts:278). Starting from the app's already-known modules, it loads each one and follows its imports, which forces them to compile (and thus register their server functions). This is deliberately bounded to the project's own source viashouldWalkForServerFns(plugin.ts:239) — it excludesnode_modules, virtual modules, and anything outside the project root. That bound is essential: it stops the walk from wandering into the framework runtime (which is large and, importantly, is what imports the resolver module we are in the middle of generating — walking into it would deadlock the build).Phase 2 — location-agnostic sweep of provider modules (
plugin.ts:317). A function called from inside another function's handler is dropped from the normal (caller) module, so Phase 1 alone misses it. But its implementation lives in a dedicated "provider" module whose handler body is intact. Phase 2 loads the provider module of every currently-known server function and lets loading pull in its direct dependencies — registering the nested functions those handlers call. It repeats until no new functions appear. Because a provider module is always a user/shared server-function implementation (never the framework runtime), it is safe to load regardless of where it lives — which is what lets this correctly discover server functions defined in separate packages, without reintroducing the framework-walk problem that Phase 1's bound guards against.One comment refresh. The surrounding comment at
plugin.ts:792was updated to reflect that, in this configuration, server functions are intentionally callable both in-process on the server and directly from the client (see §6, Security), matching the referencersbuild/RSC behavior.Figure 2 — the fix: finish discovery before writing the manifest:
flowchart TB L["Resolver load(): before writing the manifest"] L --> G{"Is SSR the provider?<br/>(Vite default)"} G -->|"No — provider env builds last,<br/>already complete"| GEN G -->|Yes| P1 subgraph P1["Phase 1 — bounded walk of app source"] P1a["Compile app modules and follow imports;<br/>skip framework runtime and node_modules"] P1b["Registers functions reached from intact<br/>server-only bodies, e.g. middleware .server()"] P1a --> P1b end subgraph P2["Phase 2 — provider-module fixpoint"] P2a["Load every known function's provider module<br/>(handler body intact; any location)"] P2b["Registers handler-nested and<br/>separate-package functions"] P2c["Repeat until no new functions appear"] P2a --> P2b --> P2c end P1 --> P2 P2 --> GEN["Generate manifest — now complete"] classDef ok fill:#eaf7ea,stroke:#2e7d32,color:#143314 class GEN ok6. Implications
Behavior — previously-broken patterns now work. Server functions reached only via a middleware, only via another server function's handler, arbitrarily nested chains, and functions defined in a separate shared package are all now present in the production manifest and resolve correctly at runtime. Nothing that worked before changes.
Security — no change; matches the reference implementation. Server functions were already, by design, callable from the client (directly by ID, or via serialization — see
createSsrRpc.ts:10for the in-process path and the resolver's client-origin gate atserver-fn-resolver-module.ts:42). In the defaultssrIsProviderconfiguration this gate is intentionally off in both Vite and the referencersbuildbundler, andrsbuildalready includes these functions in its manifest. Our fix brings Vite in line with that existing, intended behavior — it does not newly expose anything thatrsbuilddidn't already expose. (The library's documented intent: server functions are callable anywhere — over HTTP from the client, in-process on the server, skipping the network hop. Our change only fixes discovery; it does not touch dispatch, the client stub, or the origin gate.)Performance — negligible. We measured production SSR build time with the discovery step ON vs OFF, several runs each:
website≈ +3ms,basic≈ +27ms (~8%, within noise),server-functions(largest, 263 SSR modules) ≈ +10ms (~2%). The step largely front-loads work the build already does (module loads are cached), so its apparent internal cost (~400ms) is overwhelmingly overlapping, not additive. Caveat: our test apps top out at ~260 SSR modules; if a very large app ever shows cost, an easy follow-up is to parallelize the walk.Cross-framework. The change is in the framework-agnostic core, so it applies to React, Solid, and Vue Start identically. All three were validated (see §7).
Scope / limitations. The change is confined to the
ssrIsProviderVite build path; dev, the RSC/custom-provider path, and thersbuildbundler are untouched by construction and were verified unaffected.7. Testing & validation
New automated regression tests (in
e2e/react-start/server-functions/tests/server-functions.spec.ts), each reproducing a shape that was broken::721— server function reached only through a cross-file middleware.:750— server function called only from another server function's handler.:777— a multi-level cross-file chain (A → B → C), which specifically exercises the Phase-2 loop iterating more than once.:797— a server function in a separate package whose handler calls a nested server-only function in that same package. Backed by a small fixture package,e2e/react-start/server-functions-shared-lib/(seesrc/index.ts:7andsrc/internal.ts:7).All four fail on the original code (with the exact
Server function info not founderror) and pass with the fix.Full suite — all green:
nx affected):test:eslint,test:types,test:unit,test:build— all pass.server-functions57,rsc258,basic134,server-functions-global-middleware7,static-server-functions5,server-routes6,serialization-adapters8.server-functions29,server-routes2.server-functions27,server-routes2.No
Server function info not foundanywhere. Theserialization-adapterssuite passing is notable — it exercises the "return a server function to the client and call it" path, confirming the fix doesn't disturb the callable-anywhere behavior.8. Risk assessment
9. How it's packaged
Two clean, separated commits on
fix/server-fn-cross-file-manifest:8b53769ee4—chore(deps): bumps@playwright/testto^1.61.0across the workspace. Independent of the fix; resolves intermittent test-runner stalling. (117 files: package manifests + lockfile.)d33b368de7—fix(start-plugin-core): the one-file fix, a Changesets entry (@tanstack/start-plugin-corepatch,.changeset/fix-server-fn-manifest-cross-file.md), the four regression tests, and the fixture package. (17 files.)The Changesets entry means the fix will be released as a patch version of
@tanstack/start-plugin-core, cascading patch bumps to the React/Solid/Vue Start packages automatically.10. Key file reference (quick index)
packages/start-plugin-core/src/start-compiler/server-fn-resolver-module.ts:53.../server-fn-resolver-module.ts:42.../start-compiler/handleCreateServerFn.ts:350,:476.server()body stripped on client.../start-compiler/handleCreateMiddleware.ts:68.../vite/planning.ts:204,:208,:211packages/start-server-core/src/createSsrRpc.ts:10.../vite/start-compiler-plugin/plugin.ts:781.../start-compiler-plugin/plugin.ts:270(Phase 1:278, Phase 2:317).../start-compiler-plugin/plugin.ts:239.../start-compiler-plugin/plugin.ts:792e2e/react-start/server-functions/tests/server-functions.spec.ts:721,750,777,797e2e/react-start/server-functions-shared-lib/src/{index,internal}.tsSummary by CodeRabbit
Bug Fixes
Tests
Chores
@playwright/test) to a newer version across the end-to-end test projects and workspace configuration.