When a server action id resolved from request headers carries the
`#<exportName>` suffix (e.g. `/app/foo.ts#bar`), `loadServerAction()`
splits the id at `#` and calls `requireModule(file)` with the module
path only. `@vitejs/plugin-rsc`'s reference-validation virtual module
therefore emits `[vite-rsc] invalid server reference '/app/foo.ts'`,
omitting the `#<exportName>` portion. The previous match in
`isServerActionNotFoundError` compared the full actionId against the
Vite error string, so missing/stale references surfaced as a generic
500 instead of the Next.js-compatible 404 with the
`x-nextjs-action-not-found` header.
Also accept the pre-`#` module-path form for both the dev-time
`[vite-rsc] invalid server reference '<id>'` and prod-time
`server reference not found '<id>'` error shapes.
Adds a focused unit test reproducing the dev-mode case and integration
tests asserting that throwing `notFound()` from a server action returns
404 (both fetch and progressive form paths), porting fixture from
Next.js: test/e2e/app-dir/actions/app/server/form.js.
When `app/global-not-found.tsx` is defined, route-miss 404s render that
module standalone (it ships its own `<html>`/`<body>` and replaces the
root layout). Previously the entry registered global-not-found.tsx as a
static `import * as`, which co-located its CSS with the root layout's CSS
in a single chunk. Lightningcss then dropped overlapping declarations
between the two stylesheets as dead code, so any rule global-not-found
defined that the layout also defined was silently removed — breaking the
CSS cascade for 404s.
Switching the entry to `() => import("…/global-not-found.tsx")` puts the
module in its own JS chunk and lets the SSR pipeline (and `Resources`
metadata) treat its CSS independently of the rest of the app. The
fallback renderer now accepts a `loadGlobalNotFoundModule` loader (cached
per worker) instead of an eagerly-imported module.
Also adds a regression test asserting the SSR markup for `/does-not-exist`
links only global-not-found's stylesheet — never the root layout's — so
the cascade can never resolve to the wrong color.
Ported from Next.js: test/e2e/app-dir/initial-css-order/initial-css-order.test.ts
When notFound() or redirect() is called from inside a Suspense boundary
the error surfaces after the shell has already streamed, so the not-found
/ redirect intent is communicated to the client via inline <meta> tags
injected into the head section of the stream.
vinext was emitting these as `<meta name="robots" content="noindex" />`
(with a space before the self-closing slash), but Next.js serializes them
via React's HTML renderer, which produces `<meta ... content="..."/>`
without the space. The Next.js navigation tests assert on that exact
substring, so this mismatch caused ~5 deploy-suite failures.
Drop the extra space so the streamed meta tags match Next.js byte-for-byte,
and add ported regression tests against the suspense fixtures.
Ref: packages/next/src/server/app-render/make-get-server-inserted-html.tsx
Ref: test/e2e/app-dir/navigation/navigation.test.ts (SEO describe block)
When a route handler called `cookies().set()` alongside a returned
`Response` that carried raw `Set-Cookie` headers without a `Path`
attribute, vinext appended the raw line verbatim. The resulting comma-
joined `set-cookie` view then read like `bar=bar2, baz=baz2` with no
per-cookie `Path=/`, diverging from Next.js whose `appendMutableCookies`
re-runs every returned cookie through `ResponseCookies.set` (which
normalises a missing `path` to `/`).
`applyMutableCookieFallbacks` now ensures each returned cookie carries
an explicit `Path=` attribute, matching Next.js's behaviour in
`packages/next/src/server/web/spec-extension/adapters/request-cookies.ts`.
Ported the Next.js coverage from
`test/e2e/app-dir/actions/app-action.test.ts` ("should support setting
cookies in route handlers with the correct overrides") as
`nextjs-compat/set-cookies.test.ts`.
Fixes#1484
* fix(basepath): enforce scoping on rewrites/redirects/routes
Sub-issues addressed under #1333:
1. Rewrites/redirects/headers were firing on requests outside the
configured `basePath`. Threaded a `BasePathMatchState` through the
matchers and prod-server / deploy worker / dev plugin so default
rules (no `basePath: false` opt-out) only evaluate when the request
was under basePath, and opt-out rules only evaluate outside it.
5. Pages Router requests that landed outside basePath fell through to
internal route matching. After redirects and beforeFiles rewrites
run, the request now 404s when it was outside basePath and no
`basePath: false` rule rewrote it — matching Next.js's
`resolve-routes.ts:304-309`.
4. With rewrites no longer firing on out-of-basepath requests, the
downstream code that prepends basePath to destinations no longer
sees a stripped-but-already-internal pathname, eliminating the
`/docs/docs/...` doubling for the rewrite-driven cases.
App Router uses the same gating; `basePath: false` rules don't yet
fire there because `normalizeRscRequest` 404s out-of-basepath requests
before the matchers see them — flagged as follow-up in the rule
gating site. External rewrite proxying (sub-issue #2) and static
asset doubling (sub-issue #3, partly addressed by #1337/#1383) are
deferred.
Refs #1333
* test: update deploy.test signature assertions and format basepath tests
* review: address bonk nits — add inverse basePath:false tests and TODO comment
Next.js exposes `params` as `null` (effectively — via `params || null`
or `params ? await params : null` user-side) to route handlers and
`getServerSideProps`/`getStaticProps` on routes with no dynamic
segments. vinext was passing an empty object, which is truthy and
breaks user code that branches on `params`.
The fix computes a user-facing params value at each dispatch boundary
(`null` when `route.isDynamic === false`, otherwise the matched
object) while leaving internal bookkeeping — navigation context, query
merging, `useParams()` — on the empty-object shape that those
consumers expect.
References the Next.js test suite assertions:
- test/e2e/app-dir/app-routes/app-custom-routes.test.ts
"does not provide params to routes without dynamic parameters"
- test/e2e/edge-pages-support/index.test.ts
"should have correct query/params on index"
Closes#1350
When middleware or a next.config.js rewrite redirects an external URL
to an internal route target, `usePathname()` (and the navigation
context that backs it) was returning the internal rewrite destination
instead of the canonical/external path the user requested.
Preserve the pre-rewrite pathname as `canonicalPathname` and use it
when populating the App Router navigation context, while leaving
`cleanPathname` to drive internal route matching. This matches Next.js
behavior: test/e2e/app-dir/hooks/hooks.test.ts —
"should have the canonical url pathname on rewrite".
Closes#1348
* refactor(router): quarantine navigation runtime topology
* fix(router): restore manifest-backed interception runtime
App Router browser navigations lost first-hop intercepted route context after snapshot topology stopped acting as fallback proof. That made /feed to /photos/:id request the direct page payload instead of the declared modal payload.
The violated invariant was that route topology must come from the manifest, while snapshots only carry runtime proof. Resolve first-hop interception from the manifest, move RSC bootstrap streams onto the symbol-backed runtime, teach prerender extraction that script shape, and harden the runtime contract around route manifest and RSC data.
Coverage includes runtime seam tests, prerender extraction tests, planner/browser navigation tests, package rebuild, and the app-router intercepted routes E2E slice.
* fix(router): narrow prerender RSC script parsing
The prerender extractor accepted any script starting with the symbol-runtime bootstrap expression, then parsed it as though it had the exact RSC chunk-push prefix. That was harmless at the current head, but it made future runtime scripts a foot-gun.
Only the exact runtime chunk prefix and exact done marker are now accepted. A focused extractor test covers non-chunk runtime scripts that share the bootstrap expression prefix.
* fix(router): address navigation runtime review
* fix(router): resolve navigation runtime review threads
Mirror Next.js's App Router behavior at
.nextjs-ref/packages/next/src/client/script.tsx:298-376: when SSR-rendering
`<Script src={...} />` with `strategy="afterInteractive"` (the default)
or `strategy="beforeInteractive"`, call `ReactDOM.preload(src, { as: 'script', ... })`
so React Float hoists `<link rel="preload" as="script" href={src} />` into
`<head>`. The script is then fetched in parallel with the streamed HTML.
Before this change, the SSR branch of the Script shim only emitted a
`<script>` tag for `beforeInteractive` and returned `null` for every other
strategy, with no preload. The matching Next.js fixture test
test/e2e/app-dir/app-esm-js/index.test.ts ("should be able to render
nextjs api in app router") asserts:
expect($('head link[href="/test-ext.js"]').length).toBe(1)
expect($('head link[href="/test.js"]').length).toBe(1)
…which fails today because no preload links are emitted.
The existing Image shim already uses this exact React Float pattern
(packages/vinext/src/shims/image.tsx:285-286), so the SSR pipeline
already supports Float hoisting.
Tests:
- Updated existing Script SSR tests in tests/script.test.ts and
tests/shims.test.ts that asserted afterInteractive/default rendered
empty — they now assert the preload <link> is emitted (no <script>
tag), matching Next.js.
- Added tests/nextjs-compat/script-preload.test.ts that ports the
app-esm-js fixture's preload assertions against the app-basic
/script-nonce page (which already exercises afterInteractive and
beforeInteractive strategies via the existing fixture).
- crossOrigin handling matches React's type contract
("anonymous" | "use-credentials" | undefined). React normalises
"anonymous" to `crossorigin=""` in HTML; tests accept both forms.
All shim / script / page / document / nonce tests pass (1151 tests).
Calling `draftMode()` and reading `isEnabled` are plain reads and must not
opt the route out of static prerendering. Only `enable()` / `disable()`
mutate state and need to be tracked as dynamic, matching Next.js's
`trackDynamicDraftMode` (see
.nextjs-ref/packages/next/src/server/request/draft-mode.ts:152-165).
Before this change, any App Router page that called `draftMode()` — even
just to check `isEnabled` — set `dynamicUsageDetected = true`, which:
- caused build-time speculative prerender to skip the route
- caused the request-time ISR HTML write to bail out
Tests for the matching Next.js fixture
(test/e2e/app-dir/draft-mode/draft-mode.test.ts, "should not generate
rand when draft mode disabled during next start") fail today because
vinext returns a fresh random value on every request instead of the
cached prerender. After this change, the page is prerendered/cached
correctly, and `enable()`/`disable()` still bail out of caching as
expected.
The existing test that enforced the old (wrong) behavior is updated to
match Next.js, and dedicated assertions are added that `enable()` and
`disable()` each mark dynamic usage.
When users define app/global-not-found.tsx, route-miss 404s should render
that module standalone — it provides its own <html>/<body> and replaces
the root layout, matching the behavior of Next.js 16's
experimental.globalNotFound feature.
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/global-not-foundhttps://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/app-render.tsx#L495-L520
Implementation:
- Discover app/global-not-found.{tsx,ts,jsx,js} alongside the existing
global-error scan (src/index.ts).
- Thread the path through generateRscEntry via AppRouterConfig and
buildAppRscManifestCode so the generated entry imports it and binds
it as globalNotFoundModule.
- Pass the module into createAppFallbackRenderer; renderHttpAccessFallback
switches to the global-not-found document when statusCode === 404, the
route is null (route-miss), and no boundary override was supplied.
- Add a skipLayoutWrapping flag to renderAppPageHttpAccessFallback so
the standalone document isn't wrapped in the root layout, and so the
RSC payload omits layout-id entries that would otherwise refer to a
layout tree the markup doesn't contain.
Page-triggered notFound() calls keep the existing path: they still
resolve through the regular not-found.tsx boundary inside layouts —
global-not-found.tsx only takes over for route-miss 404s.
Tests:
- tests/app-fallback-renderer.test.ts adds four unit tests covering
the new branch (route-miss with global-not-found, page-call with
global-not-found, absent global-not-found, 403/401 unaffected).
- tests/entry-templates.test.ts adds four codegen assertions covering
the manifest import and the generated-entry wiring in both directions
(present and absent).
- tests/nextjs-compat/global-not-found.test.ts ports the basic Next.js
fixture as an integration test against a dev server, asserting that
the route-miss 404 renders global-not-found's <html> standalone with
exactly one <html>/<body>, and that page-call notFound() does not
use the global-not-found document.
App Router file-based metadata URLs (icon, opengraph-image, twitter-image,
apple-icon, favicon, manifest) and server-action redirect targets did not
include the configured next.config basePath. Next.js prefixes basePath into
both surfaces — bakes it into static metadata route URLs at webpack build
time and threads it through the dynamic image loader's pathnamePrefix, and
applies addPathPrefix() to redirect URLs in app-render and action-handler.
Fixes for two of the five Next.js parity gaps surfaced by
test/e2e/app-dir/app-basepath/index.test.ts:
- Metadata image URLs: opengraph-image, icon, apple-icon, twitter-image,
favicon now emit '/<basePath>/<route>/<file>?<hash>' in the rendered
<head>. Applies to both static (build-time resolved) and dynamic
(per-request) metadata file routes via a shared post-resolution helper
in applyFileBasedMetadata. Idempotent: hrefs that already start with the
configured basePath are left untouched. External URLs (http://, https://,
//, data:) are never prefixed.
- Manifest URL: the <link rel="manifest"> href is prefixed with basePath
via the same helper, so an app with basePath='/base' emits
'/base/manifest.webmanifest' instead of '/manifest.webmanifest'.
- Server-action redirect (progressive form POST): the Location header
is computed from the basePath-prefixed redirect target so
redirect('/foo') from a server action mounted at /base/... lands the
browser at /base/foo.
- Server-action redirect (RSC): the ACTION_REDIRECT_HEADER value is
prefixed with basePath. The client-side handler reads this header and
calls window.location.assign/replace verbatim, so the value must
already be a fully basePath-prefixed pathname.
Adds a shared applyActionRedirectBasePath() helper exported from
app-server-action-execution and a prefixMetadataHrefWithBasePath() helper
in file-based-metadata. Both are idempotent, external-aware, and no-op
when basePath is unset.
Surfaces still pending follow-up (tracked separately):
- Server-side redirect() from page renders ALREADY works via
applyAppPageRedirectBasePath in app-page-execution. No change needed.
- RSC fetch URLs ALREADY work because Link, router.push, and
useRouter all funnel through toBrowserNavigationHref() which
prepends basePath before createRscRequestUrl().
- Pages Router redirect basePath (separate code path in
pages-server-entry) — not covered here.
Ported test cases live in tests/nextjs-compat/basepath.test.ts.
See:
https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/loaders/metadata/discover.tshttps://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/loaders/next-metadata-image-loader.tshttps://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.tshttps://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-basepath/index.test.ts
## 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
* fix(app-router): validate RSC cache-busting params
App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request.
Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions.
Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration.
* fix(app-router): hide RSC cache tokens from navigation URLs
* fix(app-router): hide RSC cache token from searchParams
* fix(app-router): address RSC cache-busting review
* fix(app-router): preserve HTML redirect destination for direct .rsc navigations
Config redirects from next.config.js were unconditionally converted to
.rsc destinations for any request whose URL ended in .rsc. This broke
direct browser navigations to .rsc URLs (e.g. a bookmark or external
link) because the browser was redirected to an RSC payload it could not
render.
Client-side RSC fetches always send the RSC: 1 header, so restrict the
.rsc destination conversion to requests that carry that header. Direct
browser hits without the header continue to redirect to the canonical
HTML URL, matching the pre-PR behaviour for config redirects.
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.
* fix(app-router): scope layout params and propagate layout head errors
Layouts received full route params, layout generateMetadata failures were swallowed, and layout-thrown forbidden()/unauthorized() fell back through the not-found boundary path. Those diverge from Next.js when nested layouts depend on segment params, when head generation fails before page render, or when HTTP access APIs are thrown from a layout.
The generated RSC entry now delegates param slicing, head resolution, and parent access-boundary selection to typed runtime helpers. The helpers keep route-specific imports in the entry while moving the behavioral core into unit-tested modules.
Tests port Next.js layout params, global-error, forbidden, and unauthorized coverage into focused helper and integration regressions.
* test(app-router): cover layout viewport error boundaries
Layout generateViewport() failures now intentionally share the same boundary path as layout generateMetadata() failures. Copilot flagged that this behavior change needed direct coverage.
Add dev and production-preview compat assertions for co-located error.tsx handling and global-error escalation, with fixtures that throw from layout generateViewport().
* chore(app-router): address head review nits
App route handlers appended cookies().set() mutations after cookies already present on the returned Response. That made same-name mutable cookies override the Response cookies, which differs from Next.js route-handler semantics.
Next.js treats mutable cookies as fallbacks and reapplies returned Response cookies as final values. Deduplicate vinext pending mutable cookies by name, filter out pending cookies shadowed by the returned response, and then reappend the returned Set-Cookie headers last.
Adds focused helper coverage plus a nextjs-compat fixture that exercises the live App Route path.
* fix: RSC compatibility for dynamic() and layout segment context
Three related fixes for React Server Component environments:
1. **dynamic.ts: Remove "use client" and add RSC async path**
The `"use client"` directive forced `next/dynamic` into a client
component boundary, but `dynamic()` should work in server components
too. In the RSC environment, `React.lazy` is not available (the
`react-server` condition exports a stripped-down React). Added a
runtime check: when `React.lazy` is not a function, use an async
server component pattern instead (the RSC renderer natively supports
async components).
Also switched from destructured imports (`lazy`, `Suspense`,
`useState`, `useEffect`) to `React.lazy`, `React.Suspense`, etc.
to avoid importing names that don't exist under the `react-server`
condition.
2. **layout-segment-context.tsx: Remove "use client"**
This module is imported directly by the RSC entry. The `"use client"`
directive created a client component boundary that breaks the RSC
rendering pipeline. `getLayoutSegmentContext()` already returns
`null` when `React.createContext` is unavailable (RSC), and the
`LayoutSegmentProvider` gracefully falls back to passing children
through unchanged.
3. **app-rsc-entry.ts: Wrap route handler params with makeThenableParams**
Next.js 15+ changed route handler params to be async (Promises).
Route handlers that `await params` crash when params is a plain
object. `makeThenableParams()` wraps the object so it's both a
Promise and has synchronous property access.
* chore: fix formatting and update entry-templates snapshots after merge with main
* fix: restore 'use client' in layout-segment-context to fix useSelectedLayoutSegment(s)
Removing 'use client' caused LayoutSegmentProvider to run in the RSC
environment where React.createContext is undefined. getLayoutSegmentContext()
returned null, the provider became a no-op, and useSelectedLayoutSegments
always returned [] instead of the actual segments.
The 'use client' boundary is required so the RSC bundler renders this
component in the SSR/browser environment where createContext works.
* test: add regression tests for RSC dynamic() and route handler await params
Add two regression tests for the fixes in PR #466:
1. Route handler await params (Next.js 15 async params pattern):
Tests /api/catch-all/[...slugs] which uses `await params` in the
handler — verifies that makeThenableParams() correctly wraps params
so both await and direct property access work.
2. dynamic() in RSC (async component path):
Adds a pure server component fixture that uses dynamic() without
"use client". In the RSC environment React.lazy is unavailable, so
dynamic() must fall back to the async component pattern. Tests that
the dynamically-loaded component renders in the HTML output.
* fix: address bonk review comments on dynamic.ts RSC async path
- Add comments explaining LoadingComponent is intentionally ignored in
the RSC async fallback path (parent Suspense boundaries handle loading
states; error handling defers to nearest error boundary)
- Add comment explaining the 'as unknown as ComponentType<P>' cast is
safe because the RSC renderer natively supports async components, but
TypeScript's ComponentType<P> doesn't account for async return types
- Add unit tests for the AsyncServerDynamic path by stubbing React.lazy
to undefined (simulating a react-server condition without lazy).
Verifies: displayName, async resolution, bare component exports,
and that LoadingComponent is correctly ignored.
Note: React 19.x exports React.lazy from the react-server condition,
so typeof React.lazy !== 'function' does not trigger in current React.
The AsyncServerDynamic path is defensive forward-compatibility code.
The unit tests stub React.lazy to exercise it directly.
* chore: remove unused vi import from dynamic.test.ts
* docs: correct inaccurate React.lazy/RSC comments throughout
React 19.x exports React.lazy from the react-server condition, so the
typeof React.lazy !== 'function' guard does not trigger in current
React. The AsyncServerDynamic path is defensive forward-compatibility
code only — it does not execute today.
Update all comments that incorrectly claimed React.lazy is unavailable
in the react-server environment:
- dynamic.ts: top-level doc comment and guard comment
- dynamic-rsc.tsx fixture: comment explaining the RSC path
- rsc-dynamic/page.tsx fixture: explain which path actually executes
- nextjs-compat/dynamic.test.ts: clarify that the integration test
exercises LazyServer + Suspense, not AsyncServerDynamic
---------
Co-authored-by: James <james@eli.cx>
* fix: usePathname() returns "/" during SSR of "use client" page components (#688)
The navigation shim uses a registration pattern where navigation-state.ts
calls _registerStateAccessors() to upgrade navigation.ts from module-level
state to ALS-backed state. This only updates the specific module instance
that imported navigation-state.ts.
In Vite multi-environment dev mode, "use client" components can end up with
a separate module instance of navigation.ts (due to pre-bundling or different
resolver chains). That instance never receives the ALS-backed accessors and
falls back to _serverContext (null), causing usePathname() to return "/".
Fix: navigation-state.ts now also stores the accessor functions on globalThis
via Symbol.for("vinext.navigation.globalAccessors"). The default accessors in
navigation.ts check this global before falling back to module-level state.
This ensures all module instances can reach the ALS-backed state regardless
of which instance _registerStateAccessors was called on.
On the browser (where navigation-state.ts is never imported), the global key
does not exist, so the fallback to module-level _serverContext is preserved —
which is the correct behavior for client-side hydration.
Closes#688
* refactor: deduplicate global accessor key and tighten types
- Export GLOBAL_ACCESSORS_KEY from navigation.ts and import in
navigation-state.ts to prevent silent drift if the Symbol string
ever changes
- Add satisfies constraint on the accessor object so shape mismatches
are caught at compile time
- Assert warm-up fetch status in test to surface fixture setup failures
* feat: full next/compat/router support and fix check scanner for shimmed modules
- Export NextRouter interface from shims/router.ts so compat-router.ts can use it
- Add NextRouter | null return type to useRouter() in shims/compat-router.ts
- Add next/compat/router, next/form, next/web-vitals, next/constants to IMPORT_SUPPORT in check.ts so vinext check no longer flags them as unsupported
- Add integration fixture pages and components for next/compat/router in both pages-basic and app-basic fixtures
- Add integration tests in tests/pages-router.test.ts and tests/nextjs-compat/hooks.test.ts
- Add unit tests in tests/check.test.ts covering all four newly-registered imports
* test: update entry-templates snapshots for compat-router-test fixture page
* remove type assertion
* fix: add declare module for next/compat/router to fixture next-shims.d.ts files
* fix: add declare module for next/compat/router to internal next-shims.d.ts
* pin to vp setup 1.0.0
* Add production coverage for global-error compat
* Address global-error review feedback
* docs: update tracker notes for tests #3 and #4 to reflect production preview coverage
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* feat: add ISR caching for App Router route handlers
Route handlers with `export const revalidate = N` now get server-side
ISR caching (MISS/HIT/STALE with background regeneration), matching
the existing page ISR behavior. Previously only Cache-Control headers
were emitted — the handler re-executed on every request.
- Add `__isrRouteKey` helper (suffix "route") for cache key construction
- ISR cache READ before handler execution (HIT skips handler entirely)
- ISR cache WRITE on MISS via `response.clone()` + `waitUntil`
- STALE serves stale data and triggers background regen with clean
context (synthetic request, empty headers/cookies)
- Guards: production-only, GET/HEAD only, skips when handler is dynamic
(uses headers()/cookies()), skips when handler sets own Cache-Control,
filters Infinity from revalidateSeconds
- Reuses existing CachedRouteValue (kind: "APP_ROUTE") and KV serialization
* chore: update entry-templates snapshots for route handler ISR
* test: add auto-HEAD ISR cache test for route handlers
* fix: review findings — force-dynamic guard, consistent header filtering in cache writes
* test: poll for ISR background regen instead of fixed sleep
Replace fixed 1000ms sleep with a polling loop (up to 5s) to wait
for the background regen to complete. The fixed sleep was flaky
because regen timing varies with system load. Also bumps the stale
wait from 1500→2000ms for slow CI.
* fix: capture request.url before regen closure to avoid pinning Request
Hoist request.url into a local const (__revalUrl) before the
background regeneration closure. This lets the original Request
object (with its body stream and headers) be GC'd sooner on
memory-constrained Workers isolates.
* fix: address remaining review findings
- Hoist url.searchParams into __revalSearchParams before the regen
closure so the outer URL object can be GC'd during background regen
(same class of fix as the request.url hoisting)
- Add test for force-dynamic + revalidate route handler guard
- Add comment noting order-dependent test cache state
* test: add regression tests for headers() and cookies() in server actions and route handlers
* test: address review comments on action-headers tests
- Remove ghost test that always passed via 404 bailout
- Remove unused getHeaderFromActionSync fixture export
- Fix extractReturnValue to parse full first line instead of greedy {.+} match
- Fix action ID extraction to match by export name instead of array index
- Add comment clarifying why the route handler test is distinct from existing ones
* test: address second round of review comments
- Use fetchHtml helper for page fetches (consistent with other nextjs-compat tests)
- Clarify page.tsx comments: wrapper closures discard return values, named
export tests are needed for value assertions
* feat: add metadata support for appLinks, iTunes, and Twitter player/app cards
Add four missing Next.js metadata features to the Metadata type and
MetadataHead renderer:
- appLinks: renders al:* property tags (ios, iphone, ipad, android,
windows_phone, windows, windows_universal, web)
- itunes: renders apple-itunes-app name tag with app-id and app-argument
- twitter player cards: renders twitter:player, twitter:player:stream,
twitter:player:width, twitter:player:height
- twitter app cards: renders twitter:app:name/id/url per platform
(iphone, ipad, googleplay)
Includes fixture pages and tests ported from Next.js test suite.
* chore: format metadata files
* fix: resolve twitter player/app and appLinks URLs against metadataBase
* fix: un-nest twitter app card name/url checks to match Next.js behavior
In Next.js, the name, id, and url checks in the twitter app card
platform loop are independent. Our implementation nested name and url
inside the id check, so a platform without an id wouldn't get name or
url tags. This un-nests them to match Next.js parity.
Also adds the missing twitter:app:name:ipad test assertion.
* Update packages/vinext/src/shims/metadata.tsx
---------
Co-authored-by: James Anderson <james@eli.cx>
* fix: handle single object for openGraph.images and twitter.images in metadata shim
Next.js allows `openGraph.images` and `twitter.images` to be a single
object (e.g. `{ url, width, height }`) in addition to a string or an
array. The metadata shim only handled string and array cases, causing
"imgList is not iterable" when a single object was passed.
Normalize single objects to arrays before iterating. Also update the
Metadata type to reflect the full range of accepted shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fmt
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again
* refactor: delete app-dev-server.ts and update all references to entries/ directly
The file was already a pure re-export shim with no logic of its own.
Update every import/dynamic-import site to point straight at the
individual entry modules:
entries/app-rsc-entry.ts ← generateRscEntry, AppRouterConfig
entries/app-ssr-entry.ts ← generateSsrEntry
entries/app-browser-entry.ts ← generateBrowserEntry
Sites updated:
packages/vinext/src/index.ts
tests/entry-templates.test.ts
tests/app-router.test.ts
tests/shims.test.ts
Also update stale comments in:
server/middleware-codegen.ts, server/request-pipeline.ts,
server/instrumentation.ts, shims/metadata.tsx,
tests/rsc-streaming.test.ts, tests/nextjs-compat/rsc-context-lazy-stream.test.ts,
examples/app-router-cloudflare/instrumentation*.ts
* docs: update app-dev-server.ts references in markdown files to entries/
* fix: pass parent promise to generateMetadata() for ancestor metadata access
Implements Next.js's eager-execution-with-serial-resolution pattern for
generateMetadata(): all calls kick off concurrently but each segment's
parent promise resolves serially (root → leaf), so `await parent` inside
a child's generateMetadata correctly gets the accumulated ancestor metadata.
Fixes#375
* regen snaps
* fix: address review comments — remove dead array, pass searchParams to generateMetadata
- Remove unused layoutParentPromises[] array (dead code pointed out in review)
- Pass searchParams to page's resolveModuleMetadata() — the URL query string was
previously always passed as undefined, so generateMetadata() could never read
searchParams from the URL
- Reuse the already-computed spObj for pageProps.searchParams to avoid double iteration
- Add 2 regression tests for searchParams in generateMetadata
* regen snaps
* regen snaps
* fix: pass actual route params to layout generateMetadata() in fallback render
renderHTTPAccessFallbackPage was always passing {} as params to
resolveModuleMetadata(), so layouts with generateMetadata({ params })
would get undefined for all dynamic route segments (e.g. params.slug)
when rendering not-found/forbidden/unauthorized fallback pages.
Fix: use opts?.matchedParams ?? route?.params ?? {} (same expression
already used for the layout component props just below) so that
generateMetadata() in fallback layouts receives the real matched params.
Adds two regression tests to not-found.test.ts: hit an invalid slug that
triggers notFound() and assert the title contains the actual slug value.
Also updates entry-templates snapshots.
* fix: forward params to resolveModuleViewport in renderHTTPAccessFallbackPage
generateViewport({ params }) in a layout wrapping a not-found/error
boundary now receives the actual route params instead of an empty object.
Mirrors the same fix already applied to resolveModuleMetadata.
Adds generateViewport export to the layout-params-notfound fixture and
two regression tests confirming the themeColor includes the real slug.
draftMode() reads the __prerender_bypass cookie, which is
request-specific data. Without calling markDynamicUsage(), a Server
Component branching on draftMode().isEnabled could have its output
cached by ISR — serving draft content to regular users or stale
content to draft users.
headers() and cookies() already call markDynamicUsage(); draftMode()
was the only request-reading API that didn't.
When a server component, client SSR, or generateMetadata() throws with no
local error.tsx boundary, global-error.tsx renders as the last resort. It
provides its own <html> and <body> (replacing the root layout), matching
Next.js behavior.
Previously, global-error was wrapped inside the root layout in both paths:
1. In-tree ErrorBoundary in buildPageElement — the root layout's <html>
was already serialized in the RSC stream before the ErrorBoundary
activated during SSR, producing double <html>/<body> tags.
2. renderErrorBoundaryPage — explicitly wrapped the error component with
layouts regardless of whether it was a local error.tsx or global-error.
Fix:
- Remove global ErrorBoundary from buildPageElement (it can't work for
SSR because the RSC stream already contains the layout output).
- Add it conditionally for RSC requests only (client-side navigation
needs it for error recovery after hydration).
- In renderErrorBoundaryPage, track whether the resolved component is
global-error and skip layout wrapping when it is.
Strengthen test assertions to match Next.js behavior:
- Assert HTTP 200 (error boundary "handles" the error)
- Assert global-error.tsx content renders (data-testid, error message)
- Assert exactly one <html> and one <body> tag per response
* fix: nav context hydration mismatch
* fix: address bonk review — add __VINEXT_RSC_NAV__ type, serialise searchParams as entry pairs
- Add __VINEXT_RSC_NAV__ global type declaration to global.d.ts with
searchParams typed as [string, string][] (array of entry pairs)
- Change generateSsrEntry() to serialise searchParams via
[...navContext.searchParams.entries()] instead of Object.fromEntries(),
preserving duplicate keys (e.g. ?tag=a&tag=b)
- Update tests to assert the new array-of-pairs shape; new URLSearchParams()
on the client correctly handles both single and duplicate keys
* fix(app-router): don't clear RSC context before lazy stream is consumed
* fix(app-router): remove premature context clears from server action and intercepting route RSC branches
Same bug as fixed for renderHTTPAccessFallbackPage/renderErrorBoundaryPage: context
was cleared immediately after renderToReadableStream() returned, before the stream
was lazily consumed by the client. Async server components running during stream
consumption would see null context and fail.
Two additional instances fixed:
- Server action RSC re-render branch (after action.apply())
- Intercepting route RSC response branch
The getAndClearPendingCookies() / getDraftModeCookieHeader() calls in the server action
branch are intentionally preserved — they read synchronously from context set during
action execution (not during stream rendering) and must run before the response is sent.
Context cleanup is handled naturally by the AsyncLocalStorage scope from
runWithHeadersContext() unwinding after all async continuations complete.
The previous depth-based approach sliced the pathname by a numeric URL
depth, which broke when route groups were present — they don't appear
in URLs but Next.js includes them in the segments array. This also
meant dynamic params were returned as raw URL strings rather than
resolved values matching the route tree.
Replace `layoutSegmentDepths: number[]` with two new fields:
- `routeSegments: string[]` — filesystem segments from app/ root
- `layoutTreePositions: number[]` — directory depth per layout
Each layout now receives pre-computed child segments through React
context (including route groups, with dynamic params resolved to
actual values and catch-all segments joined with "/").
Additional fixes found during code review:
- Pass matched params to error/fallback rendering paths so dynamic
segments resolve correctly during error page rendering
- Skip optional catch-all segments when param is empty/undefined
- Extend routeSegments for parallel slot sub-routes
- Update stale type declaration in next-shims.d.ts
Ported from Next.js: test/e2e/app-dir/hooks/hooks.test.ts
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/hooks/hooks.test.ts
* chore: add .worktrees/ to gitignore
* feat: support `export const revalidate` on App Router GET route handlers
Read the route segment config `revalidate` from route handler modules
and set `Cache-Control: s-maxage=N, stale-while-revalidate` on GET
responses, matching Next.js behavior. Only applies when the handler
does not set its own Cache-Control header.
Unskip two E2E fixme cases and add vitest coverage.
* fix: guard revalidate=0 and add proper test fixtures
- Skip Cache-Control injection when revalidate is 0 (means "never cache")
- Add /api/custom-cache fixture that sets its own Cache-Control header
- Add /api/no-cache fixture with revalidate=0
- Fix "does not override" test to use a fixture that actually sets Cache-Control
- Add revalidate=0 test case
* Sanitize redirects/headers and use execFileSync
Replace execSync calls with execFileSync (pass argv arrays) to avoid shell interpolation and adjust package-install logic to split package manager and args. Sanitize redirect destinations in SSR paths to prevent protocol-relative open-redirects and use sanitizeDestinationLocal for Next.js redirects. Ensure middleware receives a cleaned pathname by stripping internal .rsc suffix before creating NextRequest and add a test for this behavior. Strip sensitive headers (cookie, authorization and x-middleware-*) from proxied external requests and add a test. Document cache key hazards for auth headers in fetch-cache, add a security note for dangerouslySetInnerHTML in head SSR, and set the Draft Mode cookie to include Secure in production (with tests for production/dev behavior).
* Rename middleware header to x-custom-middleware
Change middleware header name from x-middleware-test to x-custom-middleware across fixtures and tests, and clarify server comment about stripping x-middleware-* headers from responses (reserved for internal routing signals). Files updated: packages/vinext/src/server/prod-server.ts (comment tweak), tests/fixtures/pages-basic/middleware.ts (header set), and tests/pages-router.test.ts (expectations and test titles/comments). This ensures tests reflect the new public header name while preserving internal x-middleware-* semantics.
* Normalize leading slashes and strip headers
Normalize redirect/rewrite destinations by collapsing any leading slashes or backslashes to a single "/" to avoid protocol-relative redirects and handle browser-backslash behavior. Strip additional sensitive headers (x-api-key, proxy-authorization) from proxied requests and ensure all x-middleware-* internal headers are removed from responses (broadened from x-middleware-request-*). Update dev/server sanitization logic accordingly. Update tests and fixture middleware to use shortened x-mw-* header names and add tests covering backslash normalization.