mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.0
1132 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4342311377 |
test(app-router): lock navigation lifecycle settlement and root-layout hard navigation behaviour (#1053)
* test(app-router): lock navigation lifecycle settlement and root-layout hard navigation behaviour Adds 13 unit tests covering two Wave 0 baseline areas from issue #726: BASE-02 — Lock current lifecycle settlement behaviour (9 tests): - Three concurrent navigations with reverse-order payload resolution - Rapid sequential navigation: stale deferred payload does not overwrite - Stale cross-root navigation is skipped instead of hard-navigating - resolvePendingNavigationCommitDisposition exhausts skip/dispatch/hard-navigate - resolveAndClassifyNavigationCommit classifies skip when IDs diverge - Failed RSC payload settles pending router state cleanly BASE-03 — Lock root-layout hard navigation behaviour (4 tests): - renderNavigationPayload calls window.location.assign on root change - Hard-navigate settles the pending router state before navigating away - shouldHardNavigate negative/null path exhaustiveness - shouldHardNavigate positive path with distinct roots These tests close the App Router concurrency gap: the Pages Router has 10 race-condition tests in shims.test.ts but the App Router (which uses a completely different lifecycle mechanism) had zero equivalent tests for out-of-order payload resolution and stale commit rejection. * test: remove hallucinatory comments from app-browser-entry tests * test: remove duplicate shouldHardNavigate null-root assertion * test: remove low-signal helper truth-table tests, fix hard-navigate ordering assertion --------- Co-authored-by: James <james@eli.cx> |
||
|
|
d6c270f6b8 |
fix: filter internal Next.js headers from inbound requests (#1056)
* fix: filter internal Next.js headers from inbound requests Next.js strips internal headers (x-nextjs-data, x-matched-path, x-now-route-matches, x-next-resume-state-length, and x-middleware-*) via filterInternalHeaders() at the router-server entry point before any handler or middleware sees the request. This prevents attackers from forging these headers to influence routing or impersonate internal data fetches. vinext was not filtering any of these headers from inbound requests, so all four reached middleware as user-controlled request headers. Ported from Next.js INTERNAL_HEADERS list and filterInternalHeaders(): - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/server-ipc/utils.ts - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-server.ts Added filterInternalHeaders() to server/request-pipeline.ts and wired it into all request entry points: - App Router handler (app-rsc-handler.ts) - CF Worker App Router entry (app-router-entry.ts) - Node prod-server (nodeToWebRequest + Pages Router path) - Dev server Pages Router middleware path (index.ts) - Generated Pages Router worker entry (deploy.ts) Filtering runs before middleware, config matching, and routing so no subsystem sees forged internal headers. The x-middleware-* headers are stripped from requests but NOT from middleware responses — the middleware protocol (which reads x-middleware-* from Response headers, not request headers) continues to work correctly. * fix: use safe Request rebuild instead of mutating headers directly Request.headers is immutable in Cloudflare Workers / Miniflare / workerd environments. The previous in-place mutation via headers.delete() threw 'Can't modify immutable headers', causing every request to 500 in the cloudflare-dev E2E suite. Changed filterInternalHeaders() to always return a new Headers copy (never mutates the input). Call sites that use request.headers now rebuild the Request with the filtered headers, matching the existing pattern in applyMiddlewareRequestHeaders() at config-matchers.ts:495-502. * fix: clone requests when filtering headers * fix: clone Requests safely across runtimes * fix: avoid type assertions in Request cf access * fix: move header filtering from handler body to entry boundary handleAppRscRequest() is an internal handoff point, not a trust boundary. Filtering here broke the x-vinext-mw-ctx handoff in hybrid app+pages dev mode because cloned Request objects could drop coordination state from the connect handler, causing middleware to run twice. Moved filtering to the top of createAppRscHandler(), the true App Router request boundary, before headersContextFromRequest() and requestContextFromRequest() capture any context. This also fixes the security gap where forged headers were captured in the request context before filtering ran. * fix: preserve x-vinext-mw-ctx when filtering internal headers in app-rsc-handler srvx's NodeRequestHeaders reads from rawHeaders for iteration but falls back to req.headers for .get() / .has(). In the dev server we add x-vinext-mw-ctx to req.headers after the Request is built, so it is visible to .get() but lost when filterInternalHeaders iterates. Read it BEFORE iterating so applyForwardedMiddlewareContext can skip middleware and avoid double execution. Fixes CI failure on PR #1056. * fix: copy cf in both clone paths, add cloneRequestWithHeaders tests cloneRequestWithHeaders now copies the Workers cf property in both the primary (try) path and the fallback path. Added 8 unit tests locking down method, URL, redirect, signal, body, cf, and combined metadata preservation to prevent regression to the broken URL-constructor pattern. |
||
|
|
0478e4fc51 |
refactor(config): dedupe regex cache and destination param chain (#1080)
Extract the three internal regex compilation caches in config-matchers.ts to a single getCachedRegex helper, and unify the substituteDestinationParams + sanitizeDestination chain used by redirects and rewrites. Follow-up to #1058. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d2da43b42f |
refactor(routing): dedupe findFileWithExtensions helper (#1076)
* refactor(routing): dedupe findFileWithExtensions helper Extract the matcher.dottedExtensions.some(existsSync) one-liner used by both prerender.ts and dev-server.ts into routing/file-matcher.ts. Follow-up to #1058. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(build): drop stale ValidFileMatcher type import The type is no longer used after delegating to the shared findFileWithExtensions helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
604d548457 |
refactor(als): dedupe AsyncLocalStorage globalThis registration (#1081)
Extract the `Symbol.for(...) + globalThis ??= new AsyncLocalStorage()` pattern from headers.ts, cache.ts, cache-runtime.ts, fetch-cache.ts, request-context.ts, unified-request-context.ts, and the four *-state.ts shims into a single helper at shims/internal/als-registry.ts. Cross-bundle singleton property is preserved: the helper still routes through `Symbol.for(key)` (global symbol registry) and `globalThis[sym] ??= ...`, so multiple instances of the helper module — and multiple instances of each call site — all converge on the same ALS for a given key. The helper holds no module-local reference of its own. Follow-up to #1058. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b9769cc530 |
refactor(entries): drop dead locale helpers and dedupe path normalization (#1079)
- Delete unused extractLocale / detectLocaleFromHeaders / parseCookieLocaleFromHeader (the canonical implementations live in server/pages-i18n.ts and resolvePagesI18nRequest is what the entry actually calls). - Extract the repeated `.replace(/\\/g, "/")` path-separator normalization across the entry files into a shared helper. Follow-up to #1058 and #1069. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3e00dc8953 |
refactor(server): dedupe action/page/route-handler error response building (#1078)
Extract the "Payload Too Large" 413 response and NEXT_REDIRECT / NEXT_NOT_FOUND / NEXT_HTTP_ERROR_FALLBACK digest parsing into shared helpers. Follow-up to #1058. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
874ebef1bd |
refactor(check): dedupe status sort and App Router file filters (#1075)
Extract the repeated status-order comparator and the parallel page/layout/loading/error file-type predicates in check.ts into shared helpers. Follow-up to #1058. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
05933cb1c8 |
refactor(build): dedupe JSON file safe-read pattern (#1077)
Extract the repeated `try { JSON.parse(readFileSync(...)) } catch`
pattern from standalone.ts and server-manifest.ts into a shared
readJsonFile helper. Follow-up to #1058.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
64fea36aac |
refactor(server): dedupe .rsc suffix stripping (#1073)
Extract the repeated `pathname.replace(/\.rsc$/, "")` and equivalent slice-based suffix removal into a single `stripRscSuffix` helper. Follow-up to #1058. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
30775c8cfa |
refactor(plugins): dedupe og-asset read-to-base64 wrapper (#1074)
Extract the repeated try/catch file-read + base64 encode pattern in og-assets.ts into a shared helper. Follow-up to #1058. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
634808bea4 |
perf(pages): cache ISR misses from streamed render (#1068)
* perf(pages): cache ISR misses from streamed render Pages Router ISR cache misses currently render the page once for the live response and then render a second React tree to produce cache HTML. That doubles userland render, data reads, suspense work, and render-scoped side effects on every cold cache fill. The cache writer now tees the already-rendered body stream, records the cache branch with streaming UTF-8 decoding, and writes the same shell and body that the client receives. Stream failures report through the Pages Router render error path and never persist partial HTML. Tests cover the single-render cache fill, split UTF-8 chunks, failed stream writes, and the Pages Router fixture state contract. * fix(pages): keep ISR cache error reporting non-fatal * chore: rerun CI |
||
|
|
a66a62da4c |
perf(app-router): coalesce client reference preloads (#1067)
Cold App Router SSR requests currently share Vite RSC's underlying module import promises, but each request can still iterate the full client-reference manifest and allocate its own Promise.all fan-out before the completed boolean flips. That keeps first-request correctness but wastes work during cold bursts. The preload state only tracked completion, not in-flight work. Concurrent callers had no shared promise to await, and the preload logic lived inline in the SSR entry where the concurrency contract was hard to test. Add a typed client-reference preloader that shares the global manifest preload promise, dedupes per-reference work for future scoped manifests, preserves non-fatal reference load reporting, and keeps handleSsr awaiting the preload before React consumes the Flight stream. |
||
|
|
5a18d36bfd |
feat(app-router): add navigation trace reason-code shell (#1063)
* feat(app-router): add navigation trace reason-code shell Navigation commit classification can now expose compact reason-code traces for the existing stale, root-boundary, unknown-root, and dispatch outcomes. The plain disposition-only path remains allocation-light for the browser navigation hot path. The old implicit string result made it hard for follow-up lifecycle work in #726 to attach structured operational context without inventing narrative logs at each call site. This adds a narrow trace contract and threads it through the same-URL classification helper while preserving current commit behavior. Covered by app-browser-entry tests for stale commit skips, root-boundary hard navigation, unknown root-layout fallback, same-URL classification traces, and trace field ownership. * fix(app-router): tighten navigation trace shell The OPS-01 trace shell exposed an append helper before any source path needed multi-entry traces, typed reason-code values too loosely, and let the combined same-URL classifier allocate a trace that the controller never consumed. Remove the unused append helper, derive reason-code unions from a literal key-to-code map, keep the classifier on the allocation-light disposition path, and add the missing normal dispatch trace coverage. |
||
|
|
61a4551bee |
refactor(shims): dedupe cookie value serialization (#1072)
* refactor(shims): dedupe cookie value serialization Extract the shared encodeURIComponent-based value encoding from headers.ts and server.ts ResponseCookies into a single helper. Follow-up to #1049. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(shims): drop unused export of SerializeSetCookieOptions knip flagged this as an unused export — the type is only consumed within cookie-serialize.ts itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bdc1f34c35 |
refactor(server): dedupe forbidden response construction (#1071)
Extract the repeated `new Response("Forbidden", { status: 403, ... })`
pattern from dev-origin-check.ts and request-pipeline.ts into a single
shared helper. Follow-up to #1049.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
728b8e3b76 |
refactor(i18n): dedupe NEXT_LOCALE cookie parsing (#1069)
Remove the dead `parseCookieLocaleFromHeader` helper from the Pages SSR entry template. It duplicated the regex + decodeURIComponent pattern already exported from `pages-i18n.ts` (used by `resolvePagesI18nRequest`) and had no callers in the generated server entry — locale resolution goes through `resolvePagesI18nRequest` which handles the cookie itself. Follow-up to #1049. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9997ee7200 |
refactor(app-browser-entry): dedupe encoded JSON header parsing (#1070)
Extract the repeated try/catch JSON.parse(decodeURIComponent(...)) block into a single helper. Follow-up to #1049. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c1a4c8da1a |
fix(routing): accept any non-] chars in dynamic segment names (Next.js parity) (#1054)
Next.js PARAMETER_PATTERN accepts any non-] characters inside brackets: [repo:name], [variant.id], [user@domain] vinext was restricting param names to [\w-]+, silently treating segments with dots, colons, or @ as literal URLs. This fix aligns the regexes with Next.js: - app-route-graph.ts: ^\[([^\]]+)\]$ (was [\w-]+) - pages-router.ts: same - route-validation.ts: patternToNextFormat now backtracks over +/* suffixes - shims/router.ts: extractRouteParamNames now accepts dotted/colon names Also corrects a misleading comment in routePrecedence: dynamic routes CAN score negative, but the trie matcher (route-trie.ts) always checks static children before dynamic children at each node, so purely-static routes still win at request time. Next.js reference: https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-dynamic-param.ts#L175 Closes #7 |
||
|
|
e216dab483 |
refactor(app-router): introduce AppElementsWire boundary (#1059)
* refactor(app-router): introduce AppElementsWire boundary App Router payload ownership currently leaks through raw helper calls for route ids, cache keys, metadata writes, and decode paths. That makes the flat AppElements transport act like router semantics instead of a narrow wire boundary, which violates the #726-WIRE-01 ownership target. Introduce AppElementsWire as the single codec facade for AppElements wire encoding, decoding, metadata creation, and outgoing payload shaping. Route the current production encode/decode/write call sites through that facade while keeping the legacy helper names re-exported only where existing tests cover compatibility. * chore(app-router): address AppElementsWire review |
||
|
|
834ccc6ce7 |
feat(app-router): mint semantic route graph ids (#1060)
App Router route graph entries previously left route, page, layout, template, and slot identity to later render and wire layers. That kept topology meaning coupled to reconstructed payload keys instead of a graph-owned fact. Mint stable semantic IDs during graph construction, preserve them in generated RSC route metadata, and cover route-group, dynamic-segment, slot-owner, and filesystem-root stability cases. |
||
|
|
57e6b7001e |
feat(app-router): track visible commit versions for browser operations (#1061)
Browser router state changed without a versioned visible commit baseline or a typed operation record, leaving later stale-result gates without a stable comparison point. Add pending and committed OperationRecord metadata, thread explicit operation lanes through browser navigation, refresh, traverse, same-URL server actions, and HMR, and advance visibleCommitVersion through the reducer path that accepts a new visible router-state commit. Also harden the prefetch-cache unit test to wait for async router.prefetch setup rather than relying on a fixed scheduler tick. |
||
|
|
2bd7377f70 |
fix(shims): match Next metadata merge and Twitter inheritance (#1055)
* fix(shims): deep-merge nested metadata and add OG/Twitter inheritance
mergeMetadataEntries did a flat top-level key replacement, so setting
openGraph: { title: "Override" } in a child layout erased all root
OpenGraph fields like siteName and images. Next.js resolves these
per-subkey; we now shallow-merge the nested objects for openGraph,
twitter, icons, alternates, robots, and other.
Setting only openGraph.title without twitter.title produced no Twitter
Card tags because vinext skipped Next.js's postProcessMetadata step.
We now auto-fill twitter:title from og:title (or metadata:title),
twitter:description from og:description (or metadata:description),
and twitter:images from og:images, and default twitter:card to
summary_large_image when images are present.
Ported from Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolve-metadata.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts
* fixup! fix(shims): deep-merge nested metadata and add OG/Twitter inheritance
Extract postProcessMetadata from mergeMetadataEntries to avoid
polluting intermediate merge results during layout accumulation.
Run it once at the end of resolveAppPageHead after file-based
metadata has been applied, matching Next.js's accumulator pattern.
Also fix two issues caught by Copilot review and CI:
1. Twitter auto-fill now works even when openGraph is absent —
metadata.title and metadata.description serve as fallback sources.
2. Remove all as assertions from production code; use typed locals
and direct index access via Metadata's string index signature.
3. Update app-page-head.test.ts expectations to match post-processed
output and add missing regression test for no-openGraph case.
* chore: address review feedback — clone input, ?? for title, missing test
- postProcessMetadata now shallow-clones its input to prevent accidental
mutation of the caller's object.
- resolveStringTitle uses ?? instead of || to avoid treating empty
string as falsy (semantically correct but practically no difference).
- Added comment explaining why verification, appleWebApp, appLinks,
and formatDetection are excluded from DEEP_MERGE_KEYS.
- Added test: child can clear a parent openGraph subkey by setting it
to undefined, verifying twitter auto-fill skips cleared images.
* fix(shims): match Next metadata segment replacement
Nested metadata objects were being merged across segments, which kept stale parent OpenGraph, Twitter, alternates, icons, and robots fields when a child segment defined the same key.
Next only merges custom other metadata across segments. Replace the broad nested merge with an explicit other merge, keep OG-to-Twitter post-processing gated by openGraph, and update metadata tests to lock in the compatibility contract.
|
||
|
|
68c90808e0 |
fix(routing): discover nested parallel slot sub-routes from layout-only parents (#1051)
* fix(routing): discover nested parallel slot sub-routes from layout-only parents Nested parallel routes failed when the parent segment had a layout but no page.tsx. discoverSlotSubRoutes skipped any parent route without pagePath, so slots like @parallelB/nested/page.tsx under home/layout.tsx (but no home/page.tsx) were never discovered. Next.js explicitly tests this shape in parallel-routes-and-interception: app/parallel-nested/home/layout.tsx + @parallelB/default.tsx + @parallelB/nested/page.tsx creates a valid /parallel-nested/home/nested route. The fix makes three targeted changes to discoverSlotSubRoutes: 1. Remove the hard pagePath requirement. Layout-only routes with parallel slots can still own nested slot sub-routes. 2. Compute parentPageDir from the innermost layout when pagePath is null, so slot ownership checks still work for layout-only routes. 3. Relax the children default.tsx requirement, but only for layout-only parent routes. When a parent HAS a children page, default.tsx is still required as a fallback for the synthetic sub-route. Layout-only parents do not need this fallback because the children slot was never occupied. 4. Add structural-conflict detection before creating synthetic routes. This prevents layout-only root routes from generating synthetic routes that collide with existing page routes differing only by param name (e.g. /shop/:id vs /shop/:name), which validateRoutePatterns rejects. Tests ported from the Next.js parallel-routes-and-interception suite cover both route-discovery and end-to-end rendering. Refs: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts * fix(routing): correct patternsStructurallyEquivalent tree-node comparison The initial implementation of patternsStructurallyEquivalent used a naive dynamic-segment check (startsWith(':')) that produced two categories of false positives: 1. Literal vs dynamic segments (e.g. 'about' vs ':id') were considered equivalent, incorrectly suppressing valid synthetic routes. 2. Single dynamic vs catch-all segments (e.g. ':id' vs ':slug+') were considered equivalent, even though they map to different validator tree nodes ([] vs [...]). Replace the ad-hoc boolean logic with segmentTreeNodeType, which classifies each segment by its Next.js validator tree node (literal, dynamic, catchAll, optionalCatchAll). Two patterns are structurally equivalent only when every segment maps to the same tree node type. Also add a clarifying comment to the parentPageDir computation so the next developer understands why the innermost layout proxies the route directory for layout-only routes. * fix(routing): scan synthetic routes in structural conflict check Copilot review identified that patternsStructurallyEquivalent only scanned the original routes array, missing conflicts between synthetic routes created earlier in the same loop. Fixed by scanning routesByPattern.values() which includes both original and synthetic routes. Also adds a TODO comment documenting the known prerender limitation for layout-only synthetic routes (skipped at build/prerender.ts:849). These routes work in dev/SSR but won't be statically exported — this is pre-existing behaviour for all layout-only routes. * test(routing): cover structural conflict skip for synthetic parallel routes Adds a unit test proving that discoverSlotSubRoutes skips synthetic routes when an existing page route has the same structural shape but a different param name (e.g. /shop/:id page + @feed/[name]/page.tsx slot). Without this coverage the structural-conflict safeguard is easy to regress. * fix(routing): guard discoverSlotSubRoutes to layout-only UI routes Review identified that removing the old !parentRoute.pagePath guard also let route-handler-only AppRoutes enter discoverSlotSubRoutes. Route handlers have pagePath: null but are not layout-only UI routes. A shape like app/api/route.ts + app/@feed/foo/page.tsx could make the /api route handler scan the ancestor slot and materialise a synthetic UI route under /api/foo. Add an explicit isLayoutOnlyUiRoute check: !parentRoute.pagePath && !parentRoute.routePath && parentRoute.layouts.length > 0 Only page-bearing routes or layout-only UI routes can own nested parallel- slot sub-routes. Route handlers and layout-less routes are skipped. Also adds a regression test: a route.ts handler with an ancestor parallel slot must not emit synthetic routes under the handler pattern. * fix(routing): scan synthetic routes in structural conflict check + regression test The structural-conflict guard had regressed back to scanning the original routes array, missing conflicts against synthetic routes created earlier in the same discoverSlotSubRoutes pass. Fixed by scanning Array.from(routesByPattern.values()) instead. Also adds a regression test proving that two slot sub-pages with different param names under the same parent do not both materialise (e.g. @a/[id] and @b/[name] under /shop — only one of /shop/:id or /shop/:name is emitted). |
||
|
|
7b41e950bd |
fix(app-page): align redirect()/notFound() handling under loading.tsx with Next.js (#1057)
* fix(app-page-probe): always await page promise so redirect()/notFound() under loading.tsx return 307/404 The probe used to fire-and-forget the page promise when a route-level loading.tsx was present (`awaitAsyncResult: !hasLoadingBoundary`). For async pages that throw `redirect()` or `notFound()`, that swallowed the rejection during probe; the RSC stream then re-ran the page under the route Suspense boundary, where React converted the throw into a "Switched to client rendering" error serialized into a 200 HTML body instead of a clean 307/404. Awaiting the page in the probe surfaces the special error early, parity with the no-loading-boundary path. The cost is that the outer page function runs serially with the RSC render rather than in parallel — in practice most async work in `loading.tsx`-shielded routes happens in nested Suspense children, not in the outer page function body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: capture special-error digests post-render instead of awaiting probe Replaces the previous "always await page in probe" approach with a post-render swap that mirrors Next.js's mechanism in app-render.tsx: the page render's onError captures NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK digests; after the HTML stream drains, the lifecycle inspects the tracker and swaps a 307/404 in place of the streamed "Switched to client rendering" body. This avoids the 2× page invocation cost of awaiting in the probe (the previous fix called the page once during probe and again during RSC render, serially). Now the page runs once, inside the RSC render, and the route-level Suspense boundary (loading.tsx) traps the throw as designed. The buffered stream still contains the same final HTML for non-redirecting pages. Trade-off matches Next.js: loading.tsx fallback visibility is sacrificed for routes that redirect (you cannot both flush the loading state and reverse course to a 307). Non-redirecting routes with loading.tsx now buffer the full response rather than streaming progressively — same trade-off Next.js makes with its prerender-based pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "refactor: capture special-error digests post-render instead of awaiting probe" This reverts commit 179b20f88033f3731677a91faeb022389247223e. * refactor: align with Next.js — skip page probe + post-shell race for special errors Replaces the always-await fix with a Next.js-aligned pipeline: - Skip the page probe entirely when a route-level loading.tsx is present. The page now runs once, inside the RSC render, instead of twice serially. - Capture NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK digests in the rscErrorTracker via React's onError callback (previously dropped). - After the SSR shell is ready but before bytes are flushed, race the captured digest against a 50 ms swap window. A digest that fires within the window — sync throws and short-async like ~10 ms auth checks — is converted to a 307/404. Late rejections fall through to the streamed body, matching Next.js's "until-first-byte-is-flushed" semantic. Trade-offs vs. the previous always-await fix: - Page invoked once instead of twice (no more 2× latency for async outer page functions). - TTFB for loading.tsx routes goes from ~outer-page-time to ~50 ms. - loading.tsx fallback is visible immediately after the swap window closes — the OpenNext-compat E2E that broke under the buffer-everything approach passes. Slow async redirects (>50 ms) still leak as serialized stream errors. Next.js handles that case by injecting <meta http-equiv="refresh"> mid-stream; doing the same in vinext is a separate, larger change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: drop 50ms swap-window — deterministic post-shell digest check Removes the timer-race hack and replaces it with the same shape Next.js uses (`app-render.tsx:4293`): after the SSR shell promise resolves, inspect the rscErrorTracker for a captured NEXT_REDIRECT/NEXT_HTTP_ERROR_FALLBACK digest and swap the response to a 307/404 if one is present. Why this works without a timer: the SSR pipeline naturally runs many microtask awaits between the page Promise rejecting (in onError) and the lifecycle reading the tracker. Throws that resolve in microtasks during shell rendering are deterministically captured. Throws that require macrotask boundaries (real I/O, setTimeout) are NOT caught and fall through to the streamed body — exactly the same trade-off Next.js makes (the digest survives in the Flight payload for the client router to consume). Updates the regression fixture to use the deterministic case (sync throw inside an async page) instead of the pre-fix repro's setTimeout trick. The setTimeout case can't be deterministically caught without either a timer (rejected as a hack) or React-internals patches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for permanentRedirect() under loading.tsx returning 308 Adds a fixture and integration test mirroring the redirect()-with- loading.tsx case but using permanentRedirect (308). Confirms the post-shell digest swap honors the statusCode field from the NEXT_REDIRECT digest rather than coercing to the 307 default — closes a flagged gap from the Next.js parity audit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for forbidden()/unauthorized() under loading.tsx Adds fixtures and integration tests verifying the post-shell digest swap correctly preserves the status code from NEXT_HTTP_ERROR_FALLBACK digests: - forbidden() under loading.tsx → 403 with root forbidden.tsx body - unauthorized() under loading.tsx → 401 with root unauthorized.tsx body Confirms resolveAppPageSpecialError reads `parseInt(parts[1], 10)` from the digest rather than coercing to 404 — closes the parity-audit's "forbidden/unauthorized status coercion" gap (which turned out to already be handled correctly; this just locks it in). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(app-page): apply basePath to redirect Location header Mirrors Next.js's `addPathPrefix(getURLFromRedirectError(err), basePath)` in app-render.tsx — `redirect("/about")` from a page mounted under basePath "/blog" now produces `Location: /blog/about` instead of the raw "/about". - Adds `basePath` to BuildAppPageSpecialErrorResponseOptions and a small helper that resolves the redirect target against the request URL and prefixes app-internal absolute paths. - External redirects (different origin) and targets that already start with the basePath are passed through unchanged. - Threads the entry-generated `__basePath` through dispatch and into both the page and layout special-error response builders. - Closes a parity gap flagged in the Next.js audit; covered by 5 new unit assertions exercising internal / external / already-prefixed / unconfigured / root-target cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(app-page): preserve cookies().set() on redirect responses Mirrors Next.js's `appendMutableCookies(headers, requestStore.mutableCookies)` in app-render.tsx. Auth flows that do cookies().set("session", "..."); redirect("/dashboard"); must preserve the Set-Cookie on the 307, otherwise the redirected request lands without the just-issued session and the user bounces back to login. - Adds an optional `getAndClearPendingCookies` to BuildAppPageSpecialErrorResponseOptions that, when set, drains pending Set-Cookie values accumulated during the render and appends them to the redirect response. - Wires `getAndClearPendingCookies` from `vinext/shims/headers` into both the page and layout special-error response builders in dispatch. - Only applied to redirect responses to match Next.js — http-access-fallback responses leave cookies to the rendered boundary. - Closes a parity gap from the audit; covered by a new unit test exercising redirect-with-cookies, redirect-without-cookies, and the http-access-fallback-cookies-not-bled cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for notFound() under loading.tsx Adds a fixture and integration test covering the most common loading-boundary special-error case in real apps: a dynamic detail page with a loading state that calls notFound() when the record is missing. Distinct from the forbidden()/unauthorized() coverage — notFound() throws the bare "NEXT_NOT_FOUND" digest (not "NEXT_HTTP_ERROR_FALLBACK;404"), which takes a separate branch in resolveAppPageSpecialError. This locks in that the post-shell digest swap handles both branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
31b3e17843 |
fix(thenable-params): protect well-known properties and unify page/route handler implementation (#1050)
Route handlers used a local Object.assign(Promise.resolve(...), params) helper that did not protect any well-known properties from shadowing. Page params used a Proxy but only guarded then/catch/finally, missing React's Promise status field and other well-known properties. Next.js explicitly reserves a well-known property set (then, catch, finally, status, toString, hasOwnProperty, etc.) so that params with those names do not break Promise behaviour or React introspection. This change: - Expands the shared makeThenableParams shim to protect the full well-known property set, matching Next.js reflect-utils.ts. - Replaces the weak route-handler local helper with the shared shim. - Adds unit tests for status and Object.prototype method shadowing. - Adds App Router integration tests for pages and route handlers with params named then, catch, finally, and status. Ported from Next.js: - https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/utils/reflect-utils.ts - https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/cache-components/cache-components.params.test.ts |
||
|
|
7f4a43dcf4 |
refactor(routing): dedupe param decoding into shared helper (#1058)
Extract the identical `decodeParam`/`decodeParams` and `decodePatternParam`/`decodePatternParams` helpers from `route-trie.ts` and `route-pattern.ts` into a single `decodeMatchedParams` in `routing/utils.ts`. Addresses review feedback on #1049. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6c786d69a9 |
fix(routing): decode matched params with decodeURIComponent (#1049)
* fix(routing): decode matched params with decodeURIComponent Vinext's segment-safe normalization preserves encoded delimiters (%2F) for correct route matching, but the captured param values were left undecoded. This caused dynamic params to return "a%2Fb" instead of "a/b", diverging from Next.js which decodes at the route-matcher level. Add decodeURIComponent pass in trieMatch and matchRoutePattern — the two route matching entry points — so all downstream consumers (pages, route handlers, metadata, useParams) receive decoded param values. Mirrors Next.js: packages/next/src/shared/lib/router/utils/route-matcher.ts L25-27 * test(app-rsc-route-matching): update param decode assertion to match new behavior |
||
|
|
390b4608be |
fix(server): reject invalid HTTP methods with 400 in app route handlers (#1048)
Vinext previously uppercased the request method and fell through to the normal 405 response for any unrecognized method. Next.js rejects non-standard methods with 400 before dispatch. - Add isValidHTTPMethod() predicate to app-route-handler-runtime.ts - Return empty 400 in dispatchAppRouteHandler() before auto-OPTIONS/405 - Port Next.js test: HEADER => 400 Bad Request Next.js source reference: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/route-modules/app-route/module.ts#L390-L392 https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-routes/app-custom-routes.test.ts#L531-L538 |
||
|
|
9d5d1ce50d |
fix(app-router): validate RSC cache-busting params (#991)
* fix(app-router): validate RSC cache-busting params App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request. Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions. Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration. * fix(app-router): hide RSC cache tokens from navigation URLs * fix(app-router): hide RSC cache token from searchParams * fix(app-router): address RSC cache-busting review * fix(app-router): preserve HTML redirect destination for direct .rsc navigations Config redirects from next.config.js were unconditionally converted to .rsc destinations for any request whose URL ended in .rsc. This broke direct browser navigations to .rsc URLs (e.g. a bookmark or external link) because the browser was redirected to an RSC payload it could not render. Client-side RSC fetches always send the RSC: 1 header, so restrict the .rsc destination conversion to requests that carry that header. Direct browser hits without the header continue to redirect to the canonical HTML URL, matching the pre-PR behaviour for config redirects. |
||
|
|
6b7e7557a9 |
fix: support revalidate = false for App route segment config (#1046)
* fix: support revalidate = false for App route segment config (#1041) - Build-time: extractExportConstNumber now matches false and returns Infinity - Classifiers: classifyLayoutSegmentConfig and classifyAppRoute correctly classify revalidate = false as static (indefinite caching) - Runtime: getAppRouteHandlerRevalidateSeconds returns Infinity for false, enabling ISR cache gates that emit STATIC cache-control instead of SWR - Template: RSC entry resolves revalidate = false from page exports as Infinity - Cache-Control: buildRouteHandlerCacheControl emits STATIC cache-control when revalidateSeconds is Infinity, matching the page-path behavior Ported from Next.js segment config validation: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/segment-config/app/app-segment-config.ts * fix: exclude Infinity from route handler ISR read/write gates * Update packages/vinext/src/build/report.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --------- Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> |
||
|
|
d862137a64 |
fix: support non-standard robots directives via other field (#1010) (#1047)
Adds the `other` field to `RobotsRule` type and emits non-standard per-user-agent directives (e.g. Seznam Request-Rate, Yandex Clean-param) after Crawl-delay and before the blank line separating user-agent blocks. Array values expand into repeated lines; null/undefined entries are skipped. |
||
|
|
08d11a10c2 |
fix(page-request): catch generateStaticParams errors per-source, not per-loop (#1045)
When validateAppPageDynamicParams iterates multiple generateStaticParams sources, a throw in one source previously short-circuited the try-catch wrapping the whole loop, skipping remaining sources. Move the try-catch inside the loop so each source is independently guarded. |
||
|
|
fb342f5ed6 |
refactor(app-rsc): move request lifecycle into typed handler (#1036)
* refactor(app-rsc): introduce app-rsc-request-normalization module The RSC entry handler inlined all URL parsing, pathname decoding, basePath checking, RSC detection, and header sanitization directly in its _handleRequest template. That made the logic impossible to unit-test, hard to audit for security, and duplicated normalizeMountedSlotsHeader in three places. Create server/app-rsc-request-normalization.ts to own the full normalization pipeline as a single, typed function (normalizeRscRequest). The function returns a discriminated Response | NormalizedRscRequest, encoding early exits (400/404) in the type rather than as throw-then-catch pairs. The ordering within the function is security-critical and documented step by step: 1. Parse URL 2. Protocol-relative guard (on raw pathname, before normalizePath collapses //) 3. Strict segment-wise percent-decode (malformed encoding -> 400) 4. normalizePath (collapse //, resolve . and .. segments) 5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses) 6/7. RSC detection and cleanPathname 8. Sanitize X-Vinext-Interception-Context (strip null bytes) 9. Normalize x-vinext-mounted-slots (dedup, sort) Consolidate normalizeMountedSlotsHeader: the function existed independently in isr-cache.ts and as a private copy in app-elements.ts. Both now import from the new module. isr-cache.ts re-exports it for callers already importing from there. 43 new behavior tests cover the security-sensitive paths: protocol-relative variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath enforcement, %2F preservation, RSC detection by suffix and Accept header, interception context sanitization, and mounted-slots normalization. * refactor(app-rsc): wire normalizeRscRequest into the entry and callsites Replace the open-coded normalization block in the generated _handleRequest function with a single normalizeRscRequest call. The entry now delegates all security-sensitive preprocessing to the typed module rather than repeating it in a template string. Remove the local normalizeMountedSlotsHeader implementation from isr-cache.ts (was a standalone export) and remove the private copy from app-elements.ts. Both now import from app-rsc-request-normalization.ts. isr-cache.ts re-exports the function so existing callers that import from there are unaffected. Generated entry changes: - Add import for normalizeRscRequest from the new module - Remove guardProtocolRelativeUrl and stripBasePath from requestPipeline import - Remove normalizePathnameForRouteMatchStrict from routingUtils import - Remove normalizeMountedSlotsHeader from isrCache import - Replace ~20-line normalization block with 4-line call + destructure * refactor(app-rsc): move request lifecycle into typed handler The generated App Router RSC entry still owned request normalization, config routing, middleware handoff, public-file serving, action dispatch, fallback routing, and response finalization. That made lifecycle behavior hard to test directly and forced generator assertions to reach into runtime sequencing. The violated invariant was that codegen should describe the app shape while normal modules should implement behavior. This extracts the request lifecycle into createAppRscHandler, leaves the generated entry responsible for route-specific manifests and dispatch closures, and adds behavior tests for redirects, rewrites, public files, actions, route handlers, and fallback cleanup.v0.0.47 |
||
|
|
38d5be5500 | chore: add code review comment guidelines to viguy agent (#1043) | ||
|
|
74a05788dd | chore: tweak readme (#1042) | ||
|
|
54a1286839 |
fix(app): apply layout segment config to pages (#993)
* fix(app): apply layout segment config to pages * fix(app): address segment config review nits |
||
|
|
01b64a1f71 |
refactor(app-rsc): extract response finalisation into server/app-rsc-response-finalizer (#1035)
* refactor(app-rsc): introduce app-rsc-request-normalization module
The RSC entry handler inlined all URL parsing, pathname decoding, basePath
checking, RSC detection, and header sanitization directly in its
_handleRequest template. That made the logic impossible to unit-test, hard to
audit for security, and duplicated normalizeMountedSlotsHeader in three places.
Create server/app-rsc-request-normalization.ts to own the full normalization
pipeline as a single, typed function (normalizeRscRequest). The function
returns a discriminated Response | NormalizedRscRequest, encoding early exits
(400/404) in the type rather than as throw-then-catch pairs.
The ordering within the function is security-critical and documented step by
step:
1. Parse URL
2. Protocol-relative guard (on raw pathname, before normalizePath collapses //)
3. Strict segment-wise percent-decode (malformed encoding -> 400)
4. normalizePath (collapse //, resolve . and .. segments)
5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses)
6/7. RSC detection and cleanPathname
8. Sanitize X-Vinext-Interception-Context (strip null bytes)
9. Normalize x-vinext-mounted-slots (dedup, sort)
Consolidate normalizeMountedSlotsHeader: the function existed independently in
isr-cache.ts and as a private copy in app-elements.ts. Both now import from
the new module. isr-cache.ts re-exports it for callers already importing from
there.
43 new behavior tests cover the security-sensitive paths: protocol-relative
variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath
enforcement, %2F preservation, RSC detection by suffix and Accept header,
interception context sanitization, and mounted-slots normalization.
* refactor(app-rsc): extract response finalisation into typed helper
Config header application runs inline in the generated RSC entry handler()
as a template string with no tests. Every response variant (page, route
handler, server action, not-found) goes through handler(), but the shared
finalisation logic cannot be tested or reused — a classic rot factory.
Create server/app-rsc-response-finalizer.ts that owns:
- Skip 3xx redirect responses. Response.redirect() produces immutable Headers
that throw on write; Next.js also deliberately excludes config headers from
redirects.
- Normalize the request pathname (segment-wise percent-decode + collapse)
before matching config header source patterns.
- Strip basePath at a segment boundary before matching. The old inline code
used pathname.startsWith(basePath) which incorrectly matched /app2/page when
basePath is /app; stripBasePath() enforces the separator boundary.
- Apply matching config headers via applyConfigHeadersToResponse, preserving
middleware-set header precedence.
The generated entry now calls __finalizeAppRscResponse(response, request, {...})
in place of ~15 lines of template logic, and drops the now-unused
applyConfigHeadersToResponse and normalizePathnameForRouteMatch imports from
the generated code.
Tests cover: header applied to 200, empty configHeaders no-op, source
pattern miss, immutable 307 no throw, mutable 308 header skipped, basePath
strip, basePath segment-boundary (regression for startsWith bug), nested
basePath, has-condition match, has-condition miss.
|
||
|
|
3cd2bfc2be |
fix(routing): match inherited slot params against routePath, not request.url (#1040)
Follow-up to #1039. Two related polish items in the inherited-slot mirror runtime: 1. Bug under `basePath`: `buildSlotOverrides` was parsing `request.url` to derive the URL parts to match against `slot.slotPatternParts`. The original request URL still carries the configured `basePath`, but `slotPatternParts` is built from app-relative segments only — so for every basePath-configured app, the pattern match silently failed and slot params fell back to the route's matched params. Use the already normalized `routePath` parameter (basePath stripped, RSC suffix removed) instead. Added a unit test that exercises the regression. 2. Memoize `findSlotSubPages` per build via a `WeakMap` keyed by the matcher. Inherited slots get scanned once per descendant route, so without memoization a route N segments deep paid O(N) full subtree walks for every shared ancestor slot. Cache lifetime tracks the matcher's, so a long-lived dev server doesn't accumulate state across builds. Also dedupes a redundant `convertSegmentsToRouteParts` call inside `findMirroredSlotPage` (both tiers needed the same conversion). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fffda16106 |
feat(dev): React-based runtime error overlay (#1029)
Surfaces three error sources that previously only reached the console: React render errors caught by an error.tsx boundary (onCaughtError), render errors with no boundary above them (onUncaughtError), and plain script errors / unhandled promise rejections via window listeners. The overlay renders into a separate createRoot mounted on a detached div appended to body, so it survives an unmount of the main hydrateRoot tree — necessary because most errors we want to surface take that tree down. Includes a /dev-overlay-test fixture and Playwright spec covering all three error sources, dismiss, and prev/next pagination across multiple errors. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2cf21433cb |
fix(routing): mirror inherited parallel slots to descendant sub-pages (#1039)
* fix(routing): mirror inherited parallel slots to descendant sub-pages discoverInheritedParallelSlots was hard-coding pagePath to null for any ancestor slot, so inherited slots only ever rendered default.tsx — even when an obvious mirror like @breadcrumbs/about/page.tsx existed for the matching route. Track each ancestor's segment index, then look up a mirror via either a literal filesystem match or, when route groups appear on either side, by URL-parts comparison through convertSegmentsToRouteParts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(routing): extract slot-specific params for inherited mirrors When an inherited parallel slot's mirrored sub-page declares dynamic markers under different names than the route's (e.g. slot uses @breadcrumbs/distinct/[name] for a route at distinct/[id]), the runtime was passing the route's matchedParams through unchanged — so the slot page received {id} instead of {name} and rendered with an empty value. Three pieces: * `findMirroredSlotPage` now also accepts pattern-compatible matches and returns the slot's URL parts and param names. Specificity scoring picks the most-specific compatible sub-page (literal > dynamic > catch-all). * Each ParallelSlot carries optional `slotPatternParts` and `slotParamNames`, serialized through the manifest. * `buildPageElements` extracts a per-request `slotOverrides[key].params` by matching the request URL against `slotPatternParts` whenever a slot's param names aren't a subset of the route's. `pageModule` on `AppPageSlotOverride` becomes optional so a params-only override doesn't have to swap the slot's page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(routing): address bonk review on inherited parallel slot mirror * Swap inverted scoring for required vs optional catch-alls — `:name+` (matches ≥1 segments) is more constrained than `:name*` (≥0), so it now scores higher, preventing a less-specific optional catch-all from shadowing a required one when both compete for the same route. * Tighten findMirroredSlotPage docstring: the implementation collapses exact URL-parts equality and pattern compatibility into one scored loop, where literals naturally win. Rewrite the comment to reflect what the code actually does. * Document patternsCompatible's conservative rejection of literal slot segments paired with dynamic route segments (and the consequence that a literal slot sub-page never matches a catch-all route). * Note in buildSlotOverrides why an empty slotParamNames also skips the override (no dynamic markers means nothing to extract). * Add unit tests for two cases that were only covered indirectly: graph-level slotPatternParts/slotParamNames when route and slot use different param names, and a slot owned at an intermediate ancestor rather than appDir. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e51856bb0d |
fix: validate CLI --port / --hostname values, reject missing/flags-as-values (#1012) (#1037)
Previously, vinext dev --port (no value) silently returned NaN, vinext dev --hostname returned undefined, and --port --hostname 0.0.0.0 consumed --hostname as the port value. Now the parser throws clear errors when a value-taking flag is missing its value or when the next argument looks like another flag. Extracted parseArgs to cli-args.ts for testability. |
||
|
|
aba335d5f3 |
fix(dev): self-host Google Fonts in dev mode (#1038)
The build pipeline already rewrites `next/font/google` calls to embed self-hosted @font-face CSS pointing at `/<assetsDir>/_vinext_fonts/...`, but the call-site rewrite was gated behind `isBuild`. Dev requests for those URLs 404'd because nothing copies the cached `.vinext/fonts/` files into the dev server's served paths, and once `_selfHostedCSS` is present the shim no longer emits the fonts.googleapis.com `<link>` — so dev showed unstyled glyphs with no CDN fallback. Drop the build-only gate and add a `configureServer` middleware that serves the cached files directly under the same URL prefix the @font-face CSS embeds. Reuse the prod `CONTENT_TYPES` map so dev and prod hand out identical MIME types. Regression spec navigates to a new `/font-google-test` route in app-basic that uses Geist + Geist_Mono, asserts no CDN URL leaks into the HTML, and fetches a preload URL to verify the dev middleware returns a real font/woff2 body. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a3a8c0bfc1 |
refactor(app-rsc): extract request normalization into server/app-rsc-request-normalization (#1034)
* refactor(app-rsc): introduce app-rsc-request-normalization module
The RSC entry handler inlined all URL parsing, pathname decoding, basePath
checking, RSC detection, and header sanitization directly in its
_handleRequest template. That made the logic impossible to unit-test, hard to
audit for security, and duplicated normalizeMountedSlotsHeader in three places.
Create server/app-rsc-request-normalization.ts to own the full normalization
pipeline as a single, typed function (normalizeRscRequest). The function
returns a discriminated Response | NormalizedRscRequest, encoding early exits
(400/404) in the type rather than as throw-then-catch pairs.
The ordering within the function is security-critical and documented step by
step:
1. Parse URL
2. Protocol-relative guard (on raw pathname, before normalizePath collapses //)
3. Strict segment-wise percent-decode (malformed encoding -> 400)
4. normalizePath (collapse //, resolve . and .. segments)
5. basePath check + strip (missing prefix -> 404; /__vinext/ bypasses)
6/7. RSC detection and cleanPathname
8. Sanitize X-Vinext-Interception-Context (strip null bytes)
9. Normalize x-vinext-mounted-slots (dedup, sort)
Consolidate normalizeMountedSlotsHeader: the function existed independently in
isr-cache.ts and as a private copy in app-elements.ts. Both now import from
the new module. isr-cache.ts re-exports it for callers already importing from
there.
43 new behavior tests cover the security-sensitive paths: protocol-relative
variants (literal, backslash, %5C, %2F), malformed percent sequences, basePath
enforcement, %2F preservation, RSC detection by suffix and Accept header,
interception context sanitization, and mounted-slots normalization.
* refactor(app-rsc): wire normalizeRscRequest into the entry and callsites
Replace the open-coded normalization block in the generated _handleRequest
function with a single normalizeRscRequest call. The entry now delegates
all security-sensitive preprocessing to the typed module rather than
repeating it in a template string.
Remove the local normalizeMountedSlotsHeader implementation from isr-cache.ts
(was a standalone export) and remove the private copy from app-elements.ts.
Both now import from app-rsc-request-normalization.ts. isr-cache.ts re-exports
the function so existing callers that import from there are unaffected.
Generated entry changes:
- Add import for normalizeRscRequest from the new module
- Remove guardProtocolRelativeUrl and stripBasePath from requestPipeline import
- Remove normalizePathnameForRouteMatchStrict from routingUtils import
- Remove normalizeMountedSlotsHeader from isrCache import
- Replace ~20-line normalization block with 4-line call + destructure
* fix(app-rsc): remove orphaned url declaration from _handleRequest
The original const url = new URL(request.url) at the top of _handleRequest
was not removed when the normalization block was replaced with normalizeRscRequest.
The destructure const { url } = __norm on the next early-exit block redeclares
the same identifier, causing a PARSE_ERROR in Rolldown during production builds.
Remove the orphaned declaration. url now comes solely from the normalizeRscRequest
result.
* fix(test): update cleanPathname assertion for normalizeRscRequest destructuring
* refactor(app-rsc): extract normalizeMountedSlotsHeader into neutral module and add null-byte test
Moves normalizeMountedSlotsHeader from app-rsc-request-normalization.ts into server/app-mounted-slots-header.ts so it can be imported by app-elements.ts without creating a module boundary dependency from payload helpers into request-lifecycle code.
Updates - isr-cache.ts re-exports from the new module. app-elements.ts switches its import accordingly.
Adds a null-byte sanitisation test for X-Vinext-Interception-Context (previously untested security branch). Fixes the PR description to accurately describe the duplication story.
* ci: retrigger CI
* ci: retrigger CI 2
|
||
|
|
ce3af6aa21 |
refactor(app-router): extract browser navigation lifecycle controller (#1019)
* refactor(app-router): extract browser navigation lifecycle controller * fix(app-router): separate render ids from navigation ids * fix(app-router): satisfy ci lint rules * fix(app-router): narrow controller surface, add hmrReplaceTree, restore lost comments, fix tests |
||
|
|
763736eae0 |
refactor(app-rsc-entry): delegate pure helpers to typed server modules (#1017)
* refactor(app-rsc-entry): delegate pure helpers to typed server modules The generated RSC entry template contained five inline implementations of pure runtime helpers: post-middleware request context construction, RSC error handler creation, request context cleanup, RSC stream preload hint normalization, and hook warning suppression. These inline blocks repeated runtime behavior across multiple call sites and could not be unit-tested independently. Move each helper to a dedicated typed module under server/ so the generated entry imports and wires them instead of owning the logic: - buildPostMwRequestContext → server/app-post-middleware-context.ts - createRscOnErrorHandler → server/app-rsc-error-handler.ts - __clearRequestContext + setNavigationContext → server/app-request-context.ts - renderToReadableStream preload hints → server/rsc-stream-hints.ts (createRscRenderer) - hook warning console.error patch → server/app-hook-warning-suppression.ts Each helper module has focused unit tests. The entry template is 55 lines shorter and now describes the app shape; the typed modules own behaviour. * fix: remove redundant as cast and fix test module caching app-hook-warning-suppression.ts contained an `as string` cast that was redundant because the typeof check on the prior line already narrows the type. The hook warning suppression tests in tests 2-4 were passing vacuously because dynamic import() returned the cached module whose console.error patch was no longer active after test 1 restored the original. Use vi.resetModules() before each test to force fresh module evaluation. clearAppRequestContext now delegates to setAppNavigationContext(null) instead of calling the raw navigation and root-params setters directly, matching the DRY pattern from the original inline code. * fix: resolve entry helper rebase conflicts * ci: rerun checks (font-google flake) * Update packages/vinext/src/entries/app-rsc-entry.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --------- Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> |
||
|
|
a49be5c881 |
fix: add hydration-level onError replay in next/image via useLayoutEffect (#1021)
* fix: add hydration-level onError replay in next/image via useLayoutEffect (#1007) Port the upstream Next.js hydration replay pattern from vercel/next.js#93209: use a useLayoutEffect + img.src = img.src to re-trigger the error trajectory when an image fails before React hydration completes (e.g. during SSR streaming). Changes: - Add useNonWarningLayoutEffect helper (useEffect in SSR, useLayoutEffect in browser) - Add mergedRef to capture the underlying img element for the layout effect - Add useLayoutEffect that re-triggers img.src = img.src on initial mount when onError is provided - Forward ref to UnpicImage elements (ref forwarding is supported) - Add SSR rendering tests for onError, onLoad+onError, ref forwarding, and loader path * fix: address PR review feedback for next/image onError hydration replay (#1007) - Fix mergedRef callback instability: use ref object in useRef instead of array dep in useCallback, matching Next.js upstream - Add stable refs for onLoad/onError/onLoadingComplete via useRef + useEffect sync pattern to prevent layout effect re-runs - Fix layout effect deps to use stable ref objects instead of raw handler values; drop 'loading' (not in Next.js upstream deps) - Add img.complete check for onLoad replay when browser already loaded the image before hydration-level onError replay - Use React.RefObject instead of deprecated MutableRefObject - Fix eslint-disable comment placement for no-self-assign - Add SSR smoke test comments in shims.test.ts * fix(next/image): address PR review — extract helper, clean deps, add comment - Extract createSyntheticLoadEvent(img) helper for ~35→1 line in layout effect - Remove onLoadRef/onErrorRef from layout effect dep array (stable ref objects) - Add comment explaining useRef+useEffect sync timing gap * fix: guard onLoad replay with naturalWidth > 0, clean deps (#1007) - Check img.naturalWidth > 0 to prevent firing synthetic onLoad for errored images (img.complete is true for both success and error per the HTML spec) - Remove src from layout effect dep array — dead dependency since didInsertRef bails on all re-runs after mount - Add comment noting createSyntheticLoadEvent requires browser context - Fix test comment referencing non-existent image-component.test.ts * fix: port Next.js useMergedRef hook for next/image ref merging Replace the inline mergedRef callback (with [] deps) with a proper useMergedRef hook ported verbatim from next/src/client/use-merged-ref.ts. The inline version used useCallback with [] deps, which kept the callback identity stable and prevented React from re-invoking it when the parent ref changed on re-render. The ported hook uses [refA, refB] deps, which correctly triggers React's ref lifecycle: old ref gets null (cleanup), new ref gets the element. Also adds React 19 cleanup ref support (ref callbacks that return cleanup functions) and proper null-based cleanup on unmount. |
||
|
|
c98b08f5b5 |
fix(dev): exclude private-next-instrumentation-client from optimizeDeps (#1030)
The bare specifier is aliased to a user source file (or the empty-module shim), neither of which should be pre-bundled. Without this exclude, Vite's scanner treats it as an unknown npm dep on the first request, optimizes it, and triggers a full reload. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04befb5342 |
fix(navigation): hard-nav to target when render error tears down the tree (#1028)
When an RSC payload contains an error reference for a slot (e.g. a server component throws), createPendingNavigationCommit only awaits the top-level elements promise, so the dispatch goes through and React renders. The slot throws when use() unwraps the rejected reference. Without an error.tsx boundary above NavigationCommitSignal, the entire root unmounts — and because the URL update lives in NavigationCommitSignal's pre-paint layout effect, the browser is left on the previous URL with a blank page and a console-only error. Track the in-flight navigation target in a module-level pendingNavigationRecoveryHref, set it just before dispatchBrowserTree and clear it once the commit effect updates history. Add an onUncaughtError handler to hydrateRoot that reads the recovery href at fire time and window.location.assign()s to it, so the user lands on the URL they clicked and the server can render its actual error UI for that route. Wire the handler in both dev and prod (previously only onCaughtError was registered in dev). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cc484ab9f4 |
fix(hmr): full-reload after a render error tears down BrowserRoot (#1027)
A render error (e.g. a server route throws) unmounts BrowserRoot, which clears setBrowserRouterState/browserRouterStateRef. The HMR rsc:update handler used to bail silently in that state, leaving HMR effectively dead until the user manually refreshed. Track a sticky 'has ever committed' flag and trigger window.location.reload() when the tree was up before but isn't now. The next HMR push reconciles via the fresh load. The flag only goes back to false implicitly via a successful remount path (it's set in markBrowserRouterStateReady), so we don't loop — if the broken code prevents the next mount entirely, hasEverCommitted stays true on the reloaded page only until the next mount succeeds. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |