* refactor(cache): extract Cloudflare cache adapters into @vinext/cloudflare
Move the Cloudflare KV data cache and edge CDN cache adapters out of
vinext into a new publishable @vinext/cloudflare package:
- cache/kv-data-adapter(.runtime).ts (KVCacheHandler, kvDataAdapter)
- cache/cdn-adapter(.runtime).ts (CloudflareCdnCacheAdapter, cdnAdapter)
tpr.ts stays in vinext. vinext now depends on @vinext/cloudflare
(workspace:*) and the package declares vinext as a peer dep; both build
from source via tsconfig paths so there is no build-order cycle. The
vinext/cloudflare barrel still re-exports KVCacheHandler for back-compat.
Wires up tsconfig paths, a vitest source alias, root build/postinstall,
and the preview/publish workflows for the new package. Updates internal
consumers (apps/web, examples/workers-cache), docs, and tests.
* ci(create-next-app): install @vinext/cloudflare from local tarball
vinext now depends on @vinext/cloudflare, which isn't published to npm
yet. The create-next-app smoke test packs vinext locally and resolves
its deps from the registry, so the install (and dev server) failed with
ERR_PNPM_FETCH_404 for @vinext/cloudflare.
Pack @vinext/cloudflare alongside vinext and add a pnpm override in the
scaffolded project pointing at the local tarball so the dependency
resolves offline.
* refactor(cloudflare): address review feedback
- Remove the root barrel export from @vinext/cloudflare; expose only the
./cache/* subpaths via a wildcard export (no root main/types).
- vinext/cloudflare re-exports KVCacheHandler from the full subpath.
- Drop the redundant .npmignore (the package.json "files" allowlist
already restricts the publish to dist).
- Remove the unsupported imperative setCacheHandler/KVCacheHandler usage
from both READMEs; the cache plugin config is the supported approach.
- Simplify test wiring: drop the now-unused @vinext/cloudflare tsconfig
path and dedupe the vitest source alias into a shared constant.
* chore(cloudflare): drop unused vite devDependency
The @vinext/cloudflare config uses vite-plus and nothing imports vite, so
the vite devDependency was unused. build/check/knip stay green without it.
* Apply suggestion from @james-elicx
* feat(cache): configure cache adapters from vite plugin config
Add a `cache` option to the vinext() plugin so CDN and data cache
adapters can be declared in vite.config instead of calling
setDataCacheHandler() / setCdnCacheAdapter() from a worker entry:
vinext({
cache: {
cdn: { adapter: require.resolve('vinext/cloudflare/cache/cdn-adapter') },
data: { adapter: require.resolve('vinext/cloudflare/cache/kv-data-adapter') },
},
})
Each slot points at an adapter module whose default export is a factory
(DataCacheAdapterFactory / CdnCacheAdapterFactory). The plugin generates
a virtual:vinext-cache-adapters module that the App Router worker entry
calls per request (self-guarded, once per isolate), passing the host env
so binding-backed adapters (e.g. KV) can read their namespace.
Ships ready-made Cloudflare adapter entry points:
- vinext/cloudflare/cache/kv-data-adapter (KVCacheHandler)
- vinext/cloudflare/cache/cdn-adapter (CloudflareCdnCacheAdapter)
* feat(cache): add typed adapter builders (kvDataAdapter/cdnAdapter)
Instead of `{ adapter: require.resolve(...) }`, each adapter module now
also exports a config-time builder from the same path:
import { cdnAdapter } from 'vinext/cloudflare/cache/cdn-adapter';
import { kvDataAdapter } from 'vinext/cloudflare/cache/kv-data-adapter';
vinext({ cache: { cdn: cdnAdapter(), data: kvDataAdapter({ binding: 'MY_KV' }) } })
A builder returns a plain, serializable { adapter, options } descriptor —
it never touches the Workers runtime, so nothing throws at config / build
/ dev time when bindings aren't available. Descriptor `options` (e.g. the
KV binding name) are inlined into the generated registration module and
forwarded to the factory's { env, options } context, where the binding is
resolved lazily on the first request.
- shims/cache-adapter: descriptors + options-aware factory/context types
- kv-data-adapter: kvDataAdapter() builder + configurable binding/appPrefix/ttl
- cdn-adapter: cdnAdapter() builder
- raw { adapter, options } path form still supported
* test(cache): verify absolute (require.resolve) local adapter path bundles
Real Cloudflare build pointing cache.data at a local adapter file by
absolute path (what require.resolve('./adapter') yields). Proves the
generated registration module resolves the absolute import, bundles the
local adapter into the worker, and does not need any Workers context at
build time.
* refactor(cache): builder require.resolve + register across all routers/runtimes
Addresses review feedback:
* Move adapters into their own runtime modules instead of re-exporting.
Each adapter is now a builder module (kv-data-adapter.ts / cdn-adapter.ts)
plus a sibling *.runtime.ts holding the default-export factory. Type
definitions have a single home in shims/cache-adapter.ts (dropped the
re-export shim; index.ts imports the config type from there).
* The exposed builder utility resolves the relative runtime path internally
via import.meta.resolve (the ESM require.resolve), so the descriptor carries
an absolute path to the runtime factory rather than a bare specifier — the
example is just kvDataAdapter({ binding }), no require.resolve at the call site.
* Register configured cache handlers EVERYWHERE, not just the App Router worker:
- App Router: the generated RSC entry passes registerConfiguredCacheAdapters
into createAppRscHandler, which calls it per request — covering Workers,
the Node server, and dev through the one shared handler.
- Pages Router: the generated server entry registers in renderPage and
handleApiRoute (Node/dev), and the generated worker registers with env
(Workers, for KV bindings).
Registration self-guards (first call with real env wins) and is now resilient:
a factory that throws on an incompatible runtime is logged and skipped, so the
default handler stays in place instead of failing every request.
Tests: generator-level assertions that every router/runtime entry wires
registration, plus the existing builder/codegen/factory and full-build coverage.
vp check clean; app-router (339) and pages-router (272) suites pass.
* refactor(cache): keep all Cloudflare adapter code under cloudflare/
The adapter factory contract lived in shims/cache-adapter.ts (outside
cloudflare/), and the Cloudflare adapters reached out to it. Move the
contract into cloudflare/cache/adapter.ts so every Cloudflare-specific
cache adapter file is self-contained under cloudflare/ — importing only
cloudflare-local modules and the core CacheHandler/CdnCacheAdapter
interfaces it implements.
The plugin's config schema (CacheAdapterDescriptor / VinextCacheConfig)
is genuinely framework-level (it's the vinext() `cache` option), so it
moves into the codegen module the plugin already owns; index.ts imports
it from there. Builders return a structural { adapter, options } so they
don't import the descriptor type either. Deletes shims/cache-adapter.ts.
* refactor(cache): merge KV/CDN classes into the runtime adapter files
All Cloudflare cache code now lives in one directory, cloudflare/cache/,
and each runtime file holds both the implementation class and its
config-driven factory (no separate class module to reach for):
- kv-cache-handler.ts -> cache/kv-data-adapter.runtime.ts
(KVCacheHandler + ENTRY_PREFIX + createKvDataCacheAdapter default export)
- cloudflare-cdn-cache.ts -> cache/cdn-adapter.runtime.ts
(CloudflareCdnCacheAdapter + createCloudflareCdnCacheAdapter default export)
Updated importers: cloudflare/index.ts re-exports the classes from the
runtime files, tpr.ts pulls ENTRY_PREFIX from there, shims/cdn-cache.ts
imports the edge adapter from there, and the tests follow the moved paths.
git mv preserves history.
vp check clean; cache/kv/cdn/app-route/tpr/shims suites pass (1300+ tests).
* chore(cache): trim low-value comments added in this branch
Remove narrating/redundant comments that just restated the code; keep
the non-obvious why (registration ordering/resilience, import.meta.resolve
rationale, edge cache-control semantics). No code changes.
* review: address PR #1733 feedback
- Make registerCacheAdapters a required field on the RSC handler options
(the generated entry already passes it; test factory updated).
- Remove the separate cloudflare/cache/adapter.ts contract file; inline the
factory param types directly into the two runtime adapters.
- Drop the CloudflareCdnCacheAdapter re-export from cloudflare/index.ts.
- Fold the virtual:vinext-cache-adapters declaration into global.d.ts and
delete the standalone .d.ts.
- Remove the ./cloudflare/cache/* package.json export for now; README uses a
local-adapter require.resolve example with a note that the built-in adapter
export paths are pending.
- Rename the config-driven KV default binding to VINEXT_KV_CACHE (imperative
deploy/tpr path keeps VINEXT_CACHE — flagged on the thread).
* refactor(cache): align KV binding name to VINEXT_KV_CACHE everywhere
Rename the KV cache binding from VINEXT_CACHE to VINEXT_KV_CACHE across the
whole codebase so the config-driven adapter, the imperative deploy-generated
worker, TPR's wrangler detection, and the apps/web example all agree. The
unrelated X-Vinext-Cache response-header constant (VINEXT_CACHE_HEADER) is
untouched.
* tidy
* .
* .
* .
* .
* .
* Move apps/web cache to plugin config
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
* 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.