@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918
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 +211 -43
- package/dist/bin/rango.js +279 -102
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3313 -1160
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +62 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +63 -1
- 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 +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +124 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +223 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +66 -5
- package/skills/router-setup/SKILL.md +38 -3
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- 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 +319 -27
- 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 +1 -66
- 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/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +81 -147
- 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 +66 -14
- package/src/browser/navigation-client.ts +172 -109
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +79 -93
- package/src/browser/prefetch/cache.ts +180 -62
- package/src/browser/prefetch/fetch.ts +252 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +72 -10
- package/src/browser/react/NavigationProvider.tsx +83 -31
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- 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 +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- 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 +52 -1
- package/src/browser/rsc-router.tsx +111 -24
- package/src/browser/scroll-restoration.ts +29 -19
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -62
- package/src/browser/types.ts +60 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +6 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- 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 +333 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- 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 +2256 -241
- 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 +108 -290
- 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/browser.ts +0 -1
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- 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 +69 -10
- package/src/index.ts +112 -14
- 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-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +413 -275
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +107 -32
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +50 -8
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +46 -30
- package/src/router/intercept-resolution.ts +23 -23
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +207 -30
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +161 -262
- 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 +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +169 -140
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +119 -65
- 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 +329 -305
- 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 +91 -46
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +128 -42
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -409
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +28 -4
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +53 -56
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -69
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +22 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +208 -201
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +211 -52
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +289 -124
- package/src/ssr/index.tsx +22 -15
- package/src/static-handler.ts +27 -18
- 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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +124 -70
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +36 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -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 +192 -99
- 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 +51 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +236 -6
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +65 -207
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +46 -17
- package/src/vite/rango.ts +207 -125
- package/src/vite/router-discovery.ts +931 -133
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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/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 +27 -29
- package/src/vite/utils/shared-utils.ts +97 -44
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- 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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
generateHistoryKey,
|
|
9
9
|
} from "./navigation-store.js";
|
|
10
10
|
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
11
12
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
13
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
14
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
@@ -22,11 +23,15 @@ import type {
|
|
|
22
23
|
import type { EventController } from "./event-controller.js";
|
|
23
24
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
25
|
import { initRangoState } from "./rango-state.js";
|
|
26
|
+
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
25
27
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
29
|
+
import { setAppVersion } from "./app-version.js";
|
|
26
30
|
import {
|
|
27
31
|
isInterceptSegment,
|
|
28
32
|
splitInterceptSegments,
|
|
29
33
|
} from "./intercept-utils.js";
|
|
34
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
30
35
|
|
|
31
36
|
// Vite HMR types are provided by vite/client
|
|
32
37
|
|
|
@@ -113,13 +118,22 @@ export interface BrowserAppContext {
|
|
|
113
118
|
warmupEnabled?: boolean;
|
|
114
119
|
/** App version for prefetch version mismatch detection */
|
|
115
120
|
version?: string;
|
|
121
|
+
/**
|
|
122
|
+
* App-shell ref, read through on each render so renderSegments and the
|
|
123
|
+
* NavigationProvider see rootLayout/basename/version without closing over a
|
|
124
|
+
* stale snapshot. Set once from the initial payload and not swapped within a
|
|
125
|
+
* session: a cross-app navigation is a full document load (X-RSC-Reload), so
|
|
126
|
+
* the target app establishes its own shell on load. Theme, warmup, and
|
|
127
|
+
* prefetch TTL are document-lifetime too (see AppShell).
|
|
128
|
+
*/
|
|
129
|
+
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
// Module-level state for the initialized app
|
|
119
133
|
let browserAppContext: BrowserAppContext | null = null;
|
|
120
134
|
|
|
121
135
|
/**
|
|
122
|
-
* Initialize the browser app. Must be called before rendering
|
|
136
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
123
137
|
*
|
|
124
138
|
* This function:
|
|
125
139
|
* - Loads the initial RSC payload from the stream
|
|
@@ -139,7 +153,6 @@ export async function initBrowserApp(
|
|
|
139
153
|
initialTheme,
|
|
140
154
|
} = options;
|
|
141
155
|
|
|
142
|
-
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
143
156
|
const initialPayload =
|
|
144
157
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
145
158
|
|
|
@@ -164,6 +177,18 @@ export async function initBrowserApp(
|
|
|
164
177
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
165
178
|
});
|
|
166
179
|
|
|
180
|
+
// Register the active store on the module-level handle and wire the
|
|
181
|
+
// jar-divergence observer before any getRangoState() read can detect a
|
|
182
|
+
// cross-tab/server rotation. There is no global store singleton, so this
|
|
183
|
+
// handle is the live reference.
|
|
184
|
+
registerNavigationStore(store);
|
|
185
|
+
|
|
186
|
+
// Seed router identity from the initial SSR payload so the first
|
|
187
|
+
// cross-app SPA navigation can detect the app switch.
|
|
188
|
+
if (initialPayload.metadata?.routerId) {
|
|
189
|
+
store.setRouterId?.(initialPayload.metadata.routerId);
|
|
190
|
+
}
|
|
191
|
+
|
|
167
192
|
// Create event controller for reactive state management
|
|
168
193
|
const eventController = createEventController({
|
|
169
194
|
initialLocation: new URL(window.location.href),
|
|
@@ -198,13 +223,25 @@ export async function initBrowserApp(
|
|
|
198
223
|
// Create composable utilities
|
|
199
224
|
const client = createNavigationClient(deps);
|
|
200
225
|
|
|
201
|
-
//
|
|
202
|
-
|
|
226
|
+
// Capture the per-router app-shell. rootLayout, basename, and version live
|
|
227
|
+
// here and are read through the ref at call time rather than closed over.
|
|
228
|
+
// It is set once from the initial payload and not swapped within a session:
|
|
229
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the
|
|
230
|
+
// target app establishes its own shell on load.
|
|
203
231
|
const version = initialPayload.metadata?.version;
|
|
232
|
+
const appShellRef = createAppShellRef({
|
|
233
|
+
routerId: initialPayload.metadata?.routerId,
|
|
234
|
+
rootLayout: initialPayload.metadata?.rootLayout,
|
|
235
|
+
basename: initialPayload.metadata?.basename,
|
|
236
|
+
version,
|
|
237
|
+
});
|
|
204
238
|
|
|
205
|
-
// Initialize the
|
|
206
|
-
//
|
|
207
|
-
|
|
239
|
+
// Initialize the rango state cookie for cache invalidation. The build version
|
|
240
|
+
// busts cached prefetches on deploy; the server-resolved cookie name
|
|
241
|
+
// namespaces the cookie so sibling apps on the same origin don't collide
|
|
242
|
+
// (falls back to the bare default prefix if metadata lacks the name).
|
|
243
|
+
initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
|
|
244
|
+
setAppVersion(version);
|
|
208
245
|
|
|
209
246
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
210
247
|
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
@@ -213,11 +250,22 @@ export async function initBrowserApp(
|
|
|
213
250
|
initPrefetchCache(prefetchCacheTTL);
|
|
214
251
|
}
|
|
215
252
|
|
|
216
|
-
//
|
|
253
|
+
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
|
254
|
+
// client chunks (same createFromFetch the navigation client uses).
|
|
255
|
+
setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
|
|
256
|
+
|
|
257
|
+
// Create a bound renderSegments that reads rootLayout through the shell ref.
|
|
258
|
+
// The shell is set once at init and not swapped within a session (a cross-app
|
|
259
|
+
// navigation is a full document load), so this always renders this app's
|
|
260
|
+
// Document; reading through the ref just avoids closing over a stale value.
|
|
217
261
|
const renderSegments = (
|
|
218
262
|
segments: ResolvedSegment[],
|
|
219
263
|
options?: RenderSegmentsOptions,
|
|
220
|
-
) =>
|
|
264
|
+
) =>
|
|
265
|
+
baseRenderSegments(segments, {
|
|
266
|
+
...options,
|
|
267
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
268
|
+
});
|
|
221
269
|
|
|
222
270
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
223
271
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -231,10 +279,15 @@ export async function initBrowserApp(
|
|
|
231
279
|
deps,
|
|
232
280
|
onUpdate: (update) => store.emitUpdate(update),
|
|
233
281
|
renderSegments,
|
|
234
|
-
version,
|
|
235
282
|
onNavigate: (url, options) => {
|
|
236
283
|
if (!navigateFn) {
|
|
237
|
-
|
|
284
|
+
// Navigation bridge not wired yet: hard-navigate, but re-validate
|
|
285
|
+
// same-origin defensively so this init-window fallback cannot become an
|
|
286
|
+
// open redirect (the normal path validates inside the navigation bridge).
|
|
287
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
288
|
+
if (safe) {
|
|
289
|
+
window.location.href = safe;
|
|
290
|
+
}
|
|
238
291
|
return Promise.resolve();
|
|
239
292
|
}
|
|
240
293
|
return navigateFn(url, options);
|
|
@@ -249,7 +302,7 @@ export async function initBrowserApp(
|
|
|
249
302
|
client,
|
|
250
303
|
onUpdate: (update) => store.emitUpdate(update),
|
|
251
304
|
renderSegments,
|
|
252
|
-
version,
|
|
305
|
+
version: version,
|
|
253
306
|
});
|
|
254
307
|
|
|
255
308
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -294,11 +347,11 @@ export async function initBrowserApp(
|
|
|
294
347
|
// full lifecycle (fetching + streaming, before commit) without
|
|
295
348
|
// blocking on server actions.
|
|
296
349
|
if (eventController.getState().isNavigating) {
|
|
297
|
-
console.log("[
|
|
350
|
+
console.log("[Rango] HMR: Skipping — navigation in progress");
|
|
298
351
|
return;
|
|
299
352
|
}
|
|
300
353
|
|
|
301
|
-
console.log("[
|
|
354
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
302
355
|
|
|
303
356
|
const abort = new AbortController();
|
|
304
357
|
hmrAbort = abort;
|
|
@@ -316,6 +369,7 @@ export async function initBrowserApp(
|
|
|
316
369
|
segmentIds: [],
|
|
317
370
|
previousUrl: store.getSegmentState().currentUrl,
|
|
318
371
|
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
372
|
+
routerId: store.getRouterId?.(),
|
|
319
373
|
hmr: true,
|
|
320
374
|
signal: abort.signal,
|
|
321
375
|
});
|
|
@@ -329,6 +383,35 @@ export async function initBrowserApp(
|
|
|
329
383
|
throw new Error("HMR refetch returned invalid payload");
|
|
330
384
|
}
|
|
331
385
|
|
|
386
|
+
// Update version BEFORE rebuilding state so that
|
|
387
|
+
// clearHistoryCache() runs first, then the fresh segment
|
|
388
|
+
// cache entry we create below survives.
|
|
389
|
+
//
|
|
390
|
+
// Compare against the bridge's live version, not the init-time
|
|
391
|
+
// `version` const: after the first HMR bump the const is stale, so a
|
|
392
|
+
// later update with an unchanged version would otherwise re-clear the
|
|
393
|
+
// cache and re-broadcast across tabs/apps. The live read fires only
|
|
394
|
+
// on a genuine version change.
|
|
395
|
+
const newVersion = payload.metadata.version;
|
|
396
|
+
const currentVersion = navigationBridge.getVersion();
|
|
397
|
+
if (newVersion && newVersion !== currentVersion) {
|
|
398
|
+
console.log(
|
|
399
|
+
"[Rango] HMR: version changed",
|
|
400
|
+
currentVersion,
|
|
401
|
+
"→",
|
|
402
|
+
newVersion,
|
|
403
|
+
"clearing caches",
|
|
404
|
+
);
|
|
405
|
+
navigationBridge.updateVersion(newVersion);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Apply only partial segment updates. A non-partial payload during
|
|
409
|
+
// HMR is transient: the worker route table is still rebuilding after
|
|
410
|
+
// the edit, so the URL momentarily resolves to not-found/catch-all.
|
|
411
|
+
// Skip it -- the debounced follow-up refetch returns the settled
|
|
412
|
+
// route's partial payload and renders it below. We never reload here:
|
|
413
|
+
// a paramless document GET would run the SSR path and surface the
|
|
414
|
+
// not-found page during that same transient.
|
|
332
415
|
if (payload.metadata?.isPartial) {
|
|
333
416
|
const segments = payload.metadata.segments || [];
|
|
334
417
|
const matched = payload.metadata.matched || [];
|
|
@@ -368,10 +451,10 @@ export async function initBrowserApp(
|
|
|
368
451
|
|
|
369
452
|
await streamComplete;
|
|
370
453
|
handle.complete(new URL(window.location.href));
|
|
371
|
-
console.log("[
|
|
454
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
372
455
|
} catch (err) {
|
|
373
456
|
if (abort.signal.aborted) return;
|
|
374
|
-
console.warn("[
|
|
457
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
375
458
|
window.location.reload();
|
|
376
459
|
return;
|
|
377
460
|
} finally {
|
|
@@ -383,7 +466,7 @@ export async function initBrowserApp(
|
|
|
383
466
|
});
|
|
384
467
|
}
|
|
385
468
|
|
|
386
|
-
// Store context for
|
|
469
|
+
// Store context for Rango component
|
|
387
470
|
const context: BrowserAppContext = {
|
|
388
471
|
store,
|
|
389
472
|
eventController,
|
|
@@ -394,6 +477,7 @@ export async function initBrowserApp(
|
|
|
394
477
|
initialTheme: effectiveInitialTheme,
|
|
395
478
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
396
479
|
version,
|
|
480
|
+
appShellRef,
|
|
397
481
|
};
|
|
398
482
|
browserAppContext = context;
|
|
399
483
|
|
|
@@ -406,7 +490,7 @@ export async function initBrowserApp(
|
|
|
406
490
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
407
491
|
if (!browserAppContext) {
|
|
408
492
|
throw new Error(
|
|
409
|
-
"
|
|
493
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
410
494
|
);
|
|
411
495
|
}
|
|
412
496
|
return browserAppContext;
|
|
@@ -420,18 +504,18 @@ export function resetBrowserAppContext(): void {
|
|
|
420
504
|
}
|
|
421
505
|
|
|
422
506
|
/**
|
|
423
|
-
* Props for the
|
|
507
|
+
* Props for the Rango component
|
|
424
508
|
*/
|
|
425
|
-
export interface
|
|
509
|
+
export interface RangoProps {}
|
|
426
510
|
|
|
427
511
|
/**
|
|
428
|
-
*
|
|
512
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
429
513
|
*
|
|
430
514
|
* Must be called after initBrowserApp() has completed.
|
|
431
515
|
*
|
|
432
516
|
* @example
|
|
433
517
|
* ```tsx
|
|
434
|
-
* import { initBrowserApp,
|
|
518
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
435
519
|
* import { rscStream } from "rsc-html-stream/client";
|
|
436
520
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
437
521
|
*
|
|
@@ -441,14 +525,14 @@ export interface RSCRouterProps {}
|
|
|
441
525
|
* hydrateRoot(
|
|
442
526
|
* document,
|
|
443
527
|
* <React.StrictMode>
|
|
444
|
-
* <
|
|
528
|
+
* <Rango />
|
|
445
529
|
* </React.StrictMode>
|
|
446
530
|
* );
|
|
447
531
|
* }
|
|
448
532
|
* main();
|
|
449
533
|
* ```
|
|
450
534
|
*/
|
|
451
|
-
export function
|
|
535
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
452
536
|
const {
|
|
453
537
|
store,
|
|
454
538
|
eventController,
|
|
@@ -459,6 +543,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
459
543
|
initialTheme,
|
|
460
544
|
warmupEnabled,
|
|
461
545
|
version,
|
|
546
|
+
appShellRef,
|
|
462
547
|
} = getBrowserAppContext();
|
|
463
548
|
|
|
464
549
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -478,6 +563,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
478
563
|
initialTheme={initialTheme}
|
|
479
564
|
warmupEnabled={warmupEnabled}
|
|
480
565
|
version={version}
|
|
566
|
+
basename={initialPayload.metadata?.basename}
|
|
567
|
+
appShellRef={appShellRef}
|
|
481
568
|
/>
|
|
482
569
|
);
|
|
483
570
|
}
|
|
@@ -332,6 +332,8 @@ export function scrollToHash(): boolean {
|
|
|
332
332
|
* Scroll to top of page
|
|
333
333
|
*/
|
|
334
334
|
export function scrollToTop(): void {
|
|
335
|
+
if (typeof window === "undefined") return;
|
|
336
|
+
if (typeof window.scrollTo !== "function") return;
|
|
335
337
|
window.scrollTo(0, 0);
|
|
336
338
|
}
|
|
337
339
|
|
|
@@ -356,36 +358,44 @@ export function handleNavigationEnd(options: {
|
|
|
356
358
|
scroll?: boolean;
|
|
357
359
|
isStreaming?: () => boolean;
|
|
358
360
|
}): void {
|
|
359
|
-
if (!initialized) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
361
|
const { restore = false, scroll = true, isStreaming } = options;
|
|
364
362
|
|
|
365
|
-
// Don't scroll if explicitly disabled
|
|
366
|
-
if (scroll === false) {
|
|
363
|
+
// Don't scroll if explicitly disabled or not in a browser
|
|
364
|
+
if (scroll === false || typeof window === "undefined") {
|
|
367
365
|
return;
|
|
368
366
|
}
|
|
369
367
|
|
|
370
|
-
//
|
|
371
|
-
|
|
368
|
+
// Save/restore requires initialization (sessionStorage, history state).
|
|
369
|
+
// But basic scroll-to-top and hash scrolling work without it — this
|
|
370
|
+
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
371
|
+
// and remounts, creating a brief window where initialized is false.
|
|
372
|
+
if (restore && initialized) {
|
|
372
373
|
if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
|
|
373
374
|
return;
|
|
374
375
|
}
|
|
375
376
|
// Fall through to hash or top if no saved position
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
379
|
+
// scrollToHash / scrollToTop run synchronously here.
|
|
380
|
+
// handleNavigationEnd is invoked from NavigationProvider's
|
|
381
|
+
// useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
|
|
382
|
+
// captured by the upcoming paint AND by startViewTransition's snapshot.
|
|
383
|
+
// Deferring via rAF here pushed the call past the snapshot capture,
|
|
384
|
+
// making forward navigations wrapped in a layout/route view transition
|
|
385
|
+
// skip scroll-to-top — the live DOM scrolled but the captured snapshot
|
|
386
|
+
// was at the previous scroll position, so the user-facing page stayed
|
|
387
|
+
// visually clamped at the source page's scrollY (often the new tree's
|
|
388
|
+
// max scroll for tall→short navs). Y=0 / a hash element are robust
|
|
389
|
+
// against unmeasured layout, so sync scroll is correct here even
|
|
390
|
+
// before the new tree's scrollHeight settles.
|
|
391
|
+
//
|
|
392
|
+
// (The restore branch above keeps deferToNextPaint because savedY
|
|
393
|
+
// depends on the new tree's max scroll; sync scrollTo against an
|
|
394
|
+
// unmeasured DOM would clamp savedY to whatever the old/zero max was.)
|
|
395
|
+
if (scrollToHash()) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
scrollToTop();
|
|
389
399
|
}
|
|
390
400
|
|
|
391
401
|
/**
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./merge-segment-loaders.js";
|
|
7
7
|
import { assertSegmentStructure } from "./segment-structure-assert.js";
|
|
8
8
|
import { splitInterceptSegments } from "./intercept-utils.js";
|
|
9
|
+
import { debugLog } from "./logging.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Determines the merging behavior for segment reconciliation.
|
|
@@ -85,14 +86,29 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
85
86
|
const cachedSegments = new Map<string, ResolvedSegment>();
|
|
86
87
|
input.cachedSegments.forEach((s) => cachedSegments.set(s.id, s));
|
|
87
88
|
|
|
89
|
+
const diffSet = new Set(diff);
|
|
90
|
+
debugLog(
|
|
91
|
+
`[reconcile] actor=${actor}, matched=${matched.length}, diff=${diff.length}`,
|
|
92
|
+
);
|
|
93
|
+
debugLog(
|
|
94
|
+
`[reconcile] server segments: ${[...serverSegments.keys()].join(", ")}`,
|
|
95
|
+
);
|
|
96
|
+
debugLog(
|
|
97
|
+
`[reconcile] cached segments: ${[...cachedSegments.keys()].join(", ")}`,
|
|
98
|
+
);
|
|
99
|
+
|
|
88
100
|
const segments = matched
|
|
89
101
|
.map((segId: string) => {
|
|
90
102
|
const fromServer = serverSegments.get(segId);
|
|
91
103
|
const fromCache = cachedSegments.get(segId);
|
|
92
104
|
|
|
93
105
|
if (fromServer) {
|
|
106
|
+
const inDiff = diffSet.has(segId);
|
|
94
107
|
// Merge partial loader data when server returns fewer loaders than cached
|
|
95
108
|
if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
|
|
109
|
+
debugLog(
|
|
110
|
+
`[reconcile] ${segId}: MERGE loaders (server partial, ${inDiff ? "in diff" : "not in diff"})`,
|
|
111
|
+
);
|
|
96
112
|
return mergeSegmentLoaders(fromServer, fromCache);
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -143,8 +159,14 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
143
159
|
// above fails to preserve a value it should have.
|
|
144
160
|
assertSegmentStructure(fromCache, merged, context);
|
|
145
161
|
|
|
162
|
+
debugLog(
|
|
163
|
+
`[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
|
|
164
|
+
);
|
|
146
165
|
return merged;
|
|
147
166
|
}
|
|
167
|
+
debugLog(
|
|
168
|
+
`[reconcile] ${segId}: SERVER only (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, no cache entry)`,
|
|
169
|
+
);
|
|
148
170
|
return fromServer;
|
|
149
171
|
}
|
|
150
172
|
|
|
@@ -158,20 +180,20 @@ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
|
|
|
158
180
|
return fromCache;
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
debugLog(
|
|
184
|
+
`[reconcile] ${segId}: CACHE only (not from server, type=${fromCache.type}, component=${fromCache.component != null ? "yes" : "null"})`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Return the cached segment as-is, regardless of actor. We used to clear
|
|
188
|
+
// truthy `loading` here to prevent a stale Suspense fallback from
|
|
189
|
+
// committing against cached content, but that swapped the render tree
|
|
190
|
+
// from the LoaderBoundary branch to the plain OutletProvider branch
|
|
191
|
+
// inside renderSegments, causing React to unmount the entire chain
|
|
192
|
+
// (LoaderBoundary > Suspense > LoaderResolver > RouteContentWrapper >
|
|
193
|
+
// Suspender) every time the user opened an intercept or navigated back
|
|
194
|
+
// to a cached page. The flicker is now prevented by renderSegments'
|
|
195
|
+
// promise memoization keeping React's use() in "known fulfilled" state,
|
|
196
|
+
// so preserving `loading` keeps the element tree stable.
|
|
175
197
|
return fromCache;
|
|
176
198
|
})
|
|
177
199
|
.filter(Boolean) as ResolvedSegment[];
|
|
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
|
|
|
48
48
|
|
|
49
49
|
if (cachedCategory !== incomingCategory) {
|
|
50
50
|
console.warn(
|
|
51
|
-
`[
|
|
51
|
+
`[Rango] Tree structure mismatch detected in ${context} ` +
|
|
52
52
|
`for segment "${cached.id}": loading category changed from ` +
|
|
53
53
|
`"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
|
|
54
54
|
`"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
|
|
@@ -64,7 +64,7 @@ export function assertSegmentStructure(
|
|
|
64
64
|
const incomingHasMount = !!incoming.mountPath;
|
|
65
65
|
if (cachedHasMount !== incomingHasMount) {
|
|
66
66
|
console.warn(
|
|
67
|
-
`[
|
|
67
|
+
`[Rango] MountContextProvider mismatch detected in ${context} ` +
|
|
68
68
|
`for segment "${cached.id}": mountPath changed from ` +
|
|
69
69
|
`${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
|
|
70
70
|
`${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
|