@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -32,29 +32,37 @@ import {
32
32
  InvalidHandlerError,
33
33
  } from "./errors.js";
34
34
 
35
- /**
36
- * Registry entry for a host router instance.
37
- * Stores references to the live routes array and fallback, so the discovery
38
- * plugin can iterate handlers registered after createHostRouter() returns.
39
- */
40
35
  export interface HostRouterRegistryEntry {
41
36
  routes: RouteEntry[];
42
37
  fallback: RouteEntry | null;
43
38
  }
44
39
 
45
- /**
46
- * Global registry for host routers (parallel to RouterRegistry for RSC routers).
47
- * Populated by createHostRouter() so the build-time discovery plugin can find
48
- * host routers and resolve their lazy handlers to trigger sub-app createRouter() calls.
49
- */
50
40
  export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> =
51
41
  new Map();
52
42
 
53
43
  let hostRouterAutoId = 0;
54
44
 
55
- /**
56
- * Create a host router
57
- */
45
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
46
+ return (
47
+ value !== null &&
48
+ (typeof value === "object" || typeof value === "function") &&
49
+ typeof (value as { then?: unknown }).then === "function"
50
+ );
51
+ }
52
+
53
+ function looksLikeLazyModule(value: unknown): boolean {
54
+ if (value === null || typeof value !== "object" || !("default" in value)) {
55
+ return false;
56
+ }
57
+ const defaultExport = (value as { default: unknown }).default;
58
+ return (
59
+ typeof defaultExport === "function" ||
60
+ (typeof defaultExport === "object" &&
61
+ defaultExport !== null &&
62
+ "match" in defaultExport)
63
+ );
64
+ }
65
+
58
66
  export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
59
67
  const routes: RouteEntry[] = [];
60
68
  const globalMiddleware: Middleware[] = [];
@@ -68,48 +76,54 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
68
76
  }
69
77
  }
70
78
 
71
- /**
72
- * Create a route builder for chaining
73
- */
74
79
  function createRouteBuilder(
75
80
  patterns: string[],
76
81
  isFallback = false,
77
82
  ): HostRouteBuilder {
78
83
  const middleware: Middleware[] = [];
79
84
 
85
+ function register(
86
+ handler: Handler | LazyHandler,
87
+ kind: RouteEntry["kind"],
88
+ ): HostRouter {
89
+ const entry: RouteEntry = {
90
+ patterns,
91
+ middleware,
92
+ handler,
93
+ kind,
94
+ isFallback,
95
+ };
96
+
97
+ if (isFallback) {
98
+ fallbackRoute = entry;
99
+ } else {
100
+ routes.push(entry);
101
+ }
102
+
103
+ log(
104
+ `Registered ${isFallback ? "fallback" : "route"} (${kind}):`,
105
+ patterns.join(", "),
106
+ );
107
+
108
+ return router;
109
+ }
110
+
80
111
  return {
81
112
  use(...mw: Middleware[]): HostRouteBuilder {
82
113
  middleware.push(...mw);
83
114
  return this;
84
115
  },
85
116
 
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
- );
117
+ map(handler: Handler): HostRouter {
118
+ return register(handler, "handler");
119
+ },
104
120
 
105
- return router;
121
+ lazy(handler: LazyHandler): HostRouter {
122
+ return register(handler, "lazy");
106
123
  },
107
124
  };
108
125
  }
109
126
 
110
- /**
111
- * Find matching route for hostname and path
112
- */
113
127
  function findMatchingRoute(
114
128
  hostname: string,
115
129
  pathname: string,
@@ -128,9 +142,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
128
142
  return null;
129
143
  }
130
144
 
131
- /**
132
- * Execute middleware chain
133
- */
134
145
  async function executeMiddleware(
135
146
  middleware: Middleware[],
136
147
  request: Request,
@@ -149,8 +160,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
149
160
  return finalHandler();
150
161
  }
151
162
 
152
- // Guard against double next() calls — a second call would
153
- // re-enter the downstream chain and run handlers/side-effects twice.
154
163
  let nextCalled = false;
155
164
  const guardedNext = (): Promise<Response> => {
156
165
  if (nextCalled) {
@@ -168,62 +177,73 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
168
177
  return next();
169
178
  }
170
179
 
171
- /**
172
- * Execute handler (lazy or direct)
173
- */
174
180
  async function executeHandler(
175
- handler: Handler | LazyHandler,
181
+ entry: RouteEntry,
176
182
  request: Request,
177
183
  input: RouterRequestInput<any>,
178
184
  ): 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
- }
185
+ const { handler, kind } = entry;
202
186
 
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>;
187
+ if (typeof handler !== "function") {
188
+ throw new InvalidHandlerError(handler, {
189
+ cause: { handlerType: typeof handler },
190
+ });
191
+ }
192
+
193
+ if (kind === "lazy") {
194
+ return executeLazyMount(handler as LazyHandler, request, input);
195
+ }
196
+
197
+ const result = (handler as Handler)(request, input);
198
+
199
+ if (isThenable(result)) {
200
+ const awaited = await result;
201
+ if (looksLikeLazyModule(awaited)) {
202
+ throw new HostRouterError(
203
+ ".map() is for inline request handlers; use .lazy(() => import(...)) for lazy host mounts.",
204
+ );
205
+ }
206
+ return awaited as Response;
207
+ }
208
+
209
+ return result;
210
+ }
211
+
212
+ async function executeLazyMount(
213
+ loader: LazyHandler,
214
+ request: Request,
215
+ input: RouterRequestInput<any>,
216
+ ): Promise<Response> {
217
+ const module = await loader();
218
+
219
+ if (typeof module === "object" && module !== null && "default" in module) {
220
+ const defaultExport = (module as { default: Handler | HostRouter })
221
+ .default;
222
+
223
+ if (
224
+ typeof defaultExport === "object" &&
225
+ defaultExport !== null &&
226
+ "match" in defaultExport
227
+ ) {
228
+ return (defaultExport as HostRouter).match(request, input);
208
229
  }
209
230
 
210
- // Direct handler
211
- return result as Response | Promise<Response>;
231
+ return (defaultExport as Handler)(request, input);
212
232
  }
213
233
 
214
- throw new InvalidHandlerError(handler, {
215
- cause: { handlerType: typeof handler },
234
+ throw new InvalidHandlerError(loader, {
235
+ cause: {
236
+ reason:
237
+ "lazy mount did not resolve to a module with a default export; " +
238
+ "use .lazy(() => import('./sub-app')) where the module default-exports a handler or host router",
239
+ },
216
240
  });
217
241
  }
218
242
 
219
- /**
220
- * Router instance
221
- */
222
243
  const router: HostRouter = {
223
244
  host(patterns: HostPattern): HostRouteBuilder {
224
245
  const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
225
246
 
226
- // Validate and normalize patterns
227
247
  const normalized = patternsArray.map((p) => {
228
248
  validatePattern(p);
229
249
  return normalizePattern(p);
@@ -242,9 +262,8 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
242
262
  return createRouteBuilder([], true);
243
263
  },
244
264
 
245
- test(hostname: string): HostMatchResult | null {
265
+ test(hostname: string, pathname = "/"): HostMatchResult | null {
246
266
  const parts = hostname.split(".");
247
- const pathname = "/";
248
267
 
249
268
  for (const route of routes) {
250
269
  for (const pattern of route.patterns) {
@@ -252,6 +271,7 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
252
271
  return {
253
272
  pattern,
254
273
  handler: route.handler,
274
+ kind: route.kind,
255
275
  };
256
276
  }
257
277
  }
@@ -269,14 +289,11 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
269
289
  let effectiveHostname: string;
270
290
 
271
291
  try {
272
- // Handle cookie override (may throw HostRouterError)
273
292
  effectiveHostname = handleCookieOverride(request, hostOverride, input);
274
293
  } catch (error) {
275
- // If it's a HostRouterError from cookie override
276
294
  if (error instanceof HostRouterError) {
277
295
  log(`Cookie override error: ${error.message}`);
278
296
 
279
- // If fallback exists, use it
280
297
  if (fallbackRoute) {
281
298
  const fallbackInput = { ...input, error };
282
299
  const allMiddleware = [
@@ -288,12 +305,10 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
288
305
  allMiddleware,
289
306
  request,
290
307
  fallbackInput,
291
- () =>
292
- executeHandler(fallbackRoute!.handler, request, fallbackInput),
308
+ () => executeHandler(fallbackRoute!, request, fallbackInput),
293
309
  );
294
310
  }
295
311
 
296
- // Otherwise return error response with cookie deletion
297
312
  if (hostOverride) {
298
313
  return createCookieErrorResponse(
299
314
  hostOverride.cookieName,
@@ -302,7 +317,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
302
317
  }
303
318
  }
304
319
 
305
- // Re-throw non-HostRouterErrors
306
320
  throw error;
307
321
  }
308
322
 
@@ -312,7 +326,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
312
326
  log(`Cookie override: ${effectiveHostname}`);
313
327
  }
314
328
 
315
- // Find matching route
316
329
  const matchedRoute = findMatchingRoute(effectiveHostname, pathname);
317
330
 
318
331
  if (!matchedRoute) {
@@ -325,19 +338,14 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
325
338
  });
326
339
  }
327
340
 
328
- // Combine global and route-specific middleware
329
341
  const allMiddleware = [...globalMiddleware, ...matchedRoute.middleware];
330
342
 
331
- // Execute middleware chain and handler
332
343
  return executeMiddleware(allMiddleware, request, input, () =>
333
- executeHandler(matchedRoute.handler, request, input),
344
+ executeHandler(matchedRoute, request, input),
334
345
  );
335
346
  },
336
347
  };
337
348
 
338
- // Register in the global HostRouterRegistry for build-time discovery.
339
- // The routes array and fallbackRoute ref are live - they reflect routes
340
- // added via .host().map() after this point.
341
349
  const registryId = `host-router-${hostRouterAutoId++}`;
342
350
  HostRouterRegistry.set(registryId, {
343
351
  get routes() {
@@ -4,7 +4,7 @@
4
4
  * Helper functions for testing host routing.
5
5
  */
6
6
 
7
- import { matchPattern } from "./pattern-matcher.js";
7
+ import { matchPattern, parseRequest } from "./pattern-matcher.js";
8
8
 
9
9
  export interface CreateTestRequestOptions {
10
10
  host: string;
@@ -14,18 +14,6 @@ export interface CreateTestRequestOptions {
14
14
  headers?: Record<string, string>;
15
15
  }
16
16
 
17
- /**
18
- * Create a test request with specific host and cookies
19
- *
20
- * @example
21
- * ```ts
22
- * const request = createTestRequest({
23
- * host: 'admin.example.com',
24
- * path: '/dashboard',
25
- * cookies: { 'x-requested-host': 'api.example.com' }
26
- * });
27
- * ```
28
- */
29
17
  export function createTestRequest(options: CreateTestRequestOptions): Request {
30
18
  const {
31
19
  host,
@@ -38,7 +26,6 @@ export function createTestRequest(options: CreateTestRequestOptions): Request {
38
26
  const url = `http://${host}${path}`;
39
27
  const requestHeaders = new Headers(headers);
40
28
 
41
- // Add cookies if provided
42
29
  if (Object.keys(cookies).length > 0) {
43
30
  const cookieString = Object.entries(cookies)
44
31
  .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
@@ -52,28 +39,54 @@ export function createTestRequest(options: CreateTestRequestOptions): Request {
52
39
  });
53
40
  }
54
41
 
42
+ function matchPatterns(
43
+ pattern: string | string[],
44
+ hostname: string,
45
+ pathname: string,
46
+ parts: string[],
47
+ ): boolean {
48
+ const patterns = Array.isArray(pattern) ? pattern : [pattern];
49
+ return patterns.some((p) => matchPattern(p, hostname, pathname, parts));
50
+ }
51
+
55
52
  /**
56
- * Test if a pattern matches a hostname
53
+ * Test if a pattern matches a hostname (and, for path-based patterns, a pathname).
54
+ *
55
+ * `pathname` defaults to `"/"`, so a host-only pattern works with two args. Pass
56
+ * the third arg to test a path-based pattern (`**.workers.dev/admin`,
57
+ * `localhost/shop`) — without it those patterns can never match.
57
58
  *
58
59
  * @example
59
60
  * ```ts
60
- * expect(testPattern('admin.*', 'admin.example.com')).toBe(true);
61
- * expect(testPattern(['*', 'www.*'], 'example.com')).toBe(true);
61
+ * expect(testPattern("admin.*", "admin.example.com")).toBe(true);
62
+ * expect(testPattern(["*", "www.*"], "example.com")).toBe(true);
63
+ * expect(testPattern("**.workers.dev/admin", "foo.workers.dev", "/admin")).toBe(true);
62
64
  * ```
63
65
  */
64
66
  export function testPattern(
65
67
  pattern: string | string[],
66
68
  hostname: string,
69
+ pathname: string = "/",
67
70
  ): boolean {
68
- const patterns = Array.isArray(pattern) ? pattern : [pattern];
69
- const parts = hostname.split(".");
70
- const pathname = "/";
71
-
72
- for (const p of patterns) {
73
- if (matchPattern(p, hostname, pathname, parts)) {
74
- return true;
75
- }
76
- }
71
+ return matchPatterns(pattern, hostname, pathname, hostname.split("."));
72
+ }
77
73
 
78
- return false;
74
+ /**
75
+ * Test if a pattern matches a `Request` — the hostname AND pathname are taken
76
+ * from the request URL (via the same `parseRequest` the host router uses), so a
77
+ * path-based pattern is tested against a real request without splitting the URL
78
+ * by hand.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const req = new Request("https://foo.workers.dev/admin");
83
+ * expect(matchesHost("**.workers.dev/admin", req)).toBe(true);
84
+ * ```
85
+ */
86
+ export function matchesHost(
87
+ pattern: string | string[],
88
+ request: Request,
89
+ ): boolean {
90
+ const { hostname, pathname, parts } = parseRequest(request);
91
+ return matchPatterns(pattern, hostname, pathname, parts);
79
92
  }
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
  /**
@@ -83,9 +110,13 @@ export interface HostRouter {
83
110
  fallback(): HostRouteBuilder;
84
111
 
85
112
  /**
86
- * Test which handler would match a hostname
113
+ * Test which handler would match a hostname (and optional pathname).
114
+ *
115
+ * `pathname` defaults to `"/"`. Pass it to probe path-prefixed patterns
116
+ * such as `host(["example.com/admin"])`, which only match when the request
117
+ * path is under the prefix.
87
118
  */
88
- test(hostname: string): HostMatchResult | null;
119
+ test(hostname: string, pathname?: string): HostMatchResult | null;
89
120
  }
90
121
 
91
122
  /**
@@ -134,6 +165,8 @@ export interface RouteEntry {
134
165
  patterns: string[];
135
166
  middleware: Middleware[];
136
167
  handler: Handler | LazyHandler;
168
+ /** Whether `handler` is an inline request handler or a lazy module mount. */
169
+ kind: RouteEntryKind;
137
170
  isFallback?: boolean;
138
171
  }
139
172
 
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[]>>(