* fix: add missing set/delete/clear/size/toString to RequestCookies
The RequestCookies class in server.ts (used by NextRequest.cookies) was
missing mutation methods that @edge-runtime/cookies provides. Middleware
that calls cookies.set() or cookies.delete() to forward modified cookies
downstream would fail at runtime.
- Add set(), delete(), clear(), size, toString() methods
- Cache parsed map in constructor instead of re-parsing on every call
- Encode values with encodeURIComponent when syncing back to Cookie header
- delete() accepts string | string[] and returns boolean | boolean[]
matching @edge-runtime/cookies API
* refactor: DRY up RequestCookies clear/toString/syncHeader
Address review feedback:
- clear() delegates to _syncHeader() instead of inlining header deletion
- Extract _serialize() helper shared by toString() and _syncHeader()
Libraries that import `next/config.js` or `next/amp.js` (with the
explicit .js extension) failed to resolve to vinext shims. Every other
public next/* entrypoint already had both variants.
Adds a regression test that verifies all top-level next/* aliases have
a corresponding .js variant, preventing future omissions.
The App Router generated entry called mwFetchEvent.drainWaitUntil() but
discarded the return value, so middleware background promises registered
via event.waitUntil() were abandoned when the response was sent on
Workers. Register the drained promise with the execution context,
matching the Pages Router entry pattern.
* 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
* chore: upgrade vitest to 4.1
* fix: clear console.warn spy state before asserting call counts
Vitest 4 changed vi.spyOn to return the same mock when called on an
already-intercepted function. This means console.warn call records from
earlier tests accumulate in the spy, causing toHaveBeenCalledTimes(2) to
see 4 calls instead of 2.
Adding mockClear() after each spy creation resets the call count so the
assertions only reflect calls made within the test itself.
* refactor: eliminate as any / as unknown as type assertions
- shims/fetch-cache.ts: introduce ExtendedRequestInit type to replace
all (init as any)._ogBody casts and body duck-type-check casts
- shims/navigation.ts: add VinextNavigationError subclass with typed
digest field, removing all (error as any).digest writes; widen
_nativeReplaceState to | null and add null guard
- shims/constants.ts: remove (process.features as any) cast; fixed via
NodeJS.ProcessFeatures augmentation in global.d.ts
- shims/router.ts: remove listeners.get()! non-null assertion
- check.ts: remove pagesDir!, appDirPath!, importUsage.get()! non-null
assertions; use direct null-checks instead
- entries/pages-server-entry.ts: remove appFilePath!, docFilePath! non-
null assertions; use direct null-checks instead
- index.ts: drop 'unknown' step in two Readable.toWeb / proxyResponse.body
ReadableStream casts
- global.d.ts: augment NodeJS.ProcessFeatures with optional typescript
field
- cloudflare/kv-cache-handler.ts: add SerializedIncrementalCacheValue
family of types; update serializeForJSON / restoreArrayBuffers to use
them, eliminating ~10 as any casts
* fix: restore typeof guards in restoreArrayBuffers to preserve original behaviour
The refactor dropped the 'typeof value.rscData === "string"' style guards
from restoreArrayBuffers, causing it to call safeBase64ToArrayBuffer on
absent fields (undefined) and return null for entries that only have
{ kind } set — breaking the schema validation test.
Restore the typeof-string guards so the function only decodes fields that
are actually present, matching the original behaviour exactly.
Also make body/buffer optional in SerializedCachedRouteValue /
SerializedCachedImageValue to reflect runtime reality.
* fix: handle URL objects in metadata resolveUrl
The Next.js Metadata API types URL fields as `string | URL` (e.g.
`metadataBase`, `openGraph.url`, icon/image URLs). The internal
`resolveUrl()` function in `MetadataHead` only accepted `string`,
so passing a `URL` object (which is valid per Next.js types) crashed
with `url.startsWith is not a function`.
Add type coercion at the top of `resolveUrl()` to convert `URL`
objects to strings before processing.
Reproduction:
1. Set `metadataBase: new URL("https://example.com")` in a layout
2. Set `openGraph: { url: "/page" }` in a page's metadata
3. `resolveUrl` receives the URL object, calls `.startsWith()` on it
4. Crash: `url.startsWith is not a function`
With this fix, `URL` objects are coerced to strings via `.toString()`.
* fix: update Metadata types and rendering to accept string | URL fields
Address bonk review feedback:
- Add test covering URL object input for alternates.canonical and
openGraph.url to lock in the fix against regressions
- Update Metadata interface to type URL fields as string | URL,
matching Next.js types (openGraph.url/images/videos/audio,
twitter.images, icons, manifest, alternates.canonical/languages/
media/types)
- Add overload to resolveUrl() so it returns string when called with
a non-undefined argument, eliminating string | URL fallback issues
- Fix image/icon array normalisation to handle bare URL objects
alongside strings and descriptor objects
* fmt
* fix: remove dead-code ?? fallbacks and unnecessary test casts
Address bonk re-review feedback:
- Remove 'as unknown as string' casts in the URL-object test since the
Metadata interface now types those fields as string | URL
- Remove all 17 remaining 'resolveUrl(x) ?? x' fallbacks; resolveUrl
is overloaded to return string for any truthy input so these never
fired, and with string | URL fields the fallback value would have
passed a URL object directly to content=/href= attributes
* fmt
---------
Co-authored-by: James <james@eli.cx>
* 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
sitemapToXml() interpolated user-supplied values (URLs, video titles,
descriptions, uploader names) directly into XML without escaping. URLs
containing & (extremely common with query params) produced invalid XML
that search engines reject silently.
Add escapeXml() helper that handles all five XML special characters
(&, <, >, ", ') and apply it to every user-supplied value in the
sitemap serializer.
This intentionally diverges from Next.js's resolveSitemap() which also
raw-interpolates, producing malformed XML.
Adding benchmarks/* to the workspace in the vite v8 bump caused pnpm to
hoist next out of benchmarks/nextjs/node_modules into the monorepo root.
Turbopack then couldn't find next/package.json from the project directory,
failing with a workspace root inference error.
Fix by listing benchmarks/vinext and benchmarks/vinext-rolldown explicitly
instead of using the glob, leaving benchmarks/nextjs isolated with its own
node_modules.
Also remove the eslint key from next.config.ts — it was removed from the
NextConfig type in Next.js 16 and caused an 'Unrecognized key' warning.
* 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
pendingRegenerations and revalidateDurations were plain module-level
Maps, giving each Vite environment (RSC, SSR) its own copy. This meant
dedup didn't work across environments — both could trigger regeneration
for the same key simultaneously.
Use Symbol.for() on globalThis to share a single Map instance, matching
the pattern already used in fetch-cache.ts.
* fix: dedup stale background refetches in fetch cache
Every concurrent request hitting a stale cache entry was independently
firing its own originalFetch(), creating a thundering herd on popular
endpoints. Add a per-cache-key dedup map (pendingRefetches) shared
across RSC/SSR environments via Symbol.for(), matching the existing
ISR regeneration dedup pattern in isr-cache.ts.
Also adds a 60s timeout safety net that force-cleans dedup entries
when upstream fetches hang, with identity checks in both .finally()
and setTimeout to prevent slot-ownership races.
* test: add fetch cache stale dedup concurrency tests
- Concurrent stale hits trigger only one background refetch
- Completed refetches allow new ones (lifecycle test)
- Failed refetches clean up dedup entry (error-path test)
- Hung fetches are force-cleaned after timeout
- Late-settling hung fetch does not evict replacement refetch
- _resetPendingRefetches() called in beforeEach for test isolation
* Update packages/vinext/src/shims/fetch-cache.ts
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* fix: repair broken setTimeout structure from review suggestion
The applied suggestion duplicated the setTimeout block and broke
brace nesting. Fix the structure and also clear the timeout in
.finally() so completed refetches don't leave dangling timers.
* fix: wrap fake timer tests in try/finally for safety
If a test throws before vi.useRealTimers(), subsequent tests would
run under fake timers and break. Wrap both fake timer tests in
try/finally to guarantee cleanup.
* fix: guard background revalidation against caching error responses
The stale-while-revalidate background refetch unconditionally cached
whatever response came back, including 500s. A transient server error
would permanently replace good cached data. Add a `response.ok` guard
matching the existing cache-miss path.
* fix: tighten cache guard to status === 200
Per review feedback, narrow the cache guard from `response.ok` (2xx)
to `response.status === 200` in both the background revalidation path
and the cache-miss path. Non-200 success codes (201, 204, 206) are
not meaningful to cache in the fetch cache context.
---------
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
The i18n locale context (locale, defaultLocale, domainLocales,
hostname) was stored on bare globalThis properties, which are
shared across concurrent requests. Under concurrent traffic
(Cloudflare Workers or Node.js production server), request A's
locale could be overwritten by request B before A finishes
rendering, causing Link components to produce incorrect
locale-prefixed URLs.
Introduce ALS-backed i18n state following the existing pattern
used by router-state.ts and head-state.ts:
- i18n-context.ts: bridge module with fallback globalThis
accessors and a registration API for ALS-backed overrides
- i18n-state.ts: server-only module that registers ALS-backed
accessors on import, providing per-request isolation
link.tsx now reads locale context via getI18nContext() instead
of bare globalThis, dev-server.ts sets context via ssrLoadModule
for the SSR environment, and the production entry wraps
rendering in runWithI18nState().
* fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries
MemoryCacheHandler.set had an inconsistent guard on data.revalidate:
the ctx path checked `revalidate > 0` but the data path only checked
`typeof data.revalidate === "number"`, allowing revalidate:0 to set
revalidateAt = Date.now() — making entries immediately stale.
This was inconsistent with KVCacheHandler which already had the > 0
guard on both paths. The two handlers would produce different staleness
behavior for the same input: Memory → immediately stale, KV → no TTL.
Add the missing `> 0` guard to align both code paths and both handlers.
* fix: revalidate:0 should skip cache storage entirely
Both MemoryCacheHandler and KVCacheHandler had inconsistent handling of
revalidate:0. In Next.js, revalidate:0 means "don't cache." But the
handlers would still store entries — Memory made them immediately stale
(data path lacked > 0 guard), KV cached them forever (both paths
rejected 0, so revalidateAt=null meant no expiry).
Resolve effective revalidate from both ctx and data paths (data
overrides ctx), and early-return without storing when it's 0. Both
handlers now use identical logic.
Fix existing test data that incidentally used data.revalidate:0 in
tests meant to exercise stale-while-revalidate and tag invalidation.
computeInterceptTarget() used path.dirname() to climb parent directories,
but Next.js defines interception levels in terms of route segments. Route
groups like (marketing) are filesystem directories but invisible in the
URL, so path.dirname() would waste a climb on them.
Now walks backward through segments, counting only visible ones (skipping
route groups, parallel slots, and ".").
Also consolidates three copies of invisible-segment detection into a
single isInvisibleSegment() helper.
* fix: support onLoadingComplete in modern next/image shim
Add the deprecated-but-still-supported onLoadingComplete prop to the
modern next/image component, matching Next.js behavior where the
callback receives the underlying HTMLImageElement on load.
- Add onLoadingComplete to ImageProps interface and type declarations
- Wire handleLoad into all render paths (local, custom loader, remote)
- Destructure onLoad/onLoadingComplete in getImageProps to prevent leak
* fix: correct callback order and add remote URL test
- Swap onLoad/onLoadingComplete invocation order to match Next.js
(onLoad fires first, then onLoadingComplete)
- Fix stale comment that described legacy { naturalWidth, naturalHeight }
- Add remote URL (UnpicImage) SSR test for full path coverage
* fix: parse :param(constraint) in middleware matchers
The middleware matcher compiler in compileMatcherPattern() treated any
pattern containing ( as a raw regex, so /blog/:id(\d+) was compiled as
the literal regex ^/blog/:id(\d+)$ — matching "/blog/:id123" instead
of "/blog/123".
Port the constraint-aware extractConstraint() approach from
config-matchers.ts: when a ( follows a :param name, extract it as an
inline regex constraint and emit a proper capture group. Patterns
without :param( syntax (e.g. /((?!api|_next).*)) still take the raw
regex path.
Also supports :param(constraint)? for optional constrained segments
(e.g. /:locale(en|es|fr)?/about matches both /about and /en/about).
Mirrored in middleware-codegen.ts to keep runtime and generated code
in sync.
* fix: support constraints on catch-all params and add negative optional test
Address review feedback:
- Add :param*(constraint) and :param+(constraint) support so catch-all
params can also have inline regex constraints (e.g. /:path*(api|static)).
Update hasConstraints detection to match :param[*+]?( patterns.
- Add negative test for optional constrained params: /de/about must not
match /:locale(en|es|fr)?/about since de is not in the alternation.
* 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.
* 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
On Cloudflare Workers the isolate can terminate after the Response is
sent, killing in-flight error reports (e.g. Sentry HTTP requests).
Fix by having reportRequestError() self-register with
ctx.waitUntil() via getRequestExecutionContext(). This is the same
pattern used by KVCacheHandler, ISR regeneration, and fetch cache
revalidation. On Node.js (dev or vinext start) the context is null
so it's a no-op — fire-and-forget is fine because the process stays
alive.
Since reportRequestError never rejects (internal try/catch), the
redundant .catch() at all 5 call sites is removed.
* fix: Pages Router ISR background regeneration re-renders HTML
Background regeneration only re-ran getStaticProps but cached the old
HTML, so server-rendered content was permanently stale after the first
revalidation cycle. The pageData JSON was updated but never used to
re-render the page.
Now the regeneration callback:
- Re-renders the page component with fresh props
- Rebuilds __NEXT_DATA__ with fresh props
- Updates the revalidate duration map (dev server)
- Passes full locale context to getStaticProps
Fixed in both dev server and production server entry.
* ci: retrigger checks
* fix: preserve document shell during prod ISR regeneration
The prod entry's regeneration was hardcoding a simple HTML suffix,
losing custom _document content between <Main /> and <NextScript />.
Now splits the cached HTML at known markers (<div id="__next"> and
__NEXT_DATA__ script) to preserve the full document structure: head
section, gap content, and tail are carried forward from the original
cached entry.
Also updates the entry-templates snapshot.
* ci: retrigger checks
* ci: retrigger checks
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.
If SSR begins streaming then throws, res.writeHead(500) is called on an
already-sent response, causing a silent socket error. The App Router path
already has this guard — add the same check to the Pages Router path.
* fix: exclude private folders (_prefix) from App Router route discovery
Next.js treats underscore-prefixed directories in app/ as "private
folders" that are excluded from routing. This is a recommended pattern
for colocating non-route files (e.g. _components, _lib, _utils).
vinext was only excluding @-prefixed slot directories, so files like
app/_components/page.tsx would incorrectly create routes.
Add the underscore check to all four App Router filesystem scan points:
- Main page/route discovery (scanWithExtensions exclude callback)
- Slot sub-page scanner (already had it)
- Intercepting route scanner (scanForInterceptingPages)
- Intercepting page collector (collectInterceptingPages)
* chore: format app-router.ts
* fix: add headersSent guard in API route error handler
If a Pages Router API handler partially writes a response (e.g.
res.writeHead(200)) and then throws, the catch block would crash with
ERR_HTTP_HEADERS_SENT when trying to write a 500/413 response.
This matches the existing guard pattern in prod-server.ts and index.ts.
* fix: end response when headers already sent but stream not closed
If a handler calls res.writeHead()/res.write() then throws, headersSent
is true but writableEnded is false. Without res.end() the connection
hangs until timeout. Now we close the response in that case.
* chore: retrigger CI
* fix: call reportRequestError in Pages Router prod entry
Pages Router production (pages-server-entry.ts) was missing
reportRequestError() calls in the SSR and API catch blocks.
Dev (dev-server.ts, api-handler.ts) and App Router prod
(app-rsc-entry.ts) both reported errors, so instrumentation
hooks like Sentry's onRequestError silently stopped firing
in Pages Router production only.
* fix: use original URL path in SSR error report, not locale-stripped
routeUrl has the locale prefix stripped by extractLocale(), so i18n
requests like /fr/about would be reported as /about. Use url (the
original request path) to match dev-server.ts behavior.
* 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
* 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: warn on external rewrites and sanitize Content-Disposition
Add a startup warning when next.config contains rewrites targeting
external origins (e.g. https://...), alerting that credential headers
(cookie, authorization, x-api-key, proxy-authorization) are forwarded.
Sanitize contentDispositionType in image optimization and prod server
to only allow "attachment" — any other value defaults to "inline",
preventing header injection via arbitrary config values.
Closes#414
* fix: improve external rewrite warning and add Content-Disposition sanitization test
Use proper singular/plural in the external rewrite warning message
instead of "rewrite(s)". Add test assertions verifying the warning
includes the source pattern and arrow separator. Add a test confirming
that invalid contentDispositionType values default to "inline".
* fix: correct external rewrite warning to reflect actual header forwarding
proxyExternalRequest copies all request headers, not just credential
headers. Updated warning text to accurately state this and suggest
server-side fetch as an alternative. Tightened test assertion for
exact plural form.
* fix: address PR review feedback on external rewrite warning
Clarify why API routes are preferable (control over forwarded headers),
tighten test assertion to prevent false singular/plural match.
Refs: #430
* fix: refine external rewrite warning wording and add source path assertions
Mention route handlers alongside API routes in the warning suggestion,
and assert that source paths (/proxy1, /proxy2, /proxy3) appear in the
multi-rewrite warning message.
* fix: strip credential headers from outbound proxy requests
External rewrites via proxyExternalRequest now strip authorization,
cookie, proxy-authorization, and x-api-key headers before forwarding
to third-party origins. This prevents leaking user credentials to
external services.
Updated the warning message to reflect that credentials are stripped
(not forwarded) and updated tests to assert the new behavior.
Closes#430
* fix: align app-router credential stripping test with implementation
The integration test in app-router.test.ts was missed when c4a128d added
credential header stripping to proxyExternalRequest. Assertions now
correctly expect credentials to be undefined (stripped), matching the
unit test in shims.test.ts and the actual implementation.
* fix: keep Next.js rewrite forwarding
* fix: restore Next.js external rewrite header forwarding
* perf: convert og-inline-fetch-assets transform to async I/O with per-build cache
The `vinext:og-inline-fetch-assets` plugin used synchronous `fs.readFileSync()`
in Vite's transform hook, blocking the transform pipeline. This converts both
Pattern 1 (fetch inlining) and Pattern 2 (readFileSync inlining) to use
`await fs.promises.readFile()` and adds a per-build `Map<string, string>` cache
so repeated reads of the same file (common with shared fonts) hit memory instead
of disk.
* fix: use strict undefined check in og-inline cache and restore spies via afterEach
Replace falsy `if (!fileBase64)` with `if (fileBase64 === undefined)` in both
og-inline patterns to correctly handle hypothetical empty-string cache values.
Add `afterEach(() => vi.restoreAllMocks())` to prevent spy leaks on assertion
failure, replacing the manual mockRestore() call.
* fix: address og-inline review feedback
* style: format og-inline test assertions
* fix: TPR zone resolution for multi-part TLD domains
Replace hardcoded 12-entry MULTI_PART_TLDS list with progressive
domain candidate lookup. For "shop.example.co.uk", tries co.uk →
example.co.uk → shop.example.co.uk until a Cloudflare zone matches.
This fixes zone resolution for all public suffixes (.org.au, .co.in,
.gov.uk, etc.) without maintaining a TLD list. Common TLDs (.com, .io)
still resolve on the first API call.
Also fixes the missing fallback — the previous code returned null on
miss despite a TODO comment claiming fallback behavior existed.
* chore: format test file
* perf: replace O(n) linear route matching with radix trie
Every request triggered 3-6 linear scans over the full route list, calling
matchPattern() for every route until a match. For apps with many routes
this means thousands of comparisons per request.
This replaces the linear scan with a trie-based O(depth) lookup in all
4 codebases that share the matching logic:
- pages-router.ts (scanner module, dev time)
- app-router.ts (scanner module, dev time)
- app-rsc-entry.ts (generated entry, runtime)
- pages-server-entry.ts (generated entry, runtime)
The trie enforces correct precedence at each node via traversal order:
static > dynamic > catch-all > optional catch-all. Recursive DFS with
backtracking ensures dead-end branches fall through to alternatives.
matchPattern is retained only in app-rsc-entry.ts for the intercept
lookup (typically 0-5 entries, not worth trie-ifying).
* test: update entry-templates snapshot and assertion for trie-based matching
The server entry no longer inlines matchPattern — it imports the trie
module instead. Update the explicit assertion to check for buildRouteTrie
and trieMatch, and regenerate all entry template snapshots.
* fix: address review comments on trie-based route matching
- Correct "radix trie" → "trie (prefix tree)" in doc comment (no path compression)
- Remove unused `routes` parameter from `matchRoute` in app-rsc-entry
- Replace stream-of-consciousness test comment with concise explanation
* fix: update matchRoute regex in test to match new 1-arg signature
The test was asserting the old `matchRoute(url, routes)` signature in
the generated code. Updated to match the new `matchRoute(url)` signature
after the unused `routes` parameter was removed.