80 Commits

Author SHA1 Message Date
Sékou Dayifourou KEITA d29ee0eebb fix(route-handlers): align Allow and default-export behavior with Next.js (#497)
* fix: ensure route handlers send correct Allow header

* Inline route handler Allow helpers in RSC entry

* fix: refresh entry snapshots and fmt

* fix(route-handlers): align Allow and default-export behavior with Next.js

* fix(route-handlers): align default-export behavior with Next.js per review

Follow-up for PR #497 review feedback (requested changes).

Changes:

- stop throwing on default export in route handlers

- log dev-only warning when default export is detected

- ignore default export for dispatch so default-only handlers return 405

- keep sorted Allow on implicit OPTIONS and no Allow on 405

- update integration/e2e tests, snapshots, and tracking notes
2026-03-13 12:16:46 +00:00
Nathan Nguyen f3994c655c Fix next/head key deduplication (#518)
* Fix next/head key deduplication

* Fix multi-child next/head deduplication

* Align next/head key normalization with Next.js
2026-03-13 10:11:44 +00:00
Divanshu Chauhan (divkix) e6439ffce8 refactor: phase 1/2 unify per-request ALS into a shared request context (#450)
* refactor: consolidate 5 nested ALS scopes into unified request context

Every App Router request previously ran through 5-6 nested
AsyncLocalStorage.run() calls (headers, navigation, cache-state,
private-cache, fetch-cache, plus optional execution-context). Each
ALS scope push/pop has measurable cost on Workers isolates.

Introduce a single UnifiedRequestContext that holds all per-request
state in one flat object, backed by one ALS instance. Each shim module
checks isInsideUnifiedScope() first and reads its sub-fields from the
unified store, falling back to its own standalone ALS when outside
(SSR environment, Pages Router, tests).

- Create unified-request-context.ts with createRequestContext(),
  runWithRequestContext(), getRequestContext(), isInsideUnifiedScope()
- Rewire _getState() in headers, navigation-state, cache, cache-runtime,
  fetch-cache, and request-context to dual-path (unified first, own ALS
  fallback)
- Make each runWith*() a no-op inside unified scope
- Replace 5-deep nesting in app-rsc-entry.ts (main handler + ISR regen)
  with single _runWithUnifiedCtx() call
- Export ensureFetchPatch() from fetch-cache for standalone patch install
- Add 17 unit tests for unified context (isolation, nesting, concurrency)

* test: update entry-templates snapshots for unified ALS context

Snapshots were stale after the unified request context refactor changed
the generated entry code (new imports, removed 5-deep nesting).

* test: update app router waitUntil assertion

* refactor: unify request ALS across router flows

* fix: address unified request context review feedback

* fix: address unified ALS review feedback

* fix: address remaining unified ALS review feedback

* fix: wrap Pages Router ISR regeneration in unified context

Ensures patched fetch and ALS-based context are available during
background regeneration, matching App Router behavior.

- Creates fresh unified context with execution context for regen
- Calls ensureFetchPatch() to enable cache tagging
- Adds test verifying _runWithUnifiedCtx and ensureFetchPatch presence
- Updates snapshot

Part of Phase 1.5: minimal Pages Router fix for ISR parity.

* Fix dev ISR unified context parity

* fix: address unified ALS review followups

* Fix pages router unified execution context seeding

* Fix Pages Router ISR rerender state isolation

* fix: address unified ALS review feedback
2026-03-13 07:19:13 +00:00
Nathan Nguyen 7e5894103f fix: serve metadata routes in dynamic segments (#500)
* fix: serve metadata routes in dynamic segments (opengraph-image, icon, etc.)

Metadata routes like opengraph-image.tsx inside dynamic segments
(e.g., app/blog/[slug]/opengraph-image.tsx) returned 404 because the
runtime matching used strict equality against the servedUrl which
contains literal bracket notation ([slug]).

Two fixes:
- Generate patternParts for metadata routes with dynamic segments so
  the runtime uses matchPattern() instead of strict equality
- Pass extracted params to the metadata function via makeThenableParams()
  matching Next.js behavior where dynamic metadata routes receive { params }

* fix: address review — scope patternParts to dynamic routes, fix indentation

- Only generate patternParts for dynamic metadata routes (static files
  don't use params)
- Add "keep in sync" comment for segment-to-pattern conversion
- Fix mis-indented try/catch braces in generated static metadata code
2026-03-12 18:56:06 +00:00
Nathan Nguyen ef9fa6876c fix: preserve query params on middleware rewrite in App Router (#504)
* fix: preserve query params on middleware rewrite in App Router

The middleware rewrite handler in app-rsc-entry.ts only extracted
pathname from the rewrite URL, silently discarding the query string.
All downstream code (searchParams props, useSearchParams(), navigation
context, ISR cache keys) continued using the original request's search
params instead of the rewrite destination's.

Fix: mutate url.search after extracting the rewrite pathname so all
~30 downstream url.searchParams references automatically pick up the
rewrite's query string. This matches the Pages Router's existing
behavior in server/middleware.ts which already preserves pathname+search.

* test: update entry-templates snapshot
2026-03-12 18:26:17 +00:00
Nathan Nguyen 38dc409a43 feat: Add Pages Router i18n domain routing (#471)
* Add Pages Router domain locale routing

* Add coverage for Pages Router domain locale behavior

* Format Pages Router i18n domain routing changes

* Handle basePath in Pages Router i18n domains

* Refactor Pages Router i18n domain fixtures

* Update Pages Router entry snapshot after rebase

* Align domain locale redirects with Next.js

* Clarify Pages Router domain locale helpers

* Fix case-insensitive locale prefixes

* Simplify same-domain locale URL handling

* Cover same-domain locale alias redirects

* Document Pages Router i18n invariants

* Guard preferred locale in domain redirects
2026-03-12 13:49:59 +00:00
Nathan Nguyen 8f8c2b1d99 fix: emit hashChangeStart/Complete and beforeHistoryChange router events (#468)
* fix: emit hashChangeStart/Complete and beforeHistoryChange events

Next.js emits hashChangeStart/hashChangeComplete for hash-only
navigation and beforeHistoryChange before every history.pushState/
replaceState call. Our router emitted neither.

- Hash-only push/replace now emit hashChangeStart before and
  hashChangeComplete after (instead of routeChange* events)
- Normal push/replace now emit beforeHistoryChange between
  routeChangeStart and the pushState/replaceState call
- All events now pass { shallow } as the second argument per Next.js
- Popstate handler also emits beforeHistoryChange
- Updated in both useRouter() hook and Router singleton code paths

* fix: emit full URL in hash change events, not just fragment

When router.push("#section") is called, emit the full resolved URL
(e.g., "/page#section") in hashChangeStart/hashChangeComplete events
instead of just "#section". Matches Next.js behavior where the event
URL includes the pathname.

* fix: handle hash-only popstate and address review feedback

- Detect hash-only back/forward in popstate handler by tracking
  pathname+search across navigations; emit hashChangeStart/Complete
  instead of routeChangeStart/Complete and skip unnecessary page fetch
- Add comment explaining beforeHistoryChange semantics in popstate
  (fires after URL change, emitted for API compatibility)
- Cache resolveHashUrl() result in all 4 hash code paths
- Add beforeHistoryChange negative assertions in hash-only tests
- Add E2E test for hash-only back/forward navigation events

* fix: pass { shallow } to routeChangeError, add goForward hash test

- routeChangeError now passes { shallow: false } to match Next.js
  signature (err, url, { shallow }), completing the { shallow }
  migration across all router events
- Add E2E test for hash-only goForward() popstate detection

* fix: include hash in popstate beforeHistoryChange URL, stabilize forward test

- beforeHistoryChange in popstate now emits appUrl + window.location.hash
  for consistency with push/replace paths that preserve the hash fragment
- Add waitForFunction between goBack() and clear-events in the forward
  test to prevent race between popstate handler and event clearing

* fix: strip basePath from full-path hash URLs, include hash in popstate event URLs

- resolveHashUrl now handles full-path hash URLs (e.g. /base/page#hash)
  by parsing and stripping basePath, not just fragment-only URLs (#foo)
- routeChangeStart and routeChangeComplete in popstate now include
  window.location.hash for consistency with beforeHistoryChange and
  the push/replace code paths

* fix: update _lastPathnameAndSearch after beforePopState check

Move tracker update to after beforePopState confirms navigation
proceeds. Previously, if beforePopState returned false (cancelling
navigation), the tracker was already updated to the new browserUrl,
causing the next popstate to compare against the wrong baseline and
potentially misdetect a route change as hash-only.
2026-03-12 11:33:23 +00:00
Nathan Nguyen 9bd7f811e0 fix: make useParams() reactive on client-side navigation (#489)
* fix: make useParams() reactive on client-side navigation

useParams() was reading a module-level variable directly instead of
subscribing via useSyncExternalStore, so navigating between dynamic
routes (e.g. /posts/1 → /posts/2) wouldn't re-render components.

Two changes:
- useParams() now uses useSyncExternalStore with the shared listener
  system, matching usePathname() and useSearchParams()
- setClientParams() now calls notifyListeners() when params change,
  so the external store knows to re-check snapshots

* chore: fix formatting in test fixture

* fix: stabilize useParams server snapshot and deduplicate subscribe

- Add _EMPTY_PARAMS constant to avoid creating a new {} on every
  getServerSnapshot call (Object.is would never match, causing
  hydration churn)
- Extract getClientParamsSnapshot/getServerParamsSnapshot named
  functions for consistency with usePathname/useSearchParams
- Extract subscribeToNavigation helper to deduplicate the identical
  subscribe callback across all three hooks
2026-03-12 09:23:10 +00:00
Nathan Nguyen f88d76a8a6 fix: middleware custom response corrupts binary bodies in dev (#485)
The dev server read middleware custom response bodies with `text()`,
which decodes bytes as UTF-8. This corrupts any binary response body
(images, protobuf, etc.) that middleware returns via `new Response(data)`.

The prod server already uses `arrayBuffer()` + `Buffer.from()` correctly.
Apply the same approach to the dev path.

Also adds middleware fixture routes for binary responses and multi-cookie
responses, with corresponding regression tests.
2026-03-12 08:26:13 +00:00
Stephen Zhou 3fc76e4dbe fix: add next shim js variants (#482)
* fix: add next shim js variants

* Update

* Update

* trigger ci

* trigger ci

* Update

* Update

* Update

* Update

* Update
2026-03-12 08:18:34 +00:00
Jared Stowell cb2d1c9ed1 fix: Link onNavigate URL resolution for relative hrefs (#483)
* Fix onNavigate relative href report

* Verify onNavigate relative href

* Fix onNavigate URL mismatch

* Review url-utils changes
2026-03-12 08:17:43 +00:00
Divanshu Chauhan (divkix) 9d0fc0d2fb fix: bind iterator methods on request headers proxy (#480) 2026-03-12 07:46:53 +00:00
James Anderson aba48698c2 feat(build): add Next.js-style route report to vinext build output (#477)
* feat(build): add Next.js-style route report to vinext build output

- Adds build/report.ts with regex-based route classification (static, ISR, dynamic, unknown, API)
- Clean build logger suppresses Vite/Rollup noise; keeps timing lines and errors
- --verbose flag bypasses filtering and shows raw output
- Routes sorted alphabetically by path (filesystem order)
- Legend sorted alphabetically by label
- Unknown routes show a note explaining classification will improve in a future release
- 55 unit tests covering all classification paths and formatting

* fmt

* fix exmaple

* support -v

* .

* .

* fix(build-report): address review comments

- dynamic='error' now classifies as static (not ssr) — it enforces static
  rendering by throwing if dynamic APIs are used, same as force-static
- add TODO comment on extractGetStaticPropsRevalidate regex matching too broadly
- drop -v short alias for --verbose to avoid collision with -v/--version at top level
- classifyPagesRoute now returns unknown on file read failure, consistent with classifyAppRoute
- single-route table now renders with ┌ corner instead of └ (i===0 check takes priority)
- 3 new tests covering the above fixes (58 total)

* test(build-report): replace misleading classifyAppRoute tests with real fixture coverage

The two tests named 'classifies page with revalidate=0 as ssr' and
'classifies revalidate=Infinity as static' both passed null paths and
asserted unknown, not actually exercising the stated code paths.

Replace both with proper integration tests using new fixture files:
- revalidate-zero-test/page.tsx (export const revalidate = 0) → ssr
- revalidate-infinity-test/page.tsx (export const revalidate = Infinity) → static
2026-03-11 19:58:06 +00:00
James Anderson 5b60a70f94 test: headers() and cookies() in server actions and route handlers (#473)
* 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
2026-03-11 18:07:54 +00:00
Nathan Nguyen b87ab02d58 feat: metadata parity for appLinks, iTunes, Twitter player/app cards (#452)
* 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>
2026-03-11 17:40:18 +00:00
Jared Stowell f360380112 fix: Pages API body parser for invalid JSON and repeated form keys (#446)
* Fix Pages API parsing parity

* Fix Pages API body parsing parity

* Fix empty form body parity

* Regenerate entry-templates snapshots after merge

* fix: propagate statusText through prod server sendCompressed non-compressed path

The sendCompressed function had a statusText parameter but only used it in
the compressed response path. The non-compressed else branch called
res.writeHead directly without forwarding statusText, so short error
responses (like 'Invalid JSON' at 12 bytes, well below COMPRESS_THRESHOLD)
lost their custom reason phrase and fell back to the default 'Bad Request'.

Fix: replace the direct res.writeHead call in the else branch with the
writeHead closure that already handles the statusText conditional.

Also set statusText on the ApiBodyParseError response in the pages server
entry template so the value is present for prod-server to forward.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 13:24:36 +00:00
Jared Stowell 16a14c713b fix: Pages prod config headers and redirects after middleware rewrites (#448)
* Fix headers matching original path

* Review redirect ordering parity

* Fix config redirects order

* Fix dev middleware parity

* Fix dev middleware request parity

* address review: worker header/fallback parity, early-return in attachRouteHandlerMiddlewareContext, cache node request headers, add dev redirect test

* address second-round bonk comments: sanitizeDestination in worker redirect, nodeRequestHeaders comment, app-rsc-entry guard comment, E2E redirect test, updated snapshots

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 11:40:07 +00:00
Jared Stowell 3b982066d0 fix: align Pages API body parsing and res.send(Buffer) (#428)
* Fix Buffer handling in Pages API res

* fix: skip reporting handled Pages API parse errors

* address review: blank line nit, content-length parity for Buffer, fix duplicate Content-Length in sendCompressed

* fix: set content-length for Buffer in production res.send() to match dev parity

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 11:16:51 +00:00
Jared Stowell f035e1a05e fix: object-form query array serialization in next/link and next/router (#457)
* Fix query param handling

* Fix scalar query serialization
2026-03-11 10:46:57 +00:00
Nathan Nguyen 45b72eade5 fix: honor the as parameter in router.push/replace (#453)
* fix: stabilize instrumentation state and hydration E2E tests

Move Cloudflare instrumentation example state from module-level
variables to globalThis so startup and request paths share one owner
across different module instances.

Add waitForHydration() helper to Pages Router E2E tests to eliminate
flaky button-click assertions that raced against React hydration.

* fix: honor the `as` parameter in router.push/replace

The `as` parameter in push() and replace() was silently ignored
(prefixed as _as) in both the useRouter() hook and the Router
singleton. This broke masked URLs and legacy dynamic-route flows.

Extract resolveNavigationTarget() helper that uses `as` when provided,
falling back to resolveUrl(url) otherwise. Apply to all four call
sites: useRouter().push, useRouter().replace, Router.push, Router.replace.

* fix: address PR review feedback

- Add doc comment to resolveNavigationTarget explaining the url/as
  simplification vs Next.js's two-value approach
- Extract waitForHydration to shared tests/e2e/helpers.ts
- Add pathname assertion to verify router.pathname stays as route
  pattern (/posts/[id]) after as-based navigation

* fix: remove pathname assertion that tests unimplemented behavior

router.pathname currently returns the resolved path after client-side
navigation, not the route pattern. The correct behavior (returning
/posts/[id] instead of /posts/42) requires the two-value url/as
approach tracked in #462.
2026-03-11 09:00:27 +00:00
Jared Stowell c4064ebb23 fix: next/form submitter overrides and query-string GET URLs (#426)
* Add submitter handling to form shim

* Update form shim URL validation

* Fix form submit attribute casing
2026-03-11 00:31:52 +00:00
Jared Stowell 0775ce55bb fix: return ReadonlyURLSearchParams from useSearchParams (#424)
* Update navigation search params

* Fix server readonly cache scope
2026-03-10 23:56:11 +00:00
Jared Stowell 6ccdaac78e fix: sitemap XML namespaces, video entries, and alternate-language links (#427) 2026-03-10 23:54:56 +00:00
Jared Stowell 88ed5550d1 fix: honor beforePopState when useRouter is mounted (#423) 2026-03-10 23:11:53 +00:00
Jared Stowell a6c5601cc2 fix: preserve repeated route and query params in rewrites and redirects (#422)
* Update redirect matcher replacements

* Fix rewrite param substitution

* fix nits
2026-03-10 22:28:36 +00:00
Divanshu Chauhan (divkix) d38661f809 fix: dedup RSC client references to prevent module duplication in dev (#413)
* fix: dedup RSC client references to prevent module duplication in dev

When the RSC plugin creates client-in-server-package-proxy modules for
"use client" submodules within packages, it re-exports from absolute
file paths. These bypass Vite's pre-bundling in the client environment,
causing the browser to load two separate module instances — one from the
raw ESM path and one from .vite/deps/. This breaks React context
providers/consumers since createContext() runs twice.

Add vinext:client-reference-dedup plugin that intercepts absolute
node_modules path imports originating from proxy virtual modules in the
client environment and redirects them through bare specifier imports,
which Vite's import analysis routes through pre-bundling automatically.

Closes #409

* fix: make dedup test layout a server component to exercise RSC proxy path

Remove "use client" from layout.tsx so ThemeProvider import goes through
RSC → client-in-server-package-proxy (absolute path), while the page's
useTheme goes through client pre-bundling (bare specifier). This ensures
the integration test actually validates the dedup plugin works.

* docs: address PR review feedback for client-reference-dedup plugin

- Document lossy submodule-to-package mapping assumption in resolveId
- Extract load filter regex to named DEDUP_FILTER constant
- Note default export edge case in generated re-export code
2026-03-10 20:22:35 +00:00
Jared Stowell 3a5fabe7ec fix: next/headers readonly semantics and legacy sync compatibility (#402)
* Add headers/cookies legacy tests

* Fix headers sync error handling

* Fix headers sync guards

* bonk

* Update packages/vinext/src/shims/headers.ts

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

* bonk

* Clarify dynamic error comment

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: James <james@eli.cx>
2026-03-10 20:21:55 +00:00
Jared Stowell 86fe0e7c7e fix: middleware object matcher semantics and i18n parity (#400)
* Fix middleware matcher request usage

* Handle object middleware matchers

* Fix middleware object matcher

* make ci happy

* Fix default locale middleware match

* Fix default-locale matcher logic

* Restore regexp matcher support

* Fix middleware regexp matcher
2026-03-10 20:03:39 +00:00
Jökull Sólberg Auðunsson 70ff9d5788 fix: handle single object for openGraph.images and twitter.images (#411)
* 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>
2026-03-10 15:19:54 +00:00
James Anderson 0faf62ac4c feat: support generateBuildId in next.config and inject build ID at runtime (#408)
* feat: support generateBuildId in next.config and inject build ID at runtime

Resolves generateBuildId() from next.config, falling back to a random UUID (ad-blocker-safe) when not configured or when the function returns null. The resolved ID is injected as process.env.__VINEXT_BUILD_ID via Vite define and embedded into __NEXT_DATA__ for Pages Router (dev, prod, and static export). NextRequest gains a buildId accessor for middleware deployment-skew detection.

* regen snaps

* refactor: extract safeUUID helper and drop buildId param from createSSRHandler

Address review nits: deduplicate the UUID+retry loop into a safeUUID() helper,
and read process.env.__VINEXT_BUILD_ID directly in dev-server instead of
threading it as an explicit parameter through createSSRHandler.

* regen snaps

* fix: stabilize entry-templates snapshot by setting generateBuildId in pages-basic fixture

The random UUID from resolveBuildId was embedded in the server entry snapshot,
making it fail on every run. Set generateBuildId to a fixed string in the
pages-basic test fixture so the snapshot is deterministic.

* fix: move buildId to NextURL to match Next.js API surface

Next.js exposes buildId on nextUrl (request.nextUrl.buildId), not directly
on NextRequest. Move the getter to NextURL and add a pass-through on
NextRequest for convenience. Update tests to assert both paths.
2026-03-10 13:59:00 +00:00
Nathan Nguyen c524f150cc feat: support generateSitemaps() for paginated sitemaps (#399)
* test: add products/sitemap.ts fixture with generateSitemaps

Adds a nested sitemap fixture that exports generateSitemaps() returning
two IDs (0, 1). The default export receives { id } and returns entries
scoped to that batch. Used by the integration tests for paginated
sitemap support.

* feat: support generateSitemaps() for paginated sitemaps

When a sitemap.ts module exports generateSitemaps(), the runtime
metadata route handler now:

1. Matches URLs at /{prefix}/sitemap/{id}.xml instead of the base
   /{prefix}/sitemap.xml (which is no longer served)
2. Calls generateSitemaps() to get the list of valid IDs
3. Validates the requested ID against the list (404 on mismatch)
4. Calls the default export with { id: rawId } and serializes the
   result with sitemapToXml()

This matches Next.js behavior: each paginated sitemap is a standalone
<urlset> document. Next.js does not generate a <sitemapindex>.

Closes #397

* test: add integration tests for generateSitemaps and update snapshots

- /products/sitemap/0.xml serves batch-0 entries
- /products/sitemap/1.xml serves batch-1 entries
- /products/sitemap/99.xml returns 404 (invalid ID)
- /products/sitemap.xml returns 404 (base URL not served)
- scanMetadataFiles discovers nested products/sitemap.ts
- Updated entry template snapshots for new generateSitemaps handling

* fix: address Codex review — typed id and single-segment matching

- Pass the original typed id from generateSitemaps() instead of the raw
  URL string, so numeric IDs stay numeric in the handler
- Reject multi-segment paths (e.g. /sitemap/a/b.xml) to avoid stealing
  requests from legitimate nested routes
- Fix shims.test.ts to find root sitemap by servedUrl, not first match

* fix: address ask-bonk review feedback

- Break long condition into multi-line for readability
- Add TODO for makeThenableParams when upgrading to v16 Promise params
- Add string ID ("featured") to fixture alongside numeric IDs
- Add test: missing .xml extension returns 404 (ported from Next.js)
- Add test: string ID ("featured") is served correctly
- Update entry template snapshots
2026-03-10 09:26:55 +00:00
Nathan Nguyen 490e7405ef fix: preserve middleware request header deletions (#382)
* fix: preserve middleware request header deletions

* test: refresh entry template snapshots

* test: cover mixed-router middleware header overrides

* fix: clone lazy headers by iteration

* test: cover rewrite override serialization
2026-03-10 08:08:05 +00:00
Nathan Nguyen 0db5c8d99b fix: avoid shared cache headers for dynamic GET handlers (#381) 2026-03-09 17:41:22 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

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

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
Nathan Nguyen cb66cc793c feat(next-intl): auto-detect config without createNextIntlPlugin (#366)
* fix: resolve NEXT_DATA type conflict with next package types

next/dist/client/index.d.ts declares Window.__NEXT_DATA__ as NEXT_DATA,
which conflicts with our inline type in global.d.ts. The vinext-specific
extensions (__vinext, __pageModule, __appModule) aren't in NEXT_DATA,
causing TS2339 errors.

Extract vinext extensions into a VinextNextData interface and cast at
the two usage sites (client/entry.ts, shims/link.tsx) instead of
re-declaring __NEXT_DATA__ with a conflicting type.

* feat(next-intl): auto-detect next-intl config without createNextIntlPlugin

next-intl's createNextIntlPlugin() crashes in vinext because it calls
require('next/package.json') to check the Next.js version, and next
isn't installed. Instead, vinext now detects next-intl in project
dependencies and registers the next-intl/config alias automatically.

Detection probes i18n/request.{ts,tsx,js,jsx} then
src/i18n/request.{ts,tsx,js,jsx}, matching next-intl's own discovery
order. Explicit aliases from webpack/turbopack config take precedence.

Also adds a targeted warning when config loading fails due to
next-intl/plugin, telling users the wrapper is unnecessary.

Closes #202

* fix: address review feedback

- Update check.ts: next-intl status "partial" → "supported"
- Extend VinextNextData from NEXT_DATA in shims/internal/utils.ts
  instead of duplicating fields
- Add JSDoc note about monorepo resolution walk-up behavior
2026-03-09 14:24:41 +00:00
James Anderson 10cc5a2968 fix: pass parent promise to generateMetadata() for ancestor metadata access (#376)
* 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.
2026-03-09 12:37:36 +00:00
Nathan Nguyen 405e46af1d fix: preserve middleware redirect headers in production (#365)
* fix: preserve middleware redirect headers (Set-Cookie) in production

middleware.ts correctly collects non-internal headers (e.g. Set-Cookie)
on redirect responses, but three consumers dropped them:

- prod-server.ts: sent only Location header, ignored responseHeaders
- deploy.ts: worker entry sent only Location header
- index.ts: inline runMiddleware codegen didn't collect headers on redirect
  (it already did for next() and rewrite(), just not redirect)

The dev server (index.ts:2934) handled this correctly, making this a
dev/prod parity bug where cookies set during middleware redirects worked
in development but were silently dropped in production.

* test: update entry template snapshot for redirect header fix

---------

Co-authored-by: James <james@eli.cx>
2026-03-09 09:52:45 +00:00
James Anderson 7fdcdd3c69 feat(og): add @next/og / @vercel/og support with Cloudflare Workers compatibility (#356)
* feat(og): add @next/og support with Cloudflare Workers compatibility

- Add vinext:og-font-fix transform plugin that patches @vercel/og/dist/index.edge.js
  at Vite transform time: inlines the fallback font as base64 (avoids fetch() with
  import.meta.url which breaks in workerd) and defers WASM init to first use (avoids
  WebAssembly.instantiate() being blocked in Node.js module runner)
- Add @vercel/og to optimizeDeps.exclude so Vite's esbuild pre-bundler doesn't cache
  the module before the transform hook runs
- Add vinext:og-assets build plugin that copies font/WASM assets to RSC output dir
- Add /app/api/og route to app-router-cloudflare example and app-basic fixture
- Add shared Playwright e2e spec (tests/e2e/og-image.spec.ts) asserting PNG validity,
  correct 1200x630 dimensions, param-driven content, and determinism
- Register og-image.spec.ts in app-router, cloudflare-workers, and cloudflare-dev
  Playwright projects (18 tests total, all passing)

* refactor(og): generalize fetch+import.meta.url asset inlining, drop WASM lazy-init patch

Replace the @vercel/og-specific vinext:og-font-fix plugin with a general
vinext:og-inline-fetch-assets plugin that:

- Matches ANY fetch(new URL('./asset', import.meta.url)).then(res => res.arrayBuffer())
  pattern, not just the hardcoded noto-sans filename
- Resolves each asset relative to the module's on-disk path (via the transform id)
  and inlines it as base64 — works for any font version or additional assets
- Applies to any library using this pattern, not just @vercel/og

Also removes the WASM lazy-init patch: now that @vercel/og is in optimizeDeps.exclude,
the module runs through workerd (not the Node.js pre-bundler cache) where
WebAssembly.instantiate() works fine. The lazy-init workaround was only needed
when the pre-bundler cache bypassed the transform hook.

The og-assets build plugin is updated to only copy resvg.wasm (not the font,
which is now inlined and will no longer appear in the bundle by filename).

* fix(og): inline readFileSync assets from @vercel/og node build

The production RSC bundle resolves @vercel/og under the 'import' export
condition, getting dist/index.node.js instead of dist/index.edge.js.
The node build loads font and WASM via fs.readFileSync(fileURLToPath(
new URL('./file', import.meta.url))), which breaks after bundling because
import.meta.url points to the bundle output file, not the dist/ dir.

Extend vinext:og-inline-fetch-assets with a second regex pattern that
matches readFileSync(fileURLToPath(new URL(...))) and replaces it with
Buffer.from('<base64>', 'base64'), inlining both noto-sans font and
resvg.wasm at transform time. This eliminates the ENOENT errors seen
in the production build CI tests.

* refactor(og): address code review feedback

- Remove Fix 1 (font inlining) from vinext:og-font-patch — it was dead
  code since vinext:og-inline-fetch-assets (earlier in the plugin array)
  already handles the same fetch(new URL(...)) pattern generically. Only
  Fix 2 (yoga WASM extraction) remains in og-font-patch.

- Use split().join() instead of String.replace() in both Pattern 1 and
  Pattern 2 loops of og-inline-fetch-assets. String.replace(string, ...)
  only replaces the first occurrence; split().join() replaces all of them,
  which matters for libraries that may use the same pattern multiple times.

* refactor(og): use replaceAll instead of split().join()
2026-03-08 20:50:10 +00:00
James Anderson ed263df6a8 fix: nav context hydration mismatch (usePathname/useSearchParams React error) (#355)
* 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
2026-03-08 16:54:40 +00:00
James Anderson f1e98b9ae5 fix(app-router): don't clear RSC context before lazy stream is consumed (#352)
* 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.
2026-03-08 16:08:45 +00:00
Nathan Nguyen 032a2afb41 fix: preserve next.config plugin aliases for next-intl (#342)
* fix: preserve next.config plugin aliases

* refactor: probe wrapped webpack config once

* fix: tighten next.config alias warnings
2026-03-08 15:30:05 +00:00
James Anderson a5d65b851f test: add a fixme for middleware invocation count tracking (#350)
* test: add a fixme for middleware invocation count tracking

* test: clarify why registerCalled uses ESM live binding not globalThis
2026-03-08 15:19:54 +00:00
James Anderson 53257a8630 fix: layout not receiving correct params (#348)
* fix: layout not receiving correct params

* fix: layout not getting correct params on 404
2026-03-08 14:59:06 +00:00
MD YUNUS 3a4ef70c25 fix: document ESM resolution behavior for validator and similar packages (#189) (#193)
Vinext now automatically detects installed packages with broken ESM
resolution and adds them to resolve.noExternal for both the rsc and
ssr environments. This fixes 'Cannot find module' errors when using
packages like validator that have extensionless internal ESM imports.

Root cause: Vite externalizes dependencies for SSR by default, but
some packages use extensionless imports (e.g. ./util/assertString)
that fail under Node.js strict ESM resolution. Additionally, Vite 7's
Environment API means ssr.noExternal only affects the ssr environment,
not the rsc environment where server components run.

Changes:
- Add KNOWN_PROBLEMATIC_ESM_PACKAGES list (validator, date-fns)
- Add collectNoExternalPackages() to merge user config with auto-detected packages
- Propagate noExternal to both rsc and ssr environments
- Add validator ecosystem test fixture and integration tests
- Add troubleshooting documentation

Fixes #189
2026-03-08 11:14:46 +00:00
Nathan Nguyen 5d595e08cb fix: rewrite useSelectedLayoutSegment(s) to use context-based child segments (#341)
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
2026-03-08 09:09:28 +00:00
James Anderson c4c67fa5f4 fix: pages router instrumentation error reporting for api routes (#334)
* test: pages router instrumentation

* fix error handling

* add pages route to app dir

* fix api route type
2026-03-07 23:22:13 +00:00
James Anderson 80acf27933 fix: register instrumentation in rsc entry and put error handler in global scope (#330)
* test: instrumentation

* store onRequestError on globalThis

* add instrumentation registration app-dev-server

* guard against double calling runinstrumentation for appdir
2026-03-07 22:40:27 +00:00
Nathan Nguyen a78266481b feat: support export const revalidate on App Router GET route handlers (#320)
* 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
2026-03-07 17:10:25 +00:00
Nathan Nguyen 8b356e4497 test: use metadata API in app-basic root layout (#316) 2026-03-07 15:14:33 +00:00
Divanshu Chauhan (divkix) 151f099089 fix: CSS imports from node_modules crash SSR (#310)
* fix: CSS imports from node_modules crash SSR (#270)

When a node_modules package imports a `.css` file, SSR crashes with
"Unknown file extension .css" because Vite externalizes node_modules
by default — causing Node's native ESM loader to handle the import,
which it can't do for non-JS files.

Set `noExternal: true` on all server environments (Pages Router SSR,
App Router RSC, App Router SSR) to force node_modules through Vite's
transform pipeline. This matches Next.js behavior of bundling
everything for SSR. Explicitly listed `external` packages (react,
satori, resvg, yoga-wasm-web) take precedence per Vite's rules.

Skipped for Cloudflare/Nitro targets which already bundle everything.

* fix: externalize better-sqlite3 and fix test hermiticity

- Add better-sqlite3 to ssr.external in better-auth fixture config.
  Native addon packages using `bindings` break when Vite transforms them
  (stack trace introspection fails). This mirrors Next.js's
  serverExternalPackages — users with native addons must externalize them.

- Fix Pages Router CSS test mutating real workspace node_modules.
  Previously symlinked tmpDir/node_modules → repo root, then created
  fake-css-lib inside the symlink target, leaking into the real workspace.
  Now creates a real node_modules dir and symlinks individual packages.

* fix: propagate user ssr.external into RSC and SSR environments

Vite 6+ treats `ssr.*` config as sugar for `environments.ssr.*` only —
it does NOT propagate to custom environments like `rsc`. This means
packages listed in `ssr.external` (e.g. better-sqlite3) were still
being transformed by Vite in the RSC environment, breaking native
addon resolution via the `bindings` package.

Read user's `ssr.external` array and merge it into both
`environments.rsc.resolve.external` and
`environments.ssr.resolve.external` so native addons are properly
externalized from all server environments.

Fixes CI failures in better-auth ecosystem tests on Linux where the
bindings package's stack trace introspection fails when the native
module is transformed by Vite.

* fix: address review comments on node-modules CSS SSR crash fix

- Fix wrong repo URL in test comment (nicepkg/vinext -> cloudflare/vinext)
- Propagate ssr.external: true into RSC and SSR environments correctly
- Move fake test packages from committed node_modules to __test_packages__
  with file: protocol dependencies, removing fragile force-tracked files
- Add CSS module (fake-css-module-lib) coverage to Pages Router test
- Narrow Pages Router test symlinks to only required packages

* fix: address bonk review comments

- Update stale comment on top-level ssr block to reflect both React
  externalization and noExternal: true behavior; document that user
  ssr.noExternal is intentionally superseded by vinext's setting
- Remove now-redundant noExternal: ['better-auth'] from better-auth
  fixture (vinext's blanket noExternal: true already covers it)
- Merge Pages Router test's two separate 'it' blocks (both hit same URL)
  and add missing fake-css-module-rendered assertion for .module.css
- Guard symlink .catch() to only swallow EEXIST errors, not all failures
- Add comment explaining appDir: tmpDir usage in Pages Router test

---------

Co-authored-by: James <james@eli.cx>
2026-03-07 15:06:04 +00:00