@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -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/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Content Negotiation Utilities
3
+ *
4
+ * Pure functions for HTTP Accept header parsing and response type matching.
5
+ * Used by createRouter's previewMatch for content negotiation between
6
+ * RSC routes and response routes (JSON, text, image, stream, etc.).
7
+ */
8
+
9
+ // Response type -> MIME type used for Accept header matching
10
+ export const RESPONSE_TYPE_MIME: Record<string, string> = {
11
+ json: "application/json",
12
+ text: "text/plain",
13
+ xml: "application/xml",
14
+ html: "text/html",
15
+ md: "text/markdown",
16
+ };
17
+
18
+ // Reverse lookup: MIME type -> response type tag (e.g. "text/html" -> "html")
19
+ export const MIME_RESPONSE_TYPE: Record<string, string> = Object.fromEntries(
20
+ Object.entries(RESPONSE_TYPE_MIME).map(([tag, mime]) => [mime, tag]),
21
+ );
22
+
23
+ export type NamedRouteEntry =
24
+ | string
25
+ | { path: string; search?: Record<string, string> };
26
+
27
+ export function flattenNamedRoutes(
28
+ routeNames?: Record<string, NamedRouteEntry>,
29
+ ): Record<string, string> {
30
+ if (!routeNames) return {};
31
+ const flattened: Record<string, string> = {};
32
+ for (const [name, entry] of Object.entries(routeNames)) {
33
+ flattened[name] = typeof entry === "string" ? entry : entry.path;
34
+ }
35
+ return flattened;
36
+ }
37
+
38
+ export interface AcceptEntry {
39
+ mime: string;
40
+ q: number;
41
+ order: number;
42
+ }
43
+
44
+ /**
45
+ * Parse an Accept header into a sorted array of MIME entries.
46
+ * Respects q-values (default 1.0) and uses client order as tiebreaker
47
+ * when q-values are equal (matching Express/Hono behavior).
48
+ */
49
+ export function parseAcceptTypes(accept: string): AcceptEntry[] {
50
+ const entries: AcceptEntry[] = [];
51
+ const parts = accept.split(",");
52
+ for (let i = 0; i < parts.length; i++) {
53
+ const part = parts[i]!;
54
+ const segments = part.split(";");
55
+ const mime = segments[0]!.trim().toLowerCase();
56
+ if (!mime) continue;
57
+ let q = 1.0;
58
+ for (let j = 1; j < segments.length; j++) {
59
+ const param = segments[j]!.trim();
60
+ if (param.startsWith("q=")) {
61
+ q = Math.max(0, Math.min(1, Number(param.slice(2)) || 0));
62
+ }
63
+ }
64
+ entries.push({ mime, q, order: i });
65
+ }
66
+ // Sort: highest q first, then lowest client order first (stable)
67
+ entries.sort((a, b) => b.q - a.q || a.order - b.order);
68
+ return entries;
69
+ }
70
+
71
+ // Sentinel response type for RSC routes in negotiation candidates
72
+ export const RSC_RESPONSE_TYPE = "__rsc__";
73
+
74
+ /**
75
+ * Pick the best negotiate variant by walking the client's sorted Accept list.
76
+ * For each accepted MIME type (in q-value/order priority), check if any
77
+ * candidate serves that type. Wildcards match the first candidate.
78
+ * Falls back to the first candidate if nothing matches.
79
+ */
80
+ export function pickNegotiateVariant(
81
+ acceptEntries: AcceptEntry[],
82
+ candidates: Array<{ routeKey: string; responseType: string }>,
83
+ ): { routeKey: string; responseType: string } {
84
+ // Build a MIME -> candidate lookup for O(1) matching
85
+ const byCandidateMime = new Map<
86
+ string,
87
+ { routeKey: string; responseType: string }
88
+ >();
89
+ for (const c of candidates) {
90
+ const mime =
91
+ c.responseType === RSC_RESPONSE_TYPE
92
+ ? "text/html"
93
+ : RESPONSE_TYPE_MIME[c.responseType];
94
+ if (mime && !byCandidateMime.has(mime)) {
95
+ byCandidateMime.set(mime, c);
96
+ }
97
+ }
98
+
99
+ for (const entry of acceptEntries) {
100
+ if (entry.q === 0) continue;
101
+ // Wildcard matches first candidate
102
+ if (entry.mime === "*/*") return candidates[0]!;
103
+ // Type wildcard (e.g. "text/*") -- match first candidate with that type
104
+ if (entry.mime.endsWith("/*")) {
105
+ const typePrefix = entry.mime.slice(0, entry.mime.indexOf("/"));
106
+ for (const [mime, candidate] of byCandidateMime) {
107
+ if (mime.startsWith(typePrefix + "/")) return candidate;
108
+ }
109
+ continue;
110
+ }
111
+ const match = byCandidateMime.get(entry.mime);
112
+ if (match) return match;
113
+ }
114
+ // No match -- use first candidate as default
115
+ return candidates[0]!;
116
+ }
@@ -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
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Router Error Handling Utilities
3
+ *
4
+ * Error boundary and not-found boundary handling for RSC Router.
5
+ * Also includes the shared invokeOnError utility for error callback invocation.
6
+ */
7
+
8
+ import type { ReactNode } from "react";
9
+ import type { EntryData } from "../server/context";
10
+ import type {
11
+ ResolvedSegment,
12
+ ErrorInfo,
13
+ ErrorBoundaryHandler,
14
+ ErrorBoundaryFallbackProps,
15
+ NotFoundInfo,
16
+ NotFoundBoundaryHandler,
17
+ NotFoundBoundaryFallbackProps,
18
+ ErrorPhase,
19
+ OnErrorCallback,
20
+ OnErrorContext,
21
+ } from "../types";
22
+
23
+ /**
24
+ * Context required to invoke the onError callback.
25
+ * This is a subset of OnErrorContext that callers must provide.
26
+ */
27
+ export interface InvokeOnErrorContext<TEnv = any> {
28
+ request: Request;
29
+ url: URL;
30
+ routeKey?: string;
31
+ params?: Record<string, string>;
32
+ segmentId?: string;
33
+ segmentType?: "layout" | "route" | "parallel" | "loader" | "middleware";
34
+ loaderName?: string;
35
+ middlewareId?: string;
36
+ actionId?: string;
37
+ env?: TEnv;
38
+ isPartial?: boolean;
39
+ handledByBoundary?: boolean;
40
+ metadata?: Record<string, unknown>;
41
+ /** Request start time from performance.now() for duration calculation */
42
+ requestStartTime?: number;
43
+ }
44
+
45
+ /**
46
+ * Invoke the onError callback with comprehensive context.
47
+ * Catches any errors in the callback itself to prevent masking the original error.
48
+ *
49
+ * This is a shared utility used by both the router and RSC handler to ensure
50
+ * consistent error callback behavior across the codebase.
51
+ *
52
+ * @param onError - The onError callback to invoke (may be undefined)
53
+ * @param error - The error that occurred
54
+ * @param phase - The phase where the error occurred
55
+ * @param context - Additional context about the error
56
+ * @param logPrefix - Prefix for console.error messages (e.g., "Router" or "RSC")
57
+ */
58
+ export function invokeOnError<TEnv = any>(
59
+ onError: OnErrorCallback<TEnv> | undefined,
60
+ error: unknown,
61
+ phase: ErrorPhase,
62
+ context: InvokeOnErrorContext<TEnv>,
63
+ logPrefix: string = "Router",
64
+ ): void {
65
+ if (!onError) return;
66
+
67
+ const errorObj = error instanceof Error ? error : new Error(String(error));
68
+ const duration = context.requestStartTime
69
+ ? performance.now() - context.requestStartTime
70
+ : undefined;
71
+
72
+ const errorContext: OnErrorContext<TEnv> = {
73
+ error: errorObj,
74
+ phase,
75
+ request: context.request,
76
+ url: context.url,
77
+ pathname: context.url?.pathname,
78
+ method: context.request?.method,
79
+ routeKey: context.routeKey,
80
+ params: context.params,
81
+ segmentId: context.segmentId,
82
+ segmentType: context.segmentType,
83
+ loaderName: context.loaderName,
84
+ middlewareId: context.middlewareId,
85
+ actionId: context.actionId,
86
+ env: context.env,
87
+ duration,
88
+ isPartial: context.isPartial,
89
+ handledByBoundary: context.handledByBoundary,
90
+ stack: errorObj.stack,
91
+ metadata: context.metadata,
92
+ };
93
+
94
+ try {
95
+ const result = onError(errorContext);
96
+ // If onError returns a promise, catch any rejections
97
+ if (result instanceof Promise) {
98
+ result.catch((callbackError) => {
99
+ console.error(`[${logPrefix}.onError] Callback error:`, callbackError);
100
+ });
101
+ }
102
+ } catch (callbackError) {
103
+ // Log but don't throw - we don't want callback errors to mask the original error
104
+ console.error(`[${logPrefix}.onError] Callback error:`, callbackError);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Find the nearest error boundary by walking up the entry chain
110
+ * Also checks sibling layouts (orphan layouts) for error boundaries
111
+ * Returns the first fallback found, or the default error boundary if configured
112
+ */
113
+ export function findNearestErrorBoundary(
114
+ entry: EntryData | null,
115
+ defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler,
116
+ ): ReactNode | ErrorBoundaryHandler | null {
117
+ let current: EntryData | null = entry;
118
+
119
+ while (current) {
120
+ // Check if this entry has error boundaries defined
121
+ if (current.errorBoundary && current.errorBoundary.length > 0) {
122
+ // Return the last error boundary (most recently defined takes precedence)
123
+ return current.errorBoundary[current.errorBoundary.length - 1];
124
+ }
125
+
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
+ if (current.layout && current.layout.length > 0) {
131
+ for (const orphan of current.layout) {
132
+ if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
133
+ return orphan.errorBoundary[orphan.errorBoundary.length - 1];
134
+ }
135
+ }
136
+ }
137
+
138
+ current = current.parent;
139
+ }
140
+
141
+ // Return default error boundary if configured
142
+ return defaultErrorBoundary || null;
143
+ }
144
+
145
+ /**
146
+ * Find the nearest notFound boundary by walking up the entry chain
147
+ * Returns the first fallback found, or the default notFound boundary if configured
148
+ */
149
+ export function findNearestNotFoundBoundary(
150
+ entry: EntryData | null,
151
+ defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler,
152
+ ): ReactNode | NotFoundBoundaryHandler | null {
153
+ let current: EntryData | null = entry;
154
+
155
+ while (current) {
156
+ // Check if this entry has notFound boundaries defined
157
+ if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
158
+ // Return the last notFound boundary (most recently defined takes precedence)
159
+ return current.notFoundBoundary[current.notFoundBoundary.length - 1];
160
+ }
161
+ current = current.parent;
162
+ }
163
+
164
+ // Return default notFound boundary if configured
165
+ return defaultNotFoundBoundary || null;
166
+ }
167
+
168
+ /**
169
+ * Create ErrorInfo from an error object
170
+ * Sanitizes error details in production
171
+ */
172
+ export function createErrorInfo(
173
+ error: unknown,
174
+ segmentId: string,
175
+ segmentType: ErrorInfo["segmentType"],
176
+ ): ErrorInfo {
177
+ const isDev = process.env.NODE_ENV !== "production";
178
+
179
+ if (error instanceof Error) {
180
+ return {
181
+ message: isDev ? error.message : "An error occurred",
182
+ name: error.name,
183
+ code: (error as any).code,
184
+ stack: isDev ? error.stack : undefined,
185
+ cause: isDev ? error.cause : undefined,
186
+ segmentId,
187
+ segmentType,
188
+ };
189
+ }
190
+
191
+ // Non-Error thrown
192
+ return {
193
+ message: isDev ? String(error) : "An error occurred",
194
+ name: "Error",
195
+ segmentId,
196
+ segmentType,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Create an error segment with the fallback component
202
+ * Renders the fallback with error info and reset function
203
+ */
204
+ export function createErrorSegment(
205
+ errorInfo: ErrorInfo,
206
+ fallback: ReactNode | ErrorBoundaryHandler,
207
+ entry: EntryData,
208
+ params: Record<string, string>,
209
+ ): ResolvedSegment {
210
+ // Determine the component to render
211
+ let component: ReactNode;
212
+
213
+ if (typeof fallback === "function") {
214
+ // ErrorBoundaryHandler - call with error info
215
+ const props: ErrorBoundaryFallbackProps = {
216
+ error: errorInfo,
217
+ };
218
+ component = fallback(props);
219
+ } else {
220
+ // Static ReactNode fallback
221
+ component = fallback;
222
+ }
223
+
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
+ return {
227
+ id: entry.shortCode,
228
+ namespace: entry.id,
229
+ type: "error",
230
+ index: 0,
231
+ component,
232
+ params,
233
+ error: errorInfo,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Create NotFoundInfo from a DataNotFoundError
239
+ */
240
+ export function createNotFoundInfo(
241
+ error: { message: string },
242
+ segmentId: string,
243
+ segmentType: NotFoundInfo["segmentType"],
244
+ pathname?: string,
245
+ ): NotFoundInfo {
246
+ return {
247
+ message: error.message,
248
+ segmentId,
249
+ segmentType,
250
+ pathname,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Create a notFound segment with the fallback component
256
+ * Renders the fallback with not found info
257
+ */
258
+ export function createNotFoundSegment(
259
+ notFoundInfo: NotFoundInfo,
260
+ fallback: ReactNode | NotFoundBoundaryHandler,
261
+ entry: EntryData,
262
+ params: Record<string, string>,
263
+ ): ResolvedSegment {
264
+ // Determine the component to render
265
+ let component: ReactNode;
266
+
267
+ if (typeof fallback === "function") {
268
+ // NotFoundBoundaryHandler - call with props
269
+ const props: NotFoundBoundaryFallbackProps = {
270
+ notFound: notFoundInfo,
271
+ };
272
+ component = fallback(props);
273
+ } else {
274
+ // Static ReactNode fallback
275
+ component = fallback;
276
+ }
277
+
278
+ return {
279
+ id: `${entry.shortCode}.notFound`,
280
+ namespace: entry.id,
281
+ type: "notFound",
282
+ index: 0,
283
+ component,
284
+ params,
285
+ notFoundInfo,
286
+ };
287
+ }
@@ -0,0 +1,158 @@
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
+ // Prefer per-router trie (isolated) over global trie (merged).
56
+ const routeTrie = getRouterTrie(deps.routerId) ?? getRouteTrie();
57
+ if (routeTrie) {
58
+ const trieStart = performance.now();
59
+ const trieResult = tryTrieMatch(routeTrie, pathname);
60
+ pushMetric?.("match:trie", trieStart);
61
+
62
+ if (trieResult) {
63
+ // Find the RouteEntry that contains this route.
64
+ // Multiple entries can share the same staticPrefix (e.g., several
65
+ // include("/", patterns) calls all produce staticPrefix=""). Evaluate
66
+ // each candidate and pick the one whose routes include the matched key.
67
+ const entryStart = performance.now();
68
+ let entry: RouteEntry<TEnv> | undefined;
69
+ let fallbackEntry: RouteEntry<TEnv> | undefined;
70
+
71
+ for (const e of deps.routesEntries) {
72
+ if (e.staticPrefix !== trieResult.sp) continue;
73
+ if (!fallbackEntry) fallbackEntry = e;
74
+ deps.evaluateLazyEntry(e);
75
+ if (
76
+ e.routes &&
77
+ trieResult.routeKey in (e.routes as Record<string, unknown>)
78
+ ) {
79
+ entry = e;
80
+ break;
81
+ }
82
+ }
83
+
84
+ // If no entry had the route in its routes map, use the first matching
85
+ // entry as fallback (handles main entry with inline routes not yet
86
+ // reflected in its routes object).
87
+ if (!entry) entry = fallbackEntry;
88
+
89
+ // If entry not found (nested include not yet discovered), evaluate parent
90
+ if (!entry) {
91
+ const parent = deps.routesEntries.find(
92
+ (e) =>
93
+ trieResult.sp.startsWith(e.staticPrefix) &&
94
+ e.staticPrefix !== trieResult.sp,
95
+ );
96
+ if (parent) {
97
+ const lazyStart = performance.now();
98
+ deps.evaluateLazyEntry(parent);
99
+ pushMetric?.("match:lazy-eval", lazyStart);
100
+ }
101
+ entry = deps.routesEntries.find(
102
+ (e) => e.staticPrefix === trieResult.sp,
103
+ );
104
+ }
105
+ pushMetric?.("match:entry-resolve", entryStart);
106
+
107
+ if (entry) {
108
+ lastFindMatchPathname = pathname;
109
+ lastFindMatchResult = {
110
+ entry,
111
+ routeKey: trieResult.routeKey,
112
+ params: trieResult.params,
113
+ optionalParams: new Set(trieResult.optionalParams || []),
114
+ redirectTo: trieResult.redirectTo,
115
+ ancestry: trieResult.ancestry,
116
+ ...(trieResult.pr ? { pr: true } : {}),
117
+ ...(trieResult.pt ? { pt: true } : {}),
118
+ ...(trieResult.responseType
119
+ ? { responseType: trieResult.responseType }
120
+ : {}),
121
+ ...(trieResult.negotiateVariants
122
+ ? { negotiateVariants: trieResult.negotiateVariants }
123
+ : {}),
124
+ ...(trieResult.rscFirst ? { rscFirst: true } : {}),
125
+ };
126
+ return lastFindMatchResult;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Phase 2: Fall back to existing matching (regex iteration)
132
+ const regexStart = performance.now();
133
+ let result = findRouteMatch(pathname, deps.routesEntries);
134
+
135
+ // If we hit a lazy entry that needs evaluation, evaluate and retry.
136
+ // Cap iterations to prevent infinite loops from pathological nesting.
137
+ const MAX_LAZY_ITERATIONS = 100;
138
+ let iterations = 0;
139
+ while (isLazyEvaluationNeeded(result)) {
140
+ if (++iterations > MAX_LAZY_ITERATIONS) {
141
+ console.error(
142
+ `[@rangojs/router] Exceeded ${MAX_LAZY_ITERATIONS} lazy evaluation iterations ` +
143
+ `for pathname "${pathname}". This likely indicates circular lazy includes.`,
144
+ );
145
+ lastFindMatchPathname = pathname;
146
+ lastFindMatchResult = null;
147
+ return null;
148
+ }
149
+ deps.evaluateLazyEntry(result.lazyEntry);
150
+ result = findRouteMatch(pathname, deps.routesEntries);
151
+ }
152
+ pushMetric?.("match:regex-fallback", regexStart);
153
+
154
+ lastFindMatchPathname = pathname;
155
+ lastFindMatchResult = result;
156
+ return result;
157
+ };
158
+ }