@rangojs/router 0.0.0-experimental.b02a2fec → 0.0.0-experimental.b3f2d0d9

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 (255) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2151 -846
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +364 -0
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +46 -4
  18. package/skills/layout/SKILL.md +28 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +47 -12
  22. package/skills/migrate-nextjs/SKILL.md +562 -0
  23. package/skills/migrate-react-router/SKILL.md +769 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +71 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -22
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +57 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +777 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +85 -12
  47. package/src/browser/navigation-client.ts +76 -28
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +64 -26
  51. package/src/browser/prefetch/cache.ts +129 -21
  52. package/src/browser/prefetch/fetch.ts +148 -16
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +30 -2
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-reconciler.ts +36 -14
  71. package/src/browser/segment-structure-assert.ts +2 -2
  72. package/src/browser/server-action-bridge.ts +23 -30
  73. package/src/browser/types.ts +21 -0
  74. package/src/build/collect-fallback-refs.ts +107 -0
  75. package/src/build/generate-manifest.ts +60 -35
  76. package/src/build/generate-route-types.ts +2 -0
  77. package/src/build/index.ts +2 -0
  78. package/src/build/route-trie.ts +52 -25
  79. package/src/build/route-types/codegen.ts +4 -4
  80. package/src/build/route-types/include-resolution.ts +1 -1
  81. package/src/build/route-types/per-module-writer.ts +7 -4
  82. package/src/build/route-types/router-processing.ts +55 -14
  83. package/src/build/route-types/scan-filter.ts +1 -1
  84. package/src/build/route-types/source-scan.ts +118 -0
  85. package/src/build/runtime-discovery.ts +9 -20
  86. package/src/cache/cache-scope.ts +28 -42
  87. package/src/cache/cf/cf-cache-store.ts +54 -13
  88. package/src/client.rsc.tsx +3 -0
  89. package/src/client.tsx +92 -182
  90. package/src/context-var.ts +5 -5
  91. package/src/decode-loader-results.ts +36 -0
  92. package/src/errors.ts +30 -1
  93. package/src/handle.ts +26 -13
  94. package/src/host/index.ts +2 -2
  95. package/src/host/router.ts +129 -57
  96. package/src/host/types.ts +31 -2
  97. package/src/host/utils.ts +1 -1
  98. package/src/href-client.ts +140 -20
  99. package/src/index.rsc.ts +9 -4
  100. package/src/index.ts +53 -15
  101. package/src/loader-store.ts +500 -0
  102. package/src/loader.rsc.ts +21 -6
  103. package/src/loader.ts +3 -10
  104. package/src/missing-id-error.ts +68 -0
  105. package/src/outlet-context.ts +1 -1
  106. package/src/prerender.ts +4 -4
  107. package/src/response-utils.ts +37 -0
  108. package/src/reverse.ts +65 -36
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +384 -257
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +100 -28
  113. package/src/route-definition/resolve-handler-use.ts +6 -0
  114. package/src/route-definition/use-item-types.ts +32 -0
  115. package/src/route-types.ts +26 -41
  116. package/src/router/basename.ts +14 -0
  117. package/src/router/content-negotiation.ts +15 -2
  118. package/src/router/error-handling.ts +1 -1
  119. package/src/router/handler-context.ts +21 -38
  120. package/src/router/intercept-resolution.ts +4 -18
  121. package/src/router/lazy-includes.ts +8 -8
  122. package/src/router/loader-resolution.ts +19 -2
  123. package/src/router/manifest.ts +22 -13
  124. package/src/router/match-api.ts +4 -3
  125. package/src/router/match-handlers.ts +63 -20
  126. package/src/router/match-middleware/cache-lookup.ts +44 -91
  127. package/src/router/match-middleware/cache-store.ts +3 -2
  128. package/src/router/match-result.ts +53 -32
  129. package/src/router/metrics.ts +1 -1
  130. package/src/router/middleware-types.ts +15 -26
  131. package/src/router/middleware.ts +99 -84
  132. package/src/router/pattern-matching.ts +101 -17
  133. package/src/router/prerender-match.ts +1 -1
  134. package/src/router/preview-match.ts +3 -1
  135. package/src/router/request-classification.ts +4 -28
  136. package/src/router/revalidation.ts +58 -2
  137. package/src/router/router-interfaces.ts +45 -28
  138. package/src/router/router-options.ts +40 -1
  139. package/src/router/router-registry.ts +2 -5
  140. package/src/router/segment-resolution/fresh.ts +27 -6
  141. package/src/router/segment-resolution/revalidation.ts +147 -106
  142. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  143. package/src/router/substitute-pattern-params.ts +56 -0
  144. package/src/router/telemetry.ts +99 -0
  145. package/src/router/trie-matching.ts +18 -13
  146. package/src/router/types.ts +8 -0
  147. package/src/router/url-params.ts +49 -0
  148. package/src/router.ts +38 -23
  149. package/src/rsc/handler-context.ts +2 -2
  150. package/src/rsc/handler.ts +28 -69
  151. package/src/rsc/helpers.ts +91 -43
  152. package/src/rsc/index.ts +1 -1
  153. package/src/rsc/origin-guard.ts +28 -10
  154. package/src/rsc/progressive-enhancement.ts +4 -0
  155. package/src/rsc/response-route-handler.ts +46 -53
  156. package/src/rsc/rsc-rendering.ts +35 -51
  157. package/src/rsc/runtime-warnings.ts +9 -10
  158. package/src/rsc/server-action.ts +17 -37
  159. package/src/rsc/ssr-setup.ts +16 -0
  160. package/src/rsc/types.ts +8 -2
  161. package/src/search-params.ts +4 -4
  162. package/src/segment-content-promise.ts +67 -0
  163. package/src/segment-loader-promise.ts +122 -0
  164. package/src/segment-system.tsx +132 -116
  165. package/src/serialize.ts +243 -0
  166. package/src/server/context.ts +143 -53
  167. package/src/server/cookie-store.ts +28 -4
  168. package/src/server/request-context.ts +20 -42
  169. package/src/ssr/index.tsx +5 -1
  170. package/src/static-handler.ts +1 -1
  171. package/src/testing/cache-status.ts +166 -0
  172. package/src/testing/collect-handle.ts +63 -0
  173. package/src/testing/dispatch.ts +440 -0
  174. package/src/testing/dom.entry.ts +22 -0
  175. package/src/testing/e2e/fixture.ts +154 -0
  176. package/src/testing/e2e/index.ts +149 -0
  177. package/src/testing/e2e/matchers.ts +51 -0
  178. package/src/testing/e2e/page-helpers.ts +272 -0
  179. package/src/testing/e2e/parity.ts +306 -0
  180. package/src/testing/e2e/server.ts +183 -0
  181. package/src/testing/flight-matchers.ts +104 -0
  182. package/src/testing/flight-runtime.d.ts +57 -0
  183. package/src/testing/flight-tree.ts +320 -0
  184. package/src/testing/flight.entry.ts +39 -0
  185. package/src/testing/flight.ts +197 -0
  186. package/src/testing/generated-routes.ts +223 -0
  187. package/src/testing/index.ts +106 -0
  188. package/src/testing/internal/context.ts +304 -0
  189. package/src/testing/internal/flight-client-globals.ts +30 -0
  190. package/src/testing/render-route.tsx +565 -0
  191. package/src/testing/run-loader.ts +341 -0
  192. package/src/testing/run-middleware.ts +179 -0
  193. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  194. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  195. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  196. package/src/testing/vitest-stubs/version.ts +5 -0
  197. package/src/testing/vitest.ts +270 -0
  198. package/src/types/global-namespace.ts +39 -26
  199. package/src/types/handler-context.ts +68 -50
  200. package/src/types/index.ts +1 -0
  201. package/src/types/loader-types.ts +5 -6
  202. package/src/types/request-scope.ts +126 -0
  203. package/src/types/route-entry.ts +11 -0
  204. package/src/types/segments.ts +35 -2
  205. package/src/urls/include-helper.ts +34 -67
  206. package/src/urls/index.ts +0 -3
  207. package/src/urls/path-helper-types.ts +41 -7
  208. package/src/urls/path-helper.ts +17 -52
  209. package/src/urls/pattern-types.ts +36 -19
  210. package/src/urls/response-types.ts +22 -29
  211. package/src/urls/type-extraction.ts +26 -116
  212. package/src/urls/urls-function.ts +1 -5
  213. package/src/use-loader.tsx +413 -42
  214. package/src/vite/debug.ts +185 -0
  215. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  216. package/src/vite/discovery/discover-routers.ts +101 -51
  217. package/src/vite/discovery/discovery-errors.ts +194 -0
  218. package/src/vite/discovery/gate-state.ts +171 -0
  219. package/src/vite/discovery/prerender-collection.ts +67 -26
  220. package/src/vite/discovery/route-types-writer.ts +40 -84
  221. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  222. package/src/vite/discovery/state.ts +33 -0
  223. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  224. package/src/vite/index.ts +2 -0
  225. package/src/vite/plugin-types.ts +67 -0
  226. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  227. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  228. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  229. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  230. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  231. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  232. package/src/vite/plugins/expose-action-id.ts +54 -30
  233. package/src/vite/plugins/expose-id-utils.ts +12 -8
  234. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  235. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  236. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  237. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  238. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  239. package/src/vite/plugins/performance-tracks.ts +29 -25
  240. package/src/vite/plugins/use-cache-transform.ts +65 -50
  241. package/src/vite/plugins/version-injector.ts +39 -23
  242. package/src/vite/plugins/version-plugin.ts +59 -2
  243. package/src/vite/plugins/virtual-entries.ts +2 -2
  244. package/src/vite/rango.ts +116 -29
  245. package/src/vite/router-discovery.ts +750 -100
  246. package/src/vite/utils/ast-handler-extract.ts +15 -15
  247. package/src/vite/utils/banner.ts +1 -1
  248. package/src/vite/utils/bundle-analysis.ts +4 -2
  249. package/src/vite/utils/client-chunks.ts +190 -0
  250. package/src/vite/utils/forward-user-plugins.ts +193 -0
  251. package/src/vite/utils/manifest-utils.ts +21 -5
  252. package/src/vite/utils/package-resolution.ts +41 -1
  253. package/src/vite/utils/prerender-utils.ts +21 -6
  254. package/src/vite/utils/shared-utils.ts +107 -26
  255. package/src/browser/action-response-classifier.ts +0 -99
@@ -52,6 +52,34 @@ export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> =
52
52
 
53
53
  let hostRouterAutoId = 0;
54
54
 
55
+ /** Whether a value is thenable (a Promise or Promise-like). */
56
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
57
+ return (
58
+ value !== null &&
59
+ (typeof value === "object" || typeof value === "function") &&
60
+ typeof (value as { then?: unknown }).then === "function"
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Whether a resolved value looks like a module namespace from a lazy import -
66
+ * an object with a `default` export that is a function (a Handler) or a host
67
+ * router (an object with `match`). Used to detect a `.map(() => import(...))`
68
+ * misuse: an inline handler should return a Response, not a module.
69
+ */
70
+ function looksLikeLazyModule(value: unknown): boolean {
71
+ if (value === null || typeof value !== "object" || !("default" in value)) {
72
+ return false;
73
+ }
74
+ const defaultExport = (value as { default: unknown }).default;
75
+ return (
76
+ typeof defaultExport === "function" ||
77
+ (typeof defaultExport === "object" &&
78
+ defaultExport !== null &&
79
+ "match" in defaultExport)
80
+ );
81
+ }
82
+
55
83
  /**
56
84
  * Create a host router
57
85
  */
@@ -77,32 +105,44 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
77
105
  ): HostRouteBuilder {
78
106
  const middleware: Middleware[] = [];
79
107
 
108
+ function register(
109
+ handler: Handler | LazyHandler,
110
+ kind: RouteEntry["kind"],
111
+ ): HostRouter {
112
+ const entry: RouteEntry = {
113
+ patterns,
114
+ middleware,
115
+ handler,
116
+ kind,
117
+ isFallback,
118
+ };
119
+
120
+ if (isFallback) {
121
+ fallbackRoute = entry;
122
+ } else {
123
+ routes.push(entry);
124
+ }
125
+
126
+ log(
127
+ `Registered ${isFallback ? "fallback" : "route"} (${kind}):`,
128
+ patterns.join(", "),
129
+ );
130
+
131
+ return router;
132
+ }
133
+
80
134
  return {
81
135
  use(...mw: Middleware[]): HostRouteBuilder {
82
136
  middleware.push(...mw);
83
137
  return this;
84
138
  },
85
139
 
86
- map(handler: Handler | LazyHandler): HostRouter {
87
- const entry: RouteEntry = {
88
- patterns,
89
- middleware,
90
- handler,
91
- isFallback,
92
- };
93
-
94
- if (isFallback) {
95
- fallbackRoute = entry;
96
- } else {
97
- routes.push(entry);
98
- }
99
-
100
- log(
101
- `Registered ${isFallback ? "fallback" : "route"}:`,
102
- patterns.join(", "),
103
- );
140
+ map(handler: Handler): HostRouter {
141
+ return register(handler, "handler");
142
+ },
104
143
 
105
- return router;
144
+ lazy(handler: LazyHandler): HostRouter {
145
+ return register(handler, "lazy");
106
146
  },
107
147
  };
108
148
  }
@@ -169,50 +209,82 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
169
209
  }
170
210
 
171
211
  /**
172
- * Execute handler (lazy or direct)
212
+ * Execute a route entry, branching on its declared kind:
213
+ * - "lazy": await the loader, then delegate to the default export
214
+ * (a nested HostRouter via `.match`, or a request Handler directly).
215
+ * - "handler": call the inline handler with the request. A `.map()` handler
216
+ * that resolves to a module namespace (`{ default }`) is almost certainly
217
+ * a misused lazy import, so it is rejected with a clear message rather
218
+ * than silently returning a module object as the response.
173
219
  */
174
220
  async function executeHandler(
175
- handler: Handler | LazyHandler,
221
+ entry: RouteEntry,
176
222
  request: Request,
177
223
  input: RouterRequestInput<any>,
178
224
  ): Promise<Response> {
179
- // Check if it's a lazy handler (function that returns promise)
180
- if (typeof handler === "function") {
181
- const result = handler(request, input);
182
-
183
- // If it returns a promise with default export
184
- if (result && typeof result === "object" && "then" in result) {
185
- const module = await result;
186
- if (
187
- typeof module === "object" &&
188
- module !== null &&
189
- "default" in module
190
- ) {
191
- const defaultExport = (module as { default: Handler | HostRouter })
192
- .default;
193
-
194
- // If default export is a router with match method
195
- if (
196
- typeof defaultExport === "object" &&
197
- defaultExport !== null &&
198
- "match" in defaultExport
199
- ) {
200
- return (defaultExport as HostRouter).match(request, input);
201
- }
225
+ const { handler, kind } = entry;
202
226
 
203
- // Otherwise treat as handler
204
- return (defaultExport as Handler)(request, input);
205
- }
206
- // If promise resolves to Response
207
- return result as Promise<Response>;
227
+ if (typeof handler !== "function") {
228
+ throw new InvalidHandlerError(handler, {
229
+ cause: { handlerType: typeof handler },
230
+ });
231
+ }
232
+
233
+ if (kind === "lazy") {
234
+ return executeLazyMount(handler as LazyHandler, request, input);
235
+ }
236
+
237
+ const result = (handler as Handler)(request, input);
238
+
239
+ // Inline handlers may be async; await to obtain the Response and to run the
240
+ // misuse guard below.
241
+ if (isThenable(result)) {
242
+ const awaited = await result;
243
+ if (looksLikeLazyModule(awaited)) {
244
+ throw new HostRouterError(
245
+ ".map() is for inline request handlers; use .lazy(() => import(...)) for lazy host mounts.",
246
+ );
247
+ }
248
+ return awaited as Response;
249
+ }
250
+
251
+ return result;
252
+ }
253
+
254
+ /**
255
+ * Resolve a `.lazy()` mount: invoke the zero-arg loader, then dispatch to the
256
+ * module's default export.
257
+ */
258
+ async function executeLazyMount(
259
+ loader: LazyHandler,
260
+ request: Request,
261
+ input: RouterRequestInput<any>,
262
+ ): Promise<Response> {
263
+ const module = await loader();
264
+
265
+ if (typeof module === "object" && module !== null && "default" in module) {
266
+ const defaultExport = (module as { default: Handler | HostRouter })
267
+ .default;
268
+
269
+ // Default export is a nested host router
270
+ if (
271
+ typeof defaultExport === "object" &&
272
+ defaultExport !== null &&
273
+ "match" in defaultExport
274
+ ) {
275
+ return (defaultExport as HostRouter).match(request, input);
208
276
  }
209
277
 
210
- // Direct handler
211
- return result as Response | Promise<Response>;
278
+ // Otherwise treat the default export as a request handler
279
+ return (defaultExport as Handler)(request, input);
212
280
  }
213
281
 
214
- throw new InvalidHandlerError(handler, {
215
- cause: { handlerType: typeof handler },
282
+ throw new InvalidHandlerError(loader, {
283
+ cause: {
284
+ reason:
285
+ "lazy mount did not resolve to a module with a default export; " +
286
+ "use .lazy(() => import('./sub-app')) where the module default-exports a handler or host router",
287
+ },
216
288
  });
217
289
  }
218
290
 
@@ -252,6 +324,7 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
252
324
  return {
253
325
  pattern,
254
326
  handler: route.handler,
327
+ kind: route.kind,
255
328
  };
256
329
  }
257
330
  }
@@ -288,8 +361,7 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
288
361
  allMiddleware,
289
362
  request,
290
363
  fallbackInput,
291
- () =>
292
- executeHandler(fallbackRoute!.handler, request, fallbackInput),
364
+ () => executeHandler(fallbackRoute!, request, fallbackInput),
293
365
  );
294
366
  }
295
367
 
@@ -330,14 +402,14 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
330
402
 
331
403
  // Execute middleware chain and handler
332
404
  return executeMiddleware(allMiddleware, request, input, () =>
333
- executeHandler(matchedRoute.handler, request, input),
405
+ executeHandler(matchedRoute, request, input),
334
406
  );
335
407
  },
336
408
  };
337
409
 
338
410
  // Register in the global HostRouterRegistry for build-time discovery.
339
411
  // The routes array and fallbackRoute ref are live - they reflect routes
340
- // added via .host().map() after this point.
412
+ // added via .host().map()/.lazy() after this point.
341
413
  const registryId = `host-router-${hostRouterAutoId++}`;
342
414
  HostRouterRegistry.set(registryId, {
343
415
  get routes() {
package/src/host/types.ts CHANGED
@@ -35,12 +35,24 @@ export type Middleware = (
35
35
  */
36
36
  export type HostPattern = string | string[];
37
37
 
38
+ /**
39
+ * Whether a route entry is an inline request handler or a lazy module mount.
40
+ *
41
+ * Stored on the entry so discovery and runtime act on the consumer's declared
42
+ * intent instead of inferring it from the function's shape (arity/return value),
43
+ * which is ambiguous: a lazy loader may declare an ignored param, and an inline
44
+ * handler may be async. `.map()` registers `"handler"`, `.lazy()` registers
45
+ * `"lazy"`.
46
+ */
47
+ export type RouteEntryKind = "handler" | "lazy";
48
+
38
49
  /**
39
50
  * Result from testing a hostname against patterns
40
51
  */
41
52
  export interface HostMatchResult {
42
53
  pattern: string;
43
54
  handler: Handler | LazyHandler;
55
+ kind: RouteEntryKind;
44
56
  }
45
57
 
46
58
  /**
@@ -53,9 +65,24 @@ export interface HostRouteBuilder {
53
65
  use(...middleware: Middleware[]): HostRouteBuilder;
54
66
 
55
67
  /**
56
- * Map to a handler or lazy import
68
+ * Map to an inline request handler `(request, input) => Response`.
69
+ *
70
+ * For a lazily-imported sub-app or handler module, use {@link lazy} instead -
71
+ * `.map(() => import(...))` is rejected (the return type is not a `Response`)
72
+ * and would not be discovered at build time.
73
+ */
74
+ map(handler: Handler): HostRouter;
75
+
76
+ /**
77
+ * Mount a lazily-imported handler or host router:
78
+ * `.lazy(() => import("./sub-app"))`.
79
+ *
80
+ * The loader takes no arguments and resolves to a module whose `default`
81
+ * export is a request `Handler` or a nested `HostRouter`. Only `.lazy()`
82
+ * entries are invoked during build-time discovery to trigger the sub-app's
83
+ * `createRouter()` registration.
57
84
  */
58
- map(handler: Handler | LazyHandler): HostRouter;
85
+ lazy(handler: LazyHandler): HostRouter;
59
86
  }
60
87
 
61
88
  /**
@@ -134,6 +161,8 @@ export interface RouteEntry {
134
161
  patterns: string[];
135
162
  middleware: Middleware[];
136
163
  handler: Handler | LazyHandler;
164
+ /** Whether `handler` is an inline request handler or a lazy module mount. */
165
+ kind: RouteEntryKind;
137
166
  isFallback?: boolean;
138
167
  }
139
168
 
package/src/host/utils.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * app: ['*', 'www.*']
16
16
  * });
17
17
  *
18
- * router.host(hosts.admin).map(...); // Type-safe!
18
+ * router.host(hosts.admin).lazy(() => import("./apps/admin")); // Type-safe!
19
19
  * ```
20
20
  */
21
21
  export function defineHosts<T extends Record<string, string | string[]>>(
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { GetRegisteredRoutes } from "./types.js";
18
+ import type { JsonSerialize } from "./serialize.js";
18
19
  import type { ResponseEnvelope } from "./urls.js";
19
20
 
20
21
  /**
@@ -103,29 +104,75 @@ type NameForPattern<TPattern extends string, TRoutes = GetRegisteredRoutes> = {
103
104
  }[keyof TRoutes];
104
105
 
105
106
  /**
106
- * Look up the response data type for a route pattern from RegisteredRoutes.
107
- *
108
- * Works by reverse-looking up the route name for the given pattern,
109
- * then extracting the response type from the route entry.
107
+ * Strip a query (`?…`) and/or hash (`#…`) suffix before matching, so a concrete
108
+ * URL like `/api/health?ts=1` still resolves to its route's response. Removes
109
+ * from the earliest of `?`/`#`: a `#` before the first `?` (the query is part of
110
+ * a fragment, e.g. `/health#top?x=1`) is handled, as is a `/:` that only appears
111
+ * inside the query (e.g. `/health?next=/:id`).
112
+ */
113
+ type StripPathSuffix<T extends string> = T extends `${infer Base}?${string}`
114
+ ? Base extends `${infer Frag}#${string}`
115
+ ? Frag
116
+ : Base
117
+ : T extends `${infer Base}#${string}`
118
+ ? Base
119
+ : T;
120
+
121
+ /** Extract a route entry's response payload (or `never` for RSC routes). */
122
+ type ResponsePayloadOf<TRoutes, K extends keyof TRoutes> = TRoutes[K] extends {
123
+ readonly response: infer R;
124
+ }
125
+ ? Exclude<R, Response>
126
+ : never;
127
+
128
+ /**
129
+ * Look up the response payload for a route, keyed by either a route pattern
130
+ * (`/api/products/:id`) or a concrete path (`/api/products/123`). The same type
131
+ * serves a pattern lookup and a typed `fetch` wrapper that forwards a concrete
132
+ * `Rango.Path`:
110
133
  *
111
- * For static routes (no params), pattern === path:
112
- * PathResponse<"/api/health"> → { status: string; timestamp: number }
134
+ * PathResponse<"/api/products/:id"> Product // by pattern
135
+ * PathResponse<"/api/products/123"> → Product // by concrete path
113
136
  *
114
- * For dynamic routes, use the pattern:
115
- * PathResponse<"/api/products/:id"> Product
137
+ * The query/hash suffix is stripped first; the stripped key is then treated as a
138
+ * pattern when it contains a `/:param` segment and matched exactly (precise even
139
+ * for nested dynamic routes), otherwise as a concrete path matched against each
140
+ * route's `PatternToPath` template. Because those holes are `${string}`
141
+ * (slash-greedy), a concrete path under a *nested* dynamic route can match several
142
+ * patterns and union their responses — pattern lookups do not have this
143
+ * looseness. RSC routes (no response) and unmatched keys resolve to `never`.
144
+ */
145
+ type ResponsePayloadFor<
146
+ TPath extends string,
147
+ TRoutes = GetRegisteredRoutes,
148
+ > = ResponsePayloadForKey<StripPathSuffix<TPath>, TRoutes>;
149
+
150
+ type ResponsePayloadForKey<
151
+ TKey extends string,
152
+ TRoutes,
153
+ > = TKey extends `${string}/:${string}`
154
+ ? {
155
+ [K in keyof TRoutes]: RoutePattern<TRoutes, K> extends TKey
156
+ ? ResponsePayloadOf<TRoutes, K>
157
+ : never;
158
+ }[keyof TRoutes]
159
+ : {
160
+ [K in keyof TRoutes]: TKey extends PatternToPath<RoutePattern<TRoutes, K>>
161
+ ? ResponsePayloadOf<TRoutes, K>
162
+ : never;
163
+ }[keyof TRoutes];
164
+
165
+ /**
166
+ * Public response type for a route, keyed by pattern or concrete path. The
167
+ * payload is wrapped in `JsonSerialize` so it describes the JSON **wire** value a
168
+ * consumer receives from `fetch().then(r => r.json())`, not the handler's raw
169
+ * return type — e.g. a handler returning `{ createdAt: Date }` resolves here to
170
+ * `ResponseEnvelope<{ createdAt: string }>`.
116
171
  */
117
172
  export type PathResponse<
118
- TPattern extends string,
173
+ TPath extends string,
119
174
  TRoutes = GetRegisteredRoutes,
120
- > = ResponseEnvelope<
121
- {
122
- [K in keyof TRoutes]: RoutePattern<TRoutes, K> extends TPattern
123
- ? TRoutes[K] extends { readonly response: infer R }
124
- ? Exclude<R, Response>
125
- : never
126
- : never;
127
- }[keyof TRoutes]
128
- >;
175
+ > = ResponseEnvelope<JsonSerialize<ResponsePayloadFor<TPath, TRoutes>>>;
129
176
 
130
177
  /**
131
178
  * Strip trailing slash from a path (e.g., "/blog/" -> "/blog" | "/blog/")
@@ -140,7 +187,7 @@ type OptionalTrailingSlash<T extends string> = T extends `${infer Base}/`
140
187
  /**
141
188
  * Union of all valid paths from registered routes
142
189
  *
143
- * Generated from RSCRouter.RegisteredRoutes via module augmentation.
190
+ * Generated from Rango.RegisteredRoutes via module augmentation.
144
191
  * Allows optional query strings and hash fragments.
145
192
  */
146
193
  export type ValidPaths<TRoutes = GetRegisteredRoutes> =
@@ -154,6 +201,76 @@ export type ValidPaths<TRoutes = GetRegisteredRoutes> =
154
201
  }[keyof TRoutes]
155
202
  >;
156
203
 
204
+ // Module-scoped alias so the ambient `Rango.PathResponse` below can reference
205
+ // the module-level `PathResponse` without the global namespace shadowing the
206
+ // name when both are called `PathResponse`.
207
+ type GlobalPathResponse<
208
+ TPattern extends string,
209
+ TRoutes = GetRegisteredRoutes,
210
+ > = PathResponse<TPattern, TRoutes>;
211
+
212
+ /**
213
+ * Ambient path types on the `Rango` namespace.
214
+ *
215
+ * These live on the same global namespace consumers already augment for
216
+ * `Rango.Env` / `Rango.Vars`, so they are reachable with no import wherever the
217
+ * router's types are in scope. They are the public, recommended surface for
218
+ * typing anything that wraps `href()`. `ValidPaths` / `PathResponse` stay as the
219
+ * internal building blocks behind them.
220
+ */
221
+ declare global {
222
+ namespace Rango {
223
+ /**
224
+ * Union of every valid route path accepted by `href()`.
225
+ *
226
+ * Type a wrapper's path parameter as `Rango.Path` so it shares `href()`'s
227
+ * compile-time validation against the registered routes:
228
+ *
229
+ * ```ts
230
+ * import { href } from "@rangojs/router/client";
231
+ *
232
+ * export const appHref = (path: Rango.Path) => href(path);
233
+ * ```
234
+ *
235
+ * Resolves from `Rango.RegisteredRoutes` when augmented, otherwise the
236
+ * auto-generated `Rango.GeneratedRouteMap`, otherwise a permissive
237
+ * `/${string}` fallback.
238
+ */
239
+ type Path<TRoutes = GetRegisteredRoutes> = ValidPaths<TRoutes>;
240
+
241
+ /**
242
+ * Response payload for a route, looked up from the global route map by
243
+ * either a route pattern (`/api/products/:id`) or a concrete path
244
+ * (`/api/products/123`). Because it accepts a concrete `Rango.Path`, it
245
+ * doubles as the return type of a typed `fetch` wrapper:
246
+ *
247
+ * ```ts
248
+ * type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
249
+ * type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
250
+ *
251
+ * const get = async <T extends Rango.Path>(
252
+ * path: T,
253
+ * ): Promise<Rango.PathResponse<T>> =>
254
+ * fetch(href(path)).then((r) => r.json());
255
+ * ```
256
+ *
257
+ * The payload is the JSON **wire** shape (via `Rango.JsonSerialize`), not the
258
+ * handler's raw return — a handler returning `{ createdAt: Date }` resolves
259
+ * here to `ResponseEnvelope<{ createdAt: string }>`, matching what
260
+ * `fetch().then(r => r.json())` actually yields.
261
+ *
262
+ * Only resolves once `Rango.RegisteredRoutes` carries response metadata (the
263
+ * generated map has paths and search but no payloads). Pass an explicit route
264
+ * map as the second argument to look up against a non-global map (rarely
265
+ * needed in app code).
266
+ */
267
+ type PathResponse<
268
+ TPath extends string,
269
+ TRoutes = GetRegisteredRoutes,
270
+ > = GlobalPathResponse<TPath, TRoutes>;
271
+ }
272
+ }
273
+
157
274
  /**
158
275
  * Type-safe href function for client-side use
159
276
  *
@@ -186,7 +303,10 @@ export function href<T extends ValidPaths>(path: T, mount?: string): string {
186
303
  const normalizedMount = mount.endsWith("/") ? mount.slice(0, -1) : mount;
187
304
  return normalizedMount + path;
188
305
  }
189
- return path;
306
+ // ValidPaths is built from template literals so T does extend string at
307
+ // runtime, but the inference can fail past a certain route-union complexity
308
+ // and TypeScript reports T as not assignable to string.
309
+ return path as string;
190
310
  }
191
311
 
192
312
  /**
package/src/index.rsc.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  MiddlewareError,
19
19
  HandlerError,
20
20
  BuildError,
21
+ DslContextError,
21
22
  InvalidHandlerError,
22
23
  RouterError,
23
24
  Skip,
@@ -43,6 +44,7 @@ export type {
43
44
  // Revalidation types
44
45
  RevalidateParams,
45
46
  Revalidate,
47
+ ActionRef,
46
48
  RouteKeys,
47
49
  // Loader types
48
50
  LoaderDefinition,
@@ -67,7 +69,7 @@ export type {
67
69
 
68
70
  // Router options type (server-only, so import directly)
69
71
  export type {
70
- RSCRouterOptions,
72
+ RangoOptions,
71
73
  SSRStreamMode,
72
74
  SSROptions,
73
75
  ResolveStreamingContext,
@@ -151,7 +153,7 @@ export {
151
153
  // Core router (server-side)
152
154
  export {
153
155
  createRouter,
154
- type RSCRouter,
156
+ type Rango,
155
157
  type RootLayoutProps,
156
158
  type RouterRequestInput,
157
159
  } from "./router.js";
@@ -172,6 +174,9 @@ export type { PublicRequestContext as RequestContext } from "./server/request-co
172
174
  import type { PublicRequestContext } from "./server/request-context.js";
173
175
  import type { DefaultEnv } from "./types/global-namespace.js";
174
176
 
177
+ // Shared base for every user-facing request context (mirrors index.ts).
178
+ export type { RequestScope, ExecutionContext } from "./types/request-scope.js";
179
+
175
180
  export const getRequestContext: <
176
181
  TEnv = DefaultEnv,
177
182
  >() => PublicRequestContext<TEnv> = _getRequestContextInternal;
@@ -217,8 +222,8 @@ export {
217
222
  type LocationStateOptions,
218
223
  } from "./browser/react/location-state-shared.js";
219
224
 
220
- // Path-based response type lookup from RegisteredRoutes
221
- export type { PathResponse } from "./href-client.js";
225
+ // Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
226
+ // `Rango.PathResponse`, declared in href-client.ts) — no import needed.
222
227
 
223
228
  // Telemetry sink
224
229
  export { createConsoleSink } from "./router/telemetry.js";
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  MiddlewareError,
19
19
  HandlerError,
20
20
  BuildError,
21
+ DslContextError,
21
22
  InvalidHandlerError,
22
23
  RouterError,
23
24
  Skip,
@@ -43,6 +44,7 @@ export type {
43
44
  // Revalidation types
44
45
  RevalidateParams,
45
46
  Revalidate,
47
+ ActionRef,
46
48
  RouteKeys,
47
49
  // Loader types
48
50
  LoaderDefinition,
@@ -147,24 +149,52 @@ export { createVar, type ContextVar } from "./context-var.js";
147
149
  export { nonce } from "./rsc/nonce.js";
148
150
 
149
151
  /**
150
- * Error-throwing stub for server-only `Prerender` function.
152
+ * SSR/client stub for server-only `Prerender` function.
153
+ *
154
+ * Returns a lightweight stub object instead of throwing so that the
155
+ * production SSR build can safely bundle the RSC entry chunk — the SSR
156
+ * bundler resolves `@rangojs/router` to this (SSR) entry, so Prerender
157
+ * calls in RSC code must not crash at module-evaluation time.
151
158
  */
152
- export function Prerender(): never {
153
- throw serverOnlyStubError("Prerender");
159
+ export function Prerender(
160
+ _handler?: any,
161
+ _optionsOrId?: any,
162
+ __injectedId?: string,
163
+ ): any {
164
+ const id =
165
+ typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
166
+ return { __brand: "prerenderHandler" as const, $$id: id };
154
167
  }
155
168
 
156
169
  /**
157
- * Error-throwing stub for server-only `Passthrough` function.
170
+ * SSR/client stub for server-only `Passthrough` function.
158
171
  */
159
- export function Passthrough(): never {
160
- throw serverOnlyStubError("Passthrough");
172
+ export function Passthrough(
173
+ _handler?: any,
174
+ _optionsOrId?: any,
175
+ __injectedId?: string,
176
+ ): any {
177
+ const id =
178
+ typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
179
+ return { __brand: "passthroughHandler" as const, $$id: id };
161
180
  }
162
181
 
163
182
  /**
164
- * Error-throwing stub for server-only `Static` function.
183
+ * SSR/client stub for server-only `Static` function.
184
+ *
185
+ * Returns a lightweight stub object instead of throwing so that the
186
+ * production SSR build can safely bundle the RSC entry chunk — the SSR
187
+ * bundler resolves `@rangojs/router` to this (SSR) entry, so Static
188
+ * calls in RSC code must not crash at module-evaluation time.
165
189
  */
166
- export function Static(): never {
167
- throw serverOnlyStubError("Static");
190
+ export function Static(
191
+ _handler?: any,
192
+ _optionsOrId?: any,
193
+ __injectedId?: string,
194
+ ): any {
195
+ const id =
196
+ typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
197
+ return { __brand: "staticHandler" as const, $$id: id };
168
198
  }
169
199
 
170
200
  /**
@@ -236,6 +266,9 @@ export function transition(): never {
236
266
  // Request context type (safe for client)
237
267
  export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
238
268
 
269
+ // Shared base for every user-facing request context.
270
+ export type { RequestScope, ExecutionContext } from "./types/request-scope.js";
271
+
239
272
  // Cookie store types (safe for client)
240
273
  export type {
241
274
  CookieStore,
@@ -271,12 +304,17 @@ export {
271
304
  type LocationStateOptions,
272
305
  } from "./browser/react/location-state-shared.js";
273
306
 
274
- // Path-based response type lookup from RegisteredRoutes
275
- export type { PathResponse } from "./href-client.js";
276
-
277
- // Telemetry sink
278
- export { createConsoleSink } from "./router/telemetry.js";
279
- export { createOTelSink } from "./router/telemetry-otel.js";
307
+ // Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
308
+ // `Rango.PathResponse`, declared in href-client.ts) — no import needed.
309
+
310
+ // Telemetry types only — the createConsoleSink/createOTelSink values are
311
+ // server-only and live in index.rsc.ts (the `react-server` condition of the
312
+ // bare `@rangojs/router` import). Re-exporting them as values from this
313
+ // (default/client) entry would pull telemetry.ts and telemetry-otel.ts into
314
+ // the client module graph; both tree-shake to zero bytes but still appear in
315
+ // bundle analysis output and slow build-time module resolution. Consumers
316
+ // who need the values in non-RSC contexts can import from
317
+ // `@rangojs/router/server`.
280
318
  export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
281
319
  export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
282
320