@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -22,16 +22,43 @@ export function isTainted(value: unknown): boolean {
22
22
 
23
23
  /**
24
24
  * Symbol stamped on tainted ctx during "use cache" function execution.
25
- * ctx.set(), ctx.header(), ctx.setCookie(), etc. check this flag and
26
- * throw if present — those side effects would be lost on cache hit.
25
+ * cookies(), headers(), ctx.set(), ctx.header(), etc. check this flag and
26
+ * throw if present — reads would cache per-request data under a shared key,
27
+ * and side effects would be lost on cache hit.
28
+ *
29
+ * The value is a numeric reference count, not a boolean. Multiple concurrent
30
+ * cached functions sharing the same ctx/requestCtx each increment on entry
31
+ * and decrement on exit. Guards fire when count > 0.
27
32
  */
28
33
  export const INSIDE_CACHE_EXEC: unique symbol = Symbol.for(
29
34
  "rango:inside-cache-exec",
30
35
  ) as any;
31
36
 
37
+ /**
38
+ * Increment the INSIDE_CACHE_EXEC ref count on an object.
39
+ */
40
+ export function stampCacheExec(obj: object): void {
41
+ const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
42
+ (obj as any)[INSIDE_CACHE_EXEC] = current + 1;
43
+ }
44
+
45
+ /**
46
+ * Decrement the INSIDE_CACHE_EXEC ref count on an object.
47
+ * Deletes the symbol when the count reaches zero so the `in` check
48
+ * used by guards no longer fires.
49
+ */
50
+ export function unstampCacheExec(obj: object): void {
51
+ const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
52
+ if (current <= 1) {
53
+ delete (obj as any)[INSIDE_CACHE_EXEC];
54
+ } else {
55
+ (obj as any)[INSIDE_CACHE_EXEC] = current - 1;
56
+ }
57
+ }
58
+
32
59
  /**
33
60
  * Throw if ctx is inside a "use cache" execution.
34
- * Call from side-effecting ctx methods (set, header, setCookie, etc.).
61
+ * Call from side-effecting ctx methods (set, header, etc.) and cookie mutations.
35
62
  */
36
63
  export function assertNotInsideCacheExec(
37
64
  ctx: unknown,
@@ -75,7 +75,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
75
75
  * @example Using cookies for locale
76
76
  * ```typescript
77
77
  * keyGenerator: (ctx, defaultKey) => {
78
- * const locale = ctx.cookie('locale') || 'en';
78
+ * const locale = cookies().get('locale')?.value || 'en';
79
79
  * return `${locale}:${defaultKey}`;
80
80
  * }
81
81
  * ```
@@ -340,117 +340,3 @@ export interface SegmentCacheProvider {
340
340
  */
341
341
  cacheEntry(cacheKey: string, segments: ResolvedSegment[]): void;
342
342
  }
343
-
344
- // ============================================================================
345
- // Generic Cache Store (for future extensibility)
346
- // ============================================================================
347
- // These types support a general-purpose cache interface that can be used
348
- // for caching arbitrary values (responses, streams, objects). Currently,
349
- // the segment caching system uses SegmentCacheStore directly, but these
350
- // types enable future use cases like response caching or data caching.
351
-
352
- /**
353
- * Supported cache value types for the generic CacheStore interface.
354
- * @internal Reserved for future extensibility
355
- */
356
- export type CacheValue =
357
- | ReadableStream<Uint8Array>
358
- | Response
359
- | ArrayBuffer
360
- | string
361
- | unknown[] // JSON-serializable array
362
- | Record<string, unknown>; // JSON-serializable object
363
-
364
- /**
365
- * Cache entry returned by match().
366
- * @internal Reserved for future extensibility
367
- */
368
- export interface CacheEntry<T = CacheValue> {
369
- /** The cached value */
370
- value: T;
371
- /** Optional metadata stored with the entry */
372
- metadata?: CacheMetadata;
373
- }
374
-
375
- /**
376
- * Original value type for reconstruction.
377
- * @internal Reserved for future extensibility
378
- */
379
- export type CacheValueType =
380
- | "stream"
381
- | "response"
382
- | "arraybuffer"
383
- | "string"
384
- | "object";
385
-
386
- /**
387
- * Metadata associated with a cache entry.
388
- * @internal Reserved for future extensibility
389
- */
390
- export interface CacheMetadata {
391
- /** Timestamp when entry expires (ms since epoch) */
392
- expiresAt?: number;
393
- /** Tags for bulk invalidation */
394
- tags?: string[];
395
- /** Original value type for reconstruction on read */
396
- valueType?: CacheValueType;
397
- /** Response headers (preserved when caching Response) */
398
- responseHeaders?: Record<string, string>;
399
- /** Response status (preserved when caching Response) */
400
- responseStatus?: number;
401
- /** Custom metadata */
402
- [key: string]: unknown;
403
- }
404
-
405
- /**
406
- * Options for put().
407
- * @internal Reserved for future extensibility
408
- */
409
- export interface CachePutOptions {
410
- /** Time-to-live in seconds */
411
- ttl?: number;
412
- /** Metadata to store with entry */
413
- metadata?: Omit<CacheMetadata, "expiresAt">;
414
- }
415
-
416
- /**
417
- * Generic cache store interface for arbitrary value types.
418
- *
419
- * This interface is designed for future extensibility to support caching
420
- * responses, streams, and other values. Currently, segment caching uses
421
- * the SegmentCacheStore interface directly.
422
- *
423
- * Implementations must handle:
424
- * - Stream values (clone before storing, streams can only be read once)
425
- * - Promise values (await before storing)
426
- * - Expiration/TTL
427
- *
428
- * @internal Reserved for future extensibility
429
- */
430
- export interface CacheStore {
431
- /**
432
- * Retrieve a cached entry by key.
433
- * @param key - Cache key
434
- * @returns The cached entry or undefined if not found/expired
435
- */
436
- match<T = CacheValue>(key: string): Promise<CacheEntry<T> | undefined>;
437
-
438
- /**
439
- * Store a value in the cache.
440
- * @param key - Cache key
441
- * @param value - Value to cache (stream, response, string, object, etc.)
442
- * @param options - TTL, metadata, etc.
443
- */
444
- put<T extends CacheValue>(
445
- key: string,
446
- value: T,
447
- options?: CachePutOptions,
448
- ): Promise<void>;
449
-
450
- /**
451
- * Delete a cached entry.
452
- * @param key - Cache key
453
- * @returns true if entry was deleted, false if not found
454
- */
455
- delete(key: string): Promise<boolean>;
456
- }
@@ -17,7 +17,6 @@ export {
17
17
  OutletProvider,
18
18
  useOutlet,
19
19
  useLoader,
20
- useLoaderData,
21
20
  ErrorBoundary,
22
21
  type ErrorBoundaryProps,
23
22
  } from "./client.js";
package/src/client.tsx CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  LoaderBoundary,
22
22
  } from "./route-content-wrapper.js";
23
23
  import { OutletProvider } from "./outlet-provider.js";
24
+ import { MountContextProvider } from "./browser/react/mount-context.js";
24
25
 
25
26
  /**
26
27
  * Outlet component - renders child content in layouts
@@ -87,6 +88,8 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
87
88
  content = segment.component ?? null;
88
89
  }
89
90
 
91
+ let result: ReactNode;
92
+
90
93
  // If segment has a layout, wrap appropriately
91
94
  if (segment.layout) {
92
95
  // Check if this segment has loaders that need streaming
@@ -106,25 +109,23 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
106
109
  </LoaderBoundary>
107
110
  );
108
111
 
109
- return (
112
+ result = (
110
113
  <OutletProvider content={loaderAwareContent} segment={segment}>
111
114
  {segment.layout}
112
115
  </OutletProvider>
113
116
  );
117
+ } else {
118
+ // No loaders - wrap in OutletProvider so layout can use <Outlet />
119
+ result = (
120
+ <OutletProvider content={content} segment={segment}>
121
+ {segment.layout}
122
+ </OutletProvider>
123
+ );
114
124
  }
115
-
116
- // No loaders - wrap in OutletProvider so layout can use <Outlet />
117
- return (
118
- <OutletProvider content={content} segment={segment}>
119
- {segment.layout}
120
- </OutletProvider>
121
- );
122
- }
123
-
124
- // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
125
- // This is common for intercept routes that use useLoader without a custom layout
126
- if (segment.loaderDataPromise && segment.loaderIds) {
127
- return (
125
+ } else if (segment.loaderDataPromise && segment.loaderIds) {
126
+ // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
127
+ // This is common for intercept routes that use useLoader without a custom layout
128
+ result = (
128
129
  <LoaderBoundary
129
130
  loaderDataPromise={segment.loaderDataPromise}
130
131
  loaderIds={segment.loaderIds}
@@ -136,9 +137,20 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
136
137
  {content}
137
138
  </LoaderBoundary>
138
139
  );
140
+ } else {
141
+ result = content;
142
+ }
143
+
144
+ // Wrap with MountContextProvider for include() scoped parallel/intercept slots
145
+ if (segment.mountPath) {
146
+ return (
147
+ <MountContextProvider value={segment.mountPath}>
148
+ {result}
149
+ </MountContextProvider>
150
+ );
139
151
  }
140
152
 
141
- return content;
153
+ return result;
142
154
  }
143
155
 
144
156
  // Default: render child content
@@ -202,6 +214,8 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
202
214
  content = segment.component ?? null;
203
215
  }
204
216
 
217
+ let result: ReactNode;
218
+
205
219
  // If segment has a layout, wrap appropriately
206
220
  if (segment.layout) {
207
221
  // Check if this segment has loaders that need streaming
@@ -220,25 +234,23 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
220
234
  </LoaderBoundary>
221
235
  );
222
236
 
223
- return (
237
+ result = (
224
238
  <OutletProvider content={loaderAwareContent} segment={segment}>
225
239
  {segment.layout}
226
240
  </OutletProvider>
227
241
  );
242
+ } else {
243
+ // No loaders - wrap in OutletProvider so layout can use <Outlet />
244
+ result = (
245
+ <OutletProvider content={content} segment={segment}>
246
+ {segment.layout}
247
+ </OutletProvider>
248
+ );
228
249
  }
229
-
230
- // No loaders - wrap in OutletProvider so layout can use <Outlet />
231
- return (
232
- <OutletProvider content={content} segment={segment}>
233
- {segment.layout}
234
- </OutletProvider>
235
- );
236
- }
237
-
238
- // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
239
- // This is common for intercept routes that use useLoader without a custom layout
240
- if (segment.loaderDataPromise && segment.loaderIds) {
241
- return (
250
+ } else if (segment.loaderDataPromise && segment.loaderIds) {
251
+ // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
252
+ // This is common for intercept routes that use useLoader without a custom layout
253
+ result = (
242
254
  <LoaderBoundary
243
255
  loaderDataPromise={segment.loaderDataPromise}
244
256
  loaderIds={segment.loaderIds}
@@ -250,9 +262,20 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
250
262
  {content}
251
263
  </LoaderBoundary>
252
264
  );
265
+ } else {
266
+ result = content;
253
267
  }
254
268
 
255
- return content;
269
+ // Wrap with MountContextProvider for include() scoped parallel/intercept slots
270
+ if (segment.mountPath) {
271
+ return (
272
+ <MountContextProvider value={segment.mountPath}>
273
+ {result}
274
+ </MountContextProvider>
275
+ );
276
+ }
277
+
278
+ return result;
256
279
  }
257
280
 
258
281
  // OutletProvider is defined in outlet-provider.tsx to break a circular
@@ -290,52 +313,6 @@ export {
290
313
  type UseLoaderOptions,
291
314
  } from "./use-loader.js";
292
315
 
293
- /**
294
- * Hook to access all loader data in the current context
295
- *
296
- * Returns a record of all loader data available in the current outlet context
297
- * and all parent contexts. Useful for debugging or when you need access to
298
- * multiple loaders.
299
- *
300
- * @returns Record of loader name to data, or empty object if no loaders
301
- *
302
- * @example
303
- * ```tsx
304
- * "use client";
305
- * import { useLoaderData } from "rsc-router/client";
306
- *
307
- * export function DebugPanel() {
308
- * const loaderData = useLoaderData();
309
- * return <pre>{JSON.stringify(loaderData, null, 2)}</pre>;
310
- * }
311
- * ```
312
- */
313
- export function useLoaderData(): Record<string, any> {
314
- const context = useContext(OutletContext);
315
-
316
- // Collect all loader data from the context chain
317
- // Child loaders override parent loaders with the same name
318
- const result: Record<string, any> = {};
319
- const stack: OutletContextValue[] = [];
320
-
321
- // Build stack from current to root
322
- let current: OutletContextValue | null | undefined = context;
323
- while (current) {
324
- stack.push(current);
325
- current = current.parent;
326
- }
327
-
328
- // Apply from root to current (so children override parents)
329
- for (let i = stack.length - 1; i >= 0; i--) {
330
- const ctx = stack[i];
331
- if (ctx.loaderData) {
332
- Object.assign(result, ctx.loaderData);
333
- }
334
- }
335
-
336
- return result;
337
- }
338
-
339
316
  /**
340
317
  * Client-safe createLoader factory
341
318
  *
package/src/errors.ts CHANGED
@@ -327,7 +327,12 @@ export function sanitizeError(error: unknown): Response {
327
327
  return error;
328
328
  }
329
329
 
330
- const isDev = (import.meta as any).env?.DEV ?? true;
330
+ // Vite replaces import.meta.env.DEV at compile time. The fallback covers
331
+ // non-Vite environments (plain Node, test runners without Vite transforms).
332
+ // SECURITY: fail closed — default to production when the environment is ambiguous.
333
+ const isDev =
334
+ (import.meta as any).env?.DEV ??
335
+ globalThis.process?.env?.NODE_ENV === "development";
331
336
 
332
337
  if (isDev) {
333
338
  // Development: Send full error details for debugging
package/src/handle.ts CHANGED
@@ -95,7 +95,7 @@ export function createHandle<TData, TAccumulated = TData[]>(
95
95
  ): Handle<TData, TAccumulated> {
96
96
  const handleId = __injectedId ?? "";
97
97
 
98
- if (!handleId && process.env.NODE_ENV !== "production") {
98
+ if (!handleId && process.env.NODE_ENV === "development") {
99
99
  throw new Error(
100
100
  "[rsc-router] Handle is missing $$id. " +
101
101
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
@@ -28,8 +28,9 @@ import { use } from "react";
28
28
  import { useHandle } from "../browser/react/use-handle.js";
29
29
  import { Meta } from "./meta.js";
30
30
  import type { MetaDescriptor, MetaDescriptorBase } from "../router/types.js";
31
- import { getSSRThemeConfig } from "../theme/theme-context.js";
31
+ import { useThemeContext } from "../theme/theme-context.js";
32
32
  import { generateThemeScript } from "../theme/theme-script.js";
33
+ import { useNonce } from "../browser/react/nonce-context.js";
33
34
 
34
35
  // Type guards for MetaDescriptorBase variants
35
36
  function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
@@ -216,13 +217,15 @@ function AsyncMetaTag({
216
217
  */
217
218
  export function MetaTags(): React.ReactNode {
218
219
  const descriptors = useHandle(Meta) as MetaDescriptor[];
219
- const themeConfig = getSSRThemeConfig();
220
+ const themeConfig = useThemeContext()?.config ?? null;
221
+ const nonce = useNonce();
220
222
 
221
223
  return (
222
224
  <>
223
225
  {/* Theme script must be first to prevent FOUC */}
224
226
  {themeConfig && (
225
227
  <script
228
+ nonce={nonce}
226
229
  dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
227
230
  />
228
231
  )}
@@ -26,9 +26,14 @@ export function parseCookies(request: Request): Record<string, string> {
26
26
  const pairs = cookieHeader.split(";");
27
27
 
28
28
  for (const pair of pairs) {
29
- const [key, value] = pair.trim().split("=");
30
- if (key && value) {
31
- cookies[key] = decodeURIComponent(value);
29
+ const [name, ...rest] = pair.trim().split("=");
30
+ if (name && rest.length > 0) {
31
+ const value = rest.join("=");
32
+ try {
33
+ cookies[name] = decodeURIComponent(value);
34
+ } catch {
35
+ cookies[name] = value;
36
+ }
32
37
  }
33
38
  }
34
39
 
package/src/host/index.ts CHANGED
@@ -25,9 +25,6 @@
25
25
  // Core router
26
26
  export { createHostRouter } from "./router.js";
27
27
 
28
- // Host router registry for build-time discovery
29
- export { HostRouterRegistry, type HostRouterRegistryEntry } from "./router.js";
30
-
31
28
  // Utilities
32
29
  export { defineHosts } from "./utils.js";
33
30
 
@@ -149,7 +149,20 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
149
149
  return finalHandler();
150
150
  }
151
151
 
152
- return mw(request, input, next);
152
+ // Guard against double next() calls — a second call would
153
+ // re-enter the downstream chain and run handlers/side-effects twice.
154
+ let nextCalled = false;
155
+ const guardedNext = (): Promise<Response> => {
156
+ if (nextCalled) {
157
+ throw new Error(
158
+ `[HostRouter] Middleware called next() more than once.`,
159
+ );
160
+ }
161
+ nextCalled = true;
162
+ return next();
163
+ };
164
+
165
+ return mw(request, input, guardedNext);
153
166
  }
154
167
 
155
168
  return next();
@@ -182,7 +182,9 @@ export type ValidPaths<TRoutes = GetRegisteredRoutes> =
182
182
  */
183
183
  export function href<T extends ValidPaths>(path: T, mount?: string): string {
184
184
  if (mount && mount !== "/") {
185
- return mount + path;
185
+ // Strip trailing slash from mount to avoid double-slash when joining
186
+ const normalizedMount = mount.endsWith("/") ? mount.slice(0, -1) : mount;
187
+ return normalizedMount + path;
186
188
  }
187
189
  return path;
188
190
  }
package/src/index.rsc.ts CHANGED
@@ -33,7 +33,6 @@ export {
33
33
  export type {
34
34
  // Configuration types
35
35
  DocumentProps,
36
- RouterEnv,
37
36
  DefaultEnv,
38
37
  RouteDefinition,
39
38
  RouteConfig,
@@ -73,7 +72,12 @@ export type {
73
72
  } from "./types.js";
74
73
 
75
74
  // Router options type (server-only, so import directly)
76
- export type { RSCRouterOptions } from "./router.js";
75
+ export type {
76
+ RSCRouterOptions,
77
+ SSRStreamMode,
78
+ SSROptions,
79
+ ResolveStreamingContext,
80
+ } from "./router.js";
77
81
 
78
82
  // Server-side createLoader and redirect
79
83
  export {
@@ -168,12 +172,27 @@ export type { HandlerCacheConfig } from "./rsc/types.js";
168
172
  // Built-in handles (server-side)
169
173
  export { Meta } from "./handles/meta.js";
170
174
 
171
- // Request context (for accessing request data in server actions/components)
175
+ // Request context (for accessing request data in server actions/components).
176
+ // Re-exported with a narrowed return type so that public consumers only see
177
+ // public members. Internal code imports from "./server/request-context.js"
178
+ // directly and gets the full type.
179
+ import { getRequestContext as _getRequestContextInternal } from "./server/request-context.js";
180
+ export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
181
+ import type { PublicRequestContext } from "./server/request-context.js";
182
+ import type { DefaultEnv } from "./types/global-namespace.js";
183
+
184
+ export const getRequestContext: <
185
+ TEnv = DefaultEnv,
186
+ >() => PublicRequestContext<TEnv> = _getRequestContextInternal;
187
+
188
+ // Request-scoped shorthands
172
189
  export {
173
- getRequestContext,
174
- requireRequestContext,
175
- type RequestContext,
176
- } from "./server/request-context.js";
190
+ cookies,
191
+ headers,
192
+ type CookieStore,
193
+ type Cookie,
194
+ type ReadonlyHeaders,
195
+ } from "./server/cookie-store.js";
177
196
 
178
197
  // Meta types
179
198
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
@@ -201,9 +220,6 @@ export type {
201
220
  RouteParams,
202
221
  } from "./search-params.js";
203
222
 
204
- // Performance tracking (server-only)
205
- export { track } from "./server/context.js";
206
-
207
223
  // Debug utilities for route matching (development only)
208
224
  export {
209
225
  enableMatchDebug,
@@ -220,3 +236,30 @@ export {
220
236
 
221
237
  // Path-based response type lookup from RegisteredRoutes
222
238
  export type { PathResponse } from "./href-client.js";
239
+
240
+ // Telemetry sink
241
+ export { createConsoleSink } from "./router/telemetry.js";
242
+ export { createOTelSink } from "./router/telemetry-otel.js";
243
+ export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
244
+ export type {
245
+ TelemetrySink,
246
+ TelemetryEvent,
247
+ RequestStartEvent,
248
+ RequestEndEvent,
249
+ RequestErrorEvent,
250
+ RequestTimeoutEvent,
251
+ LoaderStartEvent,
252
+ LoaderEndEvent,
253
+ LoaderErrorEvent,
254
+ HandlerErrorEvent,
255
+ CacheDecisionEvent,
256
+ RevalidationDecisionEvent,
257
+ } from "./router/telemetry.js";
258
+
259
+ // Timeout types and error class
260
+ export { RouterTimeoutError } from "./router/timeout.js";
261
+ export type {
262
+ RouterTimeouts,
263
+ TimeoutPhase,
264
+ TimeoutContext,
265
+ } from "./router/timeout.js";