@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +9 -0
- package/README.md +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- 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 +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -11,8 +11,7 @@ import { createEventController } from "./event-controller.js";
|
|
|
11
11
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
12
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
13
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
14
|
-
import { NavigationProvider
|
|
15
|
-
import { initThemeConfigSync } from "../theme/theme-context.js";
|
|
14
|
+
import { NavigationProvider } from "./react/index.js";
|
|
16
15
|
import type {
|
|
17
16
|
RscPayload,
|
|
18
17
|
RscBrowserDependencies,
|
|
@@ -22,6 +21,14 @@ import type {
|
|
|
22
21
|
} from "./types.js";
|
|
23
22
|
import type { EventController } from "./event-controller.js";
|
|
24
23
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
|
+
import { initRangoState } from "./rango-state.js";
|
|
25
|
+
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
26
|
+
import { setAppVersion } from "./app-version.js";
|
|
27
|
+
import {
|
|
28
|
+
isInterceptSegment,
|
|
29
|
+
splitInterceptSegments,
|
|
30
|
+
} from "./intercept-utils.js";
|
|
31
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
25
32
|
|
|
26
33
|
// Vite HMR types are provided by vite/client
|
|
27
34
|
|
|
@@ -89,7 +96,6 @@ export interface InitBrowserAppOptions {
|
|
|
89
96
|
* Only used when themeConfig is provided.
|
|
90
97
|
*/
|
|
91
98
|
initialTheme?: Theme;
|
|
92
|
-
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
/**
|
|
@@ -107,6 +113,15 @@ export interface BrowserAppContext {
|
|
|
107
113
|
initialTheme?: Theme;
|
|
108
114
|
/** Whether connection warmup is enabled */
|
|
109
115
|
warmupEnabled?: boolean;
|
|
116
|
+
/** App version for prefetch version mismatch detection */
|
|
117
|
+
version?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Live app-shell ref. Cross-app navigations replace its contents so the
|
|
120
|
+
* NavigationProvider and renderSegments pick up the target app's
|
|
121
|
+
* rootLayout, basename, and version without consumer rerenders. Theme,
|
|
122
|
+
* warmup, and prefetch TTL are document-lifetime (see AppShell).
|
|
123
|
+
*/
|
|
124
|
+
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
// Module-level state for the initialized app
|
|
@@ -122,18 +137,26 @@ let browserAppContext: BrowserAppContext | null = null;
|
|
|
122
137
|
* - Configures HMR support
|
|
123
138
|
*/
|
|
124
139
|
export async function initBrowserApp(
|
|
125
|
-
options: InitBrowserAppOptions
|
|
140
|
+
options: InitBrowserAppOptions,
|
|
126
141
|
): Promise<BrowserAppContext> {
|
|
127
|
-
const {
|
|
142
|
+
const {
|
|
143
|
+
rscStream,
|
|
144
|
+
deps,
|
|
145
|
+
storeOptions,
|
|
146
|
+
linkInterception = true,
|
|
147
|
+
themeConfig,
|
|
148
|
+
initialTheme,
|
|
149
|
+
} = options;
|
|
128
150
|
|
|
129
|
-
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
130
151
|
const initialPayload =
|
|
131
152
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
132
153
|
|
|
133
154
|
// Extract themeConfig and initialTheme from payload if not explicitly provided
|
|
134
155
|
// This allows virtual entries to work without importing the router
|
|
135
|
-
const effectiveThemeConfig =
|
|
136
|
-
|
|
156
|
+
const effectiveThemeConfig =
|
|
157
|
+
themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
|
|
158
|
+
const effectiveInitialTheme =
|
|
159
|
+
initialTheme ?? initialPayload.metadata?.initialTheme;
|
|
137
160
|
|
|
138
161
|
// Get initial segments and compute history key from current URL
|
|
139
162
|
const initialSegments = (initialPayload.metadata?.segments ??
|
|
@@ -149,20 +172,23 @@ export async function initBrowserApp(
|
|
|
149
172
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
150
173
|
});
|
|
151
174
|
|
|
175
|
+
// Seed router identity from the initial SSR payload so the first
|
|
176
|
+
// cross-app SPA navigation can detect the app switch.
|
|
177
|
+
if (initialPayload.metadata?.routerId) {
|
|
178
|
+
store.setRouterId?.(initialPayload.metadata.routerId);
|
|
179
|
+
}
|
|
180
|
+
|
|
152
181
|
// Create event controller for reactive state management
|
|
153
182
|
const eventController = createEventController({
|
|
154
183
|
initialLocation: new URL(window.location.href),
|
|
155
184
|
});
|
|
156
185
|
|
|
157
|
-
// Initialize segments state BEFORE hydration to avoid mismatch
|
|
158
|
-
initSegmentsSync(initialPayload.metadata?.matched, initialPayload.metadata?.pathname);
|
|
159
|
-
|
|
160
|
-
// Initialize theme config for MetaTags (must match SSR state)
|
|
161
|
-
initThemeConfigSync(effectiveThemeConfig);
|
|
162
|
-
|
|
163
186
|
// Initialize event controller with segment order (even without handles)
|
|
164
187
|
eventController.setHandleData({}, initialPayload.metadata?.matched);
|
|
165
188
|
|
|
189
|
+
// Initialize route params
|
|
190
|
+
eventController.setParams(initialPayload.metadata?.params ?? {});
|
|
191
|
+
|
|
166
192
|
// Initialize handle data from initial payload BEFORE hydration
|
|
167
193
|
// This ensures useHandle returns correct data during hydration to avoid mismatch
|
|
168
194
|
// The handles property is an async generator that yields on each push
|
|
@@ -172,28 +198,61 @@ export async function initBrowserApp(
|
|
|
172
198
|
for await (const handleData of handlesGenerator) {
|
|
173
199
|
lastHandleData = handleData;
|
|
174
200
|
}
|
|
175
|
-
// Initialize
|
|
176
|
-
eventController.setHandleData(
|
|
177
|
-
|
|
201
|
+
// Initialize event controller with initial handle state before hydration.
|
|
202
|
+
eventController.setHandleData(
|
|
203
|
+
lastHandleData,
|
|
204
|
+
initialPayload.metadata?.matched,
|
|
205
|
+
);
|
|
178
206
|
|
|
179
207
|
// Update the initial cache entry with the processed handleData
|
|
180
208
|
// The cache entry was created by createNavigationStore but without handleData
|
|
181
209
|
store.updateCacheHandleData(initialHistoryKey, lastHandleData);
|
|
182
210
|
}
|
|
183
211
|
|
|
184
|
-
|
|
185
212
|
// Create composable utilities
|
|
186
213
|
const client = createNavigationClient(deps);
|
|
187
214
|
|
|
188
|
-
//
|
|
189
|
-
|
|
215
|
+
// Capture the per-router app-shell so cross-app navigations can replace
|
|
216
|
+
// it atomically. rootLayout, basename, and version live here and are
|
|
217
|
+
// read through the ref at call time rather than closed over. Theme,
|
|
218
|
+
// warmup, and prefetch TTL are deliberately excluded — they are
|
|
219
|
+
// document-lifetime and stay stable across smooth cross-app transitions.
|
|
190
220
|
const version = initialPayload.metadata?.version;
|
|
221
|
+
const appShellRef = createAppShellRef({
|
|
222
|
+
routerId: initialPayload.metadata?.routerId,
|
|
223
|
+
rootLayout: initialPayload.metadata?.rootLayout,
|
|
224
|
+
basename: initialPayload.metadata?.basename,
|
|
225
|
+
version,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Initialize the localStorage state key for cache invalidation.
|
|
229
|
+
// The build version busts cached prefetches on deploy; the routerId
|
|
230
|
+
// namespaces the key so sibling apps on the same origin don't collide.
|
|
231
|
+
initRangoState(version ?? "0", initialPayload.metadata?.routerId);
|
|
232
|
+
setAppVersion(version);
|
|
233
|
+
|
|
234
|
+
// Initialize the in-memory prefetch cache TTL from server config.
|
|
235
|
+
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
236
|
+
const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
|
|
237
|
+
if (prefetchCacheTTL !== undefined) {
|
|
238
|
+
initPrefetchCache(prefetchCacheTTL);
|
|
239
|
+
}
|
|
191
240
|
|
|
192
|
-
// Create a bound renderSegments that
|
|
241
|
+
// Create a bound renderSegments that reads rootLayout through the shell
|
|
242
|
+
// ref. On app switch the ref is updated before the tree re-renders, so
|
|
243
|
+
// the new app's Document (rootLayout) replaces the previous one.
|
|
193
244
|
const renderSegments = (
|
|
194
245
|
segments: ResolvedSegment[],
|
|
195
|
-
options?: RenderSegmentsOptions
|
|
196
|
-
) =>
|
|
246
|
+
options?: RenderSegmentsOptions,
|
|
247
|
+
) =>
|
|
248
|
+
baseRenderSegments(segments, {
|
|
249
|
+
...options,
|
|
250
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Lazy reference for navigation bridge — the action bridge is created first
|
|
254
|
+
// but may need to trigger SPA navigation for action redirects.
|
|
255
|
+
let navigateFn: ((url: string, options?: any) => Promise<void>) | null = null;
|
|
197
256
|
|
|
198
257
|
// Setup server action bridge
|
|
199
258
|
const actionBridge = createServerActionBridge({
|
|
@@ -203,7 +262,13 @@ export async function initBrowserApp(
|
|
|
203
262
|
deps,
|
|
204
263
|
onUpdate: (update) => store.emitUpdate(update),
|
|
205
264
|
renderSegments,
|
|
206
|
-
|
|
265
|
+
onNavigate: (url, options) => {
|
|
266
|
+
if (!navigateFn) {
|
|
267
|
+
window.location.href = url;
|
|
268
|
+
return Promise.resolve();
|
|
269
|
+
}
|
|
270
|
+
return navigateFn(url, options);
|
|
271
|
+
},
|
|
207
272
|
});
|
|
208
273
|
actionBridge.register();
|
|
209
274
|
|
|
@@ -214,9 +279,13 @@ export async function initBrowserApp(
|
|
|
214
279
|
client,
|
|
215
280
|
onUpdate: (update) => store.emitUpdate(update),
|
|
216
281
|
renderSegments,
|
|
217
|
-
version,
|
|
282
|
+
version: version,
|
|
283
|
+
appShellRef,
|
|
218
284
|
});
|
|
219
285
|
|
|
286
|
+
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
287
|
+
navigateFn = (url, options) => navigationBridge.navigate(url, options);
|
|
288
|
+
|
|
220
289
|
// Optionally enable global link interception
|
|
221
290
|
if (linkInterception) {
|
|
222
291
|
navigationBridge.registerLinkInterception();
|
|
@@ -225,48 +294,139 @@ export async function initBrowserApp(
|
|
|
225
294
|
// Build initial tree with rootLayout
|
|
226
295
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
227
296
|
|
|
228
|
-
// Setup HMR
|
|
297
|
+
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
298
|
+
// fire many rsc:update events in quick succession. Without debouncing,
|
|
299
|
+
// each event triggers a fetchPartial() which on slow routes can pile up
|
|
300
|
+
// and overwhelm the worker (cross-request promise issues, 500s).
|
|
229
301
|
if (import.meta.hot) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const { payload, streamComplete } = await client.fetchPartial({
|
|
240
|
-
targetUrl: window.location.href,
|
|
241
|
-
segmentIds: [],
|
|
242
|
-
previousUrl: store.getSegmentState().currentUrl,
|
|
243
|
-
hmr: true,
|
|
244
|
-
});
|
|
302
|
+
let hmrTimer: ReturnType<typeof setTimeout> | null = null;
|
|
303
|
+
let hmrAbort: AbortController | null = null;
|
|
304
|
+
|
|
305
|
+
import.meta.hot.on("rsc:update", () => {
|
|
306
|
+
// Cancel any pending debounce timer
|
|
307
|
+
if (hmrTimer !== null) {
|
|
308
|
+
clearTimeout(hmrTimer);
|
|
309
|
+
}
|
|
245
310
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
311
|
+
// Abort any in-flight HMR fetch so it doesn't race with the next one
|
|
312
|
+
if (hmrAbort) {
|
|
313
|
+
hmrAbort.abort();
|
|
314
|
+
hmrAbort = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Debounce: wait 200ms of quiet before fetching
|
|
318
|
+
hmrTimer = setTimeout(async () => {
|
|
319
|
+
hmrTimer = null;
|
|
320
|
+
|
|
321
|
+
// Don't interrupt an active user navigation — startNavigation()
|
|
322
|
+
// would abort it and refetch the old URL (window.location.href
|
|
323
|
+
// hasn't updated yet). The user's navigation will pick up the
|
|
324
|
+
// new server code when it completes. isNavigating covers the
|
|
325
|
+
// full lifecycle (fetching + streaming, before commit) without
|
|
326
|
+
// blocking on server actions.
|
|
327
|
+
if (eventController.getState().isNavigating) {
|
|
328
|
+
console.log("[RSCRouter] HMR: Skipping — navigation in progress");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
249
331
|
|
|
250
|
-
|
|
251
|
-
store.setCurrentUrl(window.location.href);
|
|
332
|
+
console.log("[RSCRouter] HMR: Server update, refetching RSC");
|
|
252
333
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const currentHandleData = eventController.getHandleState().data;
|
|
256
|
-
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
334
|
+
const abort = new AbortController();
|
|
335
|
+
hmrAbort = abort;
|
|
257
336
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
337
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
338
|
+
replace: true,
|
|
339
|
+
});
|
|
340
|
+
const streamingToken = handle.startStreaming();
|
|
341
|
+
|
|
342
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
346
|
+
targetUrl: window.location.href,
|
|
347
|
+
segmentIds: [],
|
|
348
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
349
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
350
|
+
routerId: store.getRouterId?.(),
|
|
351
|
+
hmr: true,
|
|
352
|
+
signal: abort.signal,
|
|
261
353
|
});
|
|
262
|
-
}
|
|
263
354
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
355
|
+
if (abort.signal.aborted) return;
|
|
356
|
+
|
|
357
|
+
// If the server returned a non-RSC response (404, 500 without
|
|
358
|
+
// error boundary), the payload won't have valid metadata.
|
|
359
|
+
// Reload to recover rather than leaving the page stale.
|
|
360
|
+
if (!payload.metadata) {
|
|
361
|
+
throw new Error("HMR refetch returned invalid payload");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Update version BEFORE rebuilding state so that
|
|
365
|
+
// clearHistoryCache() runs first, then the fresh segment
|
|
366
|
+
// cache entry we create below survives.
|
|
367
|
+
const newVersion = payload.metadata.version;
|
|
368
|
+
if (newVersion && newVersion !== version) {
|
|
369
|
+
console.log(
|
|
370
|
+
"[RSCRouter] HMR: version changed",
|
|
371
|
+
version,
|
|
372
|
+
"→",
|
|
373
|
+
newVersion,
|
|
374
|
+
"clearing caches",
|
|
375
|
+
);
|
|
376
|
+
navigationBridge.updateVersion(newVersion);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (payload.metadata?.isPartial) {
|
|
380
|
+
const segments = payload.metadata.segments || [];
|
|
381
|
+
const matched = payload.metadata.matched || [];
|
|
382
|
+
|
|
383
|
+
// Derive intercept state from the returned payload, not the
|
|
384
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
385
|
+
// behavior, the response won't contain intercept segments.
|
|
386
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
387
|
+
|
|
388
|
+
// Sync store intercept state with what the server returned
|
|
389
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
390
|
+
store.setInterceptSourceUrl(null);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
store.setSegmentIds(matched);
|
|
394
|
+
store.setCurrentUrl(window.location.href);
|
|
395
|
+
|
|
396
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
397
|
+
intercept: responseIsIntercept,
|
|
398
|
+
});
|
|
399
|
+
store.setHistoryKey(historyKey);
|
|
400
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
401
|
+
store.cacheSegmentsForHistory(
|
|
402
|
+
historyKey,
|
|
403
|
+
segments,
|
|
404
|
+
currentHandleData,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
408
|
+
store.emitUpdate({
|
|
409
|
+
root: renderSegments(main, {
|
|
410
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
411
|
+
}),
|
|
412
|
+
metadata: payload.metadata,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await streamComplete;
|
|
417
|
+
handle.complete(new URL(window.location.href));
|
|
418
|
+
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (abort.signal.aborted) return;
|
|
421
|
+
console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
|
|
422
|
+
window.location.reload();
|
|
423
|
+
return;
|
|
424
|
+
} finally {
|
|
425
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
426
|
+
streamingToken.end();
|
|
427
|
+
handle[Symbol.dispose]();
|
|
428
|
+
}
|
|
429
|
+
}, 200);
|
|
270
430
|
});
|
|
271
431
|
}
|
|
272
432
|
|
|
@@ -280,6 +440,8 @@ export async function initBrowserApp(
|
|
|
280
440
|
themeConfig: effectiveThemeConfig,
|
|
281
441
|
initialTheme: effectiveInitialTheme,
|
|
282
442
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
443
|
+
version,
|
|
444
|
+
appShellRef,
|
|
283
445
|
};
|
|
284
446
|
browserAppContext = context;
|
|
285
447
|
|
|
@@ -292,7 +454,7 @@ export async function initBrowserApp(
|
|
|
292
454
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
293
455
|
if (!browserAppContext) {
|
|
294
456
|
throw new Error(
|
|
295
|
-
"RSCRouter: initBrowserApp() must be called before rendering RSCRouter"
|
|
457
|
+
"RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
|
|
296
458
|
);
|
|
297
459
|
}
|
|
298
460
|
return browserAppContext;
|
|
@@ -335,18 +497,38 @@ export interface RSCRouterProps {}
|
|
|
335
497
|
* ```
|
|
336
498
|
*/
|
|
337
499
|
export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
338
|
-
const {
|
|
339
|
-
|
|
500
|
+
const {
|
|
501
|
+
store,
|
|
502
|
+
eventController,
|
|
503
|
+
bridge,
|
|
504
|
+
initialPayload,
|
|
505
|
+
initialTree,
|
|
506
|
+
themeConfig,
|
|
507
|
+
initialTheme,
|
|
508
|
+
warmupEnabled,
|
|
509
|
+
version,
|
|
510
|
+
appShellRef,
|
|
511
|
+
} = getBrowserAppContext();
|
|
512
|
+
|
|
513
|
+
// Signal that the React tree has hydrated. useEffect only fires after
|
|
514
|
+
// hydration completes, so this attribute is a stable readiness marker
|
|
515
|
+
// that does not depend on React internals like __reactFiber.
|
|
516
|
+
React.useEffect(() => {
|
|
517
|
+
document.documentElement.dataset.hydrated = "";
|
|
518
|
+
}, []);
|
|
340
519
|
|
|
341
520
|
return (
|
|
342
521
|
<NavigationProvider
|
|
343
522
|
store={store}
|
|
344
523
|
eventController={eventController}
|
|
345
|
-
initialPayload={{
|
|
524
|
+
initialPayload={{ root: initialTree, metadata: initialPayload.metadata! }}
|
|
346
525
|
bridge={bridge}
|
|
347
526
|
themeConfig={themeConfig}
|
|
348
527
|
initialTheme={initialTheme}
|
|
349
528
|
warmupEnabled={warmupEnabled}
|
|
529
|
+
version={version}
|
|
530
|
+
basename={initialPayload.metadata?.basename}
|
|
531
|
+
appShellRef={appShellRef}
|
|
350
532
|
/>
|
|
351
533
|
);
|
|
352
534
|
}
|