Commit Graph

1163 Commits

Author SHA1 Message Date
MK (fengmk2) 9d5a2e9a77 chore(deps): bump vite-plus to 0.1.21 (#1190)
- Bump vite, vite-plus, vitest catalog entries to 0.1.21
- Refresh pnpm-lock.yaml
- Regenerate AGENTS.md via vp config postinstall
- Drop unused imports (PHASE_PRODUCTION_BUILD, patternToNextFormat) flagged by upgraded lint rules
- Move react-hooks/rules-of-hooks suppression in tests/e2e/fixtures.ts to the use(errors) call site
- Add react/no-children-prop and react/no-danger-with-children suppressions on the intentional precedence test in tests/head.test.ts
2026-05-14 09:14:31 +01:00
Arpit Jain e33feeb946 ci(benchmarks): pin contents: read (#1188)
The benchmarks workflow uploads the artifact via
actions/upload-artifact (writes only to the run's artifact store, not
the repo) and POSTs results to benchmarks.vinext.workers.dev using
secrets.BENCHMARK_UPLOAD_TOKEN. The GitHub token is only used for the
checkout, so contents: read is sufficient. Top-level style matches
the rest of the repo.
2026-05-13 12:10:46 +01:00
Nathan Nguyen ee261e33eb fix(head): preserve dangerous html during client sync (#1180)
* fix(head): preserve dangerous html during client sync

Client-managed next/head tags currently drop dangerouslySetInnerHTML when hydration or navigation rebuilds vinext-owned head nodes. That diverges from Next.js and can leave SSR-injected script, style, or noscript content empty after the client projection runs.

The client path treated raw HTML as unsafe even though the SSR path already accepts the developer opt-in. Reuse one typed prop application helper for client head nodes and set innerHTML from dangerouslySetInnerHTML with the same precedence as Next.js.

Add regression coverage for client raw HTML application, empty __html fallback, and precedence over children.

* fix(head): handle array children and add children-only test coverage

Address review feedback:
- Handle Array.isArray(children) in _applyHeadPropsToElement by joining
  array elements, matching Next.js reactElementToDOM behavior
- Add test for children-only path (no dangerouslySetInnerHTML) to ensure
  the extracted helper didn't regress the common case
- Add test for array children joining

* fix(head): resolve SSR/client parity gap for empty dangerouslySetInnerHTML

Address second review: dangerouslySetInnerHTML: { __html: '' } was
handled inconsistently between SSR and client. The SSR path used a
truthiness check (if (html)) which skipped empty strings, falling
through to children. The client path checked the prop object (truthy),
so empty __html correctly took precedence.

Fix: restructure headChildToHTML to check dangerouslySetInnerHTML before
children using != null, making both paths consistent with Next.js
semantics where dangerouslySetInnerHTML always wins when present.

* fix(head): unify SSR/client content handling via getDangerouslySetInnerHTML

Address third review round:
- SSR: add Array.isArray(children) handling to headChildToHTML, matching
  the client path for cases like createElement('title', null, 'A', 'B')
- Client: replace truthiness check with getDangerouslySetInnerHTML as
  the gatekeeper (matching SSR pattern). This fixes silent content wipes
  for malformed props like dangerouslySetInnerHTML: 'string' or
  dangerouslySetInnerHTML: {} (missing __html key)
- Both paths now use the exact same entry point and condition shape:
  getDangerouslySetInnerHTML() != null
- Update tests: malformed dangerouslySetInnerHTML: {} now correctly
  falls through to children on both SSR and client

---------

Co-authored-by: James <james@eli.cx>
2026-05-13 11:34:32 +01:00
Nathan Nguyen 2d6978f1f9 fix(link): align app-router prefetch scheduling with intent (#1149)
* fix(link): align app-router prefetch scheduling with intent

Link currently schedules viewport RSC prefetches outside production and does not promote user mouse or touch intent into an immediate prefetch. That diverges from Next.js Link behavior and can waste dev-server work during rapid iteration while missing an important production navigation warmup path.

The shim now gates automatic prefetching to production, shares same-origin prefetch normalization, and schedules high-priority RSC prefetches from mouse and touch intent while preserving user handlers. Focused Link navigation tests cover development suppression, intent prefetch, prefetch=false, and dangerous-link handler passthrough.

* fix(link): align app-router prefetch scheduling with intent

Link currently schedules viewport RSC prefetches outside production and does not promote user mouse or touch intent into an immediate prefetch. That diverges from Next.js Link behavior and can waste dev-server work during rapid iteration while missing an important production navigation warmup path.

The shim now gates automatic prefetching to production, shares same-origin prefetch normalization, and schedules high-priority RSC prefetches from mouse and touch intent while preserving user handlers. Focused Link navigation tests cover development suppression, intent prefetch, prefetch=false, and dangerous-link handler passthrough.

* test: tighten link prefetch coverage

* test(link): tighten prefetch decision coverage

* test(link): address prefetch review notes
2026-05-13 11:33:30 +01:00
James Anderson a27193fcaf chore: upgrade pnpm 10 to 11 and enable frozenLockfile (#1185)
* chore: upgrade pnpm 10 to 11 and enable frozenLockfile

- Bump packageManager from pnpm@10.32.1 to pnpm@11.1.1
- Migrate .npmrc settings to pnpm-workspace.yaml (pnpm 11 only reads auth/registry from .npmrc)
  - auto-install-peers=false -> autoInstallPeers: false
  - Remove playground save-exact=true (minor pnpm add preference, not needed for builds)
- Convert onlyBuiltDependencies list to allowBuilds map (pnpm 11 format)
- Add frozenLockfile: true and preferFrozenLockfile: true
- Delete .npmrc files (contained no auth settings)

* fix: handle pnpm 11 strictDepBuilds, preserve autoInstallPeers, remove minimumReleaseAgeExclude

- Add unrs-resolver: false to allowBuilds (reviewed, deny build scripts)
- Keep autoInstallPeers: false to match existing lockfile
- Remove minimumReleaseAgeExclude entirely
- Append allowBuilds to scaffolded project's pnpm-workspace.yaml after
  create-next-app so subsequent pnpm steps don't abort
2026-05-13 09:15:02 +00:00
Nathan Nguyen d85791c2fb feat(app-router): promote segment reset semantics (#1187)
* feat(app-router): promote segment reset semantics

Implements #726-CORE-11/12 from #726.

Add typed App Router segment state-key derivation, wire template/loading/error/access boundaries to semantic reset keys, and preserve pathname fallback semantics for legacy boundary callers.

Cover dynamic params, route groups, catch-all keys, template reset ownership, and boundary reset behavior.

* fix(app-router): treat empty boundary reset keys as absent

Empty segment identities can be produced for root, route-group-only, and default slot paths. Normalize empty reset keys to null so pathname fallback continues to clear caught boundary state.

* fix(app-router): scope route reset keys to branches

Use typed segment state identities and full visible route-state paths for route-wide wrappers so unrelated branches do not collide on the same leaf value.

Add hostile unit and browser tests for dynamic and static branch collisions.
2026-05-13 10:02:02 +01:00
Yoinky3000 493e72d256 fix(app-router): catch redirect diges, render loading.tsx during cross-route navigation and invalid with calling refresh() (#1152)
* fix(app-router): catch server redirect digest, render loading.tsx across navigation

* Update page.tsx

* fix(app-router): tighten RedirectBoundary digest handling and dedupe nav-signal check

Addresses code-review findings on PR #1152:

- RedirectBoundary.reset() now clears both `redirect` and `redirectType`
  so the state never sits in a half-cleared (`null`, "replace") shape.
- getDerivedStateFromError re-throws on malformed `NEXT_REDIRECT;` digests
  (empty URL segment) instead of silently swallowing the error. Mirrors
  the server-side guard in parseNextRedirectDigest (next-error-digest.ts:51).
- Document the `error.handled` branch as a Next.js parity placeholder so
  it's clear it is not dead code, just unused by vinext today.
- Extract `getErrorDigest` / `isNavigationSignalError` into a shared
  utils/navigation-signal.ts, removing the duplicated digest classification
  in shims/error-boundary.tsx and server/dev-error-overlay.tsx.
- Update stale `snapshotRscResponse` reference in the cached-route comment
  to the current body.tee + createFromFetch flow.
- Strengthen the delayed-redirect-under-loading E2E test to also assert
  the loading.tsx fallback paints during the 50ms server delay.
- Format fix on tests/fixtures/app-basic/app/page.tsx that was failing
  the CI Check job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(app-router): invalidate client RSC caches on router.refresh()

router.refresh() previously only re-fetched the current URL via the
"refresh" navigationKind, leaving the visited-response and prefetch
caches untouched for sibling routes. After a session change (e.g.
authClient.signOut() → router.push("/") → router.refresh()), a
subsequent <Link> click to a previously visited auth-gated route
would render the stale cached RSC payload and bypass the server's
redirect() entirely.

Matches Next.js's refresh-reducer.ts, whose header comment is
explicit: "During a refresh, we invalidate the segment cache but not
the route cache. The segment cache contains the actual RSC data
which needs to be re-fetched." Next does this via an O(1) version
counter (invalidateSegmentCacheEntries → currentSegmentCacheVersion++);
vinext's bounded LRU is small enough that Map.clear() on the
visited-response + prefetch caches is equivalent in observable
behavior.

Pre-existing in upstream vinext (verified against 8edc010); only
surfaced after the redirect-digest fix earlier in this PR, because
before that the unhandled NEXT_REDIRECT digest aborted the page
render outright and masked the stale-cache reuse.

- Expose clearClientNavigationCaches as window.__VINEXT_CLEAR_NAV_CACHES__
- router.refresh() calls it synchronously before scheduling the RSC
  re-fetch inside React.startTransition. Synchronous ordering matters:
  if the clear ran inside the transition, prefetches kicked off during
  the transition's renders could repopulate stale entries into the
  just-cleared cache.
- Unit test pins the clear-before-navigate ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(app-router): address review — remove HandleRedirect reset, fix boundary nesting, add malformed digest test

Address ask-bonk review comments from all three rounds:

1. Remove reset() from HandleRedirect — calling reset() inside startTransition
   clears the boundary state, re-rendering children and re-mounting the page
   that threw redirect(). For deterministic redirects (auth guards) this
   creates an infinite loop. Next.js intentionally does not call reset() in
   HandleRedirect (redirect-boundary.tsx); the boundary stays in its 'caught'
   state rendering null until router.push()/replace() mounts fresh children.

2. Move RedirectBoundary inside Suspense (loading boundary) — Next.js nesting
   is Error > Loading > AccessFallback > Redirect > content. The previous
   placement had RedirectBoundary outside Suspense, which unmounted the
   loading fallback during redirect transitions. Now RedirectBoundary wraps
   the page slot directly, and Suspense wraps both.

3. Add comment documenting dual-format digest parsing — the parts.length >= 5
   branching handles both vinext 3/4-part and Next.js 5-part digests, which
   was non-obvious to reviewers.

4. Add test for malformed-digest re-throw guard — verifies that
   getDerivedStateFromError re-throws when the URL segment is empty
   (e.g. NEXT_REDIRECT;push;) rather than silently swallowing the error.

* fix(app-router): correct nesting-order comment, add error.handled test

Address ask-bonk round-4 review:

1. Fix the boundary nesting comment in app-page-route-wiring.tsx — it
   incorrectly claimed the order matches Next.js's
   Error > Loading > AccessFallback > Redirect > content. The actual
   vinext order is Error > AccessFallback > Loading > Redirect > content;
   the AccessFallback positioning is a pre-existing divergence.

2. Add test for the error.handled branch in RedirectErrorBoundary —
   verifies the Next.js parity placeholder returns null state when a
   redirect error is already marked as handled by an outer boundary.

* refactor: use shared header constants instead of string literals

Replace two hardcoded header strings in the visited-response cache
snapshot with the already-imported VINEXT_MOUNTED_SLOTS_HEADER and
VINEXT_PARAMS_HEADER constants from server/headers.ts.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
2026-05-12 16:48:15 +00:00
James Anderson 996d9b4f3d refactor: extract internal HTTP header names into shared constants (#1184)
* refactor: extract internal HTTP header names into shared constants

Internal header name strings (x-vinext-*, x-middleware-*, x-action-*,
RSC protocol headers, etc.) were scattered across ~30 files as raw
string literals. This made typos possible and renames difficult.

Add packages/vinext/src/server/headers.ts as the single source of
truth for all ~40 internal header name constants, organized into
categories: vinext-proprietary, RSC protocol, Next.js compat,
server action response, middleware protocol, flight headers, and
the INTERNAL_HEADERS security blocklist.

Update all consumers to import from the shared module. Existing
re-exports (VINEXT_RSC_MOUNTED_SLOTS_HEADER, VINEXT_RSC_RENDER_MODE_HEADER
from app-rsc-cache-busting.ts; FLIGHT_HEADERS from app-middleware.ts;
INTERNAL_HEADERS from request-pipeline.ts) are preserved for backward
compatibility.

* refactor: un-export header constants only used in INTERNAL_HEADERS array

knip flagged 6 constants as unused exports. They are only referenced
within the same file by the INTERNAL_HEADERS array, so drop the export
keyword and keep them as module-scoped const declarations.

* refactor: address PR review comments

- server-action-not-found.ts: use rename-import instead of intermediate
  const alias (import { X as Y } pattern)
- request-pipeline.ts: use 'export { INTERNAL_HEADERS } from' re-export
  pattern instead of aliased import + re-assignment, matching the style
  used by other re-exports in this PR

* refactor: import VINEXT_RSC_RENDER_MODE_HEADER from headers.js directly

Move the import in app-rsc-request-normalization.ts from the re-export
in app-rsc-cache-busting.js to the canonical headers.js module.

* refactor: migrate shims/link.tsx to import from headers.js directly

Move VINEXT_MOUNTED_SLOTS_HEADER import from the deprecated re-export
in app-rsc-cache-busting.ts to the canonical headers.ts module. This
was the last production consumer of the VINEXT_RSC_MOUNTED_SLOTS_HEADER
alias, so remove that deprecated export entirely.
2026-05-12 17:17:10 +01:00
dependabot[bot] e9d1749b95 chore(deps): bump next from 16.2.5 to 16.2.6 (#1183)
* bump nextjs

* .

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 14:18:00 +00:00
Nathan Nguyen ac75d43e7e fix(app-router): preserve UI during action refresh (#1147)
* fix(app-router): preserve UI during action refresh

Server action responses currently always render a page payload, and router.refresh can put BrowserRoot into a pending state that mounts loading.tsx while the same route refresh resolves. That diverges from Next.js, where non-revalidating actions can skip page rendering and refresh/revalidation should preserve the current page UI.

The server action path now omits the root payload when no data was invalidated, suppresses loading boundaries for action rerenders, and carries the same suppress-loading variant through RSC request, cache-busting, and ISR cache keys. The browser action callback now resolves no-root action payloads directly and avoids pending router state for refresh.

Regression coverage includes the no-rerender action payload, loading-boundary suppression, cache key variants, cache-busting compatibility, request normalization, and an app-router Playwright case that catches route loading mounts after action plus router.refresh.

* fix(app-router): carry semantic RSC render modes

Replace the cross-layer suppress-loading boolean with AppRscRenderMode so browser requests, request normalization, rendering, cache-busting, Vary, and ISR keys all carry the same semantic payload variant.

Decode server action responses before clearing client navigation caches so no-root return-only action results keep visited response and prefetch caches intact while redirects, root payloads, and legacy bare payloads still invalidate.

Add coverage for refresh() inside server actions and for normal versus preserve-current-UI ISR RSC cache keys.

* fix(app-router): default helper render mode to navigation

* docs: add review-requested comments for structural discriminator and ISR key format

* fix: add explicit null case to parseAppRscRenderMode switch for exhaustiveness check

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 15:09:45 +01:00
Nathan Nguyen 11cc64a3af fix(app-router): preserve useActionState form state after hydration (#1162)
* fix(app-router): preserve useActionState form state after hydration

Progressive multipart server actions executed before the initial HTML render were dropping React form state. That meant useActionState hydrated from the JavaScript initializer instead of the server action result after a no-JS form submission.

The progressive action path now decodes React form state, carries it through the App Router render pipeline, passes it to React's server and client hydration APIs, and marks form-state HTML responses as uncacheable so action results cannot be replaced by stale ISR HTML.

Adds targeted unit coverage for action decoding, SSR forwarding, cache policy, cache read bypass, and a Playwright no-JS useActionState regression.

* fix(app-router): keep progressive action result type internal

Knip reported ProgressiveServerActionResult as an unused exported type. The type is only part of this module's implementation detail for the progressive action return path, so keep it module-local.

* fix(app-router): separate progressive action cache bypass

* chore(app-router): tighten form state bootstrap wiring
2026-05-12 14:25:30 +01:00
dependabot[bot] 477411ae1b chore(deps): bump next-intl from 4.9.1 to 4.9.2 (#1160)
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.9.1 to 4.9.2.
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.9.1...v4.9.2)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.9.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 13:24:21 +00:00
Nathan Nguyen 04140e47a3 feat(app-router): promote same-layout ancestor persistence (#1167)
* feat(app-router): promote same-layout ancestor persistence

App Router soft commits previously preserved absent non-slot entries through the merge layer and kept root-layout hard-navigation policy split between browser state and planner. That made same-layout ancestor persistence implicit and allowed unknown root identity to behave like proof of reuse.

The planner now returns explicit preserveElementIds for proven same-root transitions, browser state stores layoutIds from wire metadata, and visible commits preserve only planner-approved ancestors while leaving parallel-slot persistence semantics intact.

Adds coverage for layout id metadata, unknown-root fallback, stale element removal, and visible layout persistence.

* fix(app-router): validate layout persistence proofs

App Router payload metadata treated every string in __layoutIds as planner preservation authority. That let a bad producer, stale payload, or future refactor pass a page, slot, or malformed key as proof for layout ancestor reuse.

The violated invariant is that layout persistence proof must be validated before it reaches the commit planner. Parse __layoutIds through the AppElements wire-key codec and accept only layout keys.

Also add dynamic segment browser coverage for /blog/[slug] navigation, proving param-sensitive layout and page content update while the layout client counter persists.

* fix(app-router): scope boundary layout proofs

HTTP access fallback payloads derived layout metadata from the full route definition. Layout-level access fallbacks can render only ancestor layouts, so the old proof could let the planner preserve child layouts that were absent from the response.

Derive boundary layout metadata from the rendered layout module subset and mark the root boundary unknown when no route layout is rendered.

* docs(app-router): document layout id ordering

Document the ancestor-first layoutIds invariant at the planner snapshot boundary so future payload producers do not break same-layout prefix persistence.
2026-05-12 14:19:21 +01:00
Nathan Nguyen 682b3f15d7 fix(dynamic): preserve noSSR initial delay state (#1171)
dynamic() currently renders the ssr:false server loading component with pastDelay=true. That disagrees with Next.js noSSR behavior and makes loading components that suppress UI before pastDelay render the opposite state during SSR.

The ssr:false server placeholder now passes pastDelay=false while the existing SSR-enabled and client Suspense fallback paths keep their current loading props. Tests cover both the raw prop contract and the observable null-until-delay loading behavior.
2026-05-12 14:09:58 +01:00
Nathan Nguyen 2e6d2b75c7 fix(app-router): preserve omitted redirect type context (#1172)
redirect() without an explicit type currently reaches shared digest parsing as an empty sentinel, but the parser resolves that sentinel to push before non-action callers can apply their own default. That makes SSR and route-handler redirect handling inherit the Server Action history behavior.

Preserve the omitted type as null at the digest boundary and resolve it to push only when packaging Server Action redirects. Add focused parser coverage for omitted, explicit, and non-empty raw type segments.
2026-05-12 14:08:37 +01:00
Nathan Nguyen 142f128f72 fix(headers): make draft mode status live (#1174)
* fix(headers): make draft mode status live

draftMode().isEnabled captured the initial cookie state when the request API object was created. That diverged from Next.js, where DraftMode.isEnabled is a getter over mutable provider state, so reading the same object after enable() or disable() reflects the updated draft mode status.

Keep vinext's existing request cookie map as the draft mode provider and expose isEnabled as a getter over that map. Add focused regression tests for same-object enable and disable reads.

* remove unnecessary headersContext local const — state is ALS-isolated per request

* merge duplicate draftMode isEnabled tests into existing enable/disable tests

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 13:07:38 +00:00
Nathan Nguyen d602a8fe6c fix: handle basePath hash-only page navigations (#1173) 2026-05-12 14:06:15 +01:00
Nathan Nguyen e3e4a05175 fix(app-router): preserve config headers on middleware redirects (#1175)
App Router middleware redirects skipped next.config.js headers because config header application only happened during final response finalization. Redirect responses bypass that finalizer, so matching config headers disappeared whenever middleware short-circuited with a redirect.

Apply the matched config header layer specifically for middleware redirects using the pre-middleware request context, then merge the redirect response over it so middleware headers keep precedence. Non-redirect middleware responses continue through finalization, with regression coverage to prevent duplicate additive headers.
2026-05-12 13:55:29 +01:00
Nathan Nguyen 08da5c32e9 fix(app-router): error truncated progressive RSC streams (#1176) 2026-05-12 13:53:04 +01:00
Nathan Nguyen 4b3af442a1 fix(dynamic): accept Next.js dynamic overload forms (#1179)
vinext previously treated the first next/dynamic argument as a callable loader. That broke valid Next.js call shapes such as a direct import promise and an options object with loader because React.lazy eventually tried to call a non-function value.

The shim now normalizes the public dynamic input before rendering: promises become loader functions, function loaders stay unchanged, and options objects merge with the second options argument. Focused tests cover the legacy promise form and the options-object loader form against SSR output.
2026-05-12 13:51:09 +01:00
Deepam Goyal 97f878ac94 fix: remove duplicate next-shims.d.ts from fixtures (#1170)
* fix: remove duplicate next-shims.d.ts from fixtures

Signed-off-by: Deepam Goyal <deepam02goyal@gmail.com>

* fix: use type intersections

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

* fix: remove duplicate next-shims.d.ts from examples

Apply the same consolidation from fixtures to examples/realworld-api-rest
and examples/pages-router-cloudflare. Point their tsconfigs at the shared
packages/vinext/src/shims/next-shims.d.ts instead of keeping local copies.

* fix: declare Link value before default export in next/link shim

The deleted fixture copies all had `const Link: ComponentType<LinkProps>`
before the default export. Without it the default import resolves to `any`
(masked by skipLibCheck). Add the missing value declaration.

---------

Signed-off-by: Deepam Goyal <deepam02goyal@gmail.com>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: James <james@eli.cx>
2026-05-12 11:24:55 +00:00
Nathan Nguyen 826bab4530 feat: implement next web vitals hook (#1161) 2026-05-11 17:15:12 +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 eb3295b8c4 fix(dynamic): pass complete loading props (#1163)
vinext only passed isLoading, pastDelay, and error to next/dynamic loading components. Next.js includes retry and timedOut, so loading UIs that destructure those props observed an incompatible shape. The shim assumed the loading prop contract was limited to the values it currently used.

This aligns the runtime and ambient shim declarations with Next.js by passing the full loading props shape, preserving current delay defaults, and wiring client retry through a replaceable lazy component. The regression test covers the SSR loading object shape.
2026-05-11 16:51:52 +01:00
Nathan Nguyen a75ec7c3f8 fix(app-router): append RSC vary headers in finalizer (#1165)
App Router responses can vary by RSC navigation headers. Route handlers returned custom responses through the outer finalizer without the shared RSC Vary set, so caches could reuse response variants across navigation modes.

The finalizer now merges the shared VINEXT_RSC_VARY_HEADER for non-redirect, non-static-file App Router responses. Existing page helpers that already set it are deduped by the shared Vary merge helper.

Tests cover route handler finalization, custom Vary preservation, duplicate avoidance, wildcard Vary, and static-file signal exclusion.
2026-05-11 16:45:26 +01:00
Nathan Nguyen 71acce4243 fix(headers): reuse request API promise identity (#1164)
headers() and cookies() decorated a fresh Promise on every call even when they returned the same request headers or cookie store. React.use() tracks thenables by identity, so repeated calls in one request could look like uncached request API promises.

Cache the decorated thenable by the sealed request API object, matching Next.js's WeakMap-shaped contract while preserving vinext's existing middleware invalidation path. Add regression coverage for stable same-context identity and fresh identities after middleware request header overrides.
2026-05-11 16:45:19 +01:00
Nathan Nguyen 77c4d9ce84 feat(app-router): plan root-boundary navigation decisions (#1114)
* feat(app-router): plan root-boundary navigation decisions

* test(app-router): cover unknown visible root planning

* test(app-router): cover stale root-boundary precedence

* fix(app-router): thread target href into navigation approval

Root-boundary hard-navigation planning could derive its decision URL from the pending route snapshot even though the browser executor navigated to the controller target href. That left planner traces able to disagree with the actual hard navigation target.

Require targetHref at visible commit approval boundaries, pass the controller target through navigation and same-url server-action approvals, and record it in lifecycle traces. The focused app-browser tests now assert the planner trace carries the executor URL for hard navigation decisions.

* docs(app-router): note route manifest planner follow-up

The planner input already carries routeManifest for the issue 726 route-graph slices, but CORE-07/08 always passes null and does not read it. Without a comment, the reserved field looks accidentally unused.

Document that routeManifest becomes non-null in CORE-09 and loosen the disposition test helper so render identity can diverge from navigation identity in future coverage.
2026-05-11 13:30:45 +01:00
Divanshu Chauhan (divkix) 3a9046dce2 fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections (#1153)
* fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections

Ports Next.js commit 5452439f3db2a78967178ca4180b27fb48393a19 (PR #91686)
to vinext's next/image shim and server-side image config.

Changes:
- Add isPrivateIp() helper in shims/image-config.ts (IPv4 + IPv6, no deps)
- Update validateRemoteUrl in shims/image.tsx to reject literal private-IP
  hostnames unless dangerouslyAllowLocalIP is true
- Wire dangerouslyAllowLocalIP through next-config type, Vite define,
  global.d.ts, and server-side ImageConfig
- Add unit tests for isPrivateIp and component-level private-IP guard

Closes #1065

* fix(image): use ipaddr.js for correct private-IP classification

Addresses review feedback on the SSRF guard:
- Replaces the hand-rolled isPrivateIp with ipaddr.js (same library Next.js
  uses), eliminating bugs in IPv4/IPv6 range checks
- Fixes incorrect 2001:2f::/32 check (not a real range) and adds all missing
  non-unicast ranges: CGNAT, multicast, reserved, benchmarking, teredo,
  documentation, discard, NAT64
- Correctly handles ::ffff: IPv4-mapped addresses via isIPv4MappedAddress()
- Adds explanatory comments for best-effort client-side guard and
  dangerouslyAllowLocalIP server-side config
- Adds comments in tests explaining the vi.resetModules() pattern

* fix(image): externalize ipaddr.js to prevent SSR crash

ipaddr.js is a CJS UMD module that assigns to `this.ipaddr` at the
bottom of its wrapper. When Vite's SSR module runner loads it with
`noExternal: true`, the UMD wrapper executes with `this = undefined`
(strict mode in ESM context), producing:

  TypeError: Cannot set properties of undefined (setting 'ipaddr')

This crashed every page render that imports `next/image` (via the
image-config.ts shim). The image-optimization-parity tests failed
with 500 errors.

Fix: add `ipaddr.js` to `ssr.external` in all 3 Vite environment
configs (top-level, RSC, SSR) so Node's native loader handles the
CJS module correctly.
2026-05-11 13:21:02 +01:00
James Anderson 0c4e528554 chore: build vinext on postinstall (#1159) 2026-05-11 09:46:31 +01:00
Nathan Nguyen 207246320e test: harden ecosystem fixture startup (#1146)
Co-authored-by: James <james@eli.cx>
2026-05-11 07:23:14 +00:00
Nathan Nguyen a0737e971b feat(router): expose RouteManifest default and segment boundary facts (#1136)
* feat(router): expose RouteManifest default and segment boundary facts

RouteManifest slot facts only exposed whether a default existed, and boundary facts were limited to layout-aligned outcomes. That left later #726 planner work without graph-owned IDs for default-rendering slot bindings or layoutless route-group error segments.

Add explicit default entries keyed by semantic slot ID, thread default IDs through slot declarations and default-rendering bindings, and materialize segment-level error boundaries even when no sibling layout owns that segment.

Validation: vp test run tests/app-route-graph.test.ts; vp test run tests/app-route-graph.test.ts tests/routing.test.ts; vp check packages/vinext/src/routing/app-route-graph.ts tests/app-route-graph.test.ts; vp run vinext#build.

* ci: rerun flaked checks

* ci: rerun flaked checks again

* fix(router): avoid duplicate RouteManifest error boundary writes

RouteManifest now exposes layoutless segment error boundaries, but the segment pass also revisited layout-bearing error.tsx files that the layout pass had already written.

Skip segment error entries with an owning layout so layout-owned boundaries remain single-sourced, while layoutless route-group boundaries still materialize as ownerless facts.

Validation: vp test run tests/app-route-graph.test.ts; vp check packages/vinext/src/routing/app-route-graph.ts tests/app-route-graph.test.ts.
2026-05-11 08:21:22 +01:00
Divanshu Chauhan (divkix) 99cc979e75 feat(server): add waitForAllReady forkpoint for prerender/ISR parity (#1011) (#1156)
* feat(server): add waitForAllReady forkpoint for prerender/ISR parity

Mirrors Next.js PR #93376: adds a waitForAllReady option that defers
emitting the HTML stream until React's onAllReady fires, ensuring the
full tree (including resolved Suspense boundaries) is present before
serialization.

Changes:
- app-ssr-entry.ts: accept waitForAllReady and await stream.allReady
- app-page-stream.ts: thread waitForAllReady through the pipeline
- app-page-render.ts: pass waitForAllReady: isPrerender to the HTML render

Closes #1011

* test(app-page-stream): use toHaveBeenCalledWith for waitForAllReady forwarding

Replaces the positional-tuple type cast with expect.objectContaining,
which reads more clearly and stays robust if handleSsr's signature
gains new positional args.

---------

Co-authored-by: James <james@eli.cx>
2026-05-11 07:15:18 +00:00
Divanshu Chauhan (divkix) d4e7d83f86 fix: stream Suspense fallbacks during draft mode in App Router (#1158)
## Summary
- Skip static generation context setup for draft mode requests in app-page-dispatch.ts, matching Next.js behavior where draft mode opts into dynamic HTML rendering.
- Port test fixture and streaming test from Next.js PR #93417 to verify draft-mode pages stream their loading.tsx / Suspense fallbacks.

## Test plan
- Added streaming test to tests/nextjs-compat/draft-mode.test.ts
- Added fixture pages at tests/fixtures/app-basic/app/nextjs-compat/draftmode/streaming/
- Verified draft-mode requests show Suspense fallback text in HTML response

Fixes #1009
2026-05-11 08:14:51 +01:00
Nathan Nguyen 3adf2da4c4 chore(lint): enable stricter promise and error rules (#1148)
* chore(lint): enable stricter promise and error rules

The lint config did not enforce several promise, exhaustive switch, template expression, self-import, and error-construction checks on source files. That let void callback promise boundaries and empty Error fixtures slip through review.

Enable the rules for vinext source files, apply the useful test subset, and make the existing async callback boundaries explicit. Also remove the redundant unknown union and close response bodies in the affected Pages Router tests so server teardown remains deterministic.

* chore(lint): require exhaustive test switches

Test files already enforce strict async callback and error construction checks, but exhaustive switch checking was source-only.

Enable switch exhaustiveness for tests and make the Pages Router response decoder handle the undefined encoding case explicitly. Tighten the allowedDevOrigins suite cleanup so the temporary dev server closes deterministically.
2026-05-11 08:13:00 +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 108d913bfe fix(app-router): schedule link navigations as transitions (#1133)
* fix(app-router): schedule link navigations as transitions

App Router link clicks could commit cross-route payloads synchronously because the browser entry passed route path equality into the navigation controller as a transition toggle. That diverged from Next.js, where app-router push and replace enter React.startTransition for every app navigation.

Remove the transition override from normal navigation payload commits, keep synchronous commits private for HMR and same-URL server-action handling, and start RSC Link navigations through the same programmatic transition path used by useRouter().

* test(app-router): cover link transition scheduling behavior

* docs(navigation): drop stale isSameRoute references in comments

The isSameRoute concept was removed from app-browser-entry.ts in this PR;
update two comments in navigation.ts that still referenced it.

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 21:43:08 +01:00
Nathan Nguyen 671d782062 fix(cache): scope use cache keys by deployment id (#1092)
* fix(cache): scope use cache keys by deployment id

Shared "use cache" entries only used the vinext build ID as their cache-key seed. That diverges from the upstream Next.js change for cacheHandlers and can let a Cloudflare Worker deployment reuse entries from another deployment when the deployment identity changes independently of the build fallback.

The cache runtime now resolves a deployment-aware key seed at execution time, with request-scoped Worker version metadata or NEXT_DEPLOYMENT_ID taking precedence over the build ID. Generated Wrangler config includes the CF_VERSION_METADATA binding, and the App Router Worker entry threads it into the runtime without process-global request races.

Tests cover deployment ID precedence, concurrent deployment scope isolation, build ID fallback, and generated Worker config wiring.

* fix(config): resolve deployment id for cache seeds

Deployment-aware "use cache" keys had runtime Worker and NEXT_DEPLOYMENT_ID support, but vinext did not resolve the Next.js deploymentId config field. That left config-defined deployment identity out of the build-time cache seed.

Resolve deploymentId from next.config.js before NEXT_DEPLOYMENT_ID, validate the same string shape Next.js documents, and feed the resolved value into the __VINEXT_DEPLOYMENT_ID define. The process-wide cache-runtime setter is also documented as a fallback rather than the per-request API.

Tests cover config precedence, env fallback, empty config values, and invalid deploymentId shapes.

* refactor(cache): carry deployment id through request context

The Worker version metadata path still needs request-scoped state, but cache-runtime should not own a cache-specific deployment ALS or public-looking setter.

Move runtime deployment ID storage into the unified request-context layer. The Worker boundary now writes deploymentId into request runtime context, cache-runtime reads that generic request metadata before falling back to inlined deployment/build IDs, and the tests cover direct runtime scopes plus inheritance into unified request contexts.

* refactor(cache): reuse request context for deployment id

* test(app-elements): expect artifact compatibility metadata

* refactor(cache): inline deployment id seed

* chore: rerun ci
2026-05-08 21:41:46 +01:00
James Anderson 15cff6dd75 fix(cache): encode non-ASCII characters in cache tags at construction (#1143)
Tags with non-ASCII characters (Hebrew, Arabic, CJK, emoji) can crash ISR
responses with ERR_INVALID_CHAR when written to x-next-cache-tags, and
silently break invalidation when storage form diverges from the form
revalidateTag/revalidatePath produce.

Add encodeCacheTag/encodeCacheTags helper (matches behavior of
vercel/next.js#93601) and apply it at every public boundary: cacheTag,
revalidateTag, revalidatePath, updateTag, unstable_cache options.tags,
fetch next.tags, and the path-derived _N_T_ tag builders.

Closes #1138
2026-05-08 20:36:42 +00:00
James Anderson fd2a8bb66a feat(cache): export stable io from next/cache, deprecate unstable_io (#1142)
* feat(cache): export stable `io` from next/cache, deprecate `unstable_io`

Next.js stabilized `unstable_io` as `io` in vercel/next.js#93621 without a
deprecation alias. vinext shipped the unstable name longer than upstream
canaries (since #805), so keep `unstable_io` as a transitional alias that
emits a one-time deprecation warning.

Closes #1139

* chore: apply formatter
2026-05-08 21:31:52 +01:00
Nathan Nguyen ba21f50d7c fix(app-router): emit action revalidation headers (#1117)
* fix(app-router): emit action revalidation headers

Server Actions that call revalidatePath, revalidateTag, updateTag, refresh, or mutate cookies currently return RSC responses without x-action-revalidated. The client router therefore keeps stale cached payloads after an action invalidates data.

The action execution path assumed cache invalidation only needed to touch the server-side cache handler. Record action-scoped revalidation in the cache shim, consume it when building action responses, and preserve Next.js' static-and-dynamic versus dynamic-only header values.

Tests cover normal action rerenders, redirecting actions, and refresh dynamic-only invalidation.

* fix(app-router): match profiled tag action revalidation

Server Actions using revalidateTag with a cacheLife profile should not trigger x-action-revalidated. Next.js treats profiled tag revalidation as stale-while-revalidate and does not force immediate client router cache invalidation.

Only record action revalidation for the no-profile revalidateTag form, keep the action marker cleanup symmetric on error, and cover the profiled tag case in server action tests.

* fix(app-router): notify actions for immediate tag expiry

Server Actions that call revalidateTag with an inline profile of expire zero immediately expire server cache data, but the client router still needs x-action-revalidated to drop stale RSC payloads.

Resolve revalidateTag profiles before deciding whether to mark action revalidation, matching Next.js' no-profile or expire-zero condition. Also clear the action marker on progressive action errors for symmetry with the RSC action path.

* fix(app-router): notify on revalidateTag with unknown profile

Address review feedback:
- revalidateTag with an unrecognized profile string resolves to no
  durations and the handler treats it as a full invalidation, but the
  client router was not being notified. Mark action revalidation when
  durations is missing too.
- Document the cookie override path in resolveActionRevalidationKind so
  future readers see why it bypasses the max-escalation pattern used in
  markActionRevalidation.

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 20:30:13 +00:00
Nathan Nguyen ae151bb220 fix(app): preserve middleware headers on cached pages (#1127)
* fix(app): preserve middleware headers on cached pages

* fix(app): preserve middleware status on cached pages
2026-05-08 21:25:34 +01:00
Nathan Nguyen 2068b8c343 perf(prerender): reuse embedded RSC payload (#1097)
* perf(prerender): reuse embedded RSC payload

* fix(prerender): address RSC extraction review

* fix(test): include artifact compatibility metadata

* fix(prerender): document RSC extraction invariants

* refactor(prerender): simplify RSC extractor and remove dead fallback

Cleanups on top of #1097:

1. Drop the legacy `__VINEXT_RSC__` extractor. The browser entry still
   recognizes that format for in-the-wild cached HTML, but prerender
   parses HTML it just emitted in this build, where the writer always
   emits the chunked form. The legacy branch is unreachable here.

2. Remove the silent fallback to a second `RSC: 1` invocation. The
   writer is in-tree, so a missing-marker case is a vinext-internal
   regression we want to surface loudly rather than mask by
   double-rendering. `extractRscPayloadFromPrerenderedHtml` now
   returns `string` and throws on any anomaly.

3. Replace the manual JSON-string tokenizer (parseEmbeddedJsonString +
   assertOnlyTrailingSemicolon + isWhitespace, ~80 lines) with a
   ~12-line slice + JSON.parse. Relies on the same safeJsonStringify
   invariant the original parser relied on; tampering with trailing
   code now surfaces as 'invalid chunk JSON' instead of a separate
   diagnostic, which is fine for an internal protocol.

4. Remove the obsolete TODO marker ("Extract RSC payload from the
   first response instead of invoking the handler twice") — the PR
   resolves it.

7. Tighten the chunk-presence assertion: empty HTML or HTML with only
   the done marker now throw 'no chunk scripts found in HTML' instead
   of returning the fallback sentinel.

Test updates mirror these:
- Drop the two legacy-format tests.
- Drop the "falls back to a separate RSC request" integration test
  (the fallback no longer exists).
- Update error-message assertions where the diagnostic text changed.

* fix(prerender): fall back to second invocation for non-render HTML responses

The previous cleanup made `extractRscPayloadFromPrerenderedHtml` throw
on any HTML lacking RSC embed chunks. That's wrong for one legitimate
case: middleware that short-circuits the App Router pipeline with a
custom 200 HTML response — that HTML never went through
createRscEmbedTransform, so it has no chunks and no done marker.

Distinguish two cases at the extractor level:
- No chunks AND no done marker → middleware short-circuit. Return null
  so the caller falls back to a second invocation with `RSC: 1`.
- Done marker present without chunks → real bug, throw.
- Chunks present without done marker → real bug, throw.
- Malformed/tampered chunk JSON → real bug, throw.

The driver checks for null and recovers by issuing a second handler
invocation with RSC headers, writing whatever that returns as the
.rsc file.

Adds an integration test alongside the existing dedup test:
- Middleware short-circuit: server returns 200 HTML without chunks for
  page requests, valid Flight for RSC: 1 → driver writes the fallback
  payload as .rsc, with exactly one of each request kind.

* fix(prerender): fail non-ok RSC fallback responses

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 21:23:28 +01:00
Morty 7e77fb5d25 fix: add shell: true on Windows for execFileSync calls (#742)
* fix: add `shell: true` on Windows for execFileSync calls

On Windows, `execFileSync` cannot execute `.bin/wrangler` (a shell
script) or package manager commands like `npm`/`npx` (which are `.cmd`
wrappers) without enabling the shell. This causes `ENOENT` errors
during `vinext deploy`, `vinext build`, and `vinext lint`.

Fix by adding `shell: process.platform === "win32"` to all
`execFileSync` options. This only enables the shell on Windows where
it's required, preserving the no-shell-injection guarantee on other
platforms.

Fixes `spawnSync ENOENT` on Windows for:
- `runWranglerDeploy` (deploy.ts)
- `installDeps` (deploy.ts)
- React upgrade in `deploy()` (deploy.ts)
- React upgrade in `buildApp()` (cli.ts)
- Lint commands in `lint()` (cli.ts)

* style: break long execFileSync lines to fit printWidth: 100

* fix: use ExecFileSyncOptions for execFileSync opts type

ExecSyncOptions.shell is typed as `string | undefined`, while
ExecFileSyncOptions.shell is `boolean | string | undefined`. The
explicit annotation rejected the new `shell: process.platform === "win32"`
boolean. Switching to the matching options type resolves the TS2322
error reported by `vp check` in CI.

* docs: update execFileSync comment for cross-platform shell behavior

Reflects shell: true on win32 — the array form still prevents trivial
injection, but the older comment implied shell-free execution everywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:21:27 +01:00
Nathan Nguyen 37ca6be63b fix(app-router): isolate middleware request bodies (#1132)
Middleware could consume the same ReadableStream that downstream App Router handling later needed to read. This broke POST route handlers when middleware inspected request.json() and then returned NextResponse.next().

The root cause was constructing middleware NextRequest instances from the caller-owned Request body. Clone the request body branch before wrapping or pathname-normalizing middleware requests, and cover both the constructor and middleware pipeline behavior with regression tests.
2026-05-08 21:18:10 +01:00
James Anderson 0f05e53f1f bump setup-vp (#1135) 2026-05-07 14:35:08 +00:00
Nathan Nguyen 937f5d6a09 fix(app-router): read indefinite app page cache entries (#1129)
* fix(app-router): read indefinite app page cache entries

App pages with revalidate=false currently normalize to Infinity, but the app-page cache read gate excludes Infinity. Pre-rendered static app pages can be seeded into the ISR cache and still miss that cache on every production request.

The cache read policy now treats positive Infinity as read-eligible while preserving force-dynamic, production, RSC, and nonce guards. Cached indefinite responses are normalized to the same static one-year Cache-Control header used by fresh revalidate=false responses, so cache hits do not emit s-maxage=Infinity.

Tests cover the dispatch cache hit path and the shared cached Cache-Control helper.

* chore: rerun ci

* chore: rerun ci
2026-05-07 11:23:28 +00:00
James Anderson ea096701da ci: pin pnpm in cna-test to vinext's package.json version (#1131)
The create-next-app job was failing with ERR_PNPM_UNEXPECTED_STORE.
create-next-app's bundled pnpm (10.x) installs node_modules linked to
the v10 store; vp's bundled pnpm recently bumped to v11, which then
refuses to add packages to the v10-linked node_modules.

Anchor the scaffolded project to vinext's own pinned pnpm version
(read from root package.json's packageManager field) and invoke that
pnpm directly via npx for the install step. This:

- keeps store layouts aligned between create-next-app's install and
  the subsequent vinext add
- stays in sync with future pnpm bumps without further CI edits
- bypasses vp's bundled pnpm only for this layering step; the rest of
  the job (vp exec, vp dlx) is unchanged
2026-05-07 11:14:19 +00:00
Nathan Nguyen 04368ebbcb fix(middleware): preserve credentials for external override rewrites (#1121)
Middleware request-header overrides could rebuild forwarded external rewrite requests from only the override set. That drops cookie and authorization when middleware reports a partial override, so proxied upstream requests lose credentials.

Keep Next.js-compatible deletion semantics by default, and make credential preservation an explicit opt-in for external proxy boundaries. Cover both partial preservation and explicit credential deletion.
2026-05-07 11:24:04 +01:00
Nathan Nguyen 38e6b9f6c2 fix(app-router): bypass app page cache in draft mode (#1123)
App Page ISR could serve a cached production response to a request carrying a valid __prerender_bypass cookie. That violates Next.js draft-mode semantics because preview users must render dynamically and must not populate shared page cache entries.

The dispatch layer now identifies draft-mode requests with the same cookie parser and secret used by draftMode(), skips App Page cache reads, and passes that state into render response policy. HTML and RSC render paths now treat draft mode as no-store and avoid cache capture/write work.

Tests cover valid draft cookies bypassing cached HTML, arbitrary cookie values preserving cache hits, and response-policy no-store behavior.
2026-05-07 11:23:33 +01:00
Nathan Nguyen a4497f2bf8 fix(actions): return HTTP fallback status for RSC action notFound (#1122)
RSC fetch actions that threw notFound() or another HTTP access fallback still returned a Flight payload with status 200. That diverged from Next.js, which sets the response status to the access fallback status before generating Flight for fetch actions.

The server action path now extracts the fallback status while preserving the action error in the Flight return value. The regression test covers legacy NEXT_NOT_FOUND, modern NEXT_HTTP_ERROR_FALLBACK;404, and another fallback status.
2026-05-07 11:23:03 +01:00