@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
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 +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -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 +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- 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-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -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 +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- 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 +188 -93
- 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 +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useContext, useMemo } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { prefetchDirect } from "../prefetch/fetch.js";
|
|
6
|
+
import { getAppVersion } from "../app-version.js";
|
|
6
7
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -12,6 +13,10 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
|
12
13
|
* useRouter() do not re-render on navigation state changes.
|
|
13
14
|
* For reactive navigation state, use useNavigation() instead.
|
|
14
15
|
*
|
|
16
|
+
* Methods read `basename` from the live context on each call so that
|
|
17
|
+
* cross-app navigation (app-switch) sees the current app's basename
|
|
18
|
+
* rather than the one captured at mount time.
|
|
19
|
+
*
|
|
15
20
|
* @example
|
|
16
21
|
* ```tsx
|
|
17
22
|
* const router = useRouter();
|
|
@@ -28,15 +33,26 @@ export function useRouter(): RouterInstance {
|
|
|
28
33
|
throw new Error("useRouter must be used within NavigationProvider");
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
// Stable reference: ctx is
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// Stable reference: ctx itself is stable, and reads on each method call
|
|
37
|
+
// pick up live basename values from the context (backed by a live ref
|
|
38
|
+
// in NavigationProvider), so app-switch transitions are reflected without
|
|
39
|
+
// recreating this object.
|
|
40
|
+
return useMemo<RouterInstance>(() => {
|
|
41
|
+
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
42
|
+
function withBasename(url: string): string {
|
|
43
|
+
const bn = ctx!.basename;
|
|
44
|
+
if (!bn || !url.startsWith("/") || url.startsWith(bn + "/") || url === bn)
|
|
45
|
+
return url;
|
|
46
|
+
return url === "/" ? bn : bn + url;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
34
50
|
push(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
35
|
-
return ctx.navigate(url, { ...options, replace: false });
|
|
51
|
+
return ctx.navigate(withBasename(url), { ...options, replace: false });
|
|
36
52
|
},
|
|
37
53
|
|
|
38
54
|
replace(url: string, options?: RouterNavigateOptions): Promise<void> {
|
|
39
|
-
return ctx.navigate(url, { ...options, replace: true });
|
|
55
|
+
return ctx.navigate(withBasename(url), { ...options, replace: true });
|
|
40
56
|
},
|
|
41
57
|
|
|
42
58
|
refresh(): Promise<void> {
|
|
@@ -46,18 +62,35 @@ export function useRouter(): RouterInstance {
|
|
|
46
62
|
prefetch(url: string): void {
|
|
47
63
|
const segmentState = ctx.store?.getSegmentState();
|
|
48
64
|
if (segmentState) {
|
|
49
|
-
prefetchDirect(
|
|
65
|
+
prefetchDirect(
|
|
66
|
+
withBasename(url),
|
|
67
|
+
segmentState.currentSegmentIds,
|
|
68
|
+
getAppVersion(),
|
|
69
|
+
ctx.store?.getRouterId?.(),
|
|
70
|
+
);
|
|
50
71
|
}
|
|
51
72
|
},
|
|
52
73
|
|
|
53
74
|
back(): void {
|
|
54
|
-
|
|
75
|
+
// Avoid escaping the host on the first entry of this session.
|
|
76
|
+
// Prefer the Navigation API; fall back to the router-stamped
|
|
77
|
+
// history.state.idx (set by pushHistoryWithIdx) for older browsers.
|
|
78
|
+
const nav = (window as { navigation?: { canGoBack: boolean } })
|
|
79
|
+
.navigation;
|
|
80
|
+
const canGoBack =
|
|
81
|
+
nav && typeof nav.canGoBack === "boolean"
|
|
82
|
+
? nav.canGoBack
|
|
83
|
+
: ((window.history.state as { idx?: number } | null)?.idx ?? 0) > 0;
|
|
84
|
+
if (canGoBack) {
|
|
85
|
+
window.history.back();
|
|
86
|
+
} else {
|
|
87
|
+
ctx.navigate(withBasename("/"), { replace: true });
|
|
88
|
+
}
|
|
55
89
|
},
|
|
56
90
|
|
|
57
91
|
forward(): void {
|
|
58
92
|
window.history.forward();
|
|
59
93
|
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
);
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
63
96
|
}
|
|
@@ -25,15 +25,18 @@ function parsePathname(pathname: string): string[] {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Build segments state from event controller
|
|
28
|
+
* Build segments state from event controller. `segmentIds` is the
|
|
29
|
+
* route-only list (parallels and loaders stripped) — distinct from the
|
|
30
|
+
* controller's `segmentOrder` which drives handle collection and includes
|
|
31
|
+
* parallel slot ids.
|
|
29
32
|
*/
|
|
30
33
|
function buildSegmentsState(
|
|
31
34
|
location: URL,
|
|
32
|
-
|
|
35
|
+
routeSegmentIds: string[],
|
|
33
36
|
): SegmentsState {
|
|
34
37
|
return {
|
|
35
38
|
path: parsePathname(location.pathname),
|
|
36
|
-
segmentIds:
|
|
39
|
+
segmentIds: routeSegmentIds,
|
|
37
40
|
location,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
@@ -74,7 +77,7 @@ export function useSegments<T>(
|
|
|
74
77
|
const handleState = ctx.eventController.getHandleState();
|
|
75
78
|
const segmentsState = buildSegmentsState(
|
|
76
79
|
location as URL,
|
|
77
|
-
handleState.
|
|
80
|
+
handleState.routeSegmentIds,
|
|
78
81
|
);
|
|
79
82
|
return selector ? selector(segmentsState) : segmentsState;
|
|
80
83
|
});
|
|
@@ -94,7 +97,7 @@ export function useSegments<T>(
|
|
|
94
97
|
// render-time setState calls.
|
|
95
98
|
const segmentsCache = useRef<{
|
|
96
99
|
location: URL;
|
|
97
|
-
|
|
100
|
+
routeSegmentIds: string[];
|
|
98
101
|
state: SegmentsState;
|
|
99
102
|
} | null>(null);
|
|
100
103
|
|
|
@@ -113,17 +116,17 @@ export function useSegments<T>(
|
|
|
113
116
|
if (
|
|
114
117
|
cache &&
|
|
115
118
|
cache.location === location &&
|
|
116
|
-
cache.
|
|
119
|
+
cache.routeSegmentIds === handleState.routeSegmentIds
|
|
117
120
|
) {
|
|
118
121
|
segmentsState = cache.state;
|
|
119
122
|
} else {
|
|
120
123
|
segmentsState = buildSegmentsState(
|
|
121
124
|
location as URL,
|
|
122
|
-
handleState.
|
|
125
|
+
handleState.routeSegmentIds,
|
|
123
126
|
);
|
|
124
127
|
segmentsCache.current = {
|
|
125
128
|
location: location as URL,
|
|
126
|
-
|
|
129
|
+
routeSegmentIds: handleState.routeSegmentIds,
|
|
127
130
|
state: segmentsState,
|
|
128
131
|
};
|
|
129
132
|
}
|
|
@@ -24,6 +24,31 @@ export function emptyResponse(): Response {
|
|
|
24
24
|
return new Response(null, { status: 200 });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Handle the X-RSC-Reload control header (server requests a full page reload on
|
|
29
|
+
* a version mismatch). Returns a short-circuit response when the header is
|
|
30
|
+
* present -- emptyResponse() if the URL was blocked by origin validation, or a
|
|
31
|
+
* never-resolving promise while the page reloads -- and null when absent, so
|
|
32
|
+
* the caller continues processing (e.g. the X-RSC-Redirect check). Scoped to
|
|
33
|
+
* X-RSC-Reload only; redirect handling differs between callers.
|
|
34
|
+
*/
|
|
35
|
+
export function handleReloadHeader(
|
|
36
|
+
response: Response,
|
|
37
|
+
opts: { onBlocked: () => void; onReload: (url: string) => void },
|
|
38
|
+
): Response | Promise<Response> | null {
|
|
39
|
+
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
40
|
+
if (reload === "blocked") {
|
|
41
|
+
opts.onBlocked();
|
|
42
|
+
return emptyResponse();
|
|
43
|
+
}
|
|
44
|
+
if (reload) {
|
|
45
|
+
opts.onReload(reload.url);
|
|
46
|
+
window.location.href = reload.url;
|
|
47
|
+
return new Promise<Response>(() => {});
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
/**
|
|
28
53
|
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
54
|
* Returns a new Response with one branch; the other is consumed to detect
|
|
@@ -23,10 +23,12 @@ import type { EventController } from "./event-controller.js";
|
|
|
23
23
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
24
|
import { initRangoState } from "./rango-state.js";
|
|
25
25
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
26
|
+
import { setAppVersion } from "./app-version.js";
|
|
26
27
|
import {
|
|
27
28
|
isInterceptSegment,
|
|
28
29
|
splitInterceptSegments,
|
|
29
30
|
} from "./intercept-utils.js";
|
|
31
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
30
32
|
|
|
31
33
|
// Vite HMR types are provided by vite/client
|
|
32
34
|
|
|
@@ -113,13 +115,20 @@ export interface BrowserAppContext {
|
|
|
113
115
|
warmupEnabled?: boolean;
|
|
114
116
|
/** App version for prefetch version mismatch detection */
|
|
115
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;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
// Module-level state for the initialized app
|
|
119
128
|
let browserAppContext: BrowserAppContext | null = null;
|
|
120
129
|
|
|
121
130
|
/**
|
|
122
|
-
* Initialize the browser app. Must be called before rendering
|
|
131
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
123
132
|
*
|
|
124
133
|
* This function:
|
|
125
134
|
* - Loads the initial RSC payload from the stream
|
|
@@ -139,7 +148,6 @@ export async function initBrowserApp(
|
|
|
139
148
|
initialTheme,
|
|
140
149
|
} = options;
|
|
141
150
|
|
|
142
|
-
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
143
151
|
const initialPayload =
|
|
144
152
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
145
153
|
|
|
@@ -164,6 +172,12 @@ export async function initBrowserApp(
|
|
|
164
172
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
165
173
|
});
|
|
166
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
|
+
|
|
167
181
|
// Create event controller for reactive state management
|
|
168
182
|
const eventController = createEventController({
|
|
169
183
|
initialLocation: new URL(window.location.href),
|
|
@@ -198,13 +212,24 @@ export async function initBrowserApp(
|
|
|
198
212
|
// Create composable utilities
|
|
199
213
|
const client = createNavigationClient(deps);
|
|
200
214
|
|
|
201
|
-
//
|
|
202
|
-
|
|
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.
|
|
203
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
|
+
});
|
|
204
227
|
|
|
205
228
|
// Initialize the localStorage state key for cache invalidation.
|
|
206
|
-
//
|
|
207
|
-
|
|
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);
|
|
208
233
|
|
|
209
234
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
210
235
|
// A value of 0 disables the cache; undefined falls back to the module default.
|
|
@@ -213,11 +238,17 @@ export async function initBrowserApp(
|
|
|
213
238
|
initPrefetchCache(prefetchCacheTTL);
|
|
214
239
|
}
|
|
215
240
|
|
|
216
|
-
// 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.
|
|
217
244
|
const renderSegments = (
|
|
218
245
|
segments: ResolvedSegment[],
|
|
219
246
|
options?: RenderSegmentsOptions,
|
|
220
|
-
) =>
|
|
247
|
+
) =>
|
|
248
|
+
baseRenderSegments(segments, {
|
|
249
|
+
...options,
|
|
250
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
251
|
+
});
|
|
221
252
|
|
|
222
253
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
223
254
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -231,7 +262,6 @@ export async function initBrowserApp(
|
|
|
231
262
|
deps,
|
|
232
263
|
onUpdate: (update) => store.emitUpdate(update),
|
|
233
264
|
renderSegments,
|
|
234
|
-
version,
|
|
235
265
|
onNavigate: (url, options) => {
|
|
236
266
|
if (!navigateFn) {
|
|
237
267
|
window.location.href = url;
|
|
@@ -249,7 +279,8 @@ export async function initBrowserApp(
|
|
|
249
279
|
client,
|
|
250
280
|
onUpdate: (update) => store.emitUpdate(update),
|
|
251
281
|
renderSegments,
|
|
252
|
-
version,
|
|
282
|
+
version: version,
|
|
283
|
+
appShellRef,
|
|
253
284
|
});
|
|
254
285
|
|
|
255
286
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -263,75 +294,157 @@ export async function initBrowserApp(
|
|
|
263
294
|
// Build initial tree with rootLayout
|
|
264
295
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
265
296
|
|
|
266
|
-
// 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).
|
|
267
301
|
if (import.meta.hot) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
}
|
|
310
|
+
|
|
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("[Rango] HMR: Skipping — navigation in progress");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
333
|
+
|
|
334
|
+
const abort = new AbortController();
|
|
335
|
+
hmrAbort = abort;
|
|
336
|
+
|
|
337
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
338
|
+
replace: true,
|
|
285
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,
|
|
353
|
+
});
|
|
286
354
|
|
|
287
|
-
|
|
288
|
-
const segments = payload.metadata.segments || [];
|
|
289
|
-
const matched = payload.metadata.matched || [];
|
|
355
|
+
if (abort.signal.aborted) return;
|
|
290
356
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
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
|
+
}
|
|
295
363
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
364
|
+
// Update version BEFORE rebuilding state so that
|
|
365
|
+
// clearHistoryCache() runs first, then the fresh segment
|
|
366
|
+
// cache entry we create below survives.
|
|
367
|
+
//
|
|
368
|
+
// Compare against the bridge's live version, not the init-time
|
|
369
|
+
// `version` const: after the first HMR bump the const is stale, so a
|
|
370
|
+
// later update with an unchanged version would otherwise re-clear the
|
|
371
|
+
// cache and re-broadcast across tabs/apps. The live read fires only
|
|
372
|
+
// on a genuine version change.
|
|
373
|
+
const newVersion = payload.metadata.version;
|
|
374
|
+
const currentVersion = navigationBridge.getVersion();
|
|
375
|
+
if (newVersion && newVersion !== currentVersion) {
|
|
376
|
+
console.log(
|
|
377
|
+
"[Rango] HMR: version changed",
|
|
378
|
+
currentVersion,
|
|
379
|
+
"→",
|
|
380
|
+
newVersion,
|
|
381
|
+
"clearing caches",
|
|
382
|
+
);
|
|
383
|
+
navigationBridge.updateVersion(newVersion);
|
|
299
384
|
}
|
|
300
385
|
|
|
301
|
-
|
|
302
|
-
|
|
386
|
+
// Apply only partial segment updates. A non-partial payload during
|
|
387
|
+
// HMR is transient: the worker route table is still rebuilding after
|
|
388
|
+
// the edit, so the URL momentarily resolves to not-found/catch-all.
|
|
389
|
+
// Skip it -- the debounced follow-up refetch returns the settled
|
|
390
|
+
// route's partial payload and renders it below. We never reload here:
|
|
391
|
+
// a paramless document GET would run the SSR path and surface the
|
|
392
|
+
// not-found page during that same transient.
|
|
393
|
+
if (payload.metadata?.isPartial) {
|
|
394
|
+
const segments = payload.metadata.segments || [];
|
|
395
|
+
const matched = payload.metadata.matched || [];
|
|
396
|
+
|
|
397
|
+
// Derive intercept state from the returned payload, not the
|
|
398
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
399
|
+
// behavior, the response won't contain intercept segments.
|
|
400
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
401
|
+
|
|
402
|
+
// Sync store intercept state with what the server returned
|
|
403
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
404
|
+
store.setInterceptSourceUrl(null);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
store.setSegmentIds(matched);
|
|
408
|
+
store.setCurrentUrl(window.location.href);
|
|
409
|
+
|
|
410
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
411
|
+
intercept: responseIsIntercept,
|
|
412
|
+
});
|
|
413
|
+
store.setHistoryKey(historyKey);
|
|
414
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
415
|
+
store.cacheSegmentsForHistory(
|
|
416
|
+
historyKey,
|
|
417
|
+
segments,
|
|
418
|
+
currentHandleData,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
422
|
+
store.emitUpdate({
|
|
423
|
+
root: renderSegments(main, {
|
|
424
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
425
|
+
}),
|
|
426
|
+
metadata: payload.metadata,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
303
429
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
store.emitUpdate({
|
|
317
|
-
root: renderSegments(main, {
|
|
318
|
-
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
319
|
-
}),
|
|
320
|
-
metadata: payload.metadata,
|
|
321
|
-
});
|
|
430
|
+
await streamComplete;
|
|
431
|
+
handle.complete(new URL(window.location.href));
|
|
432
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
433
|
+
} catch (err) {
|
|
434
|
+
if (abort.signal.aborted) return;
|
|
435
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
436
|
+
window.location.reload();
|
|
437
|
+
return;
|
|
438
|
+
} finally {
|
|
439
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
440
|
+
streamingToken.end();
|
|
441
|
+
handle[Symbol.dispose]();
|
|
322
442
|
}
|
|
323
|
-
|
|
324
|
-
await streamComplete;
|
|
325
|
-
handle.complete(new URL(window.location.href));
|
|
326
|
-
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
327
|
-
} finally {
|
|
328
|
-
streamingToken.end();
|
|
329
|
-
handle[Symbol.dispose]();
|
|
330
|
-
}
|
|
443
|
+
}, 200);
|
|
331
444
|
});
|
|
332
445
|
}
|
|
333
446
|
|
|
334
|
-
// Store context for
|
|
447
|
+
// Store context for Rango component
|
|
335
448
|
const context: BrowserAppContext = {
|
|
336
449
|
store,
|
|
337
450
|
eventController,
|
|
@@ -342,6 +455,7 @@ export async function initBrowserApp(
|
|
|
342
455
|
initialTheme: effectiveInitialTheme,
|
|
343
456
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
344
457
|
version,
|
|
458
|
+
appShellRef,
|
|
345
459
|
};
|
|
346
460
|
browserAppContext = context;
|
|
347
461
|
|
|
@@ -354,7 +468,7 @@ export async function initBrowserApp(
|
|
|
354
468
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
355
469
|
if (!browserAppContext) {
|
|
356
470
|
throw new Error(
|
|
357
|
-
"
|
|
471
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
358
472
|
);
|
|
359
473
|
}
|
|
360
474
|
return browserAppContext;
|
|
@@ -368,18 +482,18 @@ export function resetBrowserAppContext(): void {
|
|
|
368
482
|
}
|
|
369
483
|
|
|
370
484
|
/**
|
|
371
|
-
* Props for the
|
|
485
|
+
* Props for the Rango component
|
|
372
486
|
*/
|
|
373
|
-
export interface
|
|
487
|
+
export interface RangoProps {}
|
|
374
488
|
|
|
375
489
|
/**
|
|
376
|
-
*
|
|
490
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
377
491
|
*
|
|
378
492
|
* Must be called after initBrowserApp() has completed.
|
|
379
493
|
*
|
|
380
494
|
* @example
|
|
381
495
|
* ```tsx
|
|
382
|
-
* import { initBrowserApp,
|
|
496
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
383
497
|
* import { rscStream } from "rsc-html-stream/client";
|
|
384
498
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
385
499
|
*
|
|
@@ -389,14 +503,14 @@ export interface RSCRouterProps {}
|
|
|
389
503
|
* hydrateRoot(
|
|
390
504
|
* document,
|
|
391
505
|
* <React.StrictMode>
|
|
392
|
-
* <
|
|
506
|
+
* <Rango />
|
|
393
507
|
* </React.StrictMode>
|
|
394
508
|
* );
|
|
395
509
|
* }
|
|
396
510
|
* main();
|
|
397
511
|
* ```
|
|
398
512
|
*/
|
|
399
|
-
export function
|
|
513
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
400
514
|
const {
|
|
401
515
|
store,
|
|
402
516
|
eventController,
|
|
@@ -407,6 +521,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
407
521
|
initialTheme,
|
|
408
522
|
warmupEnabled,
|
|
409
523
|
version,
|
|
524
|
+
appShellRef,
|
|
410
525
|
} = getBrowserAppContext();
|
|
411
526
|
|
|
412
527
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -426,6 +541,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
426
541
|
initialTheme={initialTheme}
|
|
427
542
|
warmupEnabled={warmupEnabled}
|
|
428
543
|
version={version}
|
|
544
|
+
basename={initialPayload.metadata?.basename}
|
|
545
|
+
appShellRef={appShellRef}
|
|
429
546
|
/>
|
|
430
547
|
);
|
|
431
548
|
}
|