@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -12,15 +12,18 @@
12
12
  * - `**.example.com` - any depth subdomain
13
13
  * - `admin.*` - admin subdomain of any apex
14
14
  * - `example.com/admin` - specific domain with path prefix
15
+ *
16
+ * Apex vs subdomain is classified purely by dot-part COUNT (apex == exactly 2
17
+ * parts) — there is no Public Suffix List. A registrable domain under a
18
+ * multi-label public suffix (example.co.uk, shop.com.au) has 3+ parts and is
19
+ * therefore treated as a SUBDOMAIN, not an apex: `.`/`*` will NOT match it and
20
+ * `*.` WILL. If registrable-domain accuracy matters for a host-router consumer,
21
+ * supply an explicit apex/host hint rather than relying on the part count.
15
22
  */
16
23
 
17
24
  import { InvalidPatternError } from "./errors.js";
18
25
 
19
- /**
20
- * Normalize a pattern by removing trailing slashes from paths
21
- */
22
26
  export function normalizePattern(pattern: string): string {
23
- // If pattern has a path component, remove trailing slash
24
27
  const slashIndex = pattern.indexOf("/");
25
28
  if (slashIndex !== -1) {
26
29
  const domain = pattern.slice(0, slashIndex);
@@ -30,9 +33,6 @@ export function normalizePattern(pattern: string): string {
30
33
  return pattern;
31
34
  }
32
35
 
33
- /**
34
- * Parse hostname and path from request URL
35
- */
36
36
  export function parseRequest(request: Request): {
37
37
  hostname: string;
38
38
  pathname: string;
@@ -46,26 +46,14 @@ export function parseRequest(request: Request): {
46
46
  return { hostname, pathname, parts };
47
47
  }
48
48
 
49
- /**
50
- * Count subdomain levels (0 for apex, 1+ for subdomains)
51
- */
52
49
  function getSubdomainLevel(parts: string[]): number {
53
- // Apex domain has 2 parts (example.com)
54
- // Single subdomain has 3 parts (www.example.com)
55
- // Multi-level has 4+ parts (a.b.example.com)
56
50
  return Math.max(0, parts.length - 2);
57
51
  }
58
52
 
59
- /**
60
- * Check if hostname is an apex domain (no subdomains)
61
- */
62
53
  function isApexDomain(parts: string[]): boolean {
63
54
  return parts.length === 2;
64
55
  }
65
56
 
66
- /**
67
- * Match a single pattern against hostname and path
68
- */
69
57
  export function matchPattern(
70
58
  pattern: string,
71
59
  hostname: string,
@@ -74,19 +62,30 @@ export function matchPattern(
74
62
  ): boolean {
75
63
  const normalized = normalizePattern(pattern);
76
64
 
77
- // Check if pattern has path component
78
65
  const slashIndex = normalized.indexOf("/");
79
66
  const hasPath = slashIndex !== -1;
80
- const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
67
+ // Hosts are case-insensitive (RFC 3986): lowercase the domain literal and the
68
+ // request host once so matching folds case. Wildcards (*, **, .) are
69
+ // unaffected by lowercasing. The path is left untouched (paths are
70
+ // case-sensitive).
71
+ const domainPattern = (
72
+ hasPath ? normalized.slice(0, slashIndex) : normalized
73
+ ).toLowerCase();
81
74
  const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
82
75
 
83
- // First match domain
84
- const domainMatch = matchDomainPattern(domainPattern, hostname, parts);
76
+ const lowerHostname = hostname.toLowerCase();
77
+ const lowerParts =
78
+ lowerHostname === hostname ? parts : lowerHostname.split(".");
79
+
80
+ const domainMatch = matchDomainPattern(
81
+ domainPattern,
82
+ lowerHostname,
83
+ lowerParts,
84
+ );
85
85
  if (!domainMatch) {
86
86
  return false;
87
87
  }
88
88
 
89
- // Then match path (prefix match)
90
89
  if (pathPattern) {
91
90
  return pathname === pathPattern || pathname.startsWith(pathPattern + "/");
92
91
  }
@@ -94,81 +93,62 @@ export function matchPattern(
94
93
  return true;
95
94
  }
96
95
 
97
- /**
98
- * Match domain pattern against hostname
99
- */
100
96
  function matchDomainPattern(
101
97
  pattern: string,
102
98
  hostname: string,
103
99
  parts: string[],
104
100
  ): boolean {
105
- // Exact match
106
101
  if (pattern === hostname) {
107
102
  return true;
108
103
  }
109
104
 
110
- // `.` or `*` - any apex domain
111
105
  if (pattern === "." || pattern === "*") {
112
106
  return isApexDomain(parts);
113
107
  }
114
108
 
115
- // `**` - any domain (apex + all subdomains)
116
109
  if (pattern === "**") {
117
110
  return true;
118
111
  }
119
112
 
120
- // `*.` - any single-level subdomain
121
113
  if (pattern === "*.") {
122
114
  return getSubdomainLevel(parts) === 1;
123
115
  }
124
116
 
125
- // `**.` - any multi-level subdomain (2+ levels)
126
117
  if (pattern === "**.") {
127
118
  return getSubdomainLevel(parts) >= 2;
128
119
  }
129
120
 
130
- // `*.tld` - any apex domain with specific TLD (e.g., *.com)
131
121
  if (pattern.startsWith("*.") && !pattern.includes(".", 2)) {
132
122
  const tld = pattern.slice(2);
133
123
  return isApexDomain(parts) && hostname.endsWith("." + tld);
134
124
  }
135
125
 
136
- // `*.example.com` - single subdomain of specific domain
137
126
  if (pattern.startsWith("*.")) {
138
127
  const baseDomain = pattern.slice(2);
139
128
  if (hostname.endsWith("." + baseDomain)) {
140
- // Count parts: if pattern is *.example.com (3 parts),
141
- // hostname should have exactly 4 parts (www.example.com)
142
129
  const patternParts = baseDomain.split(".");
143
130
  return parts.length === patternParts.length + 1;
144
131
  }
145
132
  return false;
146
133
  }
147
134
 
148
- // `**.example.com` - any depth subdomain of specific domain
149
135
  if (pattern.startsWith("**.")) {
150
136
  const baseDomain = pattern.slice(3);
151
137
  if (hostname.endsWith("." + baseDomain)) {
152
138
  const patternParts = baseDomain.split(".");
153
- // Must have more parts than the base domain (i.e., has subdomains)
154
139
  return parts.length > patternParts.length;
155
140
  }
156
141
  return false;
157
142
  }
158
143
 
159
- // `subdomain.*` - specific subdomain of any apex domain
160
- // e.g., admin.* matches admin.example.com, admin.google.com
161
144
  if (pattern.endsWith(".*")) {
162
145
  const subdomain = pattern.slice(0, -2);
163
- // Must be single-level subdomain (3 parts total)
164
146
  if (parts.length === 3 && parts[0] === subdomain) {
165
147
  return true;
166
148
  }
167
149
  return false;
168
150
  }
169
151
 
170
- // `subdomain.**` - specific subdomain of any domain (including multi-level)
171
- // e.g., admin.** matches admin.example.com, admin.sub.example.com
172
152
  if (pattern.endsWith(".**")) {
173
153
  const subdomain = pattern.slice(0, -3);
174
154
  if (parts.length >= 3 && parts[0] === subdomain) {
@@ -177,11 +157,8 @@ function matchDomainPattern(
177
157
  return false;
178
158
  }
179
159
 
180
- // `subdomain.` - specific subdomain of any apex domain (no wildcard)
181
- // e.g., admin. matches admin.example.com, admin.google.com
182
160
  if (pattern.endsWith(".") && !pattern.includes("*")) {
183
161
  const subdomain = pattern.slice(0, -1);
184
- // Must be exactly 3 parts (subdomain.domain.tld)
185
162
  if (parts.length === 3 && parts[0] === subdomain) {
186
163
  return true;
187
164
  }
@@ -191,9 +168,6 @@ function matchDomainPattern(
191
168
  return false;
192
169
  }
193
170
 
194
- /**
195
- * Validate pattern format
196
- */
197
171
  export function validatePattern(pattern: string): void {
198
172
  if (!pattern || typeof pattern !== "string") {
199
173
  throw new InvalidPatternError(
@@ -203,12 +177,9 @@ export function validatePattern(pattern: string): void {
203
177
  );
204
178
  }
205
179
 
206
- // Check for invalid characters (spaces, etc.)
207
180
  if (/\s/.test(pattern)) {
208
181
  throw new InvalidPatternError(pattern, "contains whitespace", {
209
182
  cause: { pattern },
210
183
  });
211
184
  }
212
-
213
- // Additional validation can be added here
214
185
  }
@@ -32,27 +32,16 @@ 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
- /** Whether a value is thenable (a Promise or Promise-like). */
56
45
  function isThenable(value: unknown): value is PromiseLike<unknown> {
57
46
  return (
58
47
  value !== null &&
@@ -61,12 +50,6 @@ function isThenable(value: unknown): value is PromiseLike<unknown> {
61
50
  );
62
51
  }
63
52
 
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
53
  function looksLikeLazyModule(value: unknown): boolean {
71
54
  if (value === null || typeof value !== "object" || !("default" in value)) {
72
55
  return false;
@@ -80,9 +63,6 @@ function looksLikeLazyModule(value: unknown): boolean {
80
63
  );
81
64
  }
82
65
 
83
- /**
84
- * Create a host router
85
- */
86
66
  export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
87
67
  const routes: RouteEntry[] = [];
88
68
  const globalMiddleware: Middleware[] = [];
@@ -96,9 +76,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
96
76
  }
97
77
  }
98
78
 
99
- /**
100
- * Create a route builder for chaining
101
- */
102
79
  function createRouteBuilder(
103
80
  patterns: string[],
104
81
  isFallback = false,
@@ -147,9 +124,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
147
124
  };
148
125
  }
149
126
 
150
- /**
151
- * Find matching route for hostname and path
152
- */
153
127
  function findMatchingRoute(
154
128
  hostname: string,
155
129
  pathname: string,
@@ -168,9 +142,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
168
142
  return null;
169
143
  }
170
144
 
171
- /**
172
- * Execute middleware chain
173
- */
174
145
  async function executeMiddleware(
175
146
  middleware: Middleware[],
176
147
  request: Request,
@@ -189,8 +160,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
189
160
  return finalHandler();
190
161
  }
191
162
 
192
- // Guard against double next() calls — a second call would
193
- // re-enter the downstream chain and run handlers/side-effects twice.
194
163
  let nextCalled = false;
195
164
  const guardedNext = (): Promise<Response> => {
196
165
  if (nextCalled) {
@@ -208,15 +177,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
208
177
  return next();
209
178
  }
210
179
 
211
- /**
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.
219
- */
220
180
  async function executeHandler(
221
181
  entry: RouteEntry,
222
182
  request: Request,
@@ -236,8 +196,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
236
196
 
237
197
  const result = (handler as Handler)(request, input);
238
198
 
239
- // Inline handlers may be async; await to obtain the Response and to run the
240
- // misuse guard below.
241
199
  if (isThenable(result)) {
242
200
  const awaited = await result;
243
201
  if (looksLikeLazyModule(awaited)) {
@@ -251,10 +209,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
251
209
  return result;
252
210
  }
253
211
 
254
- /**
255
- * Resolve a `.lazy()` mount: invoke the zero-arg loader, then dispatch to the
256
- * module's default export.
257
- */
258
212
  async function executeLazyMount(
259
213
  loader: LazyHandler,
260
214
  request: Request,
@@ -266,7 +220,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
266
220
  const defaultExport = (module as { default: Handler | HostRouter })
267
221
  .default;
268
222
 
269
- // Default export is a nested host router
270
223
  if (
271
224
  typeof defaultExport === "object" &&
272
225
  defaultExport !== null &&
@@ -275,7 +228,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
275
228
  return (defaultExport as HostRouter).match(request, input);
276
229
  }
277
230
 
278
- // Otherwise treat the default export as a request handler
279
231
  return (defaultExport as Handler)(request, input);
280
232
  }
281
233
 
@@ -288,14 +240,10 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
288
240
  });
289
241
  }
290
242
 
291
- /**
292
- * Router instance
293
- */
294
243
  const router: HostRouter = {
295
244
  host(patterns: HostPattern): HostRouteBuilder {
296
245
  const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
297
246
 
298
- // Validate and normalize patterns
299
247
  const normalized = patternsArray.map((p) => {
300
248
  validatePattern(p);
301
249
  return normalizePattern(p);
@@ -314,9 +262,8 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
314
262
  return createRouteBuilder([], true);
315
263
  },
316
264
 
317
- test(hostname: string): HostMatchResult | null {
265
+ test(hostname: string, pathname = "/"): HostMatchResult | null {
318
266
  const parts = hostname.split(".");
319
- const pathname = "/";
320
267
 
321
268
  for (const route of routes) {
322
269
  for (const pattern of route.patterns) {
@@ -342,14 +289,11 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
342
289
  let effectiveHostname: string;
343
290
 
344
291
  try {
345
- // Handle cookie override (may throw HostRouterError)
346
292
  effectiveHostname = handleCookieOverride(request, hostOverride, input);
347
293
  } catch (error) {
348
- // If it's a HostRouterError from cookie override
349
294
  if (error instanceof HostRouterError) {
350
295
  log(`Cookie override error: ${error.message}`);
351
296
 
352
- // If fallback exists, use it
353
297
  if (fallbackRoute) {
354
298
  const fallbackInput = { ...input, error };
355
299
  const allMiddleware = [
@@ -365,7 +309,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
365
309
  );
366
310
  }
367
311
 
368
- // Otherwise return error response with cookie deletion
369
312
  if (hostOverride) {
370
313
  return createCookieErrorResponse(
371
314
  hostOverride.cookieName,
@@ -374,7 +317,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
374
317
  }
375
318
  }
376
319
 
377
- // Re-throw non-HostRouterErrors
378
320
  throw error;
379
321
  }
380
322
 
@@ -384,7 +326,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
384
326
  log(`Cookie override: ${effectiveHostname}`);
385
327
  }
386
328
 
387
- // Find matching route
388
329
  const matchedRoute = findMatchingRoute(effectiveHostname, pathname);
389
330
 
390
331
  if (!matchedRoute) {
@@ -397,19 +338,14 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
397
338
  });
398
339
  }
399
340
 
400
- // Combine global and route-specific middleware
401
341
  const allMiddleware = [...globalMiddleware, ...matchedRoute.middleware];
402
342
 
403
- // Execute middleware chain and handler
404
343
  return executeMiddleware(allMiddleware, request, input, () =>
405
344
  executeHandler(matchedRoute, request, input),
406
345
  );
407
346
  },
408
347
  };
409
348
 
410
- // Register in the global HostRouterRegistry for build-time discovery.
411
- // The routes array and fallbackRoute ref are live - they reflect routes
412
- // added via .host().map()/.lazy() after this point.
413
349
  const registryId = `host-router-${hostRouterAutoId++}`;
414
350
  HostRouterRegistry.set(registryId, {
415
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
@@ -110,9 +110,13 @@ export interface HostRouter {
110
110
  fallback(): HostRouteBuilder;
111
111
 
112
112
  /**
113
- * 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.
114
118
  */
115
- test(hostname: string): HostMatchResult | null;
119
+ test(hostname: string, pathname?: string): HostMatchResult | null;
116
120
  }
117
121
 
118
122
  /**
@@ -16,7 +16,6 @@
16
16
 
17
17
  import type { GetRegisteredRoutes } from "./types.js";
18
18
  import type { JsonSerialize } from "./serialize.js";
19
- import type { ResponseEnvelope } from "./urls.js";
20
19
 
21
20
  /**
22
21
  * Parse constraint values into a union type for paths
@@ -163,16 +162,16 @@ type ResponsePayloadForKey<
163
162
  }[keyof TRoutes];
164
163
 
165
164
  /**
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 }>`.
165
+ * Public response type for a route, keyed by pattern or concrete path. JSON
166
+ * response routes send the handler's return value verbatim (bare), so the
167
+ * payload is wrapped only in `JsonSerialize` to describe the JSON **wire** value
168
+ * a consumer receives from `fetch().then(r => r.json())` — e.g. a handler
169
+ * returning `{ createdAt: Date }` resolves here to `{ createdAt: string }`.
171
170
  */
172
171
  export type PathResponse<
173
172
  TPath extends string,
174
173
  TRoutes = GetRegisteredRoutes,
175
- > = ResponseEnvelope<JsonSerialize<ResponsePayloadFor<TPath, TRoutes>>>;
174
+ > = JsonSerialize<ResponsePayloadFor<TPath, TRoutes>>;
176
175
 
177
176
  /**
178
177
  * Strip trailing slash from a path (e.g., "/blog/" -> "/blog" | "/blog/")
@@ -256,7 +255,7 @@ declare global {
256
255
  *
257
256
  * The payload is the JSON **wire** shape (via `Rango.JsonSerialize`), not the
258
257
  * handler's raw return — a handler returning `{ createdAt: Date }` resolves
259
- * here to `ResponseEnvelope<{ createdAt: string }>`, matching what
258
+ * here to `{ createdAt: string }` (bare, no envelope), matching what
260
259
  * `fetch().then(r => r.json())` actually yields.
261
260
  *
262
261
  * Only resolves once `Rango.RegisteredRoutes` carries response metadata (the
@@ -299,13 +298,9 @@ declare global {
299
298
  */
300
299
  export function href<T extends ValidPaths>(path: T, mount?: string): string {
301
300
  if (mount && mount !== "/") {
302
- // Strip trailing slash from mount to avoid double-slash when joining
303
301
  const normalizedMount = mount.endsWith("/") ? mount.slice(0, -1) : mount;
304
302
  return normalizedMount + path;
305
303
  }
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
304
  return path as string;
310
305
  }
311
306