@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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/AGENTS.md +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -4,13 +4,15 @@ import type {
|
|
|
4
4
|
RscPayload,
|
|
5
5
|
} from "./types.js";
|
|
6
6
|
import { createPartialUpdater } from "./partial-update.js";
|
|
7
|
+
import { enterActionFence, exitActionFence } from "./action-fence.js";
|
|
8
|
+
import { KEEP_CACHE_HEADER } from "./cookie-name.js";
|
|
7
9
|
import { createNavigationTransaction } from "./navigation-transaction.js";
|
|
8
10
|
import {
|
|
9
11
|
reconcileSegments,
|
|
10
12
|
reconcileErrorSegments,
|
|
11
13
|
} from "./segment-reconciler.js";
|
|
12
14
|
import { startTransition } from "react";
|
|
13
|
-
import type { EventController } from "./event-controller.js";
|
|
15
|
+
import type { EventController, ActionHandle } from "./event-controller.js";
|
|
14
16
|
import {
|
|
15
17
|
toNetworkError,
|
|
16
18
|
emitNetworkError,
|
|
@@ -21,11 +23,16 @@ import {
|
|
|
21
23
|
isBrowserDebugEnabled,
|
|
22
24
|
startBrowserTransaction,
|
|
23
25
|
} from "./logging.js";
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
validateRedirectOrigin,
|
|
28
|
+
validateExternalRedirect,
|
|
29
|
+
} from "./validate-redirect-origin.js";
|
|
25
30
|
import {
|
|
26
31
|
extractRscHeaderUrl,
|
|
27
32
|
emptyResponse,
|
|
33
|
+
handleReloadHeader,
|
|
28
34
|
teeWithCompletion,
|
|
35
|
+
isForeignRouterId,
|
|
29
36
|
} from "./response-adapter.js";
|
|
30
37
|
import { mergeLocationState } from "./history-state.js";
|
|
31
38
|
import { classifyActionOutcome } from "./action-coordinator.js";
|
|
@@ -51,6 +58,27 @@ export interface ServerActionBridgeConfigWithController extends ServerActionBrid
|
|
|
51
58
|
) => Promise<void>;
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Merge an action's location-state payload into history.state, restricted to
|
|
63
|
+
* the keys this action is entitled to write. claimLocationState enforces
|
|
64
|
+
* last-initiated-wins for same-key concurrent writes (a later-initiated sibling
|
|
65
|
+
* keeps its value even if this action's response settles afterward); distinct
|
|
66
|
+
* keys from every concurrent action survive. Arbitration is scoped to the
|
|
67
|
+
* action's cohort (captured at startAction), so a newer action on another entry
|
|
68
|
+
* cannot suppress this one. No-op when nothing was set or all keys were already
|
|
69
|
+
* claimed by a later-initiated sibling in the cohort.
|
|
70
|
+
*/
|
|
71
|
+
function applyActionLocationState(
|
|
72
|
+
handle: ActionHandle,
|
|
73
|
+
locationState: Record<string, unknown> | undefined,
|
|
74
|
+
): void {
|
|
75
|
+
if (!locationState) return;
|
|
76
|
+
const winning = handle.claimLocationState(locationState);
|
|
77
|
+
if (Object.keys(winning).length > 0) {
|
|
78
|
+
mergeLocationState(winning);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
/**
|
|
55
83
|
* Create a server action bridge for handling RSC server actions
|
|
56
84
|
*
|
|
@@ -77,6 +105,26 @@ export function createServerActionBridge(
|
|
|
77
105
|
onNavigate,
|
|
78
106
|
} = config;
|
|
79
107
|
|
|
108
|
+
// SPA-navigate when onNavigate is set, else hard-reload. state is omitted (not
|
|
109
|
+
// passed as undefined) to match the header path's prior call shape.
|
|
110
|
+
// Callers pass an already same-origin-validated url; the hard-reload fallback
|
|
111
|
+
// re-validates defensively so this leaf cannot become an open redirect if a
|
|
112
|
+
// future caller forgets (the SPA path validates inside the navigation bridge).
|
|
113
|
+
async function dispatchRedirect(url: string, state?: unknown): Promise<void> {
|
|
114
|
+
if (onNavigate) {
|
|
115
|
+
await onNavigate(url, {
|
|
116
|
+
...(state !== undefined ? { state } : {}),
|
|
117
|
+
replace: true,
|
|
118
|
+
_skipCache: true,
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
122
|
+
if (safe) {
|
|
123
|
+
window.location.href = safe;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
80
128
|
let isRegistered = false;
|
|
81
129
|
|
|
82
130
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -138,14 +186,57 @@ export function createServerActionBridge(
|
|
|
138
186
|
const locationKey = window.history.state?.key;
|
|
139
187
|
log("action start", { id, argsCount: args.length });
|
|
140
188
|
|
|
141
|
-
// Start action in event controller - handles lifecycle tracking
|
|
142
|
-
|
|
189
|
+
// Start action in event controller - handles lifecycle tracking. The
|
|
190
|
+
// current history key is the action's cohort: location-state arbitration is
|
|
191
|
+
// scoped to it so a later action on a different entry cannot suppress this
|
|
192
|
+
// one (and vice versa).
|
|
193
|
+
const handle = eventController.startAction(id, args, locationKey);
|
|
194
|
+
// Whether the action's response carried the keepClientCache() directive.
|
|
195
|
+
// Set when the response arrives; gates the deferred invalidation below.
|
|
196
|
+
let keepCache = false;
|
|
197
|
+
// Whether a Response actually settled from the network (the server saw the
|
|
198
|
+
// request). Set true as the first statement in the fetch .then() below.
|
|
199
|
+
// Gates the automatic invalidation: a pre-dispatch failure (encodeReply
|
|
200
|
+
// throw or a fetch rejection — server unreachable/DNS/connection refused)
|
|
201
|
+
// leaves this false, so finalizeAction() must NOT invalidate or broadcast —
|
|
202
|
+
// nothing reached the server, so nothing could have mutated. A failed Flight
|
|
203
|
+
// DECODE after the response arrived keeps it true (the mutation may have
|
|
204
|
+
// committed, so invalidating the now-possibly-stale client cache is correct).
|
|
205
|
+
let responseReceived = false;
|
|
206
|
+
// Single deferred invalidation + fence release, run exactly ONCE however the
|
|
207
|
+
// action terminates (normal, redirect, error, abort, intercept, concurrent).
|
|
208
|
+
// This replaces main's eager clear at action start: every directive-free
|
|
209
|
+
// action invalidates once; keepClientCache() suppresses only the automatic
|
|
210
|
+
// invalidation, so a concurrent directive-free action still invalidates via
|
|
211
|
+
// its own latch. Latched so the finally AND the early SPA-redirect returns
|
|
212
|
+
// (whose Flight stream never settles) can both call it safely.
|
|
213
|
+
let actionFinalized = false;
|
|
214
|
+
// skipInvalidation: the version-mismatch reload terminal released nothing
|
|
215
|
+
// server-side, so it releases the fence without invalidating.
|
|
216
|
+
const finalizeAction = (skipInvalidation = false): void => {
|
|
217
|
+
if (actionFinalized) return;
|
|
218
|
+
actionFinalized = true;
|
|
219
|
+
// finally so a throw in invalidation cannot leak the fence (latch is set).
|
|
220
|
+
try {
|
|
221
|
+
// responseReceived gates the automatic invalidation: a pre-dispatch
|
|
222
|
+
// failure (serialize throw / fetch reject) never reached the server, so
|
|
223
|
+
// marking the cache stale + broadcasting cross-tab would be spurious.
|
|
224
|
+
if (responseReceived && !keepCache && !skipInvalidation) {
|
|
225
|
+
store.markCacheAsStaleAndBroadcast();
|
|
226
|
+
}
|
|
227
|
+
} finally {
|
|
228
|
+
exitActionFence();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
143
231
|
try {
|
|
144
232
|
const segmentState = store.getSegmentState();
|
|
145
233
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
store
|
|
234
|
+
// Raise the action fence (replaces the old eager clear). Nothing is wiped,
|
|
235
|
+
// rotated, or broadcast yet: navigations during the flight fetch fresh
|
|
236
|
+
// (no-store) and popstate is treated as SWR, but the decision to
|
|
237
|
+
// invalidate is deferred to the response so a no-op action (keepClientCache)
|
|
238
|
+
// can leave the caches and the jar untouched.
|
|
239
|
+
enterActionFence();
|
|
149
240
|
|
|
150
241
|
// Create temporary references for serialization
|
|
151
242
|
const temporaryReferences = deps.createTemporaryReferenceSet();
|
|
@@ -217,23 +308,34 @@ export function createServerActionBridge(
|
|
|
217
308
|
body: encodedBody,
|
|
218
309
|
signal: fetchAbort.signal,
|
|
219
310
|
}).then(async (response) => {
|
|
311
|
+
// A settled fetch promise means the request reached the server and a
|
|
312
|
+
// Response came back (true for 2xx, 4xx, AND 5xx — fetch only rejects
|
|
313
|
+
// on network-layer failure, never on HTTP status). Record it as the
|
|
314
|
+
// first statement so every downstream terminal can invalidate; a
|
|
315
|
+
// pre-dispatch failure never gets here and stays gated out.
|
|
316
|
+
responseReceived = true;
|
|
220
317
|
// Response arrived — disconnect fetch abort from handle abort so
|
|
221
318
|
// abortAllActions() doesn't disrupt the in-progress Flight stream.
|
|
222
319
|
handle.signal.removeEventListener("abort", onHandleAbort);
|
|
223
320
|
|
|
321
|
+
// Did the action call keepClientCache()? If so the deferred invalidation
|
|
322
|
+
// below is suppressed for THIS action (a concurrent directive-free
|
|
323
|
+
// action still invalidates via its own response).
|
|
324
|
+
keepCache = response.headers.get(KEEP_CACHE_HEADER) === "1";
|
|
325
|
+
|
|
224
326
|
// Check for version mismatch - server wants us to reload
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
327
|
+
const reloadResult = handleReloadHeader(response, {
|
|
328
|
+
onBlocked: resolveStreamComplete,
|
|
329
|
+
onReload: (url) => {
|
|
330
|
+
log("version mismatch on action, reloading", { reloadUrl: url });
|
|
331
|
+
// Never-settling terminal (navigates away), so the finally never
|
|
332
|
+
// runs: release the fence here. skipInvalidation — the mismatch
|
|
333
|
+
// short-circuits the action server-side, so nothing mutated and a
|
|
334
|
+
// broadcast would only risk hard-reloading a sibling mid-task.
|
|
335
|
+
finalizeAction(true);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
if (reloadResult) return reloadResult;
|
|
237
339
|
|
|
238
340
|
// Simple redirect from action (no state, no RSC payload).
|
|
239
341
|
// Short-circuits before createFromFetch — no Flight deserialization needed.
|
|
@@ -243,14 +345,11 @@ export function createServerActionBridge(
|
|
|
243
345
|
if (redirect && redirect !== "blocked" && !handle.signal.aborted) {
|
|
244
346
|
log("action simple redirect", { url: redirect.url });
|
|
245
347
|
handle.complete(undefined);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
} else {
|
|
252
|
-
window.location.href = redirect.url;
|
|
253
|
-
}
|
|
348
|
+
// This path returns a never-settling promise, so the finally never
|
|
349
|
+
// runs: invalidate + release the fence here (the mutation committed
|
|
350
|
+
// and we're navigating away). Latched, so the finally is a no-op.
|
|
351
|
+
finalizeAction();
|
|
352
|
+
await dispatchRedirect(redirect.url);
|
|
254
353
|
return new Promise<Response>(() => {});
|
|
255
354
|
}
|
|
256
355
|
if (redirect === "blocked") {
|
|
@@ -258,6 +357,29 @@ export function createServerActionBridge(
|
|
|
258
357
|
return emptyResponse();
|
|
259
358
|
}
|
|
260
359
|
|
|
360
|
+
// Integrity check (pre-decode): a foreign app's action response must
|
|
361
|
+
// not be decoded + applied here. This is the one decode-and-apply path
|
|
362
|
+
// the post-decode partial-update guard does NOT cover (the action
|
|
363
|
+
// bridge has its own createFromFetch -> onUpdate). Ordered after the
|
|
364
|
+
// reload/redirect handlers, which steer control responses first.
|
|
365
|
+
// Reloads via window.location.reload() rather than navigating to a
|
|
366
|
+
// target (as the navigation-client guard does): an action has no
|
|
367
|
+
// navigation target, so reloading the current URL re-syncs the
|
|
368
|
+
// document against the server-applied action effect.
|
|
369
|
+
if (
|
|
370
|
+
!handle.signal.aborted &&
|
|
371
|
+
isForeignRouterId(response, store.getRouterId?.())
|
|
372
|
+
) {
|
|
373
|
+
log("action router id mismatch, reloading to re-sync");
|
|
374
|
+
handle.complete(undefined);
|
|
375
|
+
resolveStreamComplete();
|
|
376
|
+
// Never-settling return: release the fence before the reload (the
|
|
377
|
+
// reload resets module state anyway, but stay balanced). Latched.
|
|
378
|
+
finalizeAction();
|
|
379
|
+
window.location.reload();
|
|
380
|
+
return new Promise<Response>(() => {});
|
|
381
|
+
}
|
|
382
|
+
|
|
261
383
|
// Start streaming immediately when response arrives
|
|
262
384
|
if (!handle.signal.aborted) {
|
|
263
385
|
streamingToken = handle.startStreaming();
|
|
@@ -328,6 +450,27 @@ export function createServerActionBridge(
|
|
|
328
450
|
// Check handle.signal.aborted to avoid redirecting from a stale action
|
|
329
451
|
// when the user has already navigated away.
|
|
330
452
|
if (metadata?.redirect && !handle.signal.aborted) {
|
|
453
|
+
// Explicit off-host redirect (redirect(url, { external: true })):
|
|
454
|
+
// hard-navigate, but still scheme-validate (http/https only). external
|
|
455
|
+
// waives the same-origin check, NOT scheme safety, so a forged payload
|
|
456
|
+
// carrying a javascript:/data: URL cannot script via location.assign.
|
|
457
|
+
if (metadata.redirect.external) {
|
|
458
|
+
const externalUrl = validateExternalRedirect(
|
|
459
|
+
metadata.redirect.url,
|
|
460
|
+
window.location.origin,
|
|
461
|
+
);
|
|
462
|
+
if (!externalUrl) {
|
|
463
|
+
log("blocked external action redirect payload", {
|
|
464
|
+
url: metadata.redirect.url,
|
|
465
|
+
});
|
|
466
|
+
handle.complete(returnValue?.data);
|
|
467
|
+
return returnValue?.data;
|
|
468
|
+
}
|
|
469
|
+
log("external action redirect", { url: externalUrl });
|
|
470
|
+
handle.complete(returnValue?.data);
|
|
471
|
+
window.location.assign(externalUrl);
|
|
472
|
+
return returnValue?.data;
|
|
473
|
+
}
|
|
331
474
|
const redirectUrl = validateRedirectOrigin(
|
|
332
475
|
metadata.redirect.url,
|
|
333
476
|
window.location.origin,
|
|
@@ -339,18 +482,9 @@ export function createServerActionBridge(
|
|
|
339
482
|
handle.complete(returnValue?.data);
|
|
340
483
|
return returnValue?.data;
|
|
341
484
|
}
|
|
342
|
-
const redirectState = metadata.locationState;
|
|
343
485
|
log("action redirect", { url: redirectUrl });
|
|
344
486
|
handle.complete(returnValue?.data);
|
|
345
|
-
|
|
346
|
-
await onNavigate(redirectUrl, {
|
|
347
|
-
state: redirectState,
|
|
348
|
-
replace: true,
|
|
349
|
-
_skipCache: true,
|
|
350
|
-
});
|
|
351
|
-
} else {
|
|
352
|
-
window.location.href = redirectUrl;
|
|
353
|
-
}
|
|
487
|
+
await dispatchRedirect(redirectUrl, metadata.locationState);
|
|
354
488
|
return returnValue?.data;
|
|
355
489
|
}
|
|
356
490
|
|
|
@@ -517,6 +651,20 @@ export function createServerActionBridge(
|
|
|
517
651
|
currentInterceptSource: store.getInterceptSourceUrl(),
|
|
518
652
|
});
|
|
519
653
|
|
|
654
|
+
// Apply server-set location state exhaustively here, as a successful-
|
|
655
|
+
// response effect — the terminal switch below decides rendering/refetch,
|
|
656
|
+
// not whether this metadata survives (every refetch path is storeOnly and
|
|
657
|
+
// never writes history.state). Gated on NOT navigated-away: the classifier
|
|
658
|
+
// treats either a pathname OR history-key change as diversion, so this
|
|
659
|
+
// both honors the "diverted state is dropped" contract AND prevents a
|
|
660
|
+
// cross-entry write (mergeLocationState writes the CURRENT entry, which a
|
|
661
|
+
// navigated-away action no longer owns). Done before the switch (and
|
|
662
|
+
// before the normal branch's async renderSegments) so a slow render racing
|
|
663
|
+
// a navigation cannot drop it.
|
|
664
|
+
if (scenario.type !== "navigated-away") {
|
|
665
|
+
applyActionLocationState(handle, metadata?.locationState);
|
|
666
|
+
}
|
|
667
|
+
|
|
520
668
|
switch (scenario.type) {
|
|
521
669
|
case "navigated-away": {
|
|
522
670
|
log("user navigated away during action", {
|
|
@@ -528,8 +676,9 @@ export function createServerActionBridge(
|
|
|
528
676
|
handle.clearConsolidation();
|
|
529
677
|
|
|
530
678
|
if (scenario.historyKeyChanged) {
|
|
531
|
-
|
|
532
|
-
|
|
679
|
+
// Invalidation is deferred to finalizeAction(); here we only trigger
|
|
680
|
+
// the revalidation refetch of the new route (suppressed on keep).
|
|
681
|
+
if (!scenario.onInterceptRoute && !keepCache) {
|
|
533
682
|
refetchRoute().catch((error) => {
|
|
534
683
|
if (isBackgroundSuppressible(error)) return;
|
|
535
684
|
console.error(
|
|
@@ -541,11 +690,14 @@ export function createServerActionBridge(
|
|
|
541
690
|
break;
|
|
542
691
|
}
|
|
543
692
|
|
|
544
|
-
// Same history key but different pathname - safe to refetch current
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
693
|
+
// Same history key but different pathname - safe to refetch current
|
|
694
|
+
// route. Invalidation is deferred to finalizeAction(); here we only
|
|
695
|
+
// trigger the revalidation refetch (suppressed on keep).
|
|
696
|
+
if (!keepCache) {
|
|
697
|
+
await refetchRoute({
|
|
698
|
+
interceptSourceUrl: store.getInterceptSourceUrl(),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
549
701
|
break;
|
|
550
702
|
}
|
|
551
703
|
|
|
@@ -553,8 +705,11 @@ export function createServerActionBridge(
|
|
|
553
705
|
console.warn(
|
|
554
706
|
`[Browser] Missing segments after action (HMR detected), refetching...`,
|
|
555
707
|
);
|
|
708
|
+
// Repair (not revalidation), so ungated on keepCache: a keep action
|
|
709
|
+
// resolving last must discharge a directive-free sibling's repair.
|
|
710
|
+
// See the keep row in docs/design/rango-state-cookie.md (the all-keep
|
|
711
|
+
// edge, and the benign re-mark-stale-after-refetch end-state delta).
|
|
556
712
|
await refetchRoute({ interceptSourceUrl });
|
|
557
|
-
store.broadcastCacheInvalidation();
|
|
558
713
|
break;
|
|
559
714
|
}
|
|
560
715
|
|
|
@@ -562,7 +717,8 @@ export function createServerActionBridge(
|
|
|
562
717
|
log("consolidation fetch needed", {
|
|
563
718
|
segmentIds: scenario.segmentIds,
|
|
564
719
|
});
|
|
565
|
-
//
|
|
720
|
+
// Location state already applied above (pre-switch). Calculate
|
|
721
|
+
// segments to send (exclude the ones we want fresh).
|
|
566
722
|
const currentSegmentIds = store.getSegmentState().currentSegmentIds;
|
|
567
723
|
const segmentsToSend = currentSegmentIds.filter(
|
|
568
724
|
(sid) => !scenario.segmentIds.includes(sid),
|
|
@@ -571,11 +727,11 @@ export function createServerActionBridge(
|
|
|
571
727
|
// Clear consolidation tracking before fetch
|
|
572
728
|
handle.clearConsolidation();
|
|
573
729
|
|
|
730
|
+
// Ungated on keepCache, same as hmr-missing above (see the keep row).
|
|
574
731
|
await refetchRoute({
|
|
575
732
|
segments: segmentsToSend,
|
|
576
733
|
interceptSourceUrl,
|
|
577
734
|
});
|
|
578
|
-
store.broadcastCacheInvalidation();
|
|
579
735
|
break;
|
|
580
736
|
}
|
|
581
737
|
|
|
@@ -586,6 +742,8 @@ export function createServerActionBridge(
|
|
|
586
742
|
// Only update store if history key hasn't changed (user didn't navigate away)
|
|
587
743
|
const currentKeyNow = store.getHistoryKey();
|
|
588
744
|
if (currentKeyNow === currentKey) {
|
|
745
|
+
// Location state already applied above (pre-switch); this action's
|
|
746
|
+
// UI render is skipped because a later sibling consolidates.
|
|
589
747
|
store.setSegmentIds(matched);
|
|
590
748
|
const currentHandleData = eventController.getHandleState().data;
|
|
591
749
|
store.cacheSegmentsForHistory(
|
|
@@ -625,13 +783,7 @@ export function createServerActionBridge(
|
|
|
625
783
|
onUpdate({ root: newTree, metadata: metadata! });
|
|
626
784
|
});
|
|
627
785
|
|
|
628
|
-
//
|
|
629
|
-
const actionLocationState = metadata?.locationState;
|
|
630
|
-
if (actionLocationState) {
|
|
631
|
-
mergeLocationState(actionLocationState);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Update store state
|
|
786
|
+
// Location state already applied above (pre-switch). Update store.
|
|
635
787
|
store.setSegmentIds(matched);
|
|
636
788
|
const currentHandleData = eventController.getHandleState().data;
|
|
637
789
|
store.cacheSegmentsForHistory(
|
|
@@ -639,7 +791,9 @@ export function createServerActionBridge(
|
|
|
639
791
|
fullSegments,
|
|
640
792
|
currentHandleData,
|
|
641
793
|
);
|
|
642
|
-
|
|
794
|
+
// Invalidation deferred to finalizeAction() (runs after this caches
|
|
795
|
+
// the fresh segments), suppressed when the action called
|
|
796
|
+
// keepClientCache().
|
|
643
797
|
break;
|
|
644
798
|
}
|
|
645
799
|
}
|
|
@@ -647,6 +801,11 @@ export function createServerActionBridge(
|
|
|
647
801
|
handle.complete(returnData);
|
|
648
802
|
return returnData;
|
|
649
803
|
} finally {
|
|
804
|
+
// The single deferred invalidation + fence release for this action. Runs
|
|
805
|
+
// for every terminal that settles (normal, navigated-away, error, abort,
|
|
806
|
+
// intercept, concurrent); the SPA-redirect paths above already ran it.
|
|
807
|
+
// Latched, so it fires exactly once.
|
|
808
|
+
finalizeAction();
|
|
650
809
|
handle[Symbol.dispose]();
|
|
651
810
|
}
|
|
652
811
|
}
|
package/src/browser/types.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
|
14
14
|
export interface RscPayload<TMetadata = RscMetadata> {
|
|
15
15
|
metadata?: TMetadata;
|
|
16
16
|
returnValue?: ActionResult;
|
|
17
|
-
formState?: unknown;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -32,13 +31,21 @@ export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
|
32
31
|
export interface RscMetadata {
|
|
33
32
|
pathname: string;
|
|
34
33
|
segments: ResolvedSegment[];
|
|
35
|
-
/** Router instance ID
|
|
36
|
-
*
|
|
34
|
+
/** Router instance ID — the current app's identity. A mismatch with the
|
|
35
|
+
* client's id (sent as _rsc_rid) is detected server-side and answered with
|
|
36
|
+
* X-RSC-Reload (full document load), so the client never swaps apps
|
|
37
|
+
* in-session; within a session this always equals the current app. */
|
|
37
38
|
routerId?: string;
|
|
38
39
|
isPartial?: boolean;
|
|
39
40
|
isError?: boolean;
|
|
40
41
|
matched?: string[];
|
|
41
42
|
diff?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* All segment ids re-resolved on the server, including null-component
|
|
45
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
46
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
47
|
+
*/
|
|
48
|
+
resolvedIds?: string[];
|
|
42
49
|
/** Merged route params from the matched route */
|
|
43
50
|
params?: Record<string, string>;
|
|
44
51
|
/**
|
|
@@ -63,6 +70,12 @@ export interface RscMetadata {
|
|
|
63
70
|
* Sent on initial render so the browser can configure its cache duration.
|
|
64
71
|
*/
|
|
65
72
|
prefetchCacheTTL?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Server-resolved rango state cookie name (`{prefix}_{routerId}`). The client
|
|
75
|
+
* reads it verbatim and binds the rango state cookie to it; composition
|
|
76
|
+
* happens only server-side.
|
|
77
|
+
*/
|
|
78
|
+
stateCookieName?: string;
|
|
66
79
|
/**
|
|
67
80
|
* Theme configuration from router.
|
|
68
81
|
* Included when theme is enabled in router config.
|
|
@@ -77,8 +90,18 @@ export interface RscMetadata {
|
|
|
77
90
|
basename?: string;
|
|
78
91
|
/** Whether connection warmup is enabled */
|
|
79
92
|
warmupEnabled?: boolean;
|
|
80
|
-
/**
|
|
81
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Whether the client should hydrate inside React.StrictMode. Carried on the
|
|
95
|
+
* initial full-render payload only; the browser entry reads it once at
|
|
96
|
+
* hydration. Defaults to true on the client when omitted.
|
|
97
|
+
*/
|
|
98
|
+
strictMode?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Server-side redirect with optional state (for partial requests).
|
|
101
|
+
* `external: true` (from redirect(url, { external: true })) tells the client
|
|
102
|
+
* to hard-navigate to an off-host target instead of validating same-origin.
|
|
103
|
+
*/
|
|
104
|
+
redirect?: { url: string; external?: boolean };
|
|
82
105
|
/** Server-set location state to include in history.pushState */
|
|
83
106
|
locationState?: Record<string, unknown>;
|
|
84
107
|
}
|
|
@@ -179,6 +202,15 @@ export interface TrackedActionState {
|
|
|
179
202
|
result: unknown | null;
|
|
180
203
|
}
|
|
181
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The value returned by {@link useAction} when called without a selector.
|
|
207
|
+
*
|
|
208
|
+
* This is the stable, public name for the action-state shape; consumers can
|
|
209
|
+
* name it in their own signatures (e.g. a wrapper hook). It aliases the
|
|
210
|
+
* internal {@link TrackedActionState}.
|
|
211
|
+
*/
|
|
212
|
+
export type ActionState = TrackedActionState;
|
|
213
|
+
|
|
182
214
|
/**
|
|
183
215
|
* Listener for action state changes
|
|
184
216
|
*
|
|
@@ -316,8 +348,14 @@ export interface RouterInstance {
|
|
|
316
348
|
replace(url: string, options?: RouterNavigateOptions): Promise<void>;
|
|
317
349
|
/** Refresh the current route (re-fetch server data, preserve client state) */
|
|
318
350
|
refresh(): Promise<void>;
|
|
319
|
-
/**
|
|
320
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Prefetch a URL for faster client-side transition.
|
|
353
|
+
*
|
|
354
|
+
* Pass `{ key: ":source" }` to source-scope the prefetch cache entry (parity
|
|
355
|
+
* with `<Link prefetchKey=":source">`) when the target's response can differ
|
|
356
|
+
* by source page.
|
|
357
|
+
*/
|
|
358
|
+
prefetch(url: string, options?: { key?: ":source" }): void;
|
|
321
359
|
/** Go back in browser history */
|
|
322
360
|
back(): void;
|
|
323
361
|
/** Go forward in browser history */
|
|
@@ -409,6 +447,10 @@ export interface NavigationStore {
|
|
|
409
447
|
// History-based segment cache (for back/forward navigation and partial merging)
|
|
410
448
|
getHistoryKey(): string;
|
|
411
449
|
setHistoryKey(key: string): void;
|
|
450
|
+
/** Monotonic token of the most recently committed navigation. */
|
|
451
|
+
getNavInstance(): number;
|
|
452
|
+
/** Nav-instance token recorded on a cache entry (undefined if absent). */
|
|
453
|
+
getCacheEntryInstance(historyKey: string): number | undefined;
|
|
412
454
|
cacheSegmentsForHistory(
|
|
413
455
|
historyKey: string,
|
|
414
456
|
segments: ResolvedSegment[],
|
|
@@ -420,14 +462,32 @@ export interface NavigationStore {
|
|
|
420
462
|
stale: boolean;
|
|
421
463
|
handleData?: HandleData;
|
|
422
464
|
routerId?: string;
|
|
465
|
+
/**
|
|
466
|
+
* True when the entry's handle data is incomplete (a deferred Meta was
|
|
467
|
+
* still pending at navigate-away). A popstate return must revalidate with
|
|
468
|
+
* a FULL re-render so the server re-streams handles.
|
|
469
|
+
*/
|
|
470
|
+
handlesPending?: boolean;
|
|
423
471
|
}
|
|
424
472
|
| undefined;
|
|
425
473
|
hasHistoryCache(historyKey: string): boolean;
|
|
426
|
-
|
|
474
|
+
/**
|
|
475
|
+
* Update only the handleData (and optionally the stale / handlesPending flags)
|
|
476
|
+
* of an existing cache entry. When a flag is omitted the entry's current value
|
|
477
|
+
* is preserved. `stale=true` marks a single entry stale so a popstate return
|
|
478
|
+
* revalidates it; `handlesPending=true` additionally forces that revalidation
|
|
479
|
+
* to be a full re-render (so a deferred Meta re-streams).
|
|
480
|
+
*/
|
|
481
|
+
updateCacheHandleData(
|
|
482
|
+
historyKey: string,
|
|
483
|
+
handleData: HandleData,
|
|
484
|
+
stale?: boolean,
|
|
485
|
+
handlesPending?: boolean,
|
|
486
|
+
): void;
|
|
427
487
|
markCacheAsStale(): void;
|
|
488
|
+
markHistoryCacheStale(): void;
|
|
428
489
|
markCacheAsStaleAndBroadcast(): void;
|
|
429
490
|
clearHistoryCache(): void;
|
|
430
|
-
broadcastCacheInvalidation(): void;
|
|
431
491
|
|
|
432
492
|
// Cross-tab refresh callback (set by navigation bridge)
|
|
433
493
|
setCrossTabRefreshCallback(callback: () => void): void;
|
|
@@ -483,6 +543,15 @@ export interface FetchPartialResult {
|
|
|
483
543
|
payload: RscPayload;
|
|
484
544
|
/** Promise that resolves when the response stream is fully consumed */
|
|
485
545
|
streamComplete: Promise<void>;
|
|
546
|
+
/**
|
|
547
|
+
* True only when this payload came from a prefetch-cache hit whose stream had
|
|
548
|
+
* ALREADY fully drained at fetch time (the route was fully prefetched). The
|
|
549
|
+
* commit then runs in a startTransition so loading()/Suspense content — already
|
|
550
|
+
* resolved — swaps in directly without flashing a fallback. A partially-warmed
|
|
551
|
+
* (still-streaming) prefetch hit and a cold fetch leave this false so their
|
|
552
|
+
* fallbacks stream as usual.
|
|
553
|
+
*/
|
|
554
|
+
fullyPrefetched?: boolean;
|
|
486
555
|
}
|
|
487
556
|
|
|
488
557
|
/**
|
|
@@ -540,6 +609,8 @@ export interface NavigationBridge {
|
|
|
540
609
|
refresh(): Promise<void>;
|
|
541
610
|
handlePopstate(): Promise<void>;
|
|
542
611
|
registerLinkInterception(): () => void;
|
|
612
|
+
/** Current RSC version (live, reflects the latest updateVersion). */
|
|
613
|
+
getVersion(): string | undefined;
|
|
543
614
|
/** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
|
|
544
615
|
updateVersion(newVersion: string): void;
|
|
545
616
|
}
|
|
@@ -1,29 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveSameOriginRedirect,
|
|
3
|
+
resolveExternalRedirect,
|
|
4
|
+
} from "../redirect-origin.js";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Validate that a client-consumed redirect URL (from headers or Flight payload)
|
|
3
8
|
* targets the same origin as the current page. Prevents open-redirect attacks
|
|
4
9
|
* via crafted responses.
|
|
5
10
|
*
|
|
11
|
+
* Thin wrapper over the shared {@link resolveSameOriginRedirect} rule (also used
|
|
12
|
+
* by the server guard in `rsc/redirect-guard.ts`) so client and server enforce
|
|
13
|
+
* the identical same-origin contract. Adds the client-side `console.error` on a
|
|
14
|
+
* block; the resolver itself stays pure.
|
|
15
|
+
*
|
|
6
16
|
* @returns The canonical (normalized) URL string on success, or null if blocked.
|
|
7
17
|
*/
|
|
8
18
|
export function validateRedirectOrigin(
|
|
9
19
|
url: string,
|
|
10
20
|
currentOrigin: string,
|
|
11
21
|
): string | null {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const resolved = resolveSameOriginRedirect(url, currentOrigin);
|
|
23
|
+
if (resolved === null) {
|
|
24
|
+
console.error(
|
|
25
|
+
`[rango] Redirect blocked: cross-origin or invalid target "${url}"`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate an explicit off-origin redirect (`redirect(url, { external: true })`)
|
|
33
|
+
* the client is about to hard-navigate to via `window.location.assign()`.
|
|
34
|
+
*
|
|
35
|
+
* Thin wrapper over the shared {@link resolveExternalRedirect} rule (also used by
|
|
36
|
+
* the server guard in `rsc/redirect-guard.ts`) so client and server enforce the
|
|
37
|
+
* identical contract: `external` allows an off-origin target but only an
|
|
38
|
+
* http(s) scheme. This stops a forged or mistaken external payload carrying a
|
|
39
|
+
* `javascript:`/`data:` URL from turning `location.assign` into a scriptable
|
|
40
|
+
* navigation. Adds the client-side `console.error` on a block; the resolver
|
|
41
|
+
* itself stays pure.
|
|
42
|
+
*
|
|
43
|
+
* @returns The normalized URL string on success, or null if blocked.
|
|
44
|
+
*/
|
|
45
|
+
export function validateExternalRedirect(
|
|
46
|
+
url: string,
|
|
47
|
+
currentOrigin: string,
|
|
48
|
+
): string | null {
|
|
49
|
+
const resolved = resolveExternalRedirect(url, currentOrigin);
|
|
50
|
+
if (resolved === null) {
|
|
51
|
+
console.error(
|
|
52
|
+
`[rango] External redirect blocked: non-http(s) target "${url}"`,
|
|
53
|
+
);
|
|
28
54
|
}
|
|
55
|
+
return resolved;
|
|
29
56
|
}
|