@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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Restoration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides scroll position persistence across navigations, following React Router v7 patterns:
|
|
5
|
+
* - Saves scroll positions to sessionStorage keyed by unique history entry key
|
|
6
|
+
* - Restores scroll on back/forward navigation
|
|
7
|
+
* - Scrolls to top on new navigation (unless scroll: false)
|
|
8
|
+
* - Supports hash link scrolling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interval for polling scroll restoration during streaming (ms).
|
|
15
|
+
* If content is still loading and we can't scroll to saved position,
|
|
16
|
+
* keep trying at this interval.
|
|
17
|
+
*/
|
|
18
|
+
const SCROLL_POLL_INTERVAL_MS = 50;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maximum time to keep polling for scroll restoration (ms).
|
|
22
|
+
* After this timeout, stop trying even if streaming continues.
|
|
23
|
+
*/
|
|
24
|
+
const SCROLL_POLL_TIMEOUT_MS = 5000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* In-memory cache of scroll positions.
|
|
28
|
+
* Synced with sessionStorage on pagehide.
|
|
29
|
+
*/
|
|
30
|
+
let savedScrollPositions: Record<string, number> = {};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether scroll restoration has been initialized
|
|
34
|
+
*/
|
|
35
|
+
let initialized = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom getKey function for determining scroll restoration key
|
|
39
|
+
*/
|
|
40
|
+
type GetScrollKeyFunction = (
|
|
41
|
+
location: { pathname: string; search: string; hash: string; key: string }
|
|
42
|
+
) => string;
|
|
43
|
+
|
|
44
|
+
let customGetKey: GetScrollKeyFunction | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a unique key for the current history entry.
|
|
48
|
+
* Uses history.state.key if available, otherwise generates and stores a new one.
|
|
49
|
+
*/
|
|
50
|
+
export function getHistoryStateKey(): string {
|
|
51
|
+
const state = window.history.state;
|
|
52
|
+
if (state?.key) {
|
|
53
|
+
return state.key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate a new key and store it in history.state
|
|
57
|
+
const key = Math.random().toString(36).slice(2, 10);
|
|
58
|
+
window.history.replaceState({ ...state, key }, "");
|
|
59
|
+
return key;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the scroll restoration key for a location.
|
|
64
|
+
* Uses custom getKey function if set, otherwise uses history state key.
|
|
65
|
+
*/
|
|
66
|
+
export function getScrollKey(): string {
|
|
67
|
+
if (customGetKey) {
|
|
68
|
+
const loc = window.location;
|
|
69
|
+
return customGetKey({
|
|
70
|
+
pathname: loc.pathname,
|
|
71
|
+
search: loc.search,
|
|
72
|
+
hash: loc.hash,
|
|
73
|
+
key: getHistoryStateKey(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return getHistoryStateKey();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Initialize scroll restoration.
|
|
81
|
+
* Sets manual scroll restoration mode and loads saved positions from sessionStorage.
|
|
82
|
+
*/
|
|
83
|
+
export function initScrollRestoration(options?: {
|
|
84
|
+
getKey?: GetScrollKeyFunction;
|
|
85
|
+
}): () => void {
|
|
86
|
+
if (initialized) {
|
|
87
|
+
console.warn("[Scroll] Already initialized");
|
|
88
|
+
return () => {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
initialized = true;
|
|
92
|
+
customGetKey = options?.getKey ?? null;
|
|
93
|
+
|
|
94
|
+
// Set manual scroll restoration to prevent browser's default behavior
|
|
95
|
+
window.history.scrollRestoration = "manual";
|
|
96
|
+
|
|
97
|
+
// Load saved positions from sessionStorage
|
|
98
|
+
try {
|
|
99
|
+
const stored = sessionStorage.getItem(SCROLL_STORAGE_KEY);
|
|
100
|
+
if (stored) {
|
|
101
|
+
savedScrollPositions = JSON.parse(stored);
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Ignore parse errors
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Ensure current history entry has a key
|
|
108
|
+
getHistoryStateKey();
|
|
109
|
+
|
|
110
|
+
// Save scroll positions on pagehide (before leaving/refreshing)
|
|
111
|
+
const handlePageHide = () => {
|
|
112
|
+
saveCurrentScrollPosition();
|
|
113
|
+
persistToSessionStorage();
|
|
114
|
+
// Reset to auto for browser to handle if page is restored from bfcache
|
|
115
|
+
window.history.scrollRestoration = "auto";
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
119
|
+
|
|
120
|
+
console.log("[Scroll] Initialized, loaded positions:", Object.keys(savedScrollPositions).length);
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
window.removeEventListener("pagehide", handlePageHide);
|
|
124
|
+
window.history.scrollRestoration = "auto";
|
|
125
|
+
initialized = false;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Save the current scroll position for the current history entry
|
|
131
|
+
*/
|
|
132
|
+
export function saveCurrentScrollPosition(): void {
|
|
133
|
+
const key = getScrollKey();
|
|
134
|
+
savedScrollPositions[key] = window.scrollY;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Persist scroll positions to sessionStorage
|
|
139
|
+
*/
|
|
140
|
+
function persistToSessionStorage(): void {
|
|
141
|
+
try {
|
|
142
|
+
sessionStorage.setItem(SCROLL_STORAGE_KEY, JSON.stringify(savedScrollPositions));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.warn("[Scroll] Failed to persist to sessionStorage:", e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the saved scroll position for a history key
|
|
150
|
+
*/
|
|
151
|
+
export function getSavedScrollPosition(key?: string): number | undefined {
|
|
152
|
+
const lookupKey = key ?? getScrollKey();
|
|
153
|
+
return savedScrollPositions[lookupKey];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pending poll interval for scroll restoration during streaming
|
|
158
|
+
*/
|
|
159
|
+
let pendingPollInterval: ReturnType<typeof setInterval> | null = null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Cancel any pending scroll restoration polling
|
|
163
|
+
*/
|
|
164
|
+
export function cancelScrollRestorationPolling(): void {
|
|
165
|
+
if (pendingPollInterval) {
|
|
166
|
+
clearInterval(pendingPollInterval);
|
|
167
|
+
pendingPollInterval = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Restore scroll position for the current history entry.
|
|
173
|
+
* Returns true if position was fully restored, false otherwise.
|
|
174
|
+
*
|
|
175
|
+
* @param options.retryIfStreaming - If true, poll while streaming until we can scroll to target
|
|
176
|
+
* @param options.isStreaming - Function to check if streaming is in progress
|
|
177
|
+
*/
|
|
178
|
+
export function restoreScrollPosition(options?: {
|
|
179
|
+
retryIfStreaming?: boolean;
|
|
180
|
+
isStreaming?: () => boolean;
|
|
181
|
+
}): boolean {
|
|
182
|
+
// Clear any pending polling
|
|
183
|
+
cancelScrollRestorationPolling();
|
|
184
|
+
|
|
185
|
+
const key = getScrollKey();
|
|
186
|
+
const savedY = savedScrollPositions[key];
|
|
187
|
+
|
|
188
|
+
if (typeof savedY !== "number") {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if page is tall enough to scroll to saved position
|
|
193
|
+
const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
|
|
194
|
+
const canScrollToPosition = savedY <= maxScrollY;
|
|
195
|
+
|
|
196
|
+
if (canScrollToPosition) {
|
|
197
|
+
window.scrollTo(0, savedY);
|
|
198
|
+
console.log("[Scroll] Restored position:", savedY, "for key:", key);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Scroll as far as we can for now
|
|
203
|
+
window.scrollTo(0, maxScrollY);
|
|
204
|
+
console.log("[Scroll] Partial restore to:", maxScrollY, "target:", savedY);
|
|
205
|
+
|
|
206
|
+
// Poll while streaming until we can scroll to target position
|
|
207
|
+
if (options?.retryIfStreaming && options?.isStreaming?.()) {
|
|
208
|
+
const startTime = Date.now();
|
|
209
|
+
|
|
210
|
+
pendingPollInterval = setInterval(() => {
|
|
211
|
+
// Stop if we've exceeded the timeout
|
|
212
|
+
if (Date.now() - startTime > SCROLL_POLL_TIMEOUT_MS) {
|
|
213
|
+
console.log("[Scroll] Polling timeout, giving up");
|
|
214
|
+
cancelScrollRestorationPolling();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Stop if streaming ended
|
|
219
|
+
if (!options.isStreaming?.()) {
|
|
220
|
+
console.log("[Scroll] Streaming ended, stopping poll");
|
|
221
|
+
cancelScrollRestorationPolling();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if we can now scroll to the target position
|
|
226
|
+
const currentMaxScrollY = document.documentElement.scrollHeight - window.innerHeight;
|
|
227
|
+
if (savedY <= currentMaxScrollY) {
|
|
228
|
+
window.scrollTo(0, savedY);
|
|
229
|
+
console.log("[Scroll] Poll restored position:", savedY);
|
|
230
|
+
cancelScrollRestorationPolling();
|
|
231
|
+
}
|
|
232
|
+
}, SCROLL_POLL_INTERVAL_MS);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Handle hash link scrolling.
|
|
240
|
+
* Scrolls to element with matching ID if hash is present.
|
|
241
|
+
* Returns true if scrolled to element, false otherwise.
|
|
242
|
+
*/
|
|
243
|
+
export function scrollToHash(): boolean {
|
|
244
|
+
const hash = window.location.hash;
|
|
245
|
+
if (!hash) return false;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const id = decodeURIComponent(hash.slice(1));
|
|
249
|
+
const element = document.getElementById(id);
|
|
250
|
+
if (element) {
|
|
251
|
+
element.scrollIntoView();
|
|
252
|
+
console.log("[Scroll] Scrolled to hash element:", id);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.warn("[Scroll] Failed to decode hash:", hash);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Scroll to top of page
|
|
264
|
+
*/
|
|
265
|
+
export function scrollToTop(): void {
|
|
266
|
+
window.scrollTo(0, 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Handle scroll for a new navigation.
|
|
271
|
+
* - Saves current position before navigating
|
|
272
|
+
* - Ensures new history entry has a key
|
|
273
|
+
*/
|
|
274
|
+
export function handleNavigationStart(): void {
|
|
275
|
+
if (!initialized) return;
|
|
276
|
+
saveCurrentScrollPosition();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle scroll after navigation completes.
|
|
281
|
+
* @param options.restore - If true, restore saved position (for popstate)
|
|
282
|
+
* @param options.scroll - If false, don't scroll at all
|
|
283
|
+
* @param options.isStreaming - Function to check if streaming is in progress (for retry logic)
|
|
284
|
+
*/
|
|
285
|
+
export function handleNavigationEnd(options: {
|
|
286
|
+
restore?: boolean;
|
|
287
|
+
scroll?: boolean;
|
|
288
|
+
isStreaming?: () => boolean;
|
|
289
|
+
}): void {
|
|
290
|
+
if (!initialized) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { restore = false, scroll = true, isStreaming } = options;
|
|
295
|
+
|
|
296
|
+
// Don't scroll if explicitly disabled
|
|
297
|
+
if (scroll === false) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// For back/forward (restore), try to restore saved position
|
|
302
|
+
if (restore) {
|
|
303
|
+
if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Fall through to hash or top if no saved position
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Try hash scrolling first
|
|
310
|
+
if (scrollToHash()) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Default: scroll to top
|
|
315
|
+
scrollToTop();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Update the history state key after pushState/replaceState.
|
|
320
|
+
* Call this after changing history to ensure new entry has a key.
|
|
321
|
+
*/
|
|
322
|
+
export function ensureHistoryKey(): void {
|
|
323
|
+
getHistoryStateKey();
|
|
324
|
+
}
|