@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -4,10 +4,6 @@ import type {
4
4
  ResolvedSegment,
5
5
  StreamingToken,
6
6
  } from "./types.js";
7
- import {
8
- isLocationStateEntry,
9
- resolveLocationStateEntries,
10
- } from "./react/location-state-shared.js";
11
7
  import { generateHistoryKey } from "./navigation-store.js";
12
8
  import {
13
9
  handleNavigationStart,
@@ -16,70 +12,18 @@ import {
16
12
  } from "./scroll-restoration.js";
17
13
  import type { EventController, NavigationHandle } from "./event-controller.js";
18
14
  import { debugLog } from "./logging.js";
19
-
20
- /**
21
- * Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
22
- */
23
- function isTypedLocationState(
24
- state: unknown,
25
- ): state is Record<string, unknown> {
26
- if (state === null || typeof state !== "object") return false;
27
- return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
28
- }
29
-
30
- /**
31
- * Resolve navigation state - handles both LocationStateEntry[] and plain formats
32
- */
33
- export function resolveNavigationState(state: unknown): unknown {
34
- // Check if it's an array of LocationStateEntry
35
- if (
36
- Array.isArray(state) &&
37
- state.length > 0 &&
38
- isLocationStateEntry(state[0])
39
- ) {
40
- return resolveLocationStateEntries(state);
41
- }
42
- // Return as-is for plain state formats
43
- return state;
44
- }
45
-
46
- /**
47
- * Build history state object from user state
48
- * - Typed state: spread directly into history.state
49
- * - Plain state: store in history.state.state
50
- */
51
- function buildHistoryState(
52
- userState: unknown,
53
- routerState?: { intercept?: boolean; sourceUrl?: string },
54
- serverState?: Record<string, unknown>,
55
- ): Record<string, unknown> | null {
56
- const result: Record<string, unknown> = {};
57
-
58
- // Add router internal state
59
- if (routerState?.intercept) {
60
- result.intercept = true;
61
- if (routerState.sourceUrl) {
62
- result.sourceUrl = routerState.sourceUrl;
63
- }
64
- }
65
-
66
- // Add user state
67
- if (userState !== undefined) {
68
- if (isTypedLocationState(userState)) {
69
- // Typed state: spread directly
70
- Object.assign(result, userState);
71
- } else {
72
- // Plain state: store in .state
73
- result.state = userState;
74
- }
75
- }
76
-
77
- // Merge server-set location state (from ctx.setLocationState on non-redirect responses)
78
- if (serverState) {
79
- Object.assign(result, serverState);
80
- }
81
-
82
- return Object.keys(result).length > 0 ? result : null;
15
+ import { buildHistoryState } from "./history-state.js";
16
+
17
+ // Re-export for consumers that import from navigation-transaction
18
+ export { resolveNavigationState } from "./history-state.js";
19
+
20
+ /** Check if a history state object contains location state keys. */
21
+ function hasLocationState(state: unknown): boolean {
22
+ if (!state || typeof state !== "object") return false;
23
+ return (
24
+ "state" in state ||
25
+ Object.keys(state).some((k) => k.startsWith("__rsc_ls_"))
26
+ );
83
27
  }
84
28
 
85
29
  // Polyfill Symbol.dispose for Safari and older browsers
@@ -87,11 +31,6 @@ if (typeof Symbol.dispose === "undefined") {
87
31
  (Symbol as any).dispose = Symbol("Symbol.dispose");
88
32
  }
89
33
 
90
- // Monotonic counter for tagging early-pushed history entries.
91
- // Used by disposal to verify ownership without URL comparison,
92
- // which breaks when two navigations target the same URL.
93
- let navStamp = 0;
94
-
95
34
  /**
96
35
  * Options for committing a navigation transaction
97
36
  */
@@ -154,9 +93,6 @@ export interface BoundTransaction {
154
93
  * Uses the event controller handle for lifecycle management
155
94
  */
156
95
  interface NavigationTransaction extends Disposable {
157
- /** Optimistically commit from cache - instant render before revalidation */
158
- optimisticCommit(options: CommitOptions): void;
159
- /** Final commit with server data (or reconciliation after optimistic) */
160
96
  commit(options: CommitOptions): void;
161
97
  with(
162
98
  options: Omit<CommitOptions, "segmentIds" | "segments">,
@@ -168,9 +104,6 @@ interface NavigationTransaction extends Disposable {
168
104
  /**
169
105
  * Creates a navigation transaction that coordinates with the event controller.
170
106
  * Handles loading state transitions and cleanup on completion/abort.
171
- *
172
- * Supports optimistic navigation: render from cache immediately,
173
- * then revalidate in background and reconcile if data changed.
174
107
  */
175
108
  export function createNavigationTransaction(
176
109
  store: NavigationStore,
@@ -179,102 +112,29 @@ export function createNavigationTransaction(
179
112
  options?: NavigateOptions & { skipLoadingState?: boolean },
180
113
  ): NavigationTransaction {
181
114
  let committed = false;
182
- let optimisticallyCommitted = false;
183
- let earlyStatePushed = false;
184
- let earlyStateStamp: number | null = null;
185
115
  const currentUrl = window.location.href;
186
- const currentHistoryState = window.history.state;
187
116
 
188
117
  // Start navigation in event controller (this sets loading state)
189
118
  const handle = eventController.startNavigation(url, options);
190
119
 
191
- // If state is provided, push it to history immediately so loading UI can access it
192
- // This enables "optimistic state" - showing product names in skeletons etc.
193
- if (options?.state !== undefined && !options?.replace) {
194
- earlyStateStamp = ++navStamp;
195
- const earlyHistoryState = buildHistoryState(options.state);
196
- if (earlyHistoryState) {
197
- (earlyHistoryState as any).__navStamp = earlyStateStamp;
198
- }
199
- window.history.pushState(
200
- earlyHistoryState ?? { __navStamp: earlyStateStamp },
201
- "",
202
- url,
203
- );
204
- earlyStatePushed = true;
205
- }
206
-
207
- /**
208
- * Optimistically commit from cache - renders immediately before revalidation
209
- * Sets optimisticallyCommitted flag so final commit() knows to reconcile
210
- */
211
- function optimisticCommit(opts: CommitOptions): void {
212
- optimisticallyCommitted = true;
213
-
214
- const { url, segmentIds, segments, replace, scroll } = opts;
215
- const parsedUrl = new URL(url, window.location.origin);
216
-
217
- // Save current scroll position before navigating
218
- handleNavigationStart();
219
-
220
- // Update segment state
221
- store.setSegmentIds(segmentIds);
222
- store.setCurrentUrl(url);
223
- store.setPath(parsedUrl.pathname);
224
-
225
- // Generate history key from URL
226
- const historyKey = generateHistoryKey(url);
227
- store.setHistoryKey(historyKey);
228
-
229
- // Cache segments with current handleData (will be overwritten by fresh data on final commit)
230
- const currentHandleData = eventController.getHandleState().data;
231
- store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
232
-
233
- // Build history state with user state if provided
234
- const historyState = buildHistoryState(opts.state);
235
-
236
- // Update browser URL
237
- // Use replaceState if we already pushed early (for optimistic state access)
238
- if (replace || earlyStatePushed) {
239
- window.history.replaceState(historyState, "", url);
240
- } else {
241
- window.history.pushState(historyState, "", url);
242
- }
243
-
244
- // Ensure new history entry has a scroll restoration key
245
- ensureHistoryKey();
246
-
247
- // Complete the navigation in event controller (sets idle state)
248
- handle.complete(parsedUrl);
249
-
250
- // Handle scroll after navigation
251
- handleNavigationEnd({ scroll });
252
-
253
- debugLog("[Browser] Optimistic commit from cache, historyKey:", historyKey);
254
- }
255
-
256
120
  /**
257
121
  * Commit the navigation - updates store and URL atomically
258
- * If optimisticCommit was called, this becomes a reconciliation
259
122
  */
260
123
  function commit(opts: CommitOptions): void {
261
124
  committed = true;
262
125
 
263
- // If optimistic commit already done, adjust options for reconciliation
264
- const isReconciliation = optimisticallyCommitted;
265
126
  const {
266
127
  url,
267
128
  segmentIds,
268
129
  segments,
130
+ replace,
131
+ scroll,
269
132
  storeOnly,
270
133
  intercept,
271
134
  interceptSourceUrl,
272
135
  cacheOnly,
273
136
  serverState,
274
137
  } = opts;
275
- // For reconciliation: always replace (URL already pushed), no scroll
276
- const replace = isReconciliation ? true : opts.replace;
277
- const scroll = isReconciliation ? false : opts.scroll;
278
138
 
279
139
  const parsedUrl = new URL(url, window.location.origin);
280
140
 
@@ -286,14 +146,15 @@ export function createNavigationTransaction(
286
146
  if (cacheOnly) {
287
147
  const currentHandleData = eventController.getHandleState().data;
288
148
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
149
+ // Complete the navigation handle so currentNavigation is cleared.
150
+ // Without this, the entry lingers and weakens state-machine invariants.
151
+ handle.complete(parsedUrl);
289
152
  debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
290
153
  return;
291
154
  }
292
155
 
293
- // Save current scroll position before navigating (only for non-reconciliation)
294
- if (!isReconciliation) {
295
- handleNavigationStart();
296
- }
156
+ // Save current scroll position before navigating
157
+ handleNavigationStart();
297
158
 
298
159
  // Update segment state atomically
299
160
  store.setSegmentIds(segmentIds);
@@ -302,7 +163,7 @@ export function createNavigationTransaction(
302
163
 
303
164
  store.setHistoryKey(historyKey);
304
165
 
305
- // Cache segments with current handleData for this history entry (fresh data overwrites optimistic)
166
+ // Cache segments with current handleData for this history entry
306
167
  const currentHandleData = eventController.getHandleState().data;
307
168
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
308
169
 
@@ -321,50 +182,41 @@ export function createNavigationTransaction(
321
182
  serverState,
322
183
  );
323
184
 
324
- // Update browser URL (skip if reconciliation - already done in optimisticCommit)
325
- if (!isReconciliation) {
326
- // Use replaceState if we already pushed early (for optimistic state access) or replace requested
327
- if (replace || earlyStatePushed) {
328
- window.history.replaceState(historyState, "", url);
329
- } else {
330
- window.history.pushState(historyState, "", url);
331
- }
332
- // Ensure new history entry has a scroll restoration key
333
- ensureHistoryKey();
185
+ // Snapshot old state before pushState/replaceState overwrites it.
186
+ // Used to detect when location state is being cleared.
187
+ const oldState = window.history.state;
334
188
 
335
- // Notify location state hooks when history state includes typed entries.
336
- // Needed for same-page redirects where components don't remount and
337
- // useState initializers don't re-run, even though history.state was updated.
338
- if (
339
- historyState &&
340
- Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_"))
341
- ) {
342
- window.dispatchEvent(new Event("__rsc_locationstate"));
343
- }
189
+ // Update browser URL
190
+ if (replace) {
191
+ window.history.replaceState(historyState, "", url);
192
+ } else {
193
+ window.history.pushState(historyState, "", url);
194
+ }
195
+ // Ensure new history entry has a scroll restoration key
196
+ ensureHistoryKey();
197
+
198
+ // Notify location state hooks when either old or new state carries
199
+ // location state. This covers both "set new state" and "clear old state"
200
+ // for same-page navigations where components don't remount.
201
+ if (hasLocationState(oldState) || hasLocationState(historyState)) {
202
+ window.dispatchEvent(new Event("__rsc_locationstate"));
344
203
  }
345
204
 
346
205
  // Complete the navigation in event controller (sets idle state, updates location)
347
206
  handle.complete(parsedUrl);
348
207
 
349
- // Handle scroll after navigation (skip if reconciliation)
350
- if (!isReconciliation) {
351
- handleNavigationEnd({ scroll });
352
- }
208
+ // Handle scroll after navigation
209
+ handleNavigationEnd({ scroll });
353
210
 
354
- if (isReconciliation) {
355
- debugLog("[Browser] Reconciliation commit, historyKey:", historyKey);
356
- } else {
357
- debugLog(
358
- "[Browser] Navigation committed, historyKey:",
359
- historyKey,
360
- intercept ? "(intercept)" : "",
361
- );
362
- }
211
+ debugLog(
212
+ "[Browser] Navigation committed, historyKey:",
213
+ historyKey,
214
+ intercept ? "(intercept)" : "",
215
+ );
363
216
  }
364
217
 
365
218
  return {
366
219
  handle,
367
- optimisticCommit,
368
220
  commit,
369
221
 
370
222
  /**
@@ -428,27 +280,14 @@ export function createNavigationTransaction(
428
280
  },
429
281
 
430
282
  [Symbol.dispose]() {
431
- // Superseded: another navigation took over. Roll back our early push
432
- // so the new navigation starts from a clean history position.
433
- // Guard: only rollback if our early-pushed state is still the current
434
- // history entry. Compare by stamp (monotonic counter embedded in state)
435
- // so that a newer navigation targeting the same URL is not clobbered.
283
+ // Superseded: another navigation took over.
436
284
  if (handle.signal.aborted) {
437
- if (
438
- earlyStatePushed &&
439
- !committed &&
440
- !optimisticallyCommitted &&
441
- earlyStateStamp !== null &&
442
- window.history.state?.__navStamp === earlyStateStamp
443
- ) {
444
- window.history.replaceState(currentHistoryState, "", currentUrl);
445
- }
446
285
  return;
447
286
  }
448
287
 
449
288
  // Failed (not committed): keep the target URL -- the error UI owns it.
450
289
  // Just reset the event controller to idle.
451
- if (!committed && !optimisticallyCommitted) {
290
+ if (!committed) {
452
291
  handle[Symbol.dispose]();
453
292
  }
454
293
  },