@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Custom Error Classes for Host Router
3
+ *
4
+ * All host router errors extend HostRouterError for easy instance checking.
5
+ */
6
+
7
+ /**
8
+ * Error options with cause
9
+ */
10
+ interface ErrorOptions {
11
+ cause?: unknown;
12
+ }
13
+
14
+ /**
15
+ * Base error class for all host router errors
16
+ */
17
+ export class HostRouterError extends Error {
18
+ cause?: unknown;
19
+
20
+ constructor(message: string, options?: ErrorOptions) {
21
+ super(message);
22
+ if (options?.cause) {
23
+ this.cause = options.cause;
24
+ }
25
+ this.name = "HostRouterError";
26
+ Object.setPrototypeOf(this, HostRouterError.prototype);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Error thrown when pattern validation fails
32
+ */
33
+ export class InvalidPatternError extends HostRouterError {
34
+ constructor(pattern: string, reason: string, options?: ErrorOptions) {
35
+ super(`Invalid pattern "${pattern}": ${reason}`, options);
36
+ this.name = "InvalidPatternError";
37
+ Object.setPrototypeOf(this, InvalidPatternError.prototype);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Error thrown when cookie override is not allowed
43
+ */
44
+ export class HostOverrideNotAllowedError extends HostRouterError {
45
+ constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
46
+ super(
47
+ `Host override not allowed on "${currentHost}" (cookie: ${cookieName})`,
48
+ options,
49
+ );
50
+ this.name = "HostOverrideNotAllowedError";
51
+ Object.setPrototypeOf(this, HostOverrideNotAllowedError.prototype);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Error thrown when cookie hostname is invalid
57
+ */
58
+ export class InvalidHostnameError extends HostRouterError {
59
+ constructor(hostname: string, options?: ErrorOptions) {
60
+ super(`Invalid hostname format: "${hostname}"`, options);
61
+ this.name = "InvalidHostnameError";
62
+ Object.setPrototypeOf(this, InvalidHostnameError.prototype);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Error thrown when custom validation fails
68
+ */
69
+ export class HostValidationError extends HostRouterError {
70
+ constructor(message: string, cause?: unknown) {
71
+ super(message, { cause });
72
+ this.name = "HostValidationError";
73
+ Object.setPrototypeOf(this, HostValidationError.prototype);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Error thrown when no route matches
79
+ */
80
+ export class NoRouteMatchError extends HostRouterError {
81
+ constructor(hostname: string, pathname: string, options?: ErrorOptions) {
82
+ super(`No route matched for ${hostname}${pathname}`, options);
83
+ this.name = "NoRouteMatchError";
84
+ Object.setPrototypeOf(this, NoRouteMatchError.prototype);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Error thrown when handler type is invalid
90
+ */
91
+ export class InvalidHandlerError extends HostRouterError {
92
+ constructor(handler: unknown, options?: ErrorOptions) {
93
+ super(`Invalid handler type: ${typeof handler}`, options);
94
+ this.name = "InvalidHandlerError";
95
+ Object.setPrototypeOf(this, InvalidHandlerError.prototype);
96
+ }
97
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Host Router
3
+ *
4
+ * A routing system for managing multi-application hosting based on
5
+ * domain/subdomain patterns with support for cookie-based host override
6
+ * for development environments.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createHostRouter } from '@rangojs/router/host';
11
+ *
12
+ * const router = createHostRouter();
13
+ *
14
+ * router.host(['.']).map(() => import('./apps/main'));
15
+ * router.host(['admin.*']).map(() => import('./apps/admin'));
16
+ *
17
+ * export default {
18
+ * fetch(request) {
19
+ * return router.match(request);
20
+ * }
21
+ * };
22
+ * ```
23
+ */
24
+
25
+ // Core router
26
+ export { createHostRouter } from "./router.js";
27
+
28
+ // Utilities
29
+ export { defineHosts } from "./utils.js";
30
+
31
+ // Errors
32
+ export {
33
+ HostRouterError,
34
+ InvalidPatternError,
35
+ HostOverrideNotAllowedError,
36
+ InvalidHostnameError,
37
+ HostValidationError,
38
+ NoRouteMatchError,
39
+ InvalidHandlerError,
40
+ } from "./errors.js";
41
+
42
+ // Types
43
+ export type {
44
+ HostRouter,
45
+ HostRouteBuilder,
46
+ HostRouterOptions,
47
+ Handler,
48
+ LazyHandler,
49
+ Middleware,
50
+ HostPattern,
51
+ HostMatchResult,
52
+ HostOverrideConfig,
53
+ } from "./types.js";
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Pattern Matching Engine
3
+ *
4
+ * Handles matching of hostnames and paths against various patterns:
5
+ * - `.` or `*` - any apex domain
6
+ * - `**` - any domain (apex + subdomains)
7
+ * - `*.` - any single-level subdomain
8
+ * - `**.` - any multi-level subdomain
9
+ * - `example.com` - exact domain
10
+ * - `*.com` - any apex .com domain
11
+ * - `*.example.com` - subdomain of example.com
12
+ * - `**.example.com` - any depth subdomain
13
+ * - `admin.*` - admin subdomain of any apex
14
+ * - `example.com/admin` - specific domain with path prefix
15
+ */
16
+
17
+ import { InvalidPatternError } from "./errors.js";
18
+
19
+ /**
20
+ * Normalize a pattern by removing trailing slashes from paths
21
+ */
22
+ export function normalizePattern(pattern: string): string {
23
+ // If pattern has a path component, remove trailing slash
24
+ const slashIndex = pattern.indexOf("/");
25
+ if (slashIndex !== -1) {
26
+ const domain = pattern.slice(0, slashIndex);
27
+ const path = pattern.slice(slashIndex).replace(/\/$/, "");
28
+ return domain + path;
29
+ }
30
+ return pattern;
31
+ }
32
+
33
+ /**
34
+ * Parse hostname and path from request URL
35
+ */
36
+ export function parseRequest(request: Request): {
37
+ hostname: string;
38
+ pathname: string;
39
+ parts: string[];
40
+ } {
41
+ const url = new URL(request.url);
42
+ const hostname = url.hostname;
43
+ const pathname = url.pathname;
44
+ const parts = hostname.split(".");
45
+
46
+ return { hostname, pathname, parts };
47
+ }
48
+
49
+ /**
50
+ * Count subdomain levels (0 for apex, 1+ for subdomains)
51
+ */
52
+ 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
+ return Math.max(0, parts.length - 2);
57
+ }
58
+
59
+ /**
60
+ * Check if hostname is an apex domain (no subdomains)
61
+ */
62
+ function isApexDomain(parts: string[]): boolean {
63
+ return parts.length === 2;
64
+ }
65
+
66
+ /**
67
+ * Match a single pattern against hostname and path
68
+ */
69
+ export function matchPattern(
70
+ pattern: string,
71
+ hostname: string,
72
+ pathname: string,
73
+ parts: string[],
74
+ ): boolean {
75
+ const normalized = normalizePattern(pattern);
76
+
77
+ // Check if pattern has path component
78
+ const slashIndex = normalized.indexOf("/");
79
+ const hasPath = slashIndex !== -1;
80
+ const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
81
+ const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
82
+
83
+ // First match domain
84
+ const domainMatch = matchDomainPattern(domainPattern, hostname, parts);
85
+ if (!domainMatch) {
86
+ return false;
87
+ }
88
+
89
+ // Then match path (prefix match)
90
+ if (pathPattern) {
91
+ return pathname === pathPattern || pathname.startsWith(pathPattern + "/");
92
+ }
93
+
94
+ return true;
95
+ }
96
+
97
+ /**
98
+ * Match domain pattern against hostname
99
+ */
100
+ function matchDomainPattern(
101
+ pattern: string,
102
+ hostname: string,
103
+ parts: string[],
104
+ ): boolean {
105
+ // Exact match
106
+ if (pattern === hostname) {
107
+ return true;
108
+ }
109
+
110
+ // `.` or `*` - any apex domain
111
+ if (pattern === "." || pattern === "*") {
112
+ return isApexDomain(parts);
113
+ }
114
+
115
+ // `**` - any domain (apex + all subdomains)
116
+ if (pattern === "**") {
117
+ return true;
118
+ }
119
+
120
+ // `*.` - any single-level subdomain
121
+ if (pattern === "*.") {
122
+ return getSubdomainLevel(parts) === 1;
123
+ }
124
+
125
+ // `**.` - any multi-level subdomain (2+ levels)
126
+ if (pattern === "**.") {
127
+ return getSubdomainLevel(parts) >= 2;
128
+ }
129
+
130
+ // `*.tld` - any apex domain with specific TLD (e.g., *.com)
131
+ if (pattern.startsWith("*.") && !pattern.includes(".", 2)) {
132
+ const tld = pattern.slice(2);
133
+ return isApexDomain(parts) && hostname.endsWith("." + tld);
134
+ }
135
+
136
+ // `*.example.com` - single subdomain of specific domain
137
+ if (pattern.startsWith("*.")) {
138
+ const baseDomain = pattern.slice(2);
139
+ 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
+ const patternParts = baseDomain.split(".");
143
+ return parts.length === patternParts.length + 1;
144
+ }
145
+ return false;
146
+ }
147
+
148
+ // `**.example.com` - any depth subdomain of specific domain
149
+ if (pattern.startsWith("**.")) {
150
+ const baseDomain = pattern.slice(3);
151
+ if (hostname.endsWith("." + baseDomain)) {
152
+ const patternParts = baseDomain.split(".");
153
+ // Must have more parts than the base domain (i.e., has subdomains)
154
+ return parts.length > patternParts.length;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ // `subdomain.*` - specific subdomain of any apex domain
160
+ // e.g., admin.* matches admin.example.com, admin.google.com
161
+ if (pattern.endsWith(".*")) {
162
+ const subdomain = pattern.slice(0, -2);
163
+ // Must be single-level subdomain (3 parts total)
164
+ if (parts.length === 3 && parts[0] === subdomain) {
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+
170
+ // `subdomain.**` - specific subdomain of any domain (including multi-level)
171
+ // e.g., admin.** matches admin.example.com, admin.sub.example.com
172
+ if (pattern.endsWith(".**")) {
173
+ const subdomain = pattern.slice(0, -3);
174
+ if (parts.length >= 3 && parts[0] === subdomain) {
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+
180
+ // `subdomain.` - specific subdomain of any apex domain (no wildcard)
181
+ // e.g., admin. matches admin.example.com, admin.google.com
182
+ if (pattern.endsWith(".") && !pattern.includes("*")) {
183
+ const subdomain = pattern.slice(0, -1);
184
+ // Must be exactly 3 parts (subdomain.domain.tld)
185
+ if (parts.length === 3 && parts[0] === subdomain) {
186
+ return true;
187
+ }
188
+ return false;
189
+ }
190
+
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Validate pattern format
196
+ */
197
+ export function validatePattern(pattern: string): void {
198
+ if (!pattern || typeof pattern !== "string") {
199
+ throw new InvalidPatternError(
200
+ pattern,
201
+ "Pattern must be a non-empty string",
202
+ { cause: { type: typeof pattern, value: pattern } },
203
+ );
204
+ }
205
+
206
+ // Check for invalid characters (spaces, etc.)
207
+ if (/\s/.test(pattern)) {
208
+ throw new InvalidPatternError(pattern, "contains whitespace", {
209
+ cause: { pattern },
210
+ });
211
+ }
212
+
213
+ // Additional validation can be added here
214
+ }
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Host Router Implementation
3
+ *
4
+ * Main router that handles host-based routing with middleware and cookie override.
5
+ */
6
+
7
+ import type {
8
+ HostRouter,
9
+ HostRouteBuilder,
10
+ HostRouterOptions,
11
+ Handler,
12
+ LazyHandler,
13
+ Middleware,
14
+ HostPattern,
15
+ RouteEntry,
16
+ HostMatchResult,
17
+ } from "./types.js";
18
+ import type { RouterRequestInput } from "../router/router-interfaces.js";
19
+ import {
20
+ matchPattern,
21
+ parseRequest,
22
+ normalizePattern,
23
+ validatePattern,
24
+ } from "./pattern-matcher.js";
25
+ import {
26
+ handleCookieOverride,
27
+ createCookieErrorResponse,
28
+ } from "./cookie-handler.js";
29
+ import {
30
+ HostRouterError,
31
+ NoRouteMatchError,
32
+ InvalidHandlerError,
33
+ } from "./errors.js";
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
+ export interface HostRouterRegistryEntry {
41
+ routes: RouteEntry[];
42
+ fallback: RouteEntry | null;
43
+ }
44
+
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
+ export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> =
51
+ new Map();
52
+
53
+ let hostRouterAutoId = 0;
54
+
55
+ /**
56
+ * Create a host router
57
+ */
58
+ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
59
+ const routes: RouteEntry[] = [];
60
+ const globalMiddleware: Middleware[] = [];
61
+ let fallbackRoute: RouteEntry | null = null;
62
+
63
+ const { debug = false, hostOverride } = options;
64
+
65
+ function log(message: string, ...args: any[]): void {
66
+ if (debug) {
67
+ console.log(`[HostRouter] ${message}`, ...args);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Create a route builder for chaining
73
+ */
74
+ function createRouteBuilder(
75
+ patterns: string[],
76
+ isFallback = false,
77
+ ): HostRouteBuilder {
78
+ const middleware: Middleware[] = [];
79
+
80
+ return {
81
+ use(...mw: Middleware[]): HostRouteBuilder {
82
+ middleware.push(...mw);
83
+ return this;
84
+ },
85
+
86
+ map(handler: Handler | LazyHandler): HostRouter {
87
+ const entry: RouteEntry = {
88
+ patterns,
89
+ middleware,
90
+ handler,
91
+ isFallback,
92
+ };
93
+
94
+ if (isFallback) {
95
+ fallbackRoute = entry;
96
+ } else {
97
+ routes.push(entry);
98
+ }
99
+
100
+ log(
101
+ `Registered ${isFallback ? "fallback" : "route"}:`,
102
+ patterns.join(", "),
103
+ );
104
+
105
+ return router;
106
+ },
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Find matching route for hostname and path
112
+ */
113
+ function findMatchingRoute(
114
+ hostname: string,
115
+ pathname: string,
116
+ ): RouteEntry | null {
117
+ const parts = hostname.split(".");
118
+
119
+ for (const route of routes) {
120
+ for (const pattern of route.patterns) {
121
+ if (matchPattern(pattern, hostname, pathname, parts)) {
122
+ log(`Matched pattern: "${pattern}"`);
123
+ return route;
124
+ }
125
+ }
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Execute middleware chain
133
+ */
134
+ async function executeMiddleware(
135
+ middleware: Middleware[],
136
+ request: Request,
137
+ input: RouterRequestInput<any>,
138
+ finalHandler: () => Promise<Response>,
139
+ ): Promise<Response> {
140
+ let index = 0;
141
+
142
+ async function next(): Promise<Response> {
143
+ if (index >= middleware.length) {
144
+ return finalHandler();
145
+ }
146
+
147
+ const mw = middleware[index++];
148
+ if (!mw) {
149
+ return finalHandler();
150
+ }
151
+
152
+ // Guard against double next() calls — a second call would
153
+ // re-enter the downstream chain and run handlers/side-effects twice.
154
+ let nextCalled = false;
155
+ const guardedNext = (): Promise<Response> => {
156
+ if (nextCalled) {
157
+ throw new Error(
158
+ `[HostRouter] Middleware called next() more than once.`,
159
+ );
160
+ }
161
+ nextCalled = true;
162
+ return next();
163
+ };
164
+
165
+ return mw(request, input, guardedNext);
166
+ }
167
+
168
+ return next();
169
+ }
170
+
171
+ /**
172
+ * Execute handler (lazy or direct)
173
+ */
174
+ async function executeHandler(
175
+ handler: Handler | LazyHandler,
176
+ request: Request,
177
+ input: RouterRequestInput<any>,
178
+ ): Promise<Response> {
179
+ // Check if it's a lazy handler (function that returns promise)
180
+ if (typeof handler === "function") {
181
+ const result = handler(request, input);
182
+
183
+ // If it returns a promise with default export
184
+ if (result && typeof result === "object" && "then" in result) {
185
+ const module = await result;
186
+ if (
187
+ typeof module === "object" &&
188
+ module !== null &&
189
+ "default" in module
190
+ ) {
191
+ const defaultExport = (module as { default: Handler | HostRouter })
192
+ .default;
193
+
194
+ // If default export is a router with match method
195
+ if (
196
+ typeof defaultExport === "object" &&
197
+ defaultExport !== null &&
198
+ "match" in defaultExport
199
+ ) {
200
+ return (defaultExport as HostRouter).match(request, input);
201
+ }
202
+
203
+ // Otherwise treat as handler
204
+ return (defaultExport as Handler)(request, input);
205
+ }
206
+ // If promise resolves to Response
207
+ return result as Promise<Response>;
208
+ }
209
+
210
+ // Direct handler
211
+ return result as Response | Promise<Response>;
212
+ }
213
+
214
+ throw new InvalidHandlerError(handler, {
215
+ cause: { handlerType: typeof handler },
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Router instance
221
+ */
222
+ const router: HostRouter = {
223
+ host(patterns: HostPattern): HostRouteBuilder {
224
+ const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
225
+
226
+ // Validate and normalize patterns
227
+ const normalized = patternsArray.map((p) => {
228
+ validatePattern(p);
229
+ return normalizePattern(p);
230
+ });
231
+
232
+ return createRouteBuilder(normalized, false);
233
+ },
234
+
235
+ use(...middleware: Middleware[]): HostRouter {
236
+ globalMiddleware.push(...middleware);
237
+ log(`Registered global middleware (${middleware.length})`);
238
+ return router;
239
+ },
240
+
241
+ fallback(): HostRouteBuilder {
242
+ return createRouteBuilder([], true);
243
+ },
244
+
245
+ test(hostname: string): HostMatchResult | null {
246
+ const parts = hostname.split(".");
247
+ const pathname = "/";
248
+
249
+ for (const route of routes) {
250
+ for (const pattern of route.patterns) {
251
+ if (matchPattern(pattern, hostname, pathname, parts)) {
252
+ return {
253
+ pattern,
254
+ handler: route.handler,
255
+ };
256
+ }
257
+ }
258
+ }
259
+
260
+ return null;
261
+ },
262
+
263
+ async match(
264
+ request: Request,
265
+ input: RouterRequestInput<any> = {},
266
+ ): Promise<Response> {
267
+ log(`Request: ${request.url}`);
268
+
269
+ let effectiveHostname: string;
270
+
271
+ try {
272
+ // Handle cookie override (may throw HostRouterError)
273
+ effectiveHostname = handleCookieOverride(request, hostOverride, input);
274
+ } catch (error) {
275
+ // If it's a HostRouterError from cookie override
276
+ if (error instanceof HostRouterError) {
277
+ log(`Cookie override error: ${error.message}`);
278
+
279
+ // If fallback exists, use it
280
+ if (fallbackRoute) {
281
+ const fallbackInput = { ...input, error };
282
+ const allMiddleware = [
283
+ ...globalMiddleware,
284
+ ...fallbackRoute.middleware,
285
+ ];
286
+
287
+ return executeMiddleware(
288
+ allMiddleware,
289
+ request,
290
+ fallbackInput,
291
+ () =>
292
+ executeHandler(fallbackRoute!.handler, request, fallbackInput),
293
+ );
294
+ }
295
+
296
+ // Otherwise return error response with cookie deletion
297
+ if (hostOverride) {
298
+ return createCookieErrorResponse(
299
+ hostOverride.cookieName,
300
+ error.message,
301
+ );
302
+ }
303
+ }
304
+
305
+ // Re-throw non-HostRouterErrors
306
+ throw error;
307
+ }
308
+
309
+ const { pathname } = parseRequest(request);
310
+
311
+ if (effectiveHostname !== parseRequest(request).hostname) {
312
+ log(`Cookie override: ${effectiveHostname}`);
313
+ }
314
+
315
+ // Find matching route
316
+ const matchedRoute = findMatchingRoute(effectiveHostname, pathname);
317
+
318
+ if (!matchedRoute) {
319
+ log(`No route matched`);
320
+ throw new NoRouteMatchError(effectiveHostname, pathname, {
321
+ cause: {
322
+ hostname: effectiveHostname,
323
+ pathname,
324
+ },
325
+ });
326
+ }
327
+
328
+ // Combine global and route-specific middleware
329
+ const allMiddleware = [...globalMiddleware, ...matchedRoute.middleware];
330
+
331
+ // Execute middleware chain and handler
332
+ return executeMiddleware(allMiddleware, request, input, () =>
333
+ executeHandler(matchedRoute.handler, request, input),
334
+ );
335
+ },
336
+ };
337
+
338
+ // Register in the global HostRouterRegistry for build-time discovery.
339
+ // The routes array and fallbackRoute ref are live - they reflect routes
340
+ // added via .host().map() after this point.
341
+ const registryId = `host-router-${hostRouterAutoId++}`;
342
+ HostRouterRegistry.set(registryId, {
343
+ get routes() {
344
+ return routes;
345
+ },
346
+ get fallback() {
347
+ return fallbackRoute;
348
+ },
349
+ });
350
+
351
+ return router;
352
+ }