Skip to content

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Dec 20, 2025

fixes #5976

Summary by CodeRabbit

  • New Features

    • Added a new "Server Function" test page and navigation link (route /server-only-fn) to validate server-only function behavior and layered server calls.
  • Tests

    • Added end-to-end tests covering server-only function execution from the server and expected client-side invocation failures.
  • Chores

    • Consolidated global async storage to a shared singleton so storage is consistent across bundles.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Walkthrough

Adds a server-only createServerFn and route, threads provider-vs-caller awareness through compilation and manifest generation, makes directive compilation client-aware, updates runtime server-fn resolution (importer/getServerFnById opts), and switches AsyncLocalStorage to global singletons; includes e2e tests validating server-only behavior.

Changes

Cohort / File(s) Summary
E2E Server-only function & routes
e2e/react-start/server-functions/src/functions/fnOnlyCalledByServer.ts, e2e/react-start/server-functions/src/routes/server-only-fn.tsx, e2e/react-start/server-functions/src/routes/index.tsx, e2e/react-start/server-functions/src/routeTree.gen.ts
Add a server-only createServerFn export, a new /server-only-fn route (proxy + client-obtained invocation), navigation link, and integrate the route into the generated route tree.
E2E tests
e2e/react-start/server-functions/tests/server-functions.spec.ts
Add tests: one asserting server-side proxy invocation succeeds; another ensuring direct client invocation errors (500) and is whitelisted.
Directive functions plugin (compile-time)
packages/directive-functions-plugin/src/compilers.ts, packages/directive-functions-plugin/src/index.ts
Add isClientReferenced on DirectiveFn, pass knownDirectiveFns and isClientEnvironment through compile pipeline, extend ReplacerFn signature, and ensure SSR uses consistent extracted filenames for client-referenced directives.
Server functions plugin / manifest generation
packages/server-functions-plugin/src/index.ts
Add per-caller optional getServerFnById, introduce generateManifestModule(...) to emit provider-aware manifests (optionally include isClientReferenced checks), and thread getKnownDirectiveFns into plugin env.
create-server-fn compilation & handling
packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts, packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts, packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
Add isProviderFile flag detection (via directiveSplitParam), propagate it into compile/handler logic, and only attach the handler second-argument for provider/extracted files.
Start core config & build flow
packages/start-plugin-core/src/plugin.ts, packages/start-plugin-core/src/start-manifest-plugin/plugin.ts
Introduce serverFnProviderEnv in resolved config, derive ssrIsProvider, optionally build provider env, and change manifest emission guard to check provider environment.
Compiler/tests updates for isProviderFile
packages/start-plugin-core/tests/*, packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
Update compile API to accept isProviderFile in tests and compiler calls; update snapshots to reflect provider vs caller outputs.
Start server core — RPC, handler, storage, typings
packages/start-server-core/src/createSsrRpc.ts, packages/start-server-core/src/server-functions-handler.ts, packages/start-server-core/src/request-response.ts, packages/start-server-core/src/tanstack-start.d.ts, packages/start-server-core/src/serializer/ServerFunctionSerializationAdapter.ts
createSsrRpc accepts optional importer; server-functions-handler & serializer now call getServerFnById(id, { fromClient: true }); getServerFnById typing extended; request-response moves AsyncLocalStorage to a global singleton.
Start storage context
packages/start-storage-context/src/async-local-storage.ts
Replace module-local AsyncLocalStorage with a Symbol-keyed global singleton to share storage across bundles.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SSR
    participant Manifest
    participant Provider

    Note over Client,SSR: Client-initiated server call
    Client->>SSR: HTTP/RPC -> invoke SSR handler
    SSR->>Manifest: getServerFnById(functionId, {fromClient: true})
    Manifest->>Manifest: check entry.isClientReferenced
    alt isClientReferenced == true
        Manifest-->>SSR: reject (500)
        SSR-->>Client: 500 error
    else
        Manifest->>Provider: invoke implementation (direct or via importer)
        Provider-->>Manifest: result
        Manifest-->>SSR: result
        SSR-->>Client: result
    end

    Note over SSR,Provider: Server-side proxy call (internal)
    SSR->>Provider: invoke server-only fn (importer or direct call)
    Provider-->>SSR: result
    SSR-->>Client: wrapped result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Points to focus during review:

  • Correct detection and propagation of isProviderFile / directiveSplitParam (create-server-fn plugin files).
  • Manifest generation differences when includeClientReferencedCheck is true and generated getServerFnById variants (server-functions-plugin).
  • Threading knownDirectiveFns/isClientEnvironment through directive compilation and ensuring no duplicate extraction (directive-functions-plugin).
  • Impact of getServerFnById signature change across consumers (start-server-core, generated manifests, typings).
  • Global AsyncLocalStorage initialization and safety across bundles (request-response, start-storage-context).

Possibly related PRs

Suggested reviewers

  • chorobin
  • brenelz

Poem

🐰 I hid a fn where carrots grow deep,
Only the server may wake it from sleep.
Manifests hum and importers gleam,
Proxy hops swift, tests pass like a dream.
🍃 No tree-shake, just tidy green code.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: restructure server function environment handling' is directly related to the main objective of preventing createServerFn from being tree-shaken when referenced exclusively on the server, which is the core issue in #5976.
Linked Issues check ✅ Passed The PR implements comprehensive changes to address the tree-shaking issue in #5976 by introducing environment-aware handling, provider environment configuration, client-reference tracking, and manifest-based server function resolution, directly enabling server-only functions to be preserved in production builds.
Out of Scope Changes check ✅ Passed Changes align with the objective to fix server function tree-shaking: directive-functions-plugin and server-functions-plugin track client-referenced functions; start-plugin-core detects provider files and passes context; start-server-core and storage modules support the new manifest-based resolution. E2E tests validate the fix with new server-only function scenarios.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-5976

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0e7fb8 and c902bde.

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

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/start-server-core/src/serializer/ServerFunctionSerializationAdapter.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • packages/start-server-core/src/serializer/ServerFunctionSerializationAdapter.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (1)
packages/start-server-core/src/serializer/ServerFunctionSerializationAdapter.ts (1)

18-21: LGTM! Clear security boundary established.

The addition of { fromClient: true } correctly treats deserialized function IDs as client-originated for validation purposes. When getServerFnById receives this flag, it validates that the function is marked as isClientReferenced, throwing an error if attempting to call server-only functions through deserialization. The comment effectively explains the rationale: when a function ID arrives through serialization, it crossed a network boundary and must be validated with the same rigor as direct HTTP calls.


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

@nx-cloud
Copy link

nx-cloud bot commented Dec 20, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit c902bde

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

☁️ Nx Cloud last updated this comment at 2025-12-20 21:44:04 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 20, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: c902bde

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

♻️ Duplicate comments (1)
packages/start-server-core/src/request-response.ts (1)

54-56: Same initialization race condition as in async-local-storage.ts.

This follows the same non-atomic check-then-initialize pattern as the start storage context. While the race window is minimal in practice, consider using the same fix if you address it in the other file for consistency.

🧹 Nitpick comments (2)
packages/server-functions-plugin/src/index.ts (2)

270-287: Consider extracting indentation constants for cleaner template generation.

The template literal mixing indentation levels results in inconsistent whitespace in generated code. While functionally correct, this affects readability when debugging generated manifests.

🔎 Proposed fix to normalize indentation
 function generateManifestModule(
   directiveFnsById: Record<string, DirectiveFn>,
   includeClientReferencedCheck: boolean,
 ): string {
   const manifestEntries = Object.entries(directiveFnsById)
     .map(([id, fn]) => {
-      const baseEntry = `'${id}': {
-                  functionName: '${fn.functionName}',
-        importer: () => import(${JSON.stringify(fn.extractedFilename)})${
-          includeClientReferencedCheck
-            ? `,
-        isClientReferenced: ${fn.isClientReferenced ?? true}`
-            : ''
-        }
-      }`
+      const clientRefPart = includeClientReferencedCheck
+        ? `,\n      isClientReferenced: ${fn.isClientReferenced ?? true}`
+        : ''
+      const baseEntry = `'${id}': {
+      functionName: '${fn.functionName}',
+      importer: () => import(${JSON.stringify(fn.extractedFilename)})${clientRefPart}
+    }`
       return baseEntry
     })
     .join(',')

289-297: Consider adding the function ID to the error message for easier debugging.

The error message could be more specific about which function failed the client-referenced check.

🔎 Proposed improvement
   const clientReferencedCheck = includeClientReferencedCheck
     ? `
       // If called from client, only allow client-referenced functions
       if (opts?.fromClient && !serverFnInfo.isClientReferenced) {
-        throw new Error('Server function not accessible from client: ' + id)
+        throw new Error('Server function not accessible from client (not client-referenced): ' + id)
       }
 `
     : ''
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22d1b4a and 89139a5.

📒 Files selected for processing (20)
  • e2e/react-start/server-functions/src/functions/fnOnlyCalledByServer.ts (1 hunks)
  • e2e/react-start/server-functions/src/routeTree.gen.ts (11 hunks)
  • e2e/react-start/server-functions/src/routes/index.tsx (1 hunks)
  • e2e/react-start/server-functions/src/routes/server-only-fn.tsx (1 hunks)
  • e2e/react-start/server-functions/tests/server-functions.spec.ts (1 hunks)
  • packages/directive-functions-plugin/src/compilers.ts (8 hunks)
  • packages/directive-functions-plugin/src/index.ts (4 hunks)
  • packages/server-functions-plugin/src/index.ts (4 hunks)
  • packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts (2 hunks)
  • packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts (2 hunks)
  • packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts (2 hunks)
  • packages/start-plugin-core/src/plugin.ts (5 hunks)
  • packages/start-plugin-core/src/start-manifest-plugin/plugin.ts (1 hunks)
  • packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts (1 hunks)
  • packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts (7 hunks)
  • packages/start-server-core/src/createSsrRpc.ts (1 hunks)
  • packages/start-server-core/src/request-response.ts (1 hunks)
  • packages/start-server-core/src/server-functions-handler.ts (1 hunks)
  • packages/start-server-core/src/tanstack-start.d.ts (1 hunks)
  • packages/start-storage-context/src/async-local-storage.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/start-plugin-core/src/start-manifest-plugin/plugin.ts
  • packages/start-server-core/src/tanstack-start.d.ts
  • e2e/react-start/server-functions/src/functions/fnOnlyCalledByServer.ts
  • packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts
  • packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
  • e2e/react-start/server-functions/src/routes/index.tsx
  • packages/start-server-core/src/request-response.ts
  • packages/start-plugin-core/src/plugin.ts
  • e2e/react-start/server-functions/src/routeTree.gen.ts
  • packages/start-server-core/src/server-functions-handler.ts
  • packages/directive-functions-plugin/src/index.ts
  • e2e/react-start/server-functions/tests/server-functions.spec.ts
  • packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts
  • e2e/react-start/server-functions/src/routes/server-only-fn.tsx
  • packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts
  • packages/directive-functions-plugin/src/compilers.ts
  • packages/start-storage-context/src/async-local-storage.ts
  • packages/start-server-core/src/createSsrRpc.ts
  • packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
  • packages/server-functions-plugin/src/index.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • packages/start-plugin-core/src/start-manifest-plugin/plugin.ts
  • packages/start-server-core/src/tanstack-start.d.ts
  • e2e/react-start/server-functions/src/functions/fnOnlyCalledByServer.ts
  • packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts
  • packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
  • e2e/react-start/server-functions/src/routes/index.tsx
  • packages/start-server-core/src/request-response.ts
  • packages/start-plugin-core/src/plugin.ts
  • e2e/react-start/server-functions/src/routeTree.gen.ts
  • packages/start-server-core/src/server-functions-handler.ts
  • packages/directive-functions-plugin/src/index.ts
  • e2e/react-start/server-functions/tests/server-functions.spec.ts
  • packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts
  • e2e/react-start/server-functions/src/routes/server-only-fn.tsx
  • packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts
  • packages/directive-functions-plugin/src/compilers.ts
  • packages/start-storage-context/src/async-local-storage.ts
  • packages/start-server-core/src/createSsrRpc.ts
  • packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
  • packages/server-functions-plugin/src/index.ts
🧠 Learnings (3)
📚 Learning: 2025-10-01T18:31:35.420Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: e2e/react-start/custom-basepath/src/routeTree.gen.ts:58-61
Timestamp: 2025-10-01T18:31:35.420Z
Learning: Do not review files named `routeTree.gen.ts` in TanStack Router repositories, as these are autogenerated files that should not be manually modified.

Applied to files:

  • e2e/react-start/server-functions/src/routeTree.gen.ts
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

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

Applied to files:

  • e2e/react-start/server-functions/src/routeTree.gen.ts
🧬 Code graph analysis (5)
packages/start-server-core/src/tanstack-start.d.ts (1)
packages/start-server-core/src/fake-start-server-fn-manifest.ts (1)
  • getServerFnById (1-1)
packages/directive-functions-plugin/src/index.ts (1)
packages/directive-functions-plugin/src/compilers.ts (2)
  • DirectiveFn (12-24)
  • GenerateFunctionIdFn (31-35)
e2e/react-start/server-functions/src/routes/server-only-fn.tsx (1)
e2e/react-start/server-functions/src/functions/fnOnlyCalledByServer.ts (1)
  • fnOnlyCalledByServer (4-6)
packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts (1)
packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts (1)
  • compile (204-282)
packages/server-functions-plugin/src/index.ts (1)
packages/directive-functions-plugin/src/compilers.ts (1)
  • DirectiveFn (12-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (41)
e2e/react-start/server-functions/src/functions/fnOnlyCalledByServer.ts (1)

1-6: LGTM! Clean test fixture for server-only function.

The implementation is straightforward and appropriate for E2E testing. The function correctly uses createServerFn().handler() and returns predictable test data.

e2e/react-start/server-functions/src/routes/index.tsx (1)

91-96: LGTM! Navigation link properly added.

The new link is consistent with the existing navigation structure and provides clear context about the test scenario.

e2e/react-start/server-functions/tests/server-functions.spec.ts (2)

547-565: LGTM! Test correctly validates server-to-server function calls.

The test follows established patterns and properly validates that the server-only function works when called from another server function.


567-582: LGTM! Test correctly validates security boundary.

The test properly verifies that server-only functions cannot be called from the client, and the use of whitelistErrors is appropriate for this negative test case.

e2e/react-start/server-functions/src/routes/server-only-fn.tsx (5)

1-12: LGTM! Clear documentation and appropriate imports.

The documentation clearly explains the test scenario and the expected behavior.


14-22: LGTM! Proxy function correctly demonstrates server-to-server calls.

This function properly demonstrates the intended use case where a server-only function is called from within another server function.


24-26: LGTM! Test function correctly demonstrates security boundary.

This function intentionally returns a server-only function to test that the client cannot execute it. The corresponding E2E test (lines 572-582 in server-functions.spec.ts) correctly validates that this fails with an error.


28-30: LGTM! Route properly configured.


32-105: LGTM! Component correctly implements both test scenarios.

The component provides appropriate UI for testing both the valid (server-to-server) and invalid (client-to-server) scenarios. Error handling is properly implemented in the second handler.

packages/start-storage-context/src/async-local-storage.ts (1)

13-25: Good use of global singleton pattern to fix bundle isolation.

The Symbol.for() approach ensures the same key across different realm contexts, and storing the singleton on globalThis correctly addresses the cross-bundle AsyncLocalStorage sharing requirement mentioned in the PR objectives.

packages/start-server-core/src/request-response.ts (1)

46-58: Consistent implementation of global singleton pattern.

The event storage follows the same global singleton approach as packages/start-storage-context/src/async-local-storage.ts, which maintains consistency across the codebase and ensures both storage contexts work correctly across bundle boundaries.

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

84-90: LGTM!

The guard condition correctly restricts manifest generation to the provider environment, which aligns with the new serverFnProviderEnv concept. This ensures the manifest is only generated in the appropriate environment where server functions are provided.

packages/start-server-core/src/createSsrRpc.ts (1)

5-16: LGTM!

The optional importer mechanism elegantly solves the tree-shaking issue. When an importer is provided (server-only functions in SSR provider environment), it uses the direct dynamic import to maintain the dependency chain. For client-referenced functions, it correctly falls back to manifest lookup. The inline comment clearly documents the two resolution paths.

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

75-79: LGTM!

Good defaulting logic—when no custom provider environment is configured, SSR serves as the provider, maintaining backward compatibility while enabling the new provider environment feature.


359-374: LGTM!

The build ordering is correct: building the provider environment last ensures that server functions discovered during client/SSR builds are properly included in the manifest. The guard conditions and error handling are appropriate.


415-421: No issue detected — functionName is already properly sanitized.

The concern about functionName containing characters that would break the string literal is already addressed. The functionName is sanitized via the makeIdentifierSafe function (packages/directive-functions-plugin/src/compilers.ts, lines 225-232), which removes all characters except alphanumeric characters, underscores, and dollar signs. This means functionName can only contain valid JavaScript identifier characters and cannot include single quotes, backslashes, or other problematic characters. The bracket notation in m['${d.functionName}'] is safe and requires no additional escaping.

Likely an incorrect or invalid review comment.

packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts (1)

35-35: LGTM!

The new directiveSplitParam option aligns with the updated ServerFnCompiler interface and follows a consistent naming convention.

packages/start-server-core/src/server-functions-handler.ts (1)

50-50: LGTM!

The fromClient: true flag correctly indicates that this handler processes HTTP requests originating from clients, aligning with the extended getServerFnById signature for client/server-origin tracking.

packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts (2)

204-212: LGTM!

The extended compile method signature with isProviderFile: boolean is well-typed and correctly propagates the flag through the compilation pipeline to handleCreateServerFn.


262-273: LGTM!

The isProviderFile flag is correctly passed to handleCreateServerFn, enabling the conditional second-argument inclusion for provider files.

packages/start-plugin-core/src/create-server-fn-plugin/handleCreateServerFn.ts (2)

14-18: LGTM!

Clear and helpful documentation explaining the purpose of isProviderFile and why only provider files should include the handler implementation as a second argument.


159-165: LGTM!

The conditional logic correctly ensures the handler implementation is only included as a second argument for server-side provider files, preventing code duplication in caller files that import from the extracted chunk.

packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts (2)

42-45: LGTM!

The buildDirectiveSplitParam helper correctly sanitizes the directive string into a URL-safe parameter format, enabling reliable detection of provider/extracted files.


129-132: LGTM!

The isProviderFile detection correctly happens before cleanId(id) strips query parameters, ensuring the directive split param is detected. The flag is then properly passed to the compiler.

packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts (3)

67-131: LGTM!

Excellent test coverage for the provider/caller distinction. The tests clearly differentiate:

  • Client compilation (no second argument, dead code eliminated)
  • Server caller (no second argument, implementation from extracted chunk)
  • Server provider (includes second argument as the implementation file)

The inline snapshots accurately reflect the expected compiled output for each scenario.


133-216: LGTM!

The DCE (dead code elimination) tests are properly expanded to cover both caller and provider scenarios, verifying that:

  • Server callers don't include variable definitions needed only by handlers
  • Server providers retain those variables and include the handler as a second argument

50-64: Verify the parameterized test isProviderFile configuration.

The parameterized tests set isProviderFile: env === 'server', meaning all server environment tests compile as provider files. This appears intentional to match existing snapshot expectations. If the existing snapshots in ./snapshots/server/ expect provider-style output, this is correct. However, consider whether test coverage would benefit from parameterized tests that also exercise the caller scenario (isProviderFile: false) via additional file snapshots, especially since the explicit tests below (lines 82-98) demonstrate both patterns should be tested.

packages/directive-functions-plugin/src/index.ts (3)

36-53: LGTM!

Well-designed type extensions:

  • envConsumer enables explicit client/server designation independent of environment name
  • getKnownDirectiveFns allows SSR callers to look up canonical extracted filenames from client builds, ensuring consistent imports across environments

93-129: LGTM!

The refined selection logic correctly handles the provider/caller distinction:

  • Provider options for extracted files (directive split param present)
  • Caller options for non-extracted files based on current environment
  • Fallback to provider when no matching caller exists

The debug logging clearly indicates which path is taken.


130-149: The concern about undefined envConsumer handling is incorrect.

The expression envOptions.envConsumer === 'client' always evaluates to a boolean value (true or false), not undefined. When envConsumer is undefined, the strict equality comparison returns false, which is the appropriate safe default. The type signature (isClientEnvironment?: boolean) and usage in the compiler (opts.isClientEnvironment || !!knownFn) both correctly handle this boolean value, making explicit default handling unnecessary.

Likely an incorrect or invalid review comment.

packages/server-functions-plugin/src/index.ts (4)

26-32: LGTM - Clear documentation for the new getServerFnById field.

The JSDoc clearly explains when this custom implementation is needed for server callers loading modules from different environments.


161-164: LGTM - Correct determination of SSR provider relationship.

The logic properly identifies when the SSR environment is the provider, enabling the appropriate handling of server-only-referenced functions.


175-177: LGTM - Enables SSR callers to use canonical extracted filenames.

Passing directiveFnsById via getKnownDirectiveFns allows SSR callers to resolve functions consistently with the client build.


279-282: Verify the default true for isClientReferenced is intentional.

When fn.isClientReferenced is undefined, defaulting to true means unknown functions are treated as client-referenced. Confirm this is the intended fallback behavior for security (allowing access) vs. safety (denying by default).

packages/directive-functions-plugin/src/compilers.ts (7)

19-24: LGTM - Interface extension aligns with the relevant code snippet.

The isClientReferenced field with clear documentation matches the expected interface from packages/directive-functions-plugin/src/compilers.ts shown in the relevant snippets.


42-50: LGTM - ReplacerFn type properly extended.

Adding functionName and isClientReferenced with clear documentation enables downstream consumers to handle client-aware transformations.


65-77: LGTM - Well-documented options for client-aware compilation.

The JSDoc explains when and why these options are used, making the API clear for consumers.


441-458: LGTM - Consistent function ID generation across builds.

Using relative filename ensures consistent functionId regardless of machine, and the local extracted filename is computed correctly before checking knownDirectiveFns.


460-469: Clear logic for determining client-referenced status.

The two-condition check (isClientEnvironment || !!knownFn) correctly identifies functions that are client-referenced either directly or from a previous build.


566-569: LGTM - Replacer receives all necessary context.

The additional functionName and isClientReferenced parameters enable replacers to generate appropriate code based on client-awareness.


585-593: LGTM - Directive registration includes isClientReferenced.

The directiveFnsById now stores the isClientReferenced flag, enabling downstream manifest generation to include this metadata.

Comment on lines +476 to +497
if (isClientReferenced && !compileDirectiveOpts.isDirectiveSplitParam) {
// Find if this directive function is inside a .handler() call expression
// The structure is: .handler(directiveFn, originalImpl)
// We want to remove originalImpl (the second argument)
let currentPath: babel.NodePath | null =
directiveFn as babel.NodePath | null
while (currentPath && !currentPath.parentPath?.isProgram()) {
const parent: babel.NodePath | null = currentPath.parentPath
if (
parent?.isCallExpression() &&
babel.types.isMemberExpression(parent.node.callee) &&
babel.types.isIdentifier(parent.node.callee.property) &&
parent.node.callee.property.name === 'handler' &&
parent.node.arguments.length === 2
) {
// Remove the second argument (the original implementation)
parent.node.arguments.pop()
break
}
currentPath = parent
}
}
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:

#!/bin/bash
# Search for .handler() patterns in TS/TSX files
echo "=== Searching for .handler() call patterns ==="
rg -n --type ts --type tsx '\.handler\s*\(' | head -40

echo ""
echo "=== Searching for createServerFn or directive handler patterns ==="
rg -n --type ts --type tsx 'handler\s*\(' -A 5 | head -50

Repository: TanStack/router

Length of output: 246


🏁 Script executed:

#!/bin/bash
# Check if the file exists and get its structure
echo "=== Checking file existence ==="
find . -name "compilers.ts" -o -name "compilers.js" 2>/dev/null | head -5

echo ""
echo "=== Searching for .handler patterns without type restriction ==="
rg -n '\.handler\s*\(' | grep -v node_modules | head -30

echo ""
echo "=== Searching for handler calls in directive context ==="
rg -n 'handler' packages/directive-functions-plugin/src/ 2>/dev/null | head -40

Repository: TanStack/router

Length of output: 4304


🏁 Script executed:

#!/bin/bash
# Read the compilers.ts file around the mentioned lines
echo "=== Reading compilers.ts around lines 460-510 ==="
sed -n '460,510p' packages/directive-functions-plugin/src/compilers.ts | cat -n

echo ""
echo "=== Reading broader context 440-520 ==="
wc -l packages/directive-functions-plugin/src/compilers.ts

Repository: TanStack/router

Length of output: 3114


🏁 Script executed:

#!/bin/bash
# Search for directive function patterns and .handler usage
echo "=== Searching for how directive functions use .handler ==="
rg -n -B 5 -A 5 '\.handler' packages/directive-functions-plugin/ | head -100

echo ""
echo "=== Searching for directive function test examples ==="
find packages/directive-functions-plugin -name "*.test.*" -o -name "*.spec.*" | head -5

echo ""
echo "=== Looking for examples of .handler with arguments ==="
rg -n 'handler\s*\(' packages/directive-functions-plugin/src/ -A 3 | head -60

Repository: TanStack/router

Length of output: 12369


🏁 Script executed:

#!/bin/bash
# Search for method chaining patterns that include .handler()
echo "=== Searching for chained method calls with .handler ==="
rg -n '\.\w+\s*\(' packages/directive-functions-plugin/tests/compiler.test.ts | grep -B 5 -A 5 'handler' | head -80

echo ""
echo "=== Search for nested call expressions in compiler code ==="
rg -n 'CallExpression\|MemberExpression' packages/directive-functions-plugin/src/compilers.ts | head -20

echo ""
echo "=== Check if there are tests for edge cases like chained calls ==="
grep -n 'inputValidator\|middleware' packages/directive-functions-plugin/tests/compiler.test.ts | head -10

Repository: TanStack/router

Length of output: 2397


🏁 Script executed:

#!/bin/bash
# Search more specifically for method chaining patterns
echo "=== Searching for chained methods before .handler in directive context ==="
rg -n 'createServerFn\(\)' packages/directive-functions-plugin/tests/compiler.test.ts -A 1 | head -40

echo ""
echo "=== Check the compilers code for how it handles traversal ==="
sed -n '476,510p' packages/directive-functions-plugin/src/compilers.ts

echo ""
echo "=== Look for any real-world examples with .inputValidator or .middleware ==="
rg -n '\.inputValidator\|\.middleware' packages/ --type ts -B 2 -A 5 | grep -A 5 '\.handler' | head -40

Repository: TanStack/router

Length of output: 3181


🏁 Script executed:

#!/bin/bash
# Check if there are any test cases that fail or test edge cases
echo "=== Running tests to verify edge cases ==="
cd packages/directive-functions-plugin && npm test 2>&1 | tail -30

echo ""
echo "=== Searching for any comments about limitations or edge cases in compilers.ts ==="
rg -n 'TODO\|FIXME\|NOTE\|edge case\|limitation' packages/directive-functions-plugin/src/compilers.ts | head -20

Repository: TanStack/router

Length of output: 609


The .handler() argument removal assumes exactly 2 arguments without handling unexpected cases.

The code correctly guards against processing .handler() calls that don't have exactly 2 arguments (line 489: parent.node.arguments.length === 2), which aligns with the tested directive function pattern where the second argument is the original implementation. However, the code silently skips processing if the condition isn't met—there's no error, warning, or fallback behavior. If the future API changes or edge cases arise with different argument counts, the second argument won't be removed without any indication of failure.

Consider adding:

  • An explicit error or warning if .handler() is found but has an unexpected argument count
  • Documentation clarifying the assumption that directive .handler() always receives exactly 2 arguments in this compilation phase
🤖 Prompt for AI Agents
In packages/directive-functions-plugin/src/compilers.ts around lines 476 to 497,
the walk that locates a .handler(...) call silently ignores cases where the call
exists but does not have exactly two arguments; update the logic to emit a clear
diagnostic when a .handler member call is detected with an unexpected argument
count (e.g., log a warning or throw an error depending on severity) so consumers
are alerted to the mismatch, include the callee location/context in the message,
and add a brief code comment documenting the assumption that .handler should
have exactly two args for this transform.

Comment on lines +15 to +18
export function getServerFnById(
id: string,
opts?: { fromClient?: boolean },
): Promise<ServerFn>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "fake-start-server-fn-manifest.ts" --exec cat {}

Repository: TanStack/router

Length of output: 115


Update fake manifest implementation to match the new signature.

The fake-start-server-fn-manifest.ts implementation has export async function getServerFnById(): Promise<any> {} but must accept the id: string parameter and optional opts?: { fromClient?: boolean } to match the declaration. Update to:

export async function getServerFnById(
  id: string,
  opts?: { fromClient?: boolean },
): Promise<ServerFn> {}

This mismatch will cause TypeScript strict mode type errors.

🤖 Prompt for AI Agents
In packages/start-server-core/src/tanstack-start.d.ts around lines 15 to 18, the
fake manifest implementation does not match the declared signature of
getServerFnById; update the fake implementation
(fake-start-server-fn-manifest.ts) to accept the parameters id: string and
opts?: { fromClient?: boolean } and to return Promise<ServerFn> so its exported
function signature exactly matches the declaration, ensuring TypeScript strict
mode passes.

Comment on lines +21 to +23
if (!globalObj[GLOBAL_STORAGE_KEY]) {
globalObj[GLOBAL_STORAGE_KEY] = new AsyncLocalStorage<StartStorageContext>()
}
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

Be aware of potential race condition in initialization.

The check-then-initialize pattern is not atomic. While Node.js module loading is typically single-threaded, dynamic imports or concurrent worker threads could theoretically create multiple AsyncLocalStorage instances, undermining the singleton guarantee.

Consider whether this edge case matters for your use case. If concurrent initialization is possible, you could use a more defensive pattern (though it adds complexity):

🔎 Alternative atomic initialization pattern
-if (!globalObj[GLOBAL_STORAGE_KEY]) {
-  globalObj[GLOBAL_STORAGE_KEY] = new AsyncLocalStorage<StartStorageContext>()
-}
+globalObj[GLOBAL_STORAGE_KEY] ??= new AsyncLocalStorage<StartStorageContext>()

Note: The ??= operator is atomic at the JavaScript level and more concise, though the race condition risk is minimal in practice.

📝 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
if (!globalObj[GLOBAL_STORAGE_KEY]) {
globalObj[GLOBAL_STORAGE_KEY] = new AsyncLocalStorage<StartStorageContext>()
}
globalObj[GLOBAL_STORAGE_KEY] ??= new AsyncLocalStorage<StartStorageContext>()
🤖 Prompt for AI Agents
In packages/start-storage-context/src/async-local-storage.ts around lines 21 to
23, the current check-then-assign pattern for initializing
globalObj[GLOBAL_STORAGE_KEY] can race in rare concurrent import/worker
scenarios; replace it with an atomic assignment (e.g., nullish coalescing
assignment) so the global key is set exactly once without a separate existence
check, ensuring a single AsyncLocalStorage instance is produced even under
concurrent initialization.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/start-server-core/src/createSsrRpc.ts (1)

10-14: Consider adding error context for debugging server function resolution failures.

While error propagation is working correctly, adding try-catch blocks or error context could improve debugging when server function resolution fails in production (especially for the tree-shaking scenarios this PR addresses).

Optional: Add error context for better debugging
-    const serverFn = importer
-      ? await importer()
-      : await getServerFnById(functionId)
+    try {
+      const serverFn = importer
+        ? await importer()
+        : await getServerFnById(functionId)
+    } catch (error) {
+      throw new Error(
+        `Failed to resolve server function "${functionId}" via ${importer ? 'importer' : 'manifest lookup'}: ${error}`,
+        { cause: error }
+      )
+    }

This is optional since error propagation may be handled at a higher level.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89139a5 and f0e7fb8.

📒 Files selected for processing (2)
  • packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts (1 hunks)
  • packages/start-server-core/src/createSsrRpc.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/start-plugin-core/tests/createMiddleware-create-server-fn-plugin/createMiddleware.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/start-server-core/src/createSsrRpc.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • packages/start-server-core/src/createSsrRpc.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (2)
packages/start-server-core/src/createSsrRpc.ts (2)

3-5: LGTM! Clear type definitions for importer-based resolution.

The ServerFn import and SsrRpcImporter type alias provide good type safety for the new optional importer parameter, supporting the fix for server-side-only function tree-shaking.


7-14: Pass { fromClient: false } to getServerFnById in the fallback path.

The getServerFnById signature accepts an optional second parameter opts?: { fromClient?: boolean } (see tanstack-start.d.ts). When handling client-to-server calls, server-functions-handler.ts explicitly passes { fromClient: true } to enforce client-referenced function restrictions. In createSsrRpc.ts line 14, the fallback path should clarify its context by passing { fromClient: false } to indicate these are server-only lookups, not client-initiated calls.

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.

createServerFn gets tree-shaken when only called server-side

2 participants