mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.8
1420 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e3090b08fa |
fix: dev overlay browser sourcemap stacks (#1731)
* Add browser runtime sourcemap regression test * Update pnpm lockfile * Fix dev overlay browser sourcemap stacks * Move dev stack sourcemap resolution to server * Fix knip unused stack endpoint export * Fix dev stack sourcemap proxy host handling * test: cover dev stack sourcemap helpers * fix: remove unused sourcemap type export * refactor: share dev stack sourcemap endpoint * chore: trigger ci rerun * fix: cap dev stack sourcemap request body |
||
|
|
610cf2e63a |
fix(app-router): honor per-response dynamic stale times on the client (#1712)
* fix(app-router): honor per-response dynamic stale times on the client App Router navigation reused and evicted cached RSC responses using one global prefetch TTL, so pages with different unstable_dynamicStaleTime values could not stay fresh independently. Traversal also needed a route instance boundary so browser history restores hidden state instead of reusing a forward-navigation component instance. The client cache now snapshots dynamic stale-time response metadata, computes expiry per entry, preserves staleAt across cache replays, and derives mounted-slot cache variants from the decoded payload. Browser traversal keeps route instances under React Activity while normal navigations allocate fresh route instances, matching Next's BFCache/stale-time contract. * fix(link): make prefetch dedupe freshness-aware for navigation prefetches The old prefetched.has(cacheKey) gate in prefetchUrl returned early for any exact key match, including stale entries. This meant an expired prefetch with the same cache key would never reach hasPrefetchCacheEntryForNavigation, so it would never be deleted and never refreshed. The new helper is built to check resolvePrefetchCacheEntryExpiresAt, delete stale entries, and return false. But the Link path bypassed it when the old prefetched Set contained the exact key. Fix: gate the exact-key check on the helper's freshness decision. When the helper reports the exact entry is stale and deletes it, also remove the key from the prefetched Set so the fall-through path can issue a fresh fetch. Keep the second helper call for equivalent _rsc variants. Add an integration test proving visible Links re-prefetch when their exact cache entry goes stale, not just that the helper reports stale entries as unavailable. * refactor(app-router): collapse redundant prefetch/visited-cache gates Address review feedback on the client dynamic-stale-time changes: - link.tsx: the prefetch dedupe path called hasPrefetchCacheEntryForNavigation twice when an exact entry already existed. Collapse to a single freshness-aware gate that covers both the exact entry and equivalent _rsc variants. - app-browser-entry.ts: drop the redundant early refresh return in getVisitedResponse; isVisitedResponseCacheEntryFresh already bypasses the cache for refresh navigations (and the miss path evicts the entry). * docs(app-router): document intentional refresh eviction in visited cache Per review feedback: getVisitedResponse now routes refresh navigations through isVisitedResponseCacheEntryFresh (the single source of truth), which returns false for refresh and therefore falls through to the read eviction. Document at the fall-through delete that a refresh intentionally drops any prior snapshot, since the navigation re-fetches and re-stores a fresh one. * docs(navigation): clarify load-bearing prefetch-cache dual checks Per review feedback, document two checks that look superficially redundant but preserve observable contracts: - isPrefetchCacheEntryCompatibleWithMountedSlots: the two clauses accept either the entry's pinned request-time slot context or the server-declared snapshot header; they diverge in the prefetchRscResponse case. - consumePrefetchResponse expiresAt guard: only synthesize expiresAt onto the returned snapshot when the entry already carried one, so entries without an explicit expiry round-trip unchanged for callers/tests. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
48e932e615 |
feat(cache): split CDN and data cache adapters; add Cloudflare edge adapter (#1693)
* feat(cache): split CDN and data cache adapters; add Cloudflare edge adapter Separate two caching concerns behind distinct adapters: - Data cache handler (existing CacheHandler): fetch, "use cache", unstable_cache. Canonical get/setDataCacheHandler; get/setCacheHandler kept as deprecated aliases. - CDN cache adapter (new): page-level ISR serving strategy — readPage/writePage, buildResponseHeaders (header map), ownsBackgroundRevalidation, revalidate. DefaultCdnCacheAdapter delegates storage to the data cache, so default behavior is unchanged. Page/route ISR now routes through the CDN adapter (isr-cache, app-page-cache); revalidateTag/revalidatePath/updateTag invalidate the data cache and purge the CDN adapter (default no-op). Add CloudflareCdnCacheAdapter (edge-managed): readPage null / writePage no-op, ownsBackgroundRevalidation false, emits CDN-Cache-Control for SWR + Cache-Control: no-store (no browser storage) + Cache-Tag, and purges via the request-context cache. Auto-selected via a global detector when the request context exposes a cache handle; explicit setCdnCacheAdapter wins. The request-context type stays generic (cache: unknown). Adds next.config cdnCacheHandler (symmetric with cacheHandler) and updates the worker codegen to setDataCacheHandler. * address review: drop config plumbing; refine Cloudflare cache headers - Remove the cdnCacheHandler next.config plumbing entirely (deferred); next-config.ts is back to baseline. - CloudflareCdnCacheAdapter: emit the edge directive on CDN-Cache-Control as 'public, max-age=…, stale-while-revalidate=…' (max-age, not s-maxage, so the edge caches + SWRs), and set the browser-facing Cache-Control to 'public, max-age=0, must-revalidate' so a browser never serves a stored copy without revalidating against the edge. * chore: fix formatting (vp check) for cache adapter files * feat(cache): route App Router route handlers + Pages Router ISR through the CDN adapter Closes the two parity gaps from review: route-handler and Pages Router ISR responses now emit the CDN adapter's headers (CDN-Cache-Control + Cache-Tag on edge adapters) instead of a hardcoded Cache-Control. - Add shared applyCdnResponseHeaders() in cache-control.ts; app-page-cache now uses it (drops its local helper). - Route handlers: applyRouteHandlerRevalidateHeader (fresh) and buildRouteHandlerCachedResponse (HIT/STALE) go through the adapter; routeTags hoisted in execution so the fresh response carries Cache-Tag. - Pages Router: fresh ISR response emits a path-based Cache-Tag (matching revalidatePath); HIT/STALE served response routes through the adapter. - CloudflareCdnCacheAdapter: guard so non-cacheable policies (no-store/no-cache/private) are never promoted to CDN-Cache-Control. Default behavior unchanged (adapter yields a single identical Cache-Control). * address review: simplify applyCdnResponseHeaders + strip CDN headers from stored route values - applyCdnResponseHeaders now clears only Cache-Control (the header vinext stamps internally); the adapter's own headers are applied via set() which overrides, so pre-clearing adapter-specific headers was redundant and presumptuous (per review). - buildAppRouteCacheValue strips cdn-cache-control / cache-tag so CDN policy headers are never baked into a stored route value (re-derived from the adapter on every served response). - pages-page-data buildPagesCacheResponse now uses applyCdnResponseHeaders (Headers) for consistency with every other call site. * revert presumptuous CDN header strip in buildAppRouteCacheValue Hardcoding cdn-cache-control/cache-tag in the store denylist presumes a specific adapter's header names (the same presumption rejected for applyCdnResponseHeaders) and is also unnecessary: CDN policy headers never reach a stored route value — the edge adapter's writePage is a no-op and the default adapter never emits them. Back to the original denylist. * example(workers-cache): add demo app for route-cache testing (#1695) * example(workers-cache): add demo app for route-cache testing Adds the examples/workers-cache-cloudflare demo app from feat/route-cache-request-context so the route-cache CDN adapter changes on this branch can be tested. * ci: trigger PR workflows on any base branch, not just main Drop the `branches: [main]` filter from the pull_request trigger in ci.yml, deploy-examples.yml, and preview-release.yml so these workflows run on PRs against any base branch (e.g. stacked PRs). * update lockfile * ci(deploy-examples): add workers-cache-cloudflare to deploy matrix Pull in the deploy matrix + preview-URL comment entry from feat/route-cache-request-context so the workers-cache demo app gets built, deployed, and linked on PR previews. * fix(cache): auto-select edge CDN adapter from request context, no import needed The edge-managed CDN cache adapter was only activated when a detector got registered as a side effect of importing `vinext/cloudflare`. Apps with a hand-written worker entry (e.g. the workers-cache demo) never imported it, so ISR silently fell back to the origin-managed default — emitting plain `Cache-Control` instead of `CDN-Cache-Control` / `Cache-Tag`. The adapter is platform-agnostic (it only touches the generic request-context cache surface), so move it into core as `RequestContextCdnCacheAdapter` and have `getCdnCacheAdapter()` select it directly. Resolution is now: 1. explicit `setCdnCacheAdapter()` (always wins) 2. request-context cache present (`ctx.cache`) -> edge adapter 3. otherwise -> origin-managed default Drops the detector-registry indirection and the import/registration requirement. `CloudflareCdnCacheAdapter` is kept as a re-export alias for backwards compatibility. * fix(cache): select Cloudflare edge adapter from resolver, keep it in the cloudflare module Previous commit moved the adapter into core — revert that. The CloudflareCdnCacheAdapter stays in cloudflare/cloudflare-cdn-cache.ts; the core resolver imports it and instantiates it as the built-in default when the request context exposes a host cache (ctx.cache). Drops the detector-registry side-effect-import requirement; resolution is explicit -> ctx.cache edge adapter -> origin-managed default. * example(workers-cache): show CDN-Cache-Control in the probe headers * example(workers-cache): rename example app from workers-cache-cloudflare to workers-cache Rename the example directory and update its package name, wrangler worker name, the deploy-examples matrix + preview-URL list, and the lockfile. * fix(cache): give bare stale-while-revalidate an explicit window for the CF edge The framework emits a value-less `stale-while-revalidate` (Vercel's unbounded extension). Cloudflare follows RFC 5861 and ignores the bare directive, so the edge had no stale window — entries hard-expired at max-age and the next request was a MISS instead of UPDATING. Normalize bare SWR to an explicit 1-year window in the edge adapter's toEdgeCacheControl so Cloudflare actually serves stale while revalidating. * use link component * fix(cache): let the CDN adapter own the default Cache-Control when none is set Rendered responses that produced no cacheable policy (e.g. dynamic App Router pages) were going out with no Cache-Control at all, bypassing the CDN adapter — so on the edge Cloudflare applied its own default caching heuristic instead of the adapter's policy. Add a guard in finalizeAppRscResponse (the single App Router egress, already run for every page/route-handler/metadata/not-found response) that, when no Cache-Control is present, routes through the adapter to supply the default: the edge adapter emits no-store (never accidentally edge-cache an unspecified response), the default adapter leaves it absent (unchanged). Runs only when the header is absent, so it never clobbers a policy a renderer already applied (incl. CDN-Cache-Control). Also stop applyCdnResponseHeaders from stamping an empty Cache-Control value. * refactor(cdn-cache): align adapter method names with data cache + gate ctx.cache auto-detection Address PR #1693 review feedback: - Align CdnCacheAdapter field naming with the data cache adapter (CacheHandler): readPage->get, writePage->set, revalidate->revalidateTag. buildResponseHeaders / ownsBackgroundRevalidation stay CDN-specific (no CacheHandler equivalent). Updated both implementations and all call sites. - Gate ctx.cache auto-detection behind VINEXT_CDN_CACHE_AUTO_DETECT (default off) so edge-managed page ISR is opt-in until deployment skew protection is figured out. Removed the dedicated _edgeAdapter variable; the resolved edge adapter is now stored on the single active-adapter global slot that setCdnCacheAdapter uses. Enable the flag for the workers-cache demo via wrangler.jsonc vars. * test(cdn-cache): update tests for renamed adapter API + flag-gated auto-detect Align the CDN adapter unit tests with the refactor: - get/set/revalidateTag method names (was readPage/writePage/revalidate) - bare stale-while-revalidate now normalized to an explicit window - auto-detection is gated behind VINEXT_CDN_CACHE_AUTO_DETECT (no detector / no vinext/cloudflare side-effect import) * chore: reconcile pnpm-lock.yaml after merge The merge auto-resolved pnpm-lock.yaml into a broken state (missing @vitejs/plugin-rsc entry), so `vp install` failed at CI setup. Regenerated with pnpm install --no-frozen-lockfile; frozen install now passes. |
||
|
|
1cb89fc196 |
fix(pages-router): collapse doubled basePath in client asset URLs (#1730)
A Pages Router app built with a `basePath` emits its page and `_app` chunk URLs with the basePath applied twice, e.g. `/docs/docs/_next/static/_slug_.js`. Those URLs 404, so the page never hydrates, `next.router` never initializes, and any client-side navigation (router.push to /404, /_error, etc.) silently does nothing. Vite+ bakes the configured `base` into emitted chunk fileNames on disk (`dist/client/docs/_next/static/...`) so the Cloudflare ASSETS binding can serve them directly. Vite then prepends `base` a second time when it writes `ssr-manifest.json`, producing `docs/docs/_next/static/...` entries. `augmentSsrManifestFromBundle` preserved those doubled entries verbatim (it only stripped a leading slash), while its own bundle backfill produced the correct single-prefix path. The merged manifest held both, and `resolveClientModuleUrl` returned the doubled one. The on-disk chunk fileName carries the basePath exactly once, so the public URL must too. Collapse an exact `<base>/<base>/` prefix when normalizing the Vite-sourced manifest entries so they match the single-base paths the backfill produces. A correctly single-prefixed entry is left untouched. Regression covered with a focused unit test on the manifest builder and verified end to end against the Next.js basepath/error-pages deploy suite (previously 2 failing client-navigation cases, now all green). |
||
|
|
d8ca7e8177 |
test(i18n): align shallow redirect-skip assertion with Next.js history state (#1729)
The i18n sticky-locale change (
|
||
|
|
f4d13e25b9 |
fix(i18n): make locale sticky across client navigations (#1407)
* fix(i18n): make Pages Router locale sticky across client navigations
Refs #1336 (item 2).
Writes a Next.js-compatible history state shape on every router
push/replace, captures the active locale in `state.options.locale`, and
teaches the popstate handler to:
1. Ignore history entries written by third-party code (no `__N: true`).
2. Drop the first popstate when it carries the same locale + `as` as the
current page (Safari tab-restore / BFCache replay parity).
3. Honour the locale recorded in history state when re-fetching the page
on back/forward, so default-locale roots still go through their
locale-qualified HTML endpoint.
This matches the Next.js behaviour exercised by
`test/e2e/ignore-invalid-popstateevent` and the client half of
`test/e2e/i18n-preferred-locale-detection`.
Scope boundary: the server-side `Set-Cookie: NEXT_LOCALE=...` on initial
Accept-Language detection is intentionally NOT modified here. The
existing `parseCookieLocaleFromHeader` already gives the cookie priority
over Accept-Language, and changing where the cookie is written would
overlap with #1336 item 4 (default-locale prefix normalisation).
* fix(i18n): preserve back/forward navigation under new history-state shape
The previous commit attached `__N: true` to router-owned history entries,
but two regressions slipped in:
1. `saveScrollPosition` merges scroll fields onto the *current* entry
before a push. When the current entry was the initial document load
(`state === null`), the merge produced a state object without `__N`,
which the popstate foreign-state filter then dropped — breaking
browser back/forward.
Fix: when the existing state is null, mint a router-shaped state
(`{ __N: true, url, as, options, key }`) before merging the scroll
fields, so every entry the router touches carries `__N`.
2. The Safari-replay filter compared `state.as` against `window.location`,
but `window.location` has already changed by the time popstate fires
on a real back/forward. That made every genuine back navigation look
like a replay and get ignored.
Fix: compare against `_lastPathnameAndSearch` (the URL the router
last actively navigated to). After a real back, that tracker still
points at the entry we were on, so a genuine navigation differs and
passes the filter — matching Next.js's `state.as === this.asPath`
semantics.
Verified locally:
- tests/pages-router-i18n-sticky-locale.test.ts: 7/7 pass
- tests/shims.test.ts: 944/944 pass
- tests/e2e/pages-router/{navigation,before-pop-state,hydration,router-events}.spec.ts:
28/28 pass (these were the CI failures from the previous commit).
* fix(i18n): stamp initial history entry with active locale on install
Closes the remaining gap in the popstate locale-recovery path: the
initial document entry has `state: null` until the first push, so a
back-navigation to it would fall back to the live `window.__VINEXT_LOCALE__`
— which an intervening locale-changing push may have flipped. Stamping
router-shaped state at install captures the SSR-resolved locale into
`options.locale` and lets the popstate handler restore the correct locale.
Also:
- Strip the hash from `state.as` so the Safari-replay filter's
`withBasePath(state.as) === _lastPathnameAndSearch` comparison works for
entries created from a hash-bearing URL (the tracker never carries a hash).
- Drop the unnecessary `withBasePath as withBasePathPrefix` import alias.
Adds 3 tests: install-time stamping, no-clobber of pre-existing state, and
the back-to-initial-entry locale-recovery regression.
* fix(i18n): prefer active locale over default + scope Safari-replay filter
Three small follow-ups discovered by CI on top of the i18n-sticky-locale
work:
1. `getCurrentBrowserLocale` was falling back to `__VINEXT_DEFAULT_LOCALE__`
before `__VINEXT_LOCALE__` when no path prefix was present, which threw
away the active locale on default-locale paths served under a
non-default locale. Flip the fallback order so the active locale wins —
exactly what the new `pages-router-i18n-sticky-locale.test.ts` 'Router
.push without an explicit locale' case asserts.
2. The Safari-replay filter on the first popstate event was running even
after the user had already navigated. After a hash-only `Router.push`,
the entry's `state.as` is recorded without a hash, which happens to
match `_lastPathnameAndSearch` (also without a hash); the filter then
incorrectly dropped a genuine `goBack()` popstate, freezing the E2E
router-events suite. Gate the filter behind a new `_routerDidNavigate`
flag so it only fires during the synthetic initial-load replay window.
3. Update the 6 `expect(pushState).toHaveBeenCalledWith({}, ...)`
assertions in `tests/shims.test.ts` to use
`expect.objectContaining({ __N: true })`, since pushState now carries a
Next.js-compatible state object instead of `{}`.
* test(i18n): align unprefixed-path locale tests with sticky-locale behavior
The previous "treat unprefixed path as default locale" assertions in
tests/link.test.ts and tests/shims.test.ts contradicted the intentional
sticky-locale flip in
|
||
|
|
a8b3a5e394 |
fix(pages-router): make req async-iterable for bodyParser: false (#1479) (#1678)
Next.js Pages Router API handlers using `export const config = { api:
{ bodyParser: false } }` drain the raw bytes by iterating `req` directly
(`for await (const chunk of req)`), matching Node's IncomingMessage
contract. vinext's Workers-path req was a plain object with `body` set
to the underlying ReadableStream, so the idiom threw with 500 — breaking
webhook handlers and the `middleware-fetches-with-body` deploy suite.
Surface an `[Symbol.asyncIterator]` on the synthetic `PagesReqResRequest`
that proxies the underlying Web ReadableStream reader (with a `return()`
cleanup so `break` propagates a cancel). Leave `req.body` undefined when
`bodyParser: false`, matching Next.js (`parse-body.ts` only assigns
`req.body` when `bodyParser !== false`).
Ports the relevant assertion from
test/e2e/middleware-fetches-with-body/index.test.ts in the Next.js repo.
|
||
|
|
7f0b7ee9f7 |
test(routing): regression for /api-… page prefixes (#1542) (#1662)
* test(routing): regression for /api-… page prefixes (#1542) Adds a focused unit test in tests/routing.test.ts that creates a fixture with `pages/api-docs/[...slug].tsx`, `pages/api-info/index.tsx`, and a real API route at `pages/api/hello.ts`, then asserts: - `/api-docs/:slug+` and `/api-info` are discovered as page routes, not API routes (matches the Next.js fixture at `.nextjs-ref/test/e2e/prerender/pages/api-docs/[...slug].js`). - The real `/api/hello` API route is still discovered correctly. - `matchRoute` resolves `/api-docs/first` and `/api-info` against the page route table, pointing at the correct file paths. The existing API-route detection already requires the trailing slash (`startsWith("/api/")` or `=== "/api"`) in every code path, and the `pages/` scanner only excludes the literal `"api"` directory — so the test passes on `main`. This test pins that behaviour in place to prevent a future regression of the substring-`api-` false positive called out in #1542. Refs #1542. * style: apply biome formatter to routing.test.ts |
||
|
|
6dd6e76228 |
fix(pages-router): run instrumentation-client.ts before hydration (#1474) (#1671)
The generated Pages Router client entry imported `vinext/instrumentation-client` which transitively loads the user's `instrumentation-client.ts` via the `private-next-instrumentation-client` alias. That indirection left the contract implicit and the side effects vulnerable to aggressive tree-shaking. Side-effect-import the user's file directly from the generated client entry when it exists, ahead of `hydrateRoot()`. Mirrors Next.js's `page-bootstrap.ts`, which side-effect-imports `require-instrumentation-client` before `initialize`/`hydrate`. Adds focused unit tests in `tests/entry-templates.test.ts` asserting that the generated client entry imports the user's `instrumentation-client.ts` before `hydrateRoot(` is called, and that nothing spurious is emitted when no file is present. Refs #1474. |
||
|
|
4fefbbce84 |
fix(cache): attach path tags to prerender-seeded entries so revalidatePath invalidates them (#1486) (#1688)
Build-time prerender seeding (`isrSetPrerenderedAppPage`) and TPR upload were
writing cache entries with `tags: []`. Tag-based invalidation could never
reach them, so `revalidatePath('/foo')` left the seeded HTML/RSC stale until
the natural `revalidateAt` expired.
Compute the path-derived implicit tags via `buildAppPageCacheTags(pathname, [])`
in both seed paths and forward them through to the cache handler.
Closes (in part) #1486 — plain path invalidation. Rewrite-aware
invalidation (canonical-vs-rewrite key mismatch) is left for a follow-up.
|
||
|
|
f8481ac04d |
fix(css): preserve distinct media filenames for CSS url() assets (#1725)
* fix(css): preserve distinct media filenames for CSS url() assets
Match Next.js `asset/resource` behaviour for CSS `url()` dependencies:
emit them as files under `/_next/static/media/` instead of inlining, and
keep byte-identical sources (e.g. `dark.svg` / `dark2.svg`) as separate
output files instead of letting Vite/Rolldown's content-based asset
dedupe collapse them onto one filename.
There is no config switch to disable that dedupe (vitejs/vite#8632). The
native escape hatch is `this.emitFile({ fileName })`, which is never
deduped. A build-only, client-scoped pair of plugins:
- mark: during the client CSS transform, tag each relative asset
`url()` with a private `?vinext_css_url_asset=<source-basename>`
query — the only durable carrier of per-reference provenance, since
once Rolldown dedupes, the bundle can't tell which reference came
from which source.
- restore: at `generateBundle`, read the marker back, emit a sibling
file under the wanted source name via the `fileName` escape hatch,
rewrite the reference, and strip the marker.
Each reference resolves from its own marker, so split CSS chunks stay
correct without a shared cursor or bundle-iteration-order dependence.
This is a slimmer reimplementation of the approach explored in #1703:
same mark/restore mechanism and public surface, but the restore side
drops the AssetIndexes machinery, filename-reservation loop, and
multi-fallback lookups in favour of a single basename map + emitted-set,
since the sibling-filename construction (source-stem + content-hash +
ext) makes those guarded cases unreachable.
Tests port the Next.js scss/url-global e2e into the committed
`pages-basic` and `app-basic` fixtures (built in isolation), covering
the single-stylesheet two-identical-urls case and cross-chunk provenance.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
* fix(css): merge duplicate vite import; drop ReDoS-flagged regex; clarify comments
- tests/css-url-assets.test.ts: merge the two `vite` imports into one
(import/no-duplicates lint error that failed CI Check).
- client-build-config.ts: replace `/\/+$/` trailing-slash strip with a
linear loop so CodeQL stops flagging a (false-positive) ReDoS.
- index.ts: reword the css-url mark transform's `map: null` comment — the
marker shifts columns transiently but is stripped before final output.
- css-url-assets.ts: note the basename-uniqueness assumption in assetsByBase.
Addresses review feedback on #1725.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
* test(css): use dedicated minimal fixtures for url() asset tests
The url() asset tests previously added routes to the shared app-basic /
pages-basic fixtures and built those whole apps in isolation. That made
the tests CPU-heavy and, run concurrently under CI, contended with
app-router.test.ts's static-export test enough to push it past its 30s
timeout (it passes comfortably locally and on main; the extra parallel
load was the trigger). The added app-basic route also slightly inflated
every app-basic-wide test (static export renders all routes).
Move the fixtures into dedicated minimal committed apps
(tests/fixtures/css-url-assets-{app,pages}) that build in ~1-2s. Same
observable contract (byte-identical dark.svg/dark2.svg referenced from
one stylesheet, plus the split-chunk provenance case), now isolated from
the heavyweight shared fixtures and fast enough not to contend.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
* docs(test): update css-url-assets header to reference the dedicated fixtures
The file header still described the pre-refactor layout (pages-basic /
app-basic paths that no longer exist). Point it at the dedicated
css-url-assets-{pages,app} fixtures. Comment-only.
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
---------
Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
|
||
|
|
4ffd5bf225 |
fix(app-router): ensure streamed SSR body ends with </body></html> (#1532) (#1624)
* fix(app-router): ensure SSR stream ends with </body></html> suffix (#1532) Trailing flight chunks and preinit scripts emitted by `rscEmbed.finalize()` in the tick-buffered transform's `flush()` were being appended after React Fizz's `</body></html>` closing tags, leaving the streamed document body ending in `<script>...</script>` rather than a well-formed close. Strip the first `</body></html>` we see in the buffered HTML chunks and re-emit it at the very end of `flush()`, after the final RSC scripts. Mirrors Next.js's `createMoveSuffixStream` behaviour (packages/next/src/server/stream-utils/node-web-streams-helper.ts). Ported from Next.js test: test/e2e/app-dir/app/index.test.ts — "should ensure the </body></html> suffix is at the end of the stream". Fixes #1532. * test(rsc-streaming): adjust progressive-delivery assertion for #1532 The 'delivers RSC chunks progressively' case still asserted the old order where the navigation-runtime done signal trailed </html>. After #1532 suffix-moved </body></html> to the very end of the stream, the done signal sits *between* the body content and the document close. Update the assertion to match: done > last body content, and </html> > done. * style: apply biome formatter to rsc-streaming.test.ts |
||
|
|
51690038d8 |
fix(pages-router): cancel in-flight nav on gSSP/gSP data redirect (#1465) (#1691)
* fix(pages-router): return __N_REDIRECT JSON for gSSP/gSP data redirects (#1465) When getServerSideProps/getStaticProps returns `{ redirect }` on a client-side `_next/data` navigation, vinext emitted a real HTTP redirect. The client's fetch transparently followed it to the destination's HTML (not a valid data envelope), forcing a hard reload with console errors and leaving navigation on the wrong page. Mirror Next.js: for data requests, reply 200 with `__N_REDIRECT` / `__N_REDIRECT_STATUS` in pageProps. The client router now detects these markers and re-dispatches a fresh client navigation to the destination, which increments the navigation id and supersedes (cancels) the in-flight navigation so the intermediate page never commits. HTML requests still emit a real HTTP redirect. Fixed in both server paths to keep dev/prod parity (see AGENTS.md): - prod: buildPagesRedirectResponse() in pages-page-data.ts branches on isDataReq - dev: writeGsspRedirect() in dev-server.ts branches on isDataReq - client: navigateClientData() handles pageProps.__N_REDIRECT via handleDataRedirect(), preserving push/replace mode - tests: data-endpoint redirect shape (vitest) + client cancellation (e2e) * fix(pages-router): hard-navigate to external __N_REDIRECT destination (#1465) Address review: handleDataRedirect() passed the originating page URL to the hard-navigation fallback for external/non-absolute __N_REDIRECT destinations, which would loop back to the redirecting page (e.g. an OAuth callback at https://example.com/...). Hard-navigate to the destination instead, matching Next.js's handleHardNavigation({ url: destination }). Drop the now-unused fallback parameter and add a server test asserting an external destination is preserved verbatim in the data envelope. |
||
|
|
65eaea49a4 |
feat(skip): omit proven static layouts from RSC transport (#1437)
* feat(skip): omit proven static layouts from RSC transport
App Router RSC responses re-send every layout payload on each navigation, even
when the client already holds an identical static layout from a sibling route.
That repeats transport bytes on the common sibling-page navigation path.
When the client sends a verified reuse manifest, the render lifecycle now runs a
skip-transport planner. Each manifest entry is cross-checked against the
static-layout cache proof (exact artifact compatibility, variant cache key,
payload hash, and invalidation). Layouts that pass are omitted from the outgoing
RSC payload, recorded under __skippedLayoutIds, and their per-element render
dependencies are released. The skip disposition also bypasses the RSC cache so a
partial payload never enters a shared cache, and it is suppressed for HTML
responses.
This is the enable slice. Layouts are gated on the cache proof, not yet on
per-layout runtime observations. The observation-based safety gates (param
scope, request APIs, finite revalidate, cacheLife, unstable_cache, cache-tagged
and dynamic fetches) land in the stacked hardening follow-up.
* fix(skip): preserve parallel slots owned by skipped static layouts
When the skip-transport merge restores a server-omitted static layout into
preserveElementIds, it must also restore the default/unmatched parallel slots
that layout owns. Otherwise the planner invariant ("every preserved slot's owner
layout is present in preserveElementIds, and vice versa") is violated: in the
topology-unknown path the planner returns empty slot persistence, so a
slot-owning layout skipped server-side commits with a missing slot
(mergeElements starts from the next payload and, with preserveAbsentSlots:
false, never restores it).
mergeSkippedLayoutPreservation now derives the owned slots for newly-preserved
skipped layouts via the planner's resolveDefaultOrUnmatchedSlotPersistenceForLayouts,
covering both the topology-unknown path and the known-topology past-divergence
case. Added a regression with a mounted @slot default that fails without the fix.
Also hardcode the static-layout cache-proof's observed request-API set to []
(with a comment naming the invariant) instead of reading it back from the
per-layout observation, so a future reordering of the classification gate cannot
feed stale request-API reads into the synthetic proof. Reaching that point
already requires staticLayoutIds membership, which excludes any layout that
observed a request API.
* docs(skip): explain broader-than-planner layout membership guard
Per review: clarify that the set-membership check in
mergeSkippedLayoutPreservation is intentionally broader than the planner's
prefix-based persistence, and why preserving a proven-identical layout past the
ancestor divergence point is sound. Comment-only.
* refactor(skip): drop redundant cacheable-fetch observation record
Per review: the post-key-generation recordCacheableFetchObservation in
fetch-cache.ts was strictly redundant. The synchronous record before the first
await already covers every path that reaches it (the only intervening early
return records a dynamic observation instead), and the store is an idempotent
set, so the second call added no coverage.
* fix(test): match real RSC request URL in client-reuse-manifest e2e
The 'sends a client reuse manifest for retained static layouts on soft
navigation' e2e hand-rolled its RSC-request matcher as
url.pathname === '/client-nav-test.rsc', but App Router soft-navigation RSC
requests use the bare pathname with a _rsc query param and an rsc: 1 header
(e.g. GET /client-nav-test?_rsc), so the predicate never matched and
page.waitForResponse timed out at 30s. The manifest header was being attached
correctly the whole time. Use the shared isAppRouterRscRequestForPath helper
(as the sibling test already does) so the matcher tracks the real request shape.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
49b45b9891 |
feat(ppr): add encodePrerenderRouteParams and match kind exact payload tests (#1714)
* feat(ppr): add fallback shell payload identity * refactor(ppr): address review feedback on payload identity helpers - normalizePregeneratedPathname: document the deliberate non-strict vs strict normalization choice so it isn't "fixed" into a build-time throw - addPregeneratedConcretePath: normalize the pathname internally as the single source of truth, removing the caller pre-normalize footgun - export PrerenderRouteParamsRouteMatch for #1716 consumers - cover direct un-normalized recording with a test * fix(ppr): mark PrerenderRouteParamsRouteMatch @public for knip The type is exported for #1716's serving consumers but has no in-repo reference yet, so knip's no-unused-exports gate flags it. Knip excludes exports tagged @public from that report; this keeps the reviewer-requested export without failing CI. * docs(ppr): document live-Set and dedup contracts on concrete-path registry Address bonk review notes: - getRenderedConcreteUrlPathsForRoute returns the live backing Set for allocation-free hot-path lookups; document that callers must not retain the reference across a re-seed (clear empties the map, stranding it). - parsePregeneratedConcretePaths intentionally does not dedupe repeated route patterns; document that they merge additively and value-dedup in addPregeneratedConcretePath makes the result equivalent. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
b16b9ed055 |
chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#1720)
* chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * Apply suggestion from @james-elicx --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James <james@eli.cx> |
||
|
|
dec0ee5369 |
chore: bump Next.js and React patch versions (#1721)
* chore: bump next and react patches * update other areas and bump down peer dep * . --------- Co-authored-by: James <james@eli.cx> |
||
|
|
49d2f0966d |
feat(skip): plumb client reuse manifests through the app request path (#1717)
* feat(skip): plumb client reuse manifests through the app request path The browser never sends a client reuse manifest and the server has no path to receive one, so the static-layout skip planner (#1674) and observation foundations (#1672) have nothing to act on. Skip transport stays fully off, but the transport wiring those slices depend on is missing. Add a bounded browser producer that derives a client reuse manifest from the visible AppElements state and attaches it as X-Vinext-Client-Reuse-Manifest only on soft navigations. Parse the header into a render/send-by-default disposition and thread the parsed result through the RSC handler, the generated app entry, page dispatch, and the render lifecycle. Wire the per-layout observation tracker and segment-config classification into dispatch so the enable-transport slice can read them. No RSC payload is omitted and no response cache-control changes: the parsed manifest is carried but never authorizes a skip yet. Unproven and absent- manifest requests render and send exactly as before. Slice 3 of 4 from #1437; enable-transport follows. Refs #726 * fix(skip): address review feedback on manifest plumbing - app-page-execution: defer layout debug classification until after the observation override so the emitted reason agrees with the final flag; emit a runtime-probe reason when an observation flips a build-time static layout to dynamic. - app-page-dispatch: document classifyLayoutSegmentConfigFromModule as the request-time counterpart to the build-time classifier, noting the intentional revalidate===false divergence and the dev-mode classification. - app-render-dependency: mark registerAppElementRenderDependencies as write-only until the enable slice's consumer lands. * docs(skip): clarify layout classification comments per review - app-page-dispatch: correct the classifyLayoutSegmentConfigFromModule docblock — the build-time classifier already normalizes revalidate=false to Infinity, so both agree on that case; the real difference is scope (request-time classifies dev-mode layouts not captured at build time). - app-page-dispatch: document the merge precedence in createEffectiveLayoutClassifications — module segment config is authoritative over the build-time map. * test(skip): pin observation override flipping build-time static layout to dynamic Adds a parity assertion for the request-time behavioral change the review flagged: isLayoutObservationDynamic downgrades a build-time `static` layout to `d` (and emits a runtime-probe reason) when the observed render is unsafe for static reuse, while a safe observation keeps it static. Complements the existing children-subtree probe assertion in app-page-route-wiring.test.ts. * fix(skip): fold probe-scoped dynamic usage into layout observation The probe rewrite runs layout probes inside runWithIsolatedProbeDependencies, which opens a child unified scope and resets dynamicUsageDetected. A markDynamicUsage() during the probe that leaves no other observable trace (e.g. "use cache: private") was therefore discarded on scope exit, so the parent's consumeDynamicUsage() in the Layer-3 path returned false and the layout could classify static where the pre-PR direct probe classified dynamic. recordProbeDependencies now peeks the child scope's dynamic-usage flag before it is reset and folds it into the per-layout observation (dynamicUsageObserved), which isAppLayoutObservationUnsafeForStaticReuse treats as unsafe. This keeps the observation signal a superset of the markDynamicUsage() triggers the Layer-3 dynamicDetected path covered, so the two signals can no longer diverge into a false-static. Adds peekDynamicUsage() (non-resetting read) and a regression test. * perf(skip): defer client reuse manifest production to the cache-miss path The manifest header is excluded from VINEXT_RSC_VARY_HEADER, so it never affects the cache-busting URL or the visited-response cache lookup. Producing it up front ran createClientReuseManifestHeaderFromVisibleAppState (iterates visible layout ids, builds a cache-variant proof per static layout, then binary-searches the byte budget) on every soft navigate — including ones that resolve entirely from the visited-response cache or a prefetch and never send a request, where the work is pure waste. Move production to just before the network fetch, gated on the same `navigationKind === "navigate"` condition and computed from the captured nav-start router state, so the emitted header is unchanged on the request path while the cache-hit/prefetch-hit hot paths skip it entirely. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
8498e4a37e |
fix(app-router): emit per-page dynamic stale time metadata (#1711)
* fix(app-router): emit per-page dynamic stale time metadata App Router pages that export unstable_dynamicStaleTime currently behave as if no page-level stale window exists. The server segment config resolver discards the export, so RSC responses cannot carry the per-page metadata that the client cache needs for independently configured pages. The violated invariant is that route segment config resolution must preserve page-only stale-time metadata across active parallel pages before response construction. Parse the page export, reduce active page slots by shortest stale time, thread the value through generated App page dispatch, and emit it on RSC responses for the client cache to consume. * fix(app-router): gate dynamic stale time header to dynamic-only renders Only emit X-Vinext-Dynamic-Stale-Time when the render is truly dynamic (not prerender or force-static), matching Next.js behavior where the response `d` field is only set for non-static-generation renders. The client treats the headers presence as authoritative; leaking it onto static/prerendered RSC responses would cause the client cache to apply dynamic stale time semantics to a statically-cached entry. * test(app-router): add behavioral tests for dynamic stale time gating * docs(app-page-render): add line-numbered link to Next.js source * refactor(app-router): drop redundant Number.isFinite check Number.isInteger already returns false for Infinity/NaN, so the preceding Number.isFinite guard is redundant in both the dynamic stale-time header check and resolveDynamicStaleTimeSeconds. * fix(app-router): gate dynamic stale time on cache-capture predicate The previous gating (isPrerender || isForceStatic) was too narrow for production: isPrerender is only true during the build-time prerender pass, so a production ISR render (revalidate > 0) still emitted X-Vinext-Dynamic-Stale-Time even though that render is captured into the ISR cache. Next.js gates on !workStore.isStaticGeneration; the runtime analog here is shouldCaptureRscForCacheMetadata, so add it to the guard. Adds production ISR coverage to lock down the gating. * docs(app-router): document dynamic stale time over-gating boundary Add a comment explaining that shouldCaptureRscForCacheMetadata is computed from config before the stream runs, so a production default-config route that turns dynamic via a late dynamic API loses the advisory stale-time hint even though Next.js (!isStaticGeneration) would emit it. The value is unknowable until after headers are built. Adds a complementary test pinning down this boundary behavior. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
4688fa9c16 |
fix(app-router): prerender cacheComponents root-param fallback shells (#1702)
* fix(app-router): prerender cacheComponents root-param fallback shells PR 1 of 4: core model + build integration. Model: app-ppr-fallback-shell, pregenerated-concrete-paths, prerender-manifest Build: prerender/run-prerender fallback shell artifact generation Tests: createAppPprFallbackShells, pregenerated-concrete-paths core * test(prerender-route-params): add unit tests for fallbackParamNames and validation Adds focused unit tests for the new prerender-route-params logic introduced in this PR: - encodePrerenderRouteParams round-trips fallbackParamNames and omits them when empty. - prerenderRouteParamsPayloadMatchesRoute rejects payloads whose fallbackParamNames contain unknown params or duplicates. - prerenderRouteParamsPayloadMatchesRoute returns false for valid fallback-shell matches (only exact matches are accepted today). Also adds a comment in app-ppr-fallback-shell.ts noting that placeholder brackets in shell pathnames are percent-encoded by new URL() at fetch time, so the render path must supply params via the prerender-params header rather than URL matching. * docs(prerender): clarify BUILD_ID reuse and fallback-shell param-resolution seams Address review feedback on PR #1702: - Explain why run-prerender reuses the built BUILD_ID instead of re-resolving one (config.buildId feeds prerendered-output identity). - Note that the fallback-shell prerender-params header path described in app-ppr-fallback-shell.ts is honored by the #1715 follow-up; this generation-only PR still resolves params from the URL placeholder. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
82b2720701 |
feat(app-router): support useRouter bfcacheId semantics (#1588)
* feat(app-router): implement useRouter bfcacheId semantics * fix(app-router): refine bfcacheId navigation behavior * test(app-router): align bfcache history state expectations * fix(app-router): handle catch-all bfcache identities * fix(app-router): align bfcache context semantics * fix(app-router): avoid hidden bfcache DOM retention * fix(app-router): hydrate bfcache ids from history state * fix(app-router): handle bfcacheId redirects and slots * test(app-router): expect active slot route ids * Fix bfcache id history restoration * Clarify bfcache id sentinel formatting * Address bfcacheId review feedback * Tie bfcacheId sentinels to shared constants * Address bfcacheId review clarity * Tighten bfcache slot identity check * Deduplicate bfcache history sync check * Handle empty bfcache segment ids * Fix intercepted active slot route ids * Clarify bfcacheId form state coverage * Address bfcacheId review clarity * fix(app-router): skip invisible tree-path segments in bfcache identity getVisibleTreePathSegments only stripped route groups, not parallel @slots or "." default segments. Tree paths contain raw filesystem segments, so a slot preceding visible URL segments over-counted the consumed pathname segments and produced a wrong identity prefix. That re-minted the bfcacheId for layouts/slots that actually persisted across parallel-route navigations (e.g. @modal), diverging from Next.js bfcacheId semantics. Delegate to the canonical isInvisibleSegment helper so the filter stays in sync with the route graph, and add a parallel-slot layout regression test (fails without the fix). Also tighten the hydration-map unit test to actually exercise the "no seeding from minted ids" property instead of asserting a local literal, and correct the SSR bfcache context comment. * fix(app-router): move isInvisibleSegment to browser-safe utils The previous commit imported isInvisibleSegment from app-route-graph.ts into app-browser-state.ts, but app-route-graph.ts imports node:path, node:fs and node:crypto. Pulling it into the browser bundle broke client hydration (app-with-src E2E: instrumentation-client and dev-overlay-recovery timed out waiting for hydration / the navigation runtime). Move the pure isInvisibleSegment helper into the browser-safe routing/utils.ts module (already in the client bundle), re-export it from app-route-graph.ts so Node-side import sites keep working, and have app-browser-state.ts import it from utils.js. The app-route-graph type import is now type-only. * refactor(app-router): address bfcacheId review feedback - Reject history entries with a missing bfcache version in the document-scoped version gate (stop coercing null -> 0) so stale ids from older builds / external pushState can't be restored into a fresh document on the version-0 path. - Parse AppElements metadata once per element map per commit and resolve slot bindings via a precomputed Map (was repeated readMetadata parses plus an O(slots^2) find on every navigation commit). - Read single history-state keys without cloning the whole state object; the write path still clones before mutating. - Dedupe the two identical non-negative-safe-integer validators and share path-segment / catch-all helpers via browser-safe routing/utils. - Move tree-path consumed-segment counting into routing/utils and fix optional-catch-all over-counting. - Drop the unused bfcacheIds param from createHistoryStateWithPreviousNextUrl. * refactor(routing): make catch-all segment predicates module-private isCatchAllSegment/isOptionalCatchAllSegment are only used by countConsumedPathnameSegments in the same module; drop the exports to clear knip's unused-export warning. * test(app-router): lock in bfcache version-gate rejection of missing versions Extract the version-gate decision into a pure, exported isHistoryStateBfcacheVersionCurrent(state, currentVersion) in app-history-state (re-exported via app-browser-state); the browser entry's isCurrentBfcacheVersion now delegates to it. Add a direct regression test asserting a history entry with bfcache ids but no version key is NOT treated as current even when the current version is 0 — the exact cross-document stale-id hole that the earlier `?? 0` coercion left open. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
1047cccd8a |
fix(metadata): omit unused parent arg for cached generateMetadata (#1719)
* fix(metadata): omit unused parent arg for cached generateMetadata A module-level `'use cache'` directive wraps `generateMetadata` in the cache runtime. vinext always called it as `generateMetadata(props, parent)`, where `parent` resolves to the merged ancestor metadata. When an ancestor sets `metadataBase: new URL(...)`, the cache-key encoder (`encodeReply`) awaited the parent promise and tried to serialize the `URL` instance, logging React's "Only plain objects can be passed to Server Functions from the Client. URL objects are not supported." warning. Match Next.js, which omits the `parent` argument for `generateMetadata` functions that don't use it (resolve-metadata.ts `getResult` / `useCacheFunctionInfo.usedArgs[1]`): - `resolveModuleMetadata` now only passes `parent` when `generateMetadata` declares it (`fn.length >= 2`). - `registerCachedFunction` preserves the original function's arity on the wrapper so the arity check works for cached functions. Adds regression tests for the arity gating, metadataBase preservation, and arity-preserving wrapper. * docs(metadata): note arity approximates parent usage; drop dead try/catch Address /bigbonk review feedback: - Document that fn.length approximates Next.js's usedArgs[1] analysis and diverges on default-parameter signatures. - Remove the dead try/catch around Object.defineProperty (function length is always configurable per spec). |
||
|
|
560f270ee6 |
fix: align app router navigation parity (#1705)
* fix: align app router navigation parity * fix(app-router): preserve canonical RSC and hard-nav semantics App Router RSC requests now keep the route pathname shape when adding _rsc. Stripping a trailing slash made trailingSlash routes fetch a non-canonical URL and let the server redirect the client-side RSC request back to the slash form. The MPA navigation scheduler also keys pending hard navigations by both href and history mode so a later replace does not reuse an earlier push closure. Redirect handling keeps the global browser bridge out of passive server rendering while retaining client-side redirect recovery. Adds focused regression coverage for trailingSlash RSC requests, same-href push-to-replace hard navigation, and client redirect guard loops. Updates existing navigation observers to the canonical route URL plus _rsc transport. * fix(app-router): stabilize action and prefetch parity * fix(app-router): reset MPA dedupe after bfcache restore A bfcache-restored App Router document can keep the scheduler's last pending hard-navigation marker after the external navigation already left the page. Reissuing the same href and history mode then entered the suspended MPA path without scheduling another location change. Expose a scheduler reset for the browser lifecycle boundary and call it on persisted pageshow events. Cover the scheduler contract so the same external navigation can run again after reset. * fix(app-router): isolate redirect bridge from passive boundaries Unhandled client redirect recovery currently lives inside every RedirectBoundary, while Pages Router params fallback can keep stale App Router params after an App-to-Pages transition. That makes passive boundaries own browser runtime wiring and lets stale app params win over live Pages Router params. Move the unhandled redirect bridge into BrowserRoot under AppRouterContext, keep RedirectBoundary passive, and prefer live Pages Router params when that compat context exists. Add an App-to-Pages navigation regression fixture. * fix: extract shared decodeRedirectError helper, rename stale test - Add decodeRedirectError() to shims/navigation.ts as the single source of truth for parsing redirect error digests - Replace three private helpers in error-boundary.tsx with shared decodeRedirectError() call - Replace inline digest parsing in AppRouterRedirectBridge with shared decodeRedirectError() call (fixes unsafe decodeURIComponent) - Rename test that claimed to test redirect() but actually tests <Link> navigation * refactor: eliminate unnecessary type assertions - isRedirectError: use in-narrowing instead of as-casts - AppRouterRedirectBridge: remove as after guard already narrows - RedirectErrorBoundary: inline digest access, use in for handled check * ci: retrigger checks * fix: emit noindex for Forbidden/Unauthorized boundaries, extract retryScrollTo helper * docs: clarify decodeRedirectError comment about defensive joinv0.0.55 |
||
|
|
f15267c8a1 |
fix(pages-router): run _document.getInitialProps with renderPage enhancers (#1682)
* fix(pages-router): run _document.getInitialProps with renderPage enhancers Refs #1468. Custom `pages/_document.tsx` files that override `getInitialProps` and call `ctx.renderPage({ enhanceApp, enhanceComponent })` to wrap the App/Component tree — the standard styled-components / emotion pattern for SSR style collection — silently no-op'd. The shim type advertised `renderPage`, but the SSR pipeline never invoked `getInitialProps` with a real `renderPage`, so the enhancers never ran and `<Document>` never saw the wrapped tree. This wires the Pages Router `_document.getInitialProps` contract through both the prod (`renderPagesPageResponse`) and dev (`streamPageToResponse`) pipelines: - A new `enhancePageElement(opts)` option exposes the App/Component separation needed to apply `enhanceApp` / `enhanceComponent` from the user's renderPage call. `pages-server-entry.ts` and the dev server both wire it up to wrap `App({ Component, pageProps })`. - When `DocumentComponent.getInitialProps` is defined, we invoke it with a minimal `DocumentContext` shim that includes `renderPage` and `defaultGetInitialProps`. `renderPage` rebuilds the React tree via `enhancePageElement`, drains the React stream to a string, and returns `{ html, head }`. - If renderPage was called, the streaming body fallback is replaced with the captured `html` so `_document` sees a complete body (matches Next.js's `loadDocumentInitialProps` behaviour). Any `styles` element returned by getInitialProps is rendered to HTML and appended to the document head. - If `getInitialProps` throws, returns a non-string `html`, or never invokes renderPage, we fall through to the existing streaming path — no user-visible regression for the common case where `_document` has no `getInitialProps`. Adds a focused regression in `tests/pages-page-response.test.ts` that confirms `enhanceApp` and `enhanceComponent` both run and that the enhanced tree appears in the rendered body. Mirrors `.nextjs-ref/test/development/basic/styled-components/pages/_document.js` and `.nextjs-ref/packages/next/src/server/render.tsx` (`loadDocumentInitialProps` / `enhanceComponents`). Test plan: - `pnpm test tests/pages-page-response.test.ts` - `pnpm test tests/document.test.ts tests/head.test.ts tests/shims.test.ts` - `pnpm test tests/pages-router.test.ts` - `pnpm test tests/entry-templates.test.ts` - `pnpm run check` * refactor(pages-router): consolidate _document renderPage onto shared helper Address review feedback on the _document.getInitialProps + renderPage contract: - Extract a single shared runDocumentRenderPage() helper in pages-document-initial-props.ts, used by both the prod (pages-page-response.ts) and dev (dev-server.ts) SSR pipelines. The duplicated inline implementation in dev-server.ts is removed (-86/+38). - withScriptNonce now lives in one place (the shared helper); both callers' enhancePageElement return the bare enhanced tree. - Remove dead headFromDocument (returned but never consumed). - Drop the redundant React.Fragment wrapper around enhancedElement. - Reconcile with #1677's loadUserDocumentInitialProps: getInitialProps is invoked at most once. A discriminated result (skipped/consumed/rendered) signals when the fast path must be skipped so getInitialProps is not called twice (which would re-throw a user error as a 500). - Add edge-case tests: styles returned, getInitialProps throws (clean fallback), renderPage never called. * refactor(pages-router): address re-review nits on shared renderPage helper - runDocumentRenderPage now skips the base-shim getInitialProps via the BASE_GET_INITIAL_PROPS identity check (mirrors loadUserDocumentInitialProps) so an unoverridden _document stays on the cheap fast path. - Dev now forwards { pathname, query, asPath } as the DocumentContext, matching the prod pipeline's parity. - Add a test for renderPage being called but the underlying stream render throwing (falls back cleanly to the streaming render). * fix(pages-router): drop unused RunDocumentRenderPageResult export, document pageProps capture - Remove the `export` on the internal `RunDocumentRenderPageResult` type (knip flagged it as an unused export; it is only used as the helper's return type within the same module). - Document why `enhancePageElement` captures `pageProps` from the closure rather than receiving it as a parameter (it is invoked once per request with a single `pageProps`; only the enhancers vary). * refactor(pages-router): address bonk re-review nits - dev-server enhancePageElement: document the pageProps closure capture to mirror the prod entry's comment - prod renderStylesToString: render the collected styles fragment with the plain stream renderer instead of the full <Document> shell renderer - lazify pageElement construction so it is only built on the streaming path (never used when renderPage already produced the body) |
||
|
|
a98c8ae595 |
fix(shims/head): keep charset/viewport defaults on client hydration (#1713)
* fix(shims/head): keep charset/viewport defaults on client hydration The Pages Router next/head client sync (syncClientHead) removed every [data-next-head] element and rebuilt the projection from only the user <Head> children, omitting defaultHead() (charset + viewport). After the first <Head> mounted on the client, the server-rendered charset and viewport meta tags were stripped and never re-added. PR #1677 seeded defaultHead() into the SSR collector (getSSRHeadHTML) but not the client path. Next.js's reduceComponents() always concatenates defaultHead() on both server and client, so the defaults must survive hydration here too. Seed defaultHead() into the client reduce, mirroring the SSR path. Users can still override via key="charset" / key="viewport" through the same dedupe pipeline. Adds a regression test exercising the client projection with a minimal DOM double (the repo has no jsdom). * fix(shims/head): reconcile client head in place to preserve SSR order The client sync still wiped every [data-next-head] node and re-appended the projection, which moved the SSR-emitted charset/viewport/title tags to the end of <head> (after the modulepreload/script links) and caused DOM churn/flicker on every <Head> update. Mirror Next.js's head-manager.ts updateElements(): diff the desired tags against existing ones via isEqualNode(), leave matches untouched in their original DOM position, append only genuinely new tags, remove stale ones, and prepend a newly-created <meta charset> so it stays first. Extends the regression tests with a richer DOM double asserting node reuse, order preservation, insertion, removal, and charset-first behavior. * test(shims/head): client-side override coverage; document divergences Address non-blocking review notes from /bigbonk: - Add a client-side test asserting a user key="viewport" overrides the default viewport on _syncClientHead (mirrors the SSR override test), locking in the dedupe-precedence parity claim. - Document the deliberate charset prepend if/else divergence from Next.js's head-manager.ts (which prepends then unconditionally appendChilds). - Document the <title> reconciliation divergence (Next.js sets document.title directly; we reconcile title like any tag — cosmetic only). - Reconcile issue references to #1569 / #1677 in the client describe block. |
||
|
|
26d1cc79bc |
fix(pages-router): dedupe in-flight _next/data fetches by URL (#1464) (#1684)
Concurrent `Router.push` calls (or back-to-back `<Link>` clicks) to the same gSSP route each fired their own `_next/data/<id>/<page>.json` fetch. The Next.js parity test asserts `hit: 1` after 4 rapid navigations to a slow page; vinext was producing `hit: N`. Add a module-scoped in-flight Promise cache keyed by the resolved data URL, mirroring `fetchNextData()`'s `inflightCache` in `packages/next/src/shared/lib/router/router.ts`. Concurrent callers share one `fetch()` invocation and each receive a cloned Response. Entries self-evict on settle so the next navigation re-fetches fresh. The shared fetch deliberately ignores per-caller `AbortSignal`s: stale navigations are already detected via `_navigationId` + `assertStillCurrent()` checkpoints after the await, and aborting the shared request on behalf of one superseded caller would destroy the dedup gain for every other concurrent caller. Pre-await abort still surfaces so callers see the documented `NavigationCancelledError` / `AbortError` when supersession happened before the fetch was even attempted. Wired into both `navigateClientData()` (the JSON fast path) and `resolveMiddlewareDataRedirect()` (the soft-redirect probe). |
||
|
|
ffc8e68c4a |
fix(shims/head): match Next.js charset/viewport order and merge _document.getInitialProps head (#1677)
* fix(shims/head): match Next.js charset/viewport order and merge _document.getInitialProps head (#1569) Pages Router head emission did not match Next.js's canonical order (`<meta charset>` then `<meta viewport>` then user tags, all with `data-next-head=""`) and ignored head tags returned from a user `_document.getInitialProps()`. - `next/head` shim now seeds `defaultHead()` (charset + viewport) into the SSR collector before user tags, mirroring `defaultHead()` in `next/src/shared/lib/head.tsx`. They flow through the same dedupe pipeline so users can still override via `key="charset"` / `key="viewport"`. - Serialised attribute names follow HTML conventions (`charSet` -> `charset`, `httpEquiv` -> `http-equiv`, etc.), matching Next.js's `test/e2e/next-head/index.test.ts` exact-string assertion. - `next/document` shim no longer hardcodes charset/viewport in `<Head>` (those defaults now flow from `next/head`); fallback HTML shells in `dev-server.ts` and `pages-page-response.ts` likewise drop the duplicates. - New `setDocumentInitialHead()` accessor and ALS-backed state lets the SSR pipeline forward head tags returned by user `_document.getInitialProps()` into the same dedupe pipeline as `next/head` children. Both the dev server and the prod `renderPagesPageResponse` invoke `_document.getInitialProps()` when present and pass the result through. Adds focused regression coverage in `tests/head.test.ts` for ordering, default attribute serialisation, key-based overrides, and the `_document.getInitialProps()` merge path. Refs #1569. * fix(pages-router): address review feedback on _document head merge - Extract shared `callDocumentGetInitialProps` helper under `server/document-initial-head.ts` so both `dev-server.ts` and `pages-page-response.ts` go through the same code path. The previous duplicated try/catch blocks drifted independently and were the kind of runtime-behavior that AGENTS.md says should live in a helper. - Skip the call when `DocumentComponent.getInitialProps` is still the unmodified default inherited from vinext's `next/document` shim. Comparing against the captured base reference (`DEFAULT_GET_INITIAL_PROPS`) distinguishes a user override from the default — extending `Document` without overriding the method inherits the same static function, so invoking it on every render is wasted work. - Use `jsxAttrToHtml` on the client-side `_applyHeadPropsToElement` path so `httpEquiv` is set as `http-equiv` rather than `httpequiv` (lowercased by `setAttribute`). Matches the SSR output and prevents a hydration mismatch when SSR emits the hyphenated form. - Update stale `tests/document.test.ts` Head assertions: the shim's `<Head>` no longer hardcodes charset/viewport — those flow through `next/head`'s `defaultHead()` so the dedupe pipeline handles them. |
||
|
|
60dfb11c62 |
chore(deps): bump @vitejs/plugin-rsc to 0.5.27 (#1709)
* chore(deps): bump `@vitejs/plugin-rsc` to 0.5.27 * test(rsc): cover `export * from` in 'use client' modules (#1352) Ports the build fixture from #1373 to verify the `@vitejs/plugin-rsc` 0.5.27 bump resolves the upstream "unsupported ExportAllDeclaration" error. Covers bare `export *`, nested/renamed re-exports, `export * as Ns from`, and destructured re-exported bindings. The version bump fixes this natively, so the vinext-side workaround plugin from #1373 is no longer needed. Refs #1352 |
||
|
|
49f3905b7b |
fix(config): keep packages externalized when loading TypeScript next.config (#1710)
PR #1701 added `resolve.noExternal: true` while loading a TypeScript next.config so a baseUrl-local file could shadow an installed package of the same name. That de-externalizes every installed package and forces it through Vite's module runner, which breaks CJS config plugins that call `require` / `require.resolve` at runtime — e.g. `@next/mdx`, which failed the app-router-playground production build with "require is not defined". This was not caught pre-merge because deploy-examples is skipped for fork PRs. Drop the blanket `noExternal: true`. Installed packages stay externalized (so CJS config plugins keep working), and the native Vite 8 `resolve.tsconfigPaths` resolver still resolves baseUrl-local bare imports that have no installed package of the same name (the real Next.js tsconfig-extends fixture). The only behavior we give up is shadowing an installed package with a baseUrl-local file of the same name, which no Next.js fixture exercises. - Update the shadowing test to assert the installed package wins. - Add a regression test for an @next/mdx-style CJS plugin that calls `require.resolve` at runtime (red before this change). - Document the package-precedence behavior in the README limitations entry. |
||
|
|
03845f52b2 |
ci(bonk): bump models to Opus 4.8 high and GPT-5.5 high, opencode 1.15.13 (#1708)
* ci(bonk): switch reviews to GPT-5.5 xhigh Bonk and BigBonk still routed review runs to Claude Opus 4.6 with the max variant. That leaves review commands and local opencode review agents on the old Anthropic model instead of the intended OpenAI xhigh setup. The workflows now route through Cloudflare AI Gateway to openai/gpt-5.5, request the xhigh variant, and pin opencode to 1.14.40 so the cf-ai-gateway reasoning_effort propagation fix from anomalyco/opencode#25573 is present. The viguy and reviewer agent frontmatter plus contributor docs now describe the same GPT-5.5 review setup. * ci(bonk): keep regular Bonk on Opus 4.7 The review model update should split regular Bonk and BigBonk. Regular Bonk is meant to use Claude Opus 4.7, while BigBonk should use GPT-5.5 with xhigh reasoning. Point the /bonk workflow back to the Anthropic Opus 4.7 model through Cloudflare AI Gateway and keep its max variant. Leave /bigbonk on GPT-5.5 xhigh, and update contributor docs to describe the two review paths. * ci(bonk): flip Bonk and BigBonk models The requested review model split is regular Bonk on GPT-5.5 xhigh and BigBonk on Claude Opus 4.6 max. Update only the Bonk workflow model and variant inputs. Leave the local opencode agent frontmatter and contributor docs unchanged from the existing PR branch. * ci(bonk): align agents with review workflows Bonk should run GPT-5.5 xhigh through the reviewer agent, while BigBonk keeps the viguy agent on Claude Opus 4.6 max. Route the Bonk workflow to the reviewer agent, set viguy's default model to Claude Opus 4.6, and use the same 0.2 temperature for both opencode agent definitions. * ci(bonk): use direct model ids in agent defaults OpenCode agent frontmatter should use the normal provider/model ids. The workflow inputs still need Cloudflare AI Gateway-prefixed model ids because the GitHub action routes through AI Gateway credentials. Set viguy back to anthropic/claude-opus-4-6 and reviewer to openai/gpt-5.5 while leaving the Bonk and BigBonk workflow model overrides unchanged. * Update CONTRIBUTING.md Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> * Update CONTRIBUTING.md Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> * ci(bonk): bump to Opus 4.8 high, GPT-5.5 high, opencode 1.15.13 * ci(bonk): bump models to Opus 4.8 high and GPT-5.5 high, opencode 1.15.13 * ci(bonk): bump models to Opus 4.8 high and GPT-5.5 high, opencode 1.15.13 --------- Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> |
||
|
|
5991406324 |
fix(config): resolve next.config baseUrl imports from tsconfig (#1701)
* fix(config): resolve next.config baseUrl imports from tsconfig next.config.ts could resolve tsconfig paths aliases but not bare imports covered by compilerOptions.baseUrl. This broke configs that inherit baseUrl through tsconfig extends and import local modules such as "bar" from next.config.ts. Carry the effective baseUrl out of the tsconfig reader and add a narrow Vite runner resolver for bare specifiers so local files resolve before normal package fallback. Add a regression ported from the Next.js tsconfig-extends deploy fixture. * fix(config): preserve baseUrl priority over packages next.config.ts imports that matched both a local tsconfig baseUrl target and an installed package resolved to the package first. That violated the TypeScript and Next lookup order where baseUrl local resolution wins before package fallback. Vite's module runner externalized installed bare packages before resolveId hooks ran, so the baseUrl resolver only handled missing packages. Keep config imports inside the resolver pipeline, run the baseUrl resolver before package resolution, and scope it to project-owned importers so package dependencies are not rewritten through the app baseUrl. Adds focused regression coverage for local-over-package shadowing, package fallback, node_modules importer isolation, and existing CJS/ESM package imports from next.config.ts. * fix(config): keep baseUrl lookup scoped to TypeScript configs next.config.mjs could inherit TypeScript baseUrl package shadowing because the config runner noExternal override and baseUrl resolver were enabled for every config file type. That leaked Next's TypeScript config transpilation semantics into non-TypeScript config loading. Gate the baseUrl resolver and noExternal runner override behind TypeScript config extensions while keeping existing alias loading behavior unchanged. Add a regression for mjs package shadowing and update the tsconfig helper comment to name baseUrl explicitly. * refactor(config): resolve next.config baseUrl via native Vite 8 tsconfigPaths Replace the hand-rolled tsconfigBaseUrlResolverPlugin with Vite 8's native resolve.tsconfigPaths (oxc-resolver tsconfig: 'auto'), which already mirrors Next.js's SWC paths + baseUrl handling: follows extends, resolves baseUrl-local bare imports before package fallback, and scopes the app baseUrl to project-owned importers via per-importer tsconfig discovery. Gate it behind the existing Vite-major check (matching index.ts). Vite 7 has no native equivalent, so baseUrl bare imports in next.config.ts are a documented Vite 7/8 capability gap; paths aliases still work on both via resolve.alias. Extract getViteMajorVersion into utils/vite-version.ts so next-config.ts and index.ts share one detection path without a circular import. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
044ccf7c4c |
fix: allow server-only imports from 'use server' modules (#1707)
PR #1697 added a client-environment guard that throws when a module imports "server-only", to match Next.js rejecting server-only from client-reachable code. But Server Actions modules ('use server') live in the server/action layer, not the client layer, and Next.js explicitly allows them to `import 'server-only'` (see test/e2e/app-dir/actions/app/client/actions.js). Because the client bundle holds references used to invoke the actions, the action module enters the client graph and the guard threw, failing the entire client build. This false positive broke 136 deploy-suite tests across the actions and app-basepath suites. Exempt modules with a top-level "use server" directive so genuine client modules are still rejected. Adds a regression test that builds an App Router 'use server' module importing server-only, alongside the existing negative test proving client modules are still rejected. |
||
|
|
c4e64c908b | fix: make fnv1a64 output fixed-width (#1706) | ||
|
|
8ae5abd56c | fix(app-router): preserve forwarded action redirect wrappers (#1582) | ||
|
|
66519412c3 | fix(app-router): reuse pending prefetched RSC payloads (#1704) | ||
|
|
19dfd5e009 | feat(config): add experimental.appShells config plumbing and validation (#1692) | ||
|
|
f36959dea6 |
fix(app-router): preserve middleware headers on metadata routes (#1700)
App Router metadata routes returned their generated response before the middleware response context was merged. That let static metadata defaults such as Cache-Control: public, max-age=0, must-revalidate overwrite a middleware Cache-Control header for app/favicon.ico. Apply the same middleware response-header and status merge used by other App Router response paths before returning metadata route responses, and cover the upstream Next.js no-duplicate-headers fixture. |
||
|
|
a35e43cef3 |
feat(skip): add layout safety observation foundations (#1672)
* feat(skip): add layout safety observation foundations Introduce the per-layout observation primitives that later slices need to decide whether retained static layouts can be reused. This slice adds the machinery only; nothing reads the observations yet, so behavior is unchanged. - app-layout-param-observation: per-layout completeness, param keys, structural scope, finite revalidate, request API, cacheLife, cache tag, cacheable fetch, dynamic fetch, and unstable_cache observation tracker. - app-page-probe: bounded React server subtree probe that walks layout-returned children including memo, forwardRef, and React.lazy wrappers, enforces depth and node limits, and refuses single-use iterable children. - shims: thenable params observer for sync, await, destructure, and enumeration. Cache, fetch, and unified request context record unstable_cache, cacheable fetch, and request-scoped observation state. - app-page-params: structural segment-param scope so empty optional catch-all targets are not treated as param-free downstream. No skip transport behavior. Later slices wire the planner, manifest, and encoder onto these observations. * fix(skip): address review feedback on observation foundations - Add currentFetchSoftTags reset in runWithIsolatedProbeDependencies to prevent soft tag leak from parent scope (matches runWithFetchCache isolation) - Replace unnecessary JSON.stringify in cacheLife check with direct null comparison - Add comment documenting intentional omission of markProbeComplete on probe errors (keeps completeness as unknown, safe fallback to render-and-send) * fix(skip): record cacheable-fetch observation synchronously before first await The patched fetch deferred observation and tag collection past await buildFetchCacheKey(). If a layout probe started a cacheable fetch and returned synchronously without awaiting it, the probe snapshot would see cacheableFetchCount: 0 -- a false proof that could let the skip planner reuse a stale retained layout. Move observation and tag recording before the first await in the cacheable branch. If key generation later fails and the fetch falls back to dynamic, recording both is conservative (false unsafe costs performance, not correctness). Add an integration test that exercises the real patched fetch shim without mocks to verify synchronous observation capture. * fix(test): remove polluting fetch restore and catch background promise Do not restore globalThis.fetch in test cleanup. ensureFetchPatch sets a global _PATCH_KEY guard; restoring fetch without clearing that marker makes later calls skip reinstallation silently. Catch the background fetch promise inside the probe so the patched fetch continuation (cache lookup, network fetch) cannot produce an unhandled rejection after the synchronous assertions complete. * chore: empty commit to trigger CI * chore: empty commit to trigger CI |
||
|
|
b1f2e57bc4 |
fix(pages): preserve import.meta.url source paths (#1598)
* fix(pages): preserve import.meta.url source paths
Pages Router production bundles previously left direct import.meta.url expressions for the bundler to evaluate from generated output. Server renders could expose dist/server/entry.js, and prerendered Pages output could miss the client entry needed for hydration to replace the server value with the Turbopack-style browser source URL.
The fix rewrites direct import.meta.url reads in user modules to source-file URLs at transform time, keeps new URL asset bases intact, and loads the Pages client entry from the build manifest for Node production and prerender servers.
Regression coverage ports the Next.js import-meta deploy test, including the prerender and hydrated browser path.
* fix(tests): run import.meta browser coverage in e2e
The import-meta regression test launched Playwright from a Vitest integration shard. That shard does not install Playwright browser binaries in CI, so it failed before exercising the runtime behavior.
Move the browser assertion into the existing pages-router-prod Playwright project, keep the lower-boundary server and client chunk assertions in Vitest, and cache the import-meta plugin root paths after an early source-text bailout to address review feedback.
* refactor(import-meta-url): canonicalize each module once per transform
The transform path canonicalized the same module id twice: once in the
eligibility check and again when computing the replacement value, so every
rewritten module paid for two fs.realpathSync.native() syscalls.
The eligibility check now returns the canonical id and threads it through to
the value computation, leaving a single realpath per module.
Also widens the extension match to .cts/.mts/.cjs and documents that the
new URL() skip only covers the bare URL identifier, matching Vite's own
asset-detection scope.
* test(pages): drop unrelated prerender from top-level-await setup
The top-level-await production test ran runPrerender in its setup, but the
assertions pass identically whether the routes are prerendered or rendered on
demand. The step added unrelated build work to a test that is not about
prerendering or client-entry injection.
* fix(pages): share client entry fallback
* fix(import-meta-url): address PR review feedback
- Handle ChainExpression (import.meta?.url) in new URL() asset-detection
skip so the optional-chain variant preserves Vite asset resolution
- Add explicit ChainExpression check in visit() to avoid fragile
reliance on coincident inner-node start/end ranges
- Add test for new URL('./file', import.meta?.url) preservation
- Fix decodeHtmlText entity ordering (& before ") in both
unit and E2E test helpers
- Add doc comments: excluded prefixes rationale, node_modules
early-exit optimization intent
* fix(pages): select client entry by marker in build manifest
findClientEntryFileFromManifest returned the first chunk flagged
isEntry, but a client build can emit several entry chunks (client
entry plus instrumentation/middleware) and manifest order is not
guaranteed. The on-disk fallback already disambiguates by the
vinext-client-entry / vinext-app-browser-entry marker; apply the
same check to the manifest path so both lookups agree, falling back
to the first entry only when nothing is marked.
* refactor(pages): address review feedback on client-entry/import-meta-url
- prod-server: hoist findClientEntryFile out of the manifest if/else
since it already handles a missing manifest via the on-disk fallback
- import-meta-url: document the two known limitations (destructured
access, aliased new URL() base) that match Vite's own behavior
- import-meta-url: drop the dead visit(value.callee) for new URL() —
the callee is always the bare URL identifier
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
7bb669c0e9 |
test(routing): regression for parallel slot page-over-default priority (#1339) (#1689)
Issue #1339 reported parallel route slots falling back to default.tsx instead of the matched page. That gap was already closed on main by #1389 (resolve nested slot pages over default.tsx) and #1659/#1535 (treat @children as transparent). This adds a regression test mirroring the Next.js parallel-routes-layouts fixture — the canonical "Hello from Nested" vs "default page" case — asserting that: - the children slot at /nested resolves nested/page.tsx, not nested/default.tsx; - sibling @foo/@bar slots that each own both a page and a default pick their page; and - at /nested/subroute (only @bar matches) the children slot falls back to default, @bar mirrors its subroute page, and @foo keeps its default. Locks in page-over-default priority across sibling slots so the behavior cannot silently regress. |
||
|
|
87479df147 |
fix: bound default memory cache size with LRU eviction (#1699)
MemoryCacheHandler used an unbounded Map for default ISR/fetch/cache storage. Add a Next-style memory cap with approximate byte sizing and least-recently-used eviction. Behavior: - default max size is 50 MiB, matching Next.js cacheMaxMemorySize default - cacheMaxMemorySize: 0 disables default memory storage - entries larger than the configured cap are not stored - get() refreshes LRU order - tag invalidation metadata is capped to 10k entries Wire next.config.js cacheMaxMemorySize into both App and Pages generated entries via configureMemoryCacheHandler(). Add focused unit coverage for LRU eviction, get() recency refresh, and disabled storage. |
||
|
|
0c1193fd76 |
fix: include i18n context in Pages ISR cache keys (#1698)
Pages Router ISR cache entries were keyed only by pathname. With i18n domain routing, the same pathname can render different content depending on the matched domain and default locale. A cache entry created for one domain could be reused for another domain. Add an i18n cache variant to the Pages ISR key when i18n is configured: - domain:<domain> for matched domain-locales - locale:<locale> for locale-prefix/default-locale routing Adds a Pages i18n production fixture page with getStaticProps + revalidate and verifies example.fr and example.com maintain separate ISR entries for the same pathname. |
||
|
|
534487a5bf |
fix: reject server-only imports from client-reachable modules (#1697)
Match Next.js handling for the server-only marker: imports are allowed from App Router Server Components and middleware/server targets, but client-reachable modules must fail instead of silently resolving the empty shim. Add an AST-based client-environment transform guard so comments or string literals do not false-positive. The existing middleware test continues to prove server-only remains allowed from middleware graphs, and the new Pages Router fixture mirrors Next.js's compiler error test for server-only imported from a Pages client graph. |
||
|
|
e8c188ce13 |
feat(skip): add static layout reuse proof model (#1674)
* feat(skip): add static layout reuse proof model Add the proof/planner vocabulary that decides whether a verified static layout entry from a client reuse manifest is reusable. Nothing renders this decision yet, so no payload is omitted. - skip-cache-proof: static layout reuse planner with per-entry budget, exact artifact-compatibility check, payload hash, route id, and invalidation proof. Preserves rejection traces. Returns the existing disabled disposition when not authorized. - static-layout-client-reuse-proof: static layout payload hash, route id, and artifact compatibility helpers used by the planner. - client-reuse-manifest: extends the rejection code union with the layout-safety rejection codes the planner emits, and adds the enabled skip disposition variant so future slices can carry an authorized result without retyping the union. This slice is the proof vocabulary only. The planner is callable but not invoked from the render path; encoder behavior is unchanged. * fix(skip): address review — budget counts all wire entries, default enforcement, triple-walk justification, hash stability comment, forward declarations comment, absent-manifest test * fix(skip): replace as const with explicit return type annotation * fix(skip): replace as const with ProofFieldPair type alias * fix(skip): address copilot review — pairs serialization, budget disambiguation, skip-ineligible tracking, compatibility-bridged test * fix(skip): remove routeId from renderEpoch, bind verifier entryId, add sibling-route test * fix(skip): exclude routeId from payload hash, add end-to-end sibling-route proof test * chore(skip): move proof fields to artifact-compatibility, rename to maxWireEntriesToVerify, short-circuit exact check * chore(skip): address review comments — fix hash comment, remove redundant guard, drop budget alias, dedupe exact-compatibility check |
||
|
|
00f585969d |
feat: recognize Sparkle experimental flags (varyParams, optimisticRouting, cachedNavigations) (#1655)
* feat(check): recognize Sparkle experimental flags in CONFIG_SUPPORT * test(check): add tests for Sparkle experimental flags * feat(config): warn when cachedNavigations is set without cacheComponents * test: address ask-bonk review on cachedNavigations tests - Remove redundant mockRestore() calls (afterEach handles it) - Add negative test cases for cachedNavigations not set / false - Match existing swcEnvOptions test style (warn.mock.calls.find()) |
||
|
|
88b1152eb2 |
fix(app-router): include static-sibling info in SSR response (#1525) (#1686)
Next.js's loader-tree dynamic-segment tuple includes a `staticSiblings` list (the static sibling segment names at the dynamic URL level) so the client router can decide whether a cached prefetch of a dynamic route can be reused when navigating to a static sibling URL. vinext was not emitting this metadata, so the static-siblings e2e suite failed all four substring assertions against the RSC payload. This change: - Adds `computeAppRouteStaticSiblings(allRoutes, matchedRoute)` to the app route graph. Ported from Next.js's `getStaticSiblingSegments` in the next-app-loader. For each dynamic URL level in the matched route, it collects the names of routes whose corresponding segment is static and whose preceding URL prefix matches literally. - Pre-computes the result per route at entry-generator time (`buildRouteEntries` in app-rsc-manifest) so the runtime carries a static list instead of recomputing for every request. - Threads it through `AppPageRouteWiringRoute` and emits it on the wire as a new `__staticSiblings` metadata key when non-empty. The Flight serializer already spreads the elements record into the outgoing payload, so the key lands on the RSC response unchanged. The new `tests/app-static-siblings.test.ts` covers the helper's sibling-collection rules (cross-route-group, same-directory, deeply- nested non-leak, dedup) and asserts `buildAppPageElements` emits `__staticSiblings` on dynamic routes and omits it when empty — matching the four sub-cases of Next.js's static-siblings e2e suite. Ported from Next.js: test/e2e/app-dir/static-siblings/static-siblings.test.ts Refs cloudflare/vinext#1328v0.0.54 |
||
|
|
6d4dd949f6 |
test(link): cover Pages Router onClick/preventDefault semantics (#1470) (#1685)
Adds regression coverage mirroring Next.js's
`test/e2e/new-link-behavior/index.test.ts` so the Pages Router `<Link>`
shim is guarded against ever regressing on:
1. `onClick` firing on click with the synthetic event
2. `event.preventDefault()` inside `onClick` cancelling the Link's
own client-side navigation (no router.push, no history mutation,
no popstate dispatch)
The unit-level invariants in the current Link shim match Next.js's
`linkClicked` ordering — call user onClick, check defaultPrevented,
only navigate if not prevented — so these tests pass against the
current implementation and lock that behaviour in place.
|
||
|
|
0859b07b96 |
test(pages-router): regression test for /api/* dispatch when middleware runs (#1477) (#1687)
The Next.js Deploy Suite flagged `test/e2e/proxy-request-with-middleware` as a Pages Router failure where API routes appeared to return 500 when a middleware was present. The reported failure cannot be reproduced against the prod-server: GET, POST, query strings, custom request headers, the bare `/api` index handler, and middleware-injected request headers all return 200 today. The existing `pages-basic` fixture explicitly excludes `/api` from its middleware matcher, so the existing E2E tests never drive this combination directly. Add a focused regression test that builds a tmpdir Pages Router fixture whose middleware DOES match `/api/*`, boots the prod-server, and asserts the full matrix above. The Next.js fixture's actual API handler proxies via `req.pipe(...)`, which depends on Node `IncomingMessage` stream methods that vinext's prod-server does not yet expose on the `req` object created by `createPagesReqRes`. That's a separate Node-compat gap (closer to #1338) and is out of scope for this regression coverage — leaving a test that locks down the middleware/API handoff so the deploy suite's underlying claim is no longer ambiguous. |
||
|
|
5b58cb9be9 |
refactor: move normalizePathSeparators to utils/path (#1606)
* refactor: move normalizePathSeparators to utils/path * update from other pr --------- Co-authored-by: James <james@eli.cx> |
||
|
|
879698b0bc | refactor(static-file-cache): reuse normalizePathSeparators for path handling (#1604) |