@rangojs/router 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +37 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +48 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +126 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +962 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +67 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +54 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +202 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +209 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +42 -5
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +152 -26
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +30 -10
- package/src/rsc/handler.ts +12 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +263 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +134 -87
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ServerActionBridge,
|
|
3
3
|
ServerActionBridgeConfig,
|
|
4
4
|
RscPayload,
|
|
5
|
+
ResolvedSegment,
|
|
5
6
|
} from "./types.js";
|
|
6
7
|
import { createPartialUpdater } from "./partial-update.js";
|
|
7
8
|
import { enterActionFence, exitActionFence } from "./action-fence.js";
|
|
@@ -38,6 +39,11 @@ import { mergeLocationState } from "./history-state.js";
|
|
|
38
39
|
import { classifyActionOutcome } from "./action-coordinator.js";
|
|
39
40
|
import { getAppVersion } from "./app-version.js";
|
|
40
41
|
import { collectClientRevalidationDecisions } from "../client-urls/navigation.js";
|
|
42
|
+
import {
|
|
43
|
+
decideCommitGatedOff,
|
|
44
|
+
findActionFormData,
|
|
45
|
+
type TransitionWhenActionInput,
|
|
46
|
+
} from "./transition-when.js";
|
|
41
47
|
import { CLIENT_REVALIDATION_HEADER } from "../client-urls/revalidation-protocol.js";
|
|
42
48
|
|
|
43
49
|
// Polyfill Symbol.dispose/asyncDispose for Safari and older browsers
|
|
@@ -137,6 +143,20 @@ export function createServerActionBridge(
|
|
|
137
143
|
getVersion: getAppVersion,
|
|
138
144
|
});
|
|
139
145
|
|
|
146
|
+
/**
|
|
147
|
+
* transition({ when }) for an action commit: the location on screen,
|
|
148
|
+
* refreshed by the response's params and route name when it carries them.
|
|
149
|
+
*/
|
|
150
|
+
const decideActionGatedOff = (
|
|
151
|
+
segments: readonly ResolvedSegment[],
|
|
152
|
+
metadata: RscPayload["metadata"] | undefined,
|
|
153
|
+
action: TransitionWhenActionInput,
|
|
154
|
+
): boolean =>
|
|
155
|
+
decideCommitGatedOff(store, segments, "action", {
|
|
156
|
+
from: metadata,
|
|
157
|
+
action,
|
|
158
|
+
});
|
|
159
|
+
|
|
140
160
|
/**
|
|
141
161
|
* Refetch current route via a navigation transaction.
|
|
142
162
|
* Encapsulates the repeated pattern of creating a navTx + fetchPartialUpdate
|
|
@@ -146,6 +166,8 @@ export function createServerActionBridge(
|
|
|
146
166
|
segments?: string[];
|
|
147
167
|
interceptSourceUrl?: string | null;
|
|
148
168
|
actionId?: string;
|
|
169
|
+
/** transition({ when }) `action` fields for the refetch commit. */
|
|
170
|
+
action?: TransitionWhenActionInput;
|
|
149
171
|
}): Promise<void> {
|
|
150
172
|
const src = opts?.interceptSourceUrl ?? null;
|
|
151
173
|
const navTx = createNavigationTransaction(
|
|
@@ -169,6 +191,7 @@ export function createServerActionBridge(
|
|
|
169
191
|
type: "action" as const,
|
|
170
192
|
...(src ? { interceptSourceUrl: src } : {}),
|
|
171
193
|
...(opts?.actionId !== undefined ? { actionId: opts.actionId } : {}),
|
|
194
|
+
...(opts?.action ? { action: opts.action } : {}),
|
|
172
195
|
},
|
|
173
196
|
);
|
|
174
197
|
} finally {
|
|
@@ -560,8 +583,22 @@ export function createServerActionBridge(
|
|
|
560
583
|
// Reconcile error segments with cached tree
|
|
561
584
|
const errorResult = reconcileErrorSegments(cachedSegments, segments);
|
|
562
585
|
|
|
586
|
+
// transition({ when }) decides the error-boundary commit too, with
|
|
587
|
+
// `action.error` set.
|
|
588
|
+
const errorGatedOff = decideActionGatedOff(
|
|
589
|
+
errorResult.segments,
|
|
590
|
+
metadata,
|
|
591
|
+
{
|
|
592
|
+
id,
|
|
593
|
+
formData: findActionFormData(args),
|
|
594
|
+
error:
|
|
595
|
+
returnValue && !returnValue.ok ? returnValue.data : undefined,
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
|
|
563
599
|
// Render the full tree with error segment merged with parent layouts
|
|
564
600
|
const errorTree = await renderSegments(errorResult.mainSegments, {
|
|
601
|
+
transitionGatedOff: errorGatedOff,
|
|
565
602
|
isAction: true,
|
|
566
603
|
interceptSegments:
|
|
567
604
|
errorResult.interceptSegments.length > 0
|
|
@@ -605,11 +642,13 @@ export function createServerActionBridge(
|
|
|
605
642
|
errorResult.segments,
|
|
606
643
|
currentHandleData,
|
|
607
644
|
);
|
|
645
|
+
store.rememberDisplayedEntry();
|
|
608
646
|
|
|
609
|
-
// Update UI with error boundary
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
647
|
+
// Update UI with error boundary: urgent when transition({ when })
|
|
648
|
+
// gated it off, like every gated-off commit.
|
|
649
|
+
const errorUpdate = { root: errorTree, metadata: metadata! };
|
|
650
|
+
if (errorGatedOff) onUpdate(errorUpdate);
|
|
651
|
+
else startTransition(() => onUpdate(errorUpdate));
|
|
613
652
|
|
|
614
653
|
// Throw the error so the action promise rejects
|
|
615
654
|
if (returnValue && !returnValue.ok) {
|
|
@@ -697,6 +736,13 @@ export function createServerActionBridge(
|
|
|
697
736
|
if (scenario.type !== "navigated-away") {
|
|
698
737
|
applyActionLocationState(handle, metadata?.locationState);
|
|
699
738
|
}
|
|
739
|
+
// transition({ when }) `action` for this action's commit, whichever
|
|
740
|
+
// lane applies it (normal, or a refetch below).
|
|
741
|
+
const whenAction: TransitionWhenActionInput = {
|
|
742
|
+
id,
|
|
743
|
+
formData: findActionFormData(args),
|
|
744
|
+
result: returnData,
|
|
745
|
+
};
|
|
700
746
|
|
|
701
747
|
switch (scenario.type) {
|
|
702
748
|
case "navigated-away": {
|
|
@@ -712,13 +758,15 @@ export function createServerActionBridge(
|
|
|
712
758
|
// Invalidation is deferred to finalizeAction(); here we only trigger
|
|
713
759
|
// the revalidation refetch of the new route (suppressed on keep).
|
|
714
760
|
if (!scenario.onInterceptRoute && !keepCache) {
|
|
715
|
-
refetchRoute({ actionId: id }).catch(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
761
|
+
refetchRoute({ actionId: id, action: whenAction }).catch(
|
|
762
|
+
(error) => {
|
|
763
|
+
if (isBackgroundSuppressible(error)) return;
|
|
764
|
+
console.error(
|
|
765
|
+
"[Browser] Background revalidation failed:",
|
|
766
|
+
error,
|
|
767
|
+
);
|
|
768
|
+
},
|
|
769
|
+
);
|
|
722
770
|
}
|
|
723
771
|
break;
|
|
724
772
|
}
|
|
@@ -730,6 +778,7 @@ export function createServerActionBridge(
|
|
|
730
778
|
await refetchRoute({
|
|
731
779
|
interceptSourceUrl: store.getInterceptSourceUrl(),
|
|
732
780
|
actionId: id,
|
|
781
|
+
action: whenAction,
|
|
733
782
|
});
|
|
734
783
|
}
|
|
735
784
|
break;
|
|
@@ -743,7 +792,11 @@ export function createServerActionBridge(
|
|
|
743
792
|
// resolving last must discharge a directive-free sibling's repair.
|
|
744
793
|
// See the keep row in docs/design/rango-state-cookie.md (the all-keep
|
|
745
794
|
// edge, and the benign re-mark-stale-after-refetch end-state delta).
|
|
746
|
-
await refetchRoute({
|
|
795
|
+
await refetchRoute({
|
|
796
|
+
interceptSourceUrl,
|
|
797
|
+
actionId: id,
|
|
798
|
+
action: whenAction,
|
|
799
|
+
});
|
|
747
800
|
break;
|
|
748
801
|
}
|
|
749
802
|
|
|
@@ -766,6 +819,7 @@ export function createServerActionBridge(
|
|
|
766
819
|
segments: segmentsToSend,
|
|
767
820
|
interceptSourceUrl,
|
|
768
821
|
actionId: id,
|
|
822
|
+
action: whenAction,
|
|
769
823
|
});
|
|
770
824
|
break;
|
|
771
825
|
}
|
|
@@ -791,8 +845,22 @@ export function createServerActionBridge(
|
|
|
791
845
|
}
|
|
792
846
|
|
|
793
847
|
case "normal": {
|
|
848
|
+
// transition({ when }) for the action commit (kind "action"): the
|
|
849
|
+
// page does not move, so `to` is `from`. False commits urgently (a
|
|
850
|
+
// re-suspending segment streams its loading()) and switches every
|
|
851
|
+
// <ViewTransition> class to "none"; true keeps the startTransition.
|
|
852
|
+
// This commit runs after awaits, outside the transition React opens
|
|
853
|
+
// for a form action / useActionState call, so React does not force
|
|
854
|
+
// a transition on it. The tree shape never changes, so
|
|
855
|
+
// useActionState and other client state survive (#995).
|
|
856
|
+
const gatedOff = decideActionGatedOff(
|
|
857
|
+
fullSegments,
|
|
858
|
+
metadata,
|
|
859
|
+
whenAction,
|
|
860
|
+
);
|
|
794
861
|
// Prepare new tree (await loader data resolution)
|
|
795
862
|
const newTree = await renderSegments(reconciled.mainSegments, {
|
|
863
|
+
transitionGatedOff: gatedOff,
|
|
796
864
|
isAction: true,
|
|
797
865
|
interceptSegments:
|
|
798
866
|
reconciled.interceptSegments.length > 0
|
|
@@ -832,10 +900,11 @@ export function createServerActionBridge(
|
|
|
832
900
|
fullSegments,
|
|
833
901
|
currentHandleData,
|
|
834
902
|
);
|
|
903
|
+
store.rememberDisplayedEntry(metadata?.routeName);
|
|
835
904
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
905
|
+
const actionUpdate = { root: newTree, metadata: metadata! };
|
|
906
|
+
if (gatedOff) onUpdate(actionUpdate);
|
|
907
|
+
else startTransition(() => onUpdate(actionUpdate));
|
|
839
908
|
// Invalidation deferred to finalizeAction() (runs after this caches
|
|
840
909
|
// the fresh segments), suppressed when the action called
|
|
841
910
|
// keepClientCache().
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side transition({ when }) decisions.
|
|
3
|
+
*
|
|
4
|
+
* A navigation is decided ONCE, at the first commit that presents its
|
|
5
|
+
* destination: the canonical commit for a server route, the optimistic swap
|
|
6
|
+
* for a clientUrls() cross-route navigation (the canonical commit then reuses
|
|
7
|
+
* the decision carried on its transaction), the history-cache restore or
|
|
8
|
+
* refetch commit for back/forward, the action commit (error lane included).
|
|
9
|
+
*
|
|
10
|
+
* The decision is navigation-wide: every distinct predicate (by function
|
|
11
|
+
* identity) declared by a committed segment, kept or re-sent (#989), runs, and
|
|
12
|
+
* any false gates the whole navigation off. It never changes the rendered
|
|
13
|
+
* tree's shape: every segment keeps its key and its <ViewTransition> element
|
|
14
|
+
* (segment-system.tsx renders each class "none"), and the commit is urgent
|
|
15
|
+
* instead of held (partial-update.ts). That is what keeps component state
|
|
16
|
+
* across when true -> false -> true (#995). A response carries no decision,
|
|
17
|
+
* so a reused prefetch is decided against the real source.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
ResolvedSegment,
|
|
22
|
+
RouteLocation,
|
|
23
|
+
TransitionWhenContext,
|
|
24
|
+
TransitionWhenFn,
|
|
25
|
+
TransitionWhenKind,
|
|
26
|
+
} from "../types/segments.js";
|
|
27
|
+
import { resolveTransitionWhen } from "../transition-when-ref.js";
|
|
28
|
+
import { makeIsAction } from "../router/is-action.js";
|
|
29
|
+
import { isInterceptSegment } from "./intercept-utils.js";
|
|
30
|
+
import type { NavigationStore } from "./types.js";
|
|
31
|
+
|
|
32
|
+
/** One side of the navigation before it is frozen into a RouteLocation. */
|
|
33
|
+
export interface RouteLocationInput {
|
|
34
|
+
url: string | URL;
|
|
35
|
+
params?: Readonly<Record<string, string>>;
|
|
36
|
+
routeName?: string;
|
|
37
|
+
state?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The action fields of a `kind: "action"` context. */
|
|
41
|
+
export interface TransitionWhenActionInput {
|
|
42
|
+
id?: string;
|
|
43
|
+
formData?: FormData;
|
|
44
|
+
result?: unknown;
|
|
45
|
+
error?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TransitionDecisionInput {
|
|
49
|
+
kind: TransitionWhenKind;
|
|
50
|
+
from: RouteLocationInput;
|
|
51
|
+
/** The destination; ignored for "action" and "revalidate" (`to` is `from`). */
|
|
52
|
+
to?: RouteLocationInput;
|
|
53
|
+
action?: TransitionWhenActionInput;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const EMPTY_PARAMS: Readonly<Record<string, string>> = Object.freeze({});
|
|
57
|
+
|
|
58
|
+
function createRouteLocation(input: RouteLocationInput): RouteLocation {
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
// A fresh URL: a predicate may not mutate the store's or the caller's.
|
|
61
|
+
url: new URL(
|
|
62
|
+
String(input.url),
|
|
63
|
+
typeof window !== "undefined"
|
|
64
|
+
? window.location.href
|
|
65
|
+
: "http://localhost/",
|
|
66
|
+
),
|
|
67
|
+
params: input.params ? Object.freeze({ ...input.params }) : EMPTY_PARAMS,
|
|
68
|
+
routeName: input.routeName,
|
|
69
|
+
state: input.state,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createTransitionWhenContext(
|
|
74
|
+
input: TransitionDecisionInput,
|
|
75
|
+
): TransitionWhenContext {
|
|
76
|
+
const from = createRouteLocation(input.from);
|
|
77
|
+
const inAction = input.kind === "action";
|
|
78
|
+
const { id, formData, result, error } = input.action ?? {};
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
kind: input.kind,
|
|
81
|
+
from,
|
|
82
|
+
to:
|
|
83
|
+
inAction || input.kind === "revalidate" || !input.to
|
|
84
|
+
? from
|
|
85
|
+
: createRouteLocation(input.to),
|
|
86
|
+
isAction: makeIsAction(id, inAction),
|
|
87
|
+
...(inAction && { action: Object.freeze({ id, formData, result, error }) }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run one predicate. A throw counts as false (the navigation does not hold)
|
|
93
|
+
* and is logged; it never fails the navigation.
|
|
94
|
+
*/
|
|
95
|
+
export function evaluateTransitionWhen(
|
|
96
|
+
fn: TransitionWhenFn,
|
|
97
|
+
ctx: TransitionWhenContext,
|
|
98
|
+
): boolean {
|
|
99
|
+
try {
|
|
100
|
+
return fn(ctx) !== false;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(
|
|
103
|
+
"[rango] transition({ when }) threw; the navigation does not hold.",
|
|
104
|
+
error,
|
|
105
|
+
);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Distinct predicates of the segments that declare one, in segment order. */
|
|
111
|
+
export function collectTransitionWhens(
|
|
112
|
+
segments: readonly ResolvedSegment[],
|
|
113
|
+
): Set<TransitionWhenFn> {
|
|
114
|
+
const fns = new Set<TransitionWhenFn>();
|
|
115
|
+
for (const segment of segments) {
|
|
116
|
+
const fn = resolveTransitionWhen(segment.transition?.when);
|
|
117
|
+
if (fn) fns.add(fn);
|
|
118
|
+
}
|
|
119
|
+
return fns;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Decide a commit: true when the navigation is gated off (some predicate
|
|
124
|
+
* returned false or threw). The context is built only when a predicate runs.
|
|
125
|
+
* An intercept commit is not evaluated: it never holds or animates
|
|
126
|
+
* (partial-update.ts shouldStartViewTransition). `extra` is a predicate not on
|
|
127
|
+
* a segment yet (the optimistic clientUrls() destination).
|
|
128
|
+
*/
|
|
129
|
+
export function decideTransitionGatedOff(
|
|
130
|
+
segments: readonly ResolvedSegment[],
|
|
131
|
+
input: () => TransitionDecisionInput,
|
|
132
|
+
extra?: TransitionWhenFn,
|
|
133
|
+
): boolean {
|
|
134
|
+
if (segments.some(isInterceptSegment)) return false;
|
|
135
|
+
const fns = collectTransitionWhens(segments);
|
|
136
|
+
if (extra) fns.add(extra);
|
|
137
|
+
if (fns.size === 0) return false;
|
|
138
|
+
const ctx = createTransitionWhenContext(input());
|
|
139
|
+
for (const fn of fns) if (!evaluateTransitionWhen(fn, ctx)) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Merge route params the way popstate restores them (per-segment params). */
|
|
144
|
+
export function mergeSegmentParams(
|
|
145
|
+
segments: readonly ResolvedSegment[],
|
|
146
|
+
): Record<string, string> {
|
|
147
|
+
const params: Record<string, string> = {};
|
|
148
|
+
for (const s of segments) if (s.params) Object.assign(params, s.params);
|
|
149
|
+
return params;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface CommitDecision {
|
|
153
|
+
/** Newer params / route name for the committed location (an action response's). */
|
|
154
|
+
from?: { params?: Record<string, string>; routeName?: string };
|
|
155
|
+
/** The destination, from the committed location; omit for "action" / "revalidate". */
|
|
156
|
+
to?: (from: RouteLocationInput) => RouteLocationInput;
|
|
157
|
+
action?: TransitionWhenActionInput;
|
|
158
|
+
extra?: TransitionWhenFn;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* decideTransitionGatedOff with `from` read from the store: call it before
|
|
163
|
+
* the commit moves the store. `state` is the live history.state, except for
|
|
164
|
+
* back/forward: at popstate history.state already belongs to the destination,
|
|
165
|
+
* so the entry being left comes from the store's per-entry memory (undefined
|
|
166
|
+
* after a reload, never guessed).
|
|
167
|
+
*/
|
|
168
|
+
export function decideCommitGatedOff(
|
|
169
|
+
store: NavigationStore,
|
|
170
|
+
segments: readonly ResolvedSegment[],
|
|
171
|
+
kind: TransitionWhenKind,
|
|
172
|
+
decision: CommitDecision = {},
|
|
173
|
+
): boolean {
|
|
174
|
+
return decideTransitionGatedOff(
|
|
175
|
+
segments,
|
|
176
|
+
() => {
|
|
177
|
+
const memory = store.getHistoryEntryMemory();
|
|
178
|
+
const from: RouteLocationInput = {
|
|
179
|
+
url: store.getSegmentState().currentUrl || window.location.href,
|
|
180
|
+
params:
|
|
181
|
+
decision.from?.params ??
|
|
182
|
+
mergeSegmentParams(
|
|
183
|
+
store.getCachedSegments(store.getHistoryKey())?.segments ?? [],
|
|
184
|
+
),
|
|
185
|
+
routeName: decision.from?.routeName ?? memory?.routeName,
|
|
186
|
+
state: kind === "pop" ? memory?.state : window.history.state,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
kind,
|
|
190
|
+
from,
|
|
191
|
+
to: decision.to?.(from),
|
|
192
|
+
action: decision.action,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
decision.extra,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The FormData argument a server action was called with, if any. */
|
|
200
|
+
export function findActionFormData(
|
|
201
|
+
args: readonly unknown[],
|
|
202
|
+
): FormData | undefined {
|
|
203
|
+
if (typeof FormData === "undefined") return undefined;
|
|
204
|
+
return args.find((arg): arg is FormData => arg instanceof FormData);
|
|
205
|
+
}
|
package/src/browser/types.ts
CHANGED
|
@@ -48,6 +48,12 @@ export interface RscMetadata {
|
|
|
48
48
|
resolvedIds?: string[];
|
|
49
49
|
/** Merged route params from the matched route */
|
|
50
50
|
params?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* The matched route's name (include name prefix applied), when the route is
|
|
53
|
+
* named. The browser keeps it per history entry for transition({ when })'s
|
|
54
|
+
* `from.routeName` / `to.routeName` (browser/transition-when.ts).
|
|
55
|
+
*/
|
|
56
|
+
routeName?: string;
|
|
51
57
|
/**
|
|
52
58
|
* State of named slots for this route match
|
|
53
59
|
* Key is slot name (e.g., "@modal"), value is slot state
|
|
@@ -276,6 +282,18 @@ export interface SegmentState {
|
|
|
276
282
|
currentSegmentIds: string[];
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
/**
|
|
286
|
+
* What the router remembers about one history entry for transition({ when })
|
|
287
|
+
* (NavigationStore.rememberDisplayedEntry).
|
|
288
|
+
*
|
|
289
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
290
|
+
*/
|
|
291
|
+
export interface HistoryEntryMemory {
|
|
292
|
+
readonly routeName: string | undefined;
|
|
293
|
+
/** The entry's `history.state` as last recorded. */
|
|
294
|
+
readonly state: unknown;
|
|
295
|
+
}
|
|
296
|
+
|
|
279
297
|
/**
|
|
280
298
|
* Navigation update emitted when UI should re-render
|
|
281
299
|
*
|
|
@@ -296,13 +314,67 @@ export interface NavigationUpdate {
|
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
/**
|
|
299
|
-
*
|
|
300
|
-
* -
|
|
301
|
-
*
|
|
317
|
+
* Plain object accepted as {@link PlainHistoryState}. The `any` string index
|
|
318
|
+
* admits interface-typed values (an `unknown` index would reject interfaces,
|
|
319
|
+
* which carry no implicit index signature). The `never` members reject:
|
|
320
|
+
* - a single typed entry (`GridState(value)` without the array) — its
|
|
321
|
+
* `__rsc_ls_*` fields would be spread onto history.state and
|
|
322
|
+
* `useLocationState(GridState)` would read `undefined`;
|
|
323
|
+
* - functions, including a location-state definition passed uncalled
|
|
324
|
+
* (`Symbol.hasInstance` comes from `Function.prototype`), which throw
|
|
325
|
+
* `DataCloneError`;
|
|
326
|
+
* - arrays and other iterables (`Symbol.iterator`), so an array must match the
|
|
327
|
+
* typed-entry or plain-array member instead;
|
|
328
|
+
* - built-ins tagged with `Symbol.toStringTag` (`Promise`, `WeakMap`,
|
|
329
|
+
* `WeakSet`), which throw `DataCloneError`. The cloneable tagged ones
|
|
330
|
+
* (`Map`, `Set`, `ArrayBuffer`, typed arrays) have their own members.
|
|
331
|
+
*
|
|
332
|
+
* Members are not checked: a function, symbol, or React element inside a
|
|
333
|
+
* plain object still compiles, as does a top-level `ReactElement` or DOM node
|
|
334
|
+
* (an object with none of the keys above).
|
|
335
|
+
*/
|
|
336
|
+
export interface PlainHistoryObject {
|
|
337
|
+
readonly [key: string]: any;
|
|
338
|
+
readonly __rsc_ls_key?: never;
|
|
339
|
+
readonly __rsc_ls_value?: never;
|
|
340
|
+
readonly __rsc_ls_lazy?: never;
|
|
341
|
+
readonly [Symbol.iterator]?: never;
|
|
342
|
+
readonly [Symbol.hasInstance]?: never;
|
|
343
|
+
readonly [Symbol.toStringTag]?: never;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Plain (untyped) navigation state, stored under `history.state.state` and
|
|
348
|
+
* read with `useLocationState<T>()`. A structured-clone-safe value: primitives,
|
|
349
|
+
* arrays, `Map`/`Set`, `ArrayBuffer`, typed arrays, and plain objects. Excludes
|
|
350
|
+
* functions, symbols, `Promise`/`WeakMap`/`WeakSet`, and objects carrying
|
|
351
|
+
* `__rsc_ls_*` keys (a typed entry passed without its array). Arrays, `Map`,
|
|
352
|
+
* and `Set` are checked element by element; plain object members are not
|
|
353
|
+
* checked (see {@link PlainHistoryObject}).
|
|
354
|
+
*/
|
|
355
|
+
export type PlainHistoryState =
|
|
356
|
+
| string
|
|
357
|
+
| number
|
|
358
|
+
| boolean
|
|
359
|
+
| bigint
|
|
360
|
+
| null
|
|
361
|
+
| undefined
|
|
362
|
+
| readonly PlainHistoryState[]
|
|
363
|
+
| ReadonlyMap<PlainHistoryState, PlainHistoryState>
|
|
364
|
+
| ReadonlySet<PlainHistoryState>
|
|
365
|
+
| ArrayBuffer
|
|
366
|
+
| ArrayBufferView
|
|
367
|
+
| PlainHistoryObject;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* State value for `router.push()` / `router.replace()` / `<Link state>`.
|
|
371
|
+
* - `readonly LocationStateEntry[]`: typed entries from `createLocationState()`
|
|
372
|
+
* definitions, e.g. `[GridState({ count: 3 })]` (recommended)
|
|
373
|
+
* - {@link PlainHistoryState}: plain structured-clone-safe state
|
|
302
374
|
*/
|
|
303
375
|
export type HistoryState =
|
|
304
|
-
| import("./react/location-state-shared.js").LocationStateEntry[]
|
|
305
|
-
|
|
|
376
|
+
| readonly import("./react/location-state-shared.js").LocationStateEntry[]
|
|
377
|
+
| PlainHistoryState;
|
|
306
378
|
|
|
307
379
|
/**
|
|
308
380
|
* Options for navigation operations
|
|
@@ -329,6 +401,22 @@ export interface NavigateOptions {
|
|
|
329
401
|
* ```
|
|
330
402
|
*/
|
|
331
403
|
revalidate?: boolean;
|
|
404
|
+
/**
|
|
405
|
+
* Set to `false` to present this navigation without a transition: the
|
|
406
|
+
* commit is urgent and every `<ViewTransition>` class resolves to `"none"`,
|
|
407
|
+
* the same result as a `transition({ when })` predicate returning `false`.
|
|
408
|
+
* No `when` predicate is called. Back/forward, action and revalidation
|
|
409
|
+
* commits are unaffected (they are not started by this call).
|
|
410
|
+
*
|
|
411
|
+
* @default true (the route's transition config applies)
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```tsx
|
|
415
|
+
* router.push("/photos/2", { transition: false });
|
|
416
|
+
* <Link to="/photos/2" transition={false}>Next</Link>
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
transition?: boolean;
|
|
332
420
|
/**
|
|
333
421
|
* State to pass to history.pushState/replaceState
|
|
334
422
|
* Accessible via useLocationState() hook.
|
|
@@ -350,15 +438,25 @@ export interface NavigateOptions {
|
|
|
350
438
|
* // Plain static state
|
|
351
439
|
* navigate("/product", { state: { from: "list" } });
|
|
352
440
|
*
|
|
353
|
-
* //
|
|
354
|
-
* navigate("/product", { state: ()
|
|
441
|
+
* // Compile errors (and a dev-mode runtime error for untyped callers):
|
|
442
|
+
* navigate("/product", { state: ProductState(p) }); // entry without the array
|
|
443
|
+
* navigate("/product", { state: [ProductState] }); // definition not called
|
|
355
444
|
* ```
|
|
356
445
|
*/
|
|
357
446
|
state?: HistoryState;
|
|
358
447
|
}
|
|
359
448
|
|
|
360
|
-
/**
|
|
361
|
-
|
|
449
|
+
/**
|
|
450
|
+
* @internal Extended options used only within the navigation bridge. `state`
|
|
451
|
+
* is widened to `unknown`: the redirect lanes pass state already resolved to
|
|
452
|
+
* a flat `history.state` record, and in dev Link passes an uncalled
|
|
453
|
+
* definition through to resolveNavigationState's check.
|
|
454
|
+
*/
|
|
455
|
+
export interface NavigateOptionsInternal extends Omit<
|
|
456
|
+
NavigateOptions,
|
|
457
|
+
"state"
|
|
458
|
+
> {
|
|
459
|
+
state?: unknown;
|
|
362
460
|
/** Skip segment cache (used by redirect-with-state to force re-render) */
|
|
363
461
|
_skipCache?: boolean;
|
|
364
462
|
}
|
|
@@ -465,6 +563,22 @@ export interface NavigationStore {
|
|
|
465
563
|
// History-based segment cache (for back/forward navigation and partial merging)
|
|
466
564
|
getHistoryKey(): string;
|
|
467
565
|
setHistoryKey(key: string): void;
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Per-history-entry memory for transition({ when }) sources: record the
|
|
569
|
+
* entry on screen (keyed by its `history.state.key`) with its current
|
|
570
|
+
* `history.state` and, when given, its route name (kept from the last
|
|
571
|
+
* record otherwise; every push/replace creates a new key, so a stale name
|
|
572
|
+
* never carries to another entry). Called after every commit, restore and
|
|
573
|
+
* state merge.
|
|
574
|
+
*/
|
|
575
|
+
rememberDisplayedEntry(routeName?: string): void;
|
|
576
|
+
/**
|
|
577
|
+
* The memory of the entry on screen, or of `entryKey`. At popstate
|
|
578
|
+
* history.state already belongs to the destination, so back/forward reads
|
|
579
|
+
* the entry being LEFT from here. In-memory only: empty after a reload.
|
|
580
|
+
*/
|
|
581
|
+
getHistoryEntryMemory(entryKey?: string): HistoryEntryMemory | undefined;
|
|
468
582
|
/** Monotonic token of the most recently committed navigation. */
|
|
469
583
|
getNavInstance(): number;
|
|
470
584
|
cacheSegmentsForHistory(
|
|
@@ -631,7 +745,7 @@ export interface ServerActionBridgeConfig {
|
|
|
631
745
|
* Navigation bridge for handling client-side navigation
|
|
632
746
|
*/
|
|
633
747
|
export interface NavigationBridge {
|
|
634
|
-
navigate(url: string, options?:
|
|
748
|
+
navigate(url: string, options?: NavigateOptionsInternal): Promise<void>;
|
|
635
749
|
refresh(): Promise<void>;
|
|
636
750
|
handlePopstate(): Promise<void>;
|
|
637
751
|
registerLinkInterception(): () => void;
|