@rangojs/router 0.15.1 → 0.17.0

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 (249) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +52 -0
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Taint symbol for request-scoped objects.
3
3
  *
4
- * Objects branded with NOCACHE_SYMBOL (ctx, env, req) are excluded from
4
+ * Objects branded with NOCACHE_SYMBOL (ctx objects) are excluded from
5
5
  * "use cache" cache keys and trigger handle capture mode so that side
6
6
  * effects (breadcrumbs, metadata) are recorded and replayed on cache hit.
7
7
  */
@@ -424,9 +424,8 @@ export type DocumentShellCacheEntry = ShellCacheEntry & {
424
424
  /**
425
425
  * The families a shell snapshot pins. The item/segment/response families are
426
426
  * cache-store reads/writes (recorded by RecordingShellStore); the loader family
427
- * pins the settled CONTAINER of a bake-lane loader (a loader on an entry with
428
- * no renderable loading(), executed during capture — see
429
- * docs/design/loader-container-bake.md). Excludes the shell family itself
427
+ * pins the settled CONTAINER of a bake-lane loader (lane rule: see
428
+ * resolveLoaderData, loader-cache.ts). Excludes the shell family itself
430
429
  * (getShell/putShell) — the snapshot rides INSIDE a shell entry, so recording
431
430
  * it would be self-referential.
432
431
  */
@@ -542,6 +541,11 @@ export interface CachedEntryData {
542
541
  * (see handle-snapshot.ts encodeHandles) so Promise/ReactNode handle values
543
542
  * round-trip through JSON-serializing stores instead of being flattened. */
544
543
  handles: string;
544
+ /**
545
+ * Owning loader ids index-aligned with the decoded `handles` values
546
+ * (HandleOwners). Absent: every value restores as a plain replay.
547
+ */
548
+ handleOwners?: HandleOwners;
545
549
  /** Expiration timestamp (ms since epoch) */
546
550
  expiresAt: number;
547
551
  /** Cache tags for invalidation */
@@ -583,3 +587,12 @@ export interface CacheDefaults {
583
587
  * Structure: { handleName: [values...] }
584
588
  */
585
589
  export type SegmentHandleData = Record<string, unknown[]>;
590
+
591
+ /**
592
+ * segmentId -> handleName -> the owning loader id of each recorded value, by
593
+ * index (null: not owned). Only a PPR shell capture writes owners: the
594
+ * settled pushes of an `ssr: false` loader's own body, which the record keeps
595
+ * because the prelude rendered them. restoreHandles replays an owned value
596
+ * through HandleStore.pushReplayed, so that loader's live re-run replaces it.
597
+ */
598
+ export type HandleOwners = Record<string, Record<string, (string | null)[]>>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * RSC-environment version of client exports
3
3
  *
4
- * This file is used when importing "rsc-router/client" from RSC (server components).
4
+ * This file is used when importing "@rangojs/router/client" from RSC (server components).
5
5
  * It re-exports the server's createLoader so that loader definitions work in both
6
6
  * environments with the same import.
7
7
  *
@@ -9,6 +9,10 @@
9
9
  * in RSC context, while the regular client.tsx is used in client components.
10
10
  */
11
11
 
12
+ // From html.ts directly, never via ./client.js: every export of that
13
+ // "use client" module is an opaque client reference here.
14
+ import * as Html from "./html.js";
15
+
12
16
  // Re-export everything from client.tsx (Outlet, useLoader, etc.)
13
17
  // These are safe to use in RSC context
14
18
  export {
@@ -64,7 +68,10 @@ export {
64
68
  useLinkStatus,
65
69
  type LinkStatus,
66
70
  } from "./browser/react/use-link-status.js";
67
- export { useScrollRestoration } from "./browser/react/ScrollRestoration.js";
71
+ export {
72
+ useScrollRestoration,
73
+ type ScrollRestorationProps,
74
+ } from "./browser/react/ScrollRestoration.js";
68
75
 
69
76
  export {
70
77
  Link,
@@ -72,11 +79,6 @@ export {
72
79
  type PrefetchStrategy,
73
80
  } from "./browser/react/Link.js";
74
81
 
75
- export {
76
- ScrollRestoration,
77
- type ScrollRestorationProps,
78
- } from "./browser/react/ScrollRestoration.js";
79
-
80
82
  export {
81
83
  NavigationProvider,
82
84
  type NavigationProviderProps,
@@ -91,14 +93,13 @@ export { MountContext } from "./browser/react/mount-context.js";
91
93
  export { createHandle, isHandle, type Handle } from "./handle.js";
92
94
 
93
95
  export { Meta } from "./handles/meta.js";
94
- export { MetaTags } from "./handles/MetaTags.js";
95
96
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
96
97
  export {
97
98
  Script,
98
99
  type ScriptConfig,
99
100
  type ScriptAttributes,
100
101
  } from "./handles/script.js";
101
- export { Scripts } from "./handles/Scripts.js";
102
+ export { Html };
102
103
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
103
104
 
104
105
  export {
package/src/client.tsx CHANGED
@@ -8,6 +8,9 @@ import {
8
8
  Suspense,
9
9
  type ReactNode,
10
10
  } from "react";
11
+ // Not `export * as Html`: plugin-rsc's "use client" proxy transform only
12
+ // expands bare `export *` and throws on a named one.
13
+ import * as Html from "./html.js";
11
14
  import { OutletContext, type OutletContextValue } from "./outlet-context.js";
12
15
  import {
13
16
  type ClientErrorBoundaryFallbackProps,
@@ -300,7 +303,7 @@ interface ErrorBoundaryState {
300
303
  * @example
301
304
  * ```tsx
302
305
  * "use client";
303
- * import { ErrorBoundary } from "rsc-router/client";
306
+ * import { ErrorBoundary } from "@rangojs/router/client";
304
307
  *
305
308
  * function MyComponent() {
306
309
  * return (
@@ -427,7 +430,6 @@ export {
427
430
  } from "./browser/react/use-link-status.js";
428
431
 
429
432
  export {
430
- ScrollRestoration,
431
433
  useScrollRestoration,
432
434
  type ScrollRestorationProps,
433
435
  } from "./browser/react/ScrollRestoration.js";
@@ -436,14 +438,13 @@ export { type Handle } from "./handle.js";
436
438
  export { useHandle } from "./browser/react/use-handle.js";
437
439
 
438
440
  export { Meta } from "./handles/meta.js";
439
- export { MetaTags } from "./handles/MetaTags.js";
440
441
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
441
442
  export {
442
443
  Script,
443
444
  type ScriptConfig,
444
445
  type ScriptAttributes,
445
446
  } from "./handles/script.js";
446
- export { Scripts } from "./handles/Scripts.js";
447
+ export { Html };
447
448
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
448
449
 
449
450
  export {
@@ -3,12 +3,16 @@
3
3
  import type { ReactNode, ReactElement } from "react";
4
4
  import { MetaTags } from "../handles/MetaTags.js";
5
5
  import { Scripts } from "../handles/Scripts.js";
6
+ import { ScrollRestoration } from "../browser/react/ScrollRestoration.js";
6
7
 
7
8
  /**
8
9
  * Default document component that provides a basic HTML structure.
9
10
  * Used when no custom document is provided to createRouter.
10
- * Includes MetaTags for automatic charset, viewport, and route meta support,
11
- * and Scripts (head + body sites) so the Script handle works out of the box.
11
+ * Renders every Html member: Html.Meta (charset, viewport, route meta), head +
12
+ * body Html.Scripts sites for the Script handle, and Html.ScrollRestoration.
13
+ * An app that wants a custom getKey or no scroll restoration passes its own
14
+ * document; rendering a second Html.ScrollRestoration under this one only
15
+ * warns ("[Scroll] Already initialized").
12
16
  *
13
17
  * Uses suppressHydrationWarning on <html> because the theme script
14
18
  * may modify class/style attributes before React hydrates.
@@ -27,6 +31,7 @@ export function DefaultDocument({
27
31
  <body>
28
32
  <Scripts position="body" />
29
33
  {children}
34
+ <ScrollRestoration />
30
35
  </body>
31
36
  </html>
32
37
  );
@@ -12,7 +12,7 @@
12
12
  * interface PaginationData { current: number; total: number }
13
13
  * export const Pagination = createVar<PaginationData>();
14
14
  *
15
- * // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
15
+ * // Non-cacheable var — ctx.get(User) throws inside cache() or "use cache"
16
16
  * export const User = createVar<UserData>({ cache: false });
17
17
  *
18
18
  * // handler
@@ -26,7 +26,7 @@
26
26
  export interface ContextVar<T> {
27
27
  readonly __brand: "context-var";
28
28
  readonly key: symbol;
29
- /** When false, ctx.get(var) throws inside a cache() boundary. */
29
+ /** When false, ctx.get(var) throws inside cache() or "use cache". */
30
30
  readonly cache: boolean;
31
31
  /** Phantom field to carry the type parameter. Never set at runtime. */
32
32
  readonly __type?: T;
@@ -35,9 +35,10 @@ export interface ContextVar<T> {
35
35
  export interface ContextVarOptions {
36
36
  /**
37
37
  * When false, marks this variable as non-cacheable.
38
- * Reading this var with ctx.get() inside a cache() boundary throws. Use for
39
- * inherently request-specific data (user sessions, auth tokens, etc.) that
40
- * must never be baked into cached segments.
38
+ * Reading this var with ctx.get() inside a cache() boundary or a "use cache"
39
+ * function throws. Use for inherently request-specific data (user sessions,
40
+ * auth tokens, etc.) that must never be baked into cached segments or
41
+ * entries; pass the value into a "use cache" function as an argument.
41
42
  *
42
43
  * @default true
43
44
  */
@@ -45,7 +45,7 @@ export function escapeJsonForScript(json: string): string {
45
45
  * source; for untrusted dynamic data, JSON-encode it and read it back, rather than
46
46
  * inlining it as code.
47
47
  *
48
- * Used by the Script handle's <Scripts> renderer for inline `children`.
48
+ * Used by the Script handle's <Html.Scripts> renderer for inline `children`.
49
49
  */
50
50
  export function escapeScriptBody(js: string): string {
51
51
  return js.replace(/<!--/g, "<\\u0021--").replace(/<\/(script)/gi, "<\\/$1");
@@ -2,17 +2,18 @@
2
2
 
3
3
  /**
4
4
  * Component to render collected meta descriptors in the document head.
5
+ * Public as `Html.Meta` (`@rangojs/router/client`).
5
6
  *
6
- * Deferred (Promise) meta descriptors are resolved before MetaTags renders
7
+ * Deferred (Promise) meta descriptors are resolved before `<Html.Meta />` renders
7
8
  * (server-side on the full render, client-side before apply on navigation), so
8
9
  * it only ever receives resolved descriptors and never suspends.
9
10
  *
10
- * When theme is enabled in the router config, MetaTags also renders
11
+ * When theme is enabled in the router config, `<Html.Meta />` also renders
11
12
  * the theme initialization script to prevent FOUC (flash of unstyled content).
12
- * This makes MetaTags the sole FOUC-script injector for apps that render it;
13
- * the standalone `<ThemeScript />` is only needed when MetaTags is not used.
14
- * Rendering both is safe (the inline script guards listener registration) but
15
- * redundant.
13
+ * This makes `<Html.Meta />` the sole FOUC-script injector for apps that render
14
+ * it; the standalone `<ThemeScript />` is only needed when `<Html.Meta />` is not
15
+ * used. Rendering both is safe (the inline script guards listener registration)
16
+ * but redundant.
16
17
  *
17
18
  * @example
18
19
  * ```tsx
@@ -20,7 +21,7 @@
20
21
  * return (
21
22
  * <html lang="en" suppressHydrationWarning>
22
23
  * <head>
23
- * <MetaTags />
24
+ * <Html.Meta />
24
25
  * </head>
25
26
  * <body>...</body>
26
27
  * </html>
@@ -179,17 +180,18 @@ export function renderMetaDescriptor(
179
180
  }
180
181
 
181
182
  /**
182
- * Renders all collected meta descriptors from route handlers.
183
+ * Renders all collected meta descriptors from route handlers. Public as
184
+ * `Html.Meta` (`@rangojs/router/client`).
183
185
  *
184
- * Place this component inside the `<head>` element of your document.
186
+ * Place `<Html.Meta />` inside the `<head>` element of your document.
185
187
  * It will automatically update when meta descriptors change during navigation.
186
188
  *
187
189
  * When theme is enabled in router config, also renders the theme initialization
188
190
  * script to prevent FOUC (flash of unstyled content).
189
191
  *
190
- * Deferred (Promise) meta descriptors are resolved BEFORE MetaTags renders —
192
+ * Deferred (Promise) meta descriptors are resolved BEFORE this renders —
191
193
  * server-side on the full/SSR render, client-side before apply on navigation
192
- * (resolve-by-default) — so MetaTags only ever receives resolved descriptors and
194
+ * (resolve-by-default) — so it only ever receives resolved descriptors and
193
195
  * never suspends.
194
196
  */
195
197
  export function MetaTags(): React.ReactNode {
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * Renders the scripts collected by the Script handle into the document.
4
+ * Renders the scripts collected by the Script handle into the document. Public
5
+ * as `Html.Scripts` (`@rangojs/router/client`).
5
6
  *
6
- * Place `<Scripts />` inside `<head>` (default) and, if you push body scripts,
7
- * `<Scripts position="body" />` at the top of `<body>`. Each site renders the
8
- * configs whose `position` matches; the request CSP nonce is applied
7
+ * Place `<Html.Scripts />` inside `<head>` (default) and, if you push body
8
+ * scripts, `<Html.Scripts position="body" />` at the top of `<body>`. Each site
9
+ * renders the configs whose `position` matches; the request CSP nonce is applied
9
10
  * automatically to every DOCUMENT-RENDERED <script> (consumers never pass it). An
10
11
  * async script first encountered on a soft navigation is injected client-side
11
12
  * where the nonce is unavailable, so it carries no nonce and relies on
@@ -24,11 +25,11 @@
24
25
  * ```tsx
25
26
  * <html>
26
27
  * <head>
27
- * <MetaTags />
28
- * <Scripts />
28
+ * <Html.Meta />
29
+ * <Html.Scripts />
29
30
  * </head>
30
31
  * <body>
31
- * <Scripts position="body" />
32
+ * <Html.Scripts position="body" />
32
33
  * {children}
33
34
  * </body>
34
35
  * </html>
@@ -80,10 +81,10 @@ function passthroughAttributes(
80
81
  if (dev) {
81
82
  console.warn(
82
83
  isHandler
83
- ? `[Scripts] event handler "${key}" in a script's attributes is ` +
84
+ ? `[Html.Scripts] event handler "${key}" in a script's attributes is ` +
84
85
  `dropped; callbacks cannot cross the server -> client handle ` +
85
86
  `boundary. Use a "use client" component for load/error handling.`
86
- : `[Scripts] managed field "${key}" in a script's attributes is ` +
87
+ : `[Html.Scripts] managed field "${key}" in a script's attributes is ` +
87
88
  `dropped; set it via the ScriptConfig fields (the request nonce ` +
88
89
  `is applied automatically).`,
89
90
  );
@@ -129,7 +130,7 @@ function renderScript(
129
130
  async
130
131
  ) {
131
132
  console.warn(
132
- `[Scripts] An async external script (src="${src}") is hoisted into ` +
133
+ `[Html.Scripts] An async external script (src="${src}") is hoisted into ` +
133
134
  `<head> by React; position: "body" is ignored for it.`,
134
135
  );
135
136
  }
@@ -19,7 +19,7 @@
19
19
  * return (
20
20
  * <html>
21
21
  * <head>
22
- * <MetaTags />
22
+ * <Html.Meta />
23
23
  * </head>
24
24
  * ...
25
25
  * </html>
@@ -225,7 +225,7 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
225
225
  * Built-in handle for managing document metadata.
226
226
  *
227
227
  * Use `ctx.use(Meta)` in route handlers to push meta descriptors.
228
- * Use `<MetaTags />` component to render them in the document head.
228
+ * Use the `<Html.Meta />` component to render them in the document head.
229
229
  *
230
230
  * Deferred (Promise) descriptors are resolved before collectMeta runs
231
231
  * (resolve-by-default), so they participate in deduplication and `%s`
@@ -3,22 +3,23 @@
3
3
  * route/layout handlers.
4
4
  *
5
5
  * Push from a SERVER handler with `ctx.use(Script)(config)`; render with the
6
- * `<Scripts />` component (from `@rangojs/router/client`) placed in the Document
7
- * `<head>` (and optionally a second `<Scripts position="body" />` at the top of
8
- * `<body>`). This mirrors the Meta / <MetaTags> pair.
6
+ * `<Html.Scripts />` component (from `@rangojs/router/client`) placed in the
7
+ * Document `<head>` (and optionally a second `<Html.Scripts position="body" />`
8
+ * at the top of `<body>`). This mirrors the Meta / <Html.Meta> pair.
9
9
  *
10
- * The request CSP nonce is applied AUTOMATICALLY by <Scripts> to document-rendered
11
- * scripts; consumers never pass a nonce. (An async script first loaded on a soft
12
- * navigation is injected client-side without a nonce — it relies on
13
- * 'strict-dynamic' or a host allowance; see the EXECUTION CONTRACT below and the
14
- * /scripts skill.) A ScriptConfig is fully serializable (it crosses the
15
- * server -> client handle-collection boundary), so callbacks like onLoad are NOT
16
- * supported — a consumer needing them renders their own "use client" script.
10
+ * The request CSP nonce is applied AUTOMATICALLY by <Html.Scripts> to
11
+ * document-rendered scripts; consumers never pass a nonce. (An async script
12
+ * first loaded on a soft navigation is injected client-side without a nonce —
13
+ * it relies on 'strict-dynamic' or a host allowance; see the EXECUTION CONTRACT
14
+ * below and the /scripts skill.) A ScriptConfig is fully serializable (it
15
+ * crosses the server -> client handle-collection boundary), so callbacks like
16
+ * onLoad are NOT supported — a consumer needing them renders their own
17
+ * "use client" script.
17
18
  *
18
19
  * EXECUTION CONTRACT (see the /scripts skill for the full story):
19
20
  * - Inline (`children`) and ordered external (`src`, optional `defer`) scripts
20
21
  * are DOCUMENT-LOAD scripts: they execute only when present in the initial HTML
21
- * response. <Scripts> freezes them after hydration, so a later client (soft)
22
+ * response. <Html.Scripts> freezes them after hydration, so a later client (soft)
22
23
  * navigation never inserts an inert copy — React creates client-mounted
23
24
  * <script> elements via innerHTML, which the HTML spec makes non-executing.
24
25
  * - Async external scripts (`src` + `async: true`) are React RESOURCES: they load
@@ -78,9 +79,9 @@ export type ScriptAttributes = Omit<
78
79
  /** Fields shared by every script shape. */
79
80
  interface ScriptConfigBase {
80
81
  /**
81
- * Where <Scripts> renders this script.
82
- * - "head" (default): the `<head>` <Scripts> site.
83
- * - "body": the `<Scripts position="body" />` site at the top of <body>.
82
+ * Where <Html.Scripts> renders this script.
83
+ * - "head" (default): the `<head>` <Html.Scripts> site.
84
+ * - "body": the `<Html.Scripts position="body" />` site at the top of <body>.
84
85
  * Note: an external `async` script is hoisted into <head> by React regardless.
85
86
  */
86
87
  position?: "head" | "body";
@@ -96,7 +97,7 @@ interface ScriptConfigBase {
96
97
  /**
97
98
  * Inline script: a raw JS body rendered in place, escaped against `</script>`
98
99
  * breakout. DOCUMENT-LOAD only (executes when present in the initial HTML;
99
- * <Scripts> freezes it after hydration so navigation never inserts an inert
100
+ * <Html.Scripts> freezes it after hydration so navigation never inserts an inert
100
101
  * copy). `id` is REQUIRED — inline scripts are never deduped by React, so a
101
102
  * layout and a child pushing the same bootstrap would inject it twice. It is also
102
103
  * rendered as the script's DOM `id`. Forbids `src`/`async`/`defer`. For analytics
@@ -146,7 +147,7 @@ export interface OrderedScriptConfig extends ScriptConfigBase {
146
147
  * inline (`id` + `children`), external async (`src` + `async: true`), or external
147
148
  * ordered (`src`, optional `defer`). Invalid combinations (both `src`+`children`,
148
149
  * `async`+`defer`, inline without `id`) are compile errors. The CSP nonce is
149
- * applied by <Scripts>, never here.
150
+ * applied by <Html.Scripts>, never here.
150
151
  */
151
152
  export type ScriptConfig =
152
153
  | InlineScriptConfig
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @example
8
8
  * ```typescript
9
- * import { href } from "rsc-router/client";
9
+ * import { href } from "@rangojs/router/client";
10
10
  *
11
11
  * href("/blog/my-post"); // ✓ matches /blog/:slug
12
12
  * href("/shop/product/widget"); // ✓ matches /shop/product/:slug
package/src/html.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Members of the `Html` namespace; client.tsx and client.rsc.tsx expose this
3
+ * module as `import * as Html`. A module namespace, not an object literal, so
4
+ * bundlers drop members an app never renders (a literal kept opt-in
5
+ * ScrollRestoration in every app: +313 B gzip, over the router-chunk ratchet).
6
+ * No "use client": each member stays its own module's client reference, so a
7
+ * server component can dot into `Html`. Pinned by __tests__/html.test.ts and
8
+ * testing/__tests__/html-namespace.rsc-test.tsx.
9
+ */
10
+ export { MetaTags as Meta } from "./handles/MetaTags.js";
11
+ export { Scripts } from "./handles/Scripts.js";
12
+ export { ScrollRestoration } from "./browser/react/ScrollRestoration.js";
package/src/index.rsc.ts CHANGED
@@ -51,6 +51,7 @@ export type {
51
51
  LoaderDefinition,
52
52
  LoaderFn,
53
53
  LoaderContext,
54
+ LoaderOptions,
54
55
  FetchableLoaderOptions,
55
56
  LoadOptions,
56
57
  // Error boundary types
@@ -188,6 +189,9 @@ export {
188
189
  type RouterRequestInput,
189
190
  } from "./router.js";
190
191
 
192
+ // Route manifest snapshot returned by router.debugManifest()
193
+ export type { SerializedManifest } from "./debug.js";
194
+
191
195
  // RSC handler types (server-side)
192
196
  export type { HandlerCacheConfig } from "./rsc/types.js";
193
197
  export {
@@ -254,6 +258,7 @@ export type {
254
258
  export type {
255
259
  ScopedReverseFunction,
256
260
  ReverseFunction,
261
+ GlobalReverseFunction,
257
262
  ExtractLocalRoutes,
258
263
  ParamsFor,
259
264
  } from "./reverse.js";
package/src/index.ts CHANGED
@@ -334,6 +334,7 @@ export type { BreadcrumbItem } from "./handles/breadcrumbs.js";
334
334
  export type {
335
335
  ScopedReverseFunction,
336
336
  ReverseFunction,
337
+ GlobalReverseFunction,
337
338
  ExtractLocalRoutes,
338
339
  ParamsFor,
339
340
  } from "./reverse.js";
@@ -34,6 +34,15 @@
34
34
  * the store — the hook falls back to `OutletContext.loaderData`.
35
35
  */
36
36
 
37
+ import { unwrapsSynchronously } from "./thenable-status.js";
38
+
39
+ /** The loader-segment shape announcePendingStreams reads (ResolvedSegment). */
40
+ export interface PendingStreamSource {
41
+ type: string;
42
+ loaderId?: string;
43
+ loaderData?: unknown;
44
+ }
45
+
37
46
  export interface LoaderEntry<T = unknown> {
38
47
  readonly value: T | undefined;
39
48
  /**
@@ -87,6 +96,14 @@ export interface SubscribeOptions {
87
96
  * Refreshes this bucket in place (no params/body) and rejects on failure.
88
97
  */
89
98
  refetch?: () => Promise<void>;
99
+ /**
100
+ * Called by `announcePendingStreams` (inside a transition commit) when a
101
+ * navigation stream for this bucket's loader is still pending. The hook pins
102
+ * `isLoading: true` with a useOptimistic update; React reverts it when that
103
+ * transition commits. Route-context readers only — an ephemeral reader owns
104
+ * its own lifecycle.
105
+ */
106
+ onStreamPending?: () => void;
90
107
  }
91
108
 
92
109
  interface InternalEntry {
@@ -118,6 +135,8 @@ interface InternalEntry {
118
135
  groups: Map<string, number>;
119
136
  /** Plain-GET refresh thunk for `refreshGroups`, set while in any group. */
120
137
  refetch: (() => Promise<void>) | undefined;
138
+ /** Subscribers' `onStreamPending` callbacks (see SubscribeOptions). */
139
+ streamPendingListeners: Set<() => void>;
121
140
  }
122
141
 
123
142
  /**
@@ -137,6 +156,21 @@ export class LoaderStore {
137
156
  private readonly families = new Map<string, Set<string>>();
138
157
  /** refresh group name -> set of bucket keys, for refreshGroups(). */
139
158
  private readonly groups = new Map<string, Set<string>>();
159
+ /**
160
+ * loader.$$id -> per-loader streams a committed navigation is still
161
+ * receiving. Registered and announced by announcePendingStreams(), which
162
+ * browser/partial-update.ts calls INSIDE every transition commit, so a
163
+ * reader whose content is held on screen by that transition pins
164
+ * `isLoading: true` for the data it is still showing until the transition
165
+ * lands (use-loader.tsx). Family-keyed on purpose: a persisting layout
166
+ * reader of the same createLoader the child is re-running is pinned too
167
+ * (the loader is in flight; not "this segment's copy is being replaced" —
168
+ * issue #862, working as designed). The new tree's own read of the loader
169
+ * suspends at the read site and never observes the flag. A Set, not a
170
+ * flag: overlapping navigations (second click before the first stream
171
+ * lands) keep the family pending until the LAST stream settles.
172
+ */
173
+ private readonly pendingStreams = new Map<string, Set<Promise<unknown>>>();
140
174
 
141
175
  private getOrCreate(bucketKey: string): InternalEntry {
142
176
  let e = this.entries.get(bucketKey);
@@ -151,6 +185,7 @@ export class LoaderStore {
151
185
  clearWhenSettled: false,
152
186
  groups: new Map(),
153
187
  refetch: undefined,
188
+ streamPendingListeners: new Set(),
154
189
  };
155
190
  this.entries.set(bucketKey, e);
156
191
  }
@@ -195,8 +230,11 @@ export class LoaderStore {
195
230
  e.pendingClear = false;
196
231
  e.clearWhenSettled = false;
197
232
  e.listeners.add(cb);
233
+ const onStreamPending = options?.onStreamPending;
234
+ if (onStreamPending) e.streamPendingListeners.add(onStreamPending);
198
235
  return () => {
199
236
  e.listeners.delete(cb);
237
+ if (onStreamPending) e.streamPendingListeners.delete(onStreamPending);
200
238
  // Group membership is refcounted per subscriber so refreshGroups() never
201
239
  // fetches for an unmounted reader, and a bucket shared by subscribers in
202
240
  // different groups stays in each group until ALL of that group's
@@ -479,6 +517,73 @@ export class LoaderStore {
479
517
  for (const cb of e.listeners) cb();
480
518
  }
481
519
 
520
+ /**
521
+ * Register a still-streaming per-loader result for `loaderId`. No-op for a
522
+ * settled Flight chunk (unwrapsSynchronously / "rejected"): the forceAwait
523
+ * and action lanes commit settled chunks, and a { ssr: false } loader is
524
+ * awaited before flush. Released as the stream settles, either way.
525
+ */
526
+ trackPendingStream(loaderId: string, stream: Promise<unknown>): void {
527
+ if (
528
+ unwrapsSynchronously(stream) ||
529
+ (stream as { status?: string }).status === "rejected"
530
+ ) {
531
+ return;
532
+ }
533
+ let set = this.pendingStreams.get(loaderId);
534
+ if (set?.has(stream)) return;
535
+ if (!set) {
536
+ set = new Set();
537
+ this.pendingStreams.set(loaderId, set);
538
+ }
539
+ set.add(stream);
540
+ const release = () => {
541
+ const current = this.pendingStreams.get(loaderId);
542
+ if (!current || !current.delete(stream)) return;
543
+ if (current.size === 0) this.pendingStreams.delete(loaderId);
544
+ };
545
+ stream.then(release, release);
546
+ }
547
+
548
+ /** True while any navigation stream for `loaderId` is still pending. */
549
+ isStreamPending(loaderId: string): boolean {
550
+ return this.pendingStreams.has(loaderId);
551
+ }
552
+
553
+ /**
554
+ * Register the loader segments of a tree being committed whose data is
555
+ * still a pending promise, then fire `onStreamPending` on every subscriber
556
+ * of every family with a pending stream. MUST run inside the startTransition
557
+ * that commits the tree: the hook answers with a useOptimistic update, which
558
+ * React reverts exactly when that transition commits — so the held reader
559
+ * reports isLoading:true for as long as its data stays on screen and not a
560
+ * frame longer. Outside a transition React reverts an optimistic update on
561
+ * the next render, pinning nothing useful. Registering here rather than in
562
+ * the tree build keeps register + announce in one call; settled chunks
563
+ * (cached/reused segments, forceAwait lanes) are skipped by
564
+ * trackPendingStream.
565
+ */
566
+ announcePendingStreams(segments: readonly PendingStreamSource[]): void {
567
+ for (const segment of segments) {
568
+ if (
569
+ segment.type === "loader" &&
570
+ segment.loaderId &&
571
+ segment.loaderData instanceof Promise
572
+ ) {
573
+ this.trackPendingStream(segment.loaderId, segment.loaderData);
574
+ }
575
+ }
576
+ for (const loaderId of this.pendingStreams.keys()) {
577
+ const fam = this.families.get(loaderId);
578
+ if (!fam) continue;
579
+ for (const bucketKey of fam) {
580
+ const e = this.entries.get(bucketKey);
581
+ if (!e) continue;
582
+ for (const cb of e.streamPendingListeners) cb();
583
+ }
584
+ }
585
+ }
586
+
482
587
  /**
483
588
  * Test-only escape hatch. Drops every entry. Production code should never
484
589
  * call this; the store is process-scoped and lives for the tab's lifetime.
@@ -488,6 +593,7 @@ export class LoaderStore {
488
593
  this.entries.clear();
489
594
  this.families.clear();
490
595
  this.groups.clear();
596
+ this.pendingStreams.clear();
491
597
  }
492
598
  }
493
599