Skip to content

refactor(use-cache): move server function directives to user land#2156

Draft
james-elicx wants to merge 8 commits into
fix/use-cache-nested-function-propsfrom
codex/pr-1871-userland-server-functions
Draft

refactor(use-cache): move server function directives to user land#2156
james-elicx wants to merge 8 commits into
fix/use-cache-nested-function-propsfrom
codex/pr-1871-userland-server-functions

Conversation

@james-elicx

@james-elicx james-elicx commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

POC for implementing server-function directives entirely in vinext user land instead of adding the serverFunctionDirectives option and orchestration plugin originally proposed in vitejs/vite-plugin-react#1246.

This update pins @vitejs/plugin-rsc to the latest #1246 prerelease at 50eaf476, which incorporates the merged server-reference registration API from vitejs/vite-plugin-react#1310. vinext init and deploy-time dependency installation also use that exact prerelease because the registry's stable 0.5.30 package predates #1310 despite sharing the same package version. This PR remains stacked on #1871.

Vinext-owned lifecycle

Vinext now uses a single vinext:server-function-directives plugin before rsc:use-server. Running first preserves the original module shape for mixed module-level "use server" boundaries, while #1310's independently owned claims let the two plugins update metadata without an after-plugin restoration pass.

The plugin uses RscPluginManager.serverReferences to:

  • call resolve() for plugin-rsc's canonical development and build reference identity
  • call replaceClaim() for references emitted by vinext
  • call deleteClaim() when a directive is removed or another environment does not own the reference
  • rely on plugin-rsc to aggregate compatible claims, deduplicate export names, and reject conflicting ownership
  • hand exclusive file-level ownership between vinext:server-function-directives and rsc:use-server during HMR

As a result, vinext no longer needs:

  • a second metadata plugin after rsc:use-server
  • a private metadata ownership map
  • direct writes to serverReferenceMetaMap
  • local copies of plugin-rsc's reference-key hashing or Vite URL normalization

The private /* __vinext_server_function_directives__ */ marker remains only to avoid reprocessing vinext's own transformed output; plugin-rsc does not interpret it.

Transform composition

Vinext composes the public low-level primitives directly:

  • transformWrapExport() for module-level custom directives
  • transformHoistInlineDirective() for function-level custom directives
  • transformDirectiveProxyExport() for SSR/client proxies
  • transformExpandExportAll() for module-level re-exports

Generated RSC transforms import registerServerReference from @vitejs/plugin-rsc/react/rsc/server.

Cache replay API

Cached Flight replay continues to use the API merged in vitejs/vite-plugin-react#1289:

createFromReadableStream(stream, {}, { preserveServerReferences: true })

This preserves opaque server references while replaying cached RSC without importing their implementation into the replaying RSC runtime.

Upstream dependencies

This POC now depends on:

It does not depend on:

  • a serverFunctionDirectives plugin option
  • plugin-rsc-owned custom directive orchestration
  • upstream recognition of vinext's transform marker
  • plugin-rsc preserving vinext metadata through rsc:use-server

Validation

  • focused formatting, lint, and type checks
  • frozen lockfile install verification
  • 29 server-function directive transform tests, including mixed claims, directive removal, and file-level ownership handoff
  • production vinext package build and knip checks
  • production nested cached-function action round-trip
  • production App Router E2E for direct Client Component imports and cached Flight replay with nested functions
  • development App Router HMR transition: "use cache""use server""use cache"
  • full CI, including all unit/integration shards, App Router production E2E, Linux/Windows packed create-next-app installs, example deploys, and smoke tests

Refs #1871

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2156
npm i https://pkg.pr.new/vinext@2156

commit: a1c2e67

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

/bigbonk review — PR #2156

Reviewed the userland server-function directive plugin, the cache-runtime replay change, and the test updates. I installed the pinned prerelease (@vitejs/plugin-rsc@5a2fd750.5.28) via a frozen vp install, then ran focused checks/tests:

  • vp check on server-function-directives.ts, index.ts, cache-runtime.ts, use-cache-transform.test.ts — clean
  • vp test run tests/use-cache-transform.test.ts — 28/28 pass
  • vp test run tests/app-router-production-server.test.ts -t "resolves nested 'use cache' functions passed as props" — pass
  • vp run vinext#build — completes

Heads-up on local envs: with a stale node_modules (plugin-rsc 0.5.27), vp check reports 12 TS errors in server-function-directives.ts (missing moduleDirective/parameters/directive on the transform types). These vanish after installing the pinned prerelease. Not a PR defect, but anyone reviewing locally must vp install first or they'll chase phantom errors.

Parity verification (positive)

I diffed the locally-reproduced helpers against the installed plugin-rsc source and they line up with the authoritative implementation:

  • hashString matches transforms hashString exactly (sha256, hex, first 12 chars).
  • The build/dev normalizedId branch mirrors plugin-rsc's command === "build" ? hashString(toRelativeId(id)) : normalizeViteImportAnalysisUrl(rscEnv, id).
  • The plugin ordering splice produces [vinext:server-function-directives, rsc:use-server, vinext:server-function-directive-metadata], which the new test asserts.

Findings

1. (low) matchDirective breaks for a RegExp directive carrying the g flag.
server-function-directives.ts:129. new RegExp(directive.source, directive.flags) preserves g, and value.match(pattern) on a global regex returns all match strings with no capture groups — so directiveMatch/moduleMatch[0] would be wrong for the whole directive lifecycle. The pattern.lastIndex = 0 line is a no-op for .match(). No live bug (the only definition is /^use cache.*$/, no g), but this file now exposes a generic userland directive: string | RegExp API, so stripping g (or using new RegExp(source, flags.replace("g",""))) would harden it.

2. (low) normalizeViteImportAnalysisUrl reproduction uses split("?") instead of Vite's cleanUrl.
server-function-directives.ts:221. Vite strips both ? and # (/[?#].*$/); this only splits on ?, so an id with a # fragment would keep it in the fs.existsSync check. Also the /@id/ fallback doesn't guard against an already-/@id/-prefixed id the way Vite's wrapId does. In practice these branches don't fire for RSC server ids (the server env has consumer === "server", so plugin-rsc's HMR-timestamp branch is also skipped, meaning outputs match), so this is a robustness note, not a correctness bug for the current use. Worth a comment pointing at the upstream normalizeResolvedIdToUrl so the intentional simplification is documented until the shared public-utils work lands.

3. (note) expandExportAll load uses only transformWithOxc.
server-function-directives.ts:241. Plugin-rsc's equivalent uses transformWithOxc ?? transformWithEsbuild. Fine under the Vite 8 baseline (oxc preferred per AGENTS.md), just flagging the deliberate drop of the esbuild fallback.

4. (note) cross-environment ordering assumption for the stale-proxy guard.
The serverReferenceOwnership/ownedReferences module maps drive both the "restore RSC-owned metadata after a non-owning SSR proxy pass" and "don't resurrect RSC-removed metadata" behaviors. The 28 tests exercise these transitions in a fixed order (RSC then SSR). The scheme assumes the RSC env transform lands before the SSR proxy pass for a given id in dev; genuine cross-env interleaving isn't directly tested. Given plugin-rsc drives this the same way, I don't consider it a blocker, but it's the one area I'd want an eye on if flaky metadata shows up in dev HMR.

Verdict

Logic, parity, and tests look solid. The two (low) items are hardening of the newly-exposed generic directive API rather than defects in the use cache path. No blocking issues found.

@ask-bonk

ask-bonk Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2156.

Summary

I reviewed PR #2156 (userland server-function directives). After installing the pinned prerelease via a frozen vp install, all focused checks/tests pass, the build completes, and I verified parity against the installed plugin-rsc source.

Key findings (none blocking):

  1. (low) matchDirective (server-function-directives.ts:129) preserves a g flag on RegExp directives — .match() on a global regex returns match strings without capture groups, so directiveMatch would be wrong. No live bug (/^use cache.*$/ has no g), but the file now exposes a generic userland directive API, so it's a hardening gap.

  2. (low) normalizeViteImportAnalysisUrl reproduction (:221) uses split("?") instead of Vite's cleanUrl (which also strips #), and its /@id/ fallback doesn't guard against already-prefixed ids like Vite's wrapId. Doesn't fire for RSC server ids in practice, but worth a comment.

  3. (note) expandExportAll load drops the transformWithEsbuild fallback — fine under Vite 8.

  4. (note) The stale-proxy metadata guard assumes RSC-before-SSR transform ordering in dev; tests cover the ordered cases but not genuine interleaving.

Positive verification: hashString, the build/dev normalizedId branch, and the plugin splice ordering all match the authoritative plugin-rsc implementation.

I also flagged an environment gotcha: with a stale node_modules (plugin-rsc 0.5.27), vp check surfaces 12 phantom TS errors that disappear once the pinned prerelease is installed — reviewers must vp install first.

github run

@hi-ogawa

Copy link
Copy Markdown

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants