@rangojs/router 0.0.0-experimental.2
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/CLAUDE.md +7 -0
- package/README.md +19 -0
- package/dist/vite/index.js +1298 -0
- package/package.json +140 -0
- package/skills/caching/SKILL.md +319 -0
- package/skills/document-cache/SKILL.md +152 -0
- package/skills/hooks/SKILL.md +359 -0
- package/skills/intercept/SKILL.md +292 -0
- package/skills/layout/SKILL.md +216 -0
- package/skills/loader/SKILL.md +365 -0
- package/skills/middleware/SKILL.md +442 -0
- package/skills/parallel/SKILL.md +255 -0
- package/skills/route/SKILL.md +141 -0
- package/skills/router-setup/SKILL.md +403 -0
- package/skills/theme/SKILL.md +54 -0
- package/skills/typesafety/SKILL.md +352 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/__tests__/urls.test.tsx +436 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +893 -0
- package/src/browser/navigation-client.ts +162 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +559 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +275 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-href.tsx +208 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +353 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +464 -0
- package/src/cache/__tests__/document-cache.test.ts +522 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +387 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +621 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href-context.ts +33 -0
- package/src/href.ts +177 -0
- package/src/index.rsc.ts +79 -0
- package/src/index.ts +87 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1371 -0
- package/src/route-map-builder.ts +146 -0
- package/src/route-types.ts +198 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +138 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +266 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +214 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +272 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3876 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +1060 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +237 -0
- package/src/segment-system.tsx +456 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +417 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +146 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +234 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/__tests__/theme.test.ts +120 -0
- package/src/theme/constants.ts +55 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1561 -0
- package/src/urls.ts +726 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +787 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { RequestController, DisposableAbortController } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// Polyfill Symbol.dispose for Safari and older browsers
|
|
4
|
+
if (typeof Symbol.dispose === "undefined") {
|
|
5
|
+
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a request controller for managing concurrent abort controllers
|
|
10
|
+
*
|
|
11
|
+
* This utility helps manage concurrent navigation requests by providing
|
|
12
|
+
* a way to abort all pending requests when a new navigation starts.
|
|
13
|
+
*
|
|
14
|
+
* @returns RequestController instance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const controller = createRequestController();
|
|
19
|
+
*
|
|
20
|
+
* // Start a new request
|
|
21
|
+
* const abortController = controller.create();
|
|
22
|
+
* fetch(url, { signal: abortController.signal });
|
|
23
|
+
*
|
|
24
|
+
* // Abort all pending requests (e.g., when starting new navigation)
|
|
25
|
+
* controller.abortAll();
|
|
26
|
+
*
|
|
27
|
+
* // Clean up completed request
|
|
28
|
+
* controller.remove(abortController);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createRequestController(): RequestController {
|
|
32
|
+
// Navigation controllers - aborted on new navigation
|
|
33
|
+
// Using WeakRef to allow GC if controller is no longer referenced elsewhere
|
|
34
|
+
const controllers: WeakRef<AbortController>[] = [];
|
|
35
|
+
// Action controllers - NOT aborted by navigation, only by errors
|
|
36
|
+
const actionControllers: WeakRef<AbortController>[] = [];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Remove stale (garbage collected) refs from an array
|
|
40
|
+
*/
|
|
41
|
+
function pruneStaleRefs(refs: WeakRef<AbortController>[]): void {
|
|
42
|
+
for (let i = refs.length - 1; i >= 0; i--) {
|
|
43
|
+
if (!refs[i].deref()) {
|
|
44
|
+
refs.splice(i, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* Create a new abort controller and track it for navigation
|
|
52
|
+
*
|
|
53
|
+
* @returns A new AbortController
|
|
54
|
+
*/
|
|
55
|
+
create(): AbortController {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
controllers.push(new WeakRef(controller));
|
|
58
|
+
console.log(
|
|
59
|
+
`[Browser] Created abort controller, total: ${controllers.length}`,
|
|
60
|
+
);
|
|
61
|
+
return controller;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a disposable abort controller for navigation use with `using` keyword
|
|
66
|
+
*
|
|
67
|
+
* The controller will be automatically removed from tracking when
|
|
68
|
+
* it goes out of scope, regardless of how the scope is exited.
|
|
69
|
+
*
|
|
70
|
+
* @returns A DisposableAbortController
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* async function handleNavigation() {
|
|
75
|
+
* requestController.abortAll();
|
|
76
|
+
* using { controller } = requestController.createDisposable();
|
|
77
|
+
* // ... use controller.signal ...
|
|
78
|
+
* // controller is automatically removed on scope exit
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
createDisposable(): DisposableAbortController {
|
|
83
|
+
const controller = this.create();
|
|
84
|
+
return {
|
|
85
|
+
controller,
|
|
86
|
+
[Symbol.dispose]: () => {
|
|
87
|
+
this.remove(controller);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a disposable abort controller for actions
|
|
94
|
+
*
|
|
95
|
+
* Action controllers are NOT aborted by navigation - they complete
|
|
96
|
+
* independently. Only aborted by abortAllActions() on error.
|
|
97
|
+
*
|
|
98
|
+
* @returns A DisposableAbortController
|
|
99
|
+
*/
|
|
100
|
+
createActionDisposable(): DisposableAbortController {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const ref = new WeakRef(controller);
|
|
103
|
+
actionControllers.push(ref);
|
|
104
|
+
console.log(
|
|
105
|
+
`[Browser] Created action controller, total: ${actionControllers.length}`,
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
controller,
|
|
109
|
+
[Symbol.dispose]: () => {
|
|
110
|
+
const index = actionControllers.indexOf(ref);
|
|
111
|
+
if (index !== -1) {
|
|
112
|
+
actionControllers.splice(index, 1);
|
|
113
|
+
console.log(
|
|
114
|
+
`[Browser] Removed action controller, remaining: ${actionControllers.length}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Abort all navigation controllers (NOT actions)
|
|
123
|
+
*
|
|
124
|
+
* Called when starting new navigation. Actions continue
|
|
125
|
+
* to complete in the background.
|
|
126
|
+
*/
|
|
127
|
+
abortAll(): void {
|
|
128
|
+
controllers.forEach((ref) => ref.deref()?.abort());
|
|
129
|
+
controllers.length = 0;
|
|
130
|
+
console.log(`[Browser] Aborted all navigation controllers`);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Abort all action controllers
|
|
135
|
+
*
|
|
136
|
+
* Called when an action error occurs - prevents other actions
|
|
137
|
+
* from completing and overwriting the error UI.
|
|
138
|
+
*/
|
|
139
|
+
abortAllActions(): void {
|
|
140
|
+
actionControllers.forEach((ref) => ref.deref()?.abort());
|
|
141
|
+
actionControllers.length = 0;
|
|
142
|
+
console.log(`[Browser] Aborted all action controllers`);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Remove a specific controller from tracking
|
|
147
|
+
*
|
|
148
|
+
* Call this when a request completes successfully.
|
|
149
|
+
*
|
|
150
|
+
* @param controller - The controller to remove
|
|
151
|
+
*/
|
|
152
|
+
remove(controller: AbortController): void {
|
|
153
|
+
// Prune any stale refs while searching
|
|
154
|
+
pruneStaleRefs(controllers);
|
|
155
|
+
const index = controllers.findIndex((ref) => ref.deref() === controller);
|
|
156
|
+
if (index !== -1) {
|
|
157
|
+
controllers.splice(index, 1);
|
|
158
|
+
console.log(
|
|
159
|
+
`[Browser] Removed abort controller, remaining: ${controllers.length}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
renderSegments as baseRenderSegments,
|
|
4
|
+
type RenderSegmentsOptions,
|
|
5
|
+
} from "../segment-system.js";
|
|
6
|
+
import {
|
|
7
|
+
createNavigationStore,
|
|
8
|
+
generateHistoryKey,
|
|
9
|
+
} from "./navigation-store.js";
|
|
10
|
+
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { createNavigationClient } from "./navigation-client.js";
|
|
12
|
+
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
|
+
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
14
|
+
import { NavigationProvider, initHandleDataSync, initSegmentsSync } from "./react/index.js";
|
|
15
|
+
import { initThemeConfigSync } from "../theme/theme-context.js";
|
|
16
|
+
import type {
|
|
17
|
+
RscPayload,
|
|
18
|
+
RscBrowserDependencies,
|
|
19
|
+
ResolvedSegment,
|
|
20
|
+
NavigationStore,
|
|
21
|
+
NavigationBridge,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
import type { EventController } from "./event-controller.js";
|
|
24
|
+
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
25
|
+
|
|
26
|
+
// Vite HMR types
|
|
27
|
+
declare global {
|
|
28
|
+
interface ImportMeta {
|
|
29
|
+
hot?: {
|
|
30
|
+
on(event: string, callback: () => void): void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for initializing the browser app
|
|
37
|
+
*/
|
|
38
|
+
export interface InitBrowserAppOptions {
|
|
39
|
+
/**
|
|
40
|
+
* RSC stream containing the initial payload (from rsc-html-stream/client)
|
|
41
|
+
*/
|
|
42
|
+
rscStream: ReadableStream<Uint8Array>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* RSC browser dependencies from @vitejs/plugin-rsc/browser
|
|
46
|
+
*/
|
|
47
|
+
deps: RscBrowserDependencies;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional store configuration
|
|
51
|
+
*/
|
|
52
|
+
storeOptions?: {
|
|
53
|
+
/**
|
|
54
|
+
* Maximum number of history entries to cache
|
|
55
|
+
* @default 10
|
|
56
|
+
*/
|
|
57
|
+
cacheSize?: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Enable global link interception for SPA navigation.
|
|
62
|
+
* When enabled, clicks on same-origin anchor elements are intercepted
|
|
63
|
+
* and handled via client-side navigation instead of full page loads.
|
|
64
|
+
*
|
|
65
|
+
* Links rendered with the Link component handle their own navigation
|
|
66
|
+
* regardless of this setting.
|
|
67
|
+
*
|
|
68
|
+
* Set to false to disable global interception and rely solely on
|
|
69
|
+
* Link components for SPA navigation.
|
|
70
|
+
*
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
linkInterception?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Theme configuration from router.
|
|
77
|
+
* When provided, enables theme support via useTheme hook.
|
|
78
|
+
* Pass router.themeConfig here to enable theme features.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* import { router } from "./router.js";
|
|
83
|
+
*
|
|
84
|
+
* await initBrowserApp({
|
|
85
|
+
* rscStream,
|
|
86
|
+
* deps: rscBrowser,
|
|
87
|
+
* themeConfig: router.themeConfig,
|
|
88
|
+
* initialTheme: document.documentElement.className.includes("dark") ? "dark" : "light",
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
themeConfig?: ResolvedThemeConfig | null;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Initial theme from server (typically read from cookie).
|
|
96
|
+
* Only used when themeConfig is provided.
|
|
97
|
+
*/
|
|
98
|
+
initialTheme?: Theme;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Result from initializing the browser app
|
|
103
|
+
*/
|
|
104
|
+
export interface BrowserAppContext {
|
|
105
|
+
store: NavigationStore;
|
|
106
|
+
eventController: EventController;
|
|
107
|
+
bridge: NavigationBridge;
|
|
108
|
+
initialPayload: RscPayload;
|
|
109
|
+
initialTree: React.ReactNode | Promise<React.ReactNode>;
|
|
110
|
+
/** Theme configuration (null if theme not enabled) */
|
|
111
|
+
themeConfig?: ResolvedThemeConfig | null;
|
|
112
|
+
/** Initial theme from server */
|
|
113
|
+
initialTheme?: Theme;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Module-level state for the initialized app
|
|
117
|
+
let browserAppContext: BrowserAppContext | null = null;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Initialize the browser app. Must be called before rendering RSCRouter.
|
|
121
|
+
*
|
|
122
|
+
* This function:
|
|
123
|
+
* - Loads the initial RSC payload from the stream
|
|
124
|
+
* - Creates the navigation store and event controller
|
|
125
|
+
* - Sets up action and navigation bridges
|
|
126
|
+
* - Configures HMR support
|
|
127
|
+
*/
|
|
128
|
+
export async function initBrowserApp(
|
|
129
|
+
options: InitBrowserAppOptions
|
|
130
|
+
): Promise<BrowserAppContext> {
|
|
131
|
+
const { rscStream, deps, storeOptions, linkInterception = true, themeConfig, initialTheme } = options;
|
|
132
|
+
|
|
133
|
+
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
134
|
+
const initialPayload =
|
|
135
|
+
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
136
|
+
|
|
137
|
+
// Extract themeConfig and initialTheme from payload if not explicitly provided
|
|
138
|
+
// This allows virtual entries to work without importing the router
|
|
139
|
+
const effectiveThemeConfig = themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
|
|
140
|
+
const effectiveInitialTheme = initialTheme ?? initialPayload.metadata?.initialTheme;
|
|
141
|
+
|
|
142
|
+
// Get initial segments and compute history key from current URL
|
|
143
|
+
const initialSegments = (initialPayload.metadata?.segments ??
|
|
144
|
+
[]) as ResolvedSegment[];
|
|
145
|
+
const initialHistoryKey = generateHistoryKey(window.location.href);
|
|
146
|
+
|
|
147
|
+
// Create navigation store with history-based caching
|
|
148
|
+
const store = createNavigationStore({
|
|
149
|
+
initialLocation: window.location,
|
|
150
|
+
initialSegmentIds: initialSegments.map((s) => s.id),
|
|
151
|
+
initialHistoryKey,
|
|
152
|
+
initialSegments,
|
|
153
|
+
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Create event controller for reactive state management
|
|
157
|
+
const eventController = createEventController({
|
|
158
|
+
initialLocation: new URL(window.location.href),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Initialize segments state BEFORE hydration to avoid mismatch
|
|
162
|
+
initSegmentsSync(initialPayload.metadata?.matched, initialPayload.metadata?.pathname);
|
|
163
|
+
|
|
164
|
+
// Initialize theme config for MetaTags (must match SSR state)
|
|
165
|
+
initThemeConfigSync(effectiveThemeConfig);
|
|
166
|
+
|
|
167
|
+
// Initialize event controller with segment order (even without handles)
|
|
168
|
+
eventController.setHandleData({}, initialPayload.metadata?.matched);
|
|
169
|
+
|
|
170
|
+
// Initialize handle data from initial payload BEFORE hydration
|
|
171
|
+
// This ensures useHandle returns correct data during hydration to avoid mismatch
|
|
172
|
+
// The handles property is an async generator that yields on each push
|
|
173
|
+
if (initialPayload.metadata?.handles) {
|
|
174
|
+
const handlesGenerator = initialPayload.metadata.handles;
|
|
175
|
+
let lastHandleData: Record<string, Record<string, unknown[]>> = {};
|
|
176
|
+
for await (const handleData of handlesGenerator) {
|
|
177
|
+
lastHandleData = handleData;
|
|
178
|
+
}
|
|
179
|
+
// Initialize both event controller AND module-level SSR state for hydration compatibility
|
|
180
|
+
eventController.setHandleData(lastHandleData, initialPayload.metadata?.matched);
|
|
181
|
+
initHandleDataSync(lastHandleData, initialPayload.metadata?.matched);
|
|
182
|
+
|
|
183
|
+
// Update the initial cache entry with the processed handleData
|
|
184
|
+
// The cache entry was created by createNavigationStore but without handleData
|
|
185
|
+
store.updateCacheHandleData(initialHistoryKey, lastHandleData);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
// Create composable utilities
|
|
190
|
+
const client = createNavigationClient(deps);
|
|
191
|
+
|
|
192
|
+
// Extract rootLayout and version from metadata for browser-side re-renders
|
|
193
|
+
const rootLayout = initialPayload.metadata?.rootLayout;
|
|
194
|
+
const version = initialPayload.metadata?.version;
|
|
195
|
+
|
|
196
|
+
// Create a bound renderSegments that includes rootLayout
|
|
197
|
+
const renderSegments = (
|
|
198
|
+
segments: ResolvedSegment[],
|
|
199
|
+
options?: RenderSegmentsOptions
|
|
200
|
+
) => baseRenderSegments(segments, { ...options, rootLayout });
|
|
201
|
+
|
|
202
|
+
// Setup server action bridge
|
|
203
|
+
const actionBridge = createServerActionBridge({
|
|
204
|
+
store,
|
|
205
|
+
eventController,
|
|
206
|
+
client,
|
|
207
|
+
deps,
|
|
208
|
+
onUpdate: (update) => store.emitUpdate(update),
|
|
209
|
+
renderSegments,
|
|
210
|
+
version,
|
|
211
|
+
});
|
|
212
|
+
actionBridge.register();
|
|
213
|
+
|
|
214
|
+
// Setup navigation bridge
|
|
215
|
+
const navigationBridge = createNavigationBridge({
|
|
216
|
+
store,
|
|
217
|
+
eventController,
|
|
218
|
+
client,
|
|
219
|
+
onUpdate: (update) => store.emitUpdate(update),
|
|
220
|
+
renderSegments,
|
|
221
|
+
version,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Optionally enable global link interception
|
|
225
|
+
if (linkInterception) {
|
|
226
|
+
navigationBridge.registerLinkInterception();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Build initial tree with rootLayout
|
|
230
|
+
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
231
|
+
|
|
232
|
+
// Setup HMR
|
|
233
|
+
if (import.meta.hot) {
|
|
234
|
+
import.meta.hot.on("rsc:update", async () => {
|
|
235
|
+
console.log("[RSCRouter] HMR: Server update, refetching RSC");
|
|
236
|
+
|
|
237
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
238
|
+
replace: true,
|
|
239
|
+
});
|
|
240
|
+
const streamingToken = handle.startStreaming();
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
244
|
+
targetUrl: window.location.href,
|
|
245
|
+
segmentIds: [],
|
|
246
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (payload.metadata?.isPartial) {
|
|
250
|
+
const segments = payload.metadata.segments || [];
|
|
251
|
+
const matched = payload.metadata.matched || [];
|
|
252
|
+
|
|
253
|
+
store.setSegmentIds(matched);
|
|
254
|
+
store.setCurrentUrl(window.location.href);
|
|
255
|
+
|
|
256
|
+
const historyKey = generateHistoryKey(window.location.href);
|
|
257
|
+
store.setHistoryKey(historyKey);
|
|
258
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
259
|
+
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
260
|
+
|
|
261
|
+
store.emitUpdate({
|
|
262
|
+
root: renderSegments(segments),
|
|
263
|
+
metadata: payload.metadata,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await streamComplete;
|
|
268
|
+
} finally {
|
|
269
|
+
streamingToken.end();
|
|
270
|
+
}
|
|
271
|
+
handle.complete(new URL(window.location.href));
|
|
272
|
+
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Store context for RSCRouter component
|
|
277
|
+
const context: BrowserAppContext = {
|
|
278
|
+
store,
|
|
279
|
+
eventController,
|
|
280
|
+
bridge: navigationBridge,
|
|
281
|
+
initialPayload,
|
|
282
|
+
initialTree,
|
|
283
|
+
themeConfig: effectiveThemeConfig,
|
|
284
|
+
initialTheme: effectiveInitialTheme,
|
|
285
|
+
};
|
|
286
|
+
browserAppContext = context;
|
|
287
|
+
|
|
288
|
+
return context;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get the browser app context. Throws if initBrowserApp hasn't been called.
|
|
293
|
+
*/
|
|
294
|
+
export function getBrowserAppContext(): BrowserAppContext {
|
|
295
|
+
if (!browserAppContext) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
"RSCRouter: initBrowserApp() must be called before rendering RSCRouter"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return browserAppContext;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Reset the browser app context (for testing)
|
|
305
|
+
*/
|
|
306
|
+
export function resetBrowserAppContext(): void {
|
|
307
|
+
browserAppContext = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Props for the RSCRouter component
|
|
312
|
+
*/
|
|
313
|
+
export interface RSCRouterProps {}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* RSCRouter component - renders the RSC router with all internal wiring.
|
|
317
|
+
*
|
|
318
|
+
* Must be called after initBrowserApp() has completed.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```tsx
|
|
322
|
+
* import { initBrowserApp, RSCRouter } from "rsc-router/browser";
|
|
323
|
+
* import { rscStream } from "rsc-html-stream/client";
|
|
324
|
+
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
325
|
+
*
|
|
326
|
+
* async function main() {
|
|
327
|
+
* await initBrowserApp({ rscStream, deps: rscBrowser });
|
|
328
|
+
*
|
|
329
|
+
* hydrateRoot(
|
|
330
|
+
* document,
|
|
331
|
+
* <React.StrictMode>
|
|
332
|
+
* <RSCRouter />
|
|
333
|
+
* </React.StrictMode>
|
|
334
|
+
* );
|
|
335
|
+
* }
|
|
336
|
+
* main();
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
340
|
+
const { store, eventController, bridge, initialPayload, initialTree, themeConfig, initialTheme } =
|
|
341
|
+
getBrowserAppContext();
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<NavigationProvider
|
|
345
|
+
store={store}
|
|
346
|
+
eventController={eventController}
|
|
347
|
+
initialPayload={{ ...initialPayload, root: initialTree }}
|
|
348
|
+
bridge={bridge}
|
|
349
|
+
themeConfig={themeConfig}
|
|
350
|
+
initialTheme={initialTheme}
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
}
|