refactor(use-cache): move server function directives to user land - #2156
Draft
james-elicx wants to merge 557 commits into
Draft
refactor(use-cache): move server function directives to user land#2156james-elicx wants to merge 557 commits into
james-elicx wants to merge 557 commits into
Conversation
commit: |
Contributor
|
The font self-hosting unit tests depended on live Google Fonts CSS and font file requests. When the CSS fetch hit a recoverable network error, the plugin correctly skipped _vinext injection for offline fallback, but the test expected self-hosted output. Mock both fonts.googleapis.com CSS and fonts.gstatic.com font binaries for the self-hosting assertions. The offline fallback behavior remains covered by the explicit network-error test.
…ath.posix.relative (#2308)
* fix(pages): route dotted dynamic paths in dev * fix(pages): preserve i18n dotted dynamic routes * chore(pages): clarify dotted route preflight * chore(pages): share dev hostname parsing * chore(pages): reuse dev request origin
* fix(pages): preload initial dev stylesheets * fix(pages): keep dev module path encoder internal * fix(pages): harden dev stylesheet parity * fix(pages): preserve hybrid app HMR * fix(pages): preserve CSS resource query imports
* fix(cloudflare): support sentry request errors on workers * fix(cloudflare): avoid worker conditions in node pages builds * test(cloudflare): run sentry worker e2es in ci * test(cloudflare): cover sentry render errors * test(cloudflare): mirror sentry next setup in fixtures * update lockfile * update lockfile * test(cloudflare): cover sentry client instrumentation
* fix(dev): seed Cloudflare Pages Router worker deps * fix(dev): include external store selector dep * fix(dev): guard optional worker optimizer dep * Revert "fix(dev): guard optional worker optimizer dep" This reverts commit 65530f1. * fix(dev): suppress optional worker optimizer warning * fix(dev): harden optional optimizer warning filter
* test(routing): cover optional catch-all root with empty params
Add a regression test for a ROOT-level optional catch-all page
pages/[[...markdownPath]].js whose getStaticPaths emits the empty-params
entry { markdownPath: [] } (the react.dev shape). Verifies the dev server
serves the root / HTML, the /_next/data/<id>/index.json endpoint, a
non-root concrete path, and a 404 for unlisted paths under fallback:false.
The existing optional catch-all test only covers a non-root subpath; this
guards the empty-params-at-root case, which routes through the trie's
0-segment optional catch-all match (route-trie.ts) and the empty-array
path normalization (route-pattern.ts / pages-page-data.ts).
* test(routing): tighten optional catch-all root coverage
---------
Co-authored-by: James <james@eli.cx>
* fix(link): reuse full prefetch loading shells * fix(link): share loading shell prefetch ownership
* fix(css): preserve Sass partial asset URLs * fix(css): preserve Sass use namespaces * fix(css): match Sass namespace derivation
* fix(app-router): support styled-jsx from next * fix(app-router): preserve styled jsx refresh * test(app-router): exercise styled jsx compiler * fix(app-router): keep styled jsx refresh server-safe * fix(app-router): keep styled jsx refresh server-safe * test(app-router): cover server-safe styled jsx dev * fix(app-router): detect styled jsx syntax variants * fix(app-router): scope styled jsx detection * fix(app-router): parse styled jsx attributes * fix(app-router): delegate styled jsx detection * fix(app-router): preserve vinext-only style builds * fix(app-router): detect styled jsx with AST * fix(app-router): prefer Next styled jsx runtime * fix(app-router): skip styled-jsx transforms in dependencies
* fix(app-router): preserve rewritten route identity * test(app-router): isolate rewrite prefetch regression * test(app-router): cover rewritten element keys * test(app-router): remove orphaned rewrite fixture * fix(prerender): avoid reloading RSC entry after path injection
* fix(app-router): honor basePath false rewrites * fix(app-router): preserve basePath routing boundaries * test(app-router): cover basePath handler boundaries * fix(app-router): track basePath rewrite claims * chore(app-router): remove stale basePath note * fix(app-router): allow late rewrite route posts * fix(app-router): short-circuit unclaimed basePath misses * fix(app-router): preserve out-of-basePath 404 semantics * fix(app-router): validate late basePath rewrites * docs(app-router): clarify late action rewrites * fix(app-router): resolve progressive action rewrites
) * fix(config): resolve and bundle extensionless .cjs config imports `vinext init` renames CJS config files (tailwind.config.js, postcss.config.js) to .cjs when it adds "type": "module", and app code imports them extensionlessly (import cfg from "../tailwind.config"). Two problems blocked this on the app module graph: 1. vinext overrides resolve.extensions for every Vite environment with a list that, like Vite's default, omits .cjs/.cts, so the extensionless import failed with [UNRESOLVED_IMPORT]. Append .cjs/.cts (lowest priority) to buildViteResolveExtensions' default list. 2. Once resolved, vite-plugin-commonjs rewrote the .cjs module.exports to ESM export {}, but rolldown infers moduleType: cjs from the extension and re-parsed the output as CommonJS, failing with "Cannot use export statement outside a module". Return false from the commonjs() filter for project-local .cjs/.cts so vite-plugin-commonjs skips them and rolldown's own CJS interop bundles them. Everything else returns undefined, which preserves the plugin's defaults, including its existing skip of node_modules .cjs files. Fixes #13. * fix(config): harden local cjs commonjs filter * docs(config): fix cjs regression references * test(config): cover extensionless cts config imports * docs(config): clarify cjs resolve extension defaults --------- Co-authored-by: James <james@eli.cx>
…2437) * perf(build): parallelize prerender across a pool of render processes Build-time prerender rendered every static route by fetching it from a single in-process production server driven by a promise pool. React SSR/RSC rendering is CPU-bound JS, so the pool only overlaps I/O — every render serialized on one core, and raising --prerender-concurrency did nothing. Next.js forks a worker pool and saturates every core. Fork a pool of production-server child processes (one per core, capped) and round-robin the per-route render fetch across them, keeping route scanning, getStaticPaths/static-params resolution, file writing and the manifest on the main process. Pool size scales by cores AND routes, so small apps, low-memory machines, and --prerender-concurrency 1 keep the single in-process server (no fork, no regression); running from source (no built .js worker entry) also falls back to single-process. child_process, not worker_threads: worker threads contend for CPU on this workload (measured ~2x slower per route and non-scaling), which is also why Next.js uses processes. react.dev (809 routes, cold cache, same machine): 29.9s -> 15.1s, now faster than its own Next.js build (~19.3s). An 801-route static fixture: ~22s -> ~2-6s. Prerender output is byte-identical to the single-process path on deterministic renders; workers install the same NoOp cache handler the in-process path uses. A worker that exits unexpectedly fails the build loudly instead of shipping partial output. * fix(build): harden prerender worker pool --------- Co-authored-by: James <james@eli.cx>
…ctMode> (#2433) * feat(pages): enforce reactStrictMode by wrapping client root in <StrictMode> `reactStrictMode: true` was recognized but not enforced — the app root was never wrapped in <React.StrictMode>, so dev-time strict checks (double-invoked effects/render, deprecation warnings) were silently lost. Resolve `reactStrictMode` from next.config (preserved as `boolean | null` so each router applies its own default) and, for the Pages Router, wrap the client tree in <React.StrictMode> when the value is `true`. The default matches Next.js: `null`/unset is OFF for the Pages Router (`reactStrictMode === null ? false` in define-env.ts). The wrap lives in `wrapWithRouterContext` (next/router) — the single seam every render path funnels through: the initial hydration entry (production AND the dev server's inline hydration script) and every client-side navigation `root.render()` in shims/router.ts. This mirrors Next.js, whose `doRender` closure wraps in <React.StrictMode> for both the initial hydrate and subsequent `reactRoot.render()` calls (client/index.tsx). Wrapping only the production client entry would have been inert — StrictMode does nothing in production, and the dev server hydrates via a separate template — so the flag is also threaded into createSSRHandler and the dev hydration script. The wrap is gated on a client-only `window.__VINEXT_REACT_STRICT_MODE__` flag so the server-rendered tree is never wrapped (Next.js wraps client-side only); StrictMode renders no DOM, so SSR markup and hydration are unaffected. The CommitBoundary stays outside StrictMode so its commit effect is not double-invoked (Next.js keeps `<Root>` outside <StrictMode> too). `vinext check` reports reactStrictMode as "partial": enforced for the Pages Router, but the App Router is not yet wrapped (its root is mounted by the RSC client runtime, not vinext-owned code, and Next.js defaults App Router strict mode on). * test(pages): cover strict mode navigation renders --------- Co-authored-by: James <james@eli.cx>
* fix(dev): treat .js as JSX in the optimizeDeps scanner
The dep optimizer (scanner + pre-bundler) runs its own Rolldown/esbuild
pipeline that does not go through the `vinext:jsx-in-js` transform plugin,
so JSX in plain `.js`/`.mjs` source files made the dependency scan fail with
"Unexpected JSX expression" and aborted pre-bundling.
Configure the dep optimizer to treat `.js`/`.mjs` as JSX
(`optimizeDeps.rolldownOptions.moduleTypes` on Vite 8,
`optimizeDeps.esbuildOptions.loader` on Vite 7), mirroring how the main
transform treats `.js`/`.mjs` (its `/\.m?js$/` filter). Applied to the
top-level optimizeDeps and the per-environment (rsc/ssr/client) blocks via
getDepOptimizeNodeEnvOptions.
The motivating real-world symptom is that, once the scan aborts,
pre-bundling is skipped and UMD/CJS deps can fail to interop under SSR
("window is not defined"). That downstream cascade runs through a different
optimizer path and is not what this change is verified to fix — the added
tests assert only that the dependency scan no longer aborts on
JSX-in-`.js`/`.mjs`.
* test(dev): cover pages jsx optimizer scan
* test(dev): strengthen jsx optimizer scan coverage
* fix(dev): apply jsx optimizer config to pages build clients
---------
Co-authored-by: James <james@eli.cx>
* fix(build): parse dynamic request JS files as JSX * fix(build): parse mjs cjs dynamic requests as JSX --------- Co-authored-by: James <james@eli.cx>
* fix(app-router): evict segment prefetches under memory pressure * fix(app-router): hold prefetch queue slots until body read
* fix(link): avoid reusing dynamic app route prefetches * fix(link): preserve static dynamic app prefetches * test(e2e): scope segment cache client params to prod
* fix(actions): run middleware for server action redirect targets
A server action that throws `redirect()` renders the target page inline
and returns its Flight payload with the action response, instead of
making the client re-request the target. Middleware had only run for the
action's own path, so the target's middleware never saw the request: an
action reachable on a public path could redirect to a middleware-gated
page and return that page's server-rendered payload, which the browser
client decodes and commits. Next.js has no such hole because the client
re-requests the target through the full pipeline.
Run the target's middleware against the synthetic GET before rendering
it, after the redirect render's headers context is installed so
`NextResponse.next({ request: { headers } })` overrides reach the page.
Middleware response headers merge into the action response.
Only a clean pass-through is rendered inline. A block, redirect, rewrite,
or status override diverts to the header-only 303 that already exists for
non-App-route targets, which the client re-requests through the full
request pipeline. Apps without middleware, and targets no matcher
matches, are unaffected.
* fix(actions): match redirect targets with request route identity
Follow-up to the target-middleware fix, addressing three ways the target
could still be evaluated as something other than the request the client
would have made.
`matchRoute` decodes pathname segments, so an encoded alias like
`/adm%69n` resolved to the `/admin` page while middleware and a real
navigation both saw `/adm%69n` — inline-rendering a route neither
reached. Match redirect targets with request route identity instead.
`cloneActionRedirectHeaders` carried `x-vinext-mw-ctx` onto the target's
request. In hybrid app+pages dev that header holds the Pages handler's
middleware result for the *action* path, which
`applyForwardedMiddlewareContext` replays in place of executing
middleware for the target. Strip it, which also keeps the internal header
out of the redirect render's `headers()`.
Middleware object matchers support `has`/`missing` header predicates, so
a matcher gated on `Accept` took a different branch against the render
request, which drops `Accept` with the action transport headers. Give
middleware a request that keeps it.
* docs(actions): note the redirect-target middleware divert trade-off
* fix(actions): run target middleware before importing redirect-target modules
A middleware-blocked redirect target still executed its route modules'
top-level code: the target was hydrated (dynamically imported) to decide
renderability before the middleware probe ran, so an unauthorized action
redirect could trigger module side effects, or turn a module-eval throw
into a 500 instead of the header-only fallback. Renderability is now
decided from the manifest's lazy thunks (__loadPage/__loadRouteHandler),
and hydration happens only after middleware passes the target through.
The probe leaked two more behaviors a real navigation would not produce:
- Pass-through middleware response headers merged verbatim onto the 303
action wrapper, so a middleware-set Location gave the wrapper genuine
HTTP-redirect semantics and fetch followed it before the action client
could read x-action-redirect. Middleware header merges onto the wrapper
now strip Location.
- The synthetic target requests were built with bare new Request(), which
drops the Workers cf property, so target middleware keying off
request.cf (geo checks) failed open. Both requests now re-attach cf via
the metadata helper the request clone utilities already used.
* fix(actions): carry middleware cookie mutations onto the redirect target
When action-path middleware rotated or deleted an authentication cookie,
the synthetic redirect-target request was still built from the original
inbound Cookie header, so target middleware evaluated a credential the
response was simultaneously revoking and could render the protected page
inline. The middleware's Set-Cookie mutations now feed the same
request-cookie rebuild as the action's own cookies().set() calls, in
browser order (middleware first, action wins for the same name).
Also aligns two more target-request details with the real pipeline:
- The internal _rsc transport param is stripped from the redirect target
before matching, middleware, and render, as app-rsc-handler does for
navigations, so matchers cannot branch on a query no navigation
carries. The client-facing x-action-redirect header keeps the verbatim
URL; a diverted re-request still goes through real validation.
- The middleware header merge onto the action wrapper now restores all
protocol headers (Content-Type, x-action-redirect and friends) rather
than only stripping Location, so a pass-through middleware can neither
replace the wrapper's destination nor flip the client into treating
the Flight body as non-RSC.
* fix(actions): scope redirect-target cookies and framing to browser behavior
Applying every pending Set-Cookie to the redirect target's Cookie header
ignored the cookie's Path attribute, so a mutation scoped to another
path (admin=1; Path=/account) reached a target like /admin that a real
browser navigation would never send it to, and target middleware could
authorize on it. Mutations now apply only when their Path, or the RFC
6265 default path derived from the action URL, path-matches the target.
Cookies from cookies().set() and draftMode() always carry Path=/ and are
unaffected.
Also adds Content-Length to the wrapper's protected headers: a
pass-through middleware value would misframe the freshly generated
Flight stream and let adapters truncate or reject the action response.
Drops the fixture node_modules symlink an e2e run left staged; on
Windows checkouts with core.symlinks=false it materializes as a plain
file that defeats the Playwright server's link-creation guard. The
gitignore rule loses its trailing slash so it covers symlinks and stops
these from getting staged again.
* fix(actions): use redirect target CSP nonce
* docs(actions): clarify redirect cookie projection
* fix(actions): forward middleware headers to redirect targets
* fix(actions): preserve redirect response framing
* fix(actions): preserve middleware request overrides
* fix(actions): run redirect targets through full request pipeline
* fix(actions): match redirect target header semantics
* fix(actions): mirror Next cookie forwarding
* fix(actions): preserve forwarded cookie ordering
* fix(actions): avoid stale forwarded cookies
---------
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
…ring it (#2733) * fix(app-router): authorize the interception source route before rendering it Interception renders the source route's tree for the request, so one request reaches two routes: the requested target and the claimed interception source. Middleware runs once, for the target's cleanPathname, before the source is known. The source pathname arrives in the `x-vinext-interception-context` client header, so a crafted RSC request naming a middleware-guarded route under the intercepting route causes that route to render and returns its payload, while the guard never sees the path it protects. Applications using middleware path checks as their authorization boundary lose it for any route reachable as an interception source. Run middleware for the claimed source pathname before anything renders from it, and return its response when it denies. The check is skipped when the source resolves to the route already matched for this request, which is also the case where interception does not fire, so ordinary requests and requests without an interception context keep their single middleware run. Only a returned response counts as a denial. A rewritten or normalized pathname means middleware admitted the source and merely routes it elsewhere, which is a routing concern rather than an authorization one. This boundary has no upstream counterpart because the situation does not arise upstream: the generated interception rewrite targets the intercepting route and the client keeps the segments it already holds, so Next.js never renders the source route for this request and has no second route to authorize. It is vinext rendering the source tree that creates the extra boundary. * fix(app-router): preserve action bodies during source authorization Source-route middleware authorization rebuilt its request from the Server Action request directly. That transferred the body stream and left action dispatch unable to read it. Clone the body branch before changing the source URL so middleware and the action retain independent readable streams. * fix(app-router): isolate interception source authorization * fix(app-router): fail closed on source request mutations * fix(app-router): harden interception source authorization * fix(app-router): align interception source identity * fix(app-router): close interception authorization gaps * fix(app-router): preserve matcher and body ownership * fix(app-router): align interception query authorization --------- Co-authored-by: James <james@eli.cx>
* fix(app-router): keep mounted-slot RSC MISS responses no-store finalizeAppPageRscCacheResponse derived "should I rewrite the client headers?" from the return value of scheduleAppPageRscCacheWrite. Those are independent decisions, and #2497 made them disagree: mounted-slot variants now correctly skip the persistent write (their RSC key is slot-blind), but the early return took the pending-dynamic finalization with it. The result is that a fresh ISR-eligible RSC MISS carrying X-Vinext-Mounted-Slots leaves the origin with its initial `s-maxage=..., stale-while-revalidate` instead of being rewritten to `no-store, must-revalidate`. That header is what stops a shared cache from storing a stream that may still reach cookies()/headers() below a Suspense boundary after the cache policy was chosen, so a personalized payload can be stored and replayed for the URL/variant. Apps with named parallel routes send the header on essentially every client navigation; apps without slots never enter the path. Gate the header rewrite on preserveClientResponseHeaders alone, which is already `cacheState !== "MISS"` at the only production call site. This restores the client-facing behavior that shipped before #2497 while keeping its cache-write change, and matches finalizeAppPageHtmlCacheResponse, which never coupled the two. Doing it structurally rather than adding a mountedSlotsHeader term means the next early return added to scheduleAppPageRscCacheWrite cannot silently reintroduce this. * fix(app-router): keep mounted slots out of edge caches * fix(cache): clear mounted-slot CDN overrides * docs(cache): explain mounted-slot no-store scope * fix(cache): clear pending CDN overrides * docs(cache): clarify pending header policy * test(cache): cover dynamic mounted-slot headers * test(cache): cover pending HTML CDN overrides --------- Co-authored-by: James <james@eli.cx>
…2730) * fix(app-router): let concrete Pages routes win middleware rewrites A Pages data request that middleware rewrote returned a synthetic empty JSON body whenever any App route matched the rewrite target, including a dynamic or catch-all match. Every other App-vs-Pages ownership decision in this handler treats a dynamic App match as non-owning, so a concrete Pages route at the same pathname should render instead. Skipping that arbitration meant getServerSideProps never ran for the rewrite target, and the client router, which reuses the middleware probe response when the rewrite target resolves to a Pages route, accepted the empty body as successful page data. Redirect and notFound markers the Pages route would have returned were therefore absent during client-side navigation. Restrict the shortcut to App matches that own the target outright, so dynamic matches fall through to the existing static and dynamic Pages fallback arbitration. That fallthrough reaches the tail Pages data response, which built its headers from the not-found response alone and dropped headers the middleware set on the way. Merge the middleware response headers there so a rewrite landing on a genuinely App-owned dynamic route still carries its cookies. * fix(app-router): preserve middleware headers on Pages fallbacks * fix(app-router): use Pages response merge semantics * test(app-router): cover rewritten Pages data ownership --------- Co-authored-by: James <james@eli.cx>
* fix(app-router): validate external RSC rewrites before proxying Out-of-basePath RSC requests claimed by basePath:false rewrites could reach external destinations before their missing or stale _rsc token was canonicalized. This bypassed cache-busting validation in every rewrite phase. Require every external rewrite call to validate a claimed request before proxy I/O. Regression coverage exercises GET and HEAD in beforeFiles, afterFiles, and fallback and verifies the upstream is never contacted for invalid tokens. * fix(app-router): validate middleware external RSC rewrites * fix(app-router): preserve external RSC proxy state * fix(app-router): preserve canonical RSC redirects * fix(app-router): restore external Flight headers --------- Co-authored-by: James <james@eli.cx>
* fix(app-router): stop flooring dynamic prefetch stale times
Next keeps its two client stale-time dimensions on separate rules. The
cacheLife/router bound goes through `getStaleTimeMs`, which floors at 30s
(segment-cache/cache.ts). The dynamic bound goes through
`computeDynamicStaleAt` (segment-cache/bfcache.ts), which applies no floor
at all, so `staleTimes.dynamic: 0` means a dynamic payload is never reused
across a navigation.
`resolvePrefetchedRscResponseExpiresAt` floored the *combined* value, which
`resolveRscResponseStaleTimeSeconds` had already min-combined. Since
`serverStaleTimeSeconds` floors the cacheLife half before the min, the outer
floor's only live effect was raising a dynamic bound the resolver's own
comment says it must never raise.
Every dynamic render reports its bound: `app-page-render.ts` defaults
`dynamicStaleTimeSeconds` to `experimental.staleTimes.dynamic` (0), emitted
as a header when the render is known dynamic up front and in the completion
footer when it turns dynamic mid-stream. Flooring that to 30s let a
credentialed RSC payload be replayed for 30s after a logout, role change, or
permission revocation, with no server round-trip. The consumed expiry then
propagated into the visited response cache, extending the window past the
navigation.
Floor only the unsignalled fallback, mirroring `STATIC_STALETIME_MS =
getStaleTimeMs(config)`. A signalled bound is now authoritative.
This also removes the `minimumTtlMs` plumbing, a partial workaround that
zeroed the floor for routes with a dynamic *pattern* segment. It keyed on
the wrong axis — a statically-patterned route rendering dynamically
(`/dashboard` reading `cookies()`) never matched — and the correct fix
subsumes it. The one test that relied on it now drives the same assertion
through the header a real dynamic render sends.
* fix(app-router): scope the dynamic bound to automatic prefetches
CI caught that the previous commit applied the dynamic stale-time bound to
`prefetch={true}` as well, breaking segment-cache-metadata's rewrite reuse.
Next splits reuse by prefetch kind, not only by stale-time dimension. In
`getPrefetchEntryCacheStatus` a `full` prefetch stays reusable up to
STATIC_STALETIME_MS even for dynamic content; only `auto` degrades past
DYNAMIC_STALETIME_MS. The segment-cache/bfcache.ts comment states it
directly — dynamic prefetches "use STATIC_STALETIME_MS instead of
DYNAMIC_STALETIME_MS" — and the upstream metadata test says so in prose:
"Because the link is prefetched with prefetch={true}, we should be able to
prefetch the title, even though it's dynamic."
So `prefetch={true}` is an explicit opt-in to holding dynamic content for the
static window. Carry that on the policy as `honorDynamicStaleTime`: true for
`resolveAutoAppRoutePrefetch`, false for `resolveFullAppRoutePrefetch`. A
full prefetch resolves its expiry from cacheLife alone, still floored at 30s
per `getStaleTimeMs`; an automatic one additionally honors the dynamic bound.
This is the axis the removed `minimumTtlMs` was groping for — it keyed on
route-pattern dynamism, which is neither the prefetch kind nor the render
kind.
* fix(app-router): keep the prefetch floor for explicit full prefetches
CI showed the previous commit went too far the other way: dropping the
dynamic bound entirely for `prefetch={true}` stretched those windows from 30s
to the full 300s static TTL, and client-cache's parallel-route reuse tests
started re-issuing full prefetches.
Scope the change to the floor rather than to which bounds apply. An automatic
prefetch takes a dynamic render's bound verbatim — including below the 30s
prefetch floor — so a `0` expires immediately, which is the vulnerability.
`prefetch={true}` still min-combines both bounds but keeps Next's ≥30s
prefetch floor, reproducing the previously-green behavior for full prefetches
exactly.
That confines the behavioral change to automatic prefetches, which is where
the finding lives: default `<Link>` prefetching of a dynamically rendered
route.
* ci: retrigger client-cache e2e (suspected flake)
* chore: drop e2e fixture node_modules symlinks committed by mistake
* chore: retrigger CI
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): preserve consumed prefetch during cache handoff Keep the buffered snapshot discoverable while navigation publishes the committed visited response. This prevents a remounted prefetch-enabled Link from observing a false cache miss and starting a duplicate RSC request. * test(app-router): verify prefetch handoff delay * test(app-router): latch prefetch handoff delay * test(app-router): hold cache latch through remount --------- Co-authored-by: James <james@eli.cx>
App Router hoisting manually serializes beforeInteractive Script props, bypassing React DOM filtering for string-valued event handlers. Request-influenced on* props could therefore become executable inline attributes in the server response.\n\nReject event-handler names case-insensitively at the raw HTML emission boundary while preserving legitimate attributes such as data-onload. The regression test exercises the actual Script capture and hoisted render path. Co-authored-by: James <james@eli.cx>
The App Router payload previously serialized partial render observations and slash-prefixed source page paths into document HTML. Server-only cache tags could leak into the client bootstrap, while crawlers could interpret source page metadata as URLs.\n\nKeep complete render observations at cache finalization, transport source pages as validated segments, and retain legacy source strings only for reading cached payloads during rolling deployments. Next-compatible browser source-page diagnostics remain reconstructible from the segment form.\n\nAdd codec, renderer, fallback, and production HTML regression coverage for the wire contract and the absence of server metadata from documents. Co-authored-by: James <james@eli.cx>
* fix(fetch-cache): honor RequestInit in request dedupe Request inputs with per-call overrides were deduped using the base Request even though fetch executes the overridden headers and options. Distinct authenticated requests could therefore share one response and persist it under the wrong cache key. Normalize the effective GET or HEAD request before deriving the dedupe key so request-scoped reuse and persistent cache storage stay partitioned by the options the network sees. * fix(fetch-cache): preserve effective request semantics * fix(fetch-cache): key inherited request options * fix(fetch-cache): serialize bodies with effective headers * fix(fetch-cache): normalize default request options * fix(fetch-cache): key normalized request body metadata * fix(fetch-cache): distinguish normalized body variants * fix(fetch-cache): hash body bytes without loss * fix(fetch-cache): bound body key fallback work * test(fetch-cache): lock effective auth bypass --------- Co-authored-by: James <james@eli.cx>
* fix(pages): preserve not-found response headers * fix(pages): preserve not-found headers in dev
* fix(pages): preserve gSSP headers on redirects * test(pages): cover data redirect headers
* fix(ci): pin Next.js tracker OpenCode version * fix(ci): align tracker model with Big Bonk
* fix(pages): preserve raw data URLs for middleware * fix(pages): preserve data route path with skipped normalization
* fix(pages): run middleware before image endpoint * fix(pages): preserve image rewrite query
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.0 to 0.6.2. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](zizmorcore/zizmor-action@6599ee8...3dc1ecc) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(middleware): align encoded path matching * test(pages): separate matcher and route identity * fix(middleware): preserve delimiter matcher parity * fix(middleware): preserve trailing source delimiters * test(middleware): align trailing source expectations * fix(middleware): align trailing slash matcher normalization
Bumps [voidzero-dev/setup-vp](https://github.com/voidzero-dev/setup-vp) from 1.15.0 to 1.16.1. - [Release notes](https://github.com/voidzero-dev/setup-vp/releases) - [Commits](voidzero-dev/setup-vp@250f29c...143f5f3) --- updated-dependencies: - dependency-name: voidzero-dev/setup-vp dependency-version: 1.16.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(client): bridge React for Module Federation * fix(ci): register generated React bootstrap entry --------- Co-authored-by: James <james@eli.cx>
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POC for implementing server-function directives entirely in vinext user land instead of adding the
serverFunctionDirectivesoption and orchestration plugin originally proposed in vitejs/vite-plugin-react#1246.This update pins
@vitejs/plugin-rscto the latest #1246 prerelease at50eaf476, which incorporates the merged server-reference registration API from vitejs/vite-plugin-react#1310.vinext initand deploy-time dependency installation also use that exact prerelease because the registry's stable0.5.30package 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-directivesplugin beforersc: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.serverReferencesto:resolve()for plugin-rsc's canonical development and build reference identityreplaceClaim()for references emitted by vinextdeleteClaim()when a directive is removed or another environment does not own the referencevinext:server-function-directivesandrsc:use-serverduring HMRAs a result, vinext no longer needs:
rsc:use-serverserverReferenceMetaMapThe 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 directivestransformHoistInlineDirective()for function-level custom directivestransformDirectiveProxyExport()for SSR/client proxiestransformExpandExportAll()for module-level re-exportsGenerated RSC transforms import
registerServerReferencefrom@vitejs/plugin-rsc/react/rsc/server.Cache replay API
Cached Flight replay continues to use the API merged in vitejs/vite-plugin-react#1289:
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:
max-widthmedia-query syntax #1310's publicserverReferences.resolve(),replaceClaim(), anddeleteClaim()APIspreserveServerReferencescache replay supportgetPluginApi()access toRscPluginManagerIt does not depend on:
serverFunctionDirectivesplugin optionrsc:use-serverValidation
"use cache"→"use server"→"use cache"Refs #1871