mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
v0.0.45
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e81820279e |
fix: preserve console output for caught app errors in dev (#862)
* fix: preserve console output for caught app errors in dev * address bonk review comments on PR 862 --------- Co-authored-by: James <james@eli.cx> |
||
|
|
8f224051b7 |
Preserve intercepted app-router state across server actions (#854)
* Preserve interception context on app-router actions * Keep intercept slot overrides on action rerenders * Pin React versions in static-export fixture * refactor(app-page-request): unify intercept resolution into a single state machine Collapse the parallel GET/POST intercept lookup paths into one shared internal helper that returns a tagged union: none, current-route, or source-route. Both resolveAppPageInterceptMatch and resolveAppPageActionRerenderTarget now read from the same lookup result, eliminating the double findIntercept() scan on current-route fallbacks. Also add the two missing resolveAppPageActionRerenderTarget fallthrough test cases (no intercept, current-route intercept with no matching action route) and a regression assertion that the current-route branch only performs one intercept lookup. |
||
|
|
18237955fa |
feat: static/dynamic layout detection for skip-header optimization (#767)
* feat: add classifyLayoutSegmentConfig for layout segment config detection
Reads `export const dynamic` and `export const revalidate` from layout
source files to classify them as static or dynamic. Unlike page
classification, positive revalidate values return null (ISR is a page
concept), deferring to module graph analysis for layout skip decisions.
* feat: add module graph layout classification (Layer 2)
BFS traversal of each layout's dependency tree via Vite's module graph.
If no transitive dynamic shim import (headers, cache, server) is found,
the layout is provably static. Otherwise it needs a runtime probe.
classifyAllRouteLayouts combines Layer 1 (segment config, from prior
commit) with Layer 2 (module graph), deduplicating shared layouts.
* feat: per-layout dynamic detection in probe phase (Layer 3)
Extends probeAppPageLayouts to return per-layout flags ("s"/"d")
alongside the existing Response. Three paths per layout:
- Build-time classified: pass flag through, still probe for errors
- Needs probe: run with isolated dynamic scope, detect usage
- No classification: original behavior (backward compat)
probeAppPageBeforeRender propagates layoutFlags through the result.
renderAppPageLifecycle updated to destructure the new return type.
* feat: add __layoutFlags payload metadata and thread through router state
Adds APP_LAYOUT_FLAGS_KEY to the RSC payload metadata, carrying
per-layout static/dynamic flags ("s"/"d"). readAppElementsMetadata
now parses layoutFlags with a type predicate guard.
AppRouterState and AppRouterAction carry layoutFlags. Navigate merges
flags (preserving previously-seen layouts), replace replaces them.
All dispatchBrowserTree call sites updated to pass layoutFlags.
* refactor: group classification options into single LayoutClassificationOptions type
The three optional fields (buildTimeClassifications, getLayoutId,
runWithIsolatedDynamicScope) had an all-or-nothing invariant enforced
only at runtime. Grouping them into a single optional `classification`
object makes the constraint type-safe — you either provide the full
classification context or nothing.
Also deduplicates the LayoutFlags type: canonical definition lives in
app-elements.ts, re-exported from app-page-execution.ts.
* fix: default to dynamic flag when layout probe throws non-special error
When runWithIsolatedDynamicScope throws and the error is non-special
(onLayoutError returns null), the layout was silently omitted from
layoutFlags. Now conservatively defaults to "d" — if probing failed,
the layout cannot be proven static.
* refactor: replace parallel arrays with LayoutEntry struct, tighten names
- Introduce LayoutEntry (moduleId, treePosition, segmentConfig) in
layout-classification.ts, collapsing the three parallel arrays
(layouts/layoutTreePositions/layoutSegmentConfigs) in
RouteForClassification into a single struct. Switches the inner loop
to for...of, removing the ?? 0 fallback that masked index mismatches.
- Update all five classifyAllRouteLayouts test fixtures to the struct shape.
- Add a JSDoc on LayoutFlags clarifying that "s" = static, "d" = dynamic.
- Tighten the buildTimeClassifications comment to say "keyed by layout index".
https://claude.ai/code/session_01MDJzCNao3dX9tmmVtSqsw5
* style: fix prettier formatting
https://claude.ai/code/session_01MDJzCNao3dX9tmmVtSqsw5
* Revert "style: fix prettier formatting"
This reverts commit
|
||
|
|
5f6bccbd03 |
feat: track previousNextUrl for intercepted App Router entries (#755)
* docs: refresh stale app browser entry comments * feat: track previousNextUrl for intercepted App Router entries * refactor: bind getBrowserRouterState() once in refresh case Avoid calling getBrowserRouterState() twice in the refresh branch of getRequestState by binding previousNextUrl to a local. Behavior-preserving nit raised in PR review. * refactor: drop dead history-state interception-context read path After previousNextUrl became the source of truth for interception context in history state, nothing in production writes the legacy __vinext_interceptionContext key anymore. The fall-through read in getCurrentInterceptionContext was vestigial and getCurrentInterceptionContext now derives strictly from the current pathname. - Removes the unused VINEXT_INTERCEPTION_CONTEXT_HISTORY_STATE_KEY export - Removes readHistoryStateInterceptionContext - Updates the prefetch-cache unit test to assert the new pathname-derived contract instead of the legacy history-state override * test(e2e): cover back/forward restoration of intercepted modal view Adds the back-then-forward case for App Router intercepting routes: after intercepted navigation from /feed to /photos/42, going back to /feed and then forward should restore the modal (rather than rendering the full /photos/42 page). Directly exercises the previousNextUrl read in the traverse branch of getRequestState. * docs: reword stale 'belongs to PR 5' comments Two inline comments in app-browser-entry.ts referenced "PR 5" as a future PR, but this is PR 5. Reworded to describe the deferred scope as-is: interception context on server-action POSTs and HMR re-renders is intentionally not propagated. * fix: distinguish explicit null from undefined in previousNextUrl fallback --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: James <james@eli.cx> |
||
|
|
b8c44fdd52 |
feat: encode interception context in App Router payload IDs and caches (#753)
* test: add E2E verification for layout persistence flat payload pipeline Prove the flat keyed map architecture works end-to-end: - Layout state persists across sibling navigation (counter survives) - Template remounts on segment boundary change, persists within segment - Error boundary clears on navigate-away-and-back - Back/forward preserves layout state through history - Parallel slots persist on soft nav, show default.tsx on hard nav Zero production code changes — test fixtures and Playwright specs only. * test: harden layout persistence hydration assertions * test: address layout-persistence spec gaps * test: tighten not.toBeAttached() assertions for absent elements * chore: trigger CI * feat: encode interception context in App Router payload IDs and caches * test: update App Router entry snapshots for interception encoding * fix: reuse committed interception context for soft navigations * chore: fix formatting in app-browser-entry after rebase * fix: update mounted-slot test helper calls for 4-arg createResolvedElements signature PR 3 added mounted-slot tests using createResolvedElements(routeId, root, extraEntries). PR 4 inserted interceptionContext as the 3rd param, shifting extraEntries to 4th. The object was being swallowed as interceptionContext, causing getMountedSlotIds to return []. Also add missing renderId to the interception context pending-commit test. * chore: document intentional interception context omission in HMR handler Add a comment to the HMR rsc:update handler noting that interception context is intentionally omitted, matching the existing documentation pattern in the server action handler. https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * fix: prefetch cache test now exercises interception-context partitioning The test was passing pre-computed cache keys as the rscUrl parameter to storePrefetchResponse(), which internally calls createAppPayloadCacheKey again. With the default interceptionContext=null the key passed through unchanged, so the test only verified Map key uniqueness rather than the actual partitioning logic. Pass raw RSC URLs with separate interception context parameters instead. Also add a comment clarifying that PendingNavigationCommit's top-level interceptionContext/rootLayoutTreePath/routeId are convenience aliases for the corresponding action fields. https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * fix: strip NUL bytes from interception context request header Defensively remove \0 from the incoming X-Vinext-Interception-Context header to prevent a malicious client from crafting cache keys that contain the internal NUL separator. https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * fix: resolve rebase conflicts in slot preservation tests After rebase onto upstream main, two fixes needed: - Pass extraEntries as 4th arg to createResolvedElements (3rd is now interceptionContext) - Add missing interceptionContext field to test action objects https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo * test: update entry template snapshots for NUL-byte header sanitization https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
19f4fac338 |
fix: clear stale parallel slots on traversal in mergeElements (#816)
* fix: clear stale parallel slots on traversal in mergeElements Add "traverse" as a first-class action type in the router reducer. When the browser navigates back/forward, the action type is now "traverse" instead of "navigate", which tells mergeElements to clear slot keys absent from the server response. On soft forward navigation, absent slots are still preserved (the server may omit parent layout slots that haven't changed). Only traversals clear them, because the server renders the full destination route tree on popstate. Closes #814 * refactor: collapse navigate/traverse reducer cases; add UNMATCHED_SLOT traversal test Address review feedback on #816: - Collapse the identical navigate/traverse switch cases in routerReducer, passing `action.type === "traverse"` as the clearAbsentSlots boolean. Keeps exhaustive switch and never guard intact. - Add a test pinning the subtle interaction between clearAbsentSlots=true and the UNMATCHED_SLOT preservation loop: a slot sent as UNMATCHED_SLOT during traversal (key present in next) is restored from prev real content and is not cleared by the absent-slot loop. https://claude.ai/code/session_01RhqyMQWoHBr8qrLZoUjoZj * refactor: extract toActionType helper; add reducer-level traverse tests Address second-round review feedback on #816: - Extract toActionType(kind: NavigationKind) helper to consolidate the duplicated `navigationKind === "traverse" ? "traverse" : "navigate"` ternary that appeared at two call sites in renderNavigationPayload. A comment documents the "refresh" → "navigate" mapping intent and flags both sites as needing updates if NavigationKind gains new values. - Add routerReducer integration tests in app-browser-entry.test.ts: "traverse" clears stale parallel slots; "navigate" preserves them. These pin the wiring between the reducer and mergeElements. https://claude.ai/code/session_01RhqyMQWoHBr8qrLZoUjoZj * fix ci * fix ci again --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: James <james@eli.cx> |
||
|
|
ca0d542947 |
Fix parallel slot persistence and cache variants (#811)
* Fix parallel slot persistence and cache variants * Address App Router review follow-ups * Fix generated mounted-slot regex escaping * Address review feedback on slot normalization and cache comments Align generated entry's __normalizeMountedSlotsHeader regex with the typed module in app-elements.ts (both now use /\s+/). Clarify the HTML vs RSC cache asymmetry comment in stale regen path. * Remove redundant .map(trim) and clarify regen slot context split(/\s+/) never produces tokens with leading/trailing whitespace, so .map(trim) was a no-op in both normalizeMountedSlotsHeader and its generated counterpart. Also add a comment clarifying that background regen intentionally inherits the triggering request's slot context. * Clarify regen dedup and prefetch slot-mismatch tradeoffs Add comments explaining that pathname-only regen dedup is intentional (avoids concurrent regen storms across slot variants) and that consumePrefetchResponse unconditionally removes the entry before the slot-mismatch check (simplest correct behavior, prefetch is wasted on mismatch). * Document storePrefetchResponse slot-awareness gap Add JSDoc note that storePrefetchResponse is slot-unaware: the snapshot's mountedSlotsHeader comes from the response headers rather than the caller, so consumePrefetchResponse may reject the entry on slot context mismatch. |
||
|
|
0dbfeff1af |
feat: flat keyed payload for App Router layout persistence (#750)
* Extract app page route wiring helpers
* Add slot client primitives
* Fix app page error boundary serialization
* Fix client error boundary pathname reset
* Document Next.js error boundary verification
* Implement flat App Router payload for layout persistence
* fix: address review findings in flat payload implementation
- Fix stale closure on readBrowserRouterState by using a useRef updated
synchronously during render instead of a closure captured in
useLayoutEffect. External callers (navigate, server actions, HMR) now
always read the current router state.
- Restore GlobalErrorBoundary wrapping that was dropped when switching
from buildPageElement to buildAppPageElements. Apps with
app/global-error.tsx now get their global error boundary back.
- Add exhaustive default case to routerReducer so new action types
produce a compile error and a runtime throw instead of silent undefined.
- Remove dead code: createRouteNodeSnapshot, AppRouteNodeSnapshot,
AppRouteNodeValue were defined but never imported.
- Remove deprecated buildAppPageRouteElement and its test — no
production callers remain after the flat payload cutover.
- Short-circuit normalizeAppElements when no slot keys need rewriting
to avoid unnecessary allocation on every payload.
- Align test data in error boundary RSC payload test (matchedParams
slug: "post" -> "missing" to match requestUrl /posts/missing).
* fix: normalize flat payload after use(), not before
createFromReadableStream() returns a React thenable whose .then()
returns undefined (not a Promise). Chaining .then(normalizeAppElements)
broke SSR by assigning undefined to flightRoot.
Fix: call use() on the raw thenable, then normalize synchronously
after resolution. Also widen renderAppPageLifecycle element type to
accept flat map payloads.
* fix: produce flat RSC payload on all rendering paths
The SSR entry always expects a flat Record<string, ReactNode> with
__route and __rootLayout metadata from the RSC stream. Three paths
were still producing bare ReactNode payloads:
1. renderAppPageBoundaryElementResponse only created the flat map for
isRscRequest=true, but HTML requests also flow through RSC→SSR
2. buildPageElements "no default export" early return
3. Server action "Page not found" fallback
All three now produce the flat keyed element map, fixing 17 test
failures across 404/not-found, forbidden/unauthorized, error boundary,
production build, rewrite, and encoded-slash paths.
* test: update unit tests for flat RSC payload on all paths
- Update renderElementToStream mock to extract the route element from
the flat map before rendering to HTML (mirrors real SSR entry flow)
- Update entry template snapshots for the buildPageElements changes
* fix: wrap Flight thenable in Promise.resolve() before chaining .then()
createFromReadableStream() returns a React Flight thenable whose
.then() returns undefined instead of a new Promise. The browser
entry's normalizeAppElementsPromise chained .then() on this raw
thenable, producing undefined — which crashed use() during hydration
with "An unsupported type was passed to use(): undefined".
Wrapping in Promise.resolve() first converts the Flight thenable
into a real Promise, making .then() chains work correctly.
The same fix was already applied to the SSR entry in
|