@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
@@ -12,21 +12,26 @@
12
12
  import type { LoaderFn } from "../types.js";
13
13
  import type { MiddlewareFn } from "../router/middleware.js";
14
14
 
15
- const fetchableLoaderRegistry = new Map<
16
- string,
17
- { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] }
18
- >();
15
+ export interface LoaderRegistryEntry {
16
+ fn: LoaderFn<any, any, any>;
17
+ middleware: MiddlewareFn[];
18
+ /** Whether this loader is fetchable via the _rsc_loader endpoint. */
19
+ fetchable: boolean;
20
+ }
21
+
22
+ const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
19
23
 
20
24
  export function registerFetchableLoader(
21
25
  id: string,
22
26
  fn: LoaderFn<any, any, any>,
23
27
  middleware: MiddlewareFn[],
28
+ fetchable: boolean,
24
29
  ): void {
25
- fetchableLoaderRegistry.set(id, { fn, middleware });
30
+ fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
26
31
  }
27
32
 
28
33
  export function getFetchableLoader(
29
34
  id: string,
30
- ): { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] } | undefined {
35
+ ): LoaderRegistryEntry | undefined {
31
36
  return fetchableLoaderRegistry.get(id);
32
37
  }
@@ -57,11 +57,26 @@ export interface HandleStore {
57
57
  track<T>(promise: Promise<T>): Promise<T>;
58
58
 
59
59
  /**
60
- * Promise that resolves when all tracked handlers have settled.
61
- * Does not reject - uses Promise.allSettled internally.
60
+ * Signal that no more track() calls will be made.
61
+ * settled will not resolve until seal() is called AND all tracked
62
+ * promises have settled. Calling stream() or getData() auto-seals.
63
+ */
64
+ seal(): void;
65
+
66
+ /**
67
+ * Promise that resolves when the store is sealed AND all tracked
68
+ * handlers have settled.
62
69
  */
63
70
  readonly settled: Promise<void>;
64
71
 
72
+ /**
73
+ * Optional error callback for late streaming-handle failures.
74
+ * Called when push() throws LateHandlePushError (handle pushed after
75
+ * stream completion). Allows the router to surface these errors
76
+ * to onError and telemetry.
77
+ */
78
+ onError?: (error: Error) => void;
79
+
65
80
  /**
66
81
  * Push handle data for a specific handle and segment.
67
82
  * Multiple pushes to the same handle/segment accumulate in an array.
@@ -119,9 +134,31 @@ export interface HandleStore {
119
134
  * ```
120
135
  */
121
136
  export function createHandleStore(): HandleStore {
122
- const pending: Promise<unknown>[] = [];
123
137
  const data: HandleData = {};
124
138
 
139
+ // Settlement barrier: resolved only when sealed AND inflight === 0.
140
+ // seal() signals "no more track() calls". Each track() increments
141
+ // inflightCount, each promise.finally() decrements. settled resolves
142
+ // once both conditions are met — even if tracks are added while
143
+ // earlier ones are still in flight.
144
+ let sealed = false;
145
+ let inflightCount = 0;
146
+ let drainWaiters: (() => void)[] = [];
147
+
148
+ function notifyDrain() {
149
+ if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
150
+ const waiters = drainWaiters;
151
+ drainWaiters = [];
152
+ for (const resolve of waiters) resolve();
153
+ }
154
+ }
155
+
156
+ function sealInternal() {
157
+ if (sealed) return;
158
+ sealed = true;
159
+ notifyDrain();
160
+ }
161
+
125
162
  // Queue for pending emissions and resolver for waiting consumer
126
163
  let pendingEmissions: HandleData[] = [];
127
164
  let emissionResolver: (() => void) | null = null;
@@ -148,20 +185,36 @@ export function createHandleStore(): HandleStore {
148
185
 
149
186
  return {
150
187
  track<T>(promise: Promise<T>): Promise<T> {
151
- pending.push(promise);
188
+ inflightCount++;
189
+ // Use .then(onSettle, onSettle) instead of .finally() to avoid
190
+ // creating an unhandled rejection branch when the tracked promise
191
+ // rejects (e.g. error route handlers). .finally() re-throws the
192
+ // rejection on a new branch that nobody catches, which can crash
193
+ // the server process.
194
+ const onSettle = () => {
195
+ inflightCount--;
196
+ notifyDrain();
197
+ };
198
+ promise.then(onSettle, onSettle);
152
199
  return promise;
153
200
  },
154
201
 
202
+ seal() {
203
+ sealInternal();
204
+ },
205
+
155
206
  get settled(): Promise<void> {
156
- if (pending.length === 0) {
157
- return Promise.resolve();
158
- }
159
- return Promise.allSettled(pending).then(() => {});
207
+ if (sealed && inflightCount === 0) return Promise.resolve();
208
+ return new Promise<void>((resolve) => {
209
+ drainWaiters.push(resolve);
210
+ });
160
211
  },
161
212
 
162
213
  push(handleName: string, segmentId: string, value: unknown): void {
163
214
  if (completed) {
164
- throw createLateHandlePushError(handleName, segmentId);
215
+ const error = createLateHandlePushError(handleName, segmentId);
216
+ if (this.onError) this.onError(error);
217
+ throw error;
165
218
  }
166
219
 
167
220
  if (!data[handleName]) {
@@ -178,10 +231,14 @@ export function createHandleStore(): HandleStore {
178
231
  },
179
232
 
180
233
  getData(): Promise<HandleData> {
234
+ sealInternal();
181
235
  return this.settled.then(() => cloneHandleData(data));
182
236
  },
183
237
 
184
238
  async *stream(): AsyncGenerator<HandleData, void, unknown> {
239
+ // Auto-seal: stream() is called after all track() registrations.
240
+ sealInternal();
241
+
185
242
  // Set up completion handler
186
243
  this.settled.then(() => {
187
244
  completed = true;
@@ -6,13 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { LoaderFn } from "../types.js";
9
- import type { MiddlewareFn } from "../router/middleware.js";
10
- import { getFetchableLoader } from "./fetchable-loader-store.js";
11
-
12
- interface RegisteredLoader {
13
- fn: LoaderFn<any, any, any>;
14
- middleware: MiddlewareFn[];
15
- }
9
+ import {
10
+ getFetchableLoader,
11
+ type LoaderRegistryEntry,
12
+ } from "./fetchable-loader-store.js";
16
13
 
17
14
  // Server-side cache - maps loader $$id to function and middleware
18
15
  // This is a CACHE populated by getLoaderLazy() when loaders are first accessed.
@@ -21,7 +18,7 @@ interface RegisteredLoader {
21
18
  // 1. Avoid repeated lookups/imports for the same loader
22
19
  // 2. Support lazy loading in production (loaders imported on-demand)
23
20
  // 3. Provide a stable reference for the RSC handler
24
- const loaderRegistry = new Map<string, RegisteredLoader>();
21
+ const loaderRegistry = new Map<string, LoaderRegistryEntry>();
25
22
 
26
23
  // Lazy import map - set by the loader manifest
27
24
  // Maps loader $$id to a function that imports the loader module
@@ -37,28 +34,6 @@ export function setLoaderImports(
37
34
  lazyLoaderImports = new Map(Object.entries(imports));
38
35
  }
39
36
 
40
- /**
41
- * Register a fetchable loader by $$id
42
- * Called by createLoader when fetchable option is provided
43
- */
44
- export function registerLoader(
45
- id: string,
46
- fn: LoaderFn<any, any, any>,
47
- middleware: MiddlewareFn[] = [],
48
- ): void {
49
- // Always update the registry entry. During HMR, the module is re-executed
50
- // with the new loader function, so we must replace the stale reference.
51
- loaderRegistry.set(id, { fn, middleware });
52
- }
53
-
54
- /**
55
- * Get a registered loader by $$id (synchronous)
56
- * Returns undefined if loader is not registered
57
- */
58
- export function getLoader(id: string): RegisteredLoader | undefined {
59
- return loaderRegistry.get(id);
60
- }
61
-
62
37
  /**
63
38
  * Get a loader by $$id, loading it lazily if needed
64
39
  * This is the primary method for the RSC handler to get loaders
@@ -68,7 +43,7 @@ export function getLoader(id: string): RegisteredLoader | undefined {
68
43
  */
69
44
  export async function getLoaderLazy(
70
45
  id: string,
71
- ): Promise<RegisteredLoader | undefined> {
46
+ ): Promise<LoaderRegistryEntry | undefined> {
72
47
  // Check if already cached in main registry
73
48
  const existing = loaderRegistry.get(id);
74
49
  if (existing) {
@@ -128,20 +103,6 @@ export async function getLoaderLazy(
128
103
  return undefined;
129
104
  }
130
105
 
131
- /**
132
- * Check if a loader is registered by $$id
133
- */
134
- export function hasLoader(id: string): boolean {
135
- return loaderRegistry.has(id) || getFetchableLoader(id) !== undefined;
136
- }
137
-
138
- /**
139
- * Get all registered loader IDs (for debugging)
140
- */
141
- export function getRegisteredLoaderIds(): string[] {
142
- return Array.from(loaderRegistry.keys());
143
- }
144
-
145
106
  /**
146
107
  * Register a loader by its $$id (injected by Vite plugin)
147
108
  * This is called during module loading to cache loaders
@@ -163,6 +124,10 @@ export function registerLoaderById(loader: {
163
124
 
164
125
  // Fall back to using fn from the loader object (non-fetchable loaders)
165
126
  if (loader.fn) {
166
- loaderRegistry.set(loader.$$id, { fn: loader.fn, middleware: [] });
127
+ loaderRegistry.set(loader.$$id, {
128
+ fn: loader.fn,
129
+ middleware: [],
130
+ fetchable: false,
131
+ });
167
132
  }
168
133
  }
@@ -14,7 +14,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
14
14
  import type { CookieOptions } from "../router/middleware.js";
15
15
  import type { LoaderDefinition, LoaderContext } from "../types.js";
16
16
  import type { ScopedReverseFunction } from "../reverse.js";
17
- import type { DefaultReverseRouteMap } from "../types/global-namespace.js";
17
+ import type {
18
+ DefaultReverseRouteMap,
19
+ DefaultRouteName,
20
+ } from "../types/global-namespace.js";
18
21
  import type { Handle } from "../handle.js";
19
22
  import { type ContextVar, contextGet, contextSet } from "../context-var.js";
20
23
  import { createHandleStore, type HandleStore } from "./handle-store.js";
@@ -27,8 +30,9 @@ import { THEME_COOKIE } from "../theme/constants.js";
27
30
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
28
31
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
29
32
  import { createReverseFunction } from "../router/handler-context.js";
30
- import { getGlobalRouteMap } from "../route-map-builder.js";
33
+ import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
31
34
  import { invariant } from "../errors.js";
35
+ import { isAutoGeneratedRouteName } from "../route-name.js";
32
36
 
33
37
  /**
34
38
  * Unified request context available via getRequestContext()
@@ -128,6 +132,12 @@ export interface RequestContext<
128
132
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
129
133
  _cacheStore?: SegmentCacheStore;
130
134
 
135
+ /** @internal Cache profiles for "use cache" profile resolution (per-router) */
136
+ _cacheProfiles?: Record<
137
+ string,
138
+ import("../cache/profile-registry.js").CacheProfile
139
+ >;
140
+
131
141
  /**
132
142
  * Schedule work to run after the response is sent.
133
143
  * On Cloudflare Workers, uses ctx.waitUntil().
@@ -214,14 +224,21 @@ export interface RequestContext<
214
224
  *
215
225
  * @example
216
226
  * ```typescript
217
- * ctx.setLocationState([Flash({ text: "Item saved!" })]);
227
+ * ctx.setLocationState(Flash({ text: "Item saved!" }));
218
228
  * ```
219
229
  */
220
- setLocationState(entries: LocationStateEntry[]): void;
230
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
221
231
 
222
232
  /** @internal Accumulated location state entries */
223
233
  _locationState?: LocationStateEntry[];
224
234
 
235
+ /**
236
+ * The matched route name, if the route has an explicit name.
237
+ * Undefined before route matching or for unnamed routes.
238
+ * Includes the namespace prefix from include() (e.g., "blog.post").
239
+ */
240
+ routeName?: DefaultRouteName;
241
+
225
242
  /**
226
243
  * Generate URLs from route names.
227
244
  * Uses the global route map. After route matching, scoped (`.name`) resolution
@@ -234,6 +251,20 @@ export interface RequestContext<
234
251
 
235
252
  /** @internal Route name from route matching, used for scoped reverse resolution */
236
253
  _routeName?: string;
254
+
255
+ /** @internal Previous route key (from the navigation source), used for revalidation */
256
+ _prevRouteKey?: string;
257
+
258
+ /** @internal Per-request error dedup set for onError reporting */
259
+ _reportedErrors: WeakSet<object>;
260
+
261
+ /**
262
+ * @internal Report a non-fatal background error through the router's
263
+ * onError callback. Wired by the RSC handler / router during request
264
+ * creation. Cache-runtime and other subsystems call this to surface
265
+ * errors without failing the response.
266
+ */
267
+ _reportBackgroundError?: (error: unknown, category: string) => void;
237
268
  }
238
269
 
239
270
  /**
@@ -253,10 +284,14 @@ export type PublicRequestContext<
253
284
  | "deleteCookie"
254
285
  | "_handleStore"
255
286
  | "_cacheStore"
287
+ | "_cacheProfiles"
256
288
  | "_onResponseCallbacks"
257
289
  | "_themeConfig"
258
290
  | "_locationState"
259
291
  | "_routeName"
292
+ | "_prevRouteKey"
293
+ | "_reportedErrors"
294
+ | "_reportBackgroundError"
260
295
  >;
261
296
 
262
297
  // AsyncLocalStorage instance for request context
@@ -313,9 +348,34 @@ export function setRequestContextParams(
313
348
  ctx.params = params;
314
349
  if (routeName !== undefined) {
315
350
  ctx._routeName = routeName;
351
+ ctx.routeName = (
352
+ routeName && !isAutoGeneratedRouteName(routeName)
353
+ ? routeName
354
+ : undefined
355
+ ) as DefaultRouteName | undefined;
316
356
  }
317
357
  // Update reverse with scoped resolution now that route is known
318
- ctx.reverse = createReverseFunction(getGlobalRouteMap(), routeName, params);
358
+ ctx.reverse = createReverseFunction(
359
+ getGlobalRouteMap(),
360
+ routeName,
361
+ params,
362
+ routeName ? isRouteRootScoped(routeName) : undefined,
363
+ );
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Store the previous route key on the request context.
369
+ * Called during partial-match context creation to make the navigation source
370
+ * route key available for revalidation and intercept evaluation.
371
+ * @internal
372
+ */
373
+ export function setRequestContextPrevRouteKey(
374
+ prevRouteKey: string | undefined,
375
+ ): void {
376
+ const ctx = requestContextStorage.getStore();
377
+ if (ctx && prevRouteKey !== undefined) {
378
+ ctx._prevRouteKey = prevRouteKey;
319
379
  }
320
380
  }
321
381
 
@@ -354,8 +414,15 @@ export interface CreateRequestContextOptions<TEnv> {
354
414
  request: Request;
355
415
  url: URL;
356
416
  variables: Record<string, any>;
417
+ /** Optional initial response stub headers/status to seed effective cookie reads */
418
+ initialResponse?: Response;
357
419
  /** Optional cache store for segment caching (used by CacheScope) */
358
420
  cacheStore?: SegmentCacheStore;
421
+ /** Optional cache profiles for "use cache" resolution (per-router) */
422
+ cacheProfiles?: Record<
423
+ string,
424
+ import("../cache/profile-registry.js").CacheProfile
425
+ >;
359
426
  /** Optional Cloudflare execution context for waitUntil support */
360
427
  executionContext?: ExecutionContext;
361
428
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
@@ -378,7 +445,9 @@ export function createRequestContext<TEnv>(
378
445
  request,
379
446
  url,
380
447
  variables,
448
+ initialResponse,
381
449
  cacheStore,
450
+ cacheProfiles,
382
451
  executionContext,
383
452
  themeConfig,
384
453
  } = options;
@@ -387,7 +456,13 @@ export function createRequestContext<TEnv>(
387
456
 
388
457
  // Create stub response for collecting headers/cookies.
389
458
  // All cookie/header mutations go here; cookie reads derive from it.
390
- let stubResponse = new Response(null, { status: 200 });
459
+ let stubResponse = initialResponse
460
+ ? new Response(null, {
461
+ status: initialResponse.status,
462
+ statusText: initialResponse.statusText,
463
+ headers: new Headers(initialResponse.headers),
464
+ })
465
+ : new Response(null, { status: 200 });
391
466
 
392
467
  // Create handle store and loader memoization for this request
393
468
  const handleStore = createHandleStore();
@@ -556,6 +631,7 @@ export function createRequestContext<TEnv>(
556
631
 
557
632
  _handleStore: handleStore,
558
633
  _cacheStore: cacheStore,
634
+ _cacheProfiles: cacheProfiles,
559
635
 
560
636
  waitUntil(fn: () => Promise<void>): void {
561
637
  if (executionContext?.waitUntil) {
@@ -588,14 +664,17 @@ export function createRequestContext<TEnv>(
588
664
  : undefined,
589
665
  _themeConfig: themeConfig,
590
666
 
591
- setLocationState(entries: LocationStateEntry[]): void {
667
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
592
668
  assertNotInsideCacheExec(ctx, "setLocationState");
669
+ const arr = Array.isArray(entries) ? entries : [entries];
593
670
  this._locationState = this._locationState
594
- ? [...this._locationState, ...entries]
595
- : entries;
671
+ ? [...this._locationState, ...arr]
672
+ : arr;
596
673
  },
597
674
  _locationState: undefined,
598
675
 
676
+ _reportedErrors: new WeakSet<object>(),
677
+
599
678
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
600
679
  };
601
680
 
@@ -774,6 +853,7 @@ export function createUseFunction<TEnv>(
774
853
  // Create loader context with recursive use() support
775
854
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
776
855
  params: ctx.params,
856
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
777
857
  request: ctx.request,
778
858
  searchParams: ctx.searchParams,
779
859
  search: (ctx as any).search ?? {},
@@ -794,6 +874,7 @@ export function createUseFunction<TEnv>(
794
874
  getGlobalRouteMap(),
795
875
  ctx._routeName,
796
876
  ctx.params as Record<string, string>,
877
+ ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
797
878
  ),
798
879
  };
799
880
 
package/src/ssr/index.tsx CHANGED
@@ -1,9 +1,8 @@
1
1
  import React from "react";
2
2
  import { renderSegments } from "../segment-system.js";
3
- import { initHandleDataSync } from "../browser/react/use-handle.js";
4
- import { initSegmentsSync } from "../browser/react/use-segments.js";
5
- import { initThemeConfigSync } from "../theme/theme-context.js";
3
+ import { filterSegmentOrder } from "../browser/react/filter-segment-order.js";
6
4
  import { ThemeProvider } from "../theme/ThemeProvider.js";
5
+ import { NonceContext } from "../browser/react/nonce-context.js";
7
6
  import { NavigationStoreContext } from "../browser/react/context.js";
8
7
  import type { NavigationStoreContextValue } from "../browser/react/context.js";
9
8
  import type { HandleData } from "../browser/types.js";
@@ -34,6 +33,13 @@ interface RenderToReadableStreamOptions {
34
33
  formState?: unknown;
35
34
  }
36
35
 
36
+ /**
37
+ * ReadableStream with the allReady promise added by react-dom/server.edge.
38
+ */
39
+ interface ReactDOMReadableStream extends ReadableStream<Uint8Array> {
40
+ allReady: Promise<void>;
41
+ }
42
+
37
43
  /**
38
44
  * Options for the renderHTML function
39
45
  */
@@ -50,6 +56,14 @@ export interface SSRRenderOptions {
50
56
  * Nonce for Content Security Policy (CSP)
51
57
  */
52
58
  nonce?: string;
59
+
60
+ /**
61
+ * SSR stream mode.
62
+ *
63
+ * - `"stream"` (default) — start flushing HTML immediately.
64
+ * - `"allReady"` — await `stream.allReady` before returning.
65
+ */
66
+ streamMode?: import("../router/router-options.js").SSRStreamMode;
53
67
  }
54
68
 
55
69
  /**
@@ -69,7 +83,7 @@ export interface SSRDependencies<TEnv = unknown> {
69
83
  renderToReadableStream: (
70
84
  element: React.ReactNode,
71
85
  options?: RenderToReadableStreamOptions,
72
- ) => Promise<ReadableStream<Uint8Array>>;
86
+ ) => Promise<ReactDOMReadableStream>;
73
87
 
74
88
  /**
75
89
  * injectRSCPayload from rsc-html-stream/server
@@ -139,8 +153,18 @@ async function consumeAsyncGenerator(
139
153
  * Create a minimal event controller for SSR.
140
154
  * This provides the correct pathname so useNavigation returns the right value during SSR.
141
155
  */
142
- function createSsrEventController(pathname: string): EventController {
143
- const location = new URL(pathname, "http://localhost");
156
+ function createSsrEventController(opts: {
157
+ pathname: string;
158
+ params?: Record<string, string>;
159
+ handleData?: HandleData;
160
+ matched?: string[];
161
+ }): EventController {
162
+ const location = new URL(opts.pathname, "http://localhost");
163
+ let params = opts.params ?? {};
164
+ const handleState = {
165
+ data: opts.handleData ?? {},
166
+ segmentOrder: filterSegmentOrder(opts.matched ?? []),
167
+ };
144
168
  const state: DerivedNavigationState = {
145
169
  state: "idle",
146
170
  isStreaming: false,
@@ -163,9 +187,11 @@ function createSsrEventController(pathname: string): EventController {
163
187
  subscribeToAction: () => () => {},
164
188
  subscribeToHandles: () => () => {},
165
189
  setHandleData: () => {},
166
- getHandleState: () => ({ data: {}, segmentOrder: [] }),
167
- setParams: () => {},
168
- getParams: () => ({}),
190
+ getHandleState: () => handleState,
191
+ setParams: (nextParams) => {
192
+ params = nextParams;
193
+ },
194
+ getParams: () => params,
169
195
  setLocation: () => {},
170
196
  startNavigation: () => {
171
197
  throw new Error("Navigation not supported during SSR");
@@ -177,6 +203,7 @@ function createSsrEventController(pathname: string): EventController {
177
203
  abortAllActions: () => {},
178
204
  getCurrentNavigation: () => null,
179
205
  getInflightActions: () => new Map(),
206
+ hadAnyConcurrentActions: () => false,
180
207
  };
181
208
  }
182
209
 
@@ -218,7 +245,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
218
245
  rscStream: ReadableStream<Uint8Array>,
219
246
  options?: SSRRenderOptions,
220
247
  ): Promise<ReadableStream<Uint8Array>> {
221
- const { nonce, formState } = options ?? {};
248
+ const { nonce, formState, streamMode } = options ?? {};
222
249
 
223
250
  try {
224
251
  // Tee the stream:
@@ -233,33 +260,28 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
233
260
  function SsrRoot() {
234
261
  payload ??= createFromReadableStream<RscPayload>(rscStream1);
235
262
  const resolved = React.use(payload);
236
-
237
- // Initialize segments state before children render (for useSegments hook)
238
- initSegmentsSync(
239
- resolved.metadata?.matched,
240
- resolved.metadata?.pathname,
241
- resolved.metadata?.params,
242
- );
243
-
244
- // Initialize theme config for MetaTags to render theme script
245
263
  const themeConfig = resolved.metadata?.themeConfig ?? null;
246
- initThemeConfigSync(themeConfig);
264
+ const pathname = resolved.metadata?.pathname ?? "/";
247
265
 
248
- // Await handles and initialize state before children render
266
+ // Await handles before creating SSR event controller so hooks can
267
+ // read request-local handle data via NavigationStoreContext.
249
268
  // The handles property is an async generator that yields on each push
250
269
  // Memoize the promise since async generators can only be iterated once
270
+ let handleData: HandleData = {};
251
271
  if (resolved.metadata?.handles) {
252
272
  handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
253
- const handleData = React.use(handlesPromise);
254
- initHandleDataSync(handleData, resolved.metadata.matched);
273
+ handleData = React.use(handlesPromise);
255
274
  }
256
275
 
257
- // Create SSR context with correct pathname for useNavigation
276
+ // Create SSR context with request-local pathname/params/handles.
258
277
  ssrContextValue ??= {
259
278
  store: null as any,
260
- eventController: createSsrEventController(
261
- resolved.metadata?.pathname ?? "/",
262
- ),
279
+ eventController: createSsrEventController({
280
+ pathname,
281
+ params: resolved.metadata?.params,
282
+ handleData,
283
+ matched: resolved.metadata?.matched,
284
+ }),
263
285
  navigate: async () => {},
264
286
  refresh: async () => {},
265
287
  version: resolved.metadata?.version,
@@ -290,6 +312,13 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
290
312
  );
291
313
  }
292
314
 
315
+ // Wrap with NonceContext so client components (e.g. MetaTags) can
316
+ // apply CSP nonces to inline scripts during SSR. Always present to
317
+ // match the browser-side NavigationProvider tree shape for hydration.
318
+ content = (
319
+ <NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
320
+ );
321
+
293
322
  // Wrap with NavigationStoreContext for useNavigation hook
294
323
  return (
295
324
  <NavigationStoreContext.Provider value={ssrContextValue!}>
@@ -310,6 +339,13 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
310
339
  nonce,
311
340
  });
312
341
 
342
+ // Wait for all Suspense boundaries to resolve when streamMode is "allReady".
343
+ // This buffers the entire HTML before flushing — used for bots that
344
+ // cannot process streamed HTML.
345
+ if (streamMode === "allReady") {
346
+ await htmlStream.allReady;
347
+ }
348
+
313
349
  // Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
314
350
  return htmlStream.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
315
351
  } catch (error) {
@@ -82,6 +82,13 @@ export function Static<TParams extends Record<string, any>>(
82
82
  id = maybeId ?? "";
83
83
  }
84
84
 
85
+ if (!id) {
86
+ throw new Error(
87
+ "[rsc-router] Static: missing $$id. " +
88
+ "Ensure the exposeInternalIds Vite plugin is configured.",
89
+ );
90
+ }
91
+
85
92
  return {
86
93
  __brand: "staticHandler" as const,
87
94
  $$id: id,
@@ -50,7 +50,12 @@ function readThemeFromCookie(storageKey: string): string | null {
50
50
  for (const cookie of cookies) {
51
51
  const [name, ...rest] = cookie.trim().split("=");
52
52
  if (name === storageKey) {
53
- return decodeURIComponent(rest.join("="));
53
+ const raw = rest.join("=");
54
+ try {
55
+ return decodeURIComponent(raw);
56
+ } catch {
57
+ return raw;
58
+ }
54
59
  }
55
60
  }
56
61
  return null;
@@ -54,9 +54,4 @@ export {
54
54
  export { generateThemeScript, getNonceAttribute } from "./theme-script.js";
55
55
 
56
56
  // Context (for advanced use cases)
57
- export {
58
- ThemeContext,
59
- useThemeContext,
60
- initThemeConfigSync,
61
- getSSRThemeConfig,
62
- } from "./theme-context.js";
57
+ export { ThemeContext, useThemeContext } from "./theme-context.js";