@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.
Files changed (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. 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
+ }