* fix(plugin): resolve vinext/shims/* package subpaths to local shim files
Runtime helper modules embedded into generated entries import vinext's
own shims by package subpath (e.g. `vinext/shims/headers`), while source
checkouts alias userland `next/*` imports to the local shim files. The
two specifiers resolved to different module instances, so request-scoped
singleton state (navigation context, headers) split between the source
shim copy and the package export copy.
The violated invariant is that every shim module must be a per-request
singleton regardless of import specifier. Resolve `vinext/shims/*`
through the same plugin path as the `next/*` aliases so both forms land
on the local shim files.
Exercised by the SSR shell-error recovery browser spec, whose fixture
imports vinext from the source checkout and depends on shared
navigation state across both import forms.
* fix(app-router): recover SSR shell render errors via __next_error__ document
When the HTML (Fizz) render rejects during SSR, vinext re-rendered a
server-side global-error page whose flight payload encodes the error
tree. For an app without a custom global-error.tsx that meant the
default error card with no path back to the real page: an SSR-phase-only
throw (e.g. a client component using the "throw to opt out of server
rendering" pattern) left the browser stuck on the card even though the
client render would succeed.
The violated expectation is Next.js's shell-error semantics: a failed
HTML shell is served as the default `__next_error__` error document
carrying the ORIGINAL flight payload and the bootstrap module, and the
browser re-renders the real tree from that payload with createRoot
instead of hydrating. Local error.tsx boundaries still win — they ship
inside the flight payload and catch the re-thrown error client-side.
handleSsr now resolves to that recovery document instead of rejecting,
but only when both hold:
- the error did not originate in the RSC render (no string `digest`),
so flight errors, redirect()/notFound(), and server-component throws
keep driving the existing rejection-based boundary machinery, and
- the app has no custom global-error.tsx (the generated entry knows at
build time and threads hasCustomGlobalError through dispatch/render
options); apps with one keep the server-rendered boundary re-render.
The browser entry switches from hydrateRoot to createRoot when the
document root carries id="__next_error__", dropping the error-shell
styles first.
Covered by the new ssr-error-shell-recovery browser spec (recovery to
real content, local error.tsx for SSR-only and unconditional client
throws) and the existing tests/nextjs-compat/global-error.test.ts
boundary-semantics suite.
* refactor(review): address PR 1908 non-blocking notes
- Add static prerender no-boundary recovery regression test to
ssr-error-shell-recovery.browser.spec.ts
- Document broad __next_error__ browser marker in app-browser-entry.ts
- Extract stripJsExtension to utils/path.ts and wire into all shim
resolution sites (vinext/shims/* + react-server shims)
Non-functional: targeted regression coverage + code documentation
+ minor resolver hardening per reviewer feedback.
* refactor(review): clarify shell recovery assumptions
* fix(ssr): cancel abandoned prerender streams
* refactor(ssr): clarify error shell root options
* fix(isr): recover shell errors during regeneration
* fix(ssr): scope client recovery to marked shells
* fix(plugin): explicitly filter vinext shim subpaths
* fix(plugin): retain null-prefixed shim resolution
* test(nextjs-compat): cover no-boundary shell recovery fallback
An unconditional client throw during SSR shell recovery without a local error boundary must still land on the default global-error card. Without a regression test, a future change could tear down the recovery shell and leave a blank document.\n\nAdd a production browser case that exercises the no-boundary route and asserts the default error UI after the client re-render throws again.
* fix(app-router): preserve shell recovery error semantics
* test(app-router): harden shell recovery cache semantics
* fix(app-router): mark global error responses uncacheable
* test(app-router): align error response status expectations
* fix(build): preserve null-prefixed og resolution
* fix(cache): delegate cache header cleanup to adapters
---------
Co-authored-by: James <james@eli.cx>
* fix(actions): return 404 for MPA action on app with no server actions (#1340)
A multipart form POST to an App Router page is always a server-action
attempt. When the form body decodes to no action at all (e.g. the build
has no server actions, so decodeAction returns null rather than
throwing), vinext fell through to a 200 page render instead of Next.js'
404 + x-nextjs-action-not-found. The fetch-action variant (via the
Next-Action header) already worked; only the MPA/no-JS form-POST variant
was wrong.
Gate the new 404 on the posted-to route being a page: route handlers run
after action dispatch and legitimately accept raw multipart POSTs, so
they still fall through. The page-vs-route distinction uses the static
__loadPage / __loadRouteHandler markers, available before lazy module
hydration.
Finishes the no-server-actions MPA case of #1340 (error-boundary,
content-type, and unrecognized-action handling landed in #1386/#1668).
* test(actions): add MPA no-action integration test; skip redundant route match (#1340)
Address ask-bonk review on #1853:
- Add a dev-server integration test that POSTs multipart/form-data to a
page (/about) and asserts 404 + x-nextjs-action-not-found, locking in
the entry-side matchRoute + __loadPage/__loadRouteHandler classification
end-to-end (previously only unit-tested by passing hasPageRoute directly).
- Gate the progressive-action route match on isProgressiveServerActionRequest
so matchRoute no longer runs on every App Router request — only for actual
multipart/no-actionId POST candidates.
* fix(app-router): preserve _rsc query across config redirects (#1529)
* refactor(prod-server): reuse rawQs for redirect query; clarify .rsc test comment
Address ask-bonk review on #1838:
- prod-server.ts: reuse the existing rawQs instead of re-slicing url for
the redirect Location query (identical value, removes redundancy).
- app-rsc-handler.test.ts: tighten the .rsc-without-header test comment so
it no longer overstates RSC re-detection — the guarantee is query
preservation (mirroring Next.js), not re-arming RSC on the followed hop.
* fix(app-router): match streaming metadata error responses
Vinext currently serializes metadata-originated redirects into a Flight payload for document requests. That response is not an HTML page, so direct browser visits cannot follow the streamed redirect and html-limited bots receive 200 instead of the blocking redirect.
The dispatcher also renders HTTP access fallback head metadata from layouts only, so not-found boundary metadata is omitted when generateMetadata() throws notFound().
Use Next.js's split response model: RSC requests keep the Flight redirect digest, streaming-capable document requests receive the refresh meta tag HTML, and html-limited bots fall through to the HTTP redirect. Thread the selected HTTP access boundary module into fallback rendering so boundary metadata is resolved exactly once.
* test(app-router): cover metadata RSC redirects
The unit CI failure came from a strict dispatch test receiving optional fallback fields with undefined or null values. Those fields are only meaningful when a boundary module is actually selected.
Build fallback option objects without absent boundary fields, preserving the existing call shape for ordinary page-level HTTP access fallbacks.
Add focused coverage for fromMetadata redirects on RSC requests so metadata document refresh handling cannot accidentally consume the client navigation Flight path.
* docs(app-router): update metadata redirect transport docs and drop dead boundary fallback
Address review feedback on the metadata streaming error split:
- Update the stale APP_PAGE_METADATA_ERROR_MARKER docblock to describe the
new three-way transport split (flight digest / HTML refresh meta tag / 307).
- Note the intentional minimal-stub HTML divergence from Next.js's
full-document insertion in buildMetadataRedirectHtmlResponse.
- Drop the redundant resolveAppPageHttpAccessBoundaryComponent fallback in
renderAppPageHttpAccessFallback (boundaryModule already covers both paths).
* docs(app-router): fix stale metadata redirect docs in head resolver and test reference
Address remaining bonk re-review nits:
- Update resolveModuleMetadata docblock in app-page-head.ts to describe the
three-way transport split (it still claimed redirects ride in the flight
payload even for document SSR).
- Point the APP_PAGE_METADATA_ERROR_MARKER Next.js test reference at
metadata-streaming, which is where the behavioural contracts are ported from.
---------
Co-authored-by: James <james@eli.cx>
Vitest integration CI shards by test file, so the 340-test App Router integration file becomes one oversized scheduling unit. That makes the slowest shard depend on one monolithic file even after adding native shards.
Split the App Router integration coverage into focused files and increase the integration matrix to four native Vitest shards. This keeps fileParallelism disabled for shared-fixture safety while giving CI smaller scheduling units to distribute.
Validation: vp check tests/app-router-*.test.ts vite.config.ts .github/workflows/ci.yml; vp test run --project integration tests/app-router-client-preloading.test.ts tests/app-router-dev-server.test.ts tests/app-router-external-rewrite.test.ts tests/app-router-font-google-prod.test.ts tests/app-router-isr-codegen.test.ts tests/app-router-malformed-url.test.ts tests/app-router-metadata-routes.test.ts tests/app-router-middleware-next-request.test.ts tests/app-router-next-config-codegen.test.ts tests/app-router-next-config-dev.test.ts tests/app-router-origin-check.test.ts tests/app-router-production-build.test.ts tests/app-router-production-server.test.ts tests/app-router-rsc-flight-hint.test.ts tests/app-router-rsc-plugin.test.ts tests/app-router-static-export.test.ts tests/app-router-worker-entry.test.ts