* test(app-router): reproduce deployed optimistic search navigation stall
Add a production-shaped provider filter that combines useOptimistic with same-path search navigation and streamed server results. Cover local controls and run a bounded provider cycle against the deployed Workers example, where the intermittent commit failure occurs.\n\nRefs #2865.
* test(app-router): reproduce local optimistic navigation stall
* fix(app-router): settle optimistic search navigations
* fix(app-router): preserve streaming during optimistic recovery
---------
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@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.
* ci: address zizmor security audit findings
Run `zizmor .` and address all findings:
- Pin actions to commit SHAs with version comments (unpinned-uses).
Dependabot picks up the version comment for future bumps.
- Move `contents: write` / `id-token: write` to the publish job in
publish.yml; set `permissions: {}` at the workflow level
(excessive-permissions).
- Add `permissions: {}` to deploy-examples.yml workflow + scope
smoke-test job to `contents: read` (excessive-permissions).
- Move `${{ ... }}` expansions inside run blocks into `env:` vars
in publish.yml / benchmarks.yml / nextjs-deploy-suite.yml
(template-injection).
- Set `persist-credentials: false` on all checkouts that don't push;
publish.yml needs them for `git push origin v$VERSION`, so it
carries an inline `zizmor: ignore[artipacked]` instead.
- Keep dependabot's 1-day cooldown but annotate with `zizmor: ignore`
(intentional fast cooldown).
`zizmor .` is now clean.
* ci: add zizmor GitHub Actions workflow
Run zizmor on every push to main and every PR via the upstream
`zizmorcore/zizmor-action`. Findings surface in the GitHub Code
Scanning UI (Security → Code scanning alerts).
* ci(publish): explain zizmor artipacked ignore
* chore: harden CI supply chain
- Pin voidzero-dev/setup-vp to commit SHA across all workflows
- Pin create-next-app and pkg-pr-new to exact versions
* pull setup-vp into setup action
---------
Co-authored-by: James <james@eli.cx>
* chore: bump pnpm to 10.32.1 and enable vitest agent reporter
* chore: upgrade GitHub Actions to Node 24 compatible versions
* perf: re-land unit/integration split and add CI sharding
- Restore unit/integration vitest project split (lost in vp migration)
- vitest 4 supports fileParallelism per-project (was global-only in v3),
so unit tests now run in parallel (~11s) while integration stays serial
- Add 3-shard matrix for integration tests in CI, cutting wall time from
~4.5min to ~1.5min
- Move kv-cache-handler to integration to avoid flakiness under parallelism
* chore: remove unused deploy-preview slash command workflow
* ci: add sentinel job for stable branch protection rule
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* refactor(ci): extract composite actions and improve workflow hygiene
- Add composite actions: setup-node-pnpm, build-vinext, deploy-example, comment-preview-urls
- Cache Playwright browsers in CI to avoid ~150MB download per E2E run
- Add concurrency group to deploy-examples.yml to prevent deploy pileup
- Replace inline CJS node script in create-next-app job with portable bash
- Convert ecosystem-run.yml inline Node scripts from CJS require() to ESM
- Apply composite actions across all workflows to eliminate repeated boilerplate
* refactor(ci): revert to inline steps, keep only setup-node-pnpm composite action
Remove the build-vinext, deploy-example, and comment-preview-urls composite
actions and inline their steps back into each workflow. Keep setup-node-pnpm
as the one reusable action (pnpm install + node setup). Retain all other
improvements from the previous commit: concurrency groups, Playwright browser
cache, bash-based dev-server check, ESM node scripts in ecosystem-run.
* refactor(ci): use setup-node-pnpm composite action consistently
Every workflow that does pnpm/node setup + install now uses the
setup-node-pnpm composite action instead of the inline 3-step triplet.
Added optional registry-url input to support publish.yml's npm auth.
Exceptions (intentionally kept inline):
- bonk.yml / bigbonk.yml: issue_comment trigger, GHAS constraint
- tip.yml notify-on-failure: node-only, no pnpm install needed
* Skip deploy previews for fork PRs that lack Cloudflare secrets
Fork PRs don't have access to repository secrets (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID), so deploy/smoke-test/comment steps always fail.
Add a fork detection condition to skip these steps gracefully. The build
steps still run, so example builds are still validated for fork PRs.
* Run safe CI for external contributors, add /deploy-preview slash command
Switch ci.yml from pull_request to pull_request_target so lint, typecheck,
vitest, and e2e run automatically for fork PRs without needing approval.
No secrets are used, so this is safe with untrusted code.
Add a /deploy-preview slash command workflow that lets maintainers trigger
deploy previews on fork PRs. Gated by author_association (org members,
collaborators, repo owners only).
Cloudflare employees who push branches to the main repo continue to get
automatic deploy previews via the existing deploy-examples.yml workflow.
* Revert ci.yml to pull_request, skip deploy-examples for fork PRs
Simpler approach: keep ci.yml on pull_request (first-time contributor
approval is fine, avoids cache poisoning concern with pull_request_target).
The actual fix: add a job-level if condition to deploy-examples.yml so the
entire workflow is skipped for fork PRs. This prevents the noisy
failed/skipped deploy checks on external contributor PRs.
Update docs in AGENTS.md and README.md to reflect the approach.
* Skip deploy previews for fork PRs that lack Cloudflare secrets
Fork PRs don't have access to repository secrets (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID), so deploy/smoke-test/comment steps always fail.
Add a fork detection condition to skip these steps gracefully. The build
steps still run, so example builds are still validated for fork PRs.
* Add AI-generated release summary + Google Chat notification to publish workflow