@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -12,15 +12,18 @@
12
12
  * - `**.example.com` - any depth subdomain
13
13
  * - `admin.*` - admin subdomain of any apex
14
14
  * - `example.com/admin` - specific domain with path prefix
15
+ *
16
+ * Apex vs subdomain is classified purely by dot-part COUNT (apex == exactly 2
17
+ * parts) — there is no Public Suffix List. A registrable domain under a
18
+ * multi-label public suffix (example.co.uk, shop.com.au) has 3+ parts and is
19
+ * therefore treated as a SUBDOMAIN, not an apex: `.`/`*` will NOT match it and
20
+ * `*.` WILL. If registrable-domain accuracy matters for a host-router consumer,
21
+ * supply an explicit apex/host hint rather than relying on the part count.
15
22
  */
16
23
 
17
24
  import { InvalidPatternError } from "./errors.js";
18
25
 
19
- /**
20
- * Normalize a pattern by removing trailing slashes from paths
21
- */
22
26
  export function normalizePattern(pattern: string): string {
23
- // If pattern has a path component, remove trailing slash
24
27
  const slashIndex = pattern.indexOf("/");
25
28
  if (slashIndex !== -1) {
26
29
  const domain = pattern.slice(0, slashIndex);
@@ -30,9 +33,6 @@ export function normalizePattern(pattern: string): string {
30
33
  return pattern;
31
34
  }
32
35
 
33
- /**
34
- * Parse hostname and path from request URL
35
- */
36
36
  export function parseRequest(request: Request): {
37
37
  hostname: string;
38
38
  pathname: string;
@@ -46,26 +46,14 @@ export function parseRequest(request: Request): {
46
46
  return { hostname, pathname, parts };
47
47
  }
48
48
 
49
- /**
50
- * Count subdomain levels (0 for apex, 1+ for subdomains)
51
- */
52
49
  function getSubdomainLevel(parts: string[]): number {
53
- // Apex domain has 2 parts (example.com)
54
- // Single subdomain has 3 parts (www.example.com)
55
- // Multi-level has 4+ parts (a.b.example.com)
56
50
  return Math.max(0, parts.length - 2);
57
51
  }
58
52
 
59
- /**
60
- * Check if hostname is an apex domain (no subdomains)
61
- */
62
53
  function isApexDomain(parts: string[]): boolean {
63
54
  return parts.length === 2;
64
55
  }
65
56
 
66
- /**
67
- * Match a single pattern against hostname and path
68
- */
69
57
  export function matchPattern(
70
58
  pattern: string,
71
59
  hostname: string,
@@ -74,19 +62,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
  }
@@ -32,27 +32,16 @@ import {
32
32
  InvalidHandlerError,
33
33
  } from "./errors.js";
34
34
 
35
- /**
36
- * Registry entry for a host router instance.
37
- * Stores references to the live routes array and fallback, so the discovery
38
- * plugin can iterate handlers registered after createHostRouter() returns.
39
- */
40
35
  export interface HostRouterRegistryEntry {
41
36
  routes: RouteEntry[];
42
37
  fallback: RouteEntry | null;
43
38
  }
44
39
 
45
- /**
46
- * Global registry for host routers (parallel to RouterRegistry for RSC routers).
47
- * Populated by createHostRouter() so the build-time discovery plugin can find
48
- * host routers and resolve their lazy handlers to trigger sub-app createRouter() calls.
49
- */
50
40
  export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> =
51
41
  new Map();
52
42
 
53
43
  let hostRouterAutoId = 0;
54
44
 
55
- /** Whether a value is thenable (a Promise or Promise-like). */
56
45
  function isThenable(value: unknown): value is PromiseLike<unknown> {
57
46
  return (
58
47
  value !== null &&
@@ -61,12 +50,6 @@ function isThenable(value: unknown): value is PromiseLike<unknown> {
61
50
  );
62
51
  }
63
52
 
64
- /**
65
- * Whether a resolved value looks like a module namespace from a lazy import -
66
- * an object with a `default` export that is a function (a Handler) or a host
67
- * router (an object with `match`). Used to detect a `.map(() => import(...))`
68
- * misuse: an inline handler should return a Response, not a module.
69
- */
70
53
  function looksLikeLazyModule(value: unknown): boolean {
71
54
  if (value === null || typeof value !== "object" || !("default" in value)) {
72
55
  return false;
@@ -80,9 +63,6 @@ function looksLikeLazyModule(value: unknown): boolean {
80
63
  );
81
64
  }
82
65
 
83
- /**
84
- * Create a host router
85
- */
86
66
  export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
87
67
  const routes: RouteEntry[] = [];
88
68
  const globalMiddleware: Middleware[] = [];
@@ -96,9 +76,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
96
76
  }
97
77
  }
98
78
 
99
- /**
100
- * Create a route builder for chaining
101
- */
102
79
  function createRouteBuilder(
103
80
  patterns: string[],
104
81
  isFallback = false,
@@ -147,9 +124,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
147
124
  };
148
125
  }
149
126
 
150
- /**
151
- * Find matching route for hostname and path
152
- */
153
127
  function findMatchingRoute(
154
128
  hostname: string,
155
129
  pathname: string,
@@ -168,9 +142,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
168
142
  return null;
169
143
  }
170
144
 
171
- /**
172
- * Execute middleware chain
173
- */
174
145
  async function executeMiddleware(
175
146
  middleware: Middleware[],
176
147
  request: Request,
@@ -189,8 +160,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
189
160
  return finalHandler();
190
161
  }
191
162
 
192
- // Guard against double next() calls — a second call would
193
- // re-enter the downstream chain and run handlers/side-effects twice.
194
163
  let nextCalled = false;
195
164
  const guardedNext = (): Promise<Response> => {
196
165
  if (nextCalled) {
@@ -208,15 +177,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
208
177
  return next();
209
178
  }
210
179
 
211
- /**
212
- * Execute a route entry, branching on its declared kind:
213
- * - "lazy": await the loader, then delegate to the default export
214
- * (a nested HostRouter via `.match`, or a request Handler directly).
215
- * - "handler": call the inline handler with the request. A `.map()` handler
216
- * that resolves to a module namespace (`{ default }`) is almost certainly
217
- * a misused lazy import, so it is rejected with a clear message rather
218
- * than silently returning a module object as the response.
219
- */
220
180
  async function executeHandler(
221
181
  entry: RouteEntry,
222
182
  request: Request,
@@ -236,8 +196,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
236
196
 
237
197
  const result = (handler as Handler)(request, input);
238
198
 
239
- // Inline handlers may be async; await to obtain the Response and to run the
240
- // misuse guard below.
241
199
  if (isThenable(result)) {
242
200
  const awaited = await result;
243
201
  if (looksLikeLazyModule(awaited)) {
@@ -251,10 +209,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
251
209
  return result;
252
210
  }
253
211
 
254
- /**
255
- * Resolve a `.lazy()` mount: invoke the zero-arg loader, then dispatch to the
256
- * module's default export.
257
- */
258
212
  async function executeLazyMount(
259
213
  loader: LazyHandler,
260
214
  request: Request,
@@ -266,7 +220,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
266
220
  const defaultExport = (module as { default: Handler | HostRouter })
267
221
  .default;
268
222
 
269
- // Default export is a nested host router
270
223
  if (
271
224
  typeof defaultExport === "object" &&
272
225
  defaultExport !== null &&
@@ -275,7 +228,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
275
228
  return (defaultExport as HostRouter).match(request, input);
276
229
  }
277
230
 
278
- // Otherwise treat the default export as a request handler
279
231
  return (defaultExport as Handler)(request, input);
280
232
  }
281
233
 
@@ -288,14 +240,10 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
288
240
  });
289
241
  }
290
242
 
291
- /**
292
- * Router instance
293
- */
294
243
  const router: HostRouter = {
295
244
  host(patterns: HostPattern): HostRouteBuilder {
296
245
  const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
297
246
 
298
- // Validate and normalize patterns
299
247
  const normalized = patternsArray.map((p) => {
300
248
  validatePattern(p);
301
249
  return normalizePattern(p);
@@ -314,9 +262,8 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
314
262
  return createRouteBuilder([], true);
315
263
  },
316
264
 
317
- test(hostname: string): HostMatchResult | null {
265
+ test(hostname: string, pathname = "/"): HostMatchResult | null {
318
266
  const parts = hostname.split(".");
319
- const pathname = "/";
320
267
 
321
268
  for (const route of routes) {
322
269
  for (const pattern of route.patterns) {
@@ -342,14 +289,11 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
342
289
  let effectiveHostname: string;
343
290
 
344
291
  try {
345
- // Handle cookie override (may throw HostRouterError)
346
292
  effectiveHostname = handleCookieOverride(request, hostOverride, input);
347
293
  } catch (error) {
348
- // If it's a HostRouterError from cookie override
349
294
  if (error instanceof HostRouterError) {
350
295
  log(`Cookie override error: ${error.message}`);
351
296
 
352
- // If fallback exists, use it
353
297
  if (fallbackRoute) {
354
298
  const fallbackInput = { ...input, error };
355
299
  const allMiddleware = [
@@ -365,7 +309,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
365
309
  );
366
310
  }
367
311
 
368
- // Otherwise return error response with cookie deletion
369
312
  if (hostOverride) {
370
313
  return createCookieErrorResponse(
371
314
  hostOverride.cookieName,
@@ -374,7 +317,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
374
317
  }
375
318
  }
376
319
 
377
- // Re-throw non-HostRouterErrors
378
320
  throw error;
379
321
  }
380
322
 
@@ -384,7 +326,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
384
326
  log(`Cookie override: ${effectiveHostname}`);
385
327
  }
386
328
 
387
- // Find matching route
388
329
  const matchedRoute = findMatchingRoute(effectiveHostname, pathname);
389
330
 
390
331
  if (!matchedRoute) {
@@ -397,19 +338,14 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
397
338
  });
398
339
  }
399
340
 
400
- // Combine global and route-specific middleware
401
341
  const allMiddleware = [...globalMiddleware, ...matchedRoute.middleware];
402
342
 
403
- // Execute middleware chain and handler
404
343
  return executeMiddleware(allMiddleware, request, input, () =>
405
344
  executeHandler(matchedRoute, request, input),
406
345
  );
407
346
  },
408
347
  };
409
348
 
410
- // Register in the global HostRouterRegistry for build-time discovery.
411
- // The routes array and fallbackRoute ref are live - they reflect routes
412
- // added via .host().map()/.lazy() after this point.
413
349
  const registryId = `host-router-${hostRouterAutoId++}`;
414
350
  HostRouterRegistry.set(registryId, {
415
351
  get routes() {
@@ -14,18 +14,6 @@ export interface CreateTestRequestOptions {
14
14
  headers?: Record<string, string>;
15
15
  }
16
16
 
17
- /**
18
- * Create a test request with specific host and cookies
19
- *
20
- * @example
21
- * ```ts
22
- * const request = createTestRequest({
23
- * host: 'admin.example.com',
24
- * path: '/dashboard',
25
- * cookies: { 'x-requested-host': 'api.example.com' }
26
- * });
27
- * ```
28
- */
29
17
  export function createTestRequest(options: CreateTestRequestOptions): Request {
30
18
  const {
31
19
  host,
@@ -38,7 +26,6 @@ export function createTestRequest(options: CreateTestRequestOptions): Request {
38
26
  const url = `http://${host}${path}`;
39
27
  const requestHeaders = new Headers(headers);
40
28
 
41
- // Add cookies if provided
42
29
  if (Object.keys(cookies).length > 0) {
43
30
  const cookieString = Object.entries(cookies)
44
31
  .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
@@ -52,9 +39,6 @@ export function createTestRequest(options: CreateTestRequestOptions): Request {
52
39
  });
53
40
  }
54
41
 
55
- // Try each pattern (a single pattern or any in an array) against the already
56
- // parsed host + path. Shared by testPattern and matchesHost so the
57
- // normalize-and-loop lives once.
58
42
  function matchPatterns(
59
43
  pattern: string | string[],
60
44
  hostname: string,
package/src/host/types.ts CHANGED
@@ -110,9 +110,13 @@ export interface HostRouter {
110
110
  fallback(): HostRouteBuilder;
111
111
 
112
112
  /**
113
- * Test which handler would match a hostname
113
+ * Test which handler would match a hostname (and optional pathname).
114
+ *
115
+ * `pathname` defaults to `"/"`. Pass it to probe path-prefixed patterns
116
+ * such as `host(["example.com/admin"])`, which only match when the request
117
+ * path is under the prefix.
114
118
  */
115
- test(hostname: string): HostMatchResult | null;
119
+ test(hostname: string, pathname?: string): HostMatchResult | null;
116
120
  }
117
121
 
118
122
  /**
@@ -298,13 +298,9 @@ declare global {
298
298
  */
299
299
  export function href<T extends ValidPaths>(path: T, mount?: string): string {
300
300
  if (mount && mount !== "/") {
301
- // Strip trailing slash from mount to avoid double-slash when joining
302
301
  const normalizedMount = mount.endsWith("/") ? mount.slice(0, -1) : mount;
303
302
  return normalizedMount + path;
304
303
  }
305
- // ValidPaths is built from template literals so T does extend string at
306
- // runtime, but the inference can fail past a certain route-union complexity
307
- // and TypeScript reports T as not assignable to string.
308
304
  return path as string;
309
305
  }
310
306
 
package/src/index.rsc.ts CHANGED
@@ -75,6 +75,13 @@ export type {
75
75
  ResolveStreamingContext,
76
76
  } from "./router.js";
77
77
 
78
+ // Origin-check callback types (referenced by the RangoOptions.originCheck JSDoc)
79
+ export type {
80
+ OriginCheckConfig,
81
+ OriginCheckContext,
82
+ OriginCheckPhase,
83
+ } from "./rsc/origin-guard.js";
84
+
78
85
  // Server-side createLoader and redirect
79
86
  export {
80
87
  createLoader,
@@ -107,6 +114,13 @@ export type {
107
114
 
108
115
  // Handle API
109
116
  export { createHandle, isHandle, type Handle } from "./handle.js";
117
+ export {
118
+ DEFAULT_DEFER_TIMEOUT_MS,
119
+ type DeferOptions,
120
+ type DeferredHandleEntry,
121
+ type HandlePush,
122
+ type HandlePushFn,
123
+ } from "./defer.js";
110
124
 
111
125
  // Context variable API (typed ctx.set/ctx.get tokens)
112
126
  export { createVar, type ContextVar } from "./context-var.js";
@@ -124,10 +138,15 @@ export {
124
138
  type BuildContext,
125
139
  type StaticBuildContext,
126
140
  type GetParamsContext,
141
+ type PrerenderPassthroughResult,
127
142
  } from "./prerender.js";
128
143
 
129
144
  // Static handler API
130
- export { Static, type StaticHandlerDefinition } from "./static-handler.js";
145
+ export {
146
+ Static,
147
+ type StaticHandlerDefinition,
148
+ type StaticHandlerOptions,
149
+ } from "./static-handler.js";
131
150
 
132
151
  // Django-style URL patterns (RSC/server context)
133
152
  export {
@@ -202,7 +221,13 @@ export { updateTag, revalidateTag } from "./cache/tag-invalidation.js";
202
221
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
203
222
 
204
223
  // Middleware context types
205
- export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
224
+ export type {
225
+ MiddlewareContext,
226
+ CookieOptions,
227
+ // The function type of a middleware. Public so the documented "extract the
228
+ // middleware and unit-test it with runMiddleware" pattern has a nameable type.
229
+ MiddlewareFn,
230
+ } from "./router/middleware.js";
206
231
 
207
232
  // Reverse type utilities for type-safe URL generation (Django-style URL reversal)
208
233
  export type {
package/src/index.ts CHANGED
@@ -140,6 +140,13 @@ export function redirect(): never {
140
140
 
141
141
  // Handle API (universal - works on both server and client)
142
142
  export { createHandle, isHandle, type Handle } from "./handle.js";
143
+ export {
144
+ DEFAULT_DEFER_TIMEOUT_MS,
145
+ type DeferOptions,
146
+ type DeferredHandleEntry,
147
+ type HandlePush,
148
+ type HandlePushFn,
149
+ } from "./defer.js";
143
150
 
144
151
  // Context variable API (typed ctx.set/ctx.get tokens)
145
152
  export { createVar, type ContextVar } from "./context-var.js";
@@ -1,7 +1,5 @@
1
- // Internal debug gate. Enable with INTERNAL_RANGO_DEBUG=1 in the environment.
2
- // Uses a Vite define (__RANGO_DEBUG__) for compile-time injection so it works
3
- // in all runtimes including Cloudflare Workers where process.env is unavailable.
4
- // Falls back to process.env for non-Vite contexts (tests, direct Node usage).
1
+ // Vite define for compile-time injection; falls back to process.env (tests, Node).
2
+ // Works in all runtimes including Cloudflare Workers where process.env is unavailable.
5
3
  export const INTERNAL_RANGO_DEBUG: boolean =
6
4
  typeof __RANGO_DEBUG__ !== "undefined"
7
5
  ? __RANGO_DEBUG__
package/src/loader.rsc.ts CHANGED
@@ -26,8 +26,7 @@ import { isUnderTestRunner } from "./runtime-env.js";
26
26
 
27
27
  export { getFetchableLoader };
28
28
 
29
- // Counter for runtime-fallback loader ids assigned only in a bare unit test
30
- // (no Vite plugin to inject one). Process-stable; never reached in a real build.
29
+ // Runtime-fallback counter for bare unit tests (no Vite plugin); process-stable.
31
30
  let runtimeLoaderIdCounter = 0;
32
31
 
33
32
  // Overload 1: With function only (not fetchable)
@@ -54,17 +53,10 @@ export function createLoader<T>(
54
53
  // Hidden parameter injected by Vite exposeInternalIds plugin
55
54
  __injectedId?: string,
56
55
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
57
- // The $$id will be set on the returned object by Vite plugin
58
- // For fetchable loaders, __injectedId is also passed as a parameter
59
56
  let loaderId = __injectedId || "";
60
57
 
61
- // No build-injected id. Under a test runner: fall back to a synthetic id so the
62
- // fn registers below and the loader is exercisable via runLoader(loaderHandle)
63
- // (it recovers the fn from the registry by $$id). Otherwise (dev or a real
64
- // build) it means an UNSUPPORTED shape (e.g. a namespace import
65
- // `rango.createLoader(...)`) the plugin skipped — fail loud. The rich
66
- // diagnostic stays behind the NODE_ENV check so production folds it away and
67
- // ships the small throw. isUnderTestRunner() is runtime-safe. Mirrors createHandle.
58
+ // Under test runner, fall back to synthetic id (recovers fn from registry by $$id).
59
+ // Otherwise (dev or prod), missing id means unsupported shape — fail loud.
68
60
  if (!loaderId) {
69
61
  if (isUnderTestRunner()) {
70
62
  loaderId = `__rango_runtime_loader_${runtimeLoaderIdCounter++}`;
@@ -90,12 +82,9 @@ export function createLoader<T>(
90
82
  };
91
83
  }
92
84
 
93
- // Fetchable loader - store fn in registry and return a serializable object
85
+ // Fetchable loader - store fn in registry and return a serializable object.
94
86
  const middleware: MiddlewareFn[] =
95
87
  fetchable === true ? [] : fetchable?.middleware || [];
96
-
97
- // Register the function in the internal registry by $$id (server-side only)
98
- // The loader fetch handler looks it up by $$id when load() is called from the client.
99
88
  if (fn && loaderId) {
100
89
  registerFetchableLoader(loaderId, fn, middleware, true);
101
90
  }
package/src/loader.ts CHANGED
@@ -38,8 +38,7 @@ export function createLoader<T>(
38
38
  options: FetchableLoaderOptions,
39
39
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
40
40
 
41
- // Implementation - client stub that just returns the loader definition
42
- // The $$id parameter is injected by Vite plugin, not user-provided
41
+ // Implementation - client stub ($$id injected by Vite plugin, not user-provided)
43
42
  export function createLoader<T>(
44
43
  _fn: LoaderFn<T, Record<string, string | undefined>, any>,
45
44
  _fetchable?: true | FetchableLoaderOptions,
@@ -47,13 +46,8 @@ export function createLoader<T>(
47
46
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
48
47
  const loaderId = __injectedId || "";
49
48
 
50
- // Client/SSR build of createLoader. Under a test runner it needs no id
51
- // (loaderId stays ""; the react-server build in loader.rsc.ts adds the runtime
52
- // fallback for whole-router construction). Otherwise (dev or a real build) a
53
- // missing id means an UNSUPPORTED shape the plugin skipped — fail loud rather
54
- // than ship `$$id: ""` (which would make a client useLoader read the wrong
55
- // key). The rich diagnostic stays behind the NODE_ENV check so production folds
56
- // it away and ships the small throw. isUnderTestRunner() is runtime-safe.
49
+ // Under test runner, no id needed (loaderId stays ""; loader.rsc.ts provides fallback).
50
+ // Otherwise, missing id means unsupported shape fail loud to avoid wrong key.
57
51
  if (!loaderId && !isUnderTestRunner()) {
58
52
  if (process.env.NODE_ENV !== "production") {
59
53
  throw missingInjectedIdError("Loader", "createLoader");
@@ -9,12 +9,7 @@ interface NetworkErrorThrowerProps {
9
9
 
10
10
  /**
11
11
  * Client component that throws a NetworkError during render.
12
- * Used to trigger the root error boundary when a network error occurs
13
- * during navigation or server actions.
14
- *
15
- * This must be a separate component because:
16
- * 1. Errors must be thrown during React's render phase to be caught by error boundaries
17
- * 2. The error occurs in async code (fetch), so we need to propagate it to React's render
12
+ * Errors thrown during render are caught by error boundaries; async errors are not.
18
13
  */
19
14
  export function NetworkErrorThrower({
20
15
  error,
@@ -5,11 +5,7 @@ import { OutletContext, type OutletContextValue } from "./outlet-context.js";
5
5
  import type { ResolvedSegment } from "./types.js";
6
6
 
7
7
  /**
8
- * Provider for outlet content - used internally by renderSegments
9
- *
10
- * Stores a reference to parent context so useLoader can walk up the chain
11
- * to find loader data from parent layouts. If this segment defines a loading
12
- * component, Outlet will wrap content with Suspense using that as fallback.
8
+ * Outlet content provider stores parent context for useLoader chain walking.
13
9
  */
14
10
  export function OutletProvider({
15
11
  content,