mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.2.1
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1500fe61d7 |
fix(pages): align prerender functional parity (#2471)
* fix(pages): align prerender functional parity * fix(pages): harden preview bypass comparison |
||
|
|
47b38a91f3 |
fix(app-router): recover SSR shell render errors via __next_error__ document (#1908)
* fix(plugin): resolve vinext/shims/* package subpaths to local shim files Runtime helper modules embedded into generated entries import vinext's own shims by package subpath (e.g. `vinext/shims/headers`), while source checkouts alias userland `next/*` imports to the local shim files. The two specifiers resolved to different module instances, so request-scoped singleton state (navigation context, headers) split between the source shim copy and the package export copy. The violated invariant is that every shim module must be a per-request singleton regardless of import specifier. Resolve `vinext/shims/*` through the same plugin path as the `next/*` aliases so both forms land on the local shim files. Exercised by the SSR shell-error recovery browser spec, whose fixture imports vinext from the source checkout and depends on shared navigation state across both import forms. * fix(app-router): recover SSR shell render errors via __next_error__ document When the HTML (Fizz) render rejects during SSR, vinext re-rendered a server-side global-error page whose flight payload encodes the error tree. For an app without a custom global-error.tsx that meant the default error card with no path back to the real page: an SSR-phase-only throw (e.g. a client component using the "throw to opt out of server rendering" pattern) left the browser stuck on the card even though the client render would succeed. The violated expectation is Next.js's shell-error semantics: a failed HTML shell is served as the default `__next_error__` error document carrying the ORIGINAL flight payload and the bootstrap module, and the browser re-renders the real tree from that payload with createRoot instead of hydrating. Local error.tsx boundaries still win — they ship inside the flight payload and catch the re-thrown error client-side. handleSsr now resolves to that recovery document instead of rejecting, but only when both hold: - the error did not originate in the RSC render (no string `digest`), so flight errors, redirect()/notFound(), and server-component throws keep driving the existing rejection-based boundary machinery, and - the app has no custom global-error.tsx (the generated entry knows at build time and threads hasCustomGlobalError through dispatch/render options); apps with one keep the server-rendered boundary re-render. The browser entry switches from hydrateRoot to createRoot when the document root carries id="__next_error__", dropping the error-shell styles first. Covered by the new ssr-error-shell-recovery browser spec (recovery to real content, local error.tsx for SSR-only and unconditional client throws) and the existing tests/nextjs-compat/global-error.test.ts boundary-semantics suite. * refactor(review): address PR 1908 non-blocking notes - Add static prerender no-boundary recovery regression test to ssr-error-shell-recovery.browser.spec.ts - Document broad __next_error__ browser marker in app-browser-entry.ts - Extract stripJsExtension to utils/path.ts and wire into all shim resolution sites (vinext/shims/* + react-server shims) Non-functional: targeted regression coverage + code documentation + minor resolver hardening per reviewer feedback. * refactor(review): clarify shell recovery assumptions * fix(ssr): cancel abandoned prerender streams * refactor(ssr): clarify error shell root options * fix(isr): recover shell errors during regeneration * fix(ssr): scope client recovery to marked shells * fix(plugin): explicitly filter vinext shim subpaths * fix(plugin): retain null-prefixed shim resolution * test(nextjs-compat): cover no-boundary shell recovery fallback An unconditional client throw during SSR shell recovery without a local error boundary must still land on the default global-error card. Without a regression test, a future change could tear down the recovery shell and leave a blank document.\n\nAdd a production browser case that exercises the no-boundary route and asserts the default error UI after the client re-render throws again. * fix(app-router): preserve shell recovery error semantics * test(app-router): harden shell recovery cache semantics * fix(app-router): mark global error responses uncacheable * test(app-router): align error response status expectations * fix(build): preserve null-prefixed og resolution * fix(cache): delegate cache header cleanup to adapters --------- Co-authored-by: James <james@eli.cx> |
||
|
|
48e932e615 |
feat(cache): split CDN and data cache adapters; add Cloudflare edge adapter (#1693)
* feat(cache): split CDN and data cache adapters; add Cloudflare edge adapter Separate two caching concerns behind distinct adapters: - Data cache handler (existing CacheHandler): fetch, "use cache", unstable_cache. Canonical get/setDataCacheHandler; get/setCacheHandler kept as deprecated aliases. - CDN cache adapter (new): page-level ISR serving strategy — readPage/writePage, buildResponseHeaders (header map), ownsBackgroundRevalidation, revalidate. DefaultCdnCacheAdapter delegates storage to the data cache, so default behavior is unchanged. Page/route ISR now routes through the CDN adapter (isr-cache, app-page-cache); revalidateTag/revalidatePath/updateTag invalidate the data cache and purge the CDN adapter (default no-op). Add CloudflareCdnCacheAdapter (edge-managed): readPage null / writePage no-op, ownsBackgroundRevalidation false, emits CDN-Cache-Control for SWR + Cache-Control: no-store (no browser storage) + Cache-Tag, and purges via the request-context cache. Auto-selected via a global detector when the request context exposes a cache handle; explicit setCdnCacheAdapter wins. The request-context type stays generic (cache: unknown). Adds next.config cdnCacheHandler (symmetric with cacheHandler) and updates the worker codegen to setDataCacheHandler. * address review: drop config plumbing; refine Cloudflare cache headers - Remove the cdnCacheHandler next.config plumbing entirely (deferred); next-config.ts is back to baseline. - CloudflareCdnCacheAdapter: emit the edge directive on CDN-Cache-Control as 'public, max-age=…, stale-while-revalidate=…' (max-age, not s-maxage, so the edge caches + SWRs), and set the browser-facing Cache-Control to 'public, max-age=0, must-revalidate' so a browser never serves a stored copy without revalidating against the edge. * chore: fix formatting (vp check) for cache adapter files * feat(cache): route App Router route handlers + Pages Router ISR through the CDN adapter Closes the two parity gaps from review: route-handler and Pages Router ISR responses now emit the CDN adapter's headers (CDN-Cache-Control + Cache-Tag on edge adapters) instead of a hardcoded Cache-Control. - Add shared applyCdnResponseHeaders() in cache-control.ts; app-page-cache now uses it (drops its local helper). - Route handlers: applyRouteHandlerRevalidateHeader (fresh) and buildRouteHandlerCachedResponse (HIT/STALE) go through the adapter; routeTags hoisted in execution so the fresh response carries Cache-Tag. - Pages Router: fresh ISR response emits a path-based Cache-Tag (matching revalidatePath); HIT/STALE served response routes through the adapter. - CloudflareCdnCacheAdapter: guard so non-cacheable policies (no-store/no-cache/private) are never promoted to CDN-Cache-Control. Default behavior unchanged (adapter yields a single identical Cache-Control). * address review: simplify applyCdnResponseHeaders + strip CDN headers from stored route values - applyCdnResponseHeaders now clears only Cache-Control (the header vinext stamps internally); the adapter's own headers are applied via set() which overrides, so pre-clearing adapter-specific headers was redundant and presumptuous (per review). - buildAppRouteCacheValue strips cdn-cache-control / cache-tag so CDN policy headers are never baked into a stored route value (re-derived from the adapter on every served response). - pages-page-data buildPagesCacheResponse now uses applyCdnResponseHeaders (Headers) for consistency with every other call site. * revert presumptuous CDN header strip in buildAppRouteCacheValue Hardcoding cdn-cache-control/cache-tag in the store denylist presumes a specific adapter's header names (the same presumption rejected for applyCdnResponseHeaders) and is also unnecessary: CDN policy headers never reach a stored route value — the edge adapter's writePage is a no-op and the default adapter never emits them. Back to the original denylist. * example(workers-cache): add demo app for route-cache testing (#1695) * example(workers-cache): add demo app for route-cache testing Adds the examples/workers-cache-cloudflare demo app from feat/route-cache-request-context so the route-cache CDN adapter changes on this branch can be tested. * ci: trigger PR workflows on any base branch, not just main Drop the `branches: [main]` filter from the pull_request trigger in ci.yml, deploy-examples.yml, and preview-release.yml so these workflows run on PRs against any base branch (e.g. stacked PRs). * update lockfile * ci(deploy-examples): add workers-cache-cloudflare to deploy matrix Pull in the deploy matrix + preview-URL comment entry from feat/route-cache-request-context so the workers-cache demo app gets built, deployed, and linked on PR previews. * fix(cache): auto-select edge CDN adapter from request context, no import needed The edge-managed CDN cache adapter was only activated when a detector got registered as a side effect of importing `vinext/cloudflare`. Apps with a hand-written worker entry (e.g. the workers-cache demo) never imported it, so ISR silently fell back to the origin-managed default — emitting plain `Cache-Control` instead of `CDN-Cache-Control` / `Cache-Tag`. The adapter is platform-agnostic (it only touches the generic request-context cache surface), so move it into core as `RequestContextCdnCacheAdapter` and have `getCdnCacheAdapter()` select it directly. Resolution is now: 1. explicit `setCdnCacheAdapter()` (always wins) 2. request-context cache present (`ctx.cache`) -> edge adapter 3. otherwise -> origin-managed default Drops the detector-registry indirection and the import/registration requirement. `CloudflareCdnCacheAdapter` is kept as a re-export alias for backwards compatibility. * fix(cache): select Cloudflare edge adapter from resolver, keep it in the cloudflare module Previous commit moved the adapter into core — revert that. The CloudflareCdnCacheAdapter stays in cloudflare/cloudflare-cdn-cache.ts; the core resolver imports it and instantiates it as the built-in default when the request context exposes a host cache (ctx.cache). Drops the detector-registry side-effect-import requirement; resolution is explicit -> ctx.cache edge adapter -> origin-managed default. * example(workers-cache): show CDN-Cache-Control in the probe headers * example(workers-cache): rename example app from workers-cache-cloudflare to workers-cache Rename the example directory and update its package name, wrangler worker name, the deploy-examples matrix + preview-URL list, and the lockfile. * fix(cache): give bare stale-while-revalidate an explicit window for the CF edge The framework emits a value-less `stale-while-revalidate` (Vercel's unbounded extension). Cloudflare follows RFC 5861 and ignores the bare directive, so the edge had no stale window — entries hard-expired at max-age and the next request was a MISS instead of UPDATING. Normalize bare SWR to an explicit 1-year window in the edge adapter's toEdgeCacheControl so Cloudflare actually serves stale while revalidating. * use link component * fix(cache): let the CDN adapter own the default Cache-Control when none is set Rendered responses that produced no cacheable policy (e.g. dynamic App Router pages) were going out with no Cache-Control at all, bypassing the CDN adapter — so on the edge Cloudflare applied its own default caching heuristic instead of the adapter's policy. Add a guard in finalizeAppRscResponse (the single App Router egress, already run for every page/route-handler/metadata/not-found response) that, when no Cache-Control is present, routes through the adapter to supply the default: the edge adapter emits no-store (never accidentally edge-cache an unspecified response), the default adapter leaves it absent (unchanged). Runs only when the header is absent, so it never clobbers a policy a renderer already applied (incl. CDN-Cache-Control). Also stop applyCdnResponseHeaders from stamping an empty Cache-Control value. * refactor(cdn-cache): align adapter method names with data cache + gate ctx.cache auto-detection Address PR #1693 review feedback: - Align CdnCacheAdapter field naming with the data cache adapter (CacheHandler): readPage->get, writePage->set, revalidate->revalidateTag. buildResponseHeaders / ownsBackgroundRevalidation stay CDN-specific (no CacheHandler equivalent). Updated both implementations and all call sites. - Gate ctx.cache auto-detection behind VINEXT_CDN_CACHE_AUTO_DETECT (default off) so edge-managed page ISR is opt-in until deployment skew protection is figured out. Removed the dedicated _edgeAdapter variable; the resolved edge adapter is now stored on the single active-adapter global slot that setCdnCacheAdapter uses. Enable the flag for the workers-cache demo via wrangler.jsonc vars. * test(cdn-cache): update tests for renamed adapter API + flag-gated auto-detect Align the CDN adapter unit tests with the refactor: - get/set/revalidateTag method names (was readPage/writePage/revalidate) - bare stale-while-revalidate now normalized to an explicit window - auto-detection is gated behind VINEXT_CDN_CACHE_AUTO_DETECT (no detector / no vinext/cloudflare side-effect import) * chore: reconcile pnpm-lock.yaml after merge The merge auto-resolved pnpm-lock.yaml into a broken state (missing @vitejs/plugin-rsc entry), so `vp install` failed at CI setup. Regenerated with pnpm install --no-frozen-lockfile; frozen install now passes. |
||
|
|
937f5d6a09 |
fix(app-router): read indefinite app page cache entries (#1129)
* fix(app-router): read indefinite app page cache entries App pages with revalidate=false currently normalize to Infinity, but the app-page cache read gate excludes Infinity. Pre-rendered static app pages can be seeded into the ISR cache and still miss that cache on every production request. The cache read policy now treats positive Infinity as read-eligible while preserving force-dynamic, production, RSC, and nonce guards. Cached indefinite responses are normalized to the same static one-year Cache-Control header used by fresh revalidate=false responses, so cache hits do not emit s-maxage=Infinity. Tests cover the dispatch cache hit path and the shared cached Cache-Control helper. * chore: rerun ci * chore: rerun ci |
||
|
|
f45fce00d5 |
fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses. Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules. Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known. * fix(isr): address cache life review follow-ups * chore: address latest review follow-ups * ci: pin vite-plus setup version * Revert "ci: pin vite-plus setup version" This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1. * fix: avoid blocking ISR page streams on cache metadata * fix: preserve headers for speculative cacheLife probes * fix: preserve prerender cacheLife metadata * fix: preserve legacy ISR cache metadata behavior * fix: preserve prerender seed revalidate context * fix: harden app page cache policy metadata * fix: resolve app router prerender conflict |