refactor(use-cache): move server function directives to user land - #2156
refactor(use-cache): move server function directives to user land#2156james-elicx wants to merge 38 commits into
Conversation
…forward-reference module-level code
The previous approach used `noExport: true` and appended module-level
`const ${name}_$$vcf` declarations at the end of the transformed file,
then referenced them via forward reference at the call-site. This caused
a temporal dead zone (TDZ) error because `const` bindings are not
hoisted — the call-site assignment evaluated before the TLA const was
initialized, crashing all RSC files that contain function-level "use
cache" (HTTP 500 for use-cache pages, route handlers, etc.).
Fix: keep the existing hoisting/export behaviour (`noExport` stays
false) and instead wrap `registerCachedFunction(...)` with
`registerServerReference(...)` inline at call-site in the RSC
environment. This adds the RSC serialisation metadata ($$typeof, $$id)
so cached functions can be passed as props to client components
(useActionState / formAction), while not disturbing the existing
exported binding that loadServerAction relies on.
…r nested function props
The previous approach passed the raw absolute file path as the $$id to
registerServerReference. @vitejs/plugin-rsc resolves server references by a
normalised key (sha256(toRelativeId) in build; URL-path in dev), so production
would throw "server reference not found" for any cached function passed as a
client-component prop.
Also, the module was never added to the virtual:vite-rsc/server-references
manifest because only the plugin's own "use server" transform writes to
manager.serverReferenceMetaMap. Without a manifest entry, the production
serverReferences lookup has no entry for the module at all.
Fix:
- Capture the plugin-rsc manager via the rsc:minimal plugin API in
configResolved so we can write to serverReferenceMetaMap directly.
- Compute normalizedRefKey to match vitePluginUseServer's getNormalizedId():
build → sha256(toRelativeId(id)).hex.slice(0,12)
dev → id.slice(root.length) (Vite URL path)
- After transformHoistInlineDirective succeeds, register the hoisted export
names in manager.serverReferenceMetaMap[id] so the manifest is populated.
- Pass normalizedRefKey (not raw id) to registerServerReference.
Add unit tests verifying the hash formula matches plugin-rsc's own logic.
…register manifest after rsc:use-server - Derive the build-mode reference key via plugin-rsc's own manager.toRelativeId() instead of a string slice, so the hash input is byte-for-byte identical to the plugin's hashString(toRelativeId(id)). - Reassign each hoisted inline 'use cache' export at module level to registerServerReference(registerCachedFunction(fn)) so the module export itself is the cached wrapper (Next.js parity: direct action invocation goes through the cache) and call sites/manifest imports all observe the same wrapped function. - Register serverReferenceMetaMap entries from a new vinext:use-cache-server-references plugin placed after the plugin-rsc plugins: rsc:use-server deletes metaMap entries for modules without 'use server', which wiped the entries written during the use-cache transform (prod actions 404'd with 'server reference not found'). - Deduplicate the RSC/non-RSC transform branches into a single transformHoistInlineDirective call and hoist the @vitejs/plugin-rsc/react/rsc resolution out of the per-module path. - Replace the self-referential key-formula unit test with the ported Next.js fixture (use-cache-with-server-function-props/nested-cache), a dev-mode Playwright round-trip test, and a production-server integration test that resolves the serialized references via action POSTs and asserts cached-invoke semantics.
…e server references
…erver references when the plugin-rsc manager is missing When the @vitejs/plugin-rsc manager is unavailable in the rsc environment, the inline 'use cache' transform previously fell back to a locally computed reference key and still wrapped the hoisted exports — but the manifest registration plugin bails without the manager, so the emitted reference would serialize into the RSC payload yet never resolve (silent 404 on action POST in production). Fail loudly at transform time instead; the manager is a structural invariant whenever the rsc environment exists. Adds transform-level unit tests for the fail-loud path (build + dev), the non-rsc no-manager control, and build reference-key parity with plugin-rsc.
…ment the divergence Extends the nested-fn-props fixture with a cached function that closes over a value from the cached component's scope, exercising the .bind(null, ...) bound-arg path end to end: the production round-trip test asserts the captured value appears in plaintext in the flight payload (pinning the documented divergence from Next.js, which encrypts bound args by default) and that invoking the bound reference observes the captured value; the Playwright test covers the real flight-client encodeReply round-trip in dev. A transform-level test pins that captures are emitted as plain bind args. The divergence is now also documented in the README's Known limitations section.
…him to decouple from plugin-rsc module-id normalisation The inline 'use cache' prepend imported registerServerReference from a file:// URL of @vitejs/plugin-rsc/react/rsc while the cache runtime imports the same package via the bare specifier, relying on Vite normalising both to a single module id. Re-export it instead from a new vinext-owned cache-server-reference shim whose only react/rsc specifier is the same bare one cache-runtime uses, resolved from the same importer location — one module instance by construction. The transform unit test now pins that the emitted import targets the shim and never a plugin-rsc file URL.
…tMessage path Mirror the getDate cache assertion on the closure-bound path: the fixture's getMessage now appends a Math.random() suffix so cache hits are observable, and the production-server round-trip asserts that two identical bound-arg invocations return the same cached value while a different bound arg misses instead of reusing the entry. The Playwright assertion matches the suffixed message via regex.
# Conflicts: # packages/vinext/src/index.ts # pnpm-lock.yaml
commit: |
|
|
/bigbonk review for issues |
|
/bigbonk review for issues |
|
|
Review posted to PR #2156. SummaryI reviewed PR #2156 (userland server-function directives). After installing the pinned prerelease via a frozen Key findings (none blocking):
Positive verification: I also flagged an environment gotcha: with a stale |
|
I've added a support of user land server reference registration via vitejs/vite-plugin-react#1310, which is expected to simplify manual server reference management from use cache transform plugin here. |
…d-server-functions # Conflicts: # .github/workflows/ci.yml # packages/vinext/src/deploy.ts # packages/vinext/src/index.ts # packages/vinext/src/init.ts # packages/vinext/src/shims/cache-runtime.ts # playwright.config.ts # pnpm-lock.yaml # pnpm-workspace.yaml # tests/shims.test.ts # tests/use-cache-transform.test.ts
cc55a10 to
2b26266
Compare
Performance benchmarksCompared 0 improved · 1 regressed · 5 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
/bigbonk review for issues |
|
error: Your local changes to the following files would be overwritten by checkout: |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
| @@ -0,0 +1,374 @@ | |||
| import { createRequire } from "node:module"; | |||
There was a problem hiding this comment.
/bigbonk review for issues
|
|
Review posted to PR #2156. SummaryI reviewed PR #2156 (moving callable
Key positive verifications:
Findings (none blocking):
No blocking issues found. |
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
bf7bdb4 to
36c26e3
Compare
Summary
POC for implementing callable
"use cache"server references entirely in Vinext user land instead of adding theserverFunctionDirectivesoption and orchestration plugin originally proposed in vitejs/vite-plugin-react#1246.This now targets the stable
@vitejs/plugin-rsc@0.5.34release and follows the maintainer's final callable-cache plugin shape:https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/examples/use-cache-persistent/callable-cache-plugin.ts
This PR includes the still-open #1871 callable-cache foundation and references it below.
Plugin composition
Vinext inserts
vinext:server-function-directivesimmediately beforersc:use-serverand uses plugin-rsc's public transform primitives:When
rsc()is configured manually,vinext({ rsc: false })also installs the Vinext directive plugin. Vinext must appear beforersc()in the Vite plugin array; reversed order now fails fast instead of silently dropping cache transforms.transformWrapExport()for module-level"use cache"transformHoistInlineDirective()for inline"use cache"transformDirectiveProxyExport()for SSR/client proxiesRscPluginManager.serverReferences.resolve()for canonical dev/build identitiesreplaceClaim()anddeleteClaim()for Vinext-owned referencesThe generated RSC transform imports
registerServerReferencefrom@vitejs/plugin-rsc/react/rsc/server.With plugin-rsc 0.5.34, compatible Vinext and
rsc:use-serverclaims can coexist for mixed directive modules. Vinext no longer deletes or takes ownership of plugin-rsc's claims. It preserves leading file directives when injecting runtime imports so the built-in transform can run afterwards.That supports both mixed cases:
"use server"with an inline"use cache"function"use cache"with an inline"use server"functionDifferences from the upstream example
The orchestration now matches the upstream example, while Vinext keeps framework-specific cache behavior:
"use cache: <kind>"variantsregisterCachedFunction()argumentCountandacceptsSecondArgumentfor metadata/viewport behaviorchildrenslot is not cache-serializable yetVinext's cache runtime, persistence, invalidation, cache kinds, and request API restrictions remain outside plugin-rsc.
Cache replay
Cached Flight replay uses plugin-rsc's supported API:
This preserves opaque server references while replaying cached RSC without importing their implementations into the replaying RSC runtime.
Upstream requirements
This POC now depends on the public features shipped in
@vitejs/plugin-rsc@0.5.34:getPluginApi()access toRscPluginManager"use cache"/"use server"transform compositionpreserveServerReferencesduring cache replayIt does not depend on:
serverFunctionDirectivesplugin optionserverReferenceMetaMapValidation
vp check@vitejs/plugin-rsc@0.5.34Refs #1871