@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -32,27 +32,16 @@ import {
32
32
  InvalidHandlerError,
33
33
  } from "./errors.js";
34
34
 
35
- /**
36
- * Registry entry for a host router instance.
37
- * Stores references to the live routes array and fallback, so the discovery
38
- * plugin can iterate handlers registered after createHostRouter() returns.
39
- */
40
35
  export interface HostRouterRegistryEntry {
41
36
  routes: RouteEntry[];
42
37
  fallback: RouteEntry | null;
43
38
  }
44
39
 
45
- /**
46
- * Global registry for host routers (parallel to RouterRegistry for RSC routers).
47
- * Populated by createHostRouter() so the build-time discovery plugin can find
48
- * host routers and resolve their lazy handlers to trigger sub-app createRouter() calls.
49
- */
50
40
  export const HostRouterRegistry: Map<string, HostRouterRegistryEntry> =
51
41
  new Map();
52
42
 
53
43
  let hostRouterAutoId = 0;
54
44
 
55
- /** Whether a value is thenable (a Promise or Promise-like). */
56
45
  function isThenable(value: unknown): value is PromiseLike<unknown> {
57
46
  return (
58
47
  value !== null &&
@@ -61,12 +50,6 @@ function isThenable(value: unknown): value is PromiseLike<unknown> {
61
50
  );
62
51
  }
63
52
 
64
- /**
65
- * Whether a resolved value looks like a module namespace from a lazy import -
66
- * an object with a `default` export that is a function (a Handler) or a host
67
- * router (an object with `match`). Used to detect a `.map(() => import(...))`
68
- * misuse: an inline handler should return a Response, not a module.
69
- */
70
53
  function looksLikeLazyModule(value: unknown): boolean {
71
54
  if (value === null || typeof value !== "object" || !("default" in value)) {
72
55
  return false;
@@ -80,9 +63,6 @@ function looksLikeLazyModule(value: unknown): boolean {
80
63
  );
81
64
  }
82
65
 
83
- /**
84
- * Create a host router
85
- */
86
66
  export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
87
67
  const routes: RouteEntry[] = [];
88
68
  const globalMiddleware: Middleware[] = [];
@@ -96,9 +76,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
96
76
  }
97
77
  }
98
78
 
99
- /**
100
- * Create a route builder for chaining
101
- */
102
79
  function createRouteBuilder(
103
80
  patterns: string[],
104
81
  isFallback = false,
@@ -147,9 +124,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
147
124
  };
148
125
  }
149
126
 
150
- /**
151
- * Find matching route for hostname and path
152
- */
153
127
  function findMatchingRoute(
154
128
  hostname: string,
155
129
  pathname: string,
@@ -168,9 +142,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
168
142
  return null;
169
143
  }
170
144
 
171
- /**
172
- * Execute middleware chain
173
- */
174
145
  async function executeMiddleware(
175
146
  middleware: Middleware[],
176
147
  request: Request,
@@ -189,8 +160,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
189
160
  return finalHandler();
190
161
  }
191
162
 
192
- // Guard against double next() calls — a second call would
193
- // re-enter the downstream chain and run handlers/side-effects twice.
194
163
  let nextCalled = false;
195
164
  const guardedNext = (): Promise<Response> => {
196
165
  if (nextCalled) {
@@ -208,15 +177,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
208
177
  return next();
209
178
  }
210
179
 
211
- /**
212
- * Execute a route entry, branching on its declared kind:
213
- * - "lazy": await the loader, then delegate to the default export
214
- * (a nested HostRouter via `.match`, or a request Handler directly).
215
- * - "handler": call the inline handler with the request. A `.map()` handler
216
- * that resolves to a module namespace (`{ default }`) is almost certainly
217
- * a misused lazy import, so it is rejected with a clear message rather
218
- * than silently returning a module object as the response.
219
- */
220
180
  async function executeHandler(
221
181
  entry: RouteEntry,
222
182
  request: Request,
@@ -236,8 +196,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
236
196
 
237
197
  const result = (handler as Handler)(request, input);
238
198
 
239
- // Inline handlers may be async; await to obtain the Response and to run the
240
- // misuse guard below.
241
199
  if (isThenable(result)) {
242
200
  const awaited = await result;
243
201
  if (looksLikeLazyModule(awaited)) {
@@ -251,10 +209,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
251
209
  return result;
252
210
  }
253
211
 
254
- /**
255
- * Resolve a `.lazy()` mount: invoke the zero-arg loader, then dispatch to the
256
- * module's default export.
257
- */
258
212
  async function executeLazyMount(
259
213
  loader: LazyHandler,
260
214
  request: Request,
@@ -266,7 +220,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
266
220
  const defaultExport = (module as { default: Handler | HostRouter })
267
221
  .default;
268
222
 
269
- // Default export is a nested host router
270
223
  if (
271
224
  typeof defaultExport === "object" &&
272
225
  defaultExport !== null &&
@@ -275,7 +228,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
275
228
  return (defaultExport as HostRouter).match(request, input);
276
229
  }
277
230
 
278
- // Otherwise treat the default export as a request handler
279
231
  return (defaultExport as Handler)(request, input);
280
232
  }
281
233
 
@@ -288,14 +240,10 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
288
240
  });
289
241
  }
290
242
 
291
- /**
292
- * Router instance
293
- */
294
243
  const router: HostRouter = {
295
244
  host(patterns: HostPattern): HostRouteBuilder {
296
245
  const patternsArray = Array.isArray(patterns) ? patterns : [patterns];
297
246
 
298
- // Validate and normalize patterns
299
247
  const normalized = patternsArray.map((p) => {
300
248
  validatePattern(p);
301
249
  return normalizePattern(p);
@@ -314,9 +262,8 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
314
262
  return createRouteBuilder([], true);
315
263
  },
316
264
 
317
- test(hostname: string): HostMatchResult | null {
265
+ test(hostname: string, pathname = "/"): HostMatchResult | null {
318
266
  const parts = hostname.split(".");
319
- const pathname = "/";
320
267
 
321
268
  for (const route of routes) {
322
269
  for (const pattern of route.patterns) {
@@ -342,14 +289,11 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
342
289
  let effectiveHostname: string;
343
290
 
344
291
  try {
345
- // Handle cookie override (may throw HostRouterError)
346
292
  effectiveHostname = handleCookieOverride(request, hostOverride, input);
347
293
  } catch (error) {
348
- // If it's a HostRouterError from cookie override
349
294
  if (error instanceof HostRouterError) {
350
295
  log(`Cookie override error: ${error.message}`);
351
296
 
352
- // If fallback exists, use it
353
297
  if (fallbackRoute) {
354
298
  const fallbackInput = { ...input, error };
355
299
  const allMiddleware = [
@@ -365,7 +309,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
365
309
  );
366
310
  }
367
311
 
368
- // Otherwise return error response with cookie deletion
369
312
  if (hostOverride) {
370
313
  return createCookieErrorResponse(
371
314
  hostOverride.cookieName,
@@ -374,7 +317,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
374
317
  }
375
318
  }
376
319
 
377
- // Re-throw non-HostRouterErrors
378
320
  throw error;
379
321
  }
380
322
 
@@ -384,7 +326,6 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
384
326
  log(`Cookie override: ${effectiveHostname}`);
385
327
  }
386
328
 
387
- // Find matching route
388
329
  const matchedRoute = findMatchingRoute(effectiveHostname, pathname);
389
330
 
390
331
  if (!matchedRoute) {
@@ -397,19 +338,14 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
397
338
  });
398
339
  }
399
340
 
400
- // Combine global and route-specific middleware
401
341
  const allMiddleware = [...globalMiddleware, ...matchedRoute.middleware];
402
342
 
403
- // Execute middleware chain and handler
404
343
  return executeMiddleware(allMiddleware, request, input, () =>
405
344
  executeHandler(matchedRoute, request, input),
406
345
  );
407
346
  },
408
347
  };
409
348
 
410
- // Register in the global HostRouterRegistry for build-time discovery.
411
- // The routes array and fallbackRoute ref are live - they reflect routes
412
- // added via .host().map()/.lazy() after this point.
413
349
  const registryId = `host-router-${hostRouterAutoId++}`;
414
350
  HostRouterRegistry.set(registryId, {
415
351
  get routes() {
@@ -4,7 +4,7 @@
4
4
  * Helper functions for testing host routing.
5
5
  */
6
6
 
7
- import { matchPattern } from "./pattern-matcher.js";
7
+ import { matchPattern, parseRequest } from "./pattern-matcher.js";
8
8
 
9
9
  export interface CreateTestRequestOptions {
10
10
  host: string;
@@ -14,18 +14,6 @@ export interface CreateTestRequestOptions {
14
14
  headers?: Record<string, string>;
15
15
  }
16
16
 
17
- /**
18
- * Create a test request with specific host and cookies
19
- *
20
- * @example
21
- * ```ts
22
- * const request = createTestRequest({
23
- * host: 'admin.example.com',
24
- * path: '/dashboard',
25
- * cookies: { 'x-requested-host': 'api.example.com' }
26
- * });
27
- * ```
28
- */
29
17
  export function createTestRequest(options: CreateTestRequestOptions): Request {
30
18
  const {
31
19
  host,
@@ -38,7 +26,6 @@ export function createTestRequest(options: CreateTestRequestOptions): Request {
38
26
  const url = `http://${host}${path}`;
39
27
  const requestHeaders = new Headers(headers);
40
28
 
41
- // Add cookies if provided
42
29
  if (Object.keys(cookies).length > 0) {
43
30
  const cookieString = Object.entries(cookies)
44
31
  .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
@@ -52,28 +39,54 @@ export function createTestRequest(options: CreateTestRequestOptions): Request {
52
39
  });
53
40
  }
54
41
 
42
+ function matchPatterns(
43
+ pattern: string | string[],
44
+ hostname: string,
45
+ pathname: string,
46
+ parts: string[],
47
+ ): boolean {
48
+ const patterns = Array.isArray(pattern) ? pattern : [pattern];
49
+ return patterns.some((p) => matchPattern(p, hostname, pathname, parts));
50
+ }
51
+
55
52
  /**
56
- * Test if a pattern matches a hostname
53
+ * Test if a pattern matches a hostname (and, for path-based patterns, a pathname).
54
+ *
55
+ * `pathname` defaults to `"/"`, so a host-only pattern works with two args. Pass
56
+ * the third arg to test a path-based pattern (`**.workers.dev/admin`,
57
+ * `localhost/shop`) — without it those patterns can never match.
57
58
  *
58
59
  * @example
59
60
  * ```ts
60
- * expect(testPattern('admin.*', 'admin.example.com')).toBe(true);
61
- * expect(testPattern(['*', 'www.*'], 'example.com')).toBe(true);
61
+ * expect(testPattern("admin.*", "admin.example.com")).toBe(true);
62
+ * expect(testPattern(["*", "www.*"], "example.com")).toBe(true);
63
+ * expect(testPattern("**.workers.dev/admin", "foo.workers.dev", "/admin")).toBe(true);
62
64
  * ```
63
65
  */
64
66
  export function testPattern(
65
67
  pattern: string | string[],
66
68
  hostname: string,
69
+ pathname: string = "/",
67
70
  ): boolean {
68
- const patterns = Array.isArray(pattern) ? pattern : [pattern];
69
- const parts = hostname.split(".");
70
- const pathname = "/";
71
-
72
- for (const p of patterns) {
73
- if (matchPattern(p, hostname, pathname, parts)) {
74
- return true;
75
- }
76
- }
71
+ return matchPatterns(pattern, hostname, pathname, hostname.split("."));
72
+ }
77
73
 
78
- return false;
74
+ /**
75
+ * Test if a pattern matches a `Request` — the hostname AND pathname are taken
76
+ * from the request URL (via the same `parseRequest` the host router uses), so a
77
+ * path-based pattern is tested against a real request without splitting the URL
78
+ * by hand.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const req = new Request("https://foo.workers.dev/admin");
83
+ * expect(matchesHost("**.workers.dev/admin", req)).toBe(true);
84
+ * ```
85
+ */
86
+ export function matchesHost(
87
+ pattern: string | string[],
88
+ request: Request,
89
+ ): boolean {
90
+ const { hostname, pathname, parts } = parseRequest(request);
91
+ return matchPatterns(pattern, hostname, pathname, parts);
79
92
  }
package/src/host/types.ts CHANGED
@@ -110,9 +110,13 @@ export interface HostRouter {
110
110
  fallback(): HostRouteBuilder;
111
111
 
112
112
  /**
113
- * Test which handler would match a hostname
113
+ * Test which handler would match a hostname (and optional pathname).
114
+ *
115
+ * `pathname` defaults to `"/"`. Pass it to probe path-prefixed patterns
116
+ * such as `host(["example.com/admin"])`, which only match when the request
117
+ * path is under the prefix.
114
118
  */
115
- test(hostname: string): HostMatchResult | null;
119
+ test(hostname: string, pathname?: string): HostMatchResult | null;
116
120
  }
117
121
 
118
122
  /**
@@ -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,
@@ -124,10 +131,15 @@ export {
124
131
  type BuildContext,
125
132
  type StaticBuildContext,
126
133
  type GetParamsContext,
134
+ type PrerenderPassthroughResult,
127
135
  } from "./prerender.js";
128
136
 
129
137
  // Static handler API
130
- export { Static, type StaticHandlerDefinition } from "./static-handler.js";
138
+ export {
139
+ Static,
140
+ type StaticHandlerDefinition,
141
+ type StaticHandlerOptions,
142
+ } from "./static-handler.js";
131
143
 
132
144
  // Django-style URL patterns (RSC/server context)
133
145
  export {
@@ -184,6 +196,8 @@ export const getRequestContext: <
184
196
  export {
185
197
  cookies,
186
198
  headers,
199
+ invalidateClientCache,
200
+ keepClientCache,
187
201
  type CookieStore,
188
202
  type Cookie,
189
203
  type ReadonlyHeaders,
@@ -200,7 +214,13 @@ export { updateTag, revalidateTag } from "./cache/tag-invalidation.js";
200
214
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
201
215
 
202
216
  // Middleware context types
203
- export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
217
+ export type {
218
+ MiddlewareContext,
219
+ CookieOptions,
220
+ // The function type of a middleware. Public so the documented "extract the
221
+ // middleware and unit-test it with runMiddleware" pattern has a nameable type.
222
+ MiddlewareFn,
223
+ } from "./router/middleware.js";
204
224
 
205
225
  // Reverse type utilities for type-safe URL generation (Django-style URL reversal)
206
226
  export type {
@@ -235,7 +255,26 @@ export {
235
255
  export { createConsoleSink } from "./router/telemetry.js";
236
256
  export { createOTelSink } from "./router/telemetry-otel.js";
237
257
  export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
238
- export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
258
+ // The full TelemetryEvent union PLUS its member types, so a consumer writing a
259
+ // TelemetrySink can annotate a per-`type` handler (or construct an event literal
260
+ // in a test) instead of only narrowing the opaque union.
261
+ export type {
262
+ TelemetrySink,
263
+ TelemetryEvent,
264
+ RequestStartEvent,
265
+ RequestEndEvent,
266
+ RequestErrorEvent,
267
+ LoaderStartEvent,
268
+ LoaderEndEvent,
269
+ LoaderErrorEvent,
270
+ HandlerErrorEvent,
271
+ CacheSegmentStatus,
272
+ CacheSegmentSignal,
273
+ CacheDecisionEvent,
274
+ RevalidationDecisionEvent,
275
+ RequestTimeoutEvent,
276
+ OriginCheckRejectedEvent,
277
+ } from "./router/telemetry.js";
239
278
 
240
279
  // Timeout types and error class
241
280
  export { RouterTimeoutError } from "./router/timeout.js";
package/src/index.ts CHANGED
@@ -217,6 +217,17 @@ export function headers(): never {
217
217
  throw serverOnlyStubError("headers");
218
218
  }
219
219
 
220
+ /**
221
+ * Client implementation of `invalidateClientCache()`. Unlike the server-only
222
+ * stubs above this is a REAL function under the `default` condition (it marks
223
+ * the client's caches stale); the `react-server` condition (index.rsc.ts)
224
+ * selects the server implementation that writes a rotated `Set-Cookie`.
225
+ */
226
+ export {
227
+ invalidateClientCache,
228
+ keepClientCache,
229
+ } from "./browser/invalidate-client-cache.js";
230
+
220
231
  /**
221
232
  * Error-throwing stub for server-only `createReverse` function.
222
233
  */
@@ -327,7 +338,26 @@ export {
327
338
  // who need the values in non-RSC contexts can import from
328
339
  // `@rangojs/router/server`.
329
340
  export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
330
- export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
341
+ // The full TelemetryEvent union PLUS its member types, so a consumer writing a
342
+ // TelemetrySink can annotate a per-`type` handler (or construct an event literal
343
+ // in a test) instead of only narrowing the opaque union.
344
+ export type {
345
+ TelemetrySink,
346
+ TelemetryEvent,
347
+ RequestStartEvent,
348
+ RequestEndEvent,
349
+ RequestErrorEvent,
350
+ LoaderStartEvent,
351
+ LoaderEndEvent,
352
+ LoaderErrorEvent,
353
+ HandlerErrorEvent,
354
+ CacheSegmentStatus,
355
+ CacheSegmentSignal,
356
+ CacheDecisionEvent,
357
+ RevalidationDecisionEvent,
358
+ RequestTimeoutEvent,
359
+ OriginCheckRejectedEvent,
360
+ } from "./router/telemetry.js";
331
361
 
332
362
  // Timeout types and error class
333
363
  export { RouterTimeoutError } from "./router/timeout.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
@@ -22,9 +22,13 @@ import {
22
22
  getFetchableLoader,
23
23
  } from "./server/fetchable-loader-store.js";
24
24
  import { missingInjectedIdError } from "./missing-id-error.js";
25
+ import { isUnderTestRunner } from "./runtime-env.js";
25
26
 
26
27
  export { getFetchableLoader };
27
28
 
29
+ // Runtime-fallback counter for bare unit tests (no Vite plugin); process-stable.
30
+ let runtimeLoaderIdCounter = 0;
31
+
28
32
  // Overload 1: With function only (not fetchable)
29
33
  export function createLoader<T>(
30
34
  fn: LoaderFn<T, Record<string, string | undefined>, any>,
@@ -49,12 +53,21 @@ export function createLoader<T>(
49
53
  // Hidden parameter injected by Vite exposeInternalIds plugin
50
54
  __injectedId?: string,
51
55
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
52
- // The $$id will be set on the returned object by Vite plugin
53
- // For fetchable loaders, __injectedId is also passed as a parameter
54
- const loaderId = __injectedId || "";
56
+ let loaderId = __injectedId || "";
55
57
 
56
- if (!loaderId && process.env.NODE_ENV === "development") {
57
- throw missingInjectedIdError("Loader", "createLoader");
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.
60
+ if (!loaderId) {
61
+ if (isUnderTestRunner()) {
62
+ loaderId = `__rango_runtime_loader_${runtimeLoaderIdCounter++}`;
63
+ } else if (process.env.NODE_ENV !== "production") {
64
+ throw missingInjectedIdError("Loader", "createLoader");
65
+ } else {
66
+ throw new Error(
67
+ "[rango] Loader is missing $$id — the build plugin did not inject one. " +
68
+ "Export it as `export const X = createLoader(...)`.",
69
+ );
70
+ }
58
71
  }
59
72
 
60
73
  // If not fetchable, store fn in registry (for SSR ctx.use() resolution)
@@ -69,12 +82,9 @@ export function createLoader<T>(
69
82
  };
70
83
  }
71
84
 
72
- // Fetchable loader - store fn in registry and return a serializable object
85
+ // Fetchable loader - store fn in registry and return a serializable object.
73
86
  const middleware: MiddlewareFn[] =
74
87
  fetchable === true ? [] : fetchable?.middleware || [];
75
-
76
- // Register the function in the internal registry by $$id (server-side only)
77
- // The loader fetch handler looks it up by $$id when load() is called from the client.
78
88
  if (fn && loaderId) {
79
89
  registerFetchableLoader(loaderId, fn, middleware, true);
80
90
  }
package/src/loader.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  LoaderFn,
20
20
  } from "./types.js";
21
21
  import { missingInjectedIdError } from "./missing-id-error.js";
22
+ import { isUnderTestRunner } from "./runtime-env.js";
22
23
 
23
24
  // Overload 1: With function only (not fetchable)
24
25
  export function createLoader<T>(
@@ -37,8 +38,7 @@ export function createLoader<T>(
37
38
  options: FetchableLoaderOptions,
38
39
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
39
40
 
40
- // Implementation - client stub that just returns the loader definition
41
- // The $$id parameter is injected by Vite plugin, not user-provided
41
+ // Implementation - client stub ($$id injected by Vite plugin, not user-provided)
42
42
  export function createLoader<T>(
43
43
  _fn: LoaderFn<T, Record<string, string | undefined>, any>,
44
44
  _fetchable?: true | FetchableLoaderOptions,
@@ -46,8 +46,16 @@ export function createLoader<T>(
46
46
  ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
47
47
  const loaderId = __injectedId || "";
48
48
 
49
- if (!loaderId && process.env.NODE_ENV === "development") {
50
- throw missingInjectedIdError("Loader", "createLoader");
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.
51
+ if (!loaderId && !isUnderTestRunner()) {
52
+ if (process.env.NODE_ENV !== "production") {
53
+ throw missingInjectedIdError("Loader", "createLoader");
54
+ }
55
+ throw new Error(
56
+ "[rango] Loader is missing $$id — the build plugin did not inject one. " +
57
+ "Export it as `export const X = createLoader(...)`.",
58
+ );
51
59
  }
52
60
 
53
61
  return {
@@ -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,
@@ -1,18 +1,10 @@
1
1
  /**
2
2
  * Deterministic param hashing for prerender storage keys.
3
- *
4
- * Used at build time (child process) to generate filenames and at
5
- * runtime (worker) to look up pre-rendered data. Both environments
6
- * must produce identical hashes for the same params.
7
- *
8
- * Uses a simple DJB2-based hash that works in all JS environments
9
- * (Node.js, Cloudflare Workers, browsers) without crypto imports.
3
+ * Used at build time and runtime; both must produce identical hashes.
4
+ * DJB2-based; works in all JS environments without crypto imports.
10
5
  */
11
6
 
12
- /**
13
- * Compute a deterministic hash string from route params.
14
- * For static routes (no params), returns "_".
15
- */
7
+ // For static routes (no params), returns "_".
16
8
  export function hashParams(params: Record<string, string>): string {
17
9
  const entries = Object.entries(params);
18
10
  if (entries.length === 0) return "_";
@@ -27,6 +19,13 @@ export function hashParams(params: Record<string, string>): string {
27
19
  /**
28
20
  * DJB2 hash returning an 8-char hex string.
29
21
  * Deterministic across all JS runtimes.
22
+ *
23
+ * 32-bit output: per-route collision probability hits ~50% near ~77k distinct
24
+ * param sets (birthday bound). The production store keys solely on
25
+ * routeName/paramHash and does not verify the canonical param string, so a
26
+ * collision serves the surviving entry for both param sets. Benign for typical
27
+ * catalogs; revisit (wider hash or stored-param verification) before
28
+ * pre-rendering hundreds of thousands of pages per route.
30
29
  */
31
30
  function djb2Hex(str: string): string {
32
31
  let hash = 5381;