mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.1
1163 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e7d8a159f4 |
chore: version packages (#1823)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>vinext@0.1.1 |
||
|
|
bf5b09c46f |
refactor(pages-router): unify triplicated request pipeline into runPagesRequest (#1782) (#1860)
* feat(pages-router): add runPagesRequest pipeline owner with focused tests
Introduces server/pages-request-pipeline.ts which owns the canonical
Next.js 9-step request ordering once. Returns a PagesPipelineResult
discriminated union: type:response for prod/worker callers that supply
render/api callbacks, type:render|api|next intents for dev callers that
omit them. Mirrors createAppRscHandler's thin-closure injection pattern.
Two latent drift bugs reconciled vs the worker copy:
- middlewareStatus now uses result.status ?? result.rewriteStatus
- applyMiddlewareRequestHeaders now passes preserveCredentialHeaders
Adds 28 focused behavior tests covering all pipeline branches.
Adds ./server/pages-request-pipeline export to package.json.
* refactor(pages-router): migrate prod-server.ts to delegate to runPagesRequest
Replaces the ~400-line inlined try block in startPagesRouterServer with a
thin adapter that calls runPagesRequest(webRequest, deps). The adapter
retains only Node-specific concerns: open-redirect guard, decode/400,
prerender endpoint, static assets, image opt, basePath strip, _next/data
normalization, and compression/streaming output.
Extends renderPage callback signature with optional stagedHeaders param
so CSP nonces and other pre-render header injection continue to work.
Copies __vinextStreamedHtmlResponse marker through mergeHeaders to
preserve stream-vs-buffer decision in the adapter.
276 pages-router tests, 112 routing tests, 28 pipeline tests all pass.
* refactor(pages-router): migrate deploy.ts worker template to delegate to runPagesRequest
Collapses the ~280-line inlined fetch handler into ~65 lines. Worker adapter
keeps only: registerConfiguredCacheAdapters, open-redirect guard, static-asset
404, x-nextjs-data header capture, filterInternalHeaders+cloneRequestWithHeaders,
basePath strip with hadBasePath, and image optimization. Delegates the rest to
runPagesRequest via PagesPipelineDeps.
Removes inlined imports: matchRedirect, matchRewrite, preserveRedirectDestinationQuery,
requestContextFromRequest, applyMiddlewareRequestHeaders, isExternalUrl, proxyExternalRequest,
sanitizeDestination, applyConfigHeadersToHeaderRecord, normalizeTrailingSlash, mergeHeaders,
normalizeDefaultLocalePathname, stripI18nLocaleForApiRoute, mergeRewriteQuery.
Updates deploy.test.ts assertions to verify delegation rather than inlined
step logic. 245 deploy tests pass.
* refactor(pages-router): migrate index.ts dev to runPagesRequest, delete parallel helpers
Replaces the inlined dev Pages Router pipeline with runPagesRequest. The
adapter keeps Node-specific concerns: Vite skips, cross-origin guard, image
302 redirect, .html normalization, open-redirect guard, decode/400, basePath
strip with hadBasePath, trailing-slash, _next/data normalization, rawHeaders
snapshot, cloudflare-plugin delegation.
Creates devRunMiddlewareAdapter closure that wraps the dev runMiddleware
(runner, path, i18n, basePath, trailingSlash, isDataRequest args) and
returns a MiddlewareResult-compatible object. Sets VINEXT_MW_CTX_HEADER for
hybrid app+pages mode as a side effect before returning.
Adds writeWebResponseToNodeRes() helper to stream Web Response to Node res
preserving multi-value Set-Cookie. Adds requestHeaders field to render/api
intents so dev adapter can flush post-middleware request headers to req.headers
before calling createSSRHandler/handleApiRoute.
Deletes:
- applyRedirects (lines ~4829-4869)
- proxyExternalRewriteNode (lines ~4867-4928)
- applyRewrites (lines ~4929-4951)
- applyHeaders (lines ~4951-5015)
276 pages-router tests, 112 routing tests, 245 deploy tests, 28 pipeline
tests all pass. vp check clean.
* refactor(pages-router): format deploy.test.ts string literals consistently
* fix(pages-router): restore hasAppDir dev gate; fix external proxy body/headers; update stale after-deploy assertions
Three regressions introduced by the dev pipeline refactor:
1. Hybrid app+pages dev mode: the 'render' intent branch in handlePagesMiddleware
unconditionally called createSSRHandler, missing the original hasAppDir fallthrough
gate. App Router page requests in the hybrid app-basic fixture returned 404 instead
of being deferred to the RSC plugin. Restored by matching the route before calling
the Pages SSR handler and returning next() when there is no pages match and hasAppDir
is true.
2. External proxy body/headers: proxyExternalRewriteNode read the body from the Node
IncomingMessage and the post-middleware headers from req. The refactored pipeline used
a body-less webRequest, so POST bodies were empty and middleware-modified request
headers (x-hello-from-middleware1 etc.) were not forwarded. Fixed by adding an
optional proxyExternal dep to PagesPipelineDeps; the dev adapter supplies it, building
a proper Request from the pipeline's current (post-middleware) headers and the Node
req body stream. Prod and worker adapters fall through to proxyExternalRequest as before.
3. Stale after-deploy assertions: two tests checked old inline call-site strings that
no longer appear in the generated worker entry after the delegation refactor. ctx is
still forwarded (deploy.ts:654,657); updated assertions to match the new call sites.
* fix(pages-router): restore public-file serving + API content-type; gate dev api flushes
Three behavioral regressions from the pipeline extraction, plus the bonk
review findings:
- prod-server (Node): restore post-middleware public-directory static file
serving (original step 5b). The adapter now supplies a serveStaticFile dep
that the pipeline calls after middleware (so middleware can intercept) and
before rewrites. Without it, public/ files (favicon.ico, robots.txt, etc.)
fell through to the renderer and 404'd. Adds a {type:"handled"} result for
callbacks that write their own output.
- prod-server (Node): restore the application/octet-stream content-type default
for API responses. The unified response path collapsed API and page fallbacks
to text/html; the pipeline now tags API results with isApiResponse so the Node
adapter picks octet-stream (arbitrary data) over text/html (content-sniffing
hazard). [bonk finding 1]
- index.ts (dev): gate the staged-header/req-header flushes behind a pages-api
match, mirroring the original 'if (apiMatch)' guard. The unconditional
flushRequestHeaders() wiped req.headers (incl. VINEXT_MW_CTX_HEADER) on the
api-miss -> next() path, dropping middleware context for app/api/* routes in
hybrid app+pages dev mode.
- pipeline: note {type:"next"} is reserved/unused [bonk finding 2]; document
the intentional external-proxy merge asymmetry on the bare proxyExternal
returns [bonk finding 3].
Adds 5 pipeline tests (serveStaticFile/handled ordering, isApiResponse tag).
* fix(pages-router): forward staged headers to worker renderPage; gate data-request defer
Two divergences found in code review of the extraction:
- deploy.ts (worker): the renderPage adapter dropped the pipeline's 4th
stagedHeaders arg, passing undefined for the SSR renderer's middlewareHeaders
param — so middleware-set CSP nonces were never applied to rendered HTML on
the worker, unlike the prod path. Widen the wrapper to forward stagedHeaders.
- pipeline: shouldDeferErrorPageOnMiss only gated on isDataReq, which the worker
never sets (it doesn't normalize /_next/data paths). The pre-refactor worker
gated on the x-nextjs-data header (isDataRequest), so a data-request miss now
wrongly deferred + ran fallback rewrites + re-rendered. Gate on both signals;
no-op for Node/dev where a data request already has isDataReq=true.
Updates the after-deploy assertion to the new call site and adds a pipeline
test for the data-request defer gate.
* fix(pages-router): address code-review findings (content-type, redirect query, dedup)
- prod content-type: passthrough responses (middleware short-circuits, external
proxies) no longer get a text/html default injected. Generalize the response
tag to defaultContentType (render -> text/html, api -> octet-stream, passthrough
-> unset); the Node adapter sends untagged responses verbatim via sendWebResponse,
matching pre-refactor behavior.
- redirect query: config-redirect Location now uses the raw req.url query (via a
new rawSearch dep supplied by the Node prod + dev adapters) instead of
new URL().search, which re-encoded chars and truncated at a literal '#'. The
worker keeps url.search (it only ever had a Web Request). Restores the original
prod/dev behavior.
- efficiency: matchPageRoute no longer runs twice per request on the common
no-afterFiles-rewrite path; reuse the Step 12 match unless afterFiles changed
the pathname.
- dedup: collapse prod-server's mergeWebResponse into the canonical mergeHeaders
(worker-utils) via delegation, removing the duplicate implementation and the
now-orphaned hasHeader/stripHeaders helpers. Existing mergeWebResponse tests
validate the delegation.
Updates pipeline tests for defaultContentType and adds a passthrough-verbatim
assertion. 36 pipeline + 309 features + 276 pages-router + 245 deploy + 9
after-deploy + 112 routing pass; vp check clean.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
3a1e56748f |
fix(app-router): match Pages navigation params in hybrid builds (#1741)
* fix(app-router): emit pages client entry for hybrid builds * fix pages router navigation params readiness * fix(pages-router): track hashed fallback client entry * fix(pages-router): honor navigation readiness in hooks * fix(pages-router): derive server router.isReady from SSR readiness Address review feedback on #1741: - getRouterSnapshot() now reads navigationIsReady from the SSR context on the server instead of unconditionally returning true. For pre-ready Pages routes (auto-export dynamic / query-string / rewrite-capable builds) this keeps useRouter().isReady consistent between SSR and client hydration, avoiding a hydration mismatch for components that read it in JSX. Mirrors Next.js render.tsx's server readiness rule. - Drop the redundant guard branch in getClientParamsSnapshot. - Add a regression test asserting server useRouter().isReady reflects the SSR navigationIsReady context. * fix(pages-router): align param resolution and dev/prod readiness parity Address review feedback (ask-bonk + self-review) on #1741: - Align client Pages navigation params to query-first, matching the server snapshot and Next.js's adaptForPathParams (derives path params from router.query). __NEXT_DATA__.query is kept current on client navigation, so the two snapshots are now provably identical and avoid a latent post- hydration mismatch. - Populate window.__VINEXT_PAGE_PATTERNS__ in the dev server (own script tag) so the next/navigation compat hooks can resolve a dynamic pattern from a resolved path in dev exactly as in production. Closes a dev/prod parity gap (prod set this via the client entry; dev never did). - Drop the redundant pre-readiness setSSRContext in dev; the single post- module-load call now publishes navigationIsReady before rendering, matching the prod handler. - Document that installPagesClientAssetGlobals is intentionally inert for App-only builds, and that getReadonlyPagesSearchParams' string-keyed cache is safe to share across concurrent SSR requests (immutable wrappers). - Add a dev regression test asserting __VINEXT_PAGE_PATTERNS__ exposure. * fix(pages-router): prefer URL params for client snapshots * fix(app-router): gate pages asset globals * test(app-router): add production test for hybrid basePath + assetPrefix * fix(navigation): widen usePathname return type to string | null * refactor(pages-router): dedupe navigation readiness modeling and cache pattern scan Addresses code-review cleanup findings on the Pages Router navigation readiness work: - Extract buildPagesReadinessNextData() so the dev SSR handler and the production Pages page handler derive the gssp/gsp/gip/appGip/autoExport __NEXT_DATA__ readiness flags from one place, instead of two hand-kept copies that could drift and produce a dev/prod hydration mismatch. - Extract resolvePagesNavigationPathname() shared by the client and SSR branches of getPagesNavigationContext, so the pathname null-ness rule (fallback / pre-ready auto-export dynamic) cannot diverge between them. - Memoize resolvePagesRoutePatternForPath()'s pattern scan, which ran an uncached O(routes) loop on every useSyncExternalStore snapshot read. - In getServerSearchParamsSnapshot, serialize the source only when the object-identity fast path misses, avoiding a toString() on every read. * refactor(pages-router): consistent ISR readiness data + robustness cleanups Addresses further code-review findings: - Thread the readiness flags (gssp/gsp/gip/appGip/autoExport) through the ISR background-regeneration path: renderPagesIsrHtml now serializes the same __NEXT_DATA__ readiness state the initial render emits, so a regenerated, re-cached page hydrates with the identical initial router.isReady the server computed. Previously these flags were dropped on that path — benign today (ISR-eligible getStaticProps routes never trigger the flag-gated early-ready branches) but a latent dev/prod hydration-readiness drift. Adds a shared PagesNextDataExtras type so every render path stays in lockstep. - readSsrManifest: degrade a malformed/unparseable SSR manifest to "no SSR manifest" with a warning instead of throwing and aborting server startup, restoring the previous tolerant behavior. - pages-readiness: project PagesReadinessNextData from the canonical VinextNextData and accept PagesPageModule, instead of bespoke types that could drift from the __NEXT_DATA__ shape. - getReadonlyPagesSearchParams: drop the redundant string-keyed cache slot; the per-object WeakMap is authoritative and concurrency-safe on its own. - isPagesRouterReady: drop the redundant server-side window check. * fix(pages-router): restore string-keyed readonly search-params cache Reverts the cache simplification from the previous commit. The string-keyed slot is load-bearing, not redundant: across the Pages Router pre-ready → ready transition the navigation context swaps in a new URLSearchParams instance even when the query string is unchanged, so the per-object WeakMap alone returns a fresh ReadonlyURLSearchParams wrapper and useSearchParams() loses Object.is stability — re-firing a [searchParams] effect. Caught by the rewrite-driven pre-ready static page e2e (search-params-snapshots saw ['',''] not ['']). Keeps the improved comment explaining why both cache levels are needed. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
9596b72495 |
fix(pages-router): pass revalidateReason "on-demand" to gsp/gssp (#1462) (#1856)
* fix(pages-router): pass revalidateReason "on-demand" to getStaticProps (#1462) Implements res.revalidate() for Pages Router API routes and wires the on-demand revalidation trigger through to getStaticProps/getServerSideProps context as revalidateReason: "on-demand". res.revalidate(urlPath) issues an internal HEAD request to the target path carrying the x-prerender-revalidate header (mirroring Next.js's api-resolver). The Pages render path (dev-server and prod pages-page-data) detects that header, bypasses the fresh/stale cache-hit short-circuits, and regenerates the entry synchronously with reason "on-demand". The build and stale reasons already worked; this finishes the on-demand case. Closes #1462 * fix(pages-router): authenticate on-demand revalidation with a secret (#1462) The on-demand `revalidateReason` detection treated `x-prerender-revalidate` as an unauthenticated presence check and sent the literal value "1". Any external client could send `x-prerender-revalidate: 1` to force synchronous regeneration of any ISR page, bypassing the fresh/stale cache short-circuits — a cache-stampede/DoS vector. Mirror Next.js's `checkIsOnDemandRevalidate`: a request is only treated as on-demand revalidation when the `x-prerender-revalidate` header value EQUALS a secret (the vinext analog of `previewModeId`). Introduce a per-process revalidate secret (`getRevalidateSecret`, 256-bit random hex via Web Crypto) that `res.revalidate()` attaches to its internal loopback request, and a constant-time `isOnDemandRevalidateRequest` equality check on the receiving dev-server and Pages page handler. Header presence alone is never honored. Also fix the test's HTML-reason extraction to avoid CodeQL's incomplete-sanitization / bad-HTML-regex findings, and add a negative test asserting forged `x-prerender-revalidate` values ("1", "", a guess) are rejected while the real secret is accepted. * refactor(pages-router): share res.revalidate() impl; support unstable_onlyGenerated (#1462) Address non-blocking bonk review findings: - Extract the two near-identical `res.revalidate()` implementations (`api-handler.ts`, `pages-node-compat.ts`) into a shared `performOnDemandRevalidate` helper so the secret wiring and success detection cannot drift between dev and prod. - Align success detection with Next.js's api-resolver: accept `x-nextjs-cache: REVALIDATED`, status 200, or a 404 when the caller passed `unstable_onlyGenerated`. Add `unstable_onlyGenerated` support (`x-prerender-revalidate-if-generated` header). * docs(pages-router): note x-nextjs-cache REVALIDATED branch is parity-only (#1462) * fix(pages-router): make on-demand revalidate secret build-time (shared across isolates) The on-demand ISR revalidation secret was a per-process random value. On Cloudflare Workers, res.revalidate()'s loopback fetch() can land on a different isolate than the sender, so a per-process secret mismatches across isolates and false-rejects legitimate revalidations. Mirror Next.js's previewModeId: generate one 256-bit secret at build time (in the vinext build CLI, once per build, shared across plugin instances like __VINEXT_SHARED_BUILD_ID) and bake it SERVER-ONLY into every server bundle via a Vite define (__VINEXT_REVALIDATE_SECRET). It is byte-for-byte identical in every isolate and never reaches the client bundle. getRevalidateSecret() reads the baked constant at runtime and falls back to a process-shared random secret in dev (single-process, no regression). Also persist revalidateSecret in vinext-server.json for diagnostics/tooling parity with prerenderSecret. Constant-time safeEqual validation and reject-arrays/empty/absent behavior are unchanged. Refs #1462 * test(pages-router): cover server-only revalidate secret; drop dead manifest reader Address ask-bonk review of the build-time revalidate secret: - Add tests pinning the security-critical invariant that the __VINEXT_REVALIDATE_SECRET define is baked into server environments (rsc/ssr) but NEVER into the client bundle (the client env returns null outright). A leak would ship the secret to every browser and re-open the cache-stampede vector the equality check prevents. Also harden the existing "no-ops when defineServer unset" test to explicitly clear the env var instead of relying on it happening to be unset. - Drop the unused readRevalidateSecret export and the orphaned revalidateSecret manifest field — the runtime authenticates against the baked define (both Workers isolates and the Node prod-server run the bundle), so the on-disk copy had no consumer (knip-flagged dead code). - Hoist the client early-return in vinext:compiler-define-server so the server-only guarantee is structural (removes the redundant name check), and reword the dev-fallback comments ("absent unless set during vinext build"). Refs #1462 |
||
|
|
062a0b9489 | chore(deploy-suite): bump playwright/playwright-chromium to 1.60.0 after install (#1862) | ||
|
|
c669930f56 |
fix(build): correct CSS ordering for global-not-found (#1549) (#1858)
* fix(build): correct CSS ordering for global-not-found (#1549) * test(build): assert global-not-found CSS ordering in production (#1549) Adds a production-build regression test for #1549: builds the global-not-found-css-order fixture, serves it via the Vite preview server, and asserts the route-miss 404 document serves global-not-found's own CSS (red wins) without leaking the root layout's stylesheet (green), while matched routes still serve the layout's CSS. Verified the test fails without the framework-chunk split in client-build-config.ts (the 404 leaks the layout's green) and passes with it. * chore: update lockfile for global-not-found-css-order test fixture (#1549) * refactor(build): migrate RSC framework chunk to codeSplitting; add unit tests Address bonk review on #1858 (issue #1549): - Replace the deprecated rolldown `advancedChunks` output config with the non-deprecated `codeSplitting` form for Vite 8+, mirroring the client build. Stops the per-prod-build deprecation warning. - Derive both RSC_FRAMEWORK_CHUNK_TEST (regex) and isRscFrameworkModule (predicate) from a single FRAMEWORK_PACKAGES list so they can't drift. - Add focused unit tests for createRscFrameworkChunkOutputConfig (both the Vite 7 manualChunks and Vite 8+ codeSplitting branches) and for the framework package matching (matches react/react-dom/scheduler/ react-server-dom-webpack, excludes react-icons / @react-aria/*). |
||
|
|
dbd6c6ea9c |
feat(release): reclassify commits via SHA-named changeset overrides (#1859)
* feat(release): reclassify commits via SHA-named changeset overrides
Auto-changesets are regenerated from commit subjects every push and never
committed to main, so a mislabeled commit can't be fixed by editing them, and a
hand-authored changeset can only raise a bump (max), never lower it.
Add a per-commit override: commit `.changeset/<sha>.md` and the release tooling
treats that commit as the bump declared in its frontmatter (patch->fix,
minor->feat, major->feat!, empty->chore/dropped), driving both the semver bump
and the changelog section. The file is a real changeset, so changesets/action
consumes it for the bump and deletes it on release -- overrides never accumulate.
Both create-changeset.mts (bump) and version.mts (changelog grouping) honor it,
keyed off the SHA in the filename.
* chore: reclassify #1804 as a fix
Override `feat(interception): sibling-style interception routes (#1804)`
(
|
||
|
|
29a040dd21 |
fix(app-router): include route pattern in repeated-slash Link warning (#1554) (#1855)
* fix(app-router): include route pattern in repeated-slash Link warning (#1554) The repeated forward-slashes Link warning added in #1563 fired with the wrong "in page" segment: it used `window.location.pathname` (or `"/"` during SSR) instead of Next.js's route pattern (`router.pathname`, e.g. `/my/path/[name]`). The Next.js compat test `repeated-forward-slashes-error` asserts the *full* message including that route pattern, so it kept failing in the deploy suite despite #1563 — the `/Invalid href/` regex matched but the `toContain` of the exact message did not. Source the page segment from the current render's route pattern via a new `getCurrentRoutePathnameForWarning()` helper: the Pages Router SSR context's route pattern on the server, `window.location.pathname` on the client, falling back to `"/"`. Normal Link behaviour is unchanged. Add an SSR-level compat test (app + pages router) that renders a Link with a repeated-slash href and asserts the exact warning, plus the upstream dynamic-route fixture `pages/my/path/[name]`. Finishes #1554 (the warning wasn't firing with the right page name despite #1563). Closes #1554 * fix(app-router): read route pattern via side-effect-free accessor The previous commit imported `router.ts` into `link.tsx` to source the route pattern for the repeated-slash warning. But `router.ts` has a browser-only top-level side effect (`installWindowNext({ router })`), so pulling it into the Link shim — which is in the App Router client bundle too — clobbered `window.next.router` with the Pages Router instance, breaking App Router client navigation (useParams/prefetch/interception E2E timeouts). This is exactly why `navigation.ts` deliberately avoids importing `router.ts` and reads the Pages nav context through a `Symbol.for` handle. Mirror that pattern: add a side-effect-free `route-pattern-for-warning` module that reads the SSR route pattern through a `Symbol.for` accessor published by the server-only `router-state.ts`. The Link shim imports only this tiny module, so no Pages Router side effect leaks into the App Router bundle. Behaviour is unchanged: pages-router SSR reports `in page: '/my/path/[name]'`, client/App Router fall back to `window.location.pathname` then `"/"`. |
||
|
|
dd3fb05977 |
test(pages-router): cover javascript: URL blocking (#1576) (#1854)
* fix(pages-router): emit console.error for javascript: URL navigation (#1576) Finishes the Pages Router half of #1576 (the App Router warnings already land). Adds the four Pages Router scenarios from the upstream test/e2e/app-dir/javascript-urls suite — Link `href`, Link `as`, router.push, and router.replace — as e2e fixtures + a Playwright spec, plus unit coverage for the shared Link shim's dangerous-click handler. No production change is required: the Link shim already emits the canonical "Next.js has blocked a javascript: URL as a security precaution." console.error on click for both `href` and `as`, and the Pages Router router.push/replace synchronous guard from #1575 already does the same. This PR adds the missing Pages Router coverage that proves all four scenarios. Closes #1576 * test(pages-router): suppress dev error overlay before safe navigation The router.push/replace javascript: scenarios throw synchronously (the #1575 behaviour that surfaces the console.error), which trips vinext's dev error overlay. Its backdrop intercepted the follow-up safe-link click, timing out the test even though the security block already passed. Suppress the overlay via the existing disableDevErrorOverlay helper after asserting the block, matching how other pages-router/app-router specs handle intentional dev-time errors. * test(pages-router): cover javascript: URL blocking in production build The Pages Router javascript: URL guard (Link dangerous-click handler in shims/link.tsx + router.push/replace synchronous guard in shims/router.ts, both via shims/url-safety.ts) is already correct on main. The existing e2e spec, however, only runs against the dev server (vp dev), while the Next.js deploy suite exercises these scenarios against a production build where the dev error overlay is absent and the client bundle is minified. Add a pages-router-prod Playwright spec that re-runs all four upstream scenarios (Link href, Link as, router.push, router.replace) against vinext build + vinext start, so a prod-only regression is caught here. Refs #1576 |
||
|
|
76a39cb467 |
fix(actions): return 404 for MPA action on app with no server actions (#1340) (#1853)
* fix(actions): return 404 for MPA action on app with no server actions (#1340) A multipart form POST to an App Router page is always a server-action attempt. When the form body decodes to no action at all (e.g. the build has no server actions, so decodeAction returns null rather than throwing), vinext fell through to a 200 page render instead of Next.js' 404 + x-nextjs-action-not-found. The fetch-action variant (via the Next-Action header) already worked; only the MPA/no-JS form-POST variant was wrong. Gate the new 404 on the posted-to route being a page: route handlers run after action dispatch and legitimately accept raw multipart POSTs, so they still fall through. The page-vs-route distinction uses the static __loadPage / __loadRouteHandler markers, available before lazy module hydration. Finishes the no-server-actions MPA case of #1340 (error-boundary, content-type, and unrecognized-action handling landed in #1386/#1668). * test(actions): add MPA no-action integration test; skip redundant route match (#1340) Address ask-bonk review on #1853: - Add a dev-server integration test that POSTs multipart/form-data to a page (/about) and asserts 404 + x-nextjs-action-not-found, locking in the entry-side matchRoute + __loadPage/__loadRouteHandler classification end-to-end (previously only unit-tested by passing hasPageRoute directly). - Gate the progressive-action route match on isProgressiveServerActionRequest so matchRoute no longer runs on every App Router request — only for actual multipart/no-actionId POST candidates. |
||
|
|
558ee4121e |
fix(app-router): scroll to top with hoisted children and loading.js (#1368) (#1857)
* fix(app-router): scroll to top with hoisted children and loading.js (#1368) Move the committed scroll/focus target above the route loading Suspense and handle route content whose first DOM node is hoisted into <head>, so default-navigation autoscroll runs as a committed route effect rather than relying on a post-navigation scrollTo fallback. - loading.js: the scroll target stays mounted while the loading fallback renders (matching Next.js placing ScrollAndMaybeFocusHandler above the LoadingBoundary), so scroll fires against the loading boundary DOM. - hoisted children: when findDOMNode resolves an element hoisted into <head> (e.g. <style precedence>), scroll the document to the top as the committed-effect equivalent of Next.js deferring to a higher layout-router. Finishes the remaining scroll-to-top cases of #1368 on top of the autoscroll parity landed in #1599-1602. * docs(app-router): clarify prefetch shell omits scroll target (#1368) |
||
|
|
0c7ff57710 |
fix(build): exclude Vite build manifests from Cloudflare asset uploads (#1850)
On Cloudflare Workers the ASSETS binding serves any uploaded file whose path matches the request BEFORE the Worker runs. Cloudflare builds enable `build.manifest` (and `ssrManifest` for Pages Router) so the worker entry can compute lazy chunks, which writes Vite's manifests to `dist/client/.vite/`. Because nothing excluded that directory from the asset bundle, `/.vite/manifest.json` and `/.vite/ssr-manifest.json` were publicly fetchable, leaking the full source-file -> chunk mapping (including the paths of routes that are never linked from the UI). The Node production server already blocks `/.vite/` explicitly (server/static-file-cache.ts); this adds the Cloudflare-side equivalent by writing a `.assetsignore` into `dist/client` during the cloudflare-build closeBundle hook (right after the existing `_headers` generation, after the manifests have been read). wrangler matches `.assetsignore` with gitignore semantics, so the bare `.vite` entry excludes the directory and its contents from upload, and the file is never served. The merge logic preserves any user-authored `.assetsignore` and only appends missing patterns, so it is idempotent across rebuilds. The writer lives in a small typed helper (build/assets-ignore.ts) with focused unit tests; a real Cloudflare build test asserts the wiring emits `.assetsignore` with `.vite` while the manifest still exists on disk. |
||
|
|
cfc0f8146d |
fix(app-router): resolve explicit parallel slot with no page (#1535) (#1852)
* fix(app-router): resolve explicit parallel slot with no page (#1535) When a parallel slot defines an explicit sub-page (e.g. `@slot/baz/page.tsx`) but no corresponding children page (`baz/page.tsx`) and no `default.tsx` for the children slot, vinext minted a synthetic `/baz` route with a null children page. That route shadowed the sibling catch-all and rendered an empty children prop, so the request hung (the upstream "explicit slot but no page" case timed out). Next.js serves such a slot-only sub-route's children from the nearest sibling catch-all. Add `findCatchAllPage` and use it as the children fallback in `discoverSlotSubRoutes` when no `default.tsx` exists, so `/baz` renders `[...catchAll]/page.tsx` for children while `@slot/baz/page.tsx` fills the slot. A children `default.tsx` still wins over the catch-all when both exist. Finishes the explicit-slot-but-no-page case of #1535 (the children-slot priority case landed in #1659). Fixes #1535. * test(app-router): document catch-all children params limitation (#1535) Address ask-bonk review: the synthetic slot-only sub-route (e.g. /baz) has a static URL pattern, so a catch-all children page reading params.catchAll gets empty params (Next.js passes ["baz"]). Document the divergence inline in discoverSlotSubRoutes and add a graph-level test locking in the current static shape so a future param-population fix must update it. |
||
|
|
ca690db954 |
test(actions): cover server action POST under node-runtime middleware (#1480) (#1847)
* test(actions): cover server action POST under node-runtime middleware (#1480) Adds regression coverage for cloudflare/vinext#1480: a server-action POST must still be intercepted and executed when a node-runtime middleware matches the action path and consumes the request body before falling through with NextResponse.next(). The app-basic fixture's middleware now matches `/nextjs-compat/action-node-mw` and reads the POST body (mirroring Next.js' `middleware-node.js`), with a bound form server action on that page. Two tests exercise the scenario: - tests/nextjs-compat/action-node-middleware.test.ts (dev inline-middleware path) - tests/app-router-production-server.test.ts (production RSC entry path, the layer the upstream deploy suite exercises) vinext already isolates the middleware's body branch from the downstream request (applyAppMiddleware / createNextRequest clone the body) and treats node-runtime middleware identically to edge middleware, so the action POST is dispatched correctly. These tests lock that behavior in across the dev and production request paths. Closes #1480 * style(test): apply formatter to action-node-mw prod test * test(actions): declare runtime:nodejs on fixture middleware for #1480 Address review: make the app-basic fixture middleware faithfully mirror Next.js' middleware-node.js by exporting config.runtime = 'nodejs'. vinext runs all middleware on Node regardless, so this is a behavioural no-op for the other matcher entries, but it makes the action-node-mw regression a true guard for the named node-runtime scenario from the issue title. * docs(test): point #1480 fixture comment at middleware-runtime config logic Address review nit: the config-reading logic (matcher-only, no runtime branch) lives in server/middleware-runtime.ts, so reference that file where a future runtime branch would land. |
||
|
|
49d753217b |
fix(app-router): propagate "use cache" tags to route-handler ISR entries (#1453) (#1848)
* fix(app-router): propagate "use cache" tags to route-handler ISR entries (#1453) * fix(app-router): bubble nested use-cache tags to parent on data cache HIT (#1453) |
||
|
|
9b24683503 | fix(build): don't crash on benign floating asset-import errors (#1510) (#1846) | ||
|
|
600ba69b4d |
fix(routing): propagate middleware draft cookie to pages/api fallback (#1520) (#1845)
* fix(routing): propagate middleware draft cookie to pages/api fallback (#1520)
In a hybrid app (app/ + pages/api/*), requests with no matching App Router
route fall through to the Pages Router via renderPagesFallback. The bridge
forwarded middleware-mutated request headers and response headers, but dropped
the __prerender_bypass Set-Cookie emitted by draftMode().enable()/disable()
inside middleware — that cookie lives in a request-scoped side channel that
App Router route handlers and page renders read via getDraftModeCookieHeader(),
but the Pages fallback path never did.
Thread getDraftModeCookieHeader through the bridge and append the draft cookie
to the Pages Router fallback response, matching how App Router responses surface
it. Closes the draft-mode flow called out in #1520.
Closes #1520
* test(routing): cover draft-cookie fallback in app-pages-bridge unit test
Add getDraftModeCookieHeader to the renderPagesFallback dependency stub and
assert the draft cookie is appended (API + renderPage branches) and omitted
when middleware did not enable draft mode. Keeps the unit test in sync with
the new required dependency.
* docs(routing): document hybrid-dev draft-mode limitation in pages bridge
Address review feedback on #1845: clarify in app-pages-bridge that the
draft-cookie fallback covers production (inline RSC handler), while hybrid dev
runs middleware in a separate Vite Pages SSR runner where draftMode() inside
middleware is not yet permitted — a pre-existing limitation tracked separately
from #1520. The unit-test regression flagged by review was already fixed in
|
||
|
|
6958dfd0b4 |
fix(build): expand require.context into import.meta.glob map (#1501) (#1836)
* fix(build): expand require.context into import.meta.glob map (#1501) * style: format require-context fixture page * fix(build): strip g/y regex flags and default require.context recursive to true Address ask-bonk review on #1836: - Strip global/sticky flags when reconstructing the filter regexp; a stateful g/y RegExp.test() drops every other matching module. - Default the omitted recursive arg to true to match Webpack's useSubdirectories default (was silently false). - Add a g-flagged fixture/test case locking in the flag-stripping fix. - Clarify the misleading descent comment in collectRequireContextCalls. * test(require-context): cover ctx(key) retrieval and MODULE_NOT_FOUND Address ask-bonk follow-up: lock in the runtime contract beyond .keys(). - Assert ctx('./parent/file1.js').default resolves the module namespace. - Assert an unknown key throws with code 'MODULE_NOT_FOUND'. * docs(require-context): document extensionless no-regex parity caveat Address ask-bonk note: with no filter regexp, import.meta.glob only surfaces resolvable modules, so extensionless keys Webpack would include can be dropped. Documented as a low-risk divergence (upstream Next.js's extensionless test is disabled/Turbopack-pending). |
||
|
|
3f6c174c20 |
fix(app-router): resolve query-only Link href against current path (#1540) (#1844)
A <Link href={{ query: {...} }} /> with no `pathname` was resolved by
defaulting the base to "/", producing "/?query" instead of a query-only
href. The shallow navigation then recorded the wrong history entry (the
site root) so back/forward traversal landed on the wrong URL/page.
Mirror Next.js's formatUrl() (`pathname = urlObj.pathname || ''`): when
`pathname` is omitted, emit a relative query-only href (`?query`) so the
existing relative-href resolution applies it against the current path.
Adds a ported e2e regression (middleware-rewritten shallow link history
traversal) plus a Link unit test.
Closes #1540
|
||
|
|
def033614b |
fix(app-router): render built-in fallback when global-error.tsx throws (#1548) (#1837)
* fix(app-router): render built-in fallback when global-error.tsx throws (#1548) When a user's app/global-error.tsx itself throws while rendering, vinext previously crashed the request with a raw 500. Next.js instead nests the user's global-error inside an outer boundary whose fallback is the built-in default global-error, so a broken boundary degrades to a usable 'This page couldn't load' document. This change mirrors that structure: - Client-side: wrap the user's global-error in a GlobalErrorBoundary whose fallback is the built-in DefaultGlobalError (matches Next.js's outer RootErrorBoundary errorComponent={DefaultGlobalError}). - Server-side: React's renderToReadableStream rejects on a shell-level throw even with an error boundary present, so when the global-error boundary throws during SSR we re-render with the built-in default global error. Scoped to the global-error boundary (which owns the whole document); other boundaries propagate as before. The original error message still reaches the boundary in dev and a digest in prod via sanitizeErrorForClient (unchanged). Adds nextjs-compat coverage (dev + production preview) ported from test/e2e/app-dir/global-error/error-in-global-error. Closes #1548 * fix(app-router): format TRACKING.md table and address review notes (#1548) - Reflow TRACKING.md global-error table (vp check formatting). - Document GlobalErrorBoundary's intentional lack of a resetKey. - Add a port note explaining the SSR-via-pathname throw deviates from upstream's client useEffect throw (HTTP/SSR suite has no browser). * test(app-router): unit-cover global-error server-side fallback retry (#1548) Add a focused renderAppPageErrorBoundary unit test that asserts a throwing global-error boundary re-renders with the built-in default global-error (HTTP 200), locking in the try/catch retry directly rather than only via the fixture-coupled integration test. * fix(app-router): re-throw navigation signals from throwing global-error (#1548) Narrow the server-side global-error fallback so it only degrades genuine render failures to the built-in default. Navigation/HTTP-access signals (redirect/notFound/forbidden/unauthorized) thrown from within global-error are re-thrown so they reach their dedicated handlers instead of being swallowed into a built-in 200. Addresses review feedback that the catch was broader than Next.js's React error boundary. Adds a unit test asserting a notFound() signal from global-error propagates. * docs(app-router): clarify global-error signal re-throw comments (#1548) Soften wording: re-thrown navigation signals propagate to the top-level handler (same as before this change), rather than reaching dedicated handlers from this position. No behavior change. |
||
|
|
89a12bb203 | fix(build): don't path-resolve bare specifier aliases for esmExternals (#1507) (#1843) | ||
|
|
80acc0bbf0 |
fix(app-router): clear useLinkStatus pending after interrupted navigation (#1527) (#1839)
* fix(app-router): clear useLinkStatus pending after interrupting navigation (#1527) * fix(app-router): suppress notify on Safari history replace fallback The Safari coalescing fallback in syncCurrentHistoryStatePreviousNextUrl called the patched window.history.replaceState, which fires notifyLinkNavigationStart + commitClientNavigationState. On a URL-unchanged metadata sync (refresh/traversal commit) that could clear an unrelated <Link>'s useLinkStatus() pending state a few ms early on Safari. Route the retry through replaceHistoryStateWithoutNotify so the browser sees the same native replaceState without the patched-path side effects (#1527). |
||
|
|
de2ad5278f |
fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) (#1841)
* fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) * test(app-router): cover reactMaxHeadersLength config resolution * refactor(app-router): prioritize React preloads in Link header; cover buildAppPageLinkHeader Address review feedback on #1841: - Order React preload hints before font preloads so render-critical entries survive a tight reactMaxHeadersLength cap. - Rename the buildAppPageHtmlResponse option to linkHeader (it now carries the combined header, not just fonts). - Add unit coverage for buildAppPageLinkHeader (cap, 0-disables, ordering, whole-entry truncation). |
||
|
|
aea90aca05 |
fix(build): don't crash build when a PostCSS string plugin can't be resolved (#1509) (#1840)
* fix(build): don't crash build when a PostCSS string plugin can't be resolved (#1509) resolvePostcssStringPlugins runs in the Vite config hook for every environment (RSC/SSR/client). When a named PostCSS plugin failed to resolve from the project root (e.g. a CSS-modules + PostCSS app whose config lists a plugin that doesn't resolve from where vinext looks), the thrown error was uncaught and aborted the whole build with an opaque failure. Wrap plugin resolution in try/catch and fall back to undefined so Vite's own postcss-load-config resolves the config relative to the config file instead of crashing. Closes #1509 * docs(postcss): clarify whole-config fallback on resolution failure (#1509) |
||
|
|
c32cecdca8 |
fix(app-router): preserve _rsc query across redirects (#1529) (#1838)
* fix(app-router): preserve _rsc query across config redirects (#1529) * refactor(prod-server): reuse rawQs for redirect query; clarify .rsc test comment Address ask-bonk review on #1838: - prod-server.ts: reuse the existing rawQs instead of re-slicing url for the redirect Location query (identical value, removes redundancy). - app-rsc-handler.test.ts: tighten the .rsc-without-header test comment so it no longer overstates RSC re-detection — the guarantee is query preservation (mirroring Next.js), not re-arming RSC on the followed hop. |
||
|
|
a843ea2cfa |
fix(actions): enforce serverActions.bodySizeLimit on fetch actions (#1828) (#1834)
* fix(actions): enforce serverActions.bodySizeLimit on fetch actions (#1828) Oversized fetch (client-invoked) server-action requests were rejected with a bare 413 Payload Too Large response, which bypasses the client error boundary and surfaces the wrong status/content-type. Next.js instead throws the body-exceeded error before the action runs and, for fetch actions, emits a 500 Flight response carrying the rejected action result so the nearest error boundary catches it. Mirror that: when an RSC fetch action body exceeds the configured limit (content-length pre-check or stream read), render a Flight stream with returnValue { ok: false } and no page root, returning status 500. The no-JS progressive form path keeps its 413 (no client boundary to catch it). Closes #1828 * fix(actions): use verbatim bodySizeLimit string in body-exceeded error (#1828) Address bonk review: the body-exceeded error message must contain the verbatim serverActions.bodySizeLimit config string (e.g. "2mb") to match Next.js byte-for-byte — the upstream suites assert the log message literally ('Body exceeded 2mb limit'). The previous reconstruction from the parsed byte count produced '2 MB', diverging on casing and spacing. Thread serverActionsBodySizeLimitLabel (the verbatim config value, defaulting to "1mb") through next-config -> index -> app-rsc-entry -> the RSC action handler options, and use it directly in createBodyExceededError. Removes the lossy formatBodySizeLimit reconstructor. Tests assert the verbatim string. * fix(actions): default bodySizeLimit label to Next.js' '1 MB' literal (#1828) Address bonk re-review nit: when serverActions.bodySizeLimit is unset, Next.js emits 'Body exceeded 1 MB limit.' (its defaultBodySizeLimit = '1 MB' literal). Default serverActionsBodySizeLimitLabel to '1 MB' (was '1mb') so the unconfigured-case message matches upstream byte-for-byte too. |
||
|
|
97947ce775 |
fix(app-router): apply trailingSlash to route handler request URL (#1827) (#1835)
* fix(app-router): apply trailingSlash to route handler request URL (#1827) App Router route handlers received a NextRequest whose `url` was normalized with the default `trailingSlash: false` policy, so a request to `/runtime/edge/` (with `trailingSlash: true`) surfaced `req.url` without the trailing slash even though `req.nextUrl.pathname` kept it. Thread the configured `trailingSlash` through the route-handler dispatch chain (dispatch -> execution/cache -> tracked request -> NextRequest) so `NextURL` applies the correct policy when computing `request.url`, matching Next.js (test/e2e/app-dir/app-routes-trailing-slash). Closes #1827 * test(app-router): cover node runtime for trailingSlash:false route handler (#1827) Address ask-bonk review note on PR #1835: the trailingSlash:false suite only exercised the edge runtime. Parameterize it over edge + node to match the trailingSlash:true suite's coverage. |
||
|
|
b4c829d6a2 |
feat(interception): sibling-style interception routes (#1364 Part C) (#1804)
* test(interception): add failing sibling-interception graph tests * feat(routing): discover sibling-style interception markers * feat(routing): emit sibling interception manifest facts * feat(app-router): serialize sibling intercepts in route manifest * feat(app-router): match sibling intercepts in route matcher * feat(app-router): render sibling intercept as full page response * test: add siblingIntercepts field to AppRoute test fixtures * test(interception): add sibling interception fixtures * fix(app-router): emit null proof for sibling intercepts to bypass slot-validation * test(interception): add sibling interception e2e specs * fix(interception): correct (...)→(..)(..) marker in interception-from-root fixture * fix(interception): resolve sourceRouteIndex to slot-owner route in intercept lookup When a route inherits a @slot from an ancestor via discoverInheritedParallelSlots, the inherited slot's interceptingRoutes are copied from the ancestor. Previously, createInterceptLookup used routeIndex (the inheriting descendant's index) as sourceRouteIndex, causing resolveAppPageInterceptState to see sourceRoute === currentRoute → kind='current-route' instead of kind='source-route'. Fix: build a patternToIndex map and resolve sourceRouteIndex via the intercept's sourceMatchPattern (which names the actual slot-owner route), falling back to routeIndex only when no match is found. * fix(interception): use locale-prefixed href so middleware interception fires correctly * fix(interception): address codex review issues in sibling intercept implementation - Pass effectiveParams to resolveAppPageHead so generateMetadata/generateViewport sees the intercepted route's params instead of the source route's params - Rename SIBLING_INTERCEPT_SLOT_NAME from __page to __vinext_sibling_intercept to prevent collision with a user @__page parallel route directory - Restore middleware-rewrite fixture link without locale prefix so the interception-dynamic-segment-middleware spec exercises the intended middleware rewrite path * fix(test): update slotId assertions and restore locale-prefixed fixture link - Update test assertions from slot:__page to slot:__vinext_sibling_intercept following the rename in commit |
||
|
|
6005541c19 |
fix(app-router): restore scroll snapshots on back (#1743)
* fix(app-router): restore scroll snapshots on back * fix(app-router): invalidate scroll snapshots with bfcache epoch * fix(app-router): guard synchronous popstate scroll restore * fix(app-router): strip basePath for snapshot restore targets * fix(app-router): canonicalize snapshot restore target search * docs: document sync-over-async invariant in popstate snapshot restore * docs: document cross-module ordering contracts per review Adds comments for three load-bearing invariants identified in review: - rememberHistoryStateSnapshot layout effect ordering - synchronousPopstateScrollRestoreNavigationId single-slot latch - restoredBfcacheIds/reuseCurrentBfcacheIds snapshot timing |
||
|
|
5dd7a34eb6 |
fix(app-router): isolate page CSS chunks in production (#1738)
* fix(app-router): isolate production page CSS chunks Production App Router builds statically imported every page module into the generated RSC manifest. That let Vite/Rolldown concatenate page-level global CSS from sibling routes into shared CSS assets, so a sibling hash page with scroll-padding-top changed native hash scroll offsets on unrelated pages. The manifest now emits cached dynamic loaders for page modules and loads the matched page module at the route dispatch boundary. Page generateStaticParams sources use the same loader with an explicit missing-export sentinel so prerender still distinguishes absent exports from malformed results. * test(app-router): add focused unit test for route module loader Address the non-blocking review note from PR #1738: per AGENTS.md, behavior moved out of a template string should get a focused helper test. Cover createRouteModuleLoader (promise dedup, rejection propagation), loadRouteModules (page assignment, concurrent dedup, no-op cases, missing `page` key) and loadRouteMatch (null short-circuit, match load). Also cover createLazyGenerateStaticParamsSource (delegation, missing sentinel for null/empty/non-function exports). Document the rejected-load cache intent and the per-request intercept copy in the WeakMap so future readers don't try to "fix" them as bugs. * test: stabilize dev overlay HMR recovery * chore: retrigger ci * chore: fix knip check config * test(app-router): guard intercepted modal CSS + load lazy page before probe Adds the positive-direction guard the bonk review asked for: navigating /feed -> intercepted /photos/[id] must load the lazy modal page and emit its scroll-padding-top CSS chunk. The existing test only proved the negative (CSS absent on a direct /feed visit), so a regression where the lazy modal page failed to load its CSS would have passed silently. Also fixes the probePage() intercept branch, which read intercept.page without resolving the now-lazy __pageLoader. The render path awaits the loader (resolveAppPageInterceptState), but the probe path did not, so the dynamic-bailout probe silently inspected an undefined component and never observed the intercept page's searchParams/headers access. Resolve the lazy page before probing to match the render path. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
6fa25f49ce |
docs(changelog): backfill vinext release notes from GitHub releases (#1822)
* docs(changelog): backfill vinext release notes from GitHub releases Backfill packages/vinext/CHANGELOG.md with entries for every published GitHub release from v0.0.27 through v0.0.55. Previously the changelog only contained the 0.1.0 entry (the first changeset-driven release); all prior release notes lived only on GitHub Releases. Content is taken verbatim from each GitHub Release body, with headings normalized to fit the changelog (drop the redundant 'What's Changed' h2, demote 'Contributors' to h3) and bullets normalized to '-'. * docs(changelog): use h4 subgroups in 0.0.54 for consistency The 0.0.54 entry used bold (**App Router**) pseudo-headings under Bug Fixes, while every other release uses #### h4 sub-headings. Convert them to h4 to match the rest of the changelog. |
||
|
|
2b2ed00f22 |
chore: version packages (#1759)
* chore: version packages * add header --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: James <james@eli.cx>@vinext/cloudflare@0.1.0 vinext@0.1.0 |
||
|
|
a74f833a10 |
feat(deploy): honor Worker-entry cache setters for ISR deploys (#1821)
Older apps that wired a cache backend imperatively in their Worker entry (setCacheHandler / setDataCacheHandler / setCdnCacheAdapter) were blocked on deploy because the ISR cache-adapter check only inspected the Vite config. Add workerEntryHasCacheHandler() so those backwards-compatible entries are still considered configured. Also drop the cdn adapter suggestion from formatMissingCacheAdapterError; the message now points only to the kvDataAdapter() data cache. |
||
|
|
6f689c7b14 | chore: rebuild lockfile (#1815) | ||
|
|
cda19e1c58 |
fix(app-router): preserve recent segment state with Activity BFCache (#1739)
* fix(app-router): preserve recent segment state with Activity BFCache Fresh App Router navigations were reusing the active React tree when moving between dynamic segment values. That leaked client state into a newly entered segment and made the upstream back-forward-cache suite fail before it even reached browser traversal assertions. The root cause was that vinext had history-entry bfcacheId plumbing but no separate segment state-key cache. Next.js treats those identities separately: render state is keyed by segment identity/pathname, while useRouter().bfcacheId identifies a concrete history entry. Add state-key maps for app router slots, render retained entries through React.Activity when cacheComponents is enabled, keep the active one-entry keyed reset path otherwise, and port focused Next.js regression coverage for the three-entry BFCache behavior. * fix(bfcache): store stateKeyMap in retained slot entries and re-provide on render Review feedback requested two changes: 1. Add stateKeyMap to BfcacheSlotEntry so each retained BFCache entry carries the state-key map that belonged to it when active, rather than borrowing the current navigation's map. This prevents nested hidden entries from being rendered with old elements but new pathname-derived state keys — an identity corruption bug that upstream Next.js avoids by rendering each retained entry from its own state. 2. Add a nested dynamic regression test: /nested/[section]/item/[id] with client counters in both the [section] layout and [id] page. The existing flat /page/[n] tests do not exercise nested retained-entry identity, which is exactly where the missing state-key-map bug lives. Changes: - BfcacheSlotEntry gains stateKeyMap?: Readonly<Record<string, string>> - updateBfcacheSlotEntries accepts and stores an optional stateKeyMap argument - BfcacheSlotBoundary stores the current stateKeyMap alongside elements when caching the active entry - Both the Activity visible/hidden path and the non-cacheComponents single-entry path now wrap rendered entries in BfcacheStateKeyMapContext.Provider using the entry's own stateKeyMap (falling back to current map for forward compatibility) - New e2e test verifies nested layout + page counter preservation across segment navigations * refactor(bfcache): extract shared provider stack into BfcacheEntryProviders BfcacheSlotBoundary rendered the BfcacheStateKeyMapContext / ElementsContext / SegmentContext provider stack in two places: the one-entry keyed reset path and the per-entry Activity retention list. The two stacks were identical except for where the React key sat, so the retained-entry bookkeeping and its render shape were tangled together and harder to audit than needed. Extract BfcacheEntryProviders as a pure presentational helper and render it from both paths. The keyed-reset path keeps its remount semantics: the key now sits on the BfcacheEntryProviders element, so a fresh active stateKey still remounts the whole provider subtree. Hook ordering is unchanged. All hooks stay above the early returns in BfcacheSlotBoundary, so the boundary still satisfies the Rules of Hooks across the activeStateKey undefined-to-defined transition. Covered by the slot BFCache entry-state suite in tests/shims.test.ts. * test(bfcache): isolate cacheComponents BFCache coverage in a dedicated fixture The back/forward-cache e2e suite ran against the shared app-basic fixture with cacheComponents: true. app-basic backs the single app-router Playwright server that every app-router spec uses, so enabling cacheComponents globally changed a core DOM invariant for unrelated tests: inactive route trees stay mounted as hidden Activity DOM. The contamination already leaked a :visible workaround into the interception-modal bfcacheId spec, which had nothing to do with Activity. cacheComponents is an opt-in semantic mode, not default app-router behaviour, so it does not belong on the shared baseline fixture. Move the BFCache routes into a dedicated tests/fixtures/app-bfcache fixture with cacheComponents: true, served by a new app-router-bfcache Playwright project on port 4183, and run the back-forward-cache spec there against the project baseURL. The :visible locators stay in that spec, where duplicate hidden DOM is the expected contract. Remove cacheComponents and the BFCache routes from app-basic so the shared fixture models default behaviour again, and revert the :visible workaround in use-router-bfcache-id.spec.ts now that the modal route no longer retains hidden DOM. Fix the nested item page import while moving it: nested/[section]/item/[id]/ page.tsx imported ../counter, but the counter lives two levels up at nested/[section]/counter.tsx, so it must be ../../counter. Validation: PLAYWRIGHT_PROJECT=app-router-bfcache (5 passed) and the full use-router-bfcache-id spec under app-router (11 passed), plus the slot BFCache unit suite (1172 passed) and vp check (formatting + lint + types clean). * ci: run the app-router-bfcache e2e project in the matrix The dedicated app-router-bfcache Playwright project was added to playwright.config.ts but not to the CI E2E matrix, so the back/forward-cache specs never ran in CI. Add it to the matrix project list so the BFCache suite runs on every push. It uses the default chromium browser, so the existing setup and browser-cache steps apply unchanged. * fix(bfcache): keep persisted slots mounted when cacheComponents is off Without cacheComponents there is no Activity retention, yet BfcacheSlotBoundary still keyed the active entry by its segment stateKey. That stateKey tracks the pathname (createBfcacheSegmentIdentity), so any slot whose identity moves with the URL (shared layouts, interception source slots) was remounted on navigation, discarding client state that survives a normal route change. The baseline router reconciles these slots in place. This regressed seven existing app-router e2e tests once cacheComponents was no longer masking it on the shared fixture: interception source-context and modal preservation in advanced.spec.ts, and layout-persistence.spec.ts dynamic-segment layout counter survival. Fresh-entry reset is driven by userland bfcacheId keying in the fixtures, not by remounting the slot subtree, so the keying was pure overreach in the non-cacheComponents path. Render the single active entry unkeyed when cacheComponents is off so the boundary reconciles in place like the baseline. The keyed Activity retention path is unchanged for cacheComponents. Validation: full app-router project 436 passed / 0 failed (the seven regressions now pass); use-router-bfcache-id 11 passed (fresh-push reset intact); app-bfcache 5 passed (Activity retention intact); shims unit suite 1024 passed. * docs(test): point bfcacheId spec comment at the app-router-bfcache project The header noted that Activity-backed preservation is covered in back-forward-cache.spec.ts, but that spec moved to the dedicated app-bfcache fixture under the app-router-bfcache project. Name the project and path so the cross-reference stays accurate. * docs: align cacheComponents comment with unkeyed active-segment behavior * test(bfcache): assert fresh bfcacheId after Activity-restored return * style: collapse locator chains per oxfmt * refactor(bfcache): split slot entry order from snapshots * fix(bfcache): keep non-cacheComponents slot path baseline * fix(bfcache): use string cacheComponents define * test: remove external fetch from revalidate tag fixture * fix(bfcache): harden Activity state key retention * docs(bfcache): document cacheComponents Activity caveats * fix(bfcache): preserve navigation context for fallback SSR Fallback boundary rendering can outlive the original request-scoped navigation context. That made app SSR fail fast with a missing navigation context when loading-boundary digest routes rendered their HTTP fallback shell. The SSR boundary contract still requires a navigation context for real request renders, but boundary renderers now synthesize the same typed shape from the request URL and matched params when request scope has already been cleared. * refactor(bfcache): normalize pathname in state-key map, document invariant callers Moves hydration-pathname normalization into createBfcacheSegmentStateKeyMap so SSR and client always produce byte-identical Activity keys. Documents the requireNavigationContext guaranteeing callers per review feedback. * fix(bfcache): tolerate malformed state-key pathnames * docs(bfcache): document known Next.js divergence and dead-branch contract * chore(deps): update lockfile for vite 0.1.24 * fix(app-router): preserve encoded delimiters in bfcache keys * docs(bfcache): track router-owned retention follow-up * chore: retrigger ci * fix(app-router): preserve redirect bridge fallback |
||
|
|
1c1744e0ea |
feat(ppr): add PPR fallback-shell render lifecycle tests (#1715)
* feat(ppr): add fallback shell render lifecycle * fix(ppr): address review feedback on PR #1715 - Make await params resolvable during fallback-shell prerendering by keeping the base promise as Promise.resolve(plain) and only throwing the hanging promise on sync fallback-key access. - Defer createPprFallbackShellSuspensePromise to first fallback-key access so hasDynamicBoundary and cache-task ignoring only fire when a fallback param is actually read, matching the suspend only on access contract. - Store and clear pendingAbortCleanup in preparePprFallbackShellFinalRender to avoid leaking uncancellable abort timers. - Add tests for await params resolving known params and for lazy dynamic-boundary marking when only non-fallback params are accessed. * fix(ppr): make awaited params preserve fallback-key suspension and getOwnPropertyDescriptor fully lazy - Promise now resolves to a Proxy (resolvedParams) instead of raw plain. The inner Proxy throws a hanging promise when a fallback param is accessed after await, so `const resolved = await params; resolved.slug` suspends. - Both outer and inner Proxy getOwnPropertyDescriptor traps return a getter that creates the hanging promise lazily. Object.keys(params) no longer triggers side effects merely by enumerating keys. - Added tests for awaited-params suspension and for enumeration not marking hasDynamicBoundary eagerly. * fix(ppr): restructure thenable-params with typed Proxy handlers and conditional observer semantics - Extract createResolvedParamsProxy with ProxyHandler<T> (no cast needed) - Extract createThenableParamsProxy with single as ThenableParams<T> boundary cast - Remove all as unknown as double casts - In fallback-shell mode, Promise continuations no longer observe all keys; let the resolved params proxy report actual property access lazily - Fix getOwnPropertyDescriptor to return throwing getter instead of throwing synchronously, preventing Object.keys from triggering suspension - Restructure awaited-params test to wrap entire async flow in ALS scope - Add test: awaiting params during fallback-shell observes only accessed known params * test(ppr): assert hasDynamicBoundary stays false after known-param access in fallback-shell observer test * fix(ppr): suspend escaped fallback params and settle warmup cache-ready without in-scope task Addresses bonk review on #1715: - Capture the originating fallback-shell state at params construction and create the suspense promise against it (via the new createPprFallbackShellSuspensePromiseForState), instead of re-reading the ALS store lazily. A params object that escapes the shell scope now still suspends on fallback-key access rather than leaking the raw `[slug]` placeholder, and the `get` / `getOwnPropertyDescriptor` traps no longer diverge on the out-of-scope path. - During warmup, always call scheduleCacheReadyIfSettled when a dynamic boundary is hit (not only in the final phase), so a bare headers()/cookies() access with an empty cache-task stack still re-schedules the warmup cache-ready settle. The call is a no-op while cache tasks are pending. Adds focused tests for both paths. * fix(ppr): keep cache-task counter and fallback suspension correct across phase transition Addresses follow-up bonk review on #1715: - Add a per-state cache epoch, bumped on the warmup->final transition. Cache tasks capture the epoch at creation; a task that settles in a later epoch no longer decrements the freshly-reset pendingCacheTasks counter below zero (which would permanently block resolveCacheReadyIfSettled). - Re-derive the memoized fallback-shell hanging promise when the live AbortController changes (preparePprFallbackShellFinalRender swaps it on the phase transition), so suspension always tracks the controller the lifecycle will actually abort instead of a stale warmup signal. Adds tests for the late warmup-task settle and the post-transition re-derive. --------- Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
7d280edfbe |
refactor(server): replace prerender HTML buffering with metadataReady contract (#1809)
* refactor(server): replace prerender HTML buffering with metadataReady contract * fix(server): wait for allReady before piping through tick-buffered transform when waitForAllReady is true * refactor(server): clean up AppSsrRenderResult normalization and metadataReady promise resolution * test(server): cover ISR revalidation waitForAllReady; address review nits - Add focused test asserting stale HTML cache regeneration passes waitForAllReady: true to handleSsr so Suspense fallbacks never leak into the cache entry - Document why metadataReady resolves eagerly (allReady is already awaited for the prerender path) in app-ssr-entry.ts - Remove stray blank line left by the readStreamAsText import removal - Type capturedOnError in the prerender special-error test instead of any * fix(pages): export PagesPageModule type consumed by pages-page-handler The dts bundle build (vp pack) failed because pages-page-handler.ts imports PagesPageModule from pages-page-data.js, but the type was not exported, so rolldown-plugin-dts could not resolve it. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
d9a5e63c32 |
refactor(pages): extract render orchestrator from codegen string (#1784) (#1805)
* refactor(pages): extract render orchestrator from codegen string (#1784) Move `_renderPage` (~260 lines) and `collectAssetTags` (~85 lines) out of the generated template string in `entries/pages-server-entry.ts` into typed, unit-testable server modules: - `server/pages-page-handler.ts` — `createPagesPageHandler` factory that owns the full Pages Router render lifecycle (i18n, route matching, 404/500 fallback recursion, _next/data envelope, 405 method check, ISR cache-key derivation, SSR-context setup). Mirrors how `server/app-rsc-handler.ts` is structured relative to `entries/app-rsc-entry.ts`. - `server/pages-asset-tags.ts` — `collectAssetTags` + manifest helpers, extracted from the template so they can be unit-tested directly. - `server/pages-data-route.ts` — adds `normalizePagesDataRequest` so both `renderPage` and `runMiddleware` share one implementation instead of duplicating the buildId-check logic in the template string. `entries/pages-server-entry.ts` is now thin wiring: route-table codegen, embedded config/i18n/buildId, and closures that delegate to the handler. `renderPage` keeps its exact exported signature; all callers are unaffected. No behavior change — pure code motion. Fixed a latent bug where the `isrCacheKey` wrapper in the handler was missing the `buildId` argument on non-i18n paths (causing a cache-key mismatch); caught by the existing ISR CSP-nonce test. * refactor(pages): tighten handler types and improve tests - Use ComponentType from react for AppComponent, DocumentComponent, and PageComponent closures instead of unknown — matches pages-page-response.ts which already uses ComponentType. Eliminates the DocumentComponent cast. - Widen isrCacheKey router param from literal union to string — the literal union forced as-casts everywhere it was used indirectly. All call sites pass 'pages' or 'app' string literals so the widening is safe. - Replace the shallow i18n redirect test (which just checked a route miss gave 404) with a test that actually exercises the 307 short-circuit: visiting / with Accept-Language: fr triggers resolvePagesI18nRequest to return redirectUrl and the handler returns 307 to /fr. - Add second i18n test: locale prefix already present → no redirect. - Tighten 'allows POST with gSSP' test: assert no Allow header (confirms the 405 guard was bypassed, not just that some non-405 came back). |
||
|
|
340da2dd19 |
refactor(server): extract pages fallback bridge into typed helper (#1808)
* refactor(server): extract pages fallback bridge into typed helper * test(app-router): update pages fallback codegen assertion * refactor(server): tighten buildRequestHeaders type to Headers | null The injected buildRequestHeaders dependency wires in buildRequestHeadersFromMiddlewareResponse, which returns Headers | null, but the dependency type declared a non-nullable Headers return. Tighten the type to match reality and add a test exercising the null path where the original request is forwarded unchanged. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
465e6c76c1 |
fix(build): share one RSC compatibility ID across all plugin instances (#1814)
Follow-up to #1810. createRscCompatibilityId() falls back to a fresh randomUUID() per plugin instance when no deploymentId is pinned, so a hybrid app+pages build baked two different RSC-compat tokens — the App Router buildApp() instance and the separate Pages Router vite.build() instance each minted their own. The token gates client-side RSC payload acceptance (the X-Vinext-RSC-Compatibility-Id header), so cross-instance divergence is a latent deploy-skew hazard. Coordinate it the same way as the build ID: relocate createRscCompatibilityId to config/next-config.ts (single source of truth, beside resolveBuildId / resolveDeploymentId), have the CLI resolve it once and publish it via __VINEXT_SHARED_RSC_COMPATIBILITY_ID, and have the plugin always adopt it when set. Reuses deploymentId when configured (already stable). The env var is only ever set by the build CLI, so dev and standalone resolution are unchanged. Adds a coordination test asserting the shared token lands in both the server and client output. |
||
|
|
9bab92f012 |
test(deploy): cover generated worker package exports (#1807)
Generated worker entries can import vinext subpaths that must resolve from the published package. Missing package exports break consumers even when the generated template itself looks correct. Add deploy coverage that extracts vinext subpath imports from generated App and Pages worker entries and verifies each one is covered by packages/vinext/package.json exports. Tighten the prefer-shared-utils lint rule so copied shared helpers in repo tests are reported too. Test files mask template literal fixture payloads, while source files still scan generated-source templates. |
||
|
|
aab9240e10 |
fix(app-router): hard navigate streamed redirects (#1742)
* fix(app-router): hard navigate streamed redirects * fix(navigation): guard page redirect marker checks * test(app-router): count redirect document loads |
||
|
|
e7757e48b7 | ci(bigbonk): reduce job timeout to 10 minutes (#1813) | ||
|
|
9a721ca287 |
chore(deps): bump knip and node types (#1806)
* chore(deps): bump knip and node types Bump Knip to 6.16.0 and @types/node to 25.9.2. Knip 6.16 reports previously unreported unused exported types, stale ignore entries, and external CLI binaries. Remove unnecessary type exports, refine the script entry coverage, and keep only narrow binary ignores for system or user-project commands. Validation: vp run knip; vp run check. * chore: retrigger ci |
||
|
|
cc246f5db6 |
fix(build): write BUILD_ID via writeBundle so App Router builds emit it (#1810)
* fix(build): write BUILD_ID via writeBundle so App Router builds emit it The vinext:build-id plugin used closeBundle, which does not fire during the multi-environment createBuilder().buildApp() pipeline used for App Router production builds. As a result dist/server/BUILD_ID was silently never written for pure App Router apps — only hybrid apps (which run a second vite.build() pass) and Pages Router apps got the file. This was masked until server bundles started being minified by default (#1777): the e2e deploy harness (scripts/e2e-deploy.sh) falls back to regex-parsing the buildId out of dist/server/index.js when BUILD_ID is absent, and minification mangles the `get buildId()` / `buildId = "..."` patterns the regex relies on. The result was ~700 App Router deploy-suite tests failing at setup with "Failed to extract build ID from dist/server/index.js". Switch the plugin to writeBundle (mirroring the working vinext:image-config plugin), which fires for every emitted bundle; the existing one-time write guard ensures the file is written exactly once. Add a regression assertion to the App Router production build test. * fix(build): share one build ID across all plugin instances in a build A single `vinext build` can instantiate vinext() more than once: the App Router multi-environment build (createBuilder().buildApp()) and the separate Pages Router SSR build for hybrid app+pages apps are distinct plugin instances. With no user generateBuildId, each instance resolved its own random UUID, so the App Router runtime, the Pages Router runtime, the prerender manifest, and dist/server/BUILD_ID could each get a different build ID — risking ISR/seed-cache key mismatches for hybrid apps. The CLI now resolves the build ID once (honoring the user's generateBuildId) and publishes it via __VINEXT_SHARED_BUILD_ID. The plugin adopts it unless the user supplied their own generateBuildId (which is authoritative and already shared because every instance calls it). resolveBuildId()'s standalone semantics are unchanged, so the build-only coordination never leaks into dev or tests. Verified on the hybrid app-router-cloudflare example: the App Router RSC/SSR runtime, Pages entry, prerender manifest, and BUILD_ID file now all carry the same build ID. Adds a coordination test alongside the BUILD_ID emission guard. * fix(build): always adopt shared build ID, even with generateBuildId Addresses review on #1810. The previous `!rawConfig?.generateBuildId` guard reintroduced the divergence it was meant to fix: when generateBuildId returns null (documented Next.js behavior) or is non-deterministic, resolveBuildId() mints a fresh random UUID per plugin instance, so a hybrid app+pages build still got divergent IDs across the buildApp() and Pages vite.build() instances. The CLI's resolvedNextConfig.buildId is already the fully-resolved authoritative value (it ran resolveBuildId honoring generateBuildId, including the null→UUID fallback), so the plugin now always adopts the shared ID when set. Add a regression test using generateBuildId: () => null + a shared ID, and document that the CLI intentionally does not clear the env var (the build process exits). |
||
|
|
fe731b4827 |
test(e2e): mark flaky dev-overlay build-error HMR test as fixme (#1812)
The app-router dev error overlay test "server component HMR surfaces Vite build errors after a clean load" is flaky in CI: after writing the clean file back, the build-error overlay intermittently stays visible past the 10s timeout. Mark it test.fixme to unblock CI while the recovery path is investigated. Tracked in #1811. |
||
|
|
5db85cb6eb |
perf(og): dedupe resvg/yoga wasm in server bundle (#1801)
* fix(og): dedupe resvg/yoga wasm in server bundle
vinext shipped each @vercel/og WASM module twice — once per loader
strategy — even though only one strategy runs at runtime, wasting
~1.4 MiB per build.
resvg.wasm was emitted as a hashed asset by the `import("./resvg.wasm?module")`
path (used by workerd) AND copied as a second byte-identical root file
purely for the Node.js disk-read fallback. yoga.wasm was emitted as a
hashed asset AND inlined as a ~95 KiB base64 blob in index.edge for the
Node.js fallback.
This change makes each WASM ship exactly once, with every loader path
resolving to the single emitted file:
- og-font-patch (index.ts): drop the yoga base64 preamble and switch the
Node.js yoga fallback to a disk read (node:fs + new URL), mirroring resvg.
- og-assets.ts: in generateBundle, when the bundler already emitted the
WASM asset, rewrite the `new URL("./x.wasm", import.meta.url)` fallback
reference to point at the emitted sibling asset; only copy a root file in
writeBundle when no asset was emitted.
The dedup decision keys off whether the bundler emitted the asset, never
the deploy target, so it benefits Node/self-hosted builds too. No platform
detection.
* fix(og): keep chunk sourcemap in sync on wasm fallback rewrite
Use magic-string (already a dependency) for the generateBundle fallback-URL
rewrite so the chunk's sourcemap stays valid after the edit, instead of a
blind string replace that invalidated chunk.map.
Also extract copyMissingOgWasm() as a dependency-injected helper and make the
copy-fallback test hermetic: yoga.wasm is not shipped by @vercel/og (it is
synthesized from base64 by the og-font-patch transform), so the prior test
only passed when a previous build had left yoga.wasm in node_modules.
* test(og): cover cross-directory fallback rewrite; document cross-hook state
Address bonk review observations: add a test asserting the path.posix.relative
rewrite produces a directory-relative ref when the chunk and emitted asset live
in different dirs (the main behavioral win), and document the generateBundle->
writeBundle dedupedBases dependency.
|
||
|
|
ddc529af5e |
docs(agents): forbid installing with --no-frozen-lockfile (#1802)
* docs(agents): forbid installing with --no-frozen-lockfile * docs(agents): clarify --no-frozen-lockfile rule rationale and ask user * docs(agents): correct --no-frozen-lockfile guidance |
||
|
|
dab28c4b0f |
fix(app-router): refetch same-page search navigations (#1744)
* fix(app-router): refetch same-page search navigations Same-page App Router navigations with a changed search string could replay a prefetched RSC payload instead of requesting the target payload. That diverges from Next.js, which refetches page segments for same-page navigations because search params are page inputs. Bypass visited and prefetched RSC caches for navigate operations whose pathname is unchanged but search params differ. Add a prerendered production browser regression that ports the Next.js hash navigation request assertion. * fix(app-router): hide rsc cache query from middleware * test(app-router): mirror upstream hash scroll offsets * fix(app-router): address review feedback on rsc query navigation - Reuse fixture-scoped node_modules linking in the hash RSC spec instead of symlinking repo-root node_modules, matching sibling production specs. - Add an explicit assertion that no request leaked `_rsc` to the fixture middleware (HTTP 599) for a clear failure mode. - Document that the optimistic shell is intentionally not gated by shouldBypassNavigationCache (detached commit, always superseded). - Detect the RSC cache-busting param via the encoding-aware stripper so the guard and stripper can never diverge on encoded-key edge cases. * fix(app-router): share encoding-aware matcher for _rsc guard Replace the before/after url.search comparison in requestWithoutRscCacheBustingSearchParam with a dedicated hasRscCacheBustingSearchParam helper that reuses the stripper's isRscCacheBustingSearchPair matcher. This makes the guard and stripper share a single source of truth and avoids spuriously rebuilding/normalizing requests whose only difference is degenerate empty query pairs (e.g. ?a=1&&b=2). * fix(app-router): preserve cf metadata when hiding _rsc from middleware requestWithoutRscCacheBustingSearchParam reconstructed the userland request with a bare new Request(url, source), which drops the Workers-specific cf property and omits duplex: "half" for streaming bodies (throwing on Node/undici). Add a cloneRequestWithUrl helper mirroring cloneRequestWithHeaders that overrides the URL while preserving headers/cf and setting duplex, and route the _rsc strip through it. * fix(app-router): stop _rsc leaking to external rewrite proxies The beforeFiles/afterFiles/fallback applyRewrite calls forwarded the original request (with _rsc) to proxyExternalRequest, which reconstructs the upstream URL from request.url and merges its query params — leaking the internal RSC transport query to external rewrite destinations. Pass the _rsc-stripped userlandRequest to all three calls, consistent with the middleware invariant. Add a regression test that points an external rewrite at a loopback server and asserts the forwarded URL drops _rsc while preserving userland query params. * test(app-router): wait for idle network before tracking RSC requests The query-param link is in the viewport and gets prefetched on load. On slower runtimes (WebKit), that prefetch could land after rscRequestUrls.clear(), so it was misattributed to the hash-only navigations and tripped the "no with-query-param request" assertion. Wait for networkidle before clearing, mirroring upstream's waitForIdleNetwork() in the ported Next.js test. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
d3870efe57 |
refactor(cache): deprecate imperative cache handler setters and exports (#1800)
Steer consumers toward the declarative `cache` option on the vinext() plugin (with the @vinext/cloudflare builders kvDataAdapter/cdnAdapter) instead of imperatively wiring cache handlers from a worker entry. - Mark setDataCacheHandler, setCacheHandler, and setCdnCacheAdapter as @deprecated with config-based migration examples. They remain functional as the internal registration target used by virtual:vinext-cache-adapters. - Mark the manually-instantiated handler exports MemoryCacheHandler and KVCacheHandler as @deprecated for consumers. - Update the next/cache type declarations in next-shims.d.ts to match. - Fix stale AGENTS.md claim: the default data cache is in-memory (MemoryCacheHandler) in all runtimes; KV is opt-in, not the default. |