476 Commits

Author SHA1 Message Date
Nathan Nguyen fb342f5ed6 refactor(app-rsc): move request lifecycle into typed handler (#1036)
* refactor(app-rsc): introduce app-rsc-request-normalization module

The RSC entry handler inlined all URL parsing, pathname decoding, basePath
checking, RSC detection, and header sanitization directly in its
_handleRequest template. That made the logic impossible to unit-test, hard to
audit for security, and duplicated normalizeMountedSlotsHeader in three places.

Create server/app-rsc-request-normalization.ts to own the full normalization
pipeline as a single, typed function (normalizeRscRequest). The function
returns a discriminated Response | NormalizedRscRequest, encoding early exits
(400/404) in the type rather than as throw-then-catch pairs.

The ordering within the function is security-critical and documented step by
step:
  1. Parse URL
  2. Protocol-relative guard (on raw pathname, before normalizePath collapses //)
  3. Strict segment-wise percent-decode (malformed encoding -> 400)
  4. normalizePath (collapse //, resolve . and .. segments)
  5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses)
  6/7. RSC detection and cleanPathname
  8. Sanitize X-Vinext-Interception-Context (strip null bytes)
  9. Normalize x-vinext-mounted-slots (dedup, sort)

Consolidate normalizeMountedSlotsHeader: the function existed independently in
isr-cache.ts and as a private copy in app-elements.ts. Both now import from
the new module. isr-cache.ts re-exports it for callers already importing from
there.

43 new behavior tests cover the security-sensitive paths: protocol-relative
variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath
enforcement, %2F preservation, RSC detection by suffix and Accept header,
interception context sanitization, and mounted-slots normalization.

* refactor(app-rsc): wire normalizeRscRequest into the entry and callsites

Replace the open-coded normalization block in the generated _handleRequest
function with a single normalizeRscRequest call. The entry now delegates
all security-sensitive preprocessing to the typed module rather than
repeating it in a template string.

Remove the local normalizeMountedSlotsHeader implementation from isr-cache.ts
(was a standalone export) and remove the private copy from app-elements.ts.
Both now import from app-rsc-request-normalization.ts. isr-cache.ts re-exports
the function so existing callers that import from there are unaffected.

Generated entry changes:
- Add import for normalizeRscRequest from the new module
- Remove guardProtocolRelativeUrl and stripBasePath from requestPipeline import
- Remove normalizePathnameForRouteMatchStrict from routingUtils import
- Remove normalizeMountedSlotsHeader from isrCache import
- Replace ~20-line normalization block with 4-line call + destructure

* refactor(app-rsc): move request lifecycle into typed handler

The generated App Router RSC entry still owned request normalization, config routing, middleware handoff, public-file serving, action dispatch, fallback routing, and response finalization. That made lifecycle behavior hard to test directly and forced generator assertions to reach into runtime sequencing.

The violated invariant was that codegen should describe the app shape while normal modules should implement behavior. This extracts the request lifecycle into createAppRscHandler, leaves the generated entry responsible for route-specific manifests and dispatch closures, and adds behavior tests for redirects, rewrites, public files, actions, route handlers, and fallback cleanup.
2026-05-03 19:32:45 +01:00
Nathan Nguyen 54a1286839 fix(app): apply layout segment config to pages (#993)
* fix(app): apply layout segment config to pages

* fix(app): address segment config review nits
2026-05-03 18:57:48 +01:00
Nathan Nguyen 01b64a1f71 refactor(app-rsc): extract response finalisation into server/app-rsc-response-finalizer (#1035)
* refactor(app-rsc): introduce app-rsc-request-normalization module

The RSC entry handler inlined all URL parsing, pathname decoding, basePath
checking, RSC detection, and header sanitization directly in its
_handleRequest template. That made the logic impossible to unit-test, hard to
audit for security, and duplicated normalizeMountedSlotsHeader in three places.

Create server/app-rsc-request-normalization.ts to own the full normalization
pipeline as a single, typed function (normalizeRscRequest). The function
returns a discriminated Response | NormalizedRscRequest, encoding early exits
(400/404) in the type rather than as throw-then-catch pairs.

The ordering within the function is security-critical and documented step by
step:
  1. Parse URL
  2. Protocol-relative guard (on raw pathname, before normalizePath collapses //)
  3. Strict segment-wise percent-decode (malformed encoding -> 400)
  4. normalizePath (collapse //, resolve . and .. segments)
  5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses)
  6/7. RSC detection and cleanPathname
  8. Sanitize X-Vinext-Interception-Context (strip null bytes)
  9. Normalize x-vinext-mounted-slots (dedup, sort)

Consolidate normalizeMountedSlotsHeader: the function existed independently in
isr-cache.ts and as a private copy in app-elements.ts. Both now import from
the new module. isr-cache.ts re-exports it for callers already importing from
there.

43 new behavior tests cover the security-sensitive paths: protocol-relative
variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath
enforcement, %2F preservation, RSC detection by suffix and Accept header,
interception context sanitization, and mounted-slots normalization.

* refactor(app-rsc): extract response finalisation into typed helper

Config header application runs inline in the generated RSC entry handler()
as a template string with no tests. Every response variant (page, route
handler, server action, not-found) goes through handler(), but the shared
finalisation logic cannot be tested or reused — a classic rot factory.

Create server/app-rsc-response-finalizer.ts that owns:
- Skip 3xx redirect responses. Response.redirect() produces immutable Headers
  that throw on write; Next.js also deliberately excludes config headers from
  redirects.
- Normalize the request pathname (segment-wise percent-decode + collapse)
  before matching config header source patterns.
- Strip basePath at a segment boundary before matching. The old inline code
  used pathname.startsWith(basePath) which incorrectly matched /app2/page when
  basePath is /app; stripBasePath() enforces the separator boundary.
- Apply matching config headers via applyConfigHeadersToResponse, preserving
  middleware-set header precedence.

The generated entry now calls __finalizeAppRscResponse(response, request, {...})
in place of ~15 lines of template logic, and drops the now-unused
applyConfigHeadersToResponse and normalizePathnameForRouteMatch imports from
the generated code.

Tests cover: header applied to 200, empty configHeaders no-op, source
pattern miss, immutable 307 no throw, mutable 308 header skipped, basePath
strip, basePath segment-boundary (regression for startsWith bug), nested
basePath, has-condition match, has-condition miss.
2026-05-03 18:18:32 +01:00
James Anderson 3cd2bfc2be fix(routing): match inherited slot params against routePath, not request.url (#1040)
Follow-up to #1039. Two related polish items in the inherited-slot
mirror runtime:

1. Bug under `basePath`: `buildSlotOverrides` was parsing `request.url`
   to derive the URL parts to match against `slot.slotPatternParts`. The
   original request URL still carries the configured `basePath`, but
   `slotPatternParts` is built from app-relative segments only — so for
   every basePath-configured app, the pattern match silently failed and
   slot params fell back to the route's matched params. Use the already
   normalized `routePath` parameter (basePath stripped, RSC suffix
   removed) instead. Added a unit test that exercises the regression.

2. Memoize `findSlotSubPages` per build via a `WeakMap` keyed by the
   matcher. Inherited slots get scanned once per descendant route, so
   without memoization a route N segments deep paid O(N) full subtree
   walks for every shared ancestor slot. Cache lifetime tracks the
   matcher's, so a long-lived dev server doesn't accumulate state across
   builds.

Also dedupes a redundant `convertSegmentsToRouteParts` call inside
`findMirroredSlotPage` (both tiers needed the same conversion).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:15:03 +00:00
James Anderson fffda16106 feat(dev): React-based runtime error overlay (#1029)
Surfaces three error sources that previously only reached the console:
React render errors caught by an error.tsx boundary (onCaughtError),
render errors with no boundary above them (onUncaughtError), and plain
script errors / unhandled promise rejections via window listeners.

The overlay renders into a separate createRoot mounted on a detached div
appended to body, so it survives an unmount of the main hydrateRoot tree
— necessary because most errors we want to surface take that tree down.

Includes a /dev-overlay-test fixture and Playwright spec covering all
three error sources, dismiss, and prev/next pagination across multiple
errors.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:12:03 +01:00
James Anderson 2cf21433cb fix(routing): mirror inherited parallel slots to descendant sub-pages (#1039)
* fix(routing): mirror inherited parallel slots to descendant sub-pages

discoverInheritedParallelSlots was hard-coding pagePath to null for any
ancestor slot, so inherited slots only ever rendered default.tsx — even
when an obvious mirror like @breadcrumbs/about/page.tsx existed for the
matching route. Track each ancestor's segment index, then look up a
mirror via either a literal filesystem match or, when route groups appear
on either side, by URL-parts comparison through convertSegmentsToRouteParts.

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

* fix(routing): extract slot-specific params for inherited mirrors

When an inherited parallel slot's mirrored sub-page declares dynamic
markers under different names than the route's (e.g. slot uses
@breadcrumbs/distinct/[name] for a route at distinct/[id]), the runtime
was passing the route's matchedParams through unchanged — so the slot
page received {id} instead of {name} and rendered with an empty value.

Three pieces:

* `findMirroredSlotPage` now also accepts pattern-compatible matches and
  returns the slot's URL parts and param names. Specificity scoring
  picks the most-specific compatible sub-page (literal > dynamic >
  catch-all).
* Each ParallelSlot carries optional `slotPatternParts` and
  `slotParamNames`, serialized through the manifest.
* `buildPageElements` extracts a per-request `slotOverrides[key].params`
  by matching the request URL against `slotPatternParts` whenever a
  slot's param names aren't a subset of the route's. `pageModule` on
  `AppPageSlotOverride` becomes optional so a params-only override
  doesn't have to swap the slot's page.

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

* fix(routing): address bonk review on inherited parallel slot mirror

* Swap inverted scoring for required vs optional catch-alls — `:name+`
  (matches ≥1 segments) is more constrained than `:name*` (≥0), so it
  now scores higher, preventing a less-specific optional catch-all from
  shadowing a required one when both compete for the same route.
* Tighten findMirroredSlotPage docstring: the implementation collapses
  exact URL-parts equality and pattern compatibility into one scored
  loop, where literals naturally win. Rewrite the comment to reflect
  what the code actually does.
* Document patternsCompatible's conservative rejection of literal slot
  segments paired with dynamic route segments (and the consequence that
  a literal slot sub-page never matches a catch-all route).
* Note in buildSlotOverrides why an empty slotParamNames also skips the
  override (no dynamic markers means nothing to extract).
* Add unit tests for two cases that were only covered indirectly:
  graph-level slotPatternParts/slotParamNames when route and slot use
  different param names, and a slot owned at an intermediate ancestor
  rather than appDir.

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-03 18:02:20 +01:00
Nathan Nguyen e51856bb0d fix: validate CLI --port / --hostname values, reject missing/flags-as-values (#1012) (#1037)
Previously, vinext dev --port (no value) silently returned NaN,
vinext dev --hostname returned undefined, and --port --hostname 0.0.0.0
consumed --hostname as the port value.

Now the parser throws clear errors when a value-taking flag is missing
its value or when the next argument looks like another flag. Extracted
parseArgs to cli-args.ts for testability.
2026-05-03 17:51:50 +01:00
James Anderson aba335d5f3 fix(dev): self-host Google Fonts in dev mode (#1038)
The build pipeline already rewrites `next/font/google` calls to embed
self-hosted @font-face CSS pointing at `/<assetsDir>/_vinext_fonts/...`,
but the call-site rewrite was gated behind `isBuild`. Dev requests for
those URLs 404'd because nothing copies the cached `.vinext/fonts/`
files into the dev server's served paths, and once `_selfHostedCSS` is
present the shim no longer emits the fonts.googleapis.com `<link>` —
so dev showed unstyled glyphs with no CDN fallback.

Drop the build-only gate and add a `configureServer` middleware that
serves the cached files directly under the same URL prefix the
@font-face CSS embeds. Reuse the prod `CONTENT_TYPES` map so dev and
prod hand out identical MIME types.

Regression spec navigates to a new `/font-google-test` route in
app-basic that uses Geist + Geist_Mono, asserts no CDN URL leaks into
the HTML, and fetches a preload URL to verify the dev middleware
returns a real font/woff2 body.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:49:33 +01:00
Nathan Nguyen a3a8c0bfc1 refactor(app-rsc): extract request normalization into server/app-rsc-request-normalization (#1034)
* refactor(app-rsc): introduce app-rsc-request-normalization module

The RSC entry handler inlined all URL parsing, pathname decoding, basePath
checking, RSC detection, and header sanitization directly in its
_handleRequest template. That made the logic impossible to unit-test, hard to
audit for security, and duplicated normalizeMountedSlotsHeader in three places.

Create server/app-rsc-request-normalization.ts to own the full normalization
pipeline as a single, typed function (normalizeRscRequest). The function
returns a discriminated Response | NormalizedRscRequest, encoding early exits
(400/404) in the type rather than as throw-then-catch pairs.

The ordering within the function is security-critical and documented step by
step:
  1. Parse URL
  2. Protocol-relative guard (on raw pathname, before normalizePath collapses //)
  3. Strict segment-wise percent-decode (malformed encoding -> 400)
  4. normalizePath (collapse //, resolve . and .. segments)
  5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses)
  6/7. RSC detection and cleanPathname
  8. Sanitize X-Vinext-Interception-Context (strip null bytes)
  9. Normalize x-vinext-mounted-slots (dedup, sort)

Consolidate normalizeMountedSlotsHeader: the function existed independently in
isr-cache.ts and as a private copy in app-elements.ts. Both now import from
the new module. isr-cache.ts re-exports it for callers already importing from
there.

43 new behavior tests cover the security-sensitive paths: protocol-relative
variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath
enforcement, %2F preservation, RSC detection by suffix and Accept header,
interception context sanitization, and mounted-slots normalization.

* refactor(app-rsc): wire normalizeRscRequest into the entry and callsites

Replace the open-coded normalization block in the generated _handleRequest
function with a single normalizeRscRequest call. The entry now delegates
all security-sensitive preprocessing to the typed module rather than
repeating it in a template string.

Remove the local normalizeMountedSlotsHeader implementation from isr-cache.ts
(was a standalone export) and remove the private copy from app-elements.ts.
Both now import from app-rsc-request-normalization.ts. isr-cache.ts re-exports
the function so existing callers that import from there are unaffected.

Generated entry changes:
- Add import for normalizeRscRequest from the new module
- Remove guardProtocolRelativeUrl and stripBasePath from requestPipeline import
- Remove normalizePathnameForRouteMatchStrict from routingUtils import
- Remove normalizeMountedSlotsHeader from isrCache import
- Replace ~20-line normalization block with 4-line call + destructure

* fix(app-rsc): remove orphaned url declaration from _handleRequest

The original const url = new URL(request.url) at the top of _handleRequest
was not removed when the normalization block was replaced with normalizeRscRequest.
The destructure const { url } = __norm on the next early-exit block redeclares
the same identifier, causing a PARSE_ERROR in Rolldown during production builds.

Remove the orphaned declaration. url now comes solely from the normalizeRscRequest
result.

* fix(test): update cleanPathname assertion for normalizeRscRequest destructuring

* refactor(app-rsc): extract normalizeMountedSlotsHeader into neutral module and add null-byte test

Moves normalizeMountedSlotsHeader from app-rsc-request-normalization.ts into server/app-mounted-slots-header.ts so it can be imported by app-elements.ts without creating a module boundary dependency from payload helpers into request-lifecycle code.

Updates - isr-cache.ts re-exports from the new module. app-elements.ts switches its import accordingly.

Adds a null-byte sanitisation test for X-Vinext-Interception-Context (previously untested security branch). Fixes the PR description to accurately describe the duplication story.

* ci: retrigger CI

* ci: retrigger CI 2
2026-05-03 17:49:26 +01:00
Nathan Nguyen ce3af6aa21 refactor(app-router): extract browser navigation lifecycle controller (#1019)
* refactor(app-router): extract browser navigation lifecycle controller

* fix(app-router): separate render ids from navigation ids

* fix(app-router): satisfy ci lint rules

* fix(app-router): narrow controller surface, add hmrReplaceTree, restore lost comments, fix tests
2026-05-03 17:45:53 +01:00
Nathan Nguyen 763736eae0 refactor(app-rsc-entry): delegate pure helpers to typed server modules (#1017)
* refactor(app-rsc-entry): delegate pure helpers to typed server modules

The generated RSC entry template contained five inline implementations of
pure runtime helpers: post-middleware request context construction,
RSC error handler creation, request context cleanup, RSC stream preload
hint normalization, and hook warning suppression. These inline blocks
repeated runtime behavior across multiple call sites and could not be
unit-tested independently.

Move each helper to a dedicated typed module under server/ so the
generated entry imports and wires them instead of owning the logic:
- buildPostMwRequestContext → server/app-post-middleware-context.ts
- createRscOnErrorHandler → server/app-rsc-error-handler.ts
- __clearRequestContext + setNavigationContext → server/app-request-context.ts
- renderToReadableStream preload hints → server/rsc-stream-hints.ts (createRscRenderer)
- hook warning console.error patch → server/app-hook-warning-suppression.ts

Each helper module has focused unit tests. The entry template is 55 lines
shorter and now describes the app shape; the typed modules own behaviour.

* fix: remove redundant as cast and fix test module caching

app-hook-warning-suppression.ts contained an `as string` cast that was
redundant because the typeof check on the prior line already narrows the
type. The hook warning suppression tests in tests 2-4 were passing
vacuously because dynamic import() returned the cached module whose
console.error patch was no longer active after test 1 restored the
original. Use vi.resetModules() before each test to force fresh module
evaluation.

clearAppRequestContext now delegates to setAppNavigationContext(null)
instead of calling the raw navigation and root-params setters directly,
matching the DRY pattern from the original inline code.

* fix: resolve entry helper rebase conflicts

* ci: rerun checks (font-google flake)

* Update packages/vinext/src/entries/app-rsc-entry.ts

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

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-05-03 17:34:06 +01:00
Divanshu Chauhan (divkix) a49be5c881 fix: add hydration-level onError replay in next/image via useLayoutEffect (#1021)
* fix: add hydration-level onError replay in next/image via useLayoutEffect (#1007)

Port the upstream Next.js hydration replay pattern from vercel/next.js#93209:
use a useLayoutEffect + img.src = img.src to re-trigger the error trajectory
when an image fails before React hydration completes (e.g. during SSR streaming).

Changes:
- Add useNonWarningLayoutEffect helper (useEffect in SSR, useLayoutEffect in browser)
- Add mergedRef to capture the underlying img element for the layout effect
- Add useLayoutEffect that re-triggers img.src = img.src on initial mount when onError is provided
- Forward ref to UnpicImage elements (ref forwarding is supported)
- Add SSR rendering tests for onError, onLoad+onError, ref forwarding, and loader path

* fix: address PR review feedback for next/image onError hydration replay (#1007)

- Fix mergedRef callback instability: use ref object in useRef instead of
  array dep in useCallback, matching Next.js upstream
- Add stable refs for onLoad/onError/onLoadingComplete via useRef +
  useEffect sync pattern to prevent layout effect re-runs
- Fix layout effect deps to use stable ref objects instead of raw
  handler values; drop 'loading' (not in Next.js upstream deps)
- Add img.complete check for onLoad replay when browser already loaded
  the image before hydration-level onError replay
- Use React.RefObject instead of deprecated MutableRefObject
- Fix eslint-disable comment placement for no-self-assign
- Add SSR smoke test comments in shims.test.ts

* fix(next/image): address PR review — extract helper, clean deps, add comment

- Extract createSyntheticLoadEvent(img) helper for ~35→1 line in layout effect
- Remove onLoadRef/onErrorRef from layout effect dep array (stable ref objects)
- Add comment explaining useRef+useEffect sync timing gap

* fix: guard onLoad replay with naturalWidth > 0, clean deps (#1007)

- Check img.naturalWidth > 0 to prevent firing synthetic onLoad for
  errored images (img.complete is true for both success and error per
  the HTML spec)
- Remove src from layout effect dep array — dead dependency since
  didInsertRef bails on all re-runs after mount
- Add comment noting createSyntheticLoadEvent requires browser context
- Fix test comment referencing non-existent image-component.test.ts

* fix: port Next.js useMergedRef hook for next/image ref merging

Replace the inline mergedRef callback (with [] deps) with a proper
useMergedRef hook ported verbatim from next/src/client/use-merged-ref.ts.

The inline version used useCallback with [] deps, which kept the callback
identity stable and prevented React from re-invoking it when the parent
ref changed on re-render. The ported hook uses [refA, refB] deps, which
correctly triggers React's ref lifecycle: old ref gets null (cleanup),
new ref gets the element.

Also adds React 19 cleanup ref support (ref callbacks that return cleanup
functions) and proper null-based cleanup on unmount.
2026-05-03 00:12:26 +01:00
James Anderson 04befb5342 fix(navigation): hard-nav to target when render error tears down the tree (#1028)
When an RSC payload contains an error reference for a slot (e.g. a server
component throws), createPendingNavigationCommit only awaits the top-level
elements promise, so the dispatch goes through and React renders. The slot
throws when use() unwraps the rejected reference. Without an error.tsx
boundary above NavigationCommitSignal, the entire root unmounts — and
because the URL update lives in NavigationCommitSignal's pre-paint layout
effect, the browser is left on the previous URL with a blank page and a
console-only error.

Track the in-flight navigation target in a module-level
pendingNavigationRecoveryHref, set it just before dispatchBrowserTree and
clear it once the commit effect updates history. Add an onUncaughtError
handler to hydrateRoot that reads the recovery href at fire time and
window.location.assign()s to it, so the user lands on the URL they
clicked and the server can render its actual error UI for that route.

Wire the handler in both dev and prod (previously only onCaughtError was
registered in dev).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:36:05 +01:00
James Anderson 3f287ff5ab chore: remove useless code assertion tests and snaps (#1023)
* chore: remove useless code assertion tests and snaps

* .
2026-05-02 19:54:31 +00:00
Nathan Nguyen d85421b3e5 refactor(app-router): extract buildPageElements into typed helper module (#1018)
* refactor(app-router): extract buildPageElements into typed helper module

The generated RSC entry template owned the central element-construction
path (buildPageElements) as an inline template function with ~110 lines
of behaviour. This made the code untestable directly and forced any change
to element-tree construction to be verified through snapshot diffs of the
generated output rather than through behavioural assertions.

The generated entry should describe the app shape (route manifests,
module references); runtime behaviour belongs in normal typed modules.

Extract buildPageElements into a new server/app-page-element-builder.ts
module with fully typed options (BuildPageElementsOptions), a route shape
(AppPageBuildRoute extending AppPageRouteWiringRoute), and intercept
types. The generated entry now delegates to this module with a 12-line
closure that passes the per-app module references (globalErrorModule,
rootNotFoundModule, etc.).

Next.js equivalents:
- create-component-tree.tsx -- builds the nested React element tree
- create-metadata.tsx -- resolves page head metadata including parallel
  route interpolation

Behavioural changes: none. All existing app-router behaviour is preserved.
Tests added: 9 focused unit tests covering error payload construction,
interception context threading, search-params-triggered dynamic marking,
slot override wiring, and thenable-params proxy behaviour.

* chore: register app-page-element-builder in knip entry list

The module is imported by the generated RSC entry template as a string
literal, so knip cannot statically trace the dependency. Same pattern as
app-page-dispatch.ts, app-page-head.ts, and prerender-work-unit-setup.ts.
2026-05-02 20:25:47 +01:00
Nathan Nguyen e5d3271462 refactor(app-router): extract app fallback renderer factory (#1016)
Currently the generated RSC entry defines three wrapper functions
(renderHTTPAccessFallbackPage, renderNotFoundPage, renderErrorBoundaryPage)
that reconstruct the same large options object on every call. Each wrapper
closes over app-level constants (font providers, root boundaries, metadata
routes, SSR loader, thenable-params helper, sanitizer) and also captures the
per-request Request object in its closure.

This inflates the dependency surface of the three functions: the generated
code must pass every app-level constant on every invocation, and the request
shadowing makes it impossible to see at a glance what is truly per-request
versus per-app.

Replace the three generated functions with a single factory
__createAppFallbackRenderer({ rootBoundaries, globalErrorModule, metadataRoutes,
ssrLoader, fontProviders, makeThenableParams, sanitizer, rscRenderer }) that
returns { renderHttpAccessFallback, renderNotFound, renderErrorBoundary }.
The generated entry constructs the factory once at module level and calls the
returned methods many times. Request is now passed at the call site, removing
it from the factory closure entirely.

The empty-middleware fallback ({ headers: null, status: null }) moves into
the typed factory module so generated code no longer needs to define it.

New module: packages/vinext/src/server/app-fallback-renderer.ts
Tests: tests/app-fallback-renderer.test.ts
2026-05-02 20:23:11 +01:00
Nathan Nguyen 2b958ad90e refactor: extract instrumentation lazy init to server/instrumentation-runtime.ts (#1015)
The 25-line __ensureInstrumentation block in entries/app-rsc-entry.ts was
module-level state plus idempotent init logic embedded in a generated entry
template. This is the canonical 'imperative shell with mutable bookkeeping'
pattern — it never belongs in codegen.

Lift it to a new server/instrumentation-runtime.ts module exporting
ensureInstrumentationRegistered(instrumentationModule). The generated RSC
entry now only:

1. Imports the helper (conditional on instrumentationPath).
2. Calls await ensureInstrumentationRegistered(_instrumentation) in the
   request handler.

All bookkeeping (initialized flag, shared promise, prerender skip) lives in
the typed helper, making it unit-testable and keeping the entry template thin.

New tests cover idempotency, concurrent racing, prerender skipping, and
graceful handling of modules without register/onRequestError.

Refs:
- Next.js instrumentation docs: https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
- Next.js source: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/dev/next-dev-server.ts (instrumentation boot)
- Next.js source: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/plugins/nextjs-require-cache-hot-relayer.ts (onRequestError global wiring)
2026-05-02 20:09:03 +01:00
Nathan Nguyen c33f96a904 refactor(routing): extract App Router route graph builder (#998)
App Router route discovery currently shares a single module with route caching and request-time matching. That makes discovery changes harder to review and increases the chance that graph construction work collides with generated entry changes.

The route graph construction types and discovery helpers now live in app-route-graph.ts, while app-router.ts remains the public cache and trie matching wrapper. A direct graph-builder test covers pages, route handlers, layouts, and inherited parallel slots at the new boundary.
2026-05-02 20:03:50 +01:00
Nathan Nguyen ddd7bd55bd refactor(build): move route classification injection behind a typed planner (#997)
* refactor(build): move route classification injection behind a typed planner

The Vite generateBundle hook currently owns stub detection, module-graph classification, replacement generation, and source-map invalidation. That makes generated-entry behavior harder to test without running the full plugin lifecycle.

The hook now adapts Rollup chunks and module info into a route classification injector. The injector plans the patch, validates drift cases, preserves Layer 1 priority over module-graph results, and keeps source-map invalidation explicit.

Tests cover scan-phase skips, stub drift errors, debug reason patching, seed-module absence, and the existing integration path.

* refactor(build): tighten classification stub validation
2026-05-02 19:57:31 +01:00
Nathan Nguyen f45fce00d5 fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings

Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses.

Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules.

Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known.

* fix(isr): address cache life review follow-ups

* chore: address latest review follow-ups

* ci: pin vite-plus setup version

* Revert "ci: pin vite-plus setup version"

This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1.

* fix: avoid blocking ISR page streams on cache metadata

* fix: preserve headers for speculative cacheLife probes

* fix: preserve prerender cacheLife metadata

* fix: preserve legacy ISR cache metadata behavior

* fix: preserve prerender seed revalidate context

* fix: harden app page cache policy metadata

* fix: resolve app router prerender conflict
2026-05-02 19:54:25 +01:00
Nathan Nguyen dc6b11f2ce fix(rsc): exclude client shims from dep optimization (#1020)
* fix(rsc): exclude client shims from dep optimization

App Router dev config currently excludes the vinext package but not its client shim subpaths. @vitejs/plugin-rsc tracks package client references by the original bare import source, so optimized vinext/shims/* modules can lose the matching export metadata and crash client-package proxy loading.

Add a shared optimizeDeps exclude set for vinext's known RSC client shims and merge it into the top-level, RSC, SSR, and client optimizer configs while preserving existing user and server-external excludes.

* Update rsc-client-shim-excludes.ts

Co-authored-by: James Anderson <james@eli.cx>

* Update rsc-client-shim-excludes.ts

Co-authored-by: James Anderson <james@eli.cx>

* Update build-optimization.test.ts

Co-authored-by: James Anderson <james@eli.cx>

---------

Co-authored-by: James Anderson <james@eli.cx>
2026-05-02 18:50:19 +01:00
Divanshu Chauhan (divkix) 8926edbbad fix: next/image only fire onError once per src per mount (#990) (#1004)
* fix: next/image only fire onError and onLoad once per src per mount (#990)

Use ref-based dedup to prevent onLoad and onError from firing multiple
times for the same image src across React re-renders. Ported from the
upstream Next.js fix in vercel/next.js#93209.

- Destructure onError from props (previously fell through rest spread)
- Add lastLoadedSrcRef/lastErrorSrcRef to guard against duplicate fires
- Pass onError/onLoad to all 4 render paths (was missing from UnpicImage)
- Add SSR tests covering all render paths and verifying no DOM attribute leaks

The dedup prevents infinite re-render loops when user code calls setState
inside onError/onLoad, matching Next.js behavior.

* fix: add "use client" directive to next/image shim

useRef is a hook requiring client rendering context. Without "use client", RSC executes useRef() and throws "useRef is not a function", breaking create-next-app CI.

* fix: move resolveImageSource above handler closures for readability
2026-05-02 17:50:31 +01:00
Nathan Nguyen 257a5de9c1 fix(dev): avoid app reloads for colocated files (#1014)
Adding or removing any JavaScript or TypeScript file under app currently invalidates the App Router route cache and sends a full reload. That is wrong for colocated components and helpers because the App Router only treats specific file conventions and metadata files as route structure.

The watcher was using the broad page extension matcher for app files, which confused ordinary project files with route-affecting files. Add a dev route-file predicate aligned with vinext route and metadata discovery, then use it before invalidating the generated RSC entry and root params modules.

Tests cover route convention files, metadata files, private folders, sibling directory prefix safety, and colocated non-route files.
2026-05-02 17:47:49 +01:00
Divanshu Chauhan (divkix) 66c8464903 fix: re-throw stream errors in pumpReader to prevent incomplete ISR cache data (#1003)
* fix: pumpReader now re-throws errors to prevent incomplete ISR cache data (#1002)

* test: add regression test for pumpReader error propagation (#1002)
2026-05-01 23:43:18 +01:00
Nathan Nguyen da4d0b232c fix(app-router): preserve falsy thrown values in error boundaries (#1000)
App Router error boundaries treated the stored error value itself as the presence flag. That makes thrown values like undefined, null, 0, an empty string, or false indistinguishable from an empty boundary state, so the children can render again instead of the segment fallback.

The boundary state now stores either null or a wrapper containing the thrown value, and route error component typing admits unknown values to match React and Next.js behavior. The regression coverage checks both the state transition contract and a client component route that throws every falsy literal.
2026-05-01 23:34:26 +01:00
Nathan Nguyen a3580bc79b refactor(metadata): share route pattern helpers (#996)
* refactor(metadata): share route pattern helpers

Metadata route handling repeated the same dynamic segment normalization and pattern matching in several places. That made the file-convention metadata path harder to review and kept generic routing behavior inside metadata-specific modules.

Move route pattern parsing, matching, and dynamic segment filling into a normal routing module. Reuse it from metadata route build data, file-based metadata, metadata route response matching, and app RSC intercept matching, then consolidate duplicated icon descriptor normalization in the metadata server and shim code.

Add focused route-pattern tests and keep the existing metadata and shim tests covering consumer behavior.

* fix(routing): preserve prototype-named route params

The shared route pattern matcher used a normal object for matched params, unlike the previous metadata and App RSC matchers. A route parameter named __proto__ could mutate the params object's prototype instead of being stored as an own value.

Restore a null-prototype params map and add regression coverage for single and catch-all prototype-named route params.

* fix(routing): preserve literal intercept target segments
2026-05-01 23:30:59 +01:00
Nathan Nguyen de00ed2e68 refactor(app-router): extract app page dispatch (#986)
* refactor(app-router): extract app page dispatch

App Router page requests now enter a shared dispatch module after generated route matching. The generated RSC entry still describes route modules, app-shape callbacks, and request-local values, but cache reads, static generation context, dynamic param checks, intercept responses, and lifecycle rendering move into normal typed code.

This keeps page orchestration out of codegen without changing the observable App Router behavior covered by the integration suite.

* chore: rerun ci
2026-05-01 23:14:04 +01:00
Divanshu Chauhan (divkix) ae0440b4f7 fix: support enablePrerenderSourceMaps config with default true (#973)
* fix: support enablePrerenderSourceMaps config with default true (#960)

Adds the `enablePrerenderSourceMaps` config option matching Next.js behavior.
Defaults to `true` so prerender stack traces resolve through sourcemaps to
original source files. Error results in prerender output now include full
stack traces when enabled. Node.js sourcemap support is enabled before the
prerender phase in both `cli.ts` (build) and `deploy.ts` (deploy).

Config parity: silently accepts the option; recognized as "supported" in
`vinext check` output.

* refactor: address review comments for enablePrerenderSourceMaps (#960)

- prerender.ts: simplify getErrorMessageWithStack and fix comment
  (V8's Error.stack always prepends message, remove startsWith guard)
- cli.ts: add Error.stackTraceLimit = 50 after setSourceMapsEnabled
- deploy.ts: add Error.stackTraceLimit = 50 after setSourceMapsEnabled
- next-config.test.ts: move enablePrerenderSourceMaps tests to own describe
  block and remove redundant explicit-true test

* fix(prerender): simplify getErrorMessageWithStack and add test coverage

Simplify the error stack handling in getErrorMessageWithStack by removing
the redundant nullish coalescing with empty string. The expression
`err.stack || err.message` handles all cases correctly.

Add test coverage proving that enablePrerenderSourceMaps: true (default)
includes stack traces in prerender errors. New test fixture page
`error-throw.tsx` throws during render to exercise the error path.

* fix(prerender): address review feedback - update snapshot, add config comment

- Regenerate entry-templates.test.ts.snap after adding error-throw fixture page
- Add comment clarifying enablePrerenderSourceMaps default in prerender test

* feat(config): add enablePrerenderSourceMaps config option

Adds support for the experimental.enablePrerenderSourceMaps config
option that Next.js 15+ introduced for debugging prerender issues.

Also includes merged changes from main:
- hashSalt: support experimental.outputHashSalt + NEXT_HASH_SALT env
- swcEnvOptions warning: warn about unsupported SWC polyfill option

All 75 next-config tests passing.

Closes #960
2026-05-01 22:41:12 +01:00
Divanshu Chauhan (divkix) ab8570218b fix: unstable_io() returns hanging promise during prerendering (#979)
* fix: unstable_io() returns hanging promise during prerendering (#972)

During prerendering (static export, --prerender-all, TPR), unstable_io()
must return a hanging promise to suspend React past the IO boundary.
Previously it always returned a resolved promise, matching only the
browser/client implementation.

Changes:
- Add makeHangingPromise utility (never-resolving promise, rejects on abort)
- Define typed WorkUnitStore discriminated union in workUnitAsyncStorage
- unstable_io() branches on work unit store type per Next.js's io.ts
- Set prerender work unit store in RSC entry when VINEXT_PRERENDER=1
- Add tests for prerender, request, cache, and abort scenarios

* fix: add knip entries for prerender-work-unit and work-unit types (#972)

* fix(prerender): address PR review feedback for unstable_io work unit store

- AbortController now properly aborted in .finally() to prevent listener leaks

- Add optional route field to PrerenderStore for better error messages

- Pass pathname as route in app-rsc-entry handler

- Use actual route instead of hardcoded 'unknown' in unstable_io()

- Add explicit return in default branch of cache.ts for clarity

- Remove premature knip entry for make-hanging-promise.ts

- Replace V8-internal .status check with Promise.race pattern in tests

* fix(app-rsc-entry): place route option in correct location

Move { route: __pathname } to __runWithPrerenderWorkUnit() call
instead of incorrectly appending to _handleRequest closing brace.
Fixes parse error introduced in 035a54f.

Refs #979

* fix: address prerender hanging promise review feedback

- Suppress unhandled rejection when signal already aborted in makeHangingPromise
- Add lazy pathname extraction via getter function in runWithPrerenderWorkUnit
- Add listener array cleanup after abort in makeHangingPromise
- Add test for unhandled rejection suppression on early-return path
- Update entry template snapshots for lazy route getter

* fix: resolve merge conflicts with main for prerender work unit

- Take main's refactored app-rsc-entry.ts as base (extracted runtime primitives)
- Apply prerender-work-unit wrapping around handler function
- Update tests to match refactored code structure (delegated helpers)
- Update entry-templates snapshots for new generated code structure

* fix: resolve merge conflicts with main for test files

- Keep main's tests/app-router.test.ts (already has correct refactored tests)
- Update entry-templates snapshots to include prerender-work-unit generated code
2026-05-01 10:22:33 +01:00
Divanshu Chauhan (divkix) d45c5905f3 perf: fuse RSC stream tees reducing allocs from 3 to 2 (#984)
* perf: fuse RSC stream tees reducing allocs from 3 to 2

ISR cache path teed the RSC stream twice (once for capture,
once for embed). Fuse into single tee: sideStream serves both
embed transform and raw byte accumulation via createRscEmbedTransform.

Tee 1 (capture) and Tee 2 (embed) now share one tee. Tee 3 (HTML
cache) remains separate — it operates on post-SSR HTML with a
conditional write decision.

Fixes #981

* fix: address PR review feedback for fused RSC stream tees (#981)

- Fix AppPageSsrHandler type to include sideStream and capturedRscDataRef
- Clear rawChunks after getRawBuffer() to reduce peak memory usage
- Remove misleading no-op null assignment in handleSsr else branch
- Guard await null in ISR cache write to prevent undefined in cache

* fix: repair CSP nonce handling and update snapshots for #981

- Fix renderAppPageHtmlStream to always pass ssrOptions (including scriptNonce)
  to handleSsr, instead of conditionally when sideStream is defined. This fixes
  CSP nonce regressions in dev mode.
- Update 6 snapshot occurrences in entry-templates.test.ts.snap to match the
  updated __revalCapturedRscRef.value access pattern with null safety.

Fixes CI failures for the fuse RSC stream tees PR.
2026-05-01 08:49:58 +01:00
Divanshu Chauhan (divkix) 721be81709 fix: support experimental.outputHashSalt config and NEXT_HASH_SALT env var (#978)
* fix: support experimental.outputHashSalt config and NEXT_HASH_SALT env var (#754)

Add support for Next.js experimental.outputHashSalt config option and
NEXT_HASH_SALT environment variable. When set, the salt is mixed into
content-addressed build output filenames via Rollup/Rolldown's
output.hashSalt option, producing different hashes without modifying
source code — useful for cache-busting after CDN poisoning.

Config resolution: concatenates config salt + env var salt (matching
Next.js behavior: config first, then env). When neither is set, no
salt is applied and behavior is unchanged.

* ci: re-trigger CI (Windows create-next-app flake, STATUS_STACK_BUFFER_OVERRUN)

* fix: use augmentChunkHash for outputHashSalt instead of output.hashSalt

The output.hashSalt option is webpack-specific and is silently ignored by
Rollup/Rolldown. Use the augmentChunkHash plugin hook instead, which is
supported by both bundlers and actually affects chunk content hashes.

Changes:
- Remove hashSalt parameter from getClientOutputConfigForVite()
- Add vinext:hash-salt plugin with augmentChunkHash hook
- Add experimental.outputHashSalt to CONFIG_SUPPORT map

* refactor: simplify hash-salt plugin comments

- Simplify comment about augmentChunkHash (just Rolldown, not Rollup)
- Add scope-gap note about CSS/static asset hashing not being salted
2026-05-01 08:00:54 +01:00
Divanshu Chauhan (divkix) 945ae37a1d fix: forward invalid dynamic usage errors on client-side navigations in dev (#976)
* fix: forward invalid dynamic usage errors on client-side navigations in dev (#958)

Record errors from throwIfInsideCacheScope() on the per-request
context so they survive user-code try/catch and can surface on
client-side client navigations. In dev, wrap the RSC response
body to check for recorded errors after stream consumption and
log them to the terminal.

Ported from Next.js: vercel/next.js@f5e54c0 (#93184)

* refactor: extract _recordInvalidDynamicUsageError helper in server.ts

Extract the repeated ALS lookup + error recording pattern into a
single reusable function _recordInvalidDynamicUsageError(). Replaces
4 inline try/catch blocks in _throwIfInsideCacheScope with calls to
the new helper.

Also adds a comment in app-page-render.ts explaining that HTML path
coverage is intentionally deferred (RSC-nav scope only).

No behavioral changes - same global keys, same error paths.
2026-04-30 19:57:24 +01:00
Divanshu Chauhan (divkix) 246aa1d72e fix: add SWR non-blocking guard tests for route handler ISR (#975)
* fix: add SWR non-blocking guard tests for route handler ISR (#959)

Audit confirms vinext correctly uses ctx.waitUntil() for all background
cache-write and regeneration work — no equivalent of the Next.js
pendingWaitUntil / pipe-readable blocking pattern exists.

## Changes
- Add slow ISR route handler fixture (revalidate=1, 1s handler delay)
- Add response-time assertion to existing STALE test (< 500ms)
- Add new test verifying stale response completes fast despite slow
  background regeneration

Ported from Next.js:
test/e2e/app-dir/use-cache-swr/use-cache-swr.test.ts

* docs: clarify porting attribution in SWR ISR tests

Adjust comments to note test pattern is adapted from Next.js
"use cache" SWR tests, not a direct translation. The mechanism
differs (export const revalidate vs "use cache").
2026-04-30 19:52:54 +01:00
Nathan Nguyen 1d7c031e30 feat(app-router): feed file metadata routes into head output (#891) 2026-04-30 19:38:32 +01:00
Nathan Nguyen f367f301dc fix(headers): align draft mode cookie attributes with Next.js (#992)
Draft mode cookies currently use SameSite=Lax in production and clear with Max-Age=0. That diverges from Next.js and can prevent production preview flows from receiving the bypass cookie in cross-site contexts.

Centralize draft-mode cookie attributes in the headers shim, use SameSite=None and Secure outside development, and clear the bypass cookie with an expired date. Add focused compatibility coverage for production enable and disable attributes.
2026-04-30 17:13:14 +01:00
Nathan Nguyen 1e4c175ee4 fix(routing): omit empty optional catch-all params (#987)
Optional catch-all routes currently materialize the missing segment list as an empty array. Next.js leaves the param key absent when the optional catch-all capture group does not match, so pages and app routes can observe a different params shape at the zero-segment path.

The route trie treated zero remaining segments as a captured value. The fix keeps the route match while returning an empty params object for that zero-segment branch, and applies the same normalization to standalone app RSC pattern matching.

Tests cover shared trie matching, app RSC matching, app route discovery, pages route discovery, and hyphenated optional catch-all params.
2026-04-30 10:24:51 +01:00
Nathan Nguyen 7b1d810d89 refactor(app-router): extract RSC runtime primitives (#964)
* refactor(app-router): extract RSC runtime primitives

The App Router RSC entry template currently owns low-level ISR keying, server-action body reads, and RSC error digest behavior. That makes generated code harder to inspect and leaves runtime contracts covered only through template snapshots.

Move those primitives into typed server modules and keep app-rsc-entry.ts focused on app-shape imports, route tables, and request-specific wiring. Add focused helper tests for slot normalization, streamed action body limits, production digest behavior, and generated-entry delegation.

* fix(app-router): address RSC primitive review feedback
2026-04-30 10:08:58 +01:00
Nathan Nguyen 9cae9cb271 refactor: extract early request pipeline helpers (#983) 2026-04-30 10:03:19 +01:00
Nathan Nguyen 6ea8e51643 fix(app-router): return action-not-found for stale server actions (#985)
* fix(app-router): return action-not-found for stale server actions

Unknown or stale Server Action ids currently fall into the generic action error path and return 500. That hides deployment skew from the client and misses the Next.js action-not-found response contract.

The action loader is an external runtime boundary, so the helper now treats loaded actions as unknown until narrowed, maps recognized missing-action failures to the 404 x-nextjs-action-not-found protocol, and keeps unrelated loader failures on the existing 500 reporter path.

Tests cover fetch actions, progressive form decode misses, missing exports, generic loader failures, and the App Router integration response.

* chore: rerun ci
2026-04-30 08:59:35 +01:00
Divanshu Chauhan (divkix) 155be501e1 fix: accept experimental.swcEnvOptions config without erroring (#977)
* fix: accept experimental.swcEnvOptions config without erroring (#774)

Next.js added experimental.swcEnvOptions for SWC preset-env polyfill injection.
vinext uses Vite for transforms, so polyfill injection is not applicable, but
the config key should be accepted without errors. Add:
- CONFIG_SUPPORT entry in check.ts (unsupported with detail)
- Warning during config resolution in next-config.ts
- Tests for analyzeConfig detection and dot-notation matching

* refactor: align swcEnvOptions messaging and add test coverage

- Change runtime warning from "not yet supported" to "not applicable"

- Update CONFIG_SUPPORT detail for consistent messaging

- Add test coverage for swcEnvOptions warning in next-config.test.ts

* fix(check): update test assertion for swcEnvOptions message

The CONFIG_SUPPORT message for experimental.swcEnvOptions was updated
to indicate it's not applicable since vinext uses Vite instead of SWC.
This updates the corresponding test assertion to match the new message.

Fixes CI failure in tests/check.test.ts
2026-04-30 08:53:58 +01:00
Divanshu Chauhan (divkix) dc28af1cb6 fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764) (#945)
* fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764)

Per-layout forbidden/unauthorized boundary files (forbidden.tsx, unauthorized.tsx)
are now discovered by the router and wired into the App Router component tree via
dedicated ForbiddenBoundary and UnauthorizedBoundary client components. Previously,
only NotFoundBoundary existed, causing 403/401 errors thrown during RSC rendering
(e.g. inside Suspense boundaries) to escape into the generic error shell — this
could produce mismatched Flight/HTML data and client-side 'Connection closed' failures.

Next.js upstream: vercel/next.js#92231
  https://github.com/vercel/next.js/pull/92231

* fix: address review feedback on ForbiddenBoundary/UnauthorizedBoundary (#945)

- Export ForbiddenBoundaryInner and UnauthorizedBoundaryInner for testing
- Strengthen tests to import and call getDerivedStateFromError directly
- Use exact equality instead of startsWith for status code digest matching
- Make forbiddenPaths and unauthorizedPaths non-optional on AppRoute (matching notFoundPaths)
- Fix inaccurate ported-from attribution in test header

* fix: remove duplicate forbidden/unauthorized path iteration and update comment

Removes dead code in app-rsc-entry.ts (lines 185-190) that duplicated the existing import registration loops.

Also updates the stale nesting order comment in app-page-route-wiring.tsx to include Unauthorized and Forbidden boundaries.

* fix: align NotFoundBoundary digest matching and tests with Forbidden/Unauthorized patterns

- Use exact === instead of startsWith in NotFoundBoundary digest check
- Add forbiddenPaths/unauthorizedPaths per-layout test fixtures
- Fix property ordering: singular before plural for all boundary types

* fix: resolve rebase conflicts — align manifest code with main refactor

- Use buildAppRscManifestCode for manifest generation (matching main)
- Remove redundant || [] guards for non-optional forbiddenPaths/unauthorizedPaths
- Fix test fixture indices to match new module import count

* fix: add null guards for notFoundPaths, forbiddenPaths, unauthorizedPaths in manifest

TypeError: route.notFoundPaths is not iterable when routes lack per-layout boundary files.

CI was failing on app-router.test.ts with the error. Added null guards in both registerRouteModules() and buildRouteEntries() to handle undefined arrays.

* fix: reorder route-level boundary nesting for merge with main

Move ErrorBoundary after NotFoundBoundary/ForbiddenBoundary/
UnauthorizedBoundary to match Next.js nesting order (Error >
Unauthorized > Forbidden > NotFound). Resolves merge conflict
with bc70e1c.
2026-04-30 07:46:07 +01:00
Divanshu Chauhan (divkix) f0d9aeae56 feat: add unstable_io shim for next/cache (#948) 2026-04-29 21:22:57 +01:00
James Anderson c5da1db4ff refactor: share config support list across checks (#971)
* refactor: share config support list across checks

* fmt
2026-04-29 20:17:07 +00:00
Divanshu Chauhan (divkix) b49974eb4f fix: add tests verifying strings thrown in Server Components are not swallowed (#950)
* fix: add tests verifying strings thrown in Server Components are not swallowed (#810)

Next.js had a bug where createReactServerErrorHandler returned early
for typeof thrownValue === 'string', swallowing the error before
logging. vinext's rscOnError already handles strings correctly (they
flow through the normal error path and get wrapped into Error objects).

Added:
- Fixture page that throws a string in a Server Component
- Integration test verifying the error boundary renders with the string message
- Runtime test confirming rscOnError returns a valid digest for string throws
- Static test confirming generated code has no early string-return path

Upstream: vercel/next.js@b9ca95c62d

* fix: address review feedback on string error handling tests (#950)

- Move prodRscOnError creation into beforeAll to avoid duplicating new Function construction
- Remove brittle positive assertions (instanceof Error, String(error)) from static test; keep only the negative anti-pattern guard
- Use textContentByTestId helper for integration test precision over loose html.toContain
2026-04-29 21:14:04 +01:00
Nathan Nguyen f8d2d39107 refactor: extract app prerender endpoints (#965) 2026-04-29 21:09:51 +01:00
Nathan Nguyen 78edbc3b46 refactor: Extract app route handler dispatch (#968) 2026-04-29 21:05:38 +01:00
Nathan Nguyen 9ab38193eb refactor: extract server action rsc flow (#967) 2026-04-29 18:42:32 +01:00
Nathan Nguyen 776fc9eea6 refactor: extract app rsc manifest construction (#966) 2026-04-29 18:35:08 +01:00
Divanshu Chauhan (divkix) e0854972eb fix: support file:// URLs in cacheHandler config path (#944)
* fix: support file:// URLs in cacheHandler config path (#728)

- Add cacheHandler and cacheMaxMemorySize to NextConfig type
- Add cacheHandler to ResolvedNextConfig with file:// URL handling
- Create resolveCacheHandlerPathToFilesystem() helper using fileURLToPath()
- Convert file:// URLs from import.meta.resolve() to filesystem paths
  during config resolution to prevent broken path concatenation

Matches Next.js commit 9a0214a1 which added the same file:// URL handling
for ESM projects using import.meta.resolve() for the cache handler path.

* fix: drop unused export on resolveCacheHandlerPathToFilesystem

* fix: resolve cacheMaxMemorySize and add cacheHandler tests

- Add cacheMaxMemorySize to ResolvedNextConfig and resolve it from input config,
  preventing silent config swallowing
- Add 7 tests for resolveNextConfig cacheHandler: file:// URL resolution,
  absolute/relative path passthrough, null/undefined defaults
- Also add cacheMaxMemorySize resolution and default tests
2026-04-29 18:32:39 +01:00
Divanshu Chauhan (divkix) cc0001e5ea fix: add experimental.prefetchInlining to compatibility check (#949)
* fix: add experimental.prefetchInlining to compatibility check (#860)

Next.js flipped experimental.prefetchInlining from false to true by default
in PR #92863. vinext does not implement per-segment prefetch inlining — it
uses unified RSC navigation payloads — so no behavioral change is needed.
This adds the config option to the compatibility scanner so vinext check
correctly reports it as supported.

* fix: mark experimental.prefetchInlining as partial, add test

- Change status from supported to partial (per review feedback —
  option is recognized but underlying behavior is not implemented)
- Add test case in tests/check.test.ts matching existing
  experimental.* config patterns
2026-04-29 18:30:20 +01:00