@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,218 @@
1
+ ---
2
+ name: host-router
3
+ description: Multi-app host routing with domain/subdomain patterns
4
+ argument-hint:
5
+ ---
6
+
7
+ # Host Router
8
+
9
+ Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
10
+
11
+ ## Import
12
+
13
+ ```typescript
14
+ import { createHostRouter, defineHosts } from "@rangojs/router/host";
15
+ ```
16
+
17
+ ## Basic Setup
18
+
19
+ ```typescript
20
+ // host-router.ts
21
+ import { createHostRouter } from "@rangojs/router/host";
22
+
23
+ const router = createHostRouter();
24
+
25
+ router.host(["."]).map(() => import("./apps/main"));
26
+ router.host(["admin.*"]).map(() => import("./apps/admin"));
27
+ router.host(["api.*"]).map(() => import("./apps/api"));
28
+
29
+ export default {
30
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
31
+ return router.match(request, { env, ctx });
32
+ },
33
+ };
34
+ ```
35
+
36
+ Each `.map()` receives either a direct handler `(request, input) => Response` or a lazy import `() => import(...)`. Lazy imports resolve a module with a `default` export that is either a handler function or another `HostRouter` (for nesting).
37
+
38
+ ## Pattern Syntax
39
+
40
+ | Pattern | Matches |
41
+ | ----------------- | ---------------------------------------------- |
42
+ | `.` or `*` | Any apex domain (`example.com`) |
43
+ | `**` | Any domain (apex + all subdomains) |
44
+ | `*.` | Any single-level subdomain (`www.example.com`) |
45
+ | `**. ` | Any multi-level subdomain (`a.b.example.com`) |
46
+ | `example.com` | Exact domain |
47
+ | `*.com` | Any apex `.com` domain |
48
+ | `*.example.com` | Single subdomain of `example.com` |
49
+ | `**.example.com` | Any depth subdomain of `example.com` |
50
+ | `admin.*` | `admin` subdomain of any apex domain |
51
+ | `admin.**` | `admin` subdomain of any domain |
52
+ | `admin.` | `admin` subdomain of any apex (no wildcard) |
53
+ | `example.com/api` | Domain + path prefix (prefix match) |
54
+
55
+ Patterns are tested in registration order. First match wins.
56
+
57
+ ## `defineHosts` for Type Safety
58
+
59
+ ```typescript
60
+ import { defineHosts } from "@rangojs/router/host";
61
+
62
+ const hosts = defineHosts({
63
+ admin: "admin.*",
64
+ api: "api.*",
65
+ app: [".", "www.*"],
66
+ });
67
+
68
+ router.host(hosts.admin).map(() => import("./apps/admin"));
69
+ router.host(hosts.app).map(() => import("./apps/main"));
70
+ ```
71
+
72
+ Returns a frozen object — keys are autocompleted by TypeScript.
73
+
74
+ ## Middleware
75
+
76
+ Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
77
+
78
+ ```typescript
79
+ const router = createHostRouter();
80
+
81
+ // Global — runs for all routes
82
+ router.use(async (request, input, next) => {
83
+ console.log(`[${new Date().toISOString()}] ${request.url}`);
84
+ return next();
85
+ });
86
+
87
+ // Per-route
88
+ router
89
+ .host(["admin.*"])
90
+ .use(requireAuth)
91
+ .map(() => import("./apps/admin"));
92
+ ```
93
+
94
+ Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
95
+
96
+ Calling `next()` more than once throws.
97
+
98
+ ## Fallback Handler
99
+
100
+ Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
101
+
102
+ ```typescript
103
+ const router = createHostRouter({
104
+ hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
105
+ });
106
+
107
+ // Called when cookie override fails (not for general unmatched hosts)
108
+ router.fallback().map((request) => {
109
+ return new Response("Invalid host override", { status: 400 });
110
+ });
111
+ ```
112
+
113
+ For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch:
114
+
115
+ ```typescript
116
+ import { NoRouteMatchError } from "@rangojs/router/host";
117
+
118
+ export default {
119
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
120
+ try {
121
+ return await router.match(request, { env, ctx });
122
+ } catch (err) {
123
+ if (err instanceof NoRouteMatchError) {
124
+ return new Response("Not Found", { status: 404 });
125
+ }
126
+ throw err;
127
+ }
128
+ },
129
+ };
130
+ ```
131
+
132
+ ## Cookie-Based Host Override
133
+
134
+ For development: route requests to a different app based on a cookie value, allowing developers to test different host routes from a single domain.
135
+
136
+ ```typescript
137
+ const router = createHostRouter({
138
+ hostOverride: {
139
+ cookieName: "x-dev-host",
140
+ allowedHosts: ["localhost", "**.dev.example.com"],
141
+ validate: (request, cookieValue, input) => {
142
+ // Optional custom validation — return the effective hostname
143
+ return cookieValue;
144
+ },
145
+ },
146
+ });
147
+ ```
148
+
149
+ When a request arrives:
150
+
151
+ 1. If no cookie → use actual hostname
152
+ 2. If cookie present and host is in `allowedHosts` → use cookie value as hostname
153
+ 3. If cookie present but host not allowed → throw `HostOverrideNotAllowedError`
154
+
155
+ Without a custom `validate`, the cookie value is validated as a hostname via `new URL()`.
156
+
157
+ ## Debug Mode
158
+
159
+ ```typescript
160
+ const router = createHostRouter({ debug: true });
161
+ ```
162
+
163
+ Logs pattern matching, route registration, and cookie override decisions to console.
164
+
165
+ ## Testing
166
+
167
+ ```typescript
168
+ import { createTestRequest, testPattern } from "@rangojs/router/host/testing";
169
+
170
+ // Test pattern matching
171
+ testPattern("admin.*", "admin.example.com"); // true
172
+ testPattern([".", "www.*"], "example.com"); // true
173
+
174
+ // Create requests for integration tests
175
+ const request = createTestRequest({
176
+ host: "admin.example.com",
177
+ path: "/dashboard",
178
+ cookies: { "x-dev-host": "api.example.com" },
179
+ });
180
+
181
+ // Test which route would match (without executing)
182
+ router.test("admin.example.com"); // { pattern, handler } | null
183
+ ```
184
+
185
+ ## Error Types
186
+
187
+ All errors extend `HostRouterError`:
188
+
189
+ | Error | When |
190
+ | ----------------------------- | ------------------------------------------- |
191
+ | `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
192
+ | `HostOverrideNotAllowedError` | Cookie override from disallowed host |
193
+ | `InvalidHostnameError` | Cookie value isn't a valid hostname |
194
+ | `HostValidationError` | Custom `validate` function threw |
195
+ | `NoRouteMatchError` | No host pattern matched the request |
196
+ | `InvalidHandlerError` | Handler is not a function |
197
+
198
+ See the fallback section above for a `NoRouteMatchError` catch example.
199
+
200
+ ## Nesting Host Routers
201
+
202
+ A lazy handler can resolve to another `HostRouter`:
203
+
204
+ ```typescript
205
+ // apps/regional.ts
206
+ import { createHostRouter } from "@rangojs/router/host";
207
+
208
+ const regional = createHostRouter();
209
+ regional.host(["us.*"]).map(() => import("./regions/us"));
210
+ regional.host(["eu.*"]).map(() => import("./regions/eu"));
211
+
212
+ export default regional;
213
+ ```
214
+
215
+ ```typescript
216
+ // host-router.ts
217
+ router.host(["**.regional.example.com"]).map(() => import("./apps/regional"));
218
+ ```
@@ -0,0 +1,276 @@
1
+ ---
2
+ name: i18n
3
+ description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration
4
+ argument-hint: "[topic]"
5
+ ---
6
+
7
+ # Internationalization (i18n) and Locale Routing
8
+
9
+ Rango doesn't ship an i18n module. The router gives you the URL primitives
10
+ (optional include prefixes, constraints, typed reverse) and you compose
11
+ them with whatever message library you use — `react-intl`, `lingui`,
12
+ `@formatjs/intl`, or hand-rolled.
13
+
14
+ This skill covers:
15
+
16
+ - Mounting routes under an optional locale prefix (`/`, `/en`, `/gb`)
17
+ - Constraining the prefix to a known locale set
18
+ - Resolving the active locale (URL → cookie → `Accept-Language` → default)
19
+ - Generating localized URLs via `reverse()` round-trip
20
+ - Wiring `react-intl` into an RSC route tree
21
+
22
+ ## URL Shape: Optional Locale Prefix
23
+
24
+ Mount your localized routes under an optional include prefix so the
25
+ default locale lives at the bare URL and other locales get a prefix:
26
+
27
+ ```typescript
28
+ // urls.tsx
29
+ import { urls } from "@rangojs/router";
30
+ import { menuRoutes } from "./menu";
31
+
32
+ export const urlpatterns = urls(({ include }) => [
33
+ include("/:locale?", menuRoutes, { name: "menu" }),
34
+ ]);
35
+ ```
36
+
37
+ URLs that match:
38
+
39
+ | URL | Matched route | `ctx.params.locale` |
40
+ | -------------- | --------------- | ------------------- |
41
+ | `/` | `menu.index` | `undefined` |
42
+ | `/en` | `menu.index` | `"en"` |
43
+ | `/c/breads` | `menu.category` | `undefined` |
44
+ | `/en/c/breads` | `menu.category` | `"en"` |
45
+
46
+ > **Constrain to known locales** when you want unknown locales to fall
47
+ > through to other routes (or 404) instead of being treated as a slug:
48
+ >
49
+ > ```typescript
50
+ > include("/:locale(en|gb|fr)?", menuRoutes, { name: "menu" });
51
+ > ```
52
+ >
53
+ > `/de` now 404s (constraint rejects `de`), and `/c/breads` continues to
54
+ > match `menu.category` with `locale: undefined`. Without the constraint,
55
+ > `/de` would match `menu.index` with `locale: "de"`.
56
+
57
+ ## Reading the Locale in Handlers
58
+
59
+ Absent optionals are `undefined` (not `""`), so `??` coalesces correctly:
60
+
61
+ ```typescript
62
+ import { Handler } from "@rangojs/router";
63
+
64
+ export const MenuIndex: Handler<"menu.index"> = (ctx) => {
65
+ // ctx.params.locale is `string | undefined`
66
+ const locale = resolveLocale(ctx);
67
+ return <Welcome locale={locale} />;
68
+ };
69
+ ```
70
+
71
+ The `resolveLocale` helper below implements a typical fallback chain.
72
+
73
+ ## Locale Resolution
74
+
75
+ URL is the strongest signal but you usually want a fallback chain:
76
+
77
+ 1. **URL prefix** — if the user navigates to `/gb/...`, honor it
78
+ 2. **Cookie** — sticky preference set by a previous language switcher
79
+ 3. **`Accept-Language`** — browser hint
80
+ 4. **Default** — your app default
81
+
82
+ Put it in a small helper that every locale-aware handler calls:
83
+
84
+ ```typescript
85
+ // lib/locale.ts
86
+ import { cookies, headers } from "@rangojs/router";
87
+
88
+ export const SUPPORTED_LOCALES = ["en", "gb", "fr"] as const;
89
+ export type Locale = (typeof SUPPORTED_LOCALES)[number];
90
+ const DEFAULT_LOCALE: Locale = "en";
91
+
92
+ const isSupported = (v: string): v is Locale =>
93
+ (SUPPORTED_LOCALES as readonly string[]).includes(v);
94
+
95
+ export function resolveLocale(ctx: {
96
+ params: Record<string, string | undefined>;
97
+ }): Locale {
98
+ const fromUrl = ctx.params.locale;
99
+ if (fromUrl && isSupported(fromUrl)) return fromUrl;
100
+
101
+ const fromCookie = cookies().get("locale")?.value;
102
+ if (fromCookie && isSupported(fromCookie)) return fromCookie;
103
+
104
+ const accept = headers().get("accept-language") ?? "";
105
+ for (const tag of accept.split(",")) {
106
+ const code = tag.split(";")[0].trim().split("-")[0];
107
+ if (isSupported(code)) return code as Locale;
108
+ }
109
+ return DEFAULT_LOCALE;
110
+ }
111
+ ```
112
+
113
+ If you want to redirect to the canonical URL when the resolved locale
114
+ doesn't match the URL (e.g., user has `gb` cookie but visits `/`), do
115
+ that in a global middleware so it covers actions too:
116
+
117
+ ```typescript
118
+ import { redirect } from "@rangojs/router";
119
+
120
+ router.use("/*", async (ctx, next) => {
121
+ const fromUrl = ctx.params.locale;
122
+ const resolved = resolveLocale(ctx);
123
+ if (resolved !== DEFAULT_LOCALE && !fromUrl) {
124
+ return redirect(`/${resolved}${ctx.url.pathname}`);
125
+ }
126
+ await next();
127
+ });
128
+ ```
129
+
130
+ ## Generating Localized URLs
131
+
132
+ `reverse()` treats `undefined` and `""` for an optional param as "absent"
133
+ and collapses the segment cleanly. The round-trip is symmetric with the
134
+ matcher:
135
+
136
+ ```typescript
137
+ ctx.reverse("menu.index", { locale: "" }); // → "/"
138
+ ctx.reverse("menu.index", { locale: undefined }); // → "/"
139
+ ctx.reverse("menu.index", { locale: "en" }); // → "/en"
140
+ ctx.reverse("menu.category", { locale: "en", slug: "breads" }); // → "/en/c/breads"
141
+ ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads"
142
+ ```
143
+
144
+ If the active locale is the app default and your URL strategy hides it
145
+ (`"en"` → `/`, others → `/<locale>`), normalize before calling reverse:
146
+
147
+ ```typescript
148
+ const normalized = locale === DEFAULT_LOCALE ? undefined : locale;
149
+ const href = ctx.reverse("menu.category", { locale: normalized, slug });
150
+ ```
151
+
152
+ ## react-intl Integration
153
+
154
+ `react-intl` needs a `<IntlProvider>` wrapping the tree, with `locale`
155
+ and `messages` props. The cleanest split: load messages on the server
156
+ (handler or layout), pass them through to a client provider component.
157
+
158
+ ### Messages loader
159
+
160
+ Load message bundles per locale. Keep them server-side so they stream
161
+ through the RSC payload and don't bloat the client bundle:
162
+
163
+ ```typescript
164
+ // lib/messages.ts
165
+ import type { Locale } from "./locale";
166
+
167
+ const loaders: Record<Locale, () => Promise<Record<string, string>>> = {
168
+ en: () => import("../messages/en.json").then((m) => m.default),
169
+ gb: () => import("../messages/gb.json").then((m) => m.default),
170
+ fr: () => import("../messages/fr.json").then((m) => m.default),
171
+ };
172
+
173
+ export async function loadMessages(locale: Locale) {
174
+ return loaders[locale]();
175
+ }
176
+ ```
177
+
178
+ ### Server layout: hand off to the client provider
179
+
180
+ ```tsx
181
+ // layouts/intl-layout.tsx (server component)
182
+ import type { ReactNode } from "react";
183
+ import { resolveLocale } from "../lib/locale";
184
+ import { loadMessages } from "../lib/messages";
185
+ import { IntlClientProvider } from "../components/intl-client-provider";
186
+
187
+ export async function IntlLayout({
188
+ ctx,
189
+ children,
190
+ }: {
191
+ ctx: any;
192
+ children: ReactNode;
193
+ }) {
194
+ const locale = resolveLocale(ctx);
195
+ const messages = await loadMessages(locale);
196
+ return (
197
+ <IntlClientProvider locale={locale} messages={messages}>
198
+ {children}
199
+ </IntlClientProvider>
200
+ );
201
+ }
202
+ ```
203
+
204
+ ### Client provider
205
+
206
+ ```tsx
207
+ // components/intl-client-provider.tsx
208
+ "use client";
209
+
210
+ import { IntlProvider } from "react-intl";
211
+ import type { ReactNode } from "react";
212
+
213
+ export function IntlClientProvider({
214
+ locale,
215
+ messages,
216
+ children,
217
+ }: {
218
+ locale: string;
219
+ messages: Record<string, string>;
220
+ children: ReactNode;
221
+ }) {
222
+ return (
223
+ <IntlProvider
224
+ locale={locale}
225
+ defaultLocale="en"
226
+ messages={messages}
227
+ onError={(err) => {
228
+ if (err.code === "MISSING_TRANSLATION") return; // common, log only
229
+ console.error(err);
230
+ }}
231
+ >
232
+ {children}
233
+ </IntlProvider>
234
+ );
235
+ }
236
+ ```
237
+
238
+ ### Mounting
239
+
240
+ Wrap your localized routes with the layout:
241
+
242
+ ```typescript
243
+ import { urls } from "@rangojs/router";
244
+ import { IntlLayout } from "./layouts/intl-layout";
245
+ import { menuRoutes } from "./menu";
246
+
247
+ export const urlpatterns = urls(({ layout, include }) => [
248
+ layout(IntlLayout, () => [
249
+ include("/:locale?", menuRoutes, { name: "menu" }),
250
+ ]),
251
+ ]);
252
+ ```
253
+
254
+ `<FormattedMessage>`, `useIntl()`, etc. work in any client component
255
+ under the layout. Server components can use `formatjs`'s `createIntl()`
256
+ directly with the same `messages` map for static text.
257
+
258
+ ## Common Pitfalls
259
+
260
+ | Pitfall | Fix |
261
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
262
+ | `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
263
+ | `ctx.params.locale ?? "en"` returns `""` | Pre-fix behavior. After the include-prefix fix this works correctly. |
264
+ | Bare `/` 404s when mounted via `include("/:locale?", routes)` | Requires the all-optional pattern fix in `compilePattern` (shipped). |
265
+ | Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
266
+ | Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
267
+ | Locale switcher loses search params | Read `ctx.url.search` and pass to `reverse(..., undefined, parsedSearch)`. |
268
+ | Action middleware can't read `ctx.params.locale` | Route middleware doesn't wrap action execution. Use global `router.use()` for actions. |
269
+
270
+ ## Cross-references
271
+
272
+ - `/route` — optional URL param syntax and runtime contract
273
+ - `/typesafety` — `RouteParams<"name">` typing for optionals
274
+ - `/middleware` — global vs route middleware scope (matters for actions)
275
+ - `/server-actions` — actions and the global-vs-route middleware boundary
276
+ - `/links` — `ctx.reverse()` and locale-aware URL generation