@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +121 -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 +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -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 +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- 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 +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- 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 +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- 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 +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- 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 +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- 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 +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- 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 +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- 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 +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -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 +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- 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/router/middleware-cookies.ts +0 -55
|
@@ -4,6 +4,8 @@ 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,
|
|
@@ -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";
|
|
@@ -77,6 +84,26 @@ export function createServerActionBridge(
|
|
|
77
84
|
onNavigate,
|
|
78
85
|
} = config;
|
|
79
86
|
|
|
87
|
+
// SPA-navigate when onNavigate is set, else hard-reload. state is omitted (not
|
|
88
|
+
// passed as undefined) to match the header path's prior call shape.
|
|
89
|
+
// Callers pass an already same-origin-validated url; the hard-reload fallback
|
|
90
|
+
// re-validates defensively so this leaf cannot become an open redirect if a
|
|
91
|
+
// future caller forgets (the SPA path validates inside the navigation bridge).
|
|
92
|
+
async function dispatchRedirect(url: string, state?: unknown): Promise<void> {
|
|
93
|
+
if (onNavigate) {
|
|
94
|
+
await onNavigate(url, {
|
|
95
|
+
...(state !== undefined ? { state } : {}),
|
|
96
|
+
replace: true,
|
|
97
|
+
_skipCache: true,
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
101
|
+
if (safe) {
|
|
102
|
+
window.location.href = safe;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
let isRegistered = false;
|
|
81
108
|
|
|
82
109
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -140,12 +167,52 @@ export function createServerActionBridge(
|
|
|
140
167
|
|
|
141
168
|
// Start action in event controller - handles lifecycle tracking
|
|
142
169
|
const handle = eventController.startAction(id, args);
|
|
170
|
+
// Whether the action's response carried the keepClientCache() directive.
|
|
171
|
+
// Set when the response arrives; gates the deferred invalidation below.
|
|
172
|
+
let keepCache = false;
|
|
173
|
+
// Whether a Response actually settled from the network (the server saw the
|
|
174
|
+
// request). Set true as the first statement in the fetch .then() below.
|
|
175
|
+
// Gates the automatic invalidation: a pre-dispatch failure (encodeReply
|
|
176
|
+
// throw or a fetch rejection — server unreachable/DNS/connection refused)
|
|
177
|
+
// leaves this false, so finalizeAction() must NOT invalidate or broadcast —
|
|
178
|
+
// nothing reached the server, so nothing could have mutated. A failed Flight
|
|
179
|
+
// DECODE after the response arrived keeps it true (the mutation may have
|
|
180
|
+
// committed, so invalidating the now-possibly-stale client cache is correct).
|
|
181
|
+
let responseReceived = false;
|
|
182
|
+
// Single deferred invalidation + fence release, run exactly ONCE however the
|
|
183
|
+
// action terminates (normal, redirect, error, abort, intercept, concurrent).
|
|
184
|
+
// This replaces main's eager clear at action start: every directive-free
|
|
185
|
+
// action invalidates once; keepClientCache() suppresses only the automatic
|
|
186
|
+
// invalidation, so a concurrent directive-free action still invalidates via
|
|
187
|
+
// its own latch. Latched so the finally AND the early SPA-redirect returns
|
|
188
|
+
// (whose Flight stream never settles) can both call it safely.
|
|
189
|
+
let actionFinalized = false;
|
|
190
|
+
// skipInvalidation: the version-mismatch reload terminal released nothing
|
|
191
|
+
// server-side, so it releases the fence without invalidating.
|
|
192
|
+
const finalizeAction = (skipInvalidation = false): void => {
|
|
193
|
+
if (actionFinalized) return;
|
|
194
|
+
actionFinalized = true;
|
|
195
|
+
// finally so a throw in invalidation cannot leak the fence (latch is set).
|
|
196
|
+
try {
|
|
197
|
+
// responseReceived gates the automatic invalidation: a pre-dispatch
|
|
198
|
+
// failure (serialize throw / fetch reject) never reached the server, so
|
|
199
|
+
// marking the cache stale + broadcasting cross-tab would be spurious.
|
|
200
|
+
if (responseReceived && !keepCache && !skipInvalidation) {
|
|
201
|
+
store.markCacheAsStaleAndBroadcast();
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
exitActionFence();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
143
207
|
try {
|
|
144
208
|
const segmentState = store.getSegmentState();
|
|
145
209
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
store
|
|
210
|
+
// Raise the action fence (replaces the old eager clear). Nothing is wiped,
|
|
211
|
+
// rotated, or broadcast yet: navigations during the flight fetch fresh
|
|
212
|
+
// (no-store) and popstate is treated as SWR, but the decision to
|
|
213
|
+
// invalidate is deferred to the response so a no-op action (keepClientCache)
|
|
214
|
+
// can leave the caches and the jar untouched.
|
|
215
|
+
enterActionFence();
|
|
149
216
|
|
|
150
217
|
// Create temporary references for serialization
|
|
151
218
|
const temporaryReferences = deps.createTemporaryReferenceSet();
|
|
@@ -217,23 +284,34 @@ export function createServerActionBridge(
|
|
|
217
284
|
body: encodedBody,
|
|
218
285
|
signal: fetchAbort.signal,
|
|
219
286
|
}).then(async (response) => {
|
|
287
|
+
// A settled fetch promise means the request reached the server and a
|
|
288
|
+
// Response came back (true for 2xx, 4xx, AND 5xx — fetch only rejects
|
|
289
|
+
// on network-layer failure, never on HTTP status). Record it as the
|
|
290
|
+
// first statement so every downstream terminal can invalidate; a
|
|
291
|
+
// pre-dispatch failure never gets here and stays gated out.
|
|
292
|
+
responseReceived = true;
|
|
220
293
|
// Response arrived — disconnect fetch abort from handle abort so
|
|
221
294
|
// abortAllActions() doesn't disrupt the in-progress Flight stream.
|
|
222
295
|
handle.signal.removeEventListener("abort", onHandleAbort);
|
|
223
296
|
|
|
297
|
+
// Did the action call keepClientCache()? If so the deferred invalidation
|
|
298
|
+
// below is suppressed for THIS action (a concurrent directive-free
|
|
299
|
+
// action still invalidates via its own response).
|
|
300
|
+
keepCache = response.headers.get(KEEP_CACHE_HEADER) === "1";
|
|
301
|
+
|
|
224
302
|
// Check for version mismatch - server wants us to reload
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
303
|
+
const reloadResult = handleReloadHeader(response, {
|
|
304
|
+
onBlocked: resolveStreamComplete,
|
|
305
|
+
onReload: (url) => {
|
|
306
|
+
log("version mismatch on action, reloading", { reloadUrl: url });
|
|
307
|
+
// Never-settling terminal (navigates away), so the finally never
|
|
308
|
+
// runs: release the fence here. skipInvalidation — the mismatch
|
|
309
|
+
// short-circuits the action server-side, so nothing mutated and a
|
|
310
|
+
// broadcast would only risk hard-reloading a sibling mid-task.
|
|
311
|
+
finalizeAction(true);
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
if (reloadResult) return reloadResult;
|
|
237
315
|
|
|
238
316
|
// Simple redirect from action (no state, no RSC payload).
|
|
239
317
|
// Short-circuits before createFromFetch — no Flight deserialization needed.
|
|
@@ -243,14 +321,11 @@ export function createServerActionBridge(
|
|
|
243
321
|
if (redirect && redirect !== "blocked" && !handle.signal.aborted) {
|
|
244
322
|
log("action simple redirect", { url: redirect.url });
|
|
245
323
|
handle.complete(undefined);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
} else {
|
|
252
|
-
window.location.href = redirect.url;
|
|
253
|
-
}
|
|
324
|
+
// This path returns a never-settling promise, so the finally never
|
|
325
|
+
// runs: invalidate + release the fence here (the mutation committed
|
|
326
|
+
// and we're navigating away). Latched, so the finally is a no-op.
|
|
327
|
+
finalizeAction();
|
|
328
|
+
await dispatchRedirect(redirect.url);
|
|
254
329
|
return new Promise<Response>(() => {});
|
|
255
330
|
}
|
|
256
331
|
if (redirect === "blocked") {
|
|
@@ -258,6 +333,29 @@ export function createServerActionBridge(
|
|
|
258
333
|
return emptyResponse();
|
|
259
334
|
}
|
|
260
335
|
|
|
336
|
+
// Integrity check (pre-decode): a foreign app's action response must
|
|
337
|
+
// not be decoded + applied here. This is the one decode-and-apply path
|
|
338
|
+
// the post-decode partial-update guard does NOT cover (the action
|
|
339
|
+
// bridge has its own createFromFetch -> onUpdate). Ordered after the
|
|
340
|
+
// reload/redirect handlers, which steer control responses first.
|
|
341
|
+
// Reloads via window.location.reload() rather than navigating to a
|
|
342
|
+
// target (as the navigation-client guard does): an action has no
|
|
343
|
+
// navigation target, so reloading the current URL re-syncs the
|
|
344
|
+
// document against the server-applied action effect.
|
|
345
|
+
if (
|
|
346
|
+
!handle.signal.aborted &&
|
|
347
|
+
isForeignRouterId(response, store.getRouterId?.())
|
|
348
|
+
) {
|
|
349
|
+
log("action router id mismatch, reloading to re-sync");
|
|
350
|
+
handle.complete(undefined);
|
|
351
|
+
resolveStreamComplete();
|
|
352
|
+
// Never-settling return: release the fence before the reload (the
|
|
353
|
+
// reload resets module state anyway, but stay balanced). Latched.
|
|
354
|
+
finalizeAction();
|
|
355
|
+
window.location.reload();
|
|
356
|
+
return new Promise<Response>(() => {});
|
|
357
|
+
}
|
|
358
|
+
|
|
261
359
|
// Start streaming immediately when response arrives
|
|
262
360
|
if (!handle.signal.aborted) {
|
|
263
361
|
streamingToken = handle.startStreaming();
|
|
@@ -328,6 +426,27 @@ export function createServerActionBridge(
|
|
|
328
426
|
// Check handle.signal.aborted to avoid redirecting from a stale action
|
|
329
427
|
// when the user has already navigated away.
|
|
330
428
|
if (metadata?.redirect && !handle.signal.aborted) {
|
|
429
|
+
// Explicit off-host redirect (redirect(url, { external: true })):
|
|
430
|
+
// hard-navigate, but still scheme-validate (http/https only). external
|
|
431
|
+
// waives the same-origin check, NOT scheme safety, so a forged payload
|
|
432
|
+
// carrying a javascript:/data: URL cannot script via location.assign.
|
|
433
|
+
if (metadata.redirect.external) {
|
|
434
|
+
const externalUrl = validateExternalRedirect(
|
|
435
|
+
metadata.redirect.url,
|
|
436
|
+
window.location.origin,
|
|
437
|
+
);
|
|
438
|
+
if (!externalUrl) {
|
|
439
|
+
log("blocked external action redirect payload", {
|
|
440
|
+
url: metadata.redirect.url,
|
|
441
|
+
});
|
|
442
|
+
handle.complete(returnValue?.data);
|
|
443
|
+
return returnValue?.data;
|
|
444
|
+
}
|
|
445
|
+
log("external action redirect", { url: externalUrl });
|
|
446
|
+
handle.complete(returnValue?.data);
|
|
447
|
+
window.location.assign(externalUrl);
|
|
448
|
+
return returnValue?.data;
|
|
449
|
+
}
|
|
331
450
|
const redirectUrl = validateRedirectOrigin(
|
|
332
451
|
metadata.redirect.url,
|
|
333
452
|
window.location.origin,
|
|
@@ -339,18 +458,9 @@ export function createServerActionBridge(
|
|
|
339
458
|
handle.complete(returnValue?.data);
|
|
340
459
|
return returnValue?.data;
|
|
341
460
|
}
|
|
342
|
-
const redirectState = metadata.locationState;
|
|
343
461
|
log("action redirect", { url: redirectUrl });
|
|
344
462
|
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
|
-
}
|
|
463
|
+
await dispatchRedirect(redirectUrl, metadata.locationState);
|
|
354
464
|
return returnValue?.data;
|
|
355
465
|
}
|
|
356
466
|
|
|
@@ -528,8 +638,9 @@ export function createServerActionBridge(
|
|
|
528
638
|
handle.clearConsolidation();
|
|
529
639
|
|
|
530
640
|
if (scenario.historyKeyChanged) {
|
|
531
|
-
|
|
532
|
-
|
|
641
|
+
// Invalidation is deferred to finalizeAction(); here we only trigger
|
|
642
|
+
// the revalidation refetch of the new route (suppressed on keep).
|
|
643
|
+
if (!scenario.onInterceptRoute && !keepCache) {
|
|
533
644
|
refetchRoute().catch((error) => {
|
|
534
645
|
if (isBackgroundSuppressible(error)) return;
|
|
535
646
|
console.error(
|
|
@@ -541,11 +652,14 @@ export function createServerActionBridge(
|
|
|
541
652
|
break;
|
|
542
653
|
}
|
|
543
654
|
|
|
544
|
-
// Same history key but different pathname - safe to refetch current
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
655
|
+
// Same history key but different pathname - safe to refetch current
|
|
656
|
+
// route. Invalidation is deferred to finalizeAction(); here we only
|
|
657
|
+
// trigger the revalidation refetch (suppressed on keep).
|
|
658
|
+
if (!keepCache) {
|
|
659
|
+
await refetchRoute({
|
|
660
|
+
interceptSourceUrl: store.getInterceptSourceUrl(),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
549
663
|
break;
|
|
550
664
|
}
|
|
551
665
|
|
|
@@ -553,8 +667,11 @@ export function createServerActionBridge(
|
|
|
553
667
|
console.warn(
|
|
554
668
|
`[Browser] Missing segments after action (HMR detected), refetching...`,
|
|
555
669
|
);
|
|
670
|
+
// Repair (not revalidation), so ungated on keepCache: a keep action
|
|
671
|
+
// resolving last must discharge a directive-free sibling's repair.
|
|
672
|
+
// See the keep row in docs/design/rango-state-cookie.md (the all-keep
|
|
673
|
+
// edge, and the benign re-mark-stale-after-refetch end-state delta).
|
|
556
674
|
await refetchRoute({ interceptSourceUrl });
|
|
557
|
-
store.broadcastCacheInvalidation();
|
|
558
675
|
break;
|
|
559
676
|
}
|
|
560
677
|
|
|
@@ -571,11 +688,11 @@ export function createServerActionBridge(
|
|
|
571
688
|
// Clear consolidation tracking before fetch
|
|
572
689
|
handle.clearConsolidation();
|
|
573
690
|
|
|
691
|
+
// Ungated on keepCache, same as hmr-missing above (see the keep row).
|
|
574
692
|
await refetchRoute({
|
|
575
693
|
segments: segmentsToSend,
|
|
576
694
|
interceptSourceUrl,
|
|
577
695
|
});
|
|
578
|
-
store.broadcastCacheInvalidation();
|
|
579
696
|
break;
|
|
580
697
|
}
|
|
581
698
|
|
|
@@ -639,7 +756,9 @@ export function createServerActionBridge(
|
|
|
639
756
|
fullSegments,
|
|
640
757
|
currentHandleData,
|
|
641
758
|
);
|
|
642
|
-
|
|
759
|
+
// Invalidation deferred to finalizeAction() (runs after this caches
|
|
760
|
+
// the fresh segments), suppressed when the action called
|
|
761
|
+
// keepClientCache().
|
|
643
762
|
break;
|
|
644
763
|
}
|
|
645
764
|
}
|
|
@@ -647,6 +766,11 @@ export function createServerActionBridge(
|
|
|
647
766
|
handle.complete(returnData);
|
|
648
767
|
return returnData;
|
|
649
768
|
} finally {
|
|
769
|
+
// The single deferred invalidation + fence release for this action. Runs
|
|
770
|
+
// for every terminal that settles (normal, navigated-away, error, abort,
|
|
771
|
+
// intercept, concurrent); the SPA-redirect paths above already ran it.
|
|
772
|
+
// Latched, so it fires exactly once.
|
|
773
|
+
finalizeAction();
|
|
650
774
|
handle[Symbol.dispose]();
|
|
651
775
|
}
|
|
652
776
|
}
|
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,8 +31,10 @@ 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;
|
|
@@ -69,6 +70,12 @@ export interface RscMetadata {
|
|
|
69
70
|
* Sent on initial render so the browser can configure its cache duration.
|
|
70
71
|
*/
|
|
71
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;
|
|
72
79
|
/**
|
|
73
80
|
* Theme configuration from router.
|
|
74
81
|
* Included when theme is enabled in router config.
|
|
@@ -83,8 +90,12 @@ export interface RscMetadata {
|
|
|
83
90
|
basename?: string;
|
|
84
91
|
/** Whether connection warmup is enabled */
|
|
85
92
|
warmupEnabled?: boolean;
|
|
86
|
-
/**
|
|
87
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Server-side redirect with optional state (for partial requests).
|
|
95
|
+
* `external: true` (from redirect(url, { external: true })) tells the client
|
|
96
|
+
* to hard-navigate to an off-host target instead of validating same-origin.
|
|
97
|
+
*/
|
|
98
|
+
redirect?: { url: string; external?: boolean };
|
|
88
99
|
/** Server-set location state to include in history.pushState */
|
|
89
100
|
locationState?: Record<string, unknown>;
|
|
90
101
|
}
|
|
@@ -185,6 +196,15 @@ export interface TrackedActionState {
|
|
|
185
196
|
result: unknown | null;
|
|
186
197
|
}
|
|
187
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The value returned by {@link useAction} when called without a selector.
|
|
201
|
+
*
|
|
202
|
+
* This is the stable, public name for the action-state shape; consumers can
|
|
203
|
+
* name it in their own signatures (e.g. a wrapper hook). It aliases the
|
|
204
|
+
* internal {@link TrackedActionState}.
|
|
205
|
+
*/
|
|
206
|
+
export type ActionState = TrackedActionState;
|
|
207
|
+
|
|
188
208
|
/**
|
|
189
209
|
* Listener for action state changes
|
|
190
210
|
*
|
|
@@ -322,8 +342,14 @@ export interface RouterInstance {
|
|
|
322
342
|
replace(url: string, options?: RouterNavigateOptions): Promise<void>;
|
|
323
343
|
/** Refresh the current route (re-fetch server data, preserve client state) */
|
|
324
344
|
refresh(): Promise<void>;
|
|
325
|
-
/**
|
|
326
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Prefetch a URL for faster client-side transition.
|
|
347
|
+
*
|
|
348
|
+
* Pass `{ key: ":source" }` to source-scope the prefetch cache entry (parity
|
|
349
|
+
* with `<Link prefetchKey=":source">`) when the target's response can differ
|
|
350
|
+
* by source page.
|
|
351
|
+
*/
|
|
352
|
+
prefetch(url: string, options?: { key?: ":source" }): void;
|
|
327
353
|
/** Go back in browser history */
|
|
328
354
|
back(): void;
|
|
329
355
|
/** Go forward in browser history */
|
|
@@ -431,15 +457,9 @@ export interface NavigationStore {
|
|
|
431
457
|
hasHistoryCache(historyKey: string): boolean;
|
|
432
458
|
updateCacheHandleData(historyKey: string, handleData: HandleData): void;
|
|
433
459
|
markCacheAsStale(): void;
|
|
460
|
+
markHistoryCacheStale(): void;
|
|
434
461
|
markCacheAsStaleAndBroadcast(): void;
|
|
435
462
|
clearHistoryCache(): void;
|
|
436
|
-
/**
|
|
437
|
-
* Clear this tab's nav + prefetch caches without broadcasting or rotating
|
|
438
|
-
* shared state. Intended for app-switch transitions that affect only this
|
|
439
|
-
* tab's session.
|
|
440
|
-
*/
|
|
441
|
-
clearHistoryCacheLocal(): void;
|
|
442
|
-
broadcastCacheInvalidation(): void;
|
|
443
463
|
|
|
444
464
|
// Cross-tab refresh callback (set by navigation bridge)
|
|
445
465
|
setCrossTabRefreshCallback(callback: () => void): void;
|
|
@@ -552,15 +572,10 @@ export interface NavigationBridge {
|
|
|
552
572
|
refresh(): Promise<void>;
|
|
553
573
|
handlePopstate(): Promise<void>;
|
|
554
574
|
registerLinkInterception(): () => void;
|
|
575
|
+
/** Current RSC version (live, reflects the latest updateVersion). */
|
|
576
|
+
getVersion(): string | undefined;
|
|
555
577
|
/** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
|
|
556
578
|
updateVersion(newVersion: string): void;
|
|
557
|
-
/**
|
|
558
|
-
* Replace the active app-shell snapshot (rootLayout, basename, version)
|
|
559
|
-
* atomically. Used on cross-app navigations when the response's routerId
|
|
560
|
-
* indicates the user entered a different app. Theme, warmup, and prefetch
|
|
561
|
-
* TTL are document-lifetime and not part of the shell.
|
|
562
|
-
*/
|
|
563
|
-
updateAppShell(next: import("./app-shell.js").AppShell): void;
|
|
564
579
|
}
|
|
565
580
|
|
|
566
581
|
/**
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Collect the `"use client"` client-reference keys reachable from an error /
|
|
2
|
+
// notFound boundary registration, for routing them into the dedicated
|
|
3
|
+
// `app-fallback` chunk (see vite/utils/client-chunks.ts).
|
|
4
|
+
//
|
|
5
|
+
// A boundary registration is not always a bare client element. The common,
|
|
6
|
+
// load-bearing pattern wraps the client boundary in providers a thrown handler
|
|
7
|
+
// needs (the layout that would normally supply them did not mount):
|
|
8
|
+
//
|
|
9
|
+
// defaultErrorBoundary: ({ error }) => (
|
|
10
|
+
// <FallbackIntl locales={...}>
|
|
11
|
+
// <ThemedError error={error} /> // <- the real "use client" boundary
|
|
12
|
+
// </FallbackIntl>
|
|
13
|
+
// )
|
|
14
|
+
//
|
|
15
|
+
// So the value may be (a) a handler FUNCTION returning a tree, or (b) an element
|
|
16
|
+
// tree with the client boundary nested below server wrappers. We:
|
|
17
|
+
// 1. If it's a function, CALL it with synthetic props to get the returned tree.
|
|
18
|
+
// This only constructs JSX — the inner components are element `type`s, never
|
|
19
|
+
// invoked — so no hooks run. Guarded: a boundary that needs a real render
|
|
20
|
+
// context (request globals, etc.) throws and is skipped (graceful: it simply
|
|
21
|
+
// stays on the default grouping, as before).
|
|
22
|
+
// 2. Walk the resulting tree and report every element whose `.type` is a
|
|
23
|
+
// plugin-rsc client reference.
|
|
24
|
+
//
|
|
25
|
+
// Limit: a boundary that *conditionally* renders different client components based
|
|
26
|
+
// on the runtime error cannot be resolved statically — only the branch taken with
|
|
27
|
+
// the synthetic error is seen. Such cases fall back to the default chunk; the
|
|
28
|
+
// custom `clientChunks` function is the escape hatch.
|
|
29
|
+
|
|
30
|
+
const CLIENT_REF = Symbol.for("react.client.reference");
|
|
31
|
+
const MAX_DEPTH = 40;
|
|
32
|
+
|
|
33
|
+
// Synthetic props covering the error-boundary (`{ error, reset }`) and notFound
|
|
34
|
+
// (`{ pathname }`) handler shapes. The handler destructures what it needs.
|
|
35
|
+
const SYNTHETIC_PROPS = {
|
|
36
|
+
error: new Error("rango: build-time fallback-chunk discovery"),
|
|
37
|
+
reset: () => {},
|
|
38
|
+
pathname: "/",
|
|
39
|
+
info: { componentStack: "" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface MaybeElement {
|
|
43
|
+
type?: { $$typeof?: symbol; $$id?: string };
|
|
44
|
+
props?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isReactNodeLike(v: unknown): boolean {
|
|
48
|
+
return (
|
|
49
|
+
Array.isArray(v) ||
|
|
50
|
+
(typeof v === "object" && v !== null && "$$typeof" in (v as object))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walkElementTree(
|
|
55
|
+
node: unknown,
|
|
56
|
+
report: (refKey: string) => void,
|
|
57
|
+
depth: number,
|
|
58
|
+
): void {
|
|
59
|
+
if (node == null || depth > MAX_DEPTH) return;
|
|
60
|
+
if (Array.isArray(node)) {
|
|
61
|
+
for (const child of node) walkElementTree(child, report, depth + 1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof node !== "object") return;
|
|
65
|
+
|
|
66
|
+
const el = node as MaybeElement;
|
|
67
|
+
const type = el.type;
|
|
68
|
+
if (type?.$$typeof === CLIENT_REF && typeof type.$$id === "string") {
|
|
69
|
+
// $$id is `<referenceKey>#<exportName>` in build mode — keep the referenceKey.
|
|
70
|
+
report(type.$$id.split("#")[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = el.props;
|
|
74
|
+
if (props && typeof props === "object") {
|
|
75
|
+
// Children are always nodes; other props are followed only when they look
|
|
76
|
+
// like React nodes (slots/icons), never arbitrary data objects.
|
|
77
|
+
walkElementTree(props.children, report, depth + 1);
|
|
78
|
+
for (const key in props) {
|
|
79
|
+
if (key === "children") continue;
|
|
80
|
+
const value = props[key];
|
|
81
|
+
if (isReactNodeLike(value)) walkElementTree(value, report, depth + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Report every `"use client"` client-reference key reachable from a single
|
|
88
|
+
* error/notFound boundary registration (handler function or element tree).
|
|
89
|
+
*/
|
|
90
|
+
export function collectFallbackClientRefs(
|
|
91
|
+
boundary: unknown,
|
|
92
|
+
report: (refKey: string) => void,
|
|
93
|
+
): void {
|
|
94
|
+
try {
|
|
95
|
+
let node = boundary;
|
|
96
|
+
if (typeof node === "function") {
|
|
97
|
+
node = (node as (props: unknown) => unknown)(SYNTHETIC_PROPS);
|
|
98
|
+
}
|
|
99
|
+
walkElementTree(node, report, 0);
|
|
100
|
+
} catch {
|
|
101
|
+
// The boundary needs a real render context (request globals, hooks at the
|
|
102
|
+
// top level) or its tree has hostile getters. Its client refs can't be
|
|
103
|
+
// resolved statically — skip. It stays on the default grouping (no
|
|
104
|
+
// regression vs. not collecting), and the custom clientChunks fn is the
|
|
105
|
+
// escape hatch for such cases.
|
|
106
|
+
}
|
|
107
|
+
}
|