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

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 (312) 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 +4960 -935
  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/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  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 +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Intercept Resolution
3
+ *
4
+ * Extracted from createRouter closure. Contains intercept detection and resolution
5
+ * functions for soft navigation (modals).
6
+ */
7
+
8
+ import type { ReactNode } from "react";
9
+ import type {
10
+ EntryData,
11
+ InterceptEntry,
12
+ InterceptSelectorContext,
13
+ } from "../server/context";
14
+ import type {
15
+ HandlerContext,
16
+ InternalHandlerContext,
17
+ ResolvedSegment,
18
+ } from "../types";
19
+ import { evaluateRevalidation } from "./revalidation.js";
20
+ import { getRequestContext } from "../server/request-context.js";
21
+ import { executeInterceptMiddleware } from "./middleware.js";
22
+ import { createReverseFunction } from "./handler-context.js";
23
+ import { getGlobalRouteMap } from "../route-map-builder.js";
24
+ import { handleHandlerResult } from "./segment-resolution.js";
25
+ import type { SegmentResolutionDeps } from "./types.js";
26
+ import { debugLog } from "./logging.js";
27
+ import { runInsideLoaderScope } from "../server/context.js";
28
+
29
+ /**
30
+ * Check if an intercept's when conditions are satisfied.
31
+ * All when() functions must return true for the intercept to activate.
32
+ * If no when() conditions are defined, the intercept always activates.
33
+ *
34
+ * During action revalidation, when() is NOT evaluated.
35
+ */
36
+ export function evaluateInterceptWhen(
37
+ intercept: InterceptEntry,
38
+ selectorContext: InterceptSelectorContext | null,
39
+ isAction: boolean,
40
+ ): boolean {
41
+ if (isAction) {
42
+ return true;
43
+ }
44
+
45
+ if (!intercept.when || intercept.when.length === 0) {
46
+ return true;
47
+ }
48
+
49
+ if (!selectorContext) {
50
+ return false;
51
+ }
52
+
53
+ return intercept.when.every((fn) => fn(selectorContext));
54
+ }
55
+
56
+ /**
57
+ * Find an intercept for the target route by walking up the entry chain.
58
+ * Returns the first (innermost) matching intercept along with the entry that defines it.
59
+ */
60
+ export function findInterceptForRoute(
61
+ targetRouteKey: string,
62
+ fromEntry: EntryData | null,
63
+ selectorContext: InterceptSelectorContext | null = null,
64
+ isAction: boolean = false,
65
+ ): { intercept: InterceptEntry; entry: EntryData } | null {
66
+ let current: EntryData | null = fromEntry;
67
+
68
+ while (current) {
69
+ if (current.intercept && current.intercept.length > 0) {
70
+ for (const intercept of current.intercept) {
71
+ if (
72
+ intercept.routeName === targetRouteKey &&
73
+ evaluateInterceptWhen(intercept, selectorContext, isAction)
74
+ ) {
75
+ return { intercept, entry: current };
76
+ }
77
+ }
78
+ }
79
+
80
+ if (current.layout && current.layout.length > 0) {
81
+ for (const siblingLayout of current.layout) {
82
+ if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
83
+ for (const intercept of siblingLayout.intercept) {
84
+ if (
85
+ intercept.routeName === targetRouteKey &&
86
+ evaluateInterceptWhen(intercept, selectorContext, isAction)
87
+ ) {
88
+ return { intercept, entry: siblingLayout };
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ current = current.parent;
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Resolve an intercept entry and emit segment with the slot name.
103
+ */
104
+ export async function resolveInterceptEntry<TEnv>(
105
+ interceptEntry: InterceptEntry,
106
+ parentEntry: EntryData,
107
+ params: Record<string, string>,
108
+ context: HandlerContext<any, TEnv>,
109
+ belongsToRoute: boolean,
110
+ deps: SegmentResolutionDeps<TEnv>,
111
+ revalidationContext?: {
112
+ clientSegmentIds: Set<string>;
113
+ prevParams: Record<string, string>;
114
+ request: Request;
115
+ prevUrl: URL;
116
+ nextUrl: URL;
117
+ routeKey: string;
118
+ actionContext?: {
119
+ actionId?: string;
120
+ actionUrl?: URL;
121
+ actionResult?: any;
122
+ formData?: FormData;
123
+ };
124
+ stale?: boolean;
125
+ },
126
+ ): Promise<ResolvedSegment[]> {
127
+ const segments: ResolvedSegment[] = [];
128
+
129
+ if (interceptEntry.middleware.length > 0) {
130
+ const requestCtx = getRequestContext();
131
+ if (!requestCtx?.res) {
132
+ throw new Error(
133
+ "Request context with stubResponse is required for intercept middleware",
134
+ );
135
+ }
136
+ const middlewareResponse = await executeInterceptMiddleware(
137
+ interceptEntry.middleware,
138
+ context.request,
139
+ context.env,
140
+ params,
141
+ (context as InternalHandlerContext<any, TEnv>)._variables,
142
+ requestCtx.res,
143
+ createReverseFunction(getGlobalRouteMap()),
144
+ );
145
+ if (middlewareResponse) throw middlewareResponse;
146
+ }
147
+
148
+ const loaderPromises: Promise<any>[] = [];
149
+ const loaderIds: string[] = [];
150
+
151
+ for (let i = 0; i < interceptEntry.loader.length; i++) {
152
+ const { loader, revalidate: loaderRevalidateFns } =
153
+ interceptEntry.loader[i];
154
+ const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
155
+
156
+ if (revalidationContext) {
157
+ const {
158
+ clientSegmentIds,
159
+ prevParams,
160
+ request,
161
+ prevUrl,
162
+ nextUrl,
163
+ routeKey,
164
+ actionContext,
165
+ stale,
166
+ } = revalidationContext;
167
+
168
+ const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
169
+ if (clientSegmentIds.has(interceptSegmentId)) {
170
+ const dummySegment: ResolvedSegment = {
171
+ id: segmentId,
172
+ namespace: `intercept:${interceptEntry.routeName}`,
173
+ type: "loader",
174
+ index: i,
175
+ component: null,
176
+ params,
177
+ loaderId: loader.$$id,
178
+ belongsToRoute,
179
+ };
180
+
181
+ const shouldRevalidate = await evaluateRevalidation({
182
+ segment: dummySegment,
183
+ prevParams,
184
+ getPrevSegment: null,
185
+ request,
186
+ prevUrl,
187
+ nextUrl,
188
+ revalidations: loaderRevalidateFns.map((fn, j) => ({
189
+ name: `intercept-loader-revalidate${j}`,
190
+ fn,
191
+ })),
192
+ routeKey,
193
+ context,
194
+ actionContext,
195
+ stale,
196
+ traceSource: "intercept-loader",
197
+ });
198
+
199
+ if (!shouldRevalidate) {
200
+ debugLog("intercept.loader", "skipped revalidation", {
201
+ loaderId: loader.$$id,
202
+ });
203
+ continue;
204
+ }
205
+ debugLog("intercept.loader", "revalidating", {
206
+ loaderId: loader.$$id,
207
+ stale,
208
+ });
209
+ }
210
+ }
211
+
212
+ loaderIds.push(loader.$$id);
213
+ loaderPromises.push(
214
+ deps.wrapLoaderPromise(
215
+ runInsideLoaderScope(() => context.use(loader)),
216
+ parentEntry,
217
+ segmentId,
218
+ context.pathname,
219
+ ),
220
+ );
221
+ }
222
+
223
+ const handlerResult =
224
+ typeof interceptEntry.handler === "function"
225
+ ? handleHandlerResult(interceptEntry.handler(context))
226
+ : interceptEntry.handler;
227
+
228
+ let layoutElement: ReactNode | undefined;
229
+ if (interceptEntry.layout) {
230
+ if (typeof interceptEntry.layout === "function") {
231
+ const layoutResult = await interceptEntry.layout(context);
232
+ if (layoutResult instanceof Response) {
233
+ throw layoutResult;
234
+ }
235
+ layoutElement = layoutResult;
236
+ } else {
237
+ layoutElement = interceptEntry.layout;
238
+ }
239
+ }
240
+
241
+ let component: ReactNode;
242
+ let loaderDataPromise: Promise<any[]> | any[] | undefined;
243
+
244
+ if (interceptEntry.loading && loaderPromises.length > 0) {
245
+ component =
246
+ handlerResult instanceof Promise
247
+ ? handlerResult
248
+ : (Promise.resolve(handlerResult) as ReactNode);
249
+ loaderDataPromise = Promise.all(loaderPromises);
250
+ } else if (loaderPromises.length > 0) {
251
+ loaderDataPromise = await Promise.all(loaderPromises);
252
+ component =
253
+ handlerResult instanceof Promise ? await handlerResult : handlerResult;
254
+ } else {
255
+ component =
256
+ interceptEntry.loading && handlerResult instanceof Promise
257
+ ? handlerResult
258
+ : handlerResult instanceof Promise
259
+ ? await handlerResult
260
+ : handlerResult;
261
+ }
262
+
263
+ const interceptSegment = {
264
+ id: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
265
+ namespace: `intercept:${interceptEntry.routeName}`,
266
+ type: "parallel" as const,
267
+ index: 0,
268
+ component,
269
+ loading: interceptEntry.loading === false ? null : interceptEntry.loading,
270
+ layout: layoutElement,
271
+ params,
272
+ slot: interceptEntry.slotName,
273
+ belongsToRoute,
274
+ parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
275
+ loaderDataPromise,
276
+ loaderIds: loaderIds.length > 0 ? loaderIds : undefined,
277
+ };
278
+ segments.push(interceptSegment);
279
+
280
+ return segments;
281
+ }
282
+
283
+ /**
284
+ * Resolve only the loaders for a cached intercept segment.
285
+ * Used on intercept cache hit to get fresh loader data while keeping cached component/layout.
286
+ */
287
+ export async function resolveInterceptLoadersOnly<TEnv>(
288
+ interceptEntry: InterceptEntry,
289
+ parentEntry: EntryData,
290
+ params: Record<string, string>,
291
+ context: HandlerContext<any, TEnv>,
292
+ belongsToRoute: boolean,
293
+ deps: SegmentResolutionDeps<TEnv>,
294
+ revalidationContext: {
295
+ clientSegmentIds: Set<string>;
296
+ prevParams: Record<string, string>;
297
+ request: Request;
298
+ prevUrl: URL;
299
+ nextUrl: URL;
300
+ routeKey: string;
301
+ actionContext?: {
302
+ actionId?: string;
303
+ actionUrl?: URL;
304
+ actionResult?: any;
305
+ formData?: FormData;
306
+ };
307
+ stale?: boolean;
308
+ },
309
+ ): Promise<{
310
+ loaderDataPromise: Promise<any[]> | any[];
311
+ loaderIds: string[];
312
+ } | null> {
313
+ if (interceptEntry.loader.length === 0) {
314
+ return null;
315
+ }
316
+
317
+ const loaderPromises: Promise<any>[] = [];
318
+ const loaderIds: string[] = [];
319
+
320
+ const {
321
+ clientSegmentIds,
322
+ prevParams,
323
+ request,
324
+ prevUrl,
325
+ nextUrl,
326
+ routeKey,
327
+ actionContext,
328
+ stale,
329
+ } = revalidationContext;
330
+
331
+ for (let i = 0; i < interceptEntry.loader.length; i++) {
332
+ const { loader, revalidate: loaderRevalidateFns } =
333
+ interceptEntry.loader[i];
334
+ const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
335
+
336
+ const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
337
+ if (clientSegmentIds.has(interceptSegmentId)) {
338
+ const dummySegment: ResolvedSegment = {
339
+ id: segmentId,
340
+ namespace: `intercept:${interceptEntry.routeName}`,
341
+ type: "loader",
342
+ index: i,
343
+ component: null,
344
+ params,
345
+ loaderId: loader.$$id,
346
+ belongsToRoute,
347
+ };
348
+
349
+ const shouldRevalidate = await evaluateRevalidation({
350
+ segment: dummySegment,
351
+ prevParams,
352
+ getPrevSegment: null,
353
+ request,
354
+ prevUrl,
355
+ nextUrl,
356
+ revalidations: loaderRevalidateFns.map((fn, j) => ({
357
+ name: `intercept-loader-revalidate${j}`,
358
+ fn,
359
+ })),
360
+ routeKey,
361
+ context,
362
+ actionContext,
363
+ stale,
364
+ traceSource: "intercept-loader",
365
+ });
366
+
367
+ if (!shouldRevalidate) {
368
+ debugLog("intercept.loader", "skipped on cache hit", {
369
+ loaderId: loader.$$id,
370
+ });
371
+ continue;
372
+ }
373
+ debugLog("intercept.loader", "revalidating on cache hit", {
374
+ loaderId: loader.$$id,
375
+ stale,
376
+ });
377
+ }
378
+
379
+ loaderIds.push(loader.$$id);
380
+ loaderPromises.push(
381
+ deps.wrapLoaderPromise(
382
+ runInsideLoaderScope(() => context.use(loader)),
383
+ parentEntry,
384
+ segmentId,
385
+ context.pathname,
386
+ ),
387
+ );
388
+ }
389
+
390
+ if (loaderPromises.length === 0) {
391
+ return null;
392
+ }
393
+
394
+ // Match fresh-path semantics: only defer (no await) when loading is truthy.
395
+ // `loading: false` means "no loading UI, await loaders before render" —
396
+ // same as the fresh path's `if (interceptEntry.loading && ...)` check.
397
+ const loaderDataPromise = interceptEntry.loading
398
+ ? Promise.all(loaderPromises)
399
+ : await Promise.all(loaderPromises);
400
+
401
+ return { loaderDataPromise, loaderIds };
402
+ }
@@ -0,0 +1,237 @@
1
+ import { registerRouteMap } from "../route-map-builder.js";
2
+ import { extractStaticPrefix } from "./pattern-matching.js";
3
+ import {
4
+ EntryData,
5
+ RSCRouterContext,
6
+ runWithPrefixes,
7
+ getIsolatedLazyParent,
8
+ } from "../server/context";
9
+ import type { UrlPatterns } from "../urls.js";
10
+ import type { AllUseItems, IncludeItem } from "../route-types.js";
11
+ import type { ResolvedRouteMap, RouteEntry, TrailingSlashMode } from "../types";
12
+
13
+ export interface LazyEvalDeps<TEnv = any> {
14
+ routesEntries: RouteEntry<TEnv>[];
15
+ mergedRouteMap: Record<string, string>;
16
+ nextMountIndex: () => number;
17
+ getPrecomputedByPrefix: () => Map<string, Record<string, string>> | null;
18
+ routerId?: string;
19
+ }
20
+
21
+ // Detect lazy includes in handler result and create placeholder entries
22
+ // Lazy includes are IncludeItem with lazy: true and _lazyContext
23
+ // Moved to outer scope so it can be reused by evaluateLazyEntry for nested includes
24
+ export function findLazyIncludes<TEnv = any>(
25
+ items: AllUseItems[],
26
+ ): Array<{
27
+ prefix: string;
28
+ patterns: UrlPatterns<TEnv>;
29
+ context: {
30
+ urlPrefix: string;
31
+ namePrefix: string | undefined;
32
+ parent: unknown;
33
+ rootScoped?: boolean;
34
+ };
35
+ }> {
36
+ const lazyItems: Array<{
37
+ prefix: string;
38
+ patterns: UrlPatterns<TEnv>;
39
+ context: {
40
+ urlPrefix: string;
41
+ namePrefix: string | undefined;
42
+ parent: unknown;
43
+ rootScoped?: boolean;
44
+ };
45
+ }> = [];
46
+
47
+ for (const item of items) {
48
+ if (!item) continue;
49
+ if (item.type === "include") {
50
+ const includeItem = item as IncludeItem;
51
+ if (includeItem.lazy === true && includeItem._lazyContext) {
52
+ lazyItems.push({
53
+ prefix: includeItem.prefix,
54
+ patterns: includeItem.patterns as UrlPatterns<TEnv>,
55
+ context: includeItem._lazyContext,
56
+ });
57
+ }
58
+ }
59
+ // Recursively check nested items (in layouts, etc.)
60
+ if ((item as any).uses && Array.isArray((item as any).uses)) {
61
+ lazyItems.push(...findLazyIncludes((item as any).uses));
62
+ }
63
+ }
64
+
65
+ return lazyItems;
66
+ }
67
+
68
+ /**
69
+ * Evaluate a lazy entry's patterns and populate its routes
70
+ * This runs the lazy patterns handler and updates the entry in-place
71
+ * Also detects nested lazy includes and registers them as new entries
72
+ */
73
+ export function evaluateLazyEntry<TEnv = any>(
74
+ entry: RouteEntry<TEnv>,
75
+ deps: LazyEvalDeps<TEnv>,
76
+ ): void {
77
+ if (!entry.lazy || entry.lazyEvaluated || !entry.lazyPatterns) {
78
+ return;
79
+ }
80
+
81
+ // Check for pre-computed routes from build-time data.
82
+ // Only leaf nodes (no nested includes) are precomputed, so entries with
83
+ // nested lazy includes fall through to the handler below.
84
+ // When multiple entries share the same staticPrefix (e.g., several
85
+ // include("/", ...) calls), the precomputed data merges all their routes
86
+ // into one entry. Assigning that merged set to the first matching entry
87
+ // causes findMatch to pick the wrong handler for routes belonging to a
88
+ // different include. Skip the shortcut when the prefix is shared.
89
+ const currentPrecomputed = deps.getPrecomputedByPrefix();
90
+ if (currentPrecomputed) {
91
+ const routes = currentPrecomputed.get(entry.staticPrefix);
92
+ if (routes) {
93
+ const prefixIsShared =
94
+ deps.routesEntries.filter((e) => e.staticPrefix === entry.staticPrefix)
95
+ .length > 1;
96
+ if (!prefixIsShared) {
97
+ entry.lazyEvaluated = true;
98
+ entry.routes = routes as ResolvedRouteMap<any>;
99
+ for (const [name, pattern] of Object.entries(routes)) {
100
+ deps.mergedRouteMap[name] = pattern;
101
+ }
102
+ registerRouteMap(deps.mergedRouteMap);
103
+ return;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Mark as evaluated immediately to prevent concurrent evaluation.
109
+ // JS is single-threaded but handlers.handler() could theoretically yield,
110
+ // and the while-loop in findMatch retries after evaluation.
111
+ entry.lazyEvaluated = true;
112
+
113
+ const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
114
+ const lazyContext = entry.lazyContext;
115
+
116
+ // Create a new context for evaluating the lazy patterns
117
+ const manifest = new Map<string, EntryData>();
118
+ const patterns = new Map<string, string>();
119
+ const patternsByPrefix = new Map<string, Map<string, string>>();
120
+ const trailingSlashMap = new Map<string, TrailingSlashMode>();
121
+
122
+ // Capture the handler result to detect nested lazy includes
123
+ let handlerResult: AllUseItems[] = [];
124
+
125
+ // Merge captured counters from include() to maintain consistent
126
+ // shortCode indices with sibling entries from pattern extraction
127
+ const lazyCounters: Record<string, number> = {};
128
+ if (lazyContext?.counters) {
129
+ for (const [key, value] of Object.entries(lazyContext.counters)) {
130
+ lazyCounters[key] = value;
131
+ }
132
+ }
133
+
134
+ RSCRouterContext.run(
135
+ {
136
+ manifest,
137
+ patterns,
138
+ patternsByPrefix,
139
+ trailingSlash: trailingSlashMap,
140
+ namespace: "lazy",
141
+ parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
142
+ counters: lazyCounters,
143
+ cacheProfiles: lazyContext?.cacheProfiles,
144
+ rootScoped: lazyContext?.rootScoped,
145
+ includeScope: lazyContext?.includeScope,
146
+ },
147
+ () => {
148
+ // Run the lazy patterns handler with the original context prefixes
149
+ // The prefix comes from the IncludeItem stored in lazyPatterns
150
+ const includePrefix = (entry as any)._lazyPrefix || "";
151
+ const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
152
+
153
+ if (fullPrefix || lazyContext?.namePrefix) {
154
+ runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
155
+ handlerResult = lazyPatterns.handler() as AllUseItems[];
156
+ });
157
+ } else {
158
+ handlerResult = lazyPatterns.handler() as AllUseItems[];
159
+ }
160
+ },
161
+ );
162
+
163
+ // Populate the entry's routes from the patterns
164
+ const routesObject: Record<string, string> = {};
165
+ for (const [name, pattern] of patterns.entries()) {
166
+ routesObject[name] = pattern;
167
+ // Also add to merged route map for reverse() support
168
+ const existingPattern = deps.mergedRouteMap[name];
169
+ if (existingPattern !== undefined && existingPattern !== pattern) {
170
+ console.warn(
171
+ `[@rangojs/router] Route name conflict: "${name}" already maps to "${existingPattern}", ` +
172
+ `overwriting with "${pattern}" (from lazy include). Use unique route names to avoid this.`,
173
+ );
174
+ }
175
+ deps.mergedRouteMap[name] = pattern;
176
+ }
177
+
178
+ // Update the entry in-place
179
+ entry.routes = routesObject as ResolvedRouteMap<any>;
180
+
181
+ // Note: Do NOT clear lazyPatterns/lazyContext here.
182
+ // loadManifest() needs them on every request to re-run the handler
183
+ // in the correct AsyncLocalStorage context (Store.manifest).
184
+
185
+ // Update trailing slash config if available
186
+ if (trailingSlashMap.size > 0) {
187
+ entry.trailingSlash = Object.fromEntries(trailingSlashMap);
188
+ }
189
+
190
+ // Detect nested lazy includes and register them as new entries
191
+ const nestedLazyIncludes = findLazyIncludes(handlerResult);
192
+ for (const lazyInclude of nestedLazyIncludes) {
193
+ // Compute the full URL prefix (combining parent prefix if any)
194
+ const fullPrefix = lazyInclude.context.urlPrefix
195
+ ? lazyInclude.context.urlPrefix + lazyInclude.prefix
196
+ : lazyInclude.prefix;
197
+
198
+ const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
199
+ prefix: "",
200
+ staticPrefix: extractStaticPrefix(fullPrefix),
201
+ routes: {} as ResolvedRouteMap<any>, // Empty until first match
202
+ trailingSlash: entry.trailingSlash,
203
+ handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
204
+ mountIndex: deps.nextMountIndex(),
205
+ routerId: deps.routerId,
206
+ // Lazy evaluation fields
207
+ lazy: true,
208
+ lazyPatterns: lazyInclude.patterns,
209
+ lazyContext: lazyInclude.context,
210
+ lazyEvaluated: false,
211
+ // Store the include prefix for evaluation
212
+ _lazyPrefix: lazyInclude.prefix,
213
+ };
214
+ // Insert nested lazy entry before any entry whose staticPrefix is a
215
+ // prefix of (but shorter than) this lazy entry's staticPrefix.
216
+ // This ensures more specific lazy includes are matched before
217
+ // less specific eager entries (e.g., "/href/nested" before "/href/:id").
218
+ const nestedPrefix = nestedEntry.staticPrefix;
219
+ let insertIndex = deps.routesEntries.length;
220
+ if (nestedPrefix) {
221
+ for (let i = 0; i < deps.routesEntries.length; i++) {
222
+ const existing = deps.routesEntries[i]!;
223
+ if (
224
+ nestedPrefix.startsWith(existing.staticPrefix) &&
225
+ nestedPrefix.length > existing.staticPrefix.length
226
+ ) {
227
+ insertIndex = i;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ deps.routesEntries.splice(insertIndex, 0, nestedEntry);
233
+ }
234
+
235
+ // Re-register route map for runtime reverse() usage
236
+ registerRouteMap(deps.mergedRouteMap);
237
+ }