@rangojs/router 0.0.0-experimental.7dc955ec → 0.0.0-experimental.80

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 (124) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +700 -236
  4. package/package.json +3 -3
  5. package/skills/handler-use/SKILL.md +362 -0
  6. package/skills/intercept/SKILL.md +20 -0
  7. package/skills/layout/SKILL.md +22 -0
  8. package/skills/links/SKILL.md +3 -1
  9. package/skills/loader/SKILL.md +53 -43
  10. package/skills/middleware/SKILL.md +34 -3
  11. package/skills/migrate-nextjs/SKILL.md +560 -0
  12. package/skills/migrate-react-router/SKILL.md +764 -0
  13. package/skills/parallel/SKILL.md +59 -0
  14. package/skills/prerender/SKILL.md +110 -68
  15. package/skills/rango/SKILL.md +24 -22
  16. package/skills/route/SKILL.md +24 -0
  17. package/skills/router-setup/SKILL.md +87 -2
  18. package/src/__internal.ts +1 -1
  19. package/src/browser/app-version.ts +14 -0
  20. package/src/browser/navigation-bridge.ts +37 -5
  21. package/src/browser/navigation-client.ts +98 -46
  22. package/src/browser/navigation-store.ts +43 -8
  23. package/src/browser/partial-update.ts +41 -7
  24. package/src/browser/prefetch/cache.ts +16 -6
  25. package/src/browser/prefetch/fetch.ts +68 -6
  26. package/src/browser/prefetch/queue.ts +61 -29
  27. package/src/browser/prefetch/resource-ready.ts +77 -0
  28. package/src/browser/react/Link.tsx +67 -8
  29. package/src/browser/react/NavigationProvider.tsx +13 -4
  30. package/src/browser/react/context.ts +7 -2
  31. package/src/browser/react/use-handle.ts +9 -58
  32. package/src/browser/react/use-navigation.ts +22 -2
  33. package/src/browser/react/use-router.ts +21 -8
  34. package/src/browser/rsc-router.tsx +26 -3
  35. package/src/browser/scroll-restoration.ts +10 -8
  36. package/src/browser/segment-reconciler.ts +36 -14
  37. package/src/browser/server-action-bridge.ts +8 -6
  38. package/src/browser/types.ts +27 -5
  39. package/src/build/generate-manifest.ts +6 -6
  40. package/src/build/generate-route-types.ts +3 -0
  41. package/src/build/route-trie.ts +50 -24
  42. package/src/build/route-types/include-resolution.ts +8 -1
  43. package/src/build/route-types/router-processing.ts +211 -72
  44. package/src/build/route-types/scan-filter.ts +8 -1
  45. package/src/client.tsx +84 -230
  46. package/src/handle.ts +40 -0
  47. package/src/index.rsc.ts +3 -1
  48. package/src/index.ts +46 -6
  49. package/src/prerender/store.ts +5 -4
  50. package/src/prerender.ts +138 -77
  51. package/src/reverse.ts +25 -1
  52. package/src/route-definition/dsl-helpers.ts +194 -32
  53. package/src/route-definition/helpers-types.ts +67 -19
  54. package/src/route-definition/index.ts +3 -0
  55. package/src/route-definition/redirect.ts +9 -1
  56. package/src/route-definition/resolve-handler-use.ts +149 -0
  57. package/src/route-types.ts +18 -0
  58. package/src/router/content-negotiation.ts +100 -1
  59. package/src/router/handler-context.ts +51 -15
  60. package/src/router/intercept-resolution.ts +9 -4
  61. package/src/router/lazy-includes.ts +5 -5
  62. package/src/router/loader-resolution.ts +156 -21
  63. package/src/router/manifest.ts +22 -13
  64. package/src/router/match-api.ts +124 -189
  65. package/src/router/match-middleware/cache-lookup.ts +28 -8
  66. package/src/router/match-middleware/segment-resolution.ts +53 -0
  67. package/src/router/match-result.ts +82 -4
  68. package/src/router/middleware-types.ts +0 -6
  69. package/src/router/middleware.ts +0 -3
  70. package/src/router/navigation-snapshot.ts +182 -0
  71. package/src/router/prerender-match.ts +110 -10
  72. package/src/router/preview-match.ts +30 -102
  73. package/src/router/request-classification.ts +310 -0
  74. package/src/router/route-snapshot.ts +245 -0
  75. package/src/router/router-interfaces.ts +36 -4
  76. package/src/router/router-options.ts +37 -11
  77. package/src/router/segment-resolution/fresh.ts +71 -17
  78. package/src/router/segment-resolution/helpers.ts +29 -24
  79. package/src/router/segment-resolution/revalidation.ts +87 -18
  80. package/src/router/types.ts +1 -0
  81. package/src/router.ts +54 -5
  82. package/src/rsc/handler.ts +472 -372
  83. package/src/rsc/loader-fetch.ts +23 -3
  84. package/src/rsc/manifest-init.ts +5 -1
  85. package/src/rsc/progressive-enhancement.ts +14 -2
  86. package/src/rsc/rsc-rendering.ts +10 -1
  87. package/src/rsc/server-action.ts +8 -0
  88. package/src/rsc/ssr-setup.ts +2 -2
  89. package/src/rsc/types.ts +9 -1
  90. package/src/segment-content-promise.ts +67 -0
  91. package/src/segment-loader-promise.ts +122 -0
  92. package/src/segment-system.tsx +11 -61
  93. package/src/server/context.ts +65 -5
  94. package/src/server/handle-store.ts +19 -0
  95. package/src/server/loader-registry.ts +9 -8
  96. package/src/server/request-context.ts +134 -9
  97. package/src/ssr/index.tsx +3 -0
  98. package/src/static-handler.ts +18 -6
  99. package/src/types/cache-types.ts +4 -4
  100. package/src/types/handler-context.ts +30 -20
  101. package/src/types/loader-types.ts +36 -9
  102. package/src/types/route-entry.ts +12 -1
  103. package/src/types/segments.ts +1 -1
  104. package/src/urls/include-helper.ts +24 -14
  105. package/src/urls/path-helper-types.ts +39 -6
  106. package/src/urls/path-helper.ts +47 -12
  107. package/src/urls/pattern-types.ts +12 -0
  108. package/src/urls/response-types.ts +16 -6
  109. package/src/use-loader.tsx +77 -5
  110. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  111. package/src/vite/discovery/discover-routers.ts +5 -1
  112. package/src/vite/discovery/prerender-collection.ts +128 -74
  113. package/src/vite/discovery/state.ts +13 -4
  114. package/src/vite/index.ts +4 -0
  115. package/src/vite/plugin-types.ts +60 -5
  116. package/src/vite/plugins/expose-id-utils.ts +12 -0
  117. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  118. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  119. package/src/vite/plugins/performance-tracks.ts +88 -0
  120. package/src/vite/plugins/refresh-cmd.ts +88 -26
  121. package/src/vite/rango.ts +19 -2
  122. package/src/vite/router-discovery.ts +178 -37
  123. package/src/vite/utils/prerender-utils.ts +37 -5
  124. package/src/vite/utils/shared-utils.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.7dc955ec",
3
+ "version": "0.0.0-experimental.80",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -142,7 +142,7 @@
142
142
  "test:unit:watch": "vitest"
143
143
  },
144
144
  "dependencies": {
145
- "@vitejs/plugin-rsc": "^0.5.14",
145
+ "@vitejs/plugin-rsc": "^0.5.23",
146
146
  "magic-string": "^0.30.17",
147
147
  "picomatch": "^4.0.3",
148
148
  "rsc-html-stream": "^0.0.7"
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "peerDependencies": {
164
164
  "@cloudflare/vite-plugin": "^1.25.0",
165
- "@vitejs/plugin-rsc": "^0.5.14",
165
+ "@vitejs/plugin-rsc": "^0.5.23",
166
166
  "react": "^18.0.0 || ^19.0.0",
167
167
  "vite": "^7.3.0"
168
168
  },
@@ -0,0 +1,362 @@
1
+ ---
2
+ name: handler-use
3
+ description: Attach default loaders, middleware, parallels, and other use items directly to handlers via handler.use, and compose them with explicit use() at mount sites
4
+ argument-hint: "[handler]"
5
+ ---
6
+
7
+ # Handler-Attached `.use`
8
+
9
+ A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site.
10
+
11
+ This lets handlers be **self-contained, reusable units** — a page brings its own loader, a layout brings its own middleware, a parallel slot brings its own data + skeleton — without forcing every caller to wire the same items at every mount site.
12
+
13
+ Canonical implementation reference:
14
+ [src/route-definition/resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts)
15
+
16
+ ## Defining a handler with `.use`
17
+
18
+ Attach `.use` to the function (or to the branded definition for `Static()`/`Prerender()`/`Passthrough()`):
19
+
20
+ ```typescript
21
+ import {
22
+ loader,
23
+ middleware,
24
+ loading,
25
+ createLoader,
26
+ type Handler,
27
+ } from "@rangojs/router";
28
+
29
+ export const ProductLoader = createLoader(async (ctx) =>
30
+ fetchProduct(ctx.params.slug),
31
+ );
32
+
33
+ const ProductPage: Handler<"/product/:slug"> = async (ctx) => {
34
+ const product = await ctx.use(ProductLoader);
35
+ return <ProductView product={product} />;
36
+ };
37
+
38
+ ProductPage.use = () => [
39
+ loader(ProductLoader),
40
+ loading(<ProductSkeleton />),
41
+ middleware(async (ctx, next) => {
42
+ await next();
43
+ ctx.header("Cache-Control", "private, max-age=60");
44
+ }),
45
+ ];
46
+ ```
47
+
48
+ Now `ProductPage` carries its loader, loading state, and response-header middleware regardless of where it is mounted.
49
+
50
+ ## Allowed items per mount site
51
+
52
+ `handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
53
+
54
+ | Mount site | Allowed item types |
55
+ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
+ | `layout()` | All of the above, plus `route`, `include` |
58
+ | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
+ | `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `when`, `transition` |
60
+ | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
61
+
62
+ If `handler.use()` returns a disallowed item for a mount site, registration throws:
63
+
64
+ ```
65
+ handler.use() returned middleware() which is not valid inside parallel().
66
+ Allowed types: revalidate, loader, loading, errorBoundary, notFoundBoundary, transition.
67
+ ```
68
+
69
+ The narrowest contract is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
70
+
71
+ ## Composition with explicit `use()`
72
+
73
+ Every mount site that takes a `use` callback merges in this order:
74
+
75
+ 1. **`handler.use()` items first** — the handler's defaults.
76
+ 2. **Explicit `use()` items second** — overrides specified at the mount site.
77
+
78
+ Items of the same kind from the explicit `use()` follow the existing override rules of that item type. The most important ones for composition:
79
+
80
+ - **`loading()`** — last definition wins, so explicit `loading()` replaces the handler's default.
81
+ - **`parallel({ "@slot": … })`** — the last `parallel()` call wins per slot name. Other slots from earlier calls are preserved (see `skills/parallel`).
82
+ - **`loader()`, `middleware()`, etc.** — accumulate; both the handler's and the explicit ones run.
83
+
84
+ Skip the boilerplate: if neither `handler.use` nor explicit `use()` is provided, no merge happens.
85
+
86
+ ```typescript
87
+ // Handler brings a loader + a (placeholder) loading; explicit use replaces loading.
88
+ const SidebarSlot: Handler = async (ctx) => {
89
+ const data = await ctx.use(SidebarLoader);
90
+ return <Sidebar data={data} />;
91
+ };
92
+ SidebarSlot.use = () => [
93
+ loader(SidebarLoader),
94
+ loading(<DefaultSidebarSkeleton />),
95
+ ];
96
+
97
+ parallel({ "@sidebar": SidebarSlot }, () => [
98
+ // Replaces the default skeleton; SidebarLoader from handler.use still runs.
99
+ loading(<SiteSpecificSidebarSkeleton />),
100
+ ]);
101
+ ```
102
+
103
+ ## Composable parallel slots (the main pay-off)
104
+
105
+ The parallel slot site is where `handler.use` shines. A slot handler that owns its data/loading lets a layout declare **just** the slot names — every loader, skeleton, and revalidation contract travels with the slot itself.
106
+
107
+ ### Without `handler.use` (every caller wires it up)
108
+
109
+ ```typescript
110
+ layout(<DashboardLayout />, () => [
111
+ parallel({ "@cart": CartSummary }, () => [
112
+ loader(CartLoader),
113
+ loading(<CartSkeleton />),
114
+ revalidate(revalidateCartData),
115
+ ]),
116
+ parallel({ "@notifs": NotificationPanel }, () => [
117
+ loader(NotificationsLoader),
118
+ loading(<NotifsSkeleton />),
119
+ revalidate(revalidateNotifs),
120
+ ]),
121
+ path("/dashboard", DashboardIndex, { name: "dashboard.index" }),
122
+ ]);
123
+ ```
124
+
125
+ Every layout that wants `@cart` must repeat the same loader/loading/revalidate triplet.
126
+
127
+ ### With `handler.use` (slot owns its dependencies)
128
+
129
+ ```typescript
130
+ const CartSummary: Handler = async (ctx) => {
131
+ const cart = await ctx.use(CartLoader);
132
+ return <CartSummaryView cart={cart} />;
133
+ };
134
+ CartSummary.use = () => [
135
+ loader(CartLoader),
136
+ loading(<CartSkeleton />),
137
+ revalidate(revalidateCartData),
138
+ ];
139
+
140
+ const NotificationPanel: Handler = async (ctx) => {
141
+ const items = await ctx.use(NotificationsLoader);
142
+ return <NotificationsView items={items} />;
143
+ };
144
+ NotificationPanel.use = () => [
145
+ loader(NotificationsLoader),
146
+ loading(<NotifsSkeleton />),
147
+ revalidate(revalidateNotifs),
148
+ ];
149
+
150
+ // Mount sites become declarative — no per-call data wiring.
151
+ layout(<DashboardLayout />, () => [
152
+ parallel({ "@cart": CartSummary, "@notifs": NotificationPanel }),
153
+ path("/dashboard", DashboardIndex, { name: "dashboard.index" }),
154
+ ]);
155
+
156
+ layout(<AccountLayout />, () => [
157
+ // Same slot, same defaults, zero re-wiring.
158
+ parallel({ "@cart": CartSummary }),
159
+ path("/account", AccountIndex, { name: "account.index" }),
160
+ ]);
161
+ ```
162
+
163
+ Each slot handler is now a portable, self-contained unit. Different layouts can use the same slot without copying data plumbing.
164
+
165
+ ### Streaming behavior is per-slot
166
+
167
+ A slot's `loading()` (whether from `handler.use` or explicit) makes that slot an independent streaming unit — its loader does not block the parent layout. Two slot handlers with their own loading skeletons stream independently.
168
+
169
+ ```typescript
170
+ parallel({
171
+ "@cart": CartSummary, // handler.use loading() → streams independently
172
+ "@cartBadge": CartBadge, // no loading() anywhere → awaited before paint
173
+ });
174
+ ```
175
+
176
+ ### Two scopes for explicit `use` at the mount site: shared (broadcast) and slot-local
177
+
178
+ `parallel()` accepts an explicit `use()` callback that **broadcasts** to every slot in the call ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)). That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) — every slot gets them. (Note: `middleware` is not allowed inside `parallel()`; see the allowed-types table above.)
179
+
180
+ For single-assignment items like `loading()`, broadcasting overwrites every slot's `handler.use` default. Pass a **slot descriptor** `{ handler, use }` instead: items in the descriptor's `use` apply only to that slot.
181
+
182
+ ```typescript
183
+ parallel({
184
+ "@meta": MetaSlot,
185
+ "@sidebar": {
186
+ handler: SidebarSlot,
187
+ use: () => [loading(<SidebarSkeleton />)], // ← only @sidebar
188
+ },
189
+ });
190
+ ```
191
+
192
+ Per-slot merge order is **handler.use → shared use → slot-local use** (narrowest scope wins for last-write-wins items like `loading()`):
193
+
194
+ ```typescript
195
+ parallel(
196
+ {
197
+ "@cart": {
198
+ handler: Cart,
199
+ use: () => [loading(<CartSkeleton />)], // wins for @cart
200
+ },
201
+ "@notifs": Notifs, // gets <BroadcastSkeleton />
202
+ },
203
+ () => [
204
+ loader(SharedAnalyticsLoader), // accumulates on every slot
205
+ loading(<BroadcastSkeleton />), // applies to slots without slot-local
206
+ ],
207
+ );
208
+ ```
209
+
210
+ Use the descriptor's `use` for `loading(false)` too — opting one slot out of streaming without affecting siblings:
211
+
212
+ ```typescript
213
+ parallel(
214
+ {
215
+ "@cart": { handler: Cart, use: () => [loading(false)] }, // @cart awaits
216
+ "@notifs": Notifs, // @notifs still streams with broadcast skeleton
217
+ },
218
+ () => [loading(<BroadcastSkeleton />)],
219
+ );
220
+ ```
221
+
222
+ Rule of thumb: shared `use` is for items that legitimately apply to every slot. Slot-local `use` is for per-slot precision — especially `loading()` and `loading(false)`.
223
+
224
+ ### Replacing a whole slot from a parent's `handler.use`
225
+
226
+ A handler can publish a default `parallel({...})` set via its `.use`, and the mount site can replace any individual slot by re-declaring it. Last `parallel()` per slot name wins (see `skills/parallel` § Slot Override Semantics).
227
+
228
+ ```typescript
229
+ const ProductPage: Handler<"/product/:slug"> = (ctx) => (
230
+ <article>
231
+ <ProductHero slug={ctx.params.slug} />
232
+ <ParallelOutlet name="@related" />
233
+ <ParallelOutlet name="@reviews" />
234
+ </article>
235
+ );
236
+ ProductPage.use = () => [
237
+ parallel({
238
+ "@related": DefaultRelatedProducts,
239
+ "@reviews": DefaultReviews,
240
+ }),
241
+ ];
242
+
243
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
244
+ // Override @related only; @reviews keeps the default from handler.use.
245
+ parallel({ "@related": SiteSpecificRelated }),
246
+ ]);
247
+ ```
248
+
249
+ ## Other mount sites
250
+
251
+ ### Pages (`path()`)
252
+
253
+ Page handlers can carry middleware, loaders, error boundaries, parallel slots, etc. — anything from the `path` row of the table above.
254
+
255
+ ```typescript
256
+ const CheckoutPage: Handler<"/checkout"> = async (ctx) => { /* … */ };
257
+ CheckoutPage.use = () => [
258
+ middleware(requireAuth),
259
+ loader(CartLoader),
260
+ errorBoundary(<CheckoutError />),
261
+ notFoundBoundary(<CheckoutNotFound />),
262
+ ];
263
+ ```
264
+
265
+ ### Layouts (`layout()`)
266
+
267
+ Layout handlers can carry middleware that runs for every child route, plus default parallels, includes, etc.
268
+
269
+ ```typescript
270
+ const AdminLayout: Handler = (ctx) => {
271
+ const user = ctx.get(CurrentUser);
272
+ return <Admin user={user} />;
273
+ };
274
+ AdminLayout.use = () => [
275
+ middleware(requireAdmin),
276
+ parallel({ "@adminNotifs": AdminNotifsSlot }),
277
+ ];
278
+ ```
279
+
280
+ ### Intercepts (`intercept()`)
281
+
282
+ Intercept handlers can carry their own middleware chain, loaders, and even nested layouts/routes for the modal shell.
283
+
284
+ ```typescript
285
+ const QuickViewModal: Handler = async (ctx) => {
286
+ const product = await ctx.use(ProductLoader);
287
+ return <QuickView product={product} />;
288
+ };
289
+ QuickViewModal.use = () => [
290
+ loader(ProductLoader),
291
+ loading(<QuickViewSkeleton />),
292
+ layout(<ModalChrome />),
293
+ ];
294
+ ```
295
+
296
+ ## `loading()` is a single-assignment item — scope it correctly
297
+
298
+ Most `use` items accumulate when merged: `handler.use` `middleware()` runs _and_ explicit `middleware()` runs; both `loader()` registrations apply. `loading()` is different — it mutates `entry.loading` directly, last call wins ([dsl-helpers.ts `loadingFn`](../../src/route-definition/dsl-helpers.ts)).
299
+
300
+ For pages, layouts, and intercepts that's straightforward: explicit `loading()` at the mount site replaces any `loading()` from `handler.use`. The merge order is `handler.use → explicit`, so the explicit one is the last writer and wins.
301
+
302
+ For parallel slots, the shared `parallel(..., () => [...])` callback is **broadcast** to every slot in the call. A single `loading()` placed there lands on every slot, overwriting each slot's `handler.use` default. To scope `loading()` to one slot, use the **slot descriptor** form:
303
+
304
+ ```typescript
305
+ const Cart: Handler = async (ctx) => { /* … */ };
306
+ Cart.use = () => [loader(CartLoader), loading(<CartSkeleton />)];
307
+
308
+ const Notifs: Handler = async (ctx) => { /* … */ };
309
+ Notifs.use = () => [loader(NotifsLoader), loading(<NotifsSkeleton />)];
310
+
311
+ // ✅ @cart gets a custom skeleton; @notifs keeps its handler.use default.
312
+ parallel({
313
+ "@cart": {
314
+ handler: Cart,
315
+ use: () => [loading(<CustomCartSkeleton />)],
316
+ },
317
+ "@notifs": Notifs,
318
+ });
319
+
320
+ // ✅ Opt one slot out of streaming while siblings still stream the broadcast.
321
+ parallel(
322
+ {
323
+ "@cart": { handler: Cart, use: () => [loading(false)] },
324
+ "@notifs": Notifs,
325
+ },
326
+ () => [loading(<BroadcastSkeleton />)],
327
+ );
328
+ ```
329
+
330
+ Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-local is the narrowest scope, so it wins for last-write-wins items like `loading()`. Items that accumulate within the parallel allow-list (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) compose across all three layers regardless.
331
+
332
+ Other things to keep in mind about `loading()`:
333
+
334
+ - Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading: false` handling in [match-middleware/segment-resolution.ts](../../src/router/match-middleware/segment-resolution.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
335
+
336
+ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
337
+
338
+ ## Edge cases & gotchas
339
+
340
+ - **ReactNode handlers cannot have `.use`.** A bare JSX element passed as a handler (e.g., `path("/about", <About />)`) has no function to attach properties to. Pass a function or branded definition instead.
341
+ - **Branded handlers** — `Static()`, `Prerender()`, and `Passthrough()` are positional constructors (not object-arg). Construct first, then attach `.use` to the returned definition:
342
+
343
+ ```typescript
344
+ const ProductPage = Prerender(async (ctx) => {
345
+ const product = await fetchProduct(ctx.params.slug);
346
+ return <ProductView product={product} />;
347
+ });
348
+ ProductPage.use = () => [loader(ProductLoader)];
349
+ ```
350
+
351
+ - **Items can be flat or nested arrays.** `handler.use()` results are flattened with `.flat(3)` before validation, so factory helpers that return arrays inline work the same as in regular `use()` callbacks.
352
+ - **Validation runs at registration / first match**, not at handler definition. A handler doesn't know its mount site at definition time — the same handler used in a `path()` and an `intercept()` is validated against each mount's allowed-types set when registered.
353
+ - **No silent shadowing.** If a disallowed item slips through (e.g., a layout factory returning `cache()` from a slot's `handler.use`), the runtime throws with the offending type and mount site named.
354
+
355
+ ## Cross-references
356
+
357
+ - `skills/route` — `path()` mount site basics
358
+ - `skills/layout` — `layout()` mount site basics
359
+ - `skills/parallel` — parallel slot semantics, slot override rules, streaming behavior
360
+ - `skills/intercept` — intercept mount site basics
361
+ - `skills/loader` — defining `createLoader` and reading via `ctx.use()`
362
+ - `skills/middleware` — middleware semantics and ordering
@@ -311,3 +311,23 @@ export const shopPatterns = urls(({
311
311
  ]),
312
312
  ]);
313
313
  ```
314
+
315
+ ## Handler-attached `.use`
316
+
317
+ Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route`/`when` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome.
318
+
319
+ ```typescript
320
+ const QuickViewModal: Handler = async (ctx) => {
321
+ const product = await ctx.use(ProductLoader);
322
+ return <QuickView product={product} />;
323
+ };
324
+ QuickViewModal.use = () => [
325
+ loader(ProductLoader),
326
+ loading(<QuickViewSkeleton />),
327
+ layout(<ModalChrome />),
328
+ ];
329
+
330
+ intercept("@modal", "product", QuickViewModal);
331
+ ```
332
+
333
+ Explicit `use()` at the mount site merges with `handler.use` (handler defaults first, explicit second). See [skills/handler-use](../handler-use/SKILL.md) for merge order and the per-mount-site allowed-types table.
@@ -308,3 +308,25 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
308
308
  ]),
309
309
  ]);
310
310
  ```
311
+
312
+ ## Handler-attached `.use`
313
+
314
+ Layout handlers can carry their own middleware, default parallels, and includes via `.use` so a layout becomes a self-contained unit reusable across mount sites.
315
+
316
+ ```typescript
317
+ const AdminLayout: Handler = (ctx) => {
318
+ const user = ctx.get(CurrentUser);
319
+ return <Admin user={user} />;
320
+ };
321
+ AdminLayout.use = () => [
322
+ middleware(requireAdmin),
323
+ parallel({ "@adminNotifs": AdminNotifsSlot }),
324
+ ];
325
+
326
+ // Mount site declares structure only; defaults travel with the layout.
327
+ layout(AdminLayout, () => [
328
+ path("/admin", AdminIndex, { name: "admin.index" }),
329
+ ]);
330
+ ```
331
+
332
+ Allowed item types in a layout's `.use` mirror the layout `use()` callback (the broadest set). Explicit `use()` at the mount site merges with `handler.use` (handler defaults first, explicit second). See [skills/handler-use](../handler-use/SKILL.md) for merge order and per-mount-site allowed types.
@@ -139,7 +139,9 @@ function GlobalNav() {
139
139
  }
140
140
  ```
141
141
 
142
- `href()` is an identity function at runtime but provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
142
+ `href()` provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
143
+
144
+ `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
143
145
 
144
146
  ## Client: useHref()
145
147
 
@@ -65,24 +65,10 @@ export const urlpatterns = urls(({ path, loader }) => [
65
65
 
66
66
  ## Consuming Loader Data
67
67
 
68
- Loaders are the **live data layer** they resolve fresh on every request.
69
- The way you consume them depends on whether you're in a server component
70
- (route handler) or a client component.
71
-
72
- > **IMPORTANT: Prefer consuming loaders in client components.** Keeping data
73
- > fetching in loaders and consumption in client components creates a clean
74
- > separation: the server-side handler renders static markup that can be
75
- > freely cached with `cache()`, while loader data stays fresh on every
76
- > request. When you consume loaders in server handlers via `ctx.use()`, the
77
- > handler output depends on the loader data, which means caching the handler
78
- > also caches the data — defeating the purpose of the live data layer.
79
-
80
- ### In Client Components (Preferred)
81
-
82
- Client components use `useLoader()` from `@rangojs/router/client`.
83
- The loader **must** be registered with `loader()` in the route's DSL
84
- segments so the framework knows to resolve it during SSR and stream
85
- the data to the client:
68
+ Register loaders with `loader()` in the DSL and consume them in client
69
+ components with `useLoader()`. This is the recommended pattern it keeps
70
+ data fetching on the server and consumption on the client, with a clean
71
+ separation that works correctly with `cache()`.
86
72
 
87
73
  ```typescript
88
74
  "use client";
@@ -96,40 +82,60 @@ function ProductDetails() {
96
82
  ```
97
83
 
98
84
  ```typescript
99
- // Route definition — loader() registration required for client consumption
85
+ // Route definition — loader() registration required
100
86
  path("/product/:slug", ProductPage, { name: "product" }, () => [
101
- loader(ProductLoader), // Required for useLoader() in client components
87
+ loader(ProductLoader),
102
88
  ]);
103
89
  ```
104
90
 
105
- ### In Route Handlers (Server Components)
91
+ DSL loaders are the **live data layer** — they resolve fresh on every
92
+ request, even when the route is inside a `cache()` boundary. The router
93
+ excludes them from the segment cache at storage time and re-resolves them
94
+ on retrieval. This means `cache()` gives you cached UI + fresh data by
95
+ default.
106
96
 
107
- In server components, use `ctx.use(Loader)` directly in the route handler.
108
- This doesn't require `loader()` registration in the DSL — it works
109
- standalone. **However**, prefer client-side consumption when possible (see
110
- note above).
97
+ ### Cache safety
111
98
 
112
- ```typescript
113
- import { ProductLoader } from "./loaders/product";
99
+ DSL loaders can safely read `createVar({ cache: false })` variables
100
+ because they are always resolved fresh. The read guard is bypassed for
101
+ loader functions — they never produce stale data.
102
+
103
+ ### ctx.use(Loader) — escape hatch
104
+
105
+ For cases where you need loader data in the server handler itself (e.g.,
106
+ to set ctx variables or make routing decisions), use `ctx.use(Loader)`:
114
107
 
115
- // Route handler — server component
108
+ ```typescript
116
109
  path("/product/:slug", async (ctx) => {
117
110
  const { product } = await ctx.use(ProductLoader);
118
- return <h1>{product.name}</h1>;
119
- }, { name: "product" })
111
+ ctx.set(Product, product); // make available to children
112
+ return <ProductPage />;
113
+ }, { name: "product" }, () => [
114
+ loader(ProductLoader), // still register for client consumption
115
+ ])
120
116
  ```
121
117
 
122
- When you do register with `loader()` in the DSL, `ctx.use()` returns the
118
+ When you register with `loader()` in the DSL, `ctx.use()` returns the
123
119
  same memoized result — loaders never run twice per request.
124
120
 
121
+ **Limitations of ctx.use(Loader):**
122
+
123
+ - The handler output depends on the loader data. If the route is inside
124
+ `cache()`, the handler is cached with the loader result baked in —
125
+ defeating the live data guarantee.
126
+ - Non-cacheable variable reads (`createVar({ cache: false })`) inside the
127
+ handler still throw, even if the data came from a loader.
128
+ - Prefer DSL `loader()` + client `useLoader()` for data that depends on
129
+ non-cacheable context variables.
130
+
125
131
  **Never use `useLoader()` in server components** — it is a client-only API.
126
132
 
127
133
  ### Summary
128
134
 
129
- | Context | API | `loader()` DSL required? |
130
- | ---------------------------- | ------------------- | ------------------------ |
131
- | Client component (preferred) | `useLoader(Loader)` | **Yes** |
132
- | Route handler (server) | `ctx.use(Loader)` | No |
135
+ | Pattern | API | Cache-safe | Recommended |
136
+ | ---------------------- | ------------------- | ---------- | ----------- |
137
+ | DSL + client component | `useLoader(Loader)` | Yes | Yes |
138
+ | Handler escape hatch | `ctx.use(Loader)` | No | When needed |
133
139
 
134
140
  ## Loader Context
135
141
 
@@ -548,7 +554,7 @@ export const ProductLoader = createLoader(async (ctx) => {
548
554
  .first();
549
555
 
550
556
  if (!product) {
551
- throw new Response("Product not found", { status: 404 });
557
+ notFound("Product not found");
552
558
  }
553
559
 
554
560
  return { product };
@@ -564,10 +570,9 @@ export const CartLoader = createLoader(async (ctx) => {
564
570
  return { cart };
565
571
  });
566
572
 
567
- // urls.tsx
573
+ // urls.tsx — register loaders in the DSL
568
574
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
569
575
  layout(<ShopLayout />, () => [
570
- // Shared cart loader for all shop routes
571
576
  loader(CartLoader, () => [
572
577
  revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
573
578
  ]),
@@ -579,17 +584,22 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
579
584
  ]),
580
585
  ]);
581
586
 
582
- // pages/product.tsx — server component (route handler)
587
+ // components/ProductDetails.tsx — consume in client component
588
+ "use client";
589
+ import { useLoader } from "@rangojs/router/client";
583
590
  import { ProductLoader, CartLoader } from "./loaders/shop";
584
591
 
585
- async function ProductPage(ctx) {
586
- const { product } = await ctx.use(ProductLoader);
587
- const { cart } = await ctx.use(CartLoader);
592
+ function ProductDetails() {
593
+ const { data: { product } } = useLoader(ProductLoader);
594
+ const { data: { cart } } = useLoader(CartLoader);
588
595
 
589
596
  return (
590
597
  <div>
591
598
  <h1>{product.name}</h1>
592
- <AddToCartButton productId={product.id} inCart={cart?.items.includes(product.id)} />
599
+ <AddToCartButton
600
+ productId={product.id}
601
+ inCart={cart?.items.includes(product.id)}
602
+ />
593
603
  </div>
594
604
  );
595
605
  }
@@ -26,6 +26,8 @@ const router = createRouter<AppEnv>({})
26
26
  .routes(urlpatterns);
27
27
  ```
28
28
 
29
+ When the router has a `basename`, pattern-scoped `.use()` patterns are automatically prefixed. For example, with `basename: "/app"`, `.use("/admin/*", mw)` matches `/app/admin/*`.
30
+
29
31
  ### Route middleware (`middleware()` in `urls()`)
30
32
 
31
33
  Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
@@ -135,17 +137,46 @@ export const urlpatterns = urls(({ path, layout, middleware }) => [
135
137
  ## Middleware with Multiple Handlers
136
138
 
137
139
  ```typescript
138
- // Spread multiple middleware from a single export
140
+ // Group multiple middleware in an array
139
141
  export const shopMiddleware = [loggerMiddleware, mockAuthMiddleware];
140
142
 
141
- // In routes
143
+ // In routes — pass the array directly
142
144
  layout(<ShopLayout />, () => [
143
- middleware(...shopMiddleware),
145
+ middleware(shopMiddleware),
144
146
 
145
147
  path("/shop", ShopIndex, { name: "shop" }),
146
148
  ])
147
149
  ```
148
150
 
151
+ ## Wrapping Middleware (Scoped to Children)
152
+
153
+ Use the wrapping form to scope middleware to a subset of routes without
154
+ introducing a visible layout:
155
+
156
+ ```typescript
157
+ urls(({ path, middleware }) => [
158
+ // authMw only applies to /admin and /admin/settings
159
+ middleware(authMw, () => [
160
+ path("/admin", AdminPage, { name: "admin" }),
161
+ path("/admin/settings", SettingsPage, { name: "settings" }),
162
+ ]),
163
+
164
+ // Public route — no authMw
165
+ path("/", HomePage, { name: "home" }),
166
+ ]);
167
+ ```
168
+
169
+ Multiple middleware with wrapping:
170
+
171
+ ```typescript
172
+ middleware([authMw, loggingMw], () => [
173
+ path("/admin", AdminPage, { name: "admin" }),
174
+ ]);
175
+ ```
176
+
177
+ This creates a transparent layout (`<Outlet />`) that carries the middleware.
178
+ The middleware does not affect sibling routes outside the callback.
179
+
149
180
  ## Middleware Context
150
181
 
151
182
  ```typescript