@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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 (307) 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 +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  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/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  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 +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +135 -35
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +748 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1379 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +151 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Navigation Snapshot
3
+ *
4
+ * Pure data type representing the navigation-specific state for partial requests.
5
+ * Consolidates the header parsing, previous-route matching, intercept-context
6
+ * detection, and segment ID filtering that previously lived inline in
7
+ * createMatchContextForPartial (match-api.ts).
8
+ *
9
+ * resolveNavigation() is the factory: given a request + URL + current route key,
10
+ * it returns a NavigationSnapshot (or null if no previous URL).
11
+ */
12
+
13
+ import type { RouteMatchResult } from "./pattern-matching.js";
14
+
15
+ /**
16
+ * Snapshot of navigation state for a partial (navigation/action) request.
17
+ *
18
+ * Contains the "where are we coming from?" data: previous route, intercept
19
+ * source, client segment state, and derived flags.
20
+ */
21
+ export interface NavigationSnapshot {
22
+ /** Previous page URL (from X-RSC-Router-Client-Path or Referer) */
23
+ prevUrl: URL;
24
+ /** Params from the previous route match */
25
+ prevParams: Record<string, string>;
26
+ /** Previous route match result (null if prev URL doesn't match any route) */
27
+ prevMatch: RouteMatchResult | null;
28
+
29
+ /** URL used as intercept context source */
30
+ interceptContextUrl: URL;
31
+ /** Route match for the intercept context URL */
32
+ interceptContextMatch: RouteMatchResult | null;
33
+
34
+ /** Raw segment IDs the client currently has */
35
+ clientSegmentIds: string[];
36
+ /** Set version for O(1) lookup */
37
+ clientSegmentSet: Set<string>;
38
+ /** Segment IDs filtered to remove parallel (.@) and loader (D\d+.) entries */
39
+ filteredSegmentIds: string[];
40
+
41
+ /** Whether client considers its cache stale */
42
+ stale: boolean;
43
+
44
+ /** Whether the intercept context route is the same as the current route */
45
+ isSameRouteNavigation: boolean;
46
+
47
+ /** Effective "from" URL (intercept source URL when present, else prevUrl) */
48
+ effectiveFromUrl: URL;
49
+ /** Effective "from" match (intercept source match when present, else prevMatch) */
50
+ effectiveFromMatch: RouteMatchResult | null;
51
+
52
+ /** Whether an intercept source header was present */
53
+ hasInterceptSource: boolean;
54
+
55
+ /** Whether an HMR request header was present */
56
+ isHmr: boolean;
57
+ }
58
+
59
+ export interface ResolveNavigationDeps {
60
+ findMatch: (pathname: string) => RouteMatchResult | null;
61
+ }
62
+
63
+ /**
64
+ * Resolve navigation state from a partial request.
65
+ *
66
+ * Returns null if no previous URL is available (required for partial navigation).
67
+ *
68
+ * @param request - The incoming HTTP request
69
+ * @param url - Parsed URL of the request
70
+ * @param currentRouteKey - Route key of the current (target) route match
71
+ * @param deps - Dependencies (findMatch)
72
+ */
73
+ export function resolveNavigation(
74
+ request: Request,
75
+ url: URL,
76
+ currentRouteKey: string,
77
+ deps: ResolveNavigationDeps,
78
+ ): NavigationSnapshot | null {
79
+ // Parse client state from RSC request params/headers
80
+ const clientSegmentIds =
81
+ url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
82
+ const stale = url.searchParams.get("_rsc_stale") === "true";
83
+ const previousUrl =
84
+ request.headers.get("X-RSC-Router-Client-Path") ||
85
+ request.headers.get("Referer");
86
+ const interceptSourceUrl = request.headers.get(
87
+ "X-RSC-Router-Intercept-Source",
88
+ );
89
+ const isHmr = !!request.headers.get("X-RSC-HMR");
90
+
91
+ if (!previousUrl) {
92
+ return null;
93
+ }
94
+
95
+ // Parse previous URL
96
+ let prevUrl: URL;
97
+ try {
98
+ prevUrl = new URL(previousUrl, url.origin);
99
+ } catch {
100
+ return null;
101
+ }
102
+
103
+ // Parse intercept context URL
104
+ let interceptContextUrl: URL;
105
+ try {
106
+ interceptContextUrl = interceptSourceUrl
107
+ ? new URL(interceptSourceUrl, url.origin)
108
+ : prevUrl;
109
+ } catch {
110
+ interceptContextUrl = prevUrl;
111
+ }
112
+
113
+ // Match previous and intercept context routes
114
+ const prevMatch = deps.findMatch(prevUrl.pathname);
115
+ const prevParams = prevMatch?.params || {};
116
+ const interceptContextMatch = interceptSourceUrl
117
+ ? deps.findMatch(interceptContextUrl.pathname)
118
+ : prevMatch;
119
+
120
+ // Derived state
121
+ const isSameRouteNavigation = !!(
122
+ interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
123
+ );
124
+
125
+ const hasInterceptSource = !!interceptSourceUrl;
126
+ const effectiveFromUrl = hasInterceptSource ? interceptContextUrl : prevUrl;
127
+ const effectiveFromMatch = hasInterceptSource
128
+ ? interceptContextMatch
129
+ : prevMatch;
130
+
131
+ // Filter segment IDs: remove parallel (.@) and loader (D\d+.) entries
132
+ const filteredSegmentIds = clientSegmentIds.filter((id) => {
133
+ if (id.includes(".@")) return false;
134
+ if (/D\d+\./.test(id)) return false;
135
+ return true;
136
+ });
137
+
138
+ const clientSegmentSet = new Set(clientSegmentIds);
139
+
140
+ return {
141
+ prevUrl,
142
+ prevParams,
143
+ prevMatch,
144
+ interceptContextUrl,
145
+ interceptContextMatch,
146
+ clientSegmentIds,
147
+ clientSegmentSet,
148
+ filteredSegmentIds,
149
+ stale,
150
+ isSameRouteNavigation,
151
+ effectiveFromUrl,
152
+ effectiveFromMatch,
153
+ hasInterceptSource,
154
+ isHmr,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Test helper: create a NavigationSnapshot with sensible defaults and overrides.
160
+ */
161
+ export function createNavigationSnapshot(
162
+ overrides?: Partial<NavigationSnapshot>,
163
+ ): NavigationSnapshot {
164
+ const defaultUrl = new URL("http://localhost/");
165
+ return {
166
+ prevUrl: defaultUrl,
167
+ prevParams: {},
168
+ prevMatch: null,
169
+ interceptContextUrl: defaultUrl,
170
+ interceptContextMatch: null,
171
+ clientSegmentIds: [],
172
+ clientSegmentSet: new Set(),
173
+ filteredSegmentIds: [],
174
+ stale: false,
175
+ isSameRouteNavigation: false,
176
+ effectiveFromUrl: defaultUrl,
177
+ effectiveFromMatch: null,
178
+ hasInterceptSource: false,
179
+ isHmr: false,
180
+ ...overrides,
181
+ };
182
+ }
@@ -6,15 +6,17 @@
6
6
 
7
7
  import type { RouteEntry, TrailingSlashMode } from "../types";
8
8
  import type { EntryData } from "../server/context";
9
+ import { debugLog, isRouterDebugEnabled } from "./logging.js";
9
10
 
10
11
  /**
11
12
  * Parsed segment info
12
13
  */
13
- interface ParsedSegment {
14
+ export interface ParsedSegment {
14
15
  type: "static" | "param" | "wildcard";
15
16
  value: string; // static text, param name, or "*"
16
17
  optional: boolean;
17
18
  constraint?: string[]; // enum values like ["en", "gb"]
19
+ suffix?: string; // literal text after param in same segment (e.g., ".html")
18
20
  }
19
21
 
20
22
  /**
@@ -28,7 +30,7 @@ interface ParsedSegment {
28
30
  * - Optional + Constrained: /:locale(en|gb)?
29
31
  * - Wildcard: /*
30
32
  */
31
- function parsePattern(pattern: string): ParsedSegment[] {
33
+ export function parsePattern(pattern: string): ParsedSegment[] {
32
34
  const segments: ParsedSegment[] = [];
33
35
  // Match: /segment where segment can be:
34
36
  // - static text
@@ -37,11 +39,22 @@ function parsePattern(pattern: string): ParsedSegment[] {
37
39
  // - :param(a|b)
38
40
  // - :param(a|b)?
39
41
  // - *
40
- const segmentRegex = /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?|(\*)|([^/]+))/g;
42
+ const segmentRegex =
43
+ /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
41
44
 
42
45
  let match;
43
46
  while ((match = segmentRegex.exec(pattern)) !== null) {
44
- const [, , paramName, , constraint, optional, wildcard, staticText] = match;
47
+ const [
48
+ ,
49
+ ,
50
+ paramName,
51
+ ,
52
+ constraint,
53
+ optional,
54
+ suffix,
55
+ wildcard,
56
+ staticText,
57
+ ] = match;
45
58
 
46
59
  if (wildcard) {
47
60
  segments.push({ type: "wildcard", value: "*", optional: false });
@@ -51,6 +64,7 @@ function parsePattern(pattern: string): ParsedSegment[] {
51
64
  value: paramName,
52
65
  optional: optional === "?",
53
66
  constraint: constraint ? constraint.split("|") : undefined,
67
+ suffix: suffix || undefined,
54
68
  });
55
69
  } else if (staticText) {
56
70
  segments.push({ type: "static", value: staticText, optional: false });
@@ -60,6 +74,48 @@ function parsePattern(pattern: string): ParsedSegment[] {
60
74
  return segments;
61
75
  }
62
76
 
77
+ /**
78
+ * Compiled pattern result containing regex, param metadata, and trailing slash info.
79
+ */
80
+ export interface CompiledPattern {
81
+ regex: RegExp;
82
+ paramNames: string[];
83
+ optionalParams: Set<string>;
84
+ hasTrailingSlash: boolean;
85
+ }
86
+
87
+ // Module-level cache for compiled patterns. Route patterns are a finite set
88
+ // defined at build time, so this map is bounded by the number of routes.
89
+ const compiledPatternCache = new Map<string, CompiledPattern>();
90
+
91
+ /**
92
+ * Get a compiled pattern from cache or compile and cache it.
93
+ * Avoids O(routes) regex compilations per request in the fallback path.
94
+ */
95
+ export function getCompiledPattern(pattern: string): CompiledPattern {
96
+ let compiled = compiledPatternCache.get(pattern);
97
+ if (compiled) return compiled;
98
+ compiled = compilePattern(pattern);
99
+ compiledPatternCache.set(pattern, compiled);
100
+ return compiled;
101
+ }
102
+
103
+ /**
104
+ * Return the current size of the compiled pattern cache.
105
+ * Exposed for testing.
106
+ */
107
+ export function getPatternCacheSize(): number {
108
+ return compiledPatternCache.size;
109
+ }
110
+
111
+ /**
112
+ * Clear the compiled pattern cache.
113
+ * Exposed for testing.
114
+ */
115
+ export function clearPatternCache(): void {
116
+ compiledPatternCache.clear();
117
+ }
118
+
63
119
  /**
64
120
  * Compile a route pattern to regex
65
121
  *
@@ -77,12 +133,7 @@ function parsePattern(pattern: string): ParsedSegment[] {
77
133
  * compilePattern("/:locale(en|gb)/blog") // matches /en/blog or /gb/blog
78
134
  * compilePattern("/:locale(en|gb)?/blog") // matches /blog, /en/blog, or /gb/blog
79
135
  */
80
- export function compilePattern(pattern: string): {
81
- regex: RegExp;
82
- paramNames: string[];
83
- optionalParams: Set<string>;
84
- hasTrailingSlash: boolean;
85
- } {
136
+ export function compilePattern(pattern: string): CompiledPattern {
86
137
  // Detect if pattern has trailing slash (but not just "/")
87
138
  const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
88
139
  // Remove trailing slash for parsing (we'll add it back to regex if needed)
@@ -100,16 +151,19 @@ export function compilePattern(pattern: string): {
100
151
  regexPattern += "/(.*)";
101
152
  } else if (segment.type === "param") {
102
153
  paramNames.push(segment.value);
154
+ const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
103
155
  const valuePattern = segment.constraint
104
- ? `(${segment.constraint.join("|")})`
105
- : "([^/]+)";
156
+ ? `(${segment.constraint.map(escapeRegex).join("|")})`
157
+ : segment.suffix
158
+ ? "([^/]+?)"
159
+ : "([^/]+)";
106
160
 
107
161
  if (segment.optional) {
108
162
  optionalParams.add(segment.value);
109
163
  // Optional: make the whole /segment optional
110
- regexPattern += `(?:/${valuePattern})?`;
164
+ regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
111
165
  } else {
112
- regexPattern += `/${valuePattern}`;
166
+ regexPattern += `/${valuePattern}${suffixPattern}`;
113
167
  }
114
168
  } else {
115
169
  // Static segment
@@ -214,6 +268,18 @@ export interface RouteMatchResult<TEnv = any> {
214
268
  params: Record<string, string>;
215
269
  optionalParams: Set<string>;
216
270
  redirectTo?: string;
271
+ /** Ancestry shortCodes for layout pruning (from trie match) */
272
+ ancestry?: string[];
273
+ /** Route has pre-rendered data available (from trie) */
274
+ pr?: true;
275
+ /** Passthrough: handler kept for live fallback on unknown params (from trie) */
276
+ pt?: true;
277
+ /** Response type for non-RSC routes (json, text, image, any) */
278
+ responseType?: string;
279
+ /** Negotiate variants: response-type routes sharing this path */
280
+ negotiateVariants?: Array<{ routeKey: string; responseType: string }>;
281
+ /** RSC-first: RSC route was defined before response-type variants */
282
+ rscFirst?: true;
217
283
  }
218
284
 
219
285
  /**
@@ -227,7 +293,7 @@ export interface LazyEvaluationNeeded<TEnv = any> {
227
293
  * Type guard to check if result is a lazy evaluation needed response
228
294
  */
229
295
  export function isLazyEvaluationNeeded<TEnv>(
230
- result: RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null
296
+ result: RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null,
231
297
  ): result is LazyEvaluationNeeded<TEnv> {
232
298
  return result !== null && "lazyEntry" in result;
233
299
  }
@@ -248,22 +314,33 @@ export function enableMatchDebug(enabled: boolean): void {
248
314
  }
249
315
 
250
316
  export function getMatchDebugStats(): MatchDebugStats {
251
- return { entriesChecked: debugStats.entriesChecked, entriesSkipped: debugStats.entriesSkipped, routesChecked: debugStats.routesChecked };
317
+ return {
318
+ entriesChecked: debugStats.entriesChecked,
319
+ entriesSkipped: debugStats.entriesSkipped,
320
+ routesChecked: debugStats.routesChecked,
321
+ };
252
322
  }
253
323
 
254
324
  export function findMatch<TEnv>(
255
325
  pathname: string,
256
- routesEntries: RouteEntry<TEnv>[]
326
+ routesEntries: RouteEntry<TEnv>[],
257
327
  ): RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null {
258
- if (debugEnabled) {
328
+ const effectiveDebug = debugEnabled || isRouterDebugEnabled();
329
+
330
+ if (effectiveDebug) {
259
331
  debugStats = { entriesChecked: 0, entriesSkipped: 0, routesChecked: 0 };
260
- console.log(`[findMatch] pathname="${pathname}", entries=${routesEntries.length}`);
332
+ debugLog("findMatch", "start", { pathname, entries: routesEntries.length });
261
333
  for (const e of routesEntries) {
262
- console.log(` entry: prefix="${e.prefix}", staticPrefix="${e.staticPrefix}", routes=${Object.keys(e.routes).length}`);
334
+ debugLog("findMatch", "entry", {
335
+ prefix: e.prefix,
336
+ staticPrefix: e.staticPrefix,
337
+ routeCount: Object.keys(e.routes).length,
338
+ });
263
339
  }
264
340
  }
265
341
 
266
- const pathnameHasTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
342
+ const pathnameHasTrailingSlash =
343
+ pathname.length > 1 && pathname.endsWith("/");
267
344
  // Try alternate pathname for redirect matching
268
345
  const alternatePathname = pathnameHasTrailingSlash
269
346
  ? pathname.slice(0, -1)
@@ -273,9 +350,12 @@ export function findMatch<TEnv>(
273
350
  // Short-circuit: skip entry if pathname doesn't start with static prefix
274
351
  // staticPrefix is pre-computed at registration time, so this is O(1)
275
352
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
276
- if (debugEnabled) {
353
+ if (effectiveDebug) {
277
354
  debugStats.entriesSkipped++;
278
- console.log(` SKIP entry prefix="${entry.prefix}" (staticPrefix="${entry.staticPrefix}" doesn't match)`);
355
+ debugLog("findMatch", "skipped entry", {
356
+ prefix: entry.prefix,
357
+ staticPrefix: entry.staticPrefix,
358
+ });
279
359
  }
280
360
  continue;
281
361
  }
@@ -283,20 +363,22 @@ export function findMatch<TEnv>(
283
363
  // Check if this is a lazy entry that needs evaluation
284
364
  // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
285
365
  if (entry.lazy && !entry.lazyEvaluated) {
286
- if (debugEnabled) {
287
- console.log(` LAZY entry needs evaluation: staticPrefix="${entry.staticPrefix}"`);
366
+ if (effectiveDebug) {
367
+ debugLog("findMatch", "lazy entry requires evaluation", {
368
+ staticPrefix: entry.staticPrefix,
369
+ });
288
370
  }
289
371
  return { lazyEntry: entry };
290
372
  }
291
373
 
292
- if (debugEnabled) {
374
+ if (effectiveDebug) {
293
375
  debugStats.entriesChecked++;
294
376
  }
295
377
 
296
378
  const routeEntries = Object.entries(entry.routes);
297
379
 
298
380
  for (const [routeKey, pattern] of routeEntries) {
299
- if (debugEnabled) {
381
+ if (effectiveDebug) {
300
382
  debugStats.routesChecked++;
301
383
  }
302
384
 
@@ -310,11 +392,20 @@ export function findMatch<TEnv>(
310
392
  fullPattern = entry.prefix + pattern;
311
393
  }
312
394
 
313
- const { regex, paramNames, optionalParams, hasTrailingSlash } = compilePattern(fullPattern);
395
+ const { regex, paramNames, optionalParams, hasTrailingSlash } =
396
+ getCompiledPattern(fullPattern);
314
397
 
315
398
  // Get trailing slash mode for this route (per-route config or pattern-based)
316
- const trailingSlashMode: TrailingSlashMode | undefined = entry.trailingSlash?.[routeKey];
399
+ const trailingSlashMode: TrailingSlashMode | undefined =
400
+ entry.trailingSlash?.[routeKey];
317
401
 
402
+ // Prerender flag from entry metadata (set by urls() for prerender handlers)
403
+ const prFlag = entry.prerenderRouteKeys?.has(routeKey)
404
+ ? { pr: true as const }
405
+ : {};
406
+ const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
407
+ ? { pt: true as const }
408
+ : {};
318
409
 
319
410
  // Try exact match first
320
411
  const match = regex.exec(pathname);
@@ -324,21 +415,51 @@ export function findMatch<TEnv>(
324
415
  params[name] = match[index + 1] ?? "";
325
416
  });
326
417
 
327
- if (debugEnabled) {
328
- console.log(` MATCH: routeKey="${routeKey}", pattern="${fullPattern}"`);
329
- console.log(` Stats: entriesChecked=${debugStats.entriesChecked}, entriesSkipped=${debugStats.entriesSkipped}, routesChecked=${debugStats.routesChecked}`);
418
+ if (effectiveDebug) {
419
+ debugLog("findMatch", "matched route", {
420
+ routeKey,
421
+ pattern: fullPattern,
422
+ stats: { ...debugStats },
423
+ });
330
424
  }
331
425
 
332
426
  // Check if trailing slash mode requires redirect even on exact match
333
- if (trailingSlashMode === "always" && !pathnameHasTrailingSlash && pathname !== "/") {
427
+ if (
428
+ trailingSlashMode === "always" &&
429
+ !pathnameHasTrailingSlash &&
430
+ pathname !== "/"
431
+ ) {
334
432
  // Mode says always have trailing slash, but pathname doesn't have it
335
- return { entry, routeKey, params, optionalParams, redirectTo: pathname + "/" };
433
+ return {
434
+ entry,
435
+ routeKey,
436
+ params,
437
+ optionalParams,
438
+ redirectTo: pathname + "/",
439
+ ...prFlag,
440
+ ...ptFlag,
441
+ };
336
442
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
337
443
  // Mode says never have trailing slash, but pathname has it
338
- return { entry, routeKey, params, optionalParams, redirectTo: pathname.slice(0, -1) };
444
+ return {
445
+ entry,
446
+ routeKey,
447
+ params,
448
+ optionalParams,
449
+ redirectTo: pathname.slice(0, -1),
450
+ ...prFlag,
451
+ ...ptFlag,
452
+ };
339
453
  }
340
454
 
341
- return { entry, routeKey, params, optionalParams };
455
+ return {
456
+ entry,
457
+ routeKey,
458
+ params,
459
+ optionalParams,
460
+ ...prFlag,
461
+ ...ptFlag,
462
+ };
342
463
  }
343
464
 
344
465
  // Try alternate pathname (opposite trailing slash)
@@ -352,24 +473,71 @@ export function findMatch<TEnv>(
352
473
  // Determine redirect behavior based on mode
353
474
  if (trailingSlashMode === "ignore") {
354
475
  // Match without redirect
355
- return { entry, routeKey, params, optionalParams };
476
+ return {
477
+ entry,
478
+ routeKey,
479
+ params,
480
+ optionalParams,
481
+ ...prFlag,
482
+ ...ptFlag,
483
+ };
356
484
  } else if (trailingSlashMode === "never") {
357
485
  // Redirect to no trailing slash
358
486
  if (pathnameHasTrailingSlash) {
359
- return { entry, routeKey, params, optionalParams, redirectTo: alternatePathname };
487
+ return {
488
+ entry,
489
+ routeKey,
490
+ params,
491
+ optionalParams,
492
+ redirectTo: alternatePathname,
493
+ ...prFlag,
494
+ ...ptFlag,
495
+ };
360
496
  }
361
- return { entry, routeKey, params, optionalParams };
497
+ return {
498
+ entry,
499
+ routeKey,
500
+ params,
501
+ optionalParams,
502
+ ...prFlag,
503
+ ...ptFlag,
504
+ };
362
505
  } else if (trailingSlashMode === "always") {
363
506
  // Redirect to with trailing slash
364
507
  if (!pathnameHasTrailingSlash) {
365
- return { entry, routeKey, params, optionalParams, redirectTo: alternatePathname };
508
+ return {
509
+ entry,
510
+ routeKey,
511
+ params,
512
+ optionalParams,
513
+ redirectTo: alternatePathname,
514
+ ...prFlag,
515
+ ...ptFlag,
516
+ };
366
517
  }
367
- return { entry, routeKey, params, optionalParams };
518
+ return {
519
+ entry,
520
+ routeKey,
521
+ params,
522
+ optionalParams,
523
+ ...prFlag,
524
+ ...ptFlag,
525
+ };
368
526
  } else {
369
527
  // No explicit mode - use pattern-based detection
370
528
  // Redirect to canonical form (what the pattern defines)
371
- const canonicalPath = hasTrailingSlash ? alternatePathname : pathname.slice(0, -1);
372
- return { entry, routeKey, params, optionalParams, redirectTo: canonicalPath };
529
+ const canonicalPath = hasTrailingSlash
530
+ ? alternatePathname
531
+ : pathname.slice(0, -1);
532
+ return {
533
+ entry,
534
+ routeKey,
535
+ params,
536
+ optionalParams,
537
+ redirectTo: canonicalPath,
538
+ ...prFlag,
539
+ ...ptFlag,
540
+ };
373
541
  }
374
542
  }
375
543
  }