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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.18",
3
+ "version": "0.0.0-experimental.19",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -132,6 +132,15 @@
132
132
  "access": "public",
133
133
  "tag": "experimental"
134
134
  },
135
+ "scripts": {
136
+ "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
137
+ "prepublishOnly": "pnpm build",
138
+ "typecheck": "tsc --noEmit",
139
+ "test": "playwright test",
140
+ "test:ui": "playwright test --ui",
141
+ "test:unit": "vitest run",
142
+ "test:unit:watch": "vitest"
143
+ },
135
144
  "dependencies": {
136
145
  "@vitejs/plugin-rsc": "^0.5.14",
137
146
  "magic-string": "^0.30.17",
@@ -141,12 +150,12 @@
141
150
  "devDependencies": {
142
151
  "@playwright/test": "^1.49.1",
143
152
  "@types/node": "^24.10.1",
144
- "@types/react": "^19.2.7",
145
- "@types/react-dom": "^19.2.3",
153
+ "@types/react": "catalog:",
154
+ "@types/react-dom": "catalog:",
146
155
  "esbuild": "^0.27.0",
147
156
  "jiti": "^2.6.1",
148
- "react": "^19.2.4",
149
- "react-dom": "^19.2.4",
157
+ "react": "catalog:",
158
+ "react-dom": "catalog:",
150
159
  "tinyexec": "^0.3.2",
151
160
  "typescript": "^5.3.0",
152
161
  "vitest": "^4.0.0"
@@ -164,13 +173,5 @@
164
173
  "vite": {
165
174
  "optional": true
166
175
  }
167
- },
168
- "scripts": {
169
- "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
170
- "typecheck": "tsc --noEmit",
171
- "test": "playwright test",
172
- "test:ui": "playwright test --ui",
173
- "test:unit": "vitest run",
174
- "test:unit:watch": "vitest"
175
176
  }
176
- }
177
+ }
@@ -484,7 +484,7 @@ Or via `ctx.setLocationState()` on any response:
484
484
 
485
485
  ```tsx
486
486
  (ctx) => {
487
- ctx.setLocationState([FlashMessage({ text: "Welcome back!" })]);
487
+ ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
488
488
  return <Dashboard />;
489
489
  };
490
490
  ```
@@ -8,6 +8,9 @@ argument-hint: [@slot-name] [route-to-intercept]
8
8
 
9
9
  Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
10
10
 
11
+ Canonical semantics reference:
12
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
13
+
11
14
  ## Basic Intercept
12
15
 
13
16
  ```typescript
@@ -68,6 +71,78 @@ intercept(
68
71
  )
69
72
  ```
70
73
 
74
+ ## Intercept Middleware
75
+
76
+ Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
77
+
78
+ ```
79
+ global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept handler -> intercept loaders
80
+ ```
81
+
82
+ ```typescript
83
+ intercept(
84
+ "@modal",
85
+ "product",
86
+ <ProductModal />,
87
+ () => [
88
+ middleware(async (ctx, next) => {
89
+ // Runs only for this intercept, after global and route middleware
90
+ ctx.set("interceptSource", "modal");
91
+ await next();
92
+ }),
93
+ loader(ProductLoader),
94
+ ]
95
+ )
96
+ ```
97
+
98
+ The intercept handler can read context variables set by all upstream middleware layers (global, route, and intercept-specific).
99
+
100
+ Handler/layout `ctx.set()` data follows the same rule as elsewhere:
101
+ intercepts see data produced in the current render pass, but partial
102
+ action revalidation only recomputes segments that actually revalidate.
103
+ If an intercept depends on data established by an outer layout/handler,
104
+ revalidate that outer segment too or reload/guard the data inside the
105
+ intercept.
106
+
107
+ ### Revalidation Contracts for Intercept Dependencies
108
+
109
+ Use named revalidation contracts on both the outer producer and the intercept
110
+ consumer when they share `ctx.set()` data:
111
+
112
+ ```typescript
113
+ export const revalidateProductShell = ({ actionId }) =>
114
+ actionId?.includes("src/actions/product.ts#") ?? false;
115
+
116
+ layout(ProductLayout, () => [
117
+ revalidate(revalidateProductShell), // producer reruns
118
+ intercept("@modal", "product", <ProductModal />, () => [
119
+ revalidate(revalidateProductShell), // consumer reruns
120
+ loader(ProductLoader),
121
+ ]),
122
+ ]);
123
+ ```
124
+
125
+ Compose multiple contracts if the intercept depends on multiple upstream
126
+ domains.
127
+
128
+ Helper handoff style keeps intercept trees terse:
129
+
130
+ ```typescript
131
+ import { revalidate } from "@rangojs/router";
132
+
133
+ export const revalidateProduct = () => [
134
+ revalidate(revalidateProductShell),
135
+ ];
136
+
137
+ layout(ProductLayout, () => [
138
+ revalidateProduct(),
139
+ intercept("@modal", "product", <ProductModal />, () => [
140
+ revalidateProduct(),
141
+ loader(ProductLoader),
142
+ ]),
143
+ ]);
144
+ ```
145
+
71
146
  ## Conditional Intercept with when()
72
147
 
73
148
  Only intercept based on navigation context:
@@ -166,6 +241,10 @@ Runtime behavior:
166
241
  Loaders inside the intercept always run fresh at request time, same as regular
167
242
  pre-rendered routes.
168
243
 
244
+ During action-driven partial revalidation, this same partial rule applies:
245
+ refreshing the intercept does not implicitly rebuild non-revalidated outer
246
+ segments.
247
+
169
248
  ## Complete Example
170
249
 
171
250
  ```typescript
@@ -8,6 +8,9 @@ argument-hint: [component]
8
8
 
9
9
  Layouts wrap child routes and persist during navigation within their scope.
10
10
 
11
+ Canonical semantics reference:
12
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
13
+
11
14
  ## Basic Layout
12
15
 
13
16
  ```typescript
@@ -145,6 +148,13 @@ A layout as a child of `path()` wraps the route content and can read
145
148
  data set by the route handler via `ctx.get()`. The handler always
146
149
  executes before its children.
147
150
 
151
+ This handler-first guarantee applies to a single full render pass
152
+ (initial render, prerender, or full HTML re-render). During partial
153
+ action revalidation, only the segments that revalidate are recomputed.
154
+ If an orphan layout depends on data established by an outer handler or
155
+ layout, that outer segment must also revalidate, or the orphan must
156
+ guard/reload the data independently.
157
+
148
158
  ```typescript
149
159
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
150
160
 
@@ -175,8 +185,10 @@ urls(({ path, layout, parallel }) => [
175
185
  ])
176
186
  ```
177
187
 
178
- Orphan layouts cannot call `ctx.set()` -- only the route handler and
179
- middleware can write context variables.
188
+ Orphan layouts can call `ctx.get()` to read data set by their parent
189
+ handler. They can also call `ctx.set()`, though the primary pattern is
190
+ for route handlers and middleware to write context variables and for
191
+ orphan layouts to read them.
180
192
 
181
193
  ## Layout Revalidation
182
194
 
@@ -198,6 +210,54 @@ layout(<CartLayout />, () => [
198
210
  ])
199
211
  ```
200
212
 
213
+ If child segments read data that was established by this layout or by a
214
+ route handler above them, revalidate the outer segment too. Partial
215
+ revalidation does not re-run non-revalidated ancestors just to rebuild
216
+ their `ctx.set()` state.
217
+
218
+ ### Revalidation Contracts
219
+
220
+ For shared upstream data, define named revalidation functions and reuse
221
+ them on both producer and consumer segments:
222
+
223
+ ```typescript
224
+ // revalidation-contracts.ts
225
+ export const revalidateCartData = ({ actionId }) =>
226
+ actionId?.includes("src/actions/cart.ts#addToCart") ?? false;
227
+ ```
228
+
229
+ ```typescript
230
+ layout(<CartLayout />, () => [
231
+ revalidate(revalidateCartData), // producer
232
+ path("/cart", CartPage, { name: "cart" }, () => [
233
+ revalidate(revalidateCartData), // consumer
234
+ ]),
235
+ ]);
236
+ ```
237
+
238
+ If a segment depends on multiple upstream domains, compose multiple
239
+ contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
240
+
241
+ You can also package them as importable handoff helpers:
242
+
243
+ ```typescript
244
+ // revalidation-contracts.ts
245
+ import { revalidate } from "@rangojs/router";
246
+
247
+ export const revalidateAuthData = ({ actionId }) =>
248
+ actionId?.includes("src/actions/auth.ts#") ?? false;
249
+ export const revalidateAuth = () => [revalidate(revalidateAuthData)];
250
+ ```
251
+
252
+ ```typescript
253
+ layout(<ShellLayout />, () => [
254
+ revalidateAuth(),
255
+ path("/account", AccountPage, { name: "account" }, () => [
256
+ revalidateAuth(),
257
+ ]),
258
+ ]);
259
+ ```
260
+
201
261
  ## Complete Example
202
262
 
203
263
  ```typescript
@@ -98,9 +98,12 @@ Loaders receive the same context as route handlers:
98
98
  export const ProductLoader = createLoader(async (ctx) => {
99
99
  "use server";
100
100
 
101
- // URL params
101
+ // URL params (may include client-provided overrides for fetchable loaders)
102
102
  const { slug } = ctx.params;
103
103
 
104
+ // Server-trusted route params (from URL pattern matching, cannot be overridden)
105
+ const { slug: trustedSlug } = ctx.routeParams;
106
+
104
107
  // Query params
105
108
  const variant = ctx.url.searchParams.get("variant");
106
109
 
@@ -117,6 +120,33 @@ export const ProductLoader = createLoader(async (ctx) => {
117
120
  });
118
121
  ```
119
122
 
123
+ ### params vs routeParams
124
+
125
+ - `ctx.params` — merged route params + explicit loader params. For fetchable
126
+ loaders called with `load(Loader, { params: { ... } })`, explicit params
127
+ override route-matched params.
128
+ - `ctx.routeParams` — server-trusted route params from URL pattern matching.
129
+ Cannot be overridden by client-provided params.
130
+
131
+ Use `ctx.routeParams` when you need trusted route identity for authorization
132
+ or resource scoping:
133
+
134
+ ```typescript
135
+ export const OrderLoader = createLoader(async (ctx) => {
136
+ "use server";
137
+
138
+ // Use routeParams for auth checks — client cannot spoof the URL-matched ID
139
+ const { orderId } = ctx.routeParams;
140
+ const user = ctx.get("user");
141
+
142
+ const order = await db.orders.get(orderId);
143
+ if (order.userId !== user.id)
144
+ throw new Response("Forbidden", { status: 403 });
145
+
146
+ return { order };
147
+ });
148
+ ```
149
+
120
150
  ## Loader with Children
121
151
 
122
152
  Add caching or revalidation to specific loaders:
@@ -138,6 +168,44 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
138
168
  ]);
139
169
  ```
140
170
 
171
+ ### Revalidation Contracts for Loader Dependencies
172
+
173
+ If a loader reads `ctx.get()` data produced by an outer handler/layout, share
174
+ the same named revalidation contract across producer and consumer segments.
175
+
176
+ ```typescript
177
+ // revalidation-contracts.ts
178
+ export const revalidateAccountScope = ({ actionId }) =>
179
+ actionId?.includes("src/actions/account.ts#") ?? false;
180
+
181
+ layout(AccountLayout, () => [
182
+ revalidate(revalidateAccountScope), // producer reruns
183
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
184
+ loader(OrdersLoader, () => [
185
+ revalidate(revalidateAccountScope), // consumer reruns
186
+ ]),
187
+ ]),
188
+ ]);
189
+ ```
190
+
191
+ For segments that depend on multiple upstream domains, compose multiple
192
+ contracts on both sides.
193
+
194
+ To keep loader route trees concise, export helper wrappers:
195
+
196
+ ```typescript
197
+ import { revalidate } from "@rangojs/router";
198
+
199
+ export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
200
+
201
+ layout(AccountLayout, () => [
202
+ revalidateAccount(),
203
+ path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
204
+ loader(OrdersLoader, () => [revalidateAccount()]),
205
+ ]),
206
+ ]);
207
+ ```
208
+
141
209
  ## Loaders: The Live Data Layer
142
210
 
143
211
  Loaders are the live data layer of the router. They resolve fresh on every
@@ -349,6 +417,31 @@ export const SearchLoader = createLoader(async (ctx) => {
349
417
  }, true); // true = fetchable
350
418
  ```
351
419
 
420
+ ### Fetchable Loader with Middleware
421
+
422
+ Pass an options object instead of `true` to attach per-loader middleware.
423
+ This middleware runs only on `_rsc_loader` fetch requests (client-side
424
+ `load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
425
+
426
+ ```typescript
427
+ import { createLoader } from "@rangojs/router";
428
+ import { authMiddleware } from "../middleware/auth";
429
+ import { rateLimitMiddleware } from "../middleware/rate-limit";
430
+
431
+ export const ProtectedLoader = createLoader(
432
+ async (ctx) => {
433
+ "use server";
434
+
435
+ const user = ctx.get("user");
436
+ return { orders: await db.orders.list(user.id) };
437
+ },
438
+ { middleware: [authMiddleware, rateLimitMiddleware] },
439
+ );
440
+ ```
441
+
442
+ The middleware uses the same `MiddlewareFn` signature as route/app middleware,
443
+ so you can reuse existing middleware functions directly.
444
+
352
445
  Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
353
446
  The `load()` function auto-detects the body type:
354
447
 
@@ -8,6 +8,87 @@ argument-hint: [middleware-name]
8
8
 
9
9
  Middleware runs before/after route handlers using the onion model.
10
10
 
11
+ ## Execution Model
12
+
13
+ Canonical semantics reference:
14
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
15
+
16
+ There are two levels of middleware with different execution scopes:
17
+
18
+ ### Global middleware (`router.use()`)
19
+
20
+ Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders.
21
+
22
+ ```typescript
23
+ const router = createRouter<AppEnv>({})
24
+ .use(loggerMiddleware) // all routes
25
+ .use("/admin/*", authMiddleware) // pattern-scoped
26
+ .routes(urlpatterns);
27
+ ```
28
+
29
+ ### Route middleware (`middleware()` in `urls()`)
30
+
31
+ 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).
32
+
33
+ ```
34
+ Request flow (with action):
35
+ global mw -> action executes -> route mw -> layout -> handler -> loaders
36
+
37
+ Request flow (no action):
38
+ global mw -> route mw -> layout -> handler -> loaders
39
+
40
+ Progressive enhancement (no-JS form POST):
41
+ global mw -> action executes -> route mw -> full page re-render
42
+ ```
43
+
44
+ The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
45
+
46
+ Revalidation is still partial. Route middleware wraps the render pass that
47
+ does happen, but it does not force unrelated outer segments to recompute.
48
+ If a child segment depends on data established by an outer handler/layout,
49
+ revalidate that outer segment too, or have the child guard/reload the
50
+ data itself.
51
+
52
+ ### Revalidation Contracts with Middleware-Backed Trees
53
+
54
+ Middleware can establish request-level context (`ctx.set`) for segments that
55
+ execute in the current render pass. It does not change partial revalidation
56
+ boundaries between handler/layout/parallel segments.
57
+
58
+ For shared segment data, use named revalidation contracts on both the producer
59
+ and consumer segments, even when middleware is present in the chain.
60
+
61
+ ```typescript
62
+ export const revalidateCartData = ({ actionId }) =>
63
+ actionId?.includes("src/actions/cart.ts#") ?? false;
64
+
65
+ layout(CartLayout, () => [
66
+ middleware(cartRenderMiddleware),
67
+ revalidate(revalidateCartData), // producer reruns
68
+ parallel(
69
+ { "@cart": CartSummary },
70
+ () => [revalidate(revalidateCartData)], // consumer reruns
71
+ ),
72
+ ]);
73
+ ```
74
+
75
+ You can package those contracts as importable helpers to avoid repeating
76
+ `revalidate(...)` at each segment:
77
+
78
+ ```typescript
79
+ import { revalidate } from "@rangojs/router";
80
+
81
+ export const revalidateCart = () => [revalidate(revalidateCartData)];
82
+
83
+ layout(CartLayout, () => [
84
+ middleware(cartRenderMiddleware),
85
+ revalidateCart(),
86
+ parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
87
+ ]);
88
+ ```
89
+
90
+ Route middleware is the right place for per-route concerns that affect rendering (setting context variables for handlers, adding response headers, reading cookies set by actions). It is NOT the right place for action guards -- use global middleware for that.
91
+
11
92
  ## Basic Middleware
12
93
 
13
94
  ```typescript
@@ -8,6 +8,9 @@ argument-hint: [@slot-name]
8
8
 
9
9
  Parallel routes render multiple components simultaneously in named slots.
10
10
 
11
+ Canonical semantics reference:
12
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
13
+
11
14
  ## Basic Parallel Routes
12
15
 
13
16
  ```typescript
@@ -56,8 +59,21 @@ parallel({
56
59
 
57
60
  ## Reading Handler Data
58
61
 
59
- When a parallel is inside a route that uses `ctx.set()`, it can read that
60
- data via `ctx.get()`. The route handler always executes before its children.
62
+ Parallels can read `ctx.set()` values from their parent handler or layout
63
+ via `ctx.get()`. The handler always executes before its parallels
64
+ (handler-first).
65
+
66
+ Visibility follows tree structure:
67
+
68
+ - Layout-level parallels see layout data, but not path handler data
69
+ (the path is a separate entry).
70
+ - Parallels inside a path (or its orphan layouts) see both layout and
71
+ path handler data.
72
+
73
+ This applies to full render passes. During partial action revalidation,
74
+ only revalidated segments are recomputed. If a parallel depends on data
75
+ set by an outer handler or layout, revalidate that outer segment too, or
76
+ have the parallel reload/guard the data itself.
61
77
 
62
78
  ```typescript
63
79
  path("/dashboard/:id", (ctx) => {
@@ -142,6 +158,45 @@ parallel(
142
158
  )
143
159
  ```
144
160
 
161
+ Revalidating only the parallel does not re-run outer handlers/layouts.
162
+ If the slot reads `ctx.get()` data established above it, opt the outer
163
+ segment into revalidation as well.
164
+
165
+ ### Revalidation Contracts for Parallel Dependencies
166
+
167
+ Prefer named revalidation contracts shared by both the upstream producer and
168
+ the parallel consumer:
169
+
170
+ ```typescript
171
+ // revalidation-contracts.ts
172
+ export const revalidateCartData = ({ actionId }) =>
173
+ actionId?.includes("src/actions/cart.ts#") ?? false;
174
+
175
+ layout(CartLayout, () => [
176
+ revalidate(revalidateCartData), // producer reruns
177
+ parallel(
178
+ { "@cart": CartSummary },
179
+ () => [revalidate(revalidateCartData)], // consumer reruns
180
+ ),
181
+ ]);
182
+ ```
183
+
184
+ If the slot consumes multiple upstream domains, compose the contracts on both
185
+ segments.
186
+
187
+ Handoff helper style also works:
188
+
189
+ ```typescript
190
+ import { revalidate } from "@rangojs/router";
191
+
192
+ export const revalidateCart = () => [revalidate(revalidateCartData)];
193
+
194
+ layout(CartLayout, () => [
195
+ revalidateCart(),
196
+ parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
197
+ ]);
198
+ ```
199
+
145
200
  ## Named Outlets
146
201
 
147
202
  Use `ParallelOutlet` to render slots in layouts: