* fix(build): preserve server conditions on Workers
* fix(build): filter browser from all server graphs
* refactor(build): rename server conditions plugin
* fix(pages): keep charset first in custom document heads
* fix(pages): preserve complete head ordering parity
* test(pages): assert dev document head ordering
* fix(pages): preserve head order across Fast Refresh
* fix(pages): align trace metadata head ordering in dev
* fix(pages): match Next head manager updates
* fix(pages): order unknown head tags last
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): preserve request.cf in route handlers
Cloudflare Workers attaches request.cf at the inbound request boundary, but App Router dispatch rebuilt Route Handler requests with the standard Request constructor and discarded that runtime metadata.\n\nUse the existing metadata-preserving URL clone boundary so both Node and Edge Route Handlers retain cf while keeping their current URL normalization semantics.
* fix(app-router): preserve request.cf through runtime wrapping
Route Handler request tracking can rebuild an already-normalized request when restoring basePath or applying middleware header overrides. Those reconstructions discarded Cloudflare metadata before the final NextRequest reached user code.\n\nPreserve cf at both reconstruction points while retaining the existing body-transfer and RequestInit behavior, with runtime-level coverage for each path.
* fix(app-router): treat request.cf as dynamic state
Preserving Cloudflare request metadata makes request.cf observable to Route Handlers, but the request proxy did not classify reads as dynamic. Geo-dependent responses could therefore enter the route-wide ISR cache, while static route modes exposed request-specific metadata.\n\nApply the existing ip and geo policy to cf: track reads in automatic mode, hide it under force-static, and reject it under dynamic error mode.
* fix(app-router): preserve request.cf across clones
Tracked Route Handler requests inherit the standard Request clone implementation, which omits Cloudflare request metadata. A handler that cloned before reading cf therefore lost the metadata even though the original tracked request retained it.\n\nCentralize tracked request cloning so cf is reattached before the clone is recursively wrapped with the same dynamic request policy.
* fix(app-router): track reflective request.cf access
Tracked Route Handler requests enforced dynamic policy only through the Proxy get trap. Descriptor, membership, and key-enumeration reads could expose Cloudflare metadata without marking the handler dynamic or respecting static modes.
Route explicit cf reflection through the same policy, filtering configurable cf keys under force-static and rejecting reflective access under dynamic error mode.
* fix(app-router): retain request proxy through valueOf
Binding every Request method to the underlying NextRequest let valueOf return the raw target. Subsequent request.cf reads could then bypass dynamic tracking and static-mode policy.
Bind valueOf to the proxy receiver while leaving branded Request methods on the underlying target, and cover the escape in all three request modes.
* fix(app-router): bind reflection to request proxy
Inherited Object reflection helpers were still bound to the raw NextRequest, allowing ownership checks to bypass request.cf dynamic policy.
Bind exact Object.prototype methods to the proxy receiver while preserving the branded target for Web Request methods, and cover ownership and enumerability checks in every request mode.
* fix(app-router): preserve proxy for request extensions
Binding unknown request properties to the raw NextRequest let user-defined methods and getters bypass request.cf policy.
Snapshot the runtime's built-in Request surface for branded target access while keeping own and unknown extensions on the proxy receiver. Cover both method and accessor escapes in every request mode.
* fix(app-router): hide cf before locking requests
Force-static proxies filtered request.cf from reflective operations, but a non-extensible target made that omission violate Proxy invariants.
Remove the configurable metadata before preventing extensions, and cover preventExtensions, seal, and freeze while preserving force-static policy.
* fix(app-router): bind Worker request members correctly
Cloudflare may expose Web IDL Request members as own properties, so classifying every own member as a user extension caused illegal invocation errors in Workers.
Use the captured built-in API surface regardless of property placement, while keeping unknown extension names on the proxy receiver. Add coverage for an own branded accessor.
* fix(app-router): distinguish request member shadows
Name-only branded member detection fixed Workers own-property layouts but treated post-wrap user shadows as runtime Web IDL members, allowing their this-based reads to escape the proxy.
Snapshot runtime-owned descriptors before route code receives the request and bind only unchanged implementations to the target. Later shadows retain the proxy receiver in every request mode.
* fix(app-router): detect request prototype shadows
Own-descriptor snapshots still treated replaced inherited Request members as branded, allowing prototype getters to bypass request.cf policy.
Snapshot each resolved built-in implementation and compare the currently resolved descriptor before selecting the raw target. Own and prototype shadows now retain the tracked receiver.
* fix(app-router): snapshot request built-ins before routes
* fix(app-router): preserve branded Worker request methods
* fix(app-router): preserve reflected cf after locking
* refactor(app-router): contain request.cf policy on its target
Route handlers need Workers metadata to follow static-generation policy without changing the semantics of every Request property. Use the configurable cf accessor copied onto NextRequest as the single policy boundary, and delegate request reconstruction to the canonical clone helpers.
* fix(app-router): avoid synthesizing absent request.cf
Ordinary requests should not gain an own cf property merely because route-handler dynamic tracking is active. Install the target accessor only when Workers metadata exists, while keeping direct absent reads subject to automatic, force-static, and error policy.
* fix(app-router): harden request.cf tracking
* chore(test): register worker route fixtures
* chore(app-router): deduplicate request.cf descriptor
---------
Co-authored-by: James <james@eli.cx>
* test(app-router): cover data-returning server actions skipping visible commits
Data-returning fetch actions with no revalidation must resolve their value
without applying the RSC tree — otherwise the server tree hands forms a
fresh initialState and wipes pending edits (the Payload getFormState loop).
vinext's server omits `root` for these responses (shouldSkipPageRendering in
app-server-action-execution.ts) and the client returns the value directly
without committing, mirroring Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
The behavior exists but was uncovered by unit tests and had no e2e
asserting form edits survive the roundtrip. Add both so regressions fail
loudly instead of silently resetting form state.
Unit tests (tests/app-browser-server-action-client.test.ts):
- root omitted + ok returnValue -> resolves data, no commit, no cache clear
- root omitted + failed action -> throws, no commit
- root present + revalidation header -> commits decoded tree with returnValue
- raw full-tree payload -> commits with undefined returnValue
E2e (tests/e2e/app-router/server-actions.spec.ts): a blur-triggered
data-returning action (Payload getFormState shape) keeps the pending input
edit and does not advance the page's server render counter.
* test(app-router): add revalidated void-action case; fix e2e describe placement
Follow-up from adversarial review:
- Move the form-preservation e2e out of the "Server action forwarding loop
guard" describe into its own "Data-returning server actions" block.
- Add a unit case for revalidated void actions (root present, no
returnValue): commits the tree with undefined returnValue and the
staticAndDynamic revalidation kind.
* test(app-router): lock void-action returnValue shape from round-2 review
Round-2 adversarial review findings:
- The revalidated void-action unit case mocked `{ root }` without a
returnValue, but the server always pairs a re-rendered root with a
returnValue record — void actions come back as `{ ok: true, data:
undefined }`, which Flight serializes as `$undefined` and decodes to a
truthy object. Mock the real shape and assert the real value, plus the
caller-facing `undefined` resolution.
- Clear all mocks after each unit test so a stray mockResolvedValueOnce
cannot leak across cases.
- Reword the e2e comment: the server render counter is the discriminator
for "no tree update"; the input assertion guards the user-visible
contract only (React can reconcile an uncontrolled input in place).
* fix(use-cache): support nested cache functions passed as props
* fix(use-cache): use inline registerServerReference instead of broken 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.
* fix(use-cache): use correct normalised id and register in manifest for 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.
* fix(use-cache): wrap hoisted exports as cached server references and 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.
* docs(use-cache): document dev-key normalisation scope for inline cache server references
* fix(use-cache): throw instead of emitting unresolvable inline cache server 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.
* test(use-cache): pin unencrypted closure-captured bound args and document 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.
* refactor(use-cache): route registerServerReference through a vinext shim 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.
* test(use-cache): pin cached-invoke semantics for the closure-bound getMessage 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.
* fix(use-cache): encrypt closure-bound arguments
* refactor(use-cache): use plugin-rsc directive transforms
* test(use-cache): cover directive transforms across environments
* fix(cache): update RSC directive prerelease
* fix(cache): stabilize directive reference tests
* style(cache): format HMR test
* refactor(use-cache): move server function directives to user land
* refactor(use-cache): clarify generic directive plugin naming
* refactor(use-cache): own directive plugin types
* refactor(use-cache): use plugin-rsc metadata map directly
* refactor(use-cache): own server reference metadata lifecycle
* chore(use-cache): keep directive type internal
* refactor(use-cache): adopt server reference claims
* fix(init): install required plugin-rsc prerelease
* feat(rsc): harden use cache server functions
* feat(cache): adopt plugin-rsc transform primitives
* refactor(cache): rename callable plugin
* test(init): update plugin-rsc install expectations
* test(cache): avoid reloading during HMR retries
* test(cache): align callable references with plugin-rsc
* fix(cache): align mixed directives with plugin-rsc 0.5.34
* fix(cache): harden callable use cache transforms
* fix(cache): support manually configured RSC
* fix(cache): harden manual RSC ordering
* fix(build): throw error if there is `_next` folder inside the public folder
The `_next` folder under the public folder conflicts
with the internal `_next` route, which is not expected
* fix(build): harden public asset conflict validation
---------
Co-authored-by: James <james@eli.cx>