mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
48e932e615
* 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.
269 lines
4.3 KiB
CSS
269 lines
4.3 KiB
CSS
:root {
|
|
color-scheme: light dark;
|
|
--bg: #0b1020;
|
|
--panel: #111733;
|
|
--panel-2: #161c3d;
|
|
--text: #e7ecff;
|
|
--muted: #93a0c8;
|
|
--accent: #8b5cf6;
|
|
--accent-2: #22d3ee;
|
|
--good: #34d399;
|
|
--warn: #f59e0b;
|
|
--bad: #f43f5e;
|
|
--border: rgba(255, 255, 255, 0.08);
|
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background: radial-gradient(1200px 600px at 80% -10%, rgba(139, 92, 246, 0.18), transparent),
|
|
radial-gradient(800px 400px at -10% 10%, rgba(34, 211, 238, 0.12), transparent), var(--bg);
|
|
color: var(--text);
|
|
}
|
|
|
|
a {
|
|
color: var(--accent-2);
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
main {
|
|
max-width: 980px;
|
|
margin: 0 auto;
|
|
padding: 48px 24px 96px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.25rem;
|
|
margin: 0 0 8px;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
h2 {
|
|
margin-top: 32px;
|
|
font-size: 1.25rem;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.tagline {
|
|
color: var(--muted);
|
|
margin: 0 0 32px;
|
|
max-width: 70ch;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.card {
|
|
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
min-height: 140px;
|
|
}
|
|
|
|
.card h3 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.card p {
|
|
color: var(--muted);
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.card a {
|
|
margin-top: auto;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
font-size: 0.7rem;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
font-weight: 600;
|
|
background: rgba(139, 92, 246, 0.2);
|
|
color: #d6c4ff;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.badge-hit {
|
|
background: rgba(52, 211, 153, 0.18);
|
|
color: #aaf3d4;
|
|
}
|
|
|
|
.badge-miss {
|
|
background: rgba(244, 63, 94, 0.18);
|
|
color: #ffb5c2;
|
|
}
|
|
|
|
.badge-stale {
|
|
background: rgba(245, 158, 11, 0.18);
|
|
color: #ffd692;
|
|
}
|
|
|
|
.kv {
|
|
display: grid;
|
|
grid-template-columns: max-content 1fr;
|
|
gap: 4px 16px;
|
|
margin: 12px 0 0;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.kv dt {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.kv dd {
|
|
margin: 0;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.controls button,
|
|
.controls a.button {
|
|
appearance: none;
|
|
border: 1px solid var(--border);
|
|
background: rgba(255, 255, 255, 0.04);
|
|
color: var(--text);
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: transform 0.05s ease, background 0.15s ease;
|
|
}
|
|
|
|
.controls button:hover,
|
|
.controls a.button:hover {
|
|
background: rgba(139, 92, 246, 0.2);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.controls button:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.status {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
margin-top: 8px;
|
|
min-height: 1.2em;
|
|
}
|
|
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 20px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
code {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
padding: 1px 6px;
|
|
border-radius: 6px;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.ssr-banner {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding: 14px 16px;
|
|
border-radius: 12px;
|
|
margin: 16px 0;
|
|
border: 1px solid var(--border);
|
|
position: relative;
|
|
}
|
|
|
|
.ssr-banner::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 4px;
|
|
border-radius: 12px 0 0 12px;
|
|
}
|
|
|
|
.ssr-banner strong {
|
|
font-size: 0.95rem;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.ssr-banner span {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.ssr-banner-ran {
|
|
background: rgba(245, 158, 11, 0.08);
|
|
}
|
|
|
|
.ssr-banner-ran::before {
|
|
background: var(--warn);
|
|
}
|
|
|
|
.ssr-banner-cached {
|
|
background: rgba(52, 211, 153, 0.08);
|
|
}
|
|
|
|
.ssr-banner-cached::before {
|
|
background: var(--good);
|
|
}
|
|
|
|
.ssr-banner-unknown {
|
|
background: rgba(147, 160, 200, 0.06);
|
|
}
|
|
|
|
.ssr-banner-unknown::before {
|
|
background: var(--muted);
|
|
}
|
|
|
|
.timestamp {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 1rem;
|
|
background: rgba(0, 0, 0, 0.25);
|
|
border-radius: 8px;
|
|
padding: 10px 14px;
|
|
display: inline-block;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
nav.crumbs {
|
|
margin-bottom: 24px;
|
|
color: var(--muted);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
nav.crumbs a {
|
|
color: var(--muted);
|
|
}
|