Commit Graph

40 Commits

Author SHA1 Message Date
James Anderson e00687a0f6 fix(middleware): match Pages data request metadata (#2239)
* fix(middleware): preserve Pages data routing metadata

* fix(middleware): preserve matched path on data misses

* test(pages): align middleware data miss assertions

* fix(pages): align dev middleware data misses

* fix(pages): gate data misses on real middleware

* fix(middleware): address data redirect review

* Reviewed PR #2239: fixes confirmed

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

* chore(tests): remove stray node_modules symlinks

---------

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-23 19:39:59 +01:00
Divanshu Chauhan 7487886b38 fix(prod-server): send headers-only for HEAD in sendCompressed (#1980) (#2058)
sendCompressed wrote the full body for every request. HEAD requests routed
through it (bot/crawler-buffered Pages HTML and Pages API routes) still
compressed and emitted a payload that Node then discards at the socket level.

Short-circuit with res.end() after writeHead when req.method is HEAD,
mirroring sendWebResponse. This skips spinning up a compressor for a body
that would be thrown away and keeps the buffered sender consistent with the
streamed and static-file senders, which already handle HEAD.

Co-authored-by: James <james@eli.cx>
2026-06-18 22:13:07 +00:00
MaxtuneLee ef003c0ebe fix(server): q-value-aware Accept-Encoding negotiation (#2047)
* fix(server): implement q-value-aware Accept-Encoding negotiation and add tests

* docs(server): update comments for Accept-Encoding parsing to clarify q-value handling

* fix(server): enhance q-value handling in Accept-Encoding parsing and add tests

* fix(server): enhance q-value handling in Accept-Encoding parsing and add tests for isEncodingAccepted

* fix(tests): update negotiateEncoding tests to reflect zstd as highest preference in prod-server.ts

* fix(tests): update negotiateEncoding tests

* fix(tests): remove redundant test for explicit refusal overriding wildcard in negotiateEncoding

* fix(server): normalize encoding checks in isEncodingAccepted function

* refactor(server): extract accept-encoding helpers

* fix(tests): remove redundant zlib check and import HAS_ZSTD from accept-encoding module

* fix(server): honor Accept-Encoding quality weights

* fix(server): repair accept-encoding CI checks

* fix(server): harden content encoding negotiation

* fix(server): align encoding negotiation with Next.js

* refactor(server): remove unused encoding parse state

* fix(server): normalize empty vary headers

---------

Co-authored-by: James <james@eli.cx>
2026-06-18 14:07:05 +01:00
Jerry Zhao 33b24a27c9 refactor(instrumentation): resolve instrumentation hook files in forward-slash space (#2128) 2026-06-18 00:06:17 +01:00
James Anderson bacee9ef52 fix(pages): emit canonical __NEXT_DATA__ JSON (#2043)
* fix(pages): emit canonical next data script

* test(pages): expect canonical next data markup

* fix(pages): initialize canonical next data before routing

* test(pages): parse canonical next data markup

* fix(pages): preserve router bootstrap ordering

* test(pages): expect readiness bootstrap import

* fix(pages): guard readiness bootstrap without next data
2026-06-15 23:16:23 +01:00
Nathan Nguyen 10b5086f4d fix(app-router): align router autoscroll with Next (#2004)
* fix(app-router): align router autoscroll with Next

App Router navigation could diverge from Next.js router autoscroll semantics around hydration timing, loading-shell same-page search navigations, and routes whose first committed node is React-hoisted into head. That broke the upstream router-autoscroll deploy-suite because the client marked hydration too early, static loading-shell templates were not usable for navigation, and the fallback scroll path masked the old-handler hoisted-head behavior.

The fix moves the Next-compatible hydration marker into a passive effect, wires production hydrateRoot error callbacks to match Next's implicit root-boundary and recoverable-error handling, lets authoritative loading-shell templates drive static route optimistic payloads, and skips fallback document scrolling when the committed document only exposes a React-hoisted head resource. The ported tests cover the upstream router-autoscroll behavior plus focused unit boundaries for prefetch mode, optimistic routing, head-resource detection, and hydrateRoot callbacks.

* fix(app-router): hoisted-head scroll fallback is per-intent, not global head scan

* test(app-router): add full-chain scroll intent integration test

* fix(app-router): match legacy scroll focus parity

* fix(app-router): preserve latest scroll navigation intent

* fix(app-router): scope scroll and error recovery

* fix(app-router): gate navigation failure recovery

* fix(app-router): match navigation failure handling

* fix(app-router): align scroll completion parity

* fix(app-router): retain latest failure target

* fix(app-router): preserve route error boundaries

* fix(app-router): clear discarded failure targets

* fix(app-router): align failure recovery boundaries

* fix(app-router): disarm refused recovery targets

* fix(app-router): preserve failure target ownership

* fix(app-router): match boundary recovery parity

---------

Co-authored-by: James <james@eli.cx>
2026-06-14 23:59:28 +01:00
Nathan Nguyen 5ac620c2d0 fix(cache): honor fetch opt-outs and force-dynamic revalidate parity (#1907)
* fix(cache): honor fetch opt-outs in app page output

App Router fetches that explicitly opt out of caching could still leave the rendered page eligible for static output, while dynamic = "force-dynamic" was lowered to fetchCache = "force-no-store" and overrode explicit per-fetch cache/revalidate options.

The cache decision was conflating route-level dynamic rendering with a hard segment fetchCache mode. Next.js treats force-dynamic as a default no-store mode only for fetches without explicit cache or revalidate config, and marks explicit uncached fetches as dynamic for the surrounding render.

Mark explicit uncached fetch decisions through the existing dynamic usage signal, keep force-dynamic as a separate per-request fetch default, and preserve Response.url when reconstructing cached fetch responses.

* fix(cache): correct revalidate:false and Response.url semantics

- Split revalidate: false out of the no-store path; it now caches
  indefinitely (INFINITE_CACHE) matching upstream patch-fetch.ts.
- Store actual response.url (freshResp.url / response.url) in cached
  fetch entries instead of the input request URL.
- Remove markUncachedFetchForPageOutput() from the auth-header fallback
  so it matches upstream's autoNoCache path and does not mark pages
  dynamic.
- Update tests to reflect correct revalidate: false caching behavior
  and add a test for redirect-style response URLs.

* fix(app-router): resolve target route dynamic config during intercepts and revalidation

Prevents the outer route's force-dynamic default from leaking into
interception source routes and ISR revalidation targets.

- app-rsc-entry.ts: expose __resolveRouteDynamicConfig
- app-page-dispatch.ts: pass resolveRouteDynamicConfig to dispatch,
  sync currentForceDynamicFetchDefault when the render target changes
- app-page-dispatch.test.ts: 4 tests covering intercept + revalidation
  paths for both lost and leaked defaults

* fix(app-router): sync fetchCache and force-dynamic default during action redirects/re-renders

* fix(cache): treat revalidate:false as non-explicit for force-dynamic parity

Upstream patch-fetch.ts computes noFetchConfigAndForceDynamic using
!currentFetchRevalidate (truthiness), so revalidate: false is treated
as 'no fetch revalidate config' and force-dynamic wins. vinext's
hasExplicitRevalidateValue() previously returned true for false,
blocking the force-dynamic no-store default and causing the fetch to
cache for 1 year instead.

Update hasExplicitRevalidateValue to exclude false (and 0) from the
explicit set, aligning with upstream's truthiness check. Add a parity
test covering force-dynamic + next.revalidate: false.

* fix(cache): split revalidate predicates for force-dynamic vs segment defaults

The previous fix to hasExplicitRevalidateValue() treated false and 0 as
non-explicit, which matched upstream's force-dynamic truthiness check
but broke segment cache defaults: default-cache would override
revalidate: 0 to force-cache, and default-no-store would override
revalidate: false to no-store.

Split into two helpers:
- hasExplicitRevalidateValue(): any defined value is explicit (for
  segment defaults where 0 and false are explicit opt-outs)
- isFalsyRevalidate(): false and 0 are falsy (for force-dynamic's
  noFetchConfigAndForceDynamic parity check)

Add parity tests for default-cache + revalidate: 0 and
default-no-store + revalidate: false.

* refactor(cache): extract ONE_YEAR_SECONDS and cover only-no-store revalidate:false

* fix(cache): keep dynamic fetch observations for auth bypass and cache-key fallback

Restore recordDynamicFetchObservation in the auth-header safety bypass so
auth-keyed fetches still downgrade the page output to fresh render, and
revert the cache-key-generation fallback to a plain observation so an
internal limitation (oversized/unserializable body) does not mark the
whole page dynamic. Lock in force-dynamic vs explicit segment fetchCache
precedence with a test.

* docs(cache): note deliberate no-cache dynamic-marking divergence from upstream

* fix(route-handler): apply segment fetchCache and force-dynamic fetch default to route handler dispatch

Route handlers never called setCurrentFetchCacheMode /
setCurrentForceDynamicFetchDefault, so a force-dynamic route handler did
not get the per-request no-store fetch default that page dispatch applies,
and an explicit fetchCache export on a route handler module was ignored.

Upstream's app-route module copies userland.fetchCache into the work store
and sets workStore.forceDynamic for dynamic = "force-dynamic", which
patch-fetch turns into a no-store default for fetches without explicit
cache config — for route handlers as well as pages. Mirror that in
dispatchAppRouteHandler and re-apply the state inside the background
regeneration request context.

* fix(fetch-cache): tolerate cached entries missing url and document revalidate:false cacheability

* fix(fetch-cache): fall back to the request URL for cached entries missing url

Reconstructing Response.url from the fetch input preserves the
pre-existing guarantee for legacy/foreign cache entries instead of
degrading to "". Also document why the intercept dispatch deliberately
does not save/restore fetch defaults around buildPageElement.

* docs(fetch-cache): note cached responses now join body-stream cleanup

* docs(fetch-cache): note explicit segment fetchCache beats force-dynamic default

* test(fetch-cache): pin tags-only fetch to no-store under force-dynamic

* docs(fetch-cache): document auth-bypass precedence and test no-store + auth

- Note at the auth-safety bypass that explicit no-store/no-cache/revalidate: 0
  takes the stronger branch first and fully marks the page dynamic; the bypass
  only covers implicitly-cacheable auth-keyed fetches.
- Add a test locking in that explicit no-store with auth headers routes through
  markUncachedFetchForPageOutput rather than the softer auth bypass.
- Clarify in dispatchAppRouteHandler that the fetch-cache setters are new
  wiring for route handlers, not a mirror of a pre-existing pattern.

* test(route-handler): lock no-store fetch bailing ISR for revalidating handlers

A route handler with revalidate = 60 that performs
fetch(url, { cache: "no-store" }) must skip its ISR cache write and be
marked known-dynamic, now that the patched fetch's explicit no-store
branch calls markDynamicUsage() (upstream patch-fetch parity). Exercises
the real fetch-cache shim and the real headers-shim dynamic-usage pair,
matching the app-route-handler-dispatch wiring.

---------

Co-authored-by: James <james@eli.cx>
2026-06-12 11:44:28 +01:00
MaxtuneLee 88a9947e14 fix(trailing-slash): canonical url trailing slash support (#1888)
* fix: canonical url trailing slash support

* fix(metadata): prevent trailing slash on file-like and .well-known canonical URLs

* fix(metadata): remove trailing slash handling for .well-known URLs

* test: add tests for createAppPageRouteBodyMetadata function with body placement

* fix(metadata): match trailing slash URL scope

* fix(metadata): resolve URL object alternates

---------

Co-authored-by: James <james@eli.cx>
2026-06-11 13:10:41 +01:00
James Anderson 2d9f3ffc36 fix(app-router): accept icons.other as single descriptor (#1492) (#1667)
Next.js's `metadata.icons.other` accepts either a single
`IconDescriptor` or an array of them, but `MetadataHead` iterated
it directly and threw `TypeError: metadata.icons.other is not iterable`
when given a single object. That crash bubbled up into the page
render so no `<link>` tags (favicon, shortcut, apple, custom) reached
the head at all for any page declaring `icons.other` as an object.

Normalize `metadata.icons.other` to an array before iterating, widen
its type to `OtherIconDescriptor | OtherIconDescriptor[]`, and also
forward the optional `type` attribute so descriptors like
`{ rel: "mask-icon", url, type: "image/svg+xml" }` round-trip
correctly. Ported coverage from
`test/e2e/app-dir/metadata-icons/metadata-icons.test.ts`.

Refs cloudflare/vinext#1492
2026-05-28 20:13:04 +01:00
Nathan Nguyen 4b6b438b94 fix(app-router): stream generated metadata for non-html bots (#1585)
* fix(app-router): stream generated metadata for non-html bots

Dynamic App Router metadata was always folded into the route head. That diverged from Next.js for normal browser-like requests, where generated metadata is sent through a body outlet while html-limited bots still receive blocking head metadata.

Track whether the matched route uses generateMetadata, thread next.config htmlLimitedBots into the generated RSC entry, and choose head versus body placement at page element construction.

* test(app-router): cover streaming metadata bot edge cases

Generated metadata must keep the default html-limited bot list when serialized config contains a falsy regex source. Empty config strings previously produced an empty regex, which treated every user agent as blocking and moved generated metadata back into the head.

Add regression coverage for the default Twitterbot path and the streaming body serializer, then match Next.js by falling back to the default bot regex for any falsy htmlLimitedBots value.

* fix(app-router): validate html-limited bot regex config

Invalid serialized htmlLimitedBots values could reach request handling and throw while building the bot matcher. That puts a config error on the request path and recompiles the same matcher for repeated requests.

Validate serialized regex sources while resolving next.config and reuse compiled bot matchers in the streaming metadata helper. The focused tests cover invalid config, falsy fallback, and matcher reuse.

* refactor(config): narrow next config option reads

Config resolution had several local assertions around experimental options, Turbopack aliases, output mode, and Sass options. Those assertions made the boundary wider than necessary in the code adjacent to htmlLimitedBots handling.

Read optional records, strings, arrays, and body-size inputs through typed narrowing helpers so the resolved config path keeps the same semantics without spreading unchecked asserted values.

* fix(config): keep bot regex helper out of server graph

Config resolution imported the streaming metadata server module to validate htmlLimitedBots. In RSC-backed App Router integration tests, that cross-layer import can pull server runtime modules through config resolution and surface rsc reference-validation errors.

Move the shared html-limited bot matcher cache to a neutral utils module and import it from config and the server streaming metadata helper. This keeps config validation independent from the server runtime graph while preserving the cached matcher behavior.
2026-05-26 12:07:07 +01:00
Nathan Nguyen 8df248863d fix: match Next metadata image route behavior (#1436) 2026-05-22 11:05:58 +00:00
Divanshu Chauhan (divkix) cd1013ea33 feat(config): add experimental.appShells plumbing (#1415)
* feat(config): add experimental.appShells plumbing

Accepts experimental.appShells in next.config and defines
process.env.__NEXT_APP_SHELLS=false at build time, matching
Next.js PR #93997 plumbing scope. Behavorial implementation
is gated on upstream co-flags that vinext does not yet support.

- Adds experimental.appShells to CONFIG_SUPPORT as unsupported
- Sets __NEXT_APP_SHELLS define to false regardless of config value
- Tests for vinext check detection and Vite define injection

Fixes #1405

* review: address PR nits

- Expand experimental.appShells detail to explain why it's unsupported
- Move build-time define tests into their own describe block, so the
  __NEXT_APP_SHELLS define test isn't nested under basePath

* review: drop orphaned server lifecycle from basePath describe

The two remaining resolveNextConfig tests import from source directly
and don't use the dev server.

---------

Co-authored-by: James <james@eli.cx>
2026-05-22 10:26:40 +01:00
James Anderson b06a1a1d04 fix(i18n): strip locale prefix for API routes (#1408)
* fix(i18n): strip locale prefix for API routes

Mirror Next.js's behaviour where a locale-prefixed request path like
`/fr/api/ok` is matched against `pages/api/ok` after stripping the
locale segment. Previously the locale prefix was kept through the
`/api/` startsWith check, so any locale-prefixed API URL 404'd.

Add a small helper `stripI18nLocaleForApiRoute(url, i18nConfig)` in
`server/pages-i18n.ts` that mirrors Next.js's
`normalizeLocalePath().pathname` (see
packages/next/src/shared/lib/i18n/normalize-locale-path.ts) and wire it
into all three Pages Router request pipelines so dev/prod parity holds:

- dev plugin: `packages/vinext/src/index.ts`
- prod (Node): `packages/vinext/src/server/prod-server.ts`
- prod (Cloudflare worker entry): `packages/vinext/src/deploy.ts`

App Router is intentionally unchanged: Next.js does not support the
`i18n` config field with App Router, so App Router route handlers
under `app/*/route.ts` never see a locale prefix to strip.

Refs #1336 (item 3). Items 2 (sticky locale on client navigations) and
4 (default-locale path normalisation) are being handled in parallel
PRs.

Tests:
- Unit tests for `stripI18nLocaleForApiRoute` covering basic, regional
  (`nl-NL`), query-preserving, unconfigured-prefix, and null-config
  cases (`tests/pages-i18n.test.ts`).
- Dev integration tests (`tests/features.test.ts`) and prod integration
  tests (`tests/pages-i18n-prod.test.ts`) that GET `/fr/api/ok` and
  assert a 200 "ok" response against the existing
  `pages-i18n-domains` fixture, ported from Next.js's
  `test/e2e/middleware-redirects/test/index.test.ts` ("should redirect
  to api route with locale").
- Updates to `tests/deploy.test.ts` assertions for the generated
  worker-entry template strings.

* test(after-deploy): update worker-entry assertion after locale strip

The Pages Router worker entry now calls handleApiRoute(request,
apiLookupUrl, ctx) instead of using resolvedUrl directly, so this
generated-code assertion needs the new variable name.

Refs #1336 (item 3).
2026-05-21 15:02:18 +01:00
Nathan Nguyen afc549d624 fix(router): normalize trailing slash parity (#1316)
Trailing slash handling treated every non-api path the same on server redirects, so trailingSlash:true added slashes to file-looking catch-all routes and Pages Router imperative navigation skipped the client canonicalization path used by Link.

Next.js splits the invariant between route-manifest redirects and client resolveHref normalization: file-looking paths lose a trailing slash, non-file paths gain one, and queries stay attached to the canonical pathname. Share the server redirect decision across Pages Router runtimes and apply the same client helper in Router.push and Router.replace.

Adds focused regressions for catch-all dot segments, query preservation, .well-known exclusion, and Pages Router history writes.
2026-05-19 18:06:36 +01:00
Nathan Nguyen 1551f91cbd fix(metadata): match Next dynamic route semantics (#1317)
* fix(metadata): match Next dynamic route semantics

Metadata routes diverged from Next.js in several observable cases: robots.txt emitted Sitemap before Host, dynamic metadata Response results could miss framework cache headers, TSX sitemap files were not discovered, and rendered metadata URLs treated canonical, manifest, and file-based social image routes as the same URL class.

The implementation now follows Next's metadata route extension, cache, robots, and URL resolution rules while keeping file-based social image route markers non-enumerable so resolved metadata shape does not leak internal state.

* fix(metadata): gate social image preview fallback

File-based social image routes used the preview deployment URL whenever VERCEL_URL was present outside development. That let deployment URLs override an explicit metadataBase in non-preview production environments.

Gate preview URL fallback on VERCEL_ENV=preview, add coverage for metadataBase precedence and production fallback URLs, and keep static metadata image cache headers stable in development.
2026-05-19 14:11:47 +01:00
Nathan Nguyen b69844ec3f fix(metadata): apply ancestor templates to title defaults (#1256)
Metadata title defaults currently render without the active ancestor template. That diverges from Next.js when a child layout or page provides title.default under a parent layout title.template.

The merge path resolved only the final title after collecting templates, so object defaults skipped the stashed template used for that segment. Resolve each title as it is encountered against the current ancestor template, then stash the current layout template for descendants.

Covers the child-layout default regression and updates page-default expectations to match Next.js resolveTitle semantics.
2026-05-16 18:08:40 +01:00
Nathan Nguyen 07be32258f fix(router): preserve filesystem routes before afterFiles rewrites (#1166)
* fix(router): preserve filesystem routes before afterFiles rewrites

afterFiles rewrites were evaluated before App and Pages filesystem route matches in several runtime paths. That let a rewrite override an existing non-dynamic page, which diverges from Next.js route ordering.

The fix checks the page/app match first and only applies afterFiles rewrites when no non-dynamic route wins, while still allowing afterFiles to run before dynamic routes. Regression coverage exercises App RSC handler, Pages dev/prod, and generated Worker wiring.

* test(router): align chained afterFiles rewrite expectations

The chained rewrite fixture expected an afterFiles rewrite to override a concrete /intermediate page. That is the route-ordering behavior this branch fixes.

Update the fixture to cover both intended contracts: middleware can chain into afterFiles when no page file wins, and concrete page files are not overridden by afterFiles rewrites.

* refactor(router): remove route-ordering type assertions

Validate generated Pages route metadata at the boundary instead of asserting its shape, and make the app handler test fixture route matching explicit.

* test(router): cover afterFiles order in Pages Worker

Add a built Cloudflare Pages Router regression where a concrete page route must win before an afterFiles rewrite. Align custom Pages Worker entries with the generated worker route-match gate.
2026-05-11 17:13:51 +01:00
Nathan Nguyen 8edc010a10 fix(fetch-cache): dedupe identical render fetches (#1134)
* fix(fetch-cache): dedupe identical render fetches

Uncached and no-store fetches currently bypass the persistent fetch cache by calling the original fetch directly. That diverges from Next.js, where the patched fetch wraps the original fetch in a request-scoped dedupe layer before persistent cache policy runs, so repeated GET and HEAD fetches during a render share one network response.

Add request-scoped fetch dedupe state to the fetch cache context and route network misses and cache-bypass fetches through it. The dedupe key follows the Next.js method, header, mode, redirect, credentials, referrer, referrerPolicy, and integrity semantics while still opting out for abort signals, keepalive, and side-effecting methods.

Update fetch-cache coverage for uncached render dedupe, independent response bodies, request-scope isolation, trace header exclusion, and the changed no-store/no-cache interaction.

* fix(fetch-cache): give each cloned dedupe response its own Headers

Pass `new Headers(response.headers)` per clone instead of sharing the same
Headers object across both branches and the dedupe entry. Matches Next.js'
`cloneResponse` and avoids a future-correctness trap if any code path mutates
response headers post-fetch.

Also drops two redundant `runWithFetchDedupe` calls inside `dispatchAppPage`
(the ISR revalidation render path and the intercept stream render path) — both
sites are inline anonymous functions that already inherit the dedupe scope from
the outer `dispatchAppPage` / `runAppPageRevalidationContext` wrap, so the
inner calls were no-ops and just obscured that inheritance.

* test(fetch-cache): cover Request input dedupe + always clone bodyless responses

Always construct fresh Response objects in cloneDedupeResponse, including the
bodyless path that previously returned the same `response` reference for both
tuple slots. Mirrors Next.js' cloneResponse and avoids any "disturbed response"
surprise if a runtime tracks consumption state on the shared reference. Factor
the per-clone construction into buildDedupeClone() so bodied and bodyless
clones share the headers-copy + url-restore + finalizer-register treatment.

Adds a comment near entry.response assignment noting that the unconsumed tee
branch is bounded by the render scope: when runWithFetchDedupe exits, the
dedupe map becomes unreachable and the FinalizationRegistry cancels any
still-unconsumed branch.

Also adds two tests covering the Request-object input path of
createFetchDedupeCandidate — one verifying dedupe applies for identical
Request inputs, one verifying differing non-trace headers on Request inputs
prevent dedupe.

* fix(fetch-cache): drop failed dedupe entries so later callers can retry

When the original fetch rejects, the dedupe entry stayed in the map with
response: null. Subsequent callers within the same render scope chained on
the rejected promise — propagation was correct, but they could never retry,
because the failed entry blocked fresh attempts for the rest of the scope.
React.cache() (which Next.js uses) avoids this because each call site
naturally retries on failure.

Splice the entry out of the URL bucket on rejection so a later fetch to the
same URL within the same render scope creates a fresh entry and re-issues
the upstream request.

Also align the fetch-dedupe-metadata fixture with fetch-dedupe-isr-metadata
by replacing the `as CountBody` cast with a runtime assertCountBody check.

* docs(fetch-cache): document scope inheritance + harden ISR test stabilization

Rename `dispatchAppPageWithDedupe` to `dispatchAppPageInner` so the name
reflects that it runs *inside* the dedupe scope rather than activating it.

Add comments at the four `runWithFetchDedupe` / `renderToReadableStream`
sites explaining how each one relates to the surrounding dedupe scope:

- app-page-render and app-page-boundary: defensive wrap, no-op under
  dispatch; standalone callers must keep an outer scope alive across async
  stream consumption since `runWithFetchDedupe` of a synchronous fn only
  covers the synchronous portion.
- ISR revalidation render and intercept render in dispatch: explicitly note
  why no inner wrap is needed (the outer revalidation context / dispatch
  wrapper already activated dedupe).
- runWithFetchDedupe doc: document the ALS-scope-vs-async-consumption
  caveat that ties this together.

Replace the fixed 200ms post-condition sleep in the ISR background dedupe
test with a 500ms count-stabilization poll. A stray third upstream fetch
(which would betray dedupe leaking across the metadata + page render
boundary) is now caught regardless of when it fires.

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 22:10:17 +01:00
Nathan Nguyen 5ee9860b24 fix(middleware): preserve NextResponse.next status (#1115)
NextResponse.next({ status }) currently continues routing but drops the middleware response status. That diverges from Next.js middleware handling and makes successful route rendering mask middleware-selected statuses such as 404.

The middleware runtime now exposes a status override for continue responses, and App Router plus Pages Router dev/prod consumers feed that value into their existing middleware response contexts. Rewrite status behavior remains covered by the same path.

Tests cover the middleware runtime contract, App Router context propagation, and a Pages Router dev integration case with a real matched page.
2026-05-07 09:01:04 +01:00
Nathan Nguyen 2bd7377f70 fix(shims): match Next metadata merge and Twitter inheritance (#1055)
* fix(shims): deep-merge nested metadata and add OG/Twitter inheritance

mergeMetadataEntries did a flat top-level key replacement, so setting
openGraph: { title: "Override" } in a child layout erased all root
OpenGraph fields like siteName and images. Next.js resolves these
per-subkey; we now shallow-merge the nested objects for openGraph,
twitter, icons, alternates, robots, and other.

Setting only openGraph.title without twitter.title produced no Twitter
Card tags because vinext skipped Next.js's postProcessMetadata step.
We now auto-fill twitter:title from og:title (or metadata:title),
twitter:description from og:description (or metadata:description),
and twitter:images from og:images, and default twitter:card to
summary_large_image when images are present.

Ported from Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolve-metadata.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts

* fixup! fix(shims): deep-merge nested metadata and add OG/Twitter inheritance

Extract postProcessMetadata from mergeMetadataEntries to avoid
polluting intermediate merge results during layout accumulation.
Run it once at the end of resolveAppPageHead after file-based
metadata has been applied, matching Next.js's accumulator pattern.

Also fix two issues caught by Copilot review and CI:

1. Twitter auto-fill now works even when openGraph is absent —
   metadata.title and metadata.description serve as fallback sources.

2. Remove all as assertions from production code; use typed locals
   and direct index access via Metadata's string index signature.

3. Update app-page-head.test.ts expectations to match post-processed
   output and add missing regression test for no-openGraph case.

* chore: address review feedback — clone input, ?? for title, missing test

- postProcessMetadata now shallow-clones its input to prevent accidental
  mutation of the caller's object.
- resolveStringTitle uses ?? instead of || to avoid treating empty
  string as falsy (semantically correct but practically no difference).
- Added comment explaining why verification, appleWebApp, appLinks,
  and formatDetection are excluded from DEEP_MERGE_KEYS.
- Added test: child can clear a parent openGraph subkey by setting it
  to undefined, verifying twitter auto-fill skips cleared images.

* fix(shims): match Next metadata segment replacement

Nested metadata objects were being merged across segments, which kept stale parent OpenGraph, Twitter, alternates, icons, and robots fields when a child segment defined the same key.

Next only merges custom other metadata across segments. Replace the broad nested merge with an explicit other merge, keep OG-to-Twitter post-processing gated by openGraph, and update metadata tests to lock in the compatibility contract.
2026-05-05 19:00:10 +01:00
Nathan Nguyen 1d7c031e30 feat(app-router): feed file metadata routes into head output (#891) 2026-04-30 19:38:32 +01:00
James Anderson 364e36bcd8 fix(image): strip priority prop before forwarding to UnpicImage to prevent DOM leak (#662)
* fix(image): strip priority prop before forwarding to UnpicImage to prevent DOM leak

The `priority` prop is a Next.js-specific concept that must never reach the
DOM as an attribute. On the two UnpicImage render paths (remote URL with fill
and remote URL with width+height), `priority={true}` was forwarded directly
to `@unpic/react`'s Image component which did not reliably strip it before
rendering the DOM `<img>`, triggering:

  Received `true` for a non-boolean attribute `priority`.

Fix: replace `priority={priority}` with the equivalent HTML semantics —
`loading={priority ? 'eager' : loading ?? 'lazy'}` and
`fetchPriority={priority ? 'high' : undefined}` — matching what the local
image and custom-loader paths already do correctly.

Adds 10 reproduction tests covering both affected render paths.

* test: add 50ms slack to compressed streaming timing assertion to fix CI flakiness
2026-03-23 07:54:38 +00:00
Steve Faulkner 5f1681bce7 Fix tsconfig alias transforms for RSC builds (#655)
* Fix tsconfig alias transforms for RSC builds

* Fix flaky compressed streaming timing test
2026-03-22 14:07:29 -05:00
Jared Stowell f709e22209 fix: Pages Router SSR streaming (#514)
* fix pages router streaming

* handle regressions

* fix: align pages response merge behavior

* fix: drop stale worker content-length headers

* fix: preserve no-body rewrite parity

* fix: cancel streamed HEAD responses

* test: support merged prod server shape

* Fix compressed Pages SSR streaming

* Fix ecosystem fixture port collision
2026-03-21 22:44:45 -05:00
James Anderson 2ef52ad66b fix: use page extensions for middleware files (#595)
* fix: use page extensions for middleware files

* same thing for instrumentation
2026-03-20 08:45:26 +00:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* 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
2026-03-15 10:50:13 +00:00
Webdesign29 85f8c493b7 fix: handle URL objects in metadata resolveUrl (#465)
* fix: handle URL objects in metadata resolveUrl

The Next.js Metadata API types URL fields as `string | URL` (e.g.
`metadataBase`, `openGraph.url`, icon/image URLs). The internal
`resolveUrl()` function in `MetadataHead` only accepted `string`,
so passing a `URL` object (which is valid per Next.js types) crashed
with `url.startsWith is not a function`.

Add type coercion at the top of `resolveUrl()` to convert `URL`
objects to strings before processing.

Reproduction:
1. Set `metadataBase: new URL("https://example.com")` in a layout
2. Set `openGraph: { url: "/page" }` in a page's metadata
3. `resolveUrl` receives the URL object, calls `.startsWith()` on it
4. Crash: `url.startsWith is not a function`

With this fix, `URL` objects are coerced to strings via `.toString()`.

* fix: update Metadata types and rendering to accept string | URL fields

Address bonk review feedback:
- Add test covering URL object input for alternates.canonical and
  openGraph.url to lock in the fix against regressions
- Update Metadata interface to type URL fields as string | URL,
  matching Next.js types (openGraph.url/images/videos/audio,
  twitter.images, icons, manifest, alternates.canonical/languages/
  media/types)
- Add overload to resolveUrl() so it returns string when called with
  a non-undefined argument, eliminating string | URL fallback issues
- Fix image/icon array normalisation to handle bare URL objects
  alongside strings and descriptor objects

* fmt

* fix: remove dead-code ?? fallbacks and unnecessary test casts

Address bonk re-review feedback:
- Remove 'as unknown as string' casts in the URL-object test since the
  Metadata interface now types those fields as string | URL
- Remove all 17 remaining 'resolveUrl(x) ?? x' fallbacks; resolveUrl
  is overloaded to return string for any truthy input so these never
  fired, and with string | URL fields the fallback value would have
  passed a URL object directly to content=/href= attributes

* fmt

---------

Co-authored-by: James <james@eli.cx>
2026-03-12 19:32:31 +00:00
Nathan Nguyen 8d10bc64fd fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries (#503)
* fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries

MemoryCacheHandler.set had an inconsistent guard on data.revalidate:
the ctx path checked `revalidate > 0` but the data path only checked
`typeof data.revalidate === "number"`, allowing revalidate:0 to set
revalidateAt = Date.now() — making entries immediately stale.

This was inconsistent with KVCacheHandler which already had the > 0
guard on both paths. The two handlers would produce different staleness
behavior for the same input: Memory → immediately stale, KV → no TTL.

Add the missing `> 0` guard to align both code paths and both handlers.

* fix: revalidate:0 should skip cache storage entirely

Both MemoryCacheHandler and KVCacheHandler had inconsistent handling of
revalidate:0. In Next.js, revalidate:0 means "don't cache." But the
handlers would still store entries — Memory made them immediately stale
(data path lacked > 0 guard), KV cached them forever (both paths
rejected 0, so revalidateAt=null meant no expiry).

Resolve effective revalidate from both ctx and data paths (data
overrides ctx), and early-return without storing when it's 0. Both
handlers now use identical logic.

Fix existing test data that incidentally used data.revalidate:0 in
tests meant to exercise stale-while-revalidate and tag invalidation.
2026-03-12 17:34:12 +00:00
Nathan Nguyen 38dc409a43 feat: Add Pages Router i18n domain routing (#471)
* Add Pages Router domain locale routing

* Add coverage for Pages Router domain locale behavior

* Format Pages Router i18n domain routing changes

* Handle basePath in Pages Router i18n domains

* Refactor Pages Router i18n domain fixtures

* Update Pages Router entry snapshot after rebase

* Align domain locale redirects with Next.js

* Clarify Pages Router domain locale helpers

* Fix case-insensitive locale prefixes

* Simplify same-domain locale URL handling

* Cover same-domain locale alias redirects

* Document Pages Router i18n invariants

* Guard preferred locale in domain redirects
2026-03-12 13:49:59 +00:00
Nathan Nguyen d093c3317b fix: Pages Router ISR background regeneration re-renders HTML (#487)
* fix: Pages Router ISR background regeneration re-renders HTML

Background regeneration only re-ran getStaticProps but cached the old
HTML, so server-rendered content was permanently stale after the first
revalidation cycle. The pageData JSON was updated but never used to
re-render the page.

Now the regeneration callback:
- Re-renders the page component with fresh props
- Rebuilds __NEXT_DATA__ with fresh props
- Updates the revalidate duration map (dev server)
- Passes full locale context to getStaticProps

Fixed in both dev server and production server entry.

* ci: retrigger checks

* fix: preserve document shell during prod ISR regeneration

The prod entry's regeneration was hardcoding a simple HTML suffix,
losing custom _document content between <Main /> and <NextScript />.

Now splits the cached HTML at known markers (<div id="__next"> and
__NEXT_DATA__ script) to preserve the full document structure: head
section, gap content, and tail are carried forward from the original
cached entry.

Also updates the entry-templates snapshot.

* ci: retrigger checks

* ci: retrigger checks
2026-03-12 08:28:15 +00:00
James Anderson 319f9296ff feat: ISR caching for App Router (production-only, stale-while-revalidate) (#405)
* feat: add ISR caching for App Router (production-only, stale-while-revalidate)

- Inline ISR helpers (__isrGet, __isrSet, __triggerBackgroundRegeneration,
  __isrCacheKey, __pendingRegenerations) in the generated RSC entry
- Cache READ fires before buildPageElement for prod pages with revalidate > 0,
  returning cached HTML/headers without loading component modules on HIT/STALE
- Cache WRITE (+ X-Vinext-Cache: MISS header) is guarded by
  process.env.NODE_ENV === 'production' so dev never reads/writes the cache
- Background regenration is deduplicated via __pendingRegenerations Map and
  registered with ctx.waitUntil for Cloudflare Workers
- Thread ExecutionContext (ctx) from app-router-entry.ts through to _handleRequest
- Dev mode still emits correct Cache-Control headers based on export const revalidate
- Add integration tests: Cache-Control in dev, no X-Vinext-Cache in dev, RSC
  requests bypass ISR, pages without revalidate emit no ISR headers
- Add 10 code-generation tests verifying the prod guard, helpers, and structure

* fix: remove unused isrCachePath variable and fix formatting

* fix: cache RSC wire format (rscData) in ISR entries; serve RSC requests from cache

Previously the ISR cache only stored HTML and excluded all RSC requests
from cache reads/writes. This caused two bugs:

1. RSC requests (client-side navigation, link prefetch) always triggered a
   full component tree render even when a fresh cache entry existed, wasting
   CPU on every soft-nav and prefetch.

2. rscData was never captured — the cache entry had rscData: undefined —
   so there was no way to serve RSC requests from cache even if we wanted to.

Fixes:
- Tee the RSC stream before SSR on ISR-eligible HTML requests (prod + revalidate > 0)
  to capture the RSC wire format as an ArrayBuffer stored in rscData
- Background regen also tees its RSC stream to keep rscData fresh
- Remove the !isRscRequest guard from the ISR READ block: both HTML requests
  and RSC requests now hit the cache. HTML requests get cached html; RSC
  requests get cached rscData (falling through to render if rscData is absent)
- HIT and STALE paths branch on isRscRequest: RSC responses get
  Content-Type: text/x-component and the rscData bytes; HTML responses get
  Content-Type: text/html and the html string

Tests updated:
- Replace 'RSC requests return RSC stream without cache headers' test with
  two tests that assert Cache-Control IS emitted for RSC responses on ISR
  pages (dev mode), and that Next-Router-Prefetch requests also get it
- Replace .tee() count test with assertion that >=2 tees exist (RSC + HTML)
- Add rscData storage assertions for WRITE and background regen paths
- Add assertion that ISR READ block has no !isRscRequest guard

* fix(isr): fix RSC cache miss and early-exit performance for App Router ISR

Two bugs fixed:

1. RSC requests (client-side nav / prefetch) were never cached because the RSC
   stream tee that captures rscData was placed AFTER the 'if (isRscRequest)'
   early-return, so the capture branch was never consumed. Move the tee to
   immediately after renderToReadableStream — before the isRscRequest branch —
   and register a waitUntil in the RSC path to write rscData to the cache.

2. The ISR cache read block fired after the generateStaticParams call, meaning
   a cache hit still did the expensive static-params validation. Move the ISR
   read to before generateStaticParams so a HIT skips all that work.

Tests: add two new generateRscEntry ISR assertions:
  - ISR read index < generateStaticParams index in generated code
  - __rscForResponse tee assignment before 'return new Response(__rscForResponse'

* feat(isr): cache RSC prefetch responses immediately as partial entries

RSC prefetch requests (Next-Router-Prefetch and client-side nav) now write
a partial cache entry with rscData + html:'' on MISS. Subsequent RSC requests
serve from cache (HIT) without waiting for an HTML request to populate the
entry first.

The ISR read block treats html:'' as a partial hit: RSC requests get a HIT,
but HTML requests fall through to render and overwrite the entry with a
complete html+rscData entry. This prevents blank HTML responses on first HTML
request after a prefetch-first cold start.

Also adds console.log logging for all ISR cache events (HIT/MISS/STALE/write)
in production to aid debugging on deployed Workers.

* fix(isr): use ALS to thread ExecutionContext into KVCacheHandler without constructor arg

Add runWithExecutionContext/getRequestExecutionContext to next/cache shim
backed by AsyncLocalStorage. The RSC entry handler now wraps each request
in this ALS scope so KVCacheHandler._putInBackground and _deleteInBackground
can call ctx.waitUntil() without needing ctx passed at construction time.

- cache.ts: add ExecutionContextLike interface + ALS helpers
- app-rsc-entry.ts: import runWithExecutionContext, wrap _run in ALS scope
- kv-cache-handler.ts: check ALS ctx first, fall back to constructor ctx
- deploy.ts: simplify generated template to new KVCacheHandler(env.VINEXT_CACHE)
- tests/deploy.test.ts: update expectation to match simplified constructor

* fix(isr): store activeHandler on globalThis to fix cross-environment cache isolation

Cloudflare Workers with Vite RSC loads separate module instances per
environment (worker / RSC / SSR). Previously, activeHandler was a
module-local variable, so setCacheHandler(new KVCacheHandler(...)) in
the worker environment set the worker copy while getCacheHandler() in
the RSC environment still returned a fresh MemoryCacheHandler from
its own copy — causing every ISR read/write to go to in-process
memory that is discarded after each request.

Fix: store the active handler on globalThis via Symbol.for('vinext.cacheHandler'),
the same pattern used by _cacheAls, _ctxAls, and _unstableCacheAls. All
Vite environments share the same globalThis on Cloudflare Workers, so
the KVCacheHandler set in worker/index.ts is now visible to the RSC
entry's getCacheHandler() call, enabling real KV-backed ISR cache
reads and writes.

* fix(isr): allow force-static/error pages to be served from ISR cache, prevent partial RSC write from clobbering complete entry

- Remove !isForceStatic and !isDynamicError from the ISR cache read guard.
  Both modes are compatible with ISR: they control dynamic API behaviour during
  render, not whether results can be cached. The asymmetry between the read guard
  (which excluded them) and the write guard (which did not) caused writes to
  succeed but reads to be skipped entirely, so every request returned MISS.
  Only isForceDynamic should bypass the ISR cache.

- Add a read-before-write in the RSC partial-write path. When an RSC request
  writes a partial cache entry (html: "", rscData set) before the first HTML
  request has come in, a concurrent or later RSC request could race and overwrite
  a complete html+rscData entry that the HTML write had already landed. The fix
  reads the existing entry first and skips the partial write if a fresh, complete
  entry (non-empty html) is already present, so the HTML write always wins.

* fix(isr): split HTML and RSC into separate cache keys, eliminating write races

Previously both HTML and RSC data were stored under a single key with an
html:"" partial-entry sentinel to signal 'RSC written, HTML pending'. This
caused write races on eventually-consistent stores like KV: a concurrent RSC
request could overwrite a complete html+rscData entry with a partial one,
or the read-before-write guard added to prevent that would itself race.

This mirrors how Next.js handles it in FileSystemCache: HTML is stored in
<key>.html and RSC in <key>.rsc as independent files, so each request type
reads and writes only its own key with no coordination needed.

Changes:
- Replace __isrCacheKey() with __isrHtmlKey() / __isrRscKey() which append
  :html and :rsc suffixes respectively
- ISR read block selects the key based on isRscRequest before the get() call
- RSC write path uses __isrRscKey and stores only rscData (html: "")
- HTML write path uses __isrHtmlKey and stores only html (rscData: undefined)
- Background regen writes both keys independently via Promise.all
- Remove the read-before-write hack (no longer needed)
- Remove partial-entry sentinel comments (concept no longer exists)
- HTML write path no longer awaits __isrRscDataPromise

* fix(kv-cache): always use 30-day KV TTL regardless of revalidate frequency

Previously expirationTtl was 10x the revalidate period (clamped to 60s–30d).
For pages with short revalidate windows (e.g. revalidate=5), this meant entries
were evicted from KV after ~50 seconds of no traffic, causing the next request
to block on a fresh render instead of serving stale content.

KV eviction should never be the reason a stale entry disappears — staleness is
tracked by the revalidateAt timestamp in the stored JSON, not by KV TTL. Always
storing for 30 days means stale content is always available to serve while
background regen runs, and entries only disappear after 30 days of zero traffic
or explicit tag invalidation.

* fix(isr): prefix KV cache keys with per-deployment build ID, add ttlSeconds option to KVCacheHandler

- __isrCacheKey now embeds process.env.__VINEXT_BUILD_ID (statically
  replaced by Vite's define at build time) so each deployment gets its
  own effective cache namespace in KV — stale entries from previous
  deploys are never served as hits
- vinextBuildId (crypto.randomUUID() at plugin scope) is passed to
  generateRscEntry and injected as the __VINEXT_BUILD_ID define
- KVCacheHandler accepts an optional ttlSeconds constructor option
  (defaults to 30 days) so users can override KV entry lifetime

* regen snaps

* fmt

* refactor(ctx): migrate to canonical request-context shim for ExecutionContext ALS

- shims/cache.ts: remove duplicated ALS implementation; re-export
  runWithExecutionContext and getRequestExecutionContext from the
  canonical shims/request-context.ts (Symbol.for('vinext.requestContext.als'))
- app-rsc-entry.ts: import runWithExecutionContext from request-context
  via absolute path (consistent with pages-server-entry pattern) rather
  than the bare 'vinext/shims/request-context' specifier which is not
  in the alias map
- deploy.ts: restore missing ${isrSetup} interpolation that was dropped
  during rebase conflict resolution
- Regenerate entry-template snapshots

* fix(isr): address PR review — hash collision, dedup key, regen headers, RSC key from HTML path, Infinity revalidate, Response init, deploy KV hint

* fix(isr): use __hasRsc/__hasHtml guards in cache read block; document 30-day flat KV TTL in test

* fix(isr): gate debug logs behind NEXT_PRIVATE_DEBUG_CACHE, add FNV seed comment, document headers limitation
2026-03-10 21:39:33 +00:00
Jared Stowell dea3c3e44f fix: enforce segment boundaries for basePath stripping and redirect prefixing (#393)
* Update deploy basePath stripping

* Fix redirect basePath handling

* Document basePath segment fix

* Fix external redirect basePath
2026-03-10 08:03:20 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
James Anderson 4e0294fc4f fix: middleware crash in dev mode with cloudflare plugin (#344)
* test: middleware with cloudflare plugin and routing bypass

* fix: use module runner for pages middleware

* add pages router dev test
2026-03-08 14:04:11 +00:00
Divanshu Chauhan (divkix) dc8e953f07 fix(instrumentation): defer ssrLoadModule to post-middleware hook (#289)
* fix(instrumentation): defer ssrLoadModule call to post-middleware hook

Vite 7's SSRCompatModuleRunner requires the SSR environment's transport
channel to be initialized before ssrLoadModule() can be called. During
configureServer(), the channel is not yet ready, causing a TypeError
when instrumentation.ts exists.

Move the runInstrumentation() call from the configureServer body into
the returned post-middleware function, where environments are fully
initialized.

Fixes #167

* fix(instrumentation): address review comments on deferred ssrLoadModule

- Remove unreachable .catch() on runInstrumentation() since internal
  try/catch already handles errors and the function never rejects
- Mock console.error in transport error test to suppress noisy output
  and assert the expected error message
- Rename misleading test to accurately describe what it verifies

* test: address instrumentation review feedback
2026-03-06 19:33:12 -06:00
Nathan Nguyen af0a839a67 fix: preserve multiple Set-Cookie headers in response merging (#297)
* fix: preserve multiple Set-Cookie headers in prod-server and worker entry

The response header merging in prod-server.ts and the generated Cloudflare
worker entry used Record<string, string> which flattened multiple Set-Cookie
headers — the last value won, or cookies with Expires dates got corrupted
by comma-joining.

Extract mergeResponseHeaders() helper that uses getSetCookie() to preserve
array-valued Set-Cookie headers. Apply the same fix to the worker entry
template in deploy.ts using the Headers API.

Fixes #295

* fix: address review feedback on Set-Cookie header preservation

- Add `as string` cast in deploy.ts worker entry for x-middleware-request-*
  header unpacking (matches prod-server.ts)
- Normalize Vary header with Array.isArray check in sendCompressed to handle
  the widened type correctly
- Add edge-case test for middleware cookie as plain string (not array)

* fix: apply Set-Cookie fix to example workers, remove redundant toLowerCase

- Fix the same Set-Cookie flattening bug in hand-written example worker
  entries (pages-router-cloudflare, realworld-api-rest)
- Remove 4 redundant .toLowerCase() calls — Headers.forEach() always
  yields lowercase keys

* fix: add missing as string cast in deploy.ts middleware header collection

* refactor: extract mergeHeaders to shared vinext/server/worker-utils

The mergeHeaders function was duplicated in both example worker entries
(pages-router-cloudflare, realworld-api-rest) and the deploy.ts template.
Extract it to a shared module that example workers import. The deploy.ts
template still inlines it (code generation constraint).

Reduces 3 copies to 2 (shared module + generated template).

* fix: array-accumulate Set-Cookie in config headers, fix JSDoc, add behavioral test

- deploy.ts template: config headers section was comma-joining Set-Cookie values
  using += which corrupts cookies with Expires dates and loses all but the last
  cookie when multiple config rules match. Matches the array-accumulation pattern
  already used in prod-server.ts (lines 899-907) and the middleware section above.
- worker-utils.ts + deploy.ts template JSDoc: 'Response headers take precedence'
  was misleading — Set-Cookie is additive, not overriding. Clarify both docs.
- tests/deploy.test.ts: add behavioral test for mergeHeaders via worker-utils import
  (same function inlined in the generated template), replacing reliance on
  string-contains assertions alone.

* fix: indentation in deploy.ts template, array-accumulate Set-Cookie in example workers

- deploy.ts: fix extra leading space in config headers block (lines 636-659)
  and JSDoc lines 740-743 introduced in previous commit
- pages-router-cloudflare, realworld-api-rest: config headers section was
  doing a plain assignment (middlewareHeaders[lk] = h.value) which overwrites
  array-valued Set-Cookie built up by the middleware section above; use the
  same array-accumulation pattern as prod-server.ts and the deploy.ts template

* fix: remove as any casts in prod-server.ts, add Vary handling to example workers

- prod-server.ts lines 904/906: middlewareHeaders is now Record<string, string | string[]>
  so the as any casts are unnecessary; replace with as string (consistent with the
  middleware collection section above at line 848)
- pages-router-cloudflare, realworld-api-rest: add Vary comma-joining to config
  headers section to match prod-server.ts (line 908) and deploy.ts template (line 653)

---------

Co-authored-by: James <james@eli.cx>
2026-03-06 23:53:23 +00:00
Divanshu Chauhan (divkix) 04d0086731 fix: inject default viewport meta (width=device-width, initial-scale=1) (#298)
* fix: inject default viewport meta (width=device-width, initial-scale=1)

mergeViewport() started from an empty object, so when a user exported
a viewport config with only themeColor (no width/initialScale), the
merged result was truthy but missing viewport dimensions. ViewportHead
then skipped the <meta name="viewport"> tag entirely, breaking mobile
rendering.

Seed mergeViewport with DEFAULT_VIEWPORT matching Next.js's
createDefaultViewport() defaults: { width: "device-width", initialScale: 1 }.
User-provided values still override these defaults.

* refactor: export DEFAULT_VIEWPORT and reuse at call sites

Export DEFAULT_VIEWPORT from metadata shim and replace the duplicated
inline literals in app-dev-server.ts to keep the default in one place.

* refactor: simplify mergeViewport call sites to always apply defaults

Since mergeViewport() always seeds from DEFAULT_VIEWPORT (even for
empty lists), the `viewportList.length > 0` guards and `?? DEFAULT_VIEWPORT`
fallbacks at both call sites were redundant. Remove them so the default
viewport is applied unconditionally via mergeViewport itself.

Addresses review feedback from james-elicx on PR #298.
2026-03-06 20:24:45 +00:00
Steve Faulkner 9b01dfec65 fix: load .env files into process.env before config evaluation (#234)
Closes #228. vinext was not loading .env files, so environment variables
defined in .env were unavailable to server-side code (getServerSideProps,
API routes, server components) and NEXT_PUBLIC_* vars were missing from
the client bundle defines.

Uses Vite's loadEnv() in the config hook with an empty prefix to load
all .env vars (not just VITE_-prefixed) into process.env before
loadNextConfig() runs, matching Next.js behavior.
2026-03-03 09:19:45 -06:00
Sunil Pai 8a618af9f6 fix: handle malformed percent-encoded URLs gracefully (#107)
Wrap all decodeURIComponent calls on user-controlled URL paths in
try/catch to return 400 Bad Request instead of throwing an uncaught
URIError that crashes the Node process.

A single request with a malformed percent-encoded path (e.g. /%E0%A4%A)
could terminate the entire server process. This affected all server
entry points: prod server (App + Pages Router), dev server middleware,
Cloudflare Worker entry, and generated RSC/middleware handlers.

Fixes:
- prod-server.ts: App Router and Pages Router request handlers
- app-router-entry.ts: Cloudflare Worker entry
- app-dev-server.ts: Generated RSC entry handler
- index.ts: Dev server connect middleware + generated middleware runner
  + generated NEXT_LOCALE cookie parser
- middleware.ts: Pages Router dev middleware runner

Includes 11 regression tests across app-router, pages-router, and
features test suites.
2026-02-26 10:52:57 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00