@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
@@ -39,18 +39,29 @@ export interface BreadcrumbItem {
39
39
  /**
40
40
  * Collect function for Breadcrumbs handle.
41
41
  * Flattens segments in parent-to-child order with deduplication by href
42
- * (last item for each href wins).
42
+ * (last item for each href wins). Deferred slots (`ctx.use(Breadcrumbs).defer()`)
43
+ * arrive as pending Promise entries with no href yet; they are passed through by
44
+ * identity and excluded from the href dedup so concurrent deferred crumbs do not
45
+ * all collapse under a single `undefined` href.
43
46
  */
44
47
  function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
45
48
  const all = segments.flat();
46
- const seen = new Map<string, number>();
47
49
 
50
+ const isResolvedItem = (item: unknown): item is BreadcrumbItem =>
51
+ item != null &&
52
+ typeof item === "object" &&
53
+ typeof (item as { then?: unknown }).then !== "function" &&
54
+ typeof (item as { href?: unknown }).href === "string";
55
+
56
+ const seen = new Map<string, number>();
48
57
  for (let i = 0; i < all.length; i++) {
49
- seen.set(all[i].href, i);
58
+ if (isResolvedItem(all[i])) seen.set(all[i].href, i);
50
59
  }
51
60
 
52
- // Return items in order, keeping only the last occurrence per href
53
- return all.filter((item, index) => seen.get(item.href) === index);
61
+ // Deferred items bypass dedup (excluded via !isResolvedItem check).
62
+ return all.filter(
63
+ (item, index) => !isResolvedItem(item) || seen.get(item.href) === index,
64
+ );
54
65
  }
55
66
 
56
67
  /**
@@ -35,9 +35,6 @@ import type {
35
35
  UnsetDescriptor,
36
36
  } from "../router/types.js";
37
37
 
38
- /**
39
- * Type guard for unset descriptor
40
- */
41
38
  function isUnsetDescriptor(
42
39
  descriptor: MetaDescriptor,
43
40
  ): descriptor is UnsetDescriptor {
@@ -49,9 +46,6 @@ function isUnsetDescriptor(
49
46
  );
50
47
  }
51
48
 
52
- /**
53
- * Type guard for title descriptor (any form)
54
- */
55
49
  function isTitleDescriptor(
56
50
  descriptor: MetaDescriptor,
57
51
  ): descriptor is { title: TitleDescriptor } {
@@ -62,9 +56,6 @@ function isTitleDescriptor(
62
56
  );
63
57
  }
64
58
 
65
- /**
66
- * Type guard for title template descriptor
67
- */
68
59
  function isTitleTemplate(
69
60
  title: TitleDescriptor,
70
61
  ): title is { template: string; default: string } {
@@ -76,21 +67,13 @@ function isTitleTemplate(
76
67
  );
77
68
  }
78
69
 
79
- /**
80
- * Type guard for absolute title descriptor
81
- */
82
70
  function isAbsoluteTitle(
83
71
  title: TitleDescriptor,
84
72
  ): title is { absolute: string } {
85
73
  return typeof title === "object" && title !== null && "absolute" in title;
86
74
  }
87
75
 
88
- /**
89
- * Get a unique key for a meta descriptor for deduplication.
90
- * Returns undefined for descriptors that shouldn't be deduplicated.
91
- */
92
76
  function getMetaKey(descriptor: MetaDescriptor): string | undefined {
93
- // Skip unset descriptors - they are processed separately
94
77
  if (isUnsetDescriptor(descriptor)) {
95
78
  return undefined;
96
79
  }
@@ -110,13 +93,10 @@ function getMetaKey(descriptor: MetaDescriptor): string | undefined {
110
93
  return `httpEquiv:${descriptor.httpEquiv}`;
111
94
  }
112
95
  if ("script:ld+json" in descriptor) {
113
- // JSON-LD scripts can have multiple, don't dedupe by default
114
96
  return undefined;
115
97
  }
116
98
  if ("tagName" in descriptor) {
117
- // For link tags, dedupe by rel if present
118
99
  if (descriptor.tagName === "link" && "rel" in descriptor) {
119
- // Some link rels should be unique (canonical), others not (stylesheet)
120
100
  const uniqueRels = ["canonical", "icon", "apple-touch-icon"];
121
101
  if (uniqueRels.includes(descriptor.rel as string)) {
122
102
  return `link:${descriptor.rel}`;
@@ -136,9 +116,6 @@ const defaultMetaDescriptors: MetaDescriptor[] = [
136
116
  { name: "viewport", content: "width=device-width, initial-scale=1" },
137
117
  ];
138
118
 
139
- /**
140
- * Helper to add or replace a descriptor in the result array
141
- */
142
119
  function addOrReplace(
143
120
  result: MetaDescriptor[],
144
121
  keyToIndex: Map<string, number>,
@@ -155,9 +132,6 @@ function addOrReplace(
155
132
  }
156
133
  }
157
134
 
158
- /**
159
- * Helper to update indices after removing an element
160
- */
161
135
  function updateIndicesAfterRemoval(
162
136
  keyToIndex: Map<string, number>,
163
137
  removedIndex: number,
@@ -169,17 +143,11 @@ function updateIndicesAfterRemoval(
169
143
  }
170
144
  }
171
145
 
172
- /**
173
- * Collect function for Meta handle.
174
- * Includes default meta descriptors, then deduplicates by key with later routes overriding earlier ones.
175
- * Supports title templates, absolute titles, and unset descriptors.
176
- */
177
146
  function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
178
147
  const result: MetaDescriptor[] = [];
179
148
  const keyToIndex = new Map<string, number>();
180
149
  let titleTemplate: string | undefined;
181
150
 
182
- // Add defaults first so they can be overridden
183
151
  for (const descriptor of defaultMetaDescriptors) {
184
152
  const key = getMetaKey(descriptor);
185
153
  if (key !== undefined) {
@@ -190,7 +158,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
190
158
 
191
159
  for (const descriptors of segments) {
192
160
  for (const descriptor of descriptors) {
193
- // Handle unset descriptors
194
161
  if (isUnsetDescriptor(descriptor)) {
195
162
  const keyToRemove = descriptor.unset;
196
163
  if (keyToIndex.has(keyToRemove)) {
@@ -202,14 +169,11 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
202
169
  continue;
203
170
  }
204
171
 
205
- // Handle title descriptors with template/absolute support
206
172
  if (isTitleDescriptor(descriptor)) {
207
173
  const titleValue = descriptor.title;
208
174
 
209
175
  if (isTitleTemplate(titleValue)) {
210
- // Store template for subsequent title descriptors in child segments
211
176
  titleTemplate = titleValue.template;
212
- // Set the default title
213
177
  addOrReplace(
214
178
  result,
215
179
  keyToIndex,
@@ -220,7 +184,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
220
184
  }
221
185
 
222
186
  if (isAbsoluteTitle(titleValue)) {
223
- // Absolute title bypasses any template
224
187
  addOrReplace(
225
188
  result,
226
189
  keyToIndex,
@@ -230,7 +193,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
230
193
  continue;
231
194
  }
232
195
 
233
- // String title - apply template if one exists
234
196
  const finalTitle = titleTemplate
235
197
  ? titleTemplate.replace("%s", titleValue as string)
236
198
  : titleValue;
@@ -243,7 +205,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
243
205
  continue;
244
206
  }
245
207
 
246
- // Handle all other descriptors
247
208
  const key = getMetaKey(descriptor);
248
209
  addOrReplace(result, keyToIndex, descriptor, key);
249
210
  }
@@ -1,9 +1,3 @@
1
- /**
2
- * Cookie Override Handler
3
- *
4
- * Manages cookie-based host override for development environments.
5
- */
6
-
7
1
  import type { HostOverrideConfig } from "./types.js";
8
2
  import type { RouterRequestInput } from "../router/router-interfaces.js";
9
3
  import { matchPattern, parseRequest } from "./pattern-matcher.js";
@@ -13,9 +7,6 @@ import {
13
7
  HostValidationError,
14
8
  } from "./errors.js";
15
9
 
16
- /**
17
- * Parse cookies from request
18
- */
19
10
  export function parseCookies(request: Request): Record<string, string> {
20
11
  const cookieHeader = request.headers.get("cookie");
21
12
  if (!cookieHeader) {
@@ -40,24 +31,15 @@ export function parseCookies(request: Request): Record<string, string> {
40
31
  return cookies;
41
32
  }
42
33
 
43
- /**
44
- * Get cookie value from request
45
- */
46
34
  export function getCookie(request: Request, name: string): string | undefined {
47
35
  const cookies = parseCookies(request);
48
36
  return cookies[name];
49
37
  }
50
38
 
51
- /**
52
- * Create Set-Cookie header to delete a cookie
53
- */
54
39
  export function createDeleteCookieHeader(name: string): string {
55
40
  return `${name}=; Max-Age=0; Path=/; Secure; HttpOnly`;
56
41
  }
57
42
 
58
- /**
59
- * Create error response with cookie deletion
60
- */
61
43
  export function createCookieErrorResponse(
62
44
  cookieName: string,
63
45
  message: string,
@@ -77,9 +59,6 @@ export function createCookieErrorResponse(
77
59
  );
78
60
  }
79
61
 
80
- /**
81
- * Check if current host is allowed to use override
82
- */
83
62
  export function isHostAllowed(
84
63
  request: Request,
85
64
  allowedHosts: string[],
@@ -95,12 +74,6 @@ export function isHostAllowed(
95
74
  return false;
96
75
  }
97
76
 
98
- /**
99
- * Handle cookie override logic
100
- *
101
- * Returns overridden hostname if valid, original hostname if no override.
102
- * Throws errors for invalid overrides.
103
- */
104
77
  export function handleCookieOverride(
105
78
  request: Request,
106
79
  config: HostOverrideConfig | undefined,
@@ -115,46 +88,37 @@ export function handleCookieOverride(
115
88
  const cookieValue = getCookie(request, cookieName);
116
89
  const { hostname: originalHostname } = parseRequest(request);
117
90
 
118
- // No cookie - return original hostname
119
91
  if (!cookieValue) {
120
92
  return originalHostname;
121
93
  }
122
94
 
123
- // Check if current host is allowed
124
95
  const allowed = isHostAllowed(request, allowedHosts);
125
96
 
126
- // If not allowed, throw error
127
97
  if (!allowed) {
128
98
  throw new HostOverrideNotAllowedError(originalHostname, cookieName, {
129
99
  cause: { cookieValue, currentHost: originalHostname },
130
100
  });
131
101
  }
132
102
 
133
- // If allowed and has custom validation, run it
134
103
  if (validate) {
135
104
  try {
136
105
  const validatedHostname = validate(request, cookieValue, input);
137
106
  return validatedHostname;
138
107
  } catch (error) {
139
- // Wrap in HostValidationError
140
108
  const message = error instanceof Error ? error.message : String(error);
141
109
  throw new HostValidationError(message, error);
142
110
  }
143
111
  }
144
112
 
145
- // Default validation - verify it's a valid hostname using URL constructor
146
113
  try {
147
- // Try to construct a URL with the hostname to validate it
148
114
  const testUrl = new URL(`https://${cookieValue}`);
149
115
 
150
- // Ensure the hostname matches what we provided (URL constructor normalizes it)
151
116
  if (testUrl.hostname !== cookieValue) {
152
117
  throw new InvalidHostnameError(cookieValue, {
153
118
  cause: { original: cookieValue, normalized: testUrl.hostname },
154
119
  });
155
120
  }
156
121
  } catch (error) {
157
- // If URL constructor failed, throw InvalidHostnameError with cause
158
122
  if (error instanceof InvalidHostnameError) {
159
123
  throw error;
160
124
  }
@@ -4,16 +4,10 @@
4
4
  * All host router errors extend HostRouterError for easy instance checking.
5
5
  */
6
6
 
7
- /**
8
- * Error options with cause
9
- */
10
7
  interface ErrorOptions {
11
8
  cause?: unknown;
12
9
  }
13
10
 
14
- /**
15
- * Base error class for all host router errors
16
- */
17
11
  export class HostRouterError extends Error {
18
12
  cause?: unknown;
19
13
 
@@ -27,9 +21,6 @@ export class HostRouterError extends Error {
27
21
  }
28
22
  }
29
23
 
30
- /**
31
- * Error thrown when pattern validation fails
32
- */
33
24
  export class InvalidPatternError extends HostRouterError {
34
25
  constructor(pattern: string, reason: string, options?: ErrorOptions) {
35
26
  super(`Invalid pattern "${pattern}": ${reason}`, options);
@@ -38,9 +29,6 @@ export class InvalidPatternError extends HostRouterError {
38
29
  }
39
30
  }
40
31
 
41
- /**
42
- * Error thrown when cookie override is not allowed
43
- */
44
32
  export class HostOverrideNotAllowedError extends HostRouterError {
45
33
  constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
46
34
  super(
@@ -52,9 +40,6 @@ export class HostOverrideNotAllowedError extends HostRouterError {
52
40
  }
53
41
  }
54
42
 
55
- /**
56
- * Error thrown when cookie hostname is invalid
57
- */
58
43
  export class InvalidHostnameError extends HostRouterError {
59
44
  constructor(hostname: string, options?: ErrorOptions) {
60
45
  super(`Invalid hostname format: "${hostname}"`, options);
@@ -63,9 +48,6 @@ export class InvalidHostnameError extends HostRouterError {
63
48
  }
64
49
  }
65
50
 
66
- /**
67
- * Error thrown when custom validation fails
68
- */
69
51
  export class HostValidationError extends HostRouterError {
70
52
  constructor(message: string, cause?: unknown) {
71
53
  super(message, { cause });
@@ -74,9 +56,6 @@ export class HostValidationError extends HostRouterError {
74
56
  }
75
57
  }
76
58
 
77
- /**
78
- * Error thrown when no route matches
79
- */
80
59
  export class NoRouteMatchError extends HostRouterError {
81
60
  constructor(hostname: string, pathname: string, options?: ErrorOptions) {
82
61
  super(`No route matched for ${hostname}${pathname}`, options);
@@ -85,9 +64,6 @@ export class NoRouteMatchError extends HostRouterError {
85
64
  }
86
65
  }
87
66
 
88
- /**
89
- * Error thrown when handler type is invalid
90
- */
91
67
  export class InvalidHandlerError extends HostRouterError {
92
68
  constructor(handler: unknown, options?: ErrorOptions) {
93
69
  super(`Invalid handler type: ${typeof handler}`, options);
package/src/host/index.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * const router = createHostRouter();
13
13
  *
14
- * router.host(['.']).map(() => import('./apps/main'));
15
- * router.host(['admin.*']).map(() => import('./apps/admin'));
14
+ * router.host(['.']).lazy(() => import('./apps/main'));
15
+ * router.host(['admin.*']).lazy(() => import('./apps/admin'));
16
16
  *
17
17
  * export default {
18
18
  * fetch(request) {
@@ -20,6 +20,12 @@
20
20
  * }
21
21
  * };
22
22
  * ```
23
+ *
24
+ * The host surface (`Handler`, `Middleware`, `match`, `HostOverrideConfig.validate`)
25
+ * types `input` as `RouterRequestInput<any>` by design: a host router fans out to
26
+ * heterogeneous sub-apps with differing env/vars shapes, so there is no single
27
+ * `TEnv`/`TVars` to thread through. `input.env`/`input.vars` are therefore `any`
28
+ * here; the typed env shape lives on each sub-app's `createRouter<TEnv>()`.
23
29
  */
24
30
 
25
31
  // Core router
@@ -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,16 @@ 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
67
  const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
81
68
  const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
82
69
 
83
- // First match domain
84
70
  const domainMatch = matchDomainPattern(domainPattern, hostname, parts);
85
71
  if (!domainMatch) {
86
72
  return false;
87
73
  }
88
74
 
89
- // Then match path (prefix match)
90
75
  if (pathPattern) {
91
76
  return pathname === pathPattern || pathname.startsWith(pathPattern + "/");
92
77
  }
@@ -94,81 +79,62 @@ export function matchPattern(
94
79
  return true;
95
80
  }
96
81
 
97
- /**
98
- * Match domain pattern against hostname
99
- */
100
82
  function matchDomainPattern(
101
83
  pattern: string,
102
84
  hostname: string,
103
85
  parts: string[],
104
86
  ): boolean {
105
- // Exact match
106
87
  if (pattern === hostname) {
107
88
  return true;
108
89
  }
109
90
 
110
- // `.` or `*` - any apex domain
111
91
  if (pattern === "." || pattern === "*") {
112
92
  return isApexDomain(parts);
113
93
  }
114
94
 
115
- // `**` - any domain (apex + all subdomains)
116
95
  if (pattern === "**") {
117
96
  return true;
118
97
  }
119
98
 
120
- // `*.` - any single-level subdomain
121
99
  if (pattern === "*.") {
122
100
  return getSubdomainLevel(parts) === 1;
123
101
  }
124
102
 
125
- // `**.` - any multi-level subdomain (2+ levels)
126
103
  if (pattern === "**.") {
127
104
  return getSubdomainLevel(parts) >= 2;
128
105
  }
129
106
 
130
- // `*.tld` - any apex domain with specific TLD (e.g., *.com)
131
107
  if (pattern.startsWith("*.") && !pattern.includes(".", 2)) {
132
108
  const tld = pattern.slice(2);
133
109
  return isApexDomain(parts) && hostname.endsWith("." + tld);
134
110
  }
135
111
 
136
- // `*.example.com` - single subdomain of specific domain
137
112
  if (pattern.startsWith("*.")) {
138
113
  const baseDomain = pattern.slice(2);
139
114
  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
115
  const patternParts = baseDomain.split(".");
143
116
  return parts.length === patternParts.length + 1;
144
117
  }
145
118
  return false;
146
119
  }
147
120
 
148
- // `**.example.com` - any depth subdomain of specific domain
149
121
  if (pattern.startsWith("**.")) {
150
122
  const baseDomain = pattern.slice(3);
151
123
  if (hostname.endsWith("." + baseDomain)) {
152
124
  const patternParts = baseDomain.split(".");
153
- // Must have more parts than the base domain (i.e., has subdomains)
154
125
  return parts.length > patternParts.length;
155
126
  }
156
127
  return false;
157
128
  }
158
129
 
159
- // `subdomain.*` - specific subdomain of any apex domain
160
- // e.g., admin.* matches admin.example.com, admin.google.com
161
130
  if (pattern.endsWith(".*")) {
162
131
  const subdomain = pattern.slice(0, -2);
163
- // Must be single-level subdomain (3 parts total)
164
132
  if (parts.length === 3 && parts[0] === subdomain) {
165
133
  return true;
166
134
  }
167
135
  return false;
168
136
  }
169
137
 
170
- // `subdomain.**` - specific subdomain of any domain (including multi-level)
171
- // e.g., admin.** matches admin.example.com, admin.sub.example.com
172
138
  if (pattern.endsWith(".**")) {
173
139
  const subdomain = pattern.slice(0, -3);
174
140
  if (parts.length >= 3 && parts[0] === subdomain) {
@@ -177,11 +143,8 @@ function matchDomainPattern(
177
143
  return false;
178
144
  }
179
145
 
180
- // `subdomain.` - specific subdomain of any apex domain (no wildcard)
181
- // e.g., admin. matches admin.example.com, admin.google.com
182
146
  if (pattern.endsWith(".") && !pattern.includes("*")) {
183
147
  const subdomain = pattern.slice(0, -1);
184
- // Must be exactly 3 parts (subdomain.domain.tld)
185
148
  if (parts.length === 3 && parts[0] === subdomain) {
186
149
  return true;
187
150
  }
@@ -191,9 +154,6 @@ function matchDomainPattern(
191
154
  return false;
192
155
  }
193
156
 
194
- /**
195
- * Validate pattern format
196
- */
197
157
  export function validatePattern(pattern: string): void {
198
158
  if (!pattern || typeof pattern !== "string") {
199
159
  throw new InvalidPatternError(
@@ -203,12 +163,9 @@ export function validatePattern(pattern: string): void {
203
163
  );
204
164
  }
205
165
 
206
- // Check for invalid characters (spaces, etc.)
207
166
  if (/\s/.test(pattern)) {
208
167
  throw new InvalidPatternError(pattern, "contains whitespace", {
209
168
  cause: { pattern },
210
169
  });
211
170
  }
212
-
213
- // Additional validation can be added here
214
171
  }