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

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 +55 -33
  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 +743 -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 +1373 -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 +150 -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,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 && (lazyContext as any).counters) {
129
+ const captured = (lazyContext as any).counters as Record<string, number>;
130
+ for (const [key, value] of Object.entries(captured)) {
131
+ lazyCounters[key] = value;
132
+ }
133
+ }
134
+
135
+ RSCRouterContext.run(
136
+ {
137
+ manifest,
138
+ patterns,
139
+ patternsByPrefix,
140
+ trailingSlash: trailingSlashMap,
141
+ namespace: "lazy",
142
+ parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
143
+ counters: lazyCounters,
144
+ cacheProfiles: (lazyContext as any)?.cacheProfiles,
145
+ rootScoped: (lazyContext as any)?.rootScoped,
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
+ }