@rangojs/router 0.0.0-experimental.0f44aca1

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