85 Commits

Author SHA1 Message Date
James Anderson 8eccc20163 feat(cache): support self-contained response store (#3246) 2026-09-14 10:32:46 +01:00
James Anderson 71b7c82692 chore(cache): polish workers response store package (#3245) 2026-09-14 10:32:45 +01:00
James Anderson 601e098d60 perf(cache): reduce response store Durable Object load (#3213)
* perf(cache): reduce response store Durable Object load

* refactor(cache): remove response store compatibility shims

* fix(cache): batch response store purge parameters

* fix(cache): preserve coalesced response store writes

* fix(cache): preserve concurrent response store writes

* fix(cache): fence response store tag purges

* fix(cache): order tag purge fences durably

* fix(cache): migrate response store metadata schema

* fix(cache): preserve coalesced purge semantics

* fix(cache): isolate coalesced cleanup failures

* fix(cache): harden coalesced response store writes

* fix(cache): isolate coalesced purge failures

* fix(cache): isolate post-commit alarm failures

* fix(cache): continue cleanup after alarm errors

* fix(cache): fence expired response store cleanup

* fix(cache): retain durable cleanup ownership

* fix(cache): retain cleanup fences across deletion
2026-09-14 10:32:45 +01:00
James Anderson 2c9eaf59a1 feat(cache): stream response-store cache misses (#3200) 2026-09-14 10:32:44 +01:00
James Anderson e32eb6a3d7 feat(cache): add Workers Response Store POC (#3192) 2026-09-14 10:32:43 +01:00
James Anderson 4e24e15651 docs(cloudflare): explain cached response entrypoint (#3154)
* docs(cloudflare): explain cached response entrypoints

* docs(cloudflare): distinguish private cache headers

* docs(cloudflare): clarify private gateway egress
2026-09-07 21:48:29 +01:00
James Anderson e8eaec9f39 fix(cloudflare): prewarm routed response stages (#3153)
* fix(cloudflare): prewarm routed response stages

* test(cloudflare): own prewarm middleware fixtures

* test(cloudflare): cover config-personalized cache hits

* test(cloudflare): expose edge cache status mirrors

* test(cloudflare): cover cached Pages data isolation
2026-09-07 21:48:28 +01:00
James Anderson 35344dd0b3 test(cloudflare): cover late-dynamic HTML isolation (#3159) 2026-09-02 14:35:15 +01:00
James Anderson 8370369383 fix(cloudflare): finalize CDN version metadata output (#3137)
* fix(cloudflare): finalize CDN version metadata output

* test(cloudflare): validate generated version metadata deployment

* fix(cloudflare): finalize metadata from primary output
2026-09-02 12:55:40 +01:00
James Anderson c8bea6bccf feat(cache): classify and warm static Route Handlers (#3113)
* feat(cache): classify and warm static Route Handlers

* fix(cache): identify Route Handler probe responses

* test(cache): assert Route Handler probe identity

* fix(cache): prove final Route Handler responses

* fix(cache): admit explicit Route Handler policies safely

* fix(cache): close Route Handler admission fallbacks

* fix(cache): honor explicit dynamic handler policies

* fix(cache): reject unsafe Route Handler probe responses

* fix(cache): preserve Route Handler CDN policy ownership
2026-09-01 21:25:26 +01:00
James Anderson 56b6be06d6 fix(cloudflare): discover prewarm paths from staged worker (#3090)
* fix(cloudflare): discover prewarm paths from staged worker

* fix(cloudflare): hide prerender capability from user code

* perf(cloudflare): skip discovery URL parsing on ordinary requests

* fix(cloudflare): bound staged discovery phases

* perf(pages): lazy-load staged discovery runtime

* fix(cloudflare): use the full staged discovery deadline

* fix(cloudflare): distinguish staged discovery failures

* test(cloudflare): preserve deadline-bounded discovery

* test(cloudflare): avoid wall-clock retry count

* test(cloudflare): stabilize discovery deadline coverage
2026-09-01 21:25:22 +01:00
James Anderson 2a2ce31ecc fix(app-router): support soft navigation in static exports (#3112)
* fix(app-router): support soft navigation in static exports

* test(stream): drain transforms before releasing readers

* fix(cache): preserve prerender stale metadata across footer race

* fix(app-router): validate static export navigation payloads

* fix(app-router): handle static export basePath roots

* fix(export): align basePath artifact namespace

* fix(export): namespace basePath static artifacts

* fix(export): normalize trailing basePath artifacts

* fix(export): namespace public assets under basePath

* test(export): avoid tainted fixture HTML
2026-08-27 11:01:04 +01:00
James Anderson c1a78a73d8 fix(build): support trailing-slash static exports (#3081)
* fix(build): support trailing-slash static exports

* fix(ci): initialize static export preview routes

* fix(build): emit trailing-slash 404 fallback

* docs(build): clarify static export page count
2026-08-26 23:01:09 +01:00
James Anderson 4146d69021 fix(cloudflare): verify Worker version IDs during CDN warmup (#3072)
* fix(cloudflare): revalidate stale warmup retries

* fix(cloudflare): report CDN warmup retry progress

* test(cloudflare): retry prewarm verification on fresh Worker

* feat(cloudflare): configure CDN readiness probes

* fix(cloudflare): validate CDN warmup version overrides

* fix(cloudflare): verify CDN warmup version ids

* test(cloudflare): opt into dangerous warmup fallback

* fix(cloudflare): harden warmup version validation

* docs(cloudflare): clarify version binding environments
2026-08-24 23:03:15 +01:00
James Anderson e2e2b177eb fix(cloudflare): complete warmup response and promotion contracts (#3046)
* test(cloudflare): prove deployed Pages CDN warmup

* fix(cloudflare): reject incomplete no-promote warmup

* fix(cloudflare): keep warm discovery fail-closed

* test(cloudflare): prove prewarmed App HTML reuse

* fix(cloudflare): enforce no-promote warmup contract

* fix(cloudflare): warm terminal ISR responses
2026-08-24 23:03:14 +01:00
James Anderson 369e031883 test(cloudflare): preserve independent RSC warm proof (#3020)
* test(cloudflare): preserve independent RSC warm proof

* test(cloudflare): allow cache propagation after promotion
2026-08-24 23:03:13 +01:00
James Anderson 8bb09481d6 feat(cloudflare): prewarm canonical ISR RSC requests (#3002)
* feat(cloudflare): prewarm canonical ISR RSC requests

* fix(cloudflare): forward preview deploy options

* fix(cloudflare): harden canonical RSC prewarming

* fix(test): wait for preview alias propagation

* fix(cloudflare): guarantee exact RSC prewarm reuse

* fix(cloudflare): harden canonical RSC prewarming

* fix(cloudflare): preserve definitive RSC warm identity

* fix(cloudflare): warm immutable worker versions

* test(form): expect Next-compatible absolute navigation

* fix(cloudflare): harden RSC prewarm deployment

* test(cloudflare): stabilize deployed RSC proof

* test(cloudflare): await RSC prefetch completion

* test(cloudflare): isolate RSC readiness probes

* fix(cloudflare): warm RSC through version overrides

* fix(cloudflare): parse labeled Worker version IDs

* fix(cloudflare): warm document requests as HTML

* fix(cloudflare): retry staged version propagation

* test(cloudflare): seed prewarm proof with prior Worker

* fix(cloudflare): await version override propagation

* fix(cloudflare): serialize staged cache warming

* fix(cloudflare): verify warmed RSC build identity

* fix(cloudflare): harden staged RSC cache warming

* fix(cache): keep RSC prewarm identity build-scoped

* fix(cloudflare): gate warmup on uploaded build

* fix(cache): certify canonical RSC warmup responses

* fix(cache): harden canonical RSC warmup propagation

* fix(cache): close RSC warmup review gaps

* fix(cache): preserve explicit response cache policies

* feat(cloudflare): prewarm canonical RSC variants without prerendering

* fix(cloudflare): forward deploy config from CLI

* fix(cache): close RSC prewarm reuse gaps

* fix(cloudflare): parse labeled Worker version IDs

* fix(router): preserve canonical prefetch invalidation

* test(cloudflare): align RSC seed Worker bindings

* fix(cloudflare): scope warmup deadlines per request

* test(cloudflare): avoid caching stale warmup seed

* fix(cloudflare): fail permanent warmup errors promptly

* fix(router): keep contextual RSC requests hashed

* test(cloudflare): await promoted warmup Worker

* test(cloudflare): retry browser warmup source

* revert: restore focused warmup deadline fix

* fix(cloudflare): remove warmup propagation deadline

* fix(cloudflare): allow slower warmup responses

* fix(cloudflare): confirm warmed cache hits

* fix(cloudflare): confirm cache hits without backoff

* fix(cloudflare): avoid duplicate cache warm requests

* test(cloudflare): allow prewarm propagation

* test(cloudflare): isolate loading prewarm propagation

* test(cloudflare): await promoted prewarm worker

* fix(cloudflare): wait for warm cache propagation before promotion

* test(cloudflare): allow promoted warm cache to settle

* test(cloudflare): wait after promoted build is reachable

* test(cloudflare): poll deployed version before warm cache checks

* test(cloudflare): trace warm cache identity

* test(cloudflare): trace prewarm verification location

* test(cloudflare): compare warm cache clients

* test(cloudflare): trace bypassed response identity

* fix(cloudflare): serialize warm cache variants

* test(cloudflare): report prewarm cache miss context

* test(cloudflare): await target RSC deployment readiness

* test(cloudflare): await stable worker promotion

* test(cloudflare): isolate RSC prewarm target

* fix(cloudflare): defer CDN warmup retries

* fix(cloudflare): verify warmed RSC variants

* test(cloudflare): trace RSC warm cache paths

* fix(cloudflare): separate loading-shell warm cache key

* test(cloudflare): make RSC warm reuse proof deterministic

* test(cloudflare): tolerate stale promoted Worker routing

* fix(rsc): enforce definitive loading-shell cache key
2026-08-24 23:03:12 +01:00
James Anderson 1c05615159 fix(build): preserve portable CommonJS module identity (#2826)
* fix(pages): preserve CJS globals in bundled dependencies

* fix(pages): use portable emitted CJS module identity

* test(build): expect emitted proxy module identity

* fix(build): support linked CommonJS package identity

* fix(build): harden linked CommonJS transforms

* fix(build): harden emitted CommonJS identity

* test(pages): account for fixture route table growth

* refactor(plugins): reuse shared module utilities

* test(cloudflare): cover Pages bundled CJS globals

* test(cloudflare): tolerate unmounted dev source identity

* fix(build): preserve nested Cloudflare module identity

* refactor(build): resolve deployed module filenames explicitly
2026-08-18 02:35:44 +01:00
James Anderson efa916579b fix(app-router): settle optimistic search navigations (#2952)
* 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>
2026-08-17 14:23:57 +01:00
James Anderson c296ad34db fix(app-router): retain shared layouts across loading shells (#2940)
* test(app-router): reproduce shared layout teardown on soft navigation

Add a `/layout-identity` fixture to the app-router-cloudflare example and
e2e coverage showing that a shared `[slug]` layout is discarded during a
soft navigation that only changes segments below it.

The fixture pairs the shared layout with a `loading.tsx` boundary above
it, a parallel slot beside `children`, and a nested dynamic segment with
its own layout, matching the shape a real app has. Removing the
`loading.tsx` boundary makes every assertion pass, which isolates it as
the trigger.

The five layout-identity assertions fail on main; the soft-navigation
control passes, so the teardown is the router discarding a layout it
should have retained rather than a full document load.

* fix(app-router): retain shared layouts across loading shells

* fix(app-router): preserve omitted slot segment identity

* test(app-router): follow shared param canonicalizer
2026-08-17 11:28:26 +01:00
James Anderson c2362842c9 fix(build): support deployed web worker graphs (#2314)
* fix(build): support deployed web worker graphs

* fix(build): address worker parity review findings

* fix(build): preserve web worker URL origins

* test(e2e): cover deployed web workers

* test(e2e): port upstream Cloudflare worker suite

* fix(build): preserve worker asset semantics

* fix(build): tolerate minimal resolved config mocks

* test(worker): classify Vite-equivalent coverage
2026-08-16 01:06:42 +01:00
James Anderson cf689f2e43 fix(pages): run middleware before image endpoint (#2776)
* fix(pages): run middleware before image endpoint

* fix(pages): preserve image rewrite query
2026-08-03 18:33:41 +00:00
James Anderson 2d08de067d fix(pages): preserve raw data URLs for middleware (#2775)
* fix(pages): preserve raw data URLs for middleware

* fix(pages): preserve data route path with skipped normalization
2026-08-03 18:26:09 +00:00
James Anderson 3deb467b21 fix(pages): preserve gSSP headers on redirects (#2771)
* fix(pages): preserve gSSP headers on redirects

* test(pages): cover data redirect headers
2026-08-03 15:43:57 +00:00
James Anderson 948a14a7e6 fix(examples): declare complex purge secret (#2774) 2026-08-03 15:23:57 +01:00
James Anderson 350865c466 fix(create-vinext-app): generate next env on first run (#2768)
* fix(create-vinext-app): generate next env on first run

* fix(types): keep next env renderer internal
2026-07-31 01:00:56 +01:00
Boy Steven f517288f40 fix(server): reject unsupported static asset methods (#2714)
* fix(server): reject unsupported static asset methods

* fix(server): route public asset mutations to 405

* chore: retrigger CI

* fix(server): preserve static method parity across runtimes

* fix(server): discard static probe responses safely

* fix(server): sanitize static method responses

* fix(server): preserve static method status

* fix(server): strip stale static range headers

* fix(server): preserve static rewrite ordering

* fix(dev): cache public file routes

* fix(dev): harden cached public route handling

* fix(worker): avoid unnecessary asset probes

* fix(server): preserve static asset lookup coverage

* fix(build): keep public path encoder internal

---------

Co-authored-by: James <james@eli.cx>
2026-07-28 13:59:20 +01:00
James Anderson 1c69ce3fef fix(pages-router): load custom _app/_document via resolved file paths in dev (#2692)
* fix(pages-router): load custom _app/_document via resolved file paths in dev

The dev server imported pages/_app and pages/_document by extensionless id
and relied on the module runner applying custom resolve.extensions (e.g.
".page.tsx" from pageExtensions). The runner no longer resolves those, so
apps using pageExtensions silently lost their custom App/Document (the load
failure is swallowed). Import the file path resolved by findFileWithExts
instead, at all three call sites.

* test(e2e): add pages-router-complex example app and behaviour suite

A deliberately convoluted Pages Router app that serves as a compatibility
target: the kinds of patterns that only surface in large, long-lived
enterprise pages-router codebases, each pinned by a Playwright spec whose
behaviour is verified against real Next.js (73/73 under next dev --webpack).

Highlights: custom pageExtensions everywhere (middleware.page.ts,
instrumentation.page.ts), app-shell getInitialProps with a TTL-memoised
chrome fetch and embedded-shell/draft bypasses, class _document with its own
GIP, a zone route dimension driven by middleware rewrites and a real
i18next/react-i18next runtime, catch-all routes with static-sibling
precedence and cacheable-404/redirect hygiene, record-driven template
branching on a dynamic/static/dynamic route, an urql-style server-snapshot
data layer, shallow routing + router.events + next/compat/router +
next/navigation hooks in pages, next/image with a custom loader, draft-mode
gateway APIs, a function-form next.config, and a Cloudflare setup matching
the vinext init scaffold.

The playwright project (pages-router-complex, port 4199) is intentionally
not in the CI e2e matrix: vinext currently passes 59/73, and the failures
are the compat backlog documented in the example README.

* fix(pages-router): cover compound page extensions in CI
2026-07-24 15:35:16 +01:00
James Anderson 71dbb45ca8 fix(pages): isolate on-demand revalidation requests (#2495)
* fix(pages): pin revalidate loopback origin

* test(pages): cover production revalidation origin

* fix(pages): preserve internal revalidation boundaries

* fix(pages): preserve on-demand revalidation boundaries

* fix(pages): dispatch Worker revalidation internally

* fix(pages): authenticate revalidation request context

* fix(pages): isolate revalidation transport headers

* test(pages): account for generated route table size

* fix(pages): align on-demand revalidation semantics

* fix(pages): align revalidation response cache parity

* fix(pages): preserve regenerated ISR representations

* fix(pages): align ISR cache representations

* fix(pages): preserve canonical ISR representations

* fix(pages): align cached response parity

* fix(pages): preserve custom App render props

* fix(pages): preserve optional App page props

* fix(pages): preserve optional App props on the client

* fix(pages): normalize client App page props

* fix(pages): preserve App data merge semantics

* fix(pages): keep redirect status helper internal

* fix(pages): match Next.js terminal ISR behavior

* fix(pages): preserve custom app error envelopes

* fix(pages): match Next.js dev revalidation semantics

* test(pages): align dev revalidation parity coverage

* chore(pages): remove stale cache helper exports

* fix(cache): preserve explicit no-store context
2026-07-20 18:45:06 +01:00
James Anderson 4f7eec967e fix(router): avoid repeated App path decoding (#2556)
* fix(router): avoid repeated App path decoding

* test(router): align encoded middleware pathname

* fix(router): match Next encoded path semantics

* fix(prerender): accept canonical encoded app params

* fix(router): preserve encoded interception params

* test(build): tolerate watch output replacement

* test(router): cover repeatedly encoded production paths

* fix(router): preserve action request route identity

* refactor(middleware): remove stale pathname argument

* refactor(router): align fallback interception paths

* refactor(router): derive request path identity from value

* fix(router): preserve encoded route parity

* fix(router): preserve raw config path matching

* fix(router): preserve raw encoded route identity

* fix(router): canonicalize encoded dot segments

* fix(router): restore encoded path parity after main merge

* fix(app-router): exact-match generated params

* fix(app-router): chain generated parent params

* fix(app-router): preserve generated param ownership

* fix(app-router): preserve per-result param ownership

* fix(app-router): preserve params after empty parallel generation

* fix(app-router): chain parallel static params by branch
2026-07-14 22:09:29 +01:00
James Anderson b859a03bbd feat(types): ship Next-compatible types without Next.js (#2612)
* feat(types): add vendored Next.js declarations

* feat(types): expose fallback types through vinext

* fix(types): support readonly Node environment declarations

* fix(ci): install vendored types before vinext tarballs

* test(types): avoid brittle packed consumer pins

* fix(ci): configure pnpm 11 local type overrides

* test(types): avoid registry metadata in packed install

* test(types): isolate packed declaration install

* feat(init): add standard script aliases for new apps

* fix(create-vinext-app): run typegen portably

* fix(typegen): report next env updates

* fix(create-vinext-app): use standard scripts only

* refactor(create-vinext-app): use exhaustive package manager return

* fix(create-vinext-app): fall back for unknown package managers
2026-07-14 13:27:20 +01:00
James Anderson eba70463d4 feat(cloudflare): warm prerendered paths before deploy (#2481)
* feat(cloudflare): warm prerendered paths before deploy

* fix(cloudflare): warm production CDN for uploaded versions

* fix(cloudflare): resolve Worker names for CDN warmup

* fix(cloudflare): harden CDN warmup diagnostics

* fix(cloudflare): harden CDN warmup deploy flow

* fix(cloudflare): clarify CDN warmup trigger failures

* feat(cloudflare): warm cdn from build path manifest

* fix(cloudflare): emit warm paths for skip-build deploys

* fix(cloudflare): avoid stale skip-build path manifests

* test(cloudflare): lock skip-build prerender manifest gate

* fix(cloudflare): warm fallback deploy URL from triggers

* fix(cloudflare): clarify CDN warmup deploy logs

* feat(init): offer CDN warmup deploy script

* update wrangler configs

* fix(init): default CDN warmup to yes

* fix(cloudflare): explain first warm deploy failure

* fix(cloudflare): keep deploy errors concise

* fix(init): make generated home page cacheable
2026-07-04 16:09:55 +00:00
James Anderson 5825c6807f fix(app-router): preserve client state during action revalidation (#2517)
* fix(app-router): preserve client state during action revalidation

* test(app-router): pin loading boundary to slot entry

* test(app-router): deploy action revalidation repro
2026-07-04 11:08:54 +01:00
James Anderson a3910e7aa9 feat(cloudflare): add unified worker entry (#2416)
* feat(cloudflare): add default pages router worker entry

* feat(cloudflare): add unified worker entry

* chore(web): use unified worker entry

* chore(examples): use built-in workers-cache entry

* chore(examples): keep hackernews on app router entry

* chore(cloudflare): rename unified entry to fetch handler

* chore(cloudflare): keep pages entry source helper test-local
2026-06-29 17:33:51 +00:00
James Anderson 0a48f0f7fb feat(cloudflare): move deploy command to cloudflare package (#2405)
* feat(cloudflare): move deploy command to cloudflare package

* fix(cloudflare): expose deploy cli bin
2026-06-29 16:47:28 +00:00
James Anderson 89036154cc fix(pages-router): unify client asset bootstrap (#2378)
* fix(pages-router): hydrate worker-backed dev pages

* refactor(pages-router): unify client asset bootstrap

* fix(app): preinitialize bootstrap dependencies

* test(app): distinguish preinit scripts from bootstrap

* fix(app): root-anchor bootstrap preinit modules

* test(app): cover bootstrap preinitialization in browsers

* fix(app): share client assets with nested SSR modules

* test(app): verify bootstrap preinit CSP nonces

* test(app): serve CSP nonce in worker fixture

* fix(test): read CSP script nonce property

* fix(cloudflare): emit client assets beside final workers

* fix(build): resolve pages client assets from source

* fix(app): register client assets in ssr graph

* fix(pages-router): harden client bootstrap resolution

* fix(build): emit client assets for node outputs

* fix(build): package client assets in standalone output

* test(app): reference worker nonce fixture explicitly

* fix(build): preserve client assets across environments

* refactor(build): unify client asset outputs

* fix(build): stabilize custom client asset outputs

* fix(build): finalize client assets after app builds

* fix(build): reuse client assets in hybrid builds

* fix(build): scope hybrid client assets metadata

* test(e2e): colocate Cloudflare worker fixture
2026-06-26 19:48:26 +01:00
James Anderson 25a3c26f7a feat(init): scaffold for cloudflare and node (#2279)
* feat(init): select deployment platform

* refactor(init): AST-update deployment config

* feat(init): configure Cloudflare cache and images

* fix(init): avoid backtracking in config updates

* fix(init): cover packaged Cloudflare setup

* ci: allow Cloudflare init smoke builds

* ci: fix packaged Cloudflare dependency install

* test(e2e): isolate browser fixture servers

* chore: allow Windows taskkill binary

* fix(init): configure Cloudflare image adapter

* fix(init): address platform setup review

* fix(init): address cache setup review

* fix(init): hide workers cache option

* fix(init): align generated config indentation

* fix(init): validate cloudflare setup before mutation

* fix(init): preserve additive config syntax

* fix(init): tidy interactive prompt spacing

* fix(images): rename Cloudflare optimizer builder

* refactor(images): remove draft name migration

* fix(init): install dependencies after setup

* fix(init): clarify setup recovery steps

* fix(init): ignore Wrangler state for Cloudflare

* fix lockfile
2026-06-26 17:09:19 +01:00
James Anderson b4843d75c1 feat(images): configure image optimization via vinext({ images }) adapter (#1873)
* feat(images): configure image optimization via vinext({ images }) adapter

Move server-side image optimization from a hand-wired custom worker entry to a
declarative `vinext({ images: { optimizer } })` option, mirroring the cache
adapter pattern. The default entries now handle `/_next/image` through a
registered optimizer, so no custom worker is required, and the same config
works across all targets — optimizing on Cloudflare, gracefully serving images
unoptimized on Node/dev where the binding is unavailable (like KV cache
degrading to in-memory).

- add an ImageOptimizer registry (set/getImageOptimizer +
  handleConfiguredImageOptimization) in server/image-optimization.ts
- generate `virtual:vinext-image-adapters` (registerConfiguredImageOptimizer)
  from the new `images` plugin option
- add @vinext/cloudflare/image/image-adapter: imageAdapter() builder + runtime
  factory reading the env.IMAGES binding
- handle /_next/image in the default app-router entry and the generated Pages
  worker via the registry; inline next.config `images` (allowed widths +
  security headers) into the RSC entry
- vinext deploy points App Router `main` at vinext/server/app-router-entry
  (no generated worker) and prints a hint to enable the optimizer

next.config `images` (remotePatterns, deviceSizes, dangerouslyAllowSVG, etc.)
continues to drive the standard Next.js options; the vite-config
`images.optimizer` only selects the runtime transform backend.

* fix(images): cover deploy image-hint helpers and preserve optimizer this-binding

The Check CI job failed because knip flagged viteConfigHasImageAdapter and
formatImageOptimizationHint as unused exports — they were only called inside
deploy.ts. Cover both with unit tests in tests/deploy.test.ts (mirroring the
existing viteConfigHasCacheAdapter / formatMissingCacheAdapterError suites),
which also closes the coverage gap for the new deploy hint path.

Also wrap the registered optimizer's transformImage in
handleConfiguredImageOptimization instead of detaching the method, so an
optimizer implemented as a class instance keeps its this binding.

* fix(images): honor configured deviceSizes/imageSizes on the App Router Node prod server

Review follow-up (ask-bonk):

- The App Router prod server (vinext start) validated /_next/image widths
  against the hardcoded Next.js defaults, rejecting valid optimizer URLs with
  400 when the app configures custom images.deviceSizes/imageSizes — while the
  Cloudflare worker entry and the Pages prod path already honored them. Read
  the __imageAllowedWidths constant inlined into the RSC entry (falling back to
  the defaults for older builds), matching how __assetPrefix/__basePath are read.
- Lock in the this-binding behavior of handleConfiguredImageOptimization with a
  class-instance optimizer test.

* fix(images): pass an explicit empty allowed-widths config through on vinext start

Review follow-up (ask-bonk, awareness note): the old-build fallback guard
conflated a missing __imageAllowedWidths export with an explicit empty
deviceSizes/imageSizes config, mapping the latter to the Next.js defaults on
the Node App Router path while the Cloudflare worker passes the empty array
straight through. Only fall back to the defaults when the export is absent.

* refactor(images): read App Router image config from the RSC entry, retire the JSON sidecar

Review follow-up (ask-bonk): the App Router had two parallel build-time sources
for next.config images security/header settings — the __imageConfig constant
inlined into the RSC entry (read by the Cloudflare worker entry) and the
image-config.json sidecar written by the vinext:image-config plugin (read by
vinext start). Unify on the RSC entry export: prod-server now reads
rscModule.__imageConfig, keeping image-config.json only as a read-side fallback
for dist outputs built by older vinext versions, and the sidecar writer plugin
is removed.

* fix(deploy): keep wrangler main on a user-authored worker entry for App Router

Review follow-up (ask-bonk): an App Router app with a custom worker/index.ts
but no wrangler.jsonc would have had its custom worker silently dropped —
generateWranglerConfig unconditionally pointed main at the default
vinext/server/app-router-entry. Respect hasWorkerEntry so a user-authored
worker keeps winning for both routers, with a regression test.

* fix(images): expose Cloudflare optimizer under images path

* fix(deploy): install Cloudflare image adapter package

* fix(examples): declare Cloudflare image adapter package

* test(images): update App Router image config codegen assertions

* fix(examples): configure image optimizer adapters
2026-06-23 20:20:36 +01:00
James Anderson 6e998ec114 fix(middleware): run on missing build assets (#2242)
* fix(middleware): run on missing build assets

* fix(middleware): preserve missing asset rewrites on workers
2026-06-22 23:36:49 +01:00
James Anderson 656a381de9 fix(og): constrain inlined assets to trusted roots (#2172)
* fix(og): constrain inlined assets to trusted roots

* fix(og): canonicalize asset package boundaries

* test(og): cover canonical boundary intersections

* fix(og): support file-symlinked dependency modules

* docs(og): clarify logical asset boundary resolution

* fix(og): pin symlinked modules to matching packages

* fix(og): keep linked package boundaries narrow

* fix(og): reject workspace-root asset boundaries

* fix(og): name-pin external package boundaries

* fix(og): validate linked package ownership

* fix(og): track linked package ownership

* fix(og): harden linked package provenance

* fix(og): confine captured alias packages

* fix(og): pin captured directory aliases

* fix(og): require alias package ownership

* fix(og): support single-segment aliases

* perf(og): preserve alias lookup precedence

* refactor(og): extract asset ownership policy

* test(og): cover linked package fonts in e2e

* fix(og): recognize resolved package aliases

* fix(og): support regex package aliases

* test(og): isolate linked font fixture

* docs(test): clarify linked font coverage
2026-06-19 16:08:46 +01:00
Nathan Nguyen a3d2f92152 fix(router): honor hybrid pages route priority (#1997)
* fix(router): honor hybrid pages route priority

* fix(router): share hybrid owner decision with client navigation

PR #1997 fixed the server-side route ownership for direct document loads,
but the same invariant broke for client-side soft navigations and the
matching prefetch path:

- navigateClientSide() always delegated to the App runtime's RSC fetch,
  even when the Pages route had higher priority. renderPagesFallback()
  short-circuits RSC requests with null, so the App catch-all won.
- prefetchUrl() prefetched an RSC stream for any URL that matched an App
  route, again ignoring Pages ownership.
- The Pages entry was loaded on every hybrid request, even when a static
  App route had already matched (Pages can never win in that case).

Expose the Pages route manifest on the client via a new
__VINEXT_PAGES_LINK_PREFETCH_ROUTES__ window global emitted by both the
App and Pages browser entries. The link shim consults a shared
resolveHybridClientRouteOwner helper that mirrors the server-side
pagesRouteHasPriorityOverAppRoute comparison. When Pages owns the URL,
the click handler issues a window.location navigation and the prefetch
path returns early.

Gate the renderPagesFallback call behind a static-App-route check in
handleAppRscRequest: when a static App route matches, the bridge
cannot win, so skip the eager Pages entry load. Centralise the
comparison in a new resolveHybridRouteOwner helper so server and
client reach the same answer for the same (URL, route pair).

Adds the missing client-navigation coverage to the use-params e2e
fixture (Link from /app/ to /pages-dir/foobar) and unit tests for
the shared owner decision.

* fix(router): mirror server hybrid owner decision on the client

PR review flagged two split-brain bugs in the previous hybrid
ownership fix: a hand-copied client comparator and a Link-only
ownership gate that left programmatic App Router navigations on
the wrong path.

The hand-copied routePrecedence in hybrid-client-route-owner.ts
omitted the static-prefix reduction that lives in
routing/utils.ts#routePrecedence, and used a strict-less-than
comparison that returned App for identical dynamic patterns. The
server returns Pages for both cases (Pages providers sort ahead
of App providers, and routePrecedence subtracts 50 per static
prefix segment). The split produced a real ownership disagreement
on overlapping patterns like /_sites/:slug* (Pages) vs /:slug*
(App).

Add a shared compareHybridRoutePatterns to routing/utils.ts as
the single source of truth: the static/dynamic short-circuits plus
a sortRoutes call (which carries the static-prefix reduction and
the Pages-first equal-pattern tiebreak). The server
pagesRouteHasPriorityOverAppRoute and the client
resolveHybridClientRouteOwner both delegate to it, so they
cannot diverge. Drop the hand-copied routePrecedence entirely.

Wire the ownership check at the App navigation runtime boundary
so useRouter().push, useRouter().replace, gesturePush, and form
submits all get the same hard-nav contract as the Link click
handler. Specifically:

- navigateClientSide: after same-origin normalization, if Pages
  owns the URL, hard-navigate via window.location and return
  (matching the existing external-URL branch).
- _appRouter.prefetch: short-circuit RSC URL construction for
  Pages-owned targets so we do not warm an unusable cache entry.

Centralise the existing two inline hard-nav branches in
navigateClientSide into a hardNavigateTo helper for clarity.

Tests:

- Direct unit tests for compareHybridRoutePatterns covering
  identical-dynamic tiebreak, static-prefix dynamic overlap,
  static-prefix catch-all overlap, and infix-static bonus.
- Direct unit tests for resolveHybridClientRouteOwner mirroring
  the server assertions plus a basePath-stripping test.
- e2e: useRouter().push('/pages-dir/foobar') from an App page
  resolves to the Pages document.
- e2e: useRouter().prefetch('/pages-dir/foobar') issues zero
  RSC requests for the target URL.

* docs(router): fix stale score in static-prefix catch-all test comment

The hand-copied comparator note quoted the dynamic-segment scores
(51 / 1000) for the optional-catch-all example (/_sites/:slug*
vs /:slug*). The current optional-catch-all scoring actually
produces 1951 vs 2000. The test assertion was correct; only the
comment was stale.

* test(use-params): fix direct-load single dynamic param to use /a instead of /a/b

* fix(router): compare hybrid routes structurally

* fix(router): reject hybrid route conflicts

* fix(router): refresh hybrid route ownership

* fix(router): preserve hybrid routing lifecycle

* fix(router): recheck pages routes after rewrites

* fix(router): preserve rewritten pages queries

* fix(router): preserve rewritten route ownership

* fix(router): resolve client rewrites sequentially

* fix(router): apply rewrite phases sequentially

* fix(router): preserve rewrite params and endpoints

* fix(router): hand off endpoint navigations

* fix(router): preserve rewrite fragment params

* test(e2e): avoid hybrid fixture route conflicts

* test(pages): await async config validation

* test(hybrid): avoid duplicate page fixtures

* chore(router): clarify hybrid priority semantics

* refactor(router): remove duplicate app route matcher

* docs(router): clarify client hybrid comparator

---------

Co-authored-by: James <james@eli.cx>
2026-06-15 00:22:00 +01:00
James Anderson 2e83e7f558 fix(edge-wasm): handle *.wasm?module imports in non-Cloudflare builds (#1877)
* fix(edge-wasm): handle \`*.wasm?module\` imports in non-Cloudflare builds (#1351)

In plain Node.js builds (deploy-suite / standalone `vinext start`),
Rolldown has no built-in handler for the `?module` query on `.wasm`
imports, so middleware and edge API routes that use:

  import wasm from './add.wasm?module'

would throw at bundle time.

Root cause: the `?module` query is a Cloudflare Workers / workerd
convention handled by `@cloudflare/vite-plugin`'s
`additionalModulesPlugin` (enforce:"pre"). When that plugin is absent
(e.g. after `vinext init` which emits a plain `vite.config.ts` with
only `vinext()`), no plugin resolves the query.

Fix: add a new `vinext:wasm-module-import` Vite plugin (enforce:"pre")
that, in the absence of @cloudflare/vite-plugin:

1. Intercepts `*.wasm?module` imports in `resolveId`.
2. Reads the WASM binary at load time and inlines it as base64.
3. Exports `await WebAssembly.compile(buffer)` via a virtual module —
   valid in Node.js (top-level await in ESM).

The `hasCloudflarePlugin` guard is checked inside the hook handler
(not as an array-spread condition) so it reflects the value set by
the `config` hook after plugin registration.

workerd forbids compiling WASM from bytes at runtime, so this path
intentionally never runs when @cloudflare/vite-plugin is present.

Fixes #1351.

* fix(wasm-module-import): suppress no-control-regex lint on virtual module regexes

Replace bare null-byte with unicode-escape in the two filter/replace regexes
and add oxlint-disable-next-line comments following the same convention used
in client-reference-dedup.ts. Fixes the Check job failure.

* fix(wasm-module-import): exclude @vercel/og from plugin scope, add resolveId tests

- Add importer guard in resolveId to skip imports originating from
  @vercel/og (resvg.wasm, yoga.wasm): vinext:og-font-patch converts
  those to dynamic imports with a .catch() disk-read fallback; if
  wasm-module-import intercepts them, the Node.js fallback never runs,
  the ~1.3 MB resvg WASM ships twice, and findEmittedWasmAsset dedup
  in vinext:og-assets breaks. Guard mirrors the pattern in the existing
  isVinextOgShimImporter() helper.
- Remove dead `return null` after `this.error()` (which throws); use
  `bytes!` non-null assertion to satisfy TypeScript.
- Add resolveId test suite: happy path, ?module query stripping, null
  resolver, og-collision guard (plain + virtual-prefix importer), and
  non-og pass-through.

Fixes ask-bonk findings on PR #1877.

* refactor(wasm-module-import): drop non-null assertion, document TLA and filter intent

* test(wasm-module-import): add build-level coverage, document runtime-gating assumption

* refactor(wasm-module-import): extract to plugins/, guard client environment, watch wasm file in dev

* fix(wasm-module-import): delegate target-owned modules

* refactor(wasm-module-import): share query stripping
2026-06-11 12:06:39 +00:00
James Anderson de33c3e5a0 fix(build): inline ../-relative font assets in OG routes (#1866)
* fix(build): inline ../-relative font assets in OG routes

The vinext:og-inline-fetch-assets plugin only matched paths starting
with "./" (e.g. "./font.ttf"), but Next.js test fixtures for OG custom
fonts use "../"-relative paths like "../../../assets/typewr__.ttf".

Without the inline, at runtime:
- Cloudflare Workers: import.meta.url is "worker" (not a URL), so
  new URL("../../../assets/...", import.meta.url) throws TypeError.
- Node.js: fetch() does not support file:// URLs, so the inlined
  file:// URL produced by the import-meta-url plugin fails.

Fix: relax the regex from `\.\/[^"']+` to `\.[^"']+` so any
dot-relative path (./x, ../x, ../../x, etc.) is inlined as base64.
Apply the same fix to the readFileSync(fileURLToPath(...)) pattern.

Failing tests addressed:
- test/e2e/og-routes-custom-font: should render og with custom font
  for app routes (edge runtime fetch + Node.js fs.promises.readFile)
- test/e2e/app-dir/metadata-font: should handle custom fonts in both
  edge and nodejs runtime

* fix(build): inline OG font assets when a formatter adds a trailing comma

The vinext:og-inline-fetch-assets regex only matched the single-line,
comma-less form of `fetch(new URL(...)).then((res) => res.arrayBuffer())`.
When a formatter (Prettier `trailingComma: "all"`, oxfmt) wraps the call
across lines it appends a trailing comma:

    .then((res) =>
      res.arrayBuffer(),
    )

which no longer matched, so the font was left as a runtime fetch. On
Cloudflare Workers `import.meta.url` is "worker", so
`new URL("../...", "worker")` throws "TypeError: Invalid URL" and the OG
route returns 500 — even though the ../-relative path itself was already
supported. Relax both `.then(...)` alternatives with an optional `,?`.

Add real e2e coverage (`/api/og-custom-font`) mirroring Next.js'
og-routes-custom-font fixture: an edge OG route that loads
assets/noto-sans.ttf via
`fetch(new URL("../../../assets/noto-sans.ttf", import.meta.url))`. It
runs in tests/e2e/og-image.spec.ts across the app-router (Node dev),
cloudflare-dev and cloudflare-workers (workerd) projects, with the route
and font added to both the app-basic fixture and the app-router-cloudflare
example. Also adds a unit test for the formatted/trailing-comma shape in
tests/og-inline.test.ts.

* fix(build): inline OG font assets with a semicolon-terminated block body

Follow-up from the /bigbonk review on PR #1866: the block-body alternative
of the og-inline-fetch-assets regex ended `…\.arrayBuffer\(\)\s*\}?\s*,?\s*\)`
with no allowance for a `;` before `}`, so formatter output such as

    .then((res) => {
      return res.arrayBuffer();
    })
    .then(function (res) { return res.arrayBuffer(); })

was left as a runtime fetch — which throws "Invalid URL" on Workers
(import.meta.url === "worker"). Add `;?` before the block-body `}` and unit
tests for the arrow- and function-expression block-body forms.
2026-06-09 15:07:49 +01:00
James Anderson e40870e114 fix(og): lazy-load @vercel/og to keep it out of the main worker entry (#1774)
* fix(og): lazy-load @vercel/og to code-split it out of the worker entry

The next/og shim statically imported @vercel/og, so a top-level
`import { ImageResponse } from "next/og"` inlined ~800 KB of satori + resvg +
wasm/fonts into the always-loaded server entry. Import it via dynamic import()
inside the async stream callback so it is always a separate chunk.

app-router-cloudflare dist/server/index.js: ~1.67 MB -> ~875 KB.

* chore: drop changeset

* fix(og): copy resvg.wasm from split chunk + drop example workaround

Address review feedback on the lazy @vercel/og change:

- vinext:og-assets scanned only index.js for the resvg.wasm reference to
  decide whether to copy the Node.js disk-read fallback asset. Now that
  @vercel/og is code-split into its own chunk, that reference is no longer in
  index.js, so the copy was skipped (breaking the Node target's OG fallback).
  Scan all emitted chunks via the writeBundle bundle arg instead.
- app-router-playground used a manual `await import("next/og")` workaround
  that is no longer needed; switch it to the idiomatic static import.
2026-06-05 23:23:12 +01:00
James Anderson 8d00797ada feat(cache): extract Cloudflare cache adapters into @vinext/cloudflare (#1748)
* 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
2026-06-05 14:57:20 +01:00
James Anderson f0f6aa72e0 feat(cache): configure cache adapters from vite plugin config (#1733)
* 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>
2026-06-04 23:30:33 +00:00
James Anderson 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.
2026-06-04 12:09:27 +01:00
James Anderson 61cf2e78f5 fix(image): emit /_next/image URLs (#1562)
* fix(image): emit /_next/image URLs to match Next.js

Closes #1513

The default image loader and optimization endpoint switched from the
vinext-specific /_vinext/image path to Next.js's canonical /_next/image.
This unblocks the deploy suite tests that import Next.js's expected
URL shape (/_next/image?url=...&w=...&q=...).

* refactor(image): use IMAGE_OPTIMIZATION_PATH constant at remaining call sites

Replace hardcoded "/_next/image" strings in index.ts, app-rsc-handler.ts,
and the generated worker entry templates in deploy.ts with the
IMAGE_OPTIMIZATION_PATH constant from server/image-optimization, matching
the pattern already used in prod-server.ts. Prevents future drift if the
path ever changes again.

* feat(image): accept both /_next/image and /_vinext/image at the optimizer

Add a VINEXT_IMAGE_OPTIMIZATION_PATH constant and an
isImageOptimizationPath() helper, then route through every match site
(prod-server, dev server passthrough, app RSC handler, generated worker
templates, and shipped example workers). Apps that wire image URLs to
either prefix now hit the same handler; new URLs are still emitted via
IMAGE_OPTIMIZATION_PATH.
2026-05-26 10:28:22 +01:00
James Anderson 1ac7b6514b fix(examples): next build failure for app-router-playground (#1234) 2026-05-15 13:40:59 +01:00
James Anderson 2acec092be chore: add next-env.d.ts to .gitignore (#1224) 2026-05-15 08:31:54 +00:00