@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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 (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Content Negotiation Utilities
3
+ *
4
+ * Pure functions for HTTP Accept header parsing and response type matching.
5
+ * Used by previewMatch and classifyRequest for content negotiation between
6
+ * RSC routes and response routes (JSON, text, image, stream, etc.).
7
+ */
8
+
9
+ import type { EntryData } from "../server/context.js";
10
+ import type { CollectedMiddleware } from "./middleware-types.js";
11
+ import { collectRouteMiddleware } from "./middleware.js";
12
+ import { loadManifest } from "./manifest.js";
13
+ import { traverseBack } from "./pattern-matching.js";
14
+ import type { RouteMatchResult } from "./pattern-matching.js";
15
+ import type { RouteSnapshot } from "./route-snapshot.js";
16
+
17
+ // Response type -> MIME type used for Accept header matching
18
+ export const RESPONSE_TYPE_MIME: Record<string, string> = {
19
+ json: "application/json",
20
+ text: "text/plain",
21
+ xml: "application/xml",
22
+ html: "text/html",
23
+ md: "text/markdown",
24
+ };
25
+
26
+ // Reverse lookup: MIME type -> response type tag (e.g. "text/html" -> "html")
27
+ export const MIME_RESPONSE_TYPE: Record<string, string> = Object.fromEntries(
28
+ Object.entries(RESPONSE_TYPE_MIME).map(([tag, mime]) => [mime, tag]),
29
+ );
30
+
31
+ export type NamedRouteEntry =
32
+ | string
33
+ | { path: string; search?: Record<string, string> };
34
+
35
+ export function flattenNamedRoutes(
36
+ routeNames?: Record<string, NamedRouteEntry>,
37
+ ): Record<string, string> {
38
+ if (!routeNames) return {};
39
+ const flattened: Record<string, string> = {};
40
+ for (const [name, entry] of Object.entries(routeNames)) {
41
+ flattened[name] = typeof entry === "string" ? entry : entry.path;
42
+ }
43
+ return flattened;
44
+ }
45
+
46
+ export interface AcceptEntry {
47
+ mime: string;
48
+ q: number;
49
+ order: number;
50
+ }
51
+
52
+ /**
53
+ * Parse an Accept header into a sorted array of MIME entries.
54
+ * Respects q-values (default 1.0) and uses client order as tiebreaker
55
+ * when q-values are equal (matching Express/Hono behavior).
56
+ */
57
+ export function parseAcceptTypes(accept: string): AcceptEntry[] {
58
+ const entries: AcceptEntry[] = [];
59
+ const parts = accept.split(",");
60
+ for (let i = 0; i < parts.length; i++) {
61
+ const part = parts[i]!;
62
+ const segments = part.split(";");
63
+ const mime = segments[0]!.trim().toLowerCase();
64
+ if (!mime) continue;
65
+ let q = 1.0;
66
+ for (let j = 1; j < segments.length; j++) {
67
+ const param = segments[j]!.trim();
68
+ if (param.startsWith("q=")) {
69
+ q = Math.max(0, Math.min(1, Number(param.slice(2)) || 0));
70
+ }
71
+ }
72
+ entries.push({ mime, q, order: i });
73
+ }
74
+ // Sort: highest q first, then lowest client order first (stable)
75
+ entries.sort((a, b) => b.q - a.q || a.order - b.order);
76
+ return entries;
77
+ }
78
+
79
+ // Sentinel response type for RSC routes in negotiation candidates
80
+ export const RSC_RESPONSE_TYPE = "__rsc__";
81
+
82
+ /**
83
+ * Pick the best negotiate variant by walking the client's sorted Accept list.
84
+ * For each accepted MIME type (in q-value/order priority), check if any
85
+ * candidate serves that type. Wildcards match the first candidate.
86
+ * Falls back to the first candidate if nothing matches.
87
+ */
88
+ export function pickNegotiateVariant(
89
+ acceptEntries: AcceptEntry[],
90
+ candidates: Array<{ routeKey: string; responseType: string }>,
91
+ ): { routeKey: string; responseType: string } {
92
+ // Build a MIME -> candidate lookup for O(1) matching
93
+ const byCandidateMime = new Map<
94
+ string,
95
+ { routeKey: string; responseType: string }
96
+ >();
97
+ for (const c of candidates) {
98
+ const mime =
99
+ c.responseType === RSC_RESPONSE_TYPE
100
+ ? "text/html"
101
+ : RESPONSE_TYPE_MIME[c.responseType];
102
+ if (mime && !byCandidateMime.has(mime)) {
103
+ byCandidateMime.set(mime, c);
104
+ }
105
+ }
106
+
107
+ for (const entry of acceptEntries) {
108
+ if (entry.q === 0) continue;
109
+ // Wildcard matches first candidate
110
+ if (entry.mime === "*/*") return candidates[0]!;
111
+ // Type wildcard (e.g. "text/*") -- match first candidate with that type
112
+ if (entry.mime.endsWith("/*")) {
113
+ const typePrefix = entry.mime.slice(0, entry.mime.indexOf("/"));
114
+ for (const [mime, candidate] of byCandidateMime) {
115
+ if (mime.startsWith(typePrefix + "/")) return candidate;
116
+ }
117
+ continue;
118
+ }
119
+ const match = byCandidateMime.get(entry.mime);
120
+ if (match) return match;
121
+ }
122
+ // No match -- use first candidate as default
123
+ return candidates[0]!;
124
+ }
125
+
126
+ /**
127
+ * Result of content negotiation for a route with negotiate variants.
128
+ */
129
+ export interface NegotiationResult {
130
+ /** The winning response type */
131
+ responseType: string;
132
+ /** Handler function for the winning variant */
133
+ handler: Function;
134
+ /** Manifest entry for the winning variant (may differ from primary) */
135
+ manifestEntry: EntryData;
136
+ /** Route middleware for the winning variant */
137
+ routeMiddleware: CollectedMiddleware[];
138
+ /** Always true — negotiation occurred */
139
+ negotiated: true;
140
+ }
141
+
142
+ /**
143
+ * Perform content negotiation for a route with negotiate variants.
144
+ *
145
+ * Returns a NegotiationResult when a response route wins negotiation.
146
+ * Returns null when RSC wins or no negotiation is needed.
147
+ *
148
+ * Shared by previewMatch and classifyRequest to avoid duplicating
149
+ * the candidate-building and variant-loading logic.
150
+ */
151
+ export async function negotiateRoute(
152
+ request: Request,
153
+ pathname: string,
154
+ snapshot: RouteSnapshot,
155
+ ): Promise<NegotiationResult | null> {
156
+ const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
157
+ if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
158
+ return null;
159
+ }
160
+
161
+ const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
162
+
163
+ // Build candidate list preserving definition order.
164
+ const variants = matched.negotiateVariants;
165
+ let candidates: Array<{ routeKey: string; responseType: string }>;
166
+ if (responseType) {
167
+ candidates = [...variants, { routeKey: matched.routeKey, responseType }];
168
+ } else {
169
+ const rscCandidate = {
170
+ routeKey: matched.routeKey,
171
+ responseType: RSC_RESPONSE_TYPE,
172
+ };
173
+ candidates = matched.rscFirst
174
+ ? [rscCandidate, ...variants]
175
+ : [...variants, rscCandidate];
176
+ }
177
+
178
+ const variant = pickNegotiateVariant(acceptEntries, candidates);
179
+
180
+ // RSC won negotiation
181
+ if (variant.responseType === RSC_RESPONSE_TYPE) {
182
+ return null;
183
+ }
184
+
185
+ // Primary response-type won — use existing manifest entry and middleware
186
+ if (responseType && variant.routeKey === matched.routeKey) {
187
+ return {
188
+ responseType,
189
+ handler: manifestEntry.handler as Function,
190
+ manifestEntry,
191
+ routeMiddleware,
192
+ negotiated: true,
193
+ };
194
+ }
195
+
196
+ // Different variant won — load its manifest entry
197
+ const negotiateEntry = await loadManifest(
198
+ matched.entry,
199
+ variant.routeKey,
200
+ pathname,
201
+ undefined,
202
+ false,
203
+ );
204
+ const variantMiddleware = collectRouteMiddleware(
205
+ traverseBack(negotiateEntry),
206
+ matched.params,
207
+ );
208
+ return {
209
+ responseType: variant.responseType,
210
+ handler: negotiateEntry.handler as Function,
211
+ manifestEntry: negotiateEntry,
212
+ routeMiddleware: variantMiddleware,
213
+ negotiated: true,
214
+ };
215
+ }
@@ -0,0 +1,72 @@
1
+ import { type EntryData, getContext } from "../server/context";
2
+ import { serializeManifest, type SerializedManifest } from "../debug.js";
3
+ import { createRouteHelpers } from "../route-definition.js";
4
+ import MapRootLayout from "../server/root-layout.js";
5
+ import type { RouteEntry, TrailingSlashMode } from "../types";
6
+
7
+ /**
8
+ * Build a serialized manifest from all route entries for debug inspection.
9
+ * Used by the `debugManifest()` method on the router instance.
10
+ */
11
+ export async function buildDebugManifest<TEnv = any>(
12
+ routesEntries: RouteEntry<TEnv>[],
13
+ ): Promise<SerializedManifest> {
14
+ const manifest = new Map<string, EntryData>();
15
+
16
+ for (const entry of routesEntries) {
17
+ const Store = {
18
+ manifest,
19
+ namespace: `debug.M${entry.mountIndex}`,
20
+ parent: null as EntryData | null,
21
+ counters: {} as Record<string, number>,
22
+ mountIndex: entry.mountIndex,
23
+ patterns: new Map<string, string>(),
24
+ trailingSlash: new Map<string, TrailingSlashMode>(),
25
+ };
26
+
27
+ await getContext().runWithStore(
28
+ Store,
29
+ `debug.M${entry.mountIndex}`,
30
+ null,
31
+ async () => {
32
+ const helpers = createRouteHelpers();
33
+
34
+ // Wrap handler execution in root layout (same as loadManifest)
35
+ let promiseResult: Promise<any> | null = null;
36
+ helpers.layout(MapRootLayout, () => {
37
+ const result = entry.handler();
38
+ if (result instanceof Promise) {
39
+ promiseResult = result;
40
+ return [];
41
+ }
42
+ return result;
43
+ });
44
+
45
+ if (promiseResult !== null) {
46
+ const load = await (promiseResult as Promise<any>);
47
+ if (load && typeof load === "object" && "default" in load) {
48
+ // Promise<{ default: fn }> — e.g. dynamic import
49
+ if (typeof load.default !== "function") {
50
+ throw new Error(
51
+ `[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
52
+ `got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
53
+ );
54
+ }
55
+ load.default(helpers);
56
+ } else if (typeof load === "function") {
57
+ // Promise<fn>
58
+ load(helpers);
59
+ } else {
60
+ // Reject unsupported async handler results (same policy as manifest.ts)
61
+ throw new Error(
62
+ `[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
63
+ `Lazy route handlers must resolve to a function or { default: fn }.`,
64
+ );
65
+ }
66
+ }
67
+ },
68
+ );
69
+ }
70
+
71
+ return serializeManifest(manifest);
72
+ }
@@ -60,7 +60,7 @@ export function invokeOnError<TEnv = any>(
60
60
  error: unknown,
61
61
  phase: ErrorPhase,
62
62
  context: InvokeOnErrorContext<TEnv>,
63
- logPrefix: string = "Router"
63
+ logPrefix: string = "Router",
64
64
  ): void {
65
65
  if (!onError) return;
66
66
 
@@ -74,8 +74,8 @@ export function invokeOnError<TEnv = any>(
74
74
  phase,
75
75
  request: context.request,
76
76
  url: context.url,
77
- pathname: context.url.pathname,
78
- method: context.request.method,
77
+ pathname: context.url?.pathname,
78
+ method: context.request?.method,
79
79
  routeKey: context.routeKey,
80
80
  params: context.params,
81
81
  segmentId: context.segmentId,
@@ -112,7 +112,7 @@ export function invokeOnError<TEnv = any>(
112
112
  */
113
113
  export function findNearestErrorBoundary(
114
114
  entry: EntryData | null,
115
- defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler
115
+ defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler,
116
116
  ): ReactNode | ErrorBoundaryHandler | null {
117
117
  let current: EntryData | null = entry;
118
118
 
@@ -148,7 +148,7 @@ export function findNearestErrorBoundary(
148
148
  */
149
149
  export function findNearestNotFoundBoundary(
150
150
  entry: EntryData | null,
151
- defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler
151
+ defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler,
152
152
  ): ReactNode | NotFoundBoundaryHandler | null {
153
153
  let current: EntryData | null = entry;
154
154
 
@@ -172,7 +172,7 @@ export function findNearestNotFoundBoundary(
172
172
  export function createErrorInfo(
173
173
  error: unknown,
174
174
  segmentId: string,
175
- segmentType: ErrorInfo["segmentType"]
175
+ segmentType: ErrorInfo["segmentType"],
176
176
  ): ErrorInfo {
177
177
  const isDev = process.env.NODE_ENV !== "production";
178
178
 
@@ -205,7 +205,7 @@ export function createErrorSegment(
205
205
  errorInfo: ErrorInfo,
206
206
  fallback: ReactNode | ErrorBoundaryHandler,
207
207
  entry: EntryData,
208
- params: Record<string, string>
208
+ params: Record<string, string>,
209
209
  ): ResolvedSegment {
210
210
  // Determine the component to render
211
211
  let component: ReactNode;
@@ -241,7 +241,7 @@ export function createNotFoundInfo(
241
241
  error: { message: string },
242
242
  segmentId: string,
243
243
  segmentType: NotFoundInfo["segmentType"],
244
- pathname?: string
244
+ pathname?: string,
245
245
  ): NotFoundInfo {
246
246
  return {
247
247
  message: error.message,
@@ -259,7 +259,7 @@ export function createNotFoundSegment(
259
259
  notFoundInfo: NotFoundInfo,
260
260
  fallback: ReactNode | NotFoundBoundaryHandler,
261
261
  entry: EntryData,
262
- params: Record<string, string>
262
+ params: Record<string, string>,
263
263
  ): ResolvedSegment {
264
264
  // Determine the component to render
265
265
  let component: ReactNode;
@@ -0,0 +1,160 @@
1
+ import { tryTrieMatch } from "./trie-matching.js";
2
+ import { getRouteTrie, getRouterTrie } from "../route-map-builder.js";
3
+ import {
4
+ findMatch as findRouteMatch,
5
+ isLazyEvaluationNeeded,
6
+ type RouteMatchResult,
7
+ } from "./pattern-matching.js";
8
+ import type { MetricsStore } from "../server/context";
9
+ import type { RouteEntry } from "../types";
10
+
11
+ export interface FindMatchDeps<TEnv = any> {
12
+ routesEntries: RouteEntry<TEnv>[];
13
+ evaluateLazyEntry: (entry: RouteEntry<TEnv>) => void;
14
+ routerId: string;
15
+ }
16
+
17
+ /**
18
+ * Create a findMatch function bound to router state.
19
+ * Includes single-entry cache to avoid redundant matching within the same request.
20
+ */
21
+ export function createFindMatch<TEnv = any>(
22
+ deps: FindMatchDeps<TEnv>,
23
+ ): (pathname: string, ms?: MetricsStore) => RouteMatchResult<TEnv> | null {
24
+ // Single-entry cache for findMatch to avoid redundant matching within the same request.
25
+ // previewMatch and match both call findMatch with the same pathname — this ensures
26
+ // the route matching work (which may check thousands of routes) only happens once.
27
+ let lastFindMatchPathname: string | null = null;
28
+ let lastFindMatchResult: RouteMatchResult<TEnv> | null = null;
29
+
30
+ // Wrapper for findMatch that uses routesEntries
31
+ // Handles lazy evaluation by evaluating lazy entries on first match.
32
+ // Phase 1: try O(path_length) trie match.
33
+ // Phase 2: fall back to regex iteration.
34
+ return function findMatch(
35
+ pathname: string,
36
+ ms?: MetricsStore,
37
+ ): RouteMatchResult<TEnv> | null {
38
+ // Return cached result if same pathname (avoids double-match per request)
39
+ if (lastFindMatchPathname === pathname) {
40
+ return lastFindMatchResult;
41
+ }
42
+
43
+ // Helper to push sub-metrics
44
+ const pushMetric = ms
45
+ ? (label: string, start: number) => {
46
+ ms.metrics.push({
47
+ label,
48
+ duration: performance.now() - start,
49
+ startTime: start - ms.requestStart,
50
+ });
51
+ }
52
+ : undefined;
53
+
54
+ // Phase 1: Try trie match (O(path_length))
55
+ // Only use the per-router trie. The global trie merges routes from ALL
56
+ // routers and must not be used — in multi-router setups (host routing)
57
+ // overlapping paths like "/" would match the wrong app's route.
58
+ const routeTrie = getRouterTrie(deps.routerId);
59
+ if (routeTrie) {
60
+ const trieStart = performance.now();
61
+ const trieResult = tryTrieMatch(routeTrie, pathname);
62
+ pushMetric?.("match:trie", trieStart);
63
+
64
+ if (trieResult) {
65
+ // Find the RouteEntry that contains this route.
66
+ // Multiple entries can share the same staticPrefix (e.g., several
67
+ // include("/", patterns) calls all produce staticPrefix=""). Evaluate
68
+ // each candidate and pick the one whose routes include the matched key.
69
+ const entryStart = performance.now();
70
+ let entry: RouteEntry<TEnv> | undefined;
71
+ let fallbackEntry: RouteEntry<TEnv> | undefined;
72
+
73
+ for (const e of deps.routesEntries) {
74
+ if (e.staticPrefix !== trieResult.sp) continue;
75
+ if (!fallbackEntry) fallbackEntry = e;
76
+ deps.evaluateLazyEntry(e);
77
+ if (
78
+ e.routes &&
79
+ trieResult.routeKey in (e.routes as Record<string, unknown>)
80
+ ) {
81
+ entry = e;
82
+ break;
83
+ }
84
+ }
85
+
86
+ // If no entry had the route in its routes map, use the first matching
87
+ // entry as fallback (handles main entry with inline routes not yet
88
+ // reflected in its routes object).
89
+ if (!entry) entry = fallbackEntry;
90
+
91
+ // If entry not found (nested include not yet discovered), evaluate parent
92
+ if (!entry) {
93
+ const parent = deps.routesEntries.find(
94
+ (e) =>
95
+ trieResult.sp.startsWith(e.staticPrefix) &&
96
+ e.staticPrefix !== trieResult.sp,
97
+ );
98
+ if (parent) {
99
+ const lazyStart = performance.now();
100
+ deps.evaluateLazyEntry(parent);
101
+ pushMetric?.("match:lazy-eval", lazyStart);
102
+ }
103
+ entry = deps.routesEntries.find(
104
+ (e) => e.staticPrefix === trieResult.sp,
105
+ );
106
+ }
107
+ pushMetric?.("match:entry-resolve", entryStart);
108
+
109
+ if (entry) {
110
+ lastFindMatchPathname = pathname;
111
+ lastFindMatchResult = {
112
+ entry,
113
+ routeKey: trieResult.routeKey,
114
+ params: trieResult.params,
115
+ optionalParams: new Set(trieResult.optionalParams || []),
116
+ redirectTo: trieResult.redirectTo,
117
+ ancestry: trieResult.ancestry,
118
+ ...(trieResult.pr ? { pr: true } : {}),
119
+ ...(trieResult.pt ? { pt: true } : {}),
120
+ ...(trieResult.responseType
121
+ ? { responseType: trieResult.responseType }
122
+ : {}),
123
+ ...(trieResult.negotiateVariants
124
+ ? { negotiateVariants: trieResult.negotiateVariants }
125
+ : {}),
126
+ ...(trieResult.rscFirst ? { rscFirst: true } : {}),
127
+ };
128
+ return lastFindMatchResult;
129
+ }
130
+ }
131
+ }
132
+
133
+ // Phase 2: Fall back to existing matching (regex iteration)
134
+ const regexStart = performance.now();
135
+ let result = findRouteMatch(pathname, deps.routesEntries);
136
+
137
+ // If we hit a lazy entry that needs evaluation, evaluate and retry.
138
+ // Cap iterations to prevent infinite loops from pathological nesting.
139
+ const MAX_LAZY_ITERATIONS = 100;
140
+ let iterations = 0;
141
+ while (isLazyEvaluationNeeded(result)) {
142
+ if (++iterations > MAX_LAZY_ITERATIONS) {
143
+ console.error(
144
+ `[@rangojs/router] Exceeded ${MAX_LAZY_ITERATIONS} lazy evaluation iterations ` +
145
+ `for pathname "${pathname}". This likely indicates circular lazy includes.`,
146
+ );
147
+ lastFindMatchPathname = pathname;
148
+ lastFindMatchResult = null;
149
+ return null;
150
+ }
151
+ deps.evaluateLazyEntry(result.lazyEntry);
152
+ result = findRouteMatch(pathname, deps.routesEntries);
153
+ }
154
+ pushMetric?.("match:regex-fallback", regexStart);
155
+
156
+ lastFindMatchPathname = pathname;
157
+ lastFindMatchResult = result;
158
+ return result;
159
+ };
160
+ }