@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
@@ -1,11 +1,7 @@
1
1
  /**
2
- * Prerender Store
3
- *
4
- * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
- * a function injected into the RSC entry that returns the manifest module
7
- * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
- * that anchors import() resolution relative to the manifest file.
2
+ * Prerender Store — reads pre-rendered segment data from the worker bundle.
3
+ * Manifest module (injected via globalThis.__loadPrerenderManifestModule)
4
+ * contains key-to-specifier map and loadPrerenderAsset for import() resolution.
9
5
  */
10
6
 
11
7
  import type { SerializedSegmentData } from "../cache/types.js";
@@ -101,13 +97,20 @@ export function createPrerenderStore(): PrerenderStore | null {
101
97
  if (!globalThis.__loadPrerenderManifestModule) return null;
102
98
 
103
99
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
104
- let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
105
- null;
100
+ let manifestModulePromise: Promise<PrerenderManifestModule> | null = null;
106
101
 
107
- function loadManifestModule(): Promise<PrerenderManifestModule | null> {
102
+ function loadManifestModule(): Promise<PrerenderManifestModule> {
108
103
  if (!manifestModulePromise) {
104
+ // Do not cache a failed manifest-module load: clear the memoized promise
105
+ // on rejection so the next get() retries, and let the error propagate
106
+ // (consistent with the per-asset load policy below) instead of caching a
107
+ // null for the isolate lifetime, which would silently degrade every
108
+ // prerendered route to a miss after one transient failure.
109
109
  manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
110
- () => null,
110
+ (err) => {
111
+ manifestModulePromise = null;
112
+ throw err;
113
+ },
111
114
  );
112
115
  }
113
116
  return manifestModulePromise;
@@ -120,7 +123,6 @@ export function createPrerenderStore(): PrerenderStore | null {
120
123
  if (cached) return cached;
121
124
 
122
125
  const promise = loadManifestModule().then((mod) => {
123
- if (!mod) return null;
124
126
  const specifier = mod.default[key];
125
127
  if (!specifier) return null;
126
128
  // Let asset load errors propagate — a missing/corrupted artifact
@@ -129,29 +131,20 @@ export function createPrerenderStore(): PrerenderStore | null {
129
131
  // (which the handler stub would misreport as a 404).
130
132
  return mod.loadPrerenderAsset(specifier).then((asset) => asset.default);
131
133
  });
132
- cache.set(key, promise);
134
+ // Only memoize once the manifest module resolved: a manifest-load
135
+ // rejection must not poison the per-key cache, or the retry above is moot.
136
+ cache.set(
137
+ key,
138
+ promise.catch((err) => {
139
+ cache.delete(key);
140
+ throw err;
141
+ }),
142
+ );
133
143
  return promise;
134
144
  },
135
145
  };
136
146
  }
137
147
 
138
- /**
139
- * Load the prerender manifest index for test introspection.
140
- * Returns the key→specifier map or null if unavailable.
141
- */
142
- export async function loadPrerenderManifestIndex(): Promise<Record<
143
- string,
144
- string
145
- > | null> {
146
- if (!globalThis.__loadPrerenderManifestModule) return null;
147
- try {
148
- const mod = await globalThis.__loadPrerenderManifestModule();
149
- return mod.default;
150
- } catch {
151
- return null;
152
- }
153
- }
154
-
155
148
  /**
156
149
  * Create a static segment store.
157
150
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
package/src/prerender.ts CHANGED
@@ -38,6 +38,7 @@ import type { ReverseFunction } from "./reverse.js";
38
38
  import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
39
39
  import type { UseItems, HandlerUseItem } from "./route-types.js";
40
40
  import { isCachedFunction } from "./cache/taint.js";
41
+ import { isUnderTestRunner } from "./runtime-env.js";
41
42
 
42
43
  // -- Named route resolution types -------------------------------------------
43
44
 
@@ -273,6 +274,11 @@ export interface PrerenderHandlerDefinition<
273
274
  use?: () => UseItems<HandlerUseItem>;
274
275
  }
275
276
 
277
+ // Process-stable fallback id counter (mirrors createHandle / createLoader). Only
278
+ // assigned in a bare unit test where the Vite plugin did not inject an id; never
279
+ // fires in a real build (the plugin always injects).
280
+ let runtimePrerenderIdCounter = 0;
281
+
276
282
  // -- Overloads --------------------------------------------------------------
277
283
  //
278
284
  // T accepts: named route string (global or .local) OR explicit param object.
@@ -376,12 +382,27 @@ export function Prerender<TParams extends Record<string, any>>(
376
382
  );
377
383
  }
378
384
 
379
- if (!id) {
385
+ // Throw unless under a test runner. The plugin always injects $$id for a
386
+ // supported `export const` Prerender on every build, so a missing id means
387
+ // either no plugin (a bare test — fall back below) or an UNSUPPORTED shape the
388
+ // plugin silently skipped (dev OR a real build — fail loud; a synthetic id
389
+ // would degrade to a silent prerender miss). The message is already small (no
390
+ // stack-parsing diagnostic), so it ships as-is. isUnderTestRunner() is
391
+ // runtime-safe — never a bare `process.env` access.
392
+ if (!id && !isUnderTestRunner()) {
380
393
  throw new Error(
381
- "[rango] Prerender: missing $$id. " +
382
- "Ensure the exposeInternalIds Vite plugin is configured.",
394
+ "[rango] Prerender: missing $$id. Use `export const X = Prerender(...)` " +
395
+ "and ensure the exposeInternalIds Vite plugin is configured.",
383
396
  );
384
397
  }
398
+ // Under vitest with no plugin id: assign a process-stable runtime id so a
399
+ // whole-app router with Prerender routes constructs in a bare test (for
400
+ // dispatch / assertGeneratedRoutesMatch). Never reached in a real build (the
401
+ // throw above fires there); prerender storage/lookup keys on routeName +
402
+ // paramHash, never $$id (mirrors createHandle / createLoader).
403
+ if (!id) {
404
+ id = `__rango_runtime_prerender_${runtimePrerenderIdCounter++}`;
405
+ }
385
406
 
386
407
  return {
387
408
  __brand: "prerenderHandler" as const,
@@ -421,6 +442,40 @@ export function isPrerenderPassthrough(
421
442
  );
422
443
  }
423
444
 
445
+ /**
446
+ * Detect whether any resolved segment carries the passthrough sentinel.
447
+ *
448
+ * A build handler signals passthrough by returning `ctx.passthrough()` (the
449
+ * PRERENDER_PASSTHROUGH sentinel), which lands on the segment's `component`.
450
+ * But when the route declares `loading()`, the handler result is deferred
451
+ * upstream (segment-resolution/fresh.ts), so `component` is a thenable resolving
452
+ * to the sentinel rather than the sentinel itself — a synchronous
453
+ * `isPrerenderPassthrough(component)` on the Promise returns false and the build
454
+ * bakes a corrupt artifact instead of deferring. Resolve thenables first.
455
+ *
456
+ * Rejections are swallowed here: a throwing build handler resurfaces during
457
+ * segment serialization, preserving the prior error-handling behavior.
458
+ */
459
+ export async function detectPrerenderPassthrough(
460
+ segments: ReadonlyArray<{ component: unknown }>,
461
+ ): Promise<boolean> {
462
+ for (const seg of segments) {
463
+ let component: unknown = seg.component;
464
+ if (
465
+ component &&
466
+ typeof (component as { then?: unknown }).then === "function"
467
+ ) {
468
+ try {
469
+ component = await component;
470
+ } catch {
471
+ continue;
472
+ }
473
+ }
474
+ if (isPrerenderPassthrough(component)) return true;
475
+ }
476
+ return false;
477
+ }
478
+
424
479
  // -- Type guards ------------------------------------------------------------
425
480
 
426
481
  /**
@@ -3,26 +3,17 @@
3
3
  import { Component, useState, type ReactNode } from "react";
4
4
  import type { ClientErrorBoundaryFallbackProps } from "./types.js";
5
5
 
6
- /**
7
- * Check if an error is a network-related error
8
- */
9
6
  function isNetworkError(error: Error): boolean {
10
7
  return error.name === "NetworkError";
11
8
  }
12
9
 
13
- /**
14
- * Network error fallback UI with retry functionality
15
- * Shows a connection-specific message and allows retrying via page refresh
16
- */
17
10
  function NetworkErrorFallback({
18
11
  error,
19
- reset,
20
12
  }: ClientErrorBoundaryFallbackProps): ReactNode {
21
13
  const [isRetrying, setIsRetrying] = useState(false);
22
14
 
23
15
  const handleRetry = (): void => {
24
16
  setIsRetrying(true);
25
- // Refresh the page to retry the request
26
17
  window.location.reload();
27
18
  };
28
19
 
@@ -42,7 +33,6 @@ function NetworkErrorFallback({
42
33
  marginBottom: "1rem",
43
34
  }}
44
35
  >
45
- {/* Simple cloud with x icon using CSS */}
46
36
  <span style={{ color: "#9ca3af" }}>&#9729;</span>
47
37
  </div>
48
38
  <h1
@@ -101,10 +91,6 @@ function NetworkErrorFallback({
101
91
  );
102
92
  }
103
93
 
104
- /**
105
- * Default fallback UI for root error boundary
106
- * This is shown when an unhandled error bubbles up to the root
107
- */
108
94
  function RootErrorFallback({
109
95
  error,
110
96
  reset,
@@ -230,7 +216,6 @@ export class RootErrorBoundary extends Component<
230
216
  }
231
217
 
232
218
  componentDidMount(): void {
233
- // Listen for popstate (back/forward navigation) to reset error state
234
219
  window.addEventListener("popstate", this.handlePopState);
235
220
  }
236
221
 
@@ -247,15 +232,13 @@ export class RootErrorBoundary extends Component<
247
232
  }
248
233
 
249
234
  componentDidUpdate(prevProps: { children: ReactNode }): void {
250
- // Reset error state when children change (e.g., navigation)
251
- // This allows the app to recover after navigation away from an errored route
235
+ // Reset error on children change (navigation).
252
236
  if (this.state.hasError && prevProps.children !== this.props.children) {
253
237
  this.setState({ hasError: false, error: null });
254
238
  }
255
239
  }
256
240
 
257
241
  handlePopState = (): void => {
258
- // Reset error state on back/forward navigation
259
242
  if (this.state.hasError) {
260
243
  this.setState({ hasError: false, error: null });
261
244
  }
@@ -276,7 +259,6 @@ export class RootErrorBoundary extends Component<
276
259
  segmentType: "route" as const,
277
260
  };
278
261
 
279
- // Use specialized fallback for network errors
280
262
  if (isNetworkError(this.state.error)) {
281
263
  return <NetworkErrorFallback error={errorInfo} reset={this.reset} />;
282
264
  }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import type { ReactNode } from "react";
3
- import { Suspense, use, useId } from "react";
3
+ import { Suspense, use } from "react";
4
4
  import { invariant } from "./errors";
5
5
  import { OutletProvider } from "./outlet-provider.js";
6
6
  import type { ResolvedSegment } from "./types.js";
@@ -36,37 +36,6 @@ export function RouteContentWrapper({
36
36
  );
37
37
  }
38
38
 
39
- export function RouteContentWrapperCallback<T>({
40
- resolve,
41
- fallback,
42
- children,
43
- }: {
44
- resolve: Promise<T> | T;
45
- fallback?: ReactNode;
46
- children: (data: T) => ReactNode;
47
- }): ReactNode {
48
- const id = useId();
49
- invariant(children, "RouteContentWrapperCallback requires children");
50
- invariant(
51
- typeof children === "function",
52
- "RouteContentWrapperCallback requires children to be a function",
53
- );
54
- invariant(
55
- resolve !== undefined,
56
- "RouteContentWrapperCallback requires resolve",
57
- );
58
- return (
59
- <Suspense
60
- fallback={fallback ?? null}
61
- key={"route-content-suspense-callback-" + id}
62
- >
63
- <SuspenderCallback resolve={resolve} key={id}>
64
- {children}
65
- </SuspenderCallback>
66
- </Suspense>
67
- );
68
- }
69
-
70
39
  const Suspender = ({
71
40
  content,
72
41
  }: {
@@ -77,18 +46,6 @@ const Suspender = ({
77
46
  return use(content);
78
47
  };
79
48
 
80
- const SuspenderCallback = <T,>({
81
- resolve,
82
- children,
83
- }: {
84
- resolve: Promise<T> | T;
85
- children: (data: T) => ReactNode;
86
- }): ReactNode => {
87
- return resolve instanceof Promise
88
- ? children(use(resolve))
89
- : children(resolve);
90
- };
91
-
92
49
  /**
93
50
  * LoaderBoundary - Client component that resolves loader promises and renders OutletProvider
94
51
  *
@@ -302,15 +302,15 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
302
302
  * Supports these call signatures:
303
303
  * - cache() - no args, uses app-level defaults (for loader caching)
304
304
  * - cache(() => [...]) - wraps children with app-level defaults
305
- * - cache('profileName') - uses a named cache profile
306
- * - cache('profileName', () => [...]) - named profile with children
307
305
  * - cache({ ttl: 60 }, () => [...]) - with explicit options
306
+ *
307
+ * Named cache profiles are applied via the `"use cache: <profile>"` directive,
308
+ * not a `cache("profileName")` form in the route tree.
308
309
  */
309
310
  const cache: RouteHelpers<any, any>["cache"] = (
310
311
  optionsOrChildren?:
311
312
  | PartialCacheOptions
312
313
  | false
313
- | string
314
314
  | (() => UseItems<AllUseItems>),
315
315
  maybeChildren?: () => UseItems<AllUseItems>,
316
316
  ) => {
@@ -326,18 +326,6 @@ const cache: RouteHelpers<any, any>["cache"] = (
326
326
  // cache() - no args, use defaults
327
327
  options = {};
328
328
  children = undefined;
329
- } else if (typeof optionsOrChildren === "string") {
330
- // cache('profileName') or cache('profileName', () => [...])
331
- // Resolve from context-scoped profiles (set per-router via HelperContext).
332
- const ctxStore = RangoContext.getStore();
333
- const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
334
- invariant(
335
- profile,
336
- `cache("${optionsOrChildren}"): unknown cache profile. ` +
337
- `Define it in createRouter({ cacheProfiles: { "${optionsOrChildren}": { ttl: ... } } }).`,
338
- );
339
- options = { ttl: profile.ttl, swr: profile.swr, tags: profile.tags };
340
- children = maybeChildren;
341
329
  } else if (typeof optionsOrChildren === "function") {
342
330
  // cache(() => [...]) - use empty options (will use defaults)
343
331
  options = {};
@@ -393,10 +381,10 @@ const cache: RouteHelpers<any, any>["cache"] = (
393
381
  return { name: namespace, type: "cache" } as CacheItem;
394
382
  }
395
383
 
396
- // Inside a loader() use() callback, only the direct form — cache()/cache(opts)/
397
- // cache("profile") — writes cache config to the loader entry. The wrapper
398
- // form creates a structural cache boundary with its own children scope, which
399
- // has no effect on the loader and would silently no-op.
384
+ // Inside a loader() use() callback, only the direct form — cache()/cache(opts)
385
+ // — writes cache config to the loader entry. The wrapper form creates a
386
+ // structural cache boundary with its own children scope, which has no effect
387
+ // on the loader and would silently no-op.
400
388
  invariant(
401
389
  !(ctx.parent && (ctx.parent as any).type === "loader"),
402
390
  "cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
@@ -441,10 +441,8 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
441
441
  cache: {
442
442
  (): CacheItem;
443
443
  (children: () => UseItems<AllUseItems>): CacheItem;
444
- (profileName: string): CacheItem;
445
- (profileName: string, use: () => UseItems<AllUseItems>): CacheItem;
446
444
  (
447
- options: PartialCacheOptions | false,
445
+ options: PartialCacheOptions<TEnv> | false,
448
446
  use?: () => UseItems<AllUseItems>,
449
447
  ): CacheItem;
450
448
  };
@@ -497,6 +495,8 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
497
495
  * @param children - Optional callback returning child routes to wrap
498
496
  */
499
497
  transition: {
498
+ (): TransitionItem;
499
+ (children: () => UseItems<AllUseItems>): TransitionItem;
500
500
  (config: TransitionConfig): TransitionItem;
501
501
  (
502
502
  config: TransitionConfig,
@@ -85,9 +85,19 @@ export function redirect(
85
85
  }
86
86
 
87
87
  // Auto-prefix root-relative URLs with basename for app-local redirects.
88
+ // Treat the URL as already-prefixed when the basename is followed by a path
89
+ // separator, a query, a fragment, or end-of-string, so "/admin?tab=x" and
90
+ // "/admin#frag" are not double-prefixed into "/admin/admin?tab=x".
88
91
  const bn = _getRequestContext()?._basename;
89
92
  let resolvedUrl = url;
90
- if (bn && url.startsWith("/") && !url.startsWith(bn + "/") && url !== bn) {
93
+ if (
94
+ bn &&
95
+ url.startsWith("/") &&
96
+ url !== bn &&
97
+ !url.startsWith(bn + "/") &&
98
+ !url.startsWith(bn + "?") &&
99
+ !url.startsWith(bn + "#")
100
+ ) {
91
101
  resolvedUrl = url === "/" ? bn : bn + url;
92
102
  }
93
103
 
@@ -8,15 +8,10 @@
8
8
  * See docs/manifests.md for the full data flow.
9
9
  */
10
10
 
11
- // Singleton route map instance - populated incrementally as routes are encountered
12
11
  let globalRouteMap: Record<string, string> = {};
13
12
 
14
- // Cached complete manifest - includes all routes (including lazy includes)
15
- // Set from runtime cache or build-time import
16
13
  let cachedManifest: Record<string, string> | null = null;
17
14
 
18
- // Pre-computed route entries from build-time prefix tree leaf nodes.
19
- // Used by evaluateLazyEntry() to skip running the handler for route matching.
20
15
  let cachedPrecomputedEntries: Array<{
21
16
  staticPrefix: string;
22
17
  routes: Record<string, string>;
@@ -43,7 +38,6 @@ export function registerRouteMap(map: Record<string, string>): void {
43
38
  * @internal
44
39
  */
45
40
  export function getGlobalRouteMap(): Record<string, string> {
46
- // Cached manifest is complete (includes lazy routes), so prefer it
47
41
  if (cachedManifest) {
48
42
  return cachedManifest;
49
43
  }
@@ -231,10 +225,6 @@ export function waitForManifestReady(): Promise<void> | null {
231
225
  return manifestReadyPromise;
232
226
  }
233
227
 
234
- // ============================================================================
235
- // Route Scope Registry
236
- // ============================================================================
237
-
238
228
  // Tracks whether each route is at root scope (no named include boundary above).
239
229
  // Used by dot-local reverse resolution to decide whether bare-name fallback
240
230
  // is allowed after scoped lookups are exhausted.
@@ -259,14 +249,8 @@ export function isRouteRootScoped(routeName: string): boolean | undefined {
259
249
  return rootScopeRoutes.get(routeName);
260
250
  }
261
251
 
262
- // ============================================================================
263
- // Search Schema Registry
264
- // ============================================================================
265
-
266
252
  import type { SearchSchema } from "./search-params.js";
267
253
 
268
- // Global search schema map: route name -> search schema descriptor.
269
- // Populated by path() when a search option is provided.
270
254
  const globalSearchSchemas: Map<string, SearchSchema> = new Map();
271
255
 
272
256
  export function registerSearchSchema(
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize a router basename to its canonical form: a single leading slash,
3
+ * no trailing slash, and `undefined` for an empty or bare-"/" value.
4
+ *
5
+ * This is the single source of truth used by both createRouter() (so the RSC
6
+ * handler stores a canonical basename on the request context) and the testing
7
+ * primitives (so a consumer can pass the same un-normalized string their
8
+ * createRouter() accepts and observe the same redirect() prefixing).
9
+ */
10
+ export function normalizeBasename(basename?: string): string | undefined {
11
+ if (!basename) return undefined;
12
+ const trimmed = basename.replace(/^\/+|\/+$/g, "");
13
+ return trimmed ? "/" + trimmed : undefined;
14
+ }
@@ -14,7 +14,6 @@ import { traverseBack } from "./pattern-matching.js";
14
14
  import type { RouteMatchResult } from "./pattern-matching.js";
15
15
  import type { RouteSnapshot } from "./route-snapshot.js";
16
16
 
17
- // Response type -> MIME type used for Accept header matching
18
17
  export const RESPONSE_TYPE_MIME: Record<string, string> = {
19
18
  json: "application/json",
20
19
  text: "text/plain",
@@ -23,7 +22,6 @@ export const RESPONSE_TYPE_MIME: Record<string, string> = {
23
22
  md: "text/markdown",
24
23
  };
25
24
 
26
- // Reverse lookup: MIME type -> response type tag (e.g. "text/html" -> "html")
27
25
  export const MIME_RESPONSE_TYPE: Record<string, string> = Object.fromEntries(
28
26
  Object.entries(RESPONSE_TYPE_MIME).map(([tag, mime]) => [mime, tag]),
29
27
  );
@@ -71,12 +69,10 @@ export function parseAcceptTypes(accept: string): AcceptEntry[] {
71
69
  }
72
70
  entries.push({ mime, q, order: i });
73
71
  }
74
- // Sort: highest q first, then lowest client order first (stable)
75
72
  entries.sort((a, b) => b.q - a.q || a.order - b.order);
76
73
  return entries;
77
74
  }
78
75
 
79
- // Sentinel response type for RSC routes in negotiation candidates
80
76
  export const RSC_RESPONSE_TYPE = "__rsc__";
81
77
 
82
78
  /**
@@ -89,7 +85,6 @@ export function pickNegotiateVariant(
89
85
  acceptEntries: AcceptEntry[],
90
86
  candidates: Array<{ routeKey: string; responseType: string }>,
91
87
  ): { routeKey: string; responseType: string } {
92
- // Build a MIME -> candidate lookup for O(1) matching
93
88
  const byCandidateMime = new Map<
94
89
  string,
95
90
  { routeKey: string; responseType: string }
@@ -106,9 +101,7 @@ export function pickNegotiateVariant(
106
101
 
107
102
  for (const entry of acceptEntries) {
108
103
  if (entry.q === 0) continue;
109
- // Wildcard matches first candidate
110
104
  if (entry.mime === "*/*") return candidates[0]!;
111
- // Type wildcard (e.g. "text/*") -- match first candidate with that type
112
105
  if (entry.mime.endsWith("/*")) {
113
106
  const typePrefix = entry.mime.slice(0, entry.mime.indexOf("/"));
114
107
  for (const [mime, candidate] of byCandidateMime) {
@@ -119,7 +112,6 @@ export function pickNegotiateVariant(
119
112
  const match = byCandidateMime.get(entry.mime);
120
113
  if (match) return match;
121
114
  }
122
- // No match -- use first candidate as default
123
115
  return candidates[0]!;
124
116
  }
125
117
 
@@ -173,7 +165,6 @@ export async function negotiateRoute(
173
165
 
174
166
  const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
175
167
 
176
- // Build candidate list preserving definition order.
177
168
  const variants = matched.negotiateVariants;
178
169
  let candidates: Array<{ routeKey: string; responseType: string }>;
179
170
  if (responseType) {
@@ -190,12 +181,10 @@ export async function negotiateRoute(
190
181
 
191
182
  const variant = pickNegotiateVariant(acceptEntries, candidates);
192
183
 
193
- // RSC won negotiation
194
184
  if (variant.responseType === RSC_RESPONSE_TYPE) {
195
185
  return null;
196
186
  }
197
187
 
198
- // Primary response-type won — use existing manifest entry and middleware
199
188
  if (responseType && variant.routeKey === matched.routeKey) {
200
189
  return {
201
190
  responseType,
@@ -205,8 +194,6 @@ export async function negotiateRoute(
205
194
  negotiated: true,
206
195
  };
207
196
  }
208
-
209
- // Different variant won — load its manifest entry
210
197
  const negotiateEntry = await loadManifest(
211
198
  matched.entry,
212
199
  variant.routeKey,
@@ -117,16 +117,10 @@ export function findNearestErrorBoundary(
117
117
  let current: EntryData | null = entry;
118
118
 
119
119
  while (current) {
120
- // Check if this entry has error boundaries defined
121
120
  if (current.errorBoundary && current.errorBoundary.length > 0) {
122
- // Return the last error boundary (most recently defined takes precedence)
123
121
  return current.errorBoundary[current.errorBoundary.length - 1];
124
122
  }
125
123
 
126
- // Check orphan layouts for error boundaries
127
- // Orphan layouts are siblings that render alongside the main route chain
128
- // They can define error boundaries that catch errors from routes in the same route group
129
- // Check from first to last (first sibling takes precedence as the "outer" wrapper)
130
124
  if (current.layout && current.layout.length > 0) {
131
125
  for (const orphan of current.layout) {
132
126
  if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
@@ -153,11 +147,21 @@ export function findNearestNotFoundBoundary(
153
147
  let current: EntryData | null = entry;
154
148
 
155
149
  while (current) {
156
- // Check if this entry has notFound boundaries defined
157
150
  if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
158
- // Return the last notFound boundary (most recently defined takes precedence)
159
151
  return current.notFoundBoundary[current.notFoundBoundary.length - 1];
160
152
  }
153
+
154
+ // Check orphan layouts mirroring findNearestErrorBoundary: notFoundBoundary
155
+ // attaches identically (onto parent.notFoundBoundary), and an orphan layout
156
+ // (parent=null) is reachable only via this scan. First sibling is "outer".
157
+ if (current.layout && current.layout.length > 0) {
158
+ for (const orphan of current.layout) {
159
+ if (orphan.notFoundBoundary && orphan.notFoundBoundary.length > 0) {
160
+ return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
161
+ }
162
+ }
163
+ }
164
+
161
165
  current = current.parent;
162
166
  }
163
167
 
@@ -207,22 +211,17 @@ export function createErrorSegment(
207
211
  entry: EntryData,
208
212
  params: Record<string, string>,
209
213
  ): ResolvedSegment {
210
- // Determine the component to render
211
214
  let component: ReactNode;
212
215
 
213
216
  if (typeof fallback === "function") {
214
- // ErrorBoundaryHandler - call with error info
215
217
  const props: ErrorBoundaryFallbackProps = {
216
218
  error: errorInfo,
217
219
  };
218
220
  component = fallback(props);
219
221
  } else {
220
- // Static ReactNode fallback
221
222
  component = fallback;
222
223
  }
223
224
 
224
- // Error segment uses the same ID as the layout that has the error boundary
225
- // The error boundary content replaces the layout's outlet content
226
225
  return {
227
226
  id: entry.shortCode,
228
227
  namespace: entry.id,
@@ -261,17 +260,14 @@ export function createNotFoundSegment(
261
260
  entry: EntryData,
262
261
  params: Record<string, string>,
263
262
  ): ResolvedSegment {
264
- // Determine the component to render
265
263
  let component: ReactNode;
266
264
 
267
265
  if (typeof fallback === "function") {
268
- // NotFoundBoundaryHandler - call with props
269
266
  const props: NotFoundBoundaryFallbackProps = {
270
267
  notFound: notFoundInfo,
271
268
  };
272
269
  component = fallback(props);
273
270
  } else {
274
- // Static ReactNode fallback
275
271
  component = fallback;
276
272
  }
277
273