@rangojs/router 0.0.0-experimental.002d056c

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 +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -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 +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  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 +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -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 +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -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 +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -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 +547 -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 +479 -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 +982 -0
  105. package/src/cache/cf/index.ts +29 -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 +44 -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 +281 -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 +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -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 +193 -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 +749 -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 +320 -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 +1242 -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 +291 -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 +1006 -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 +237 -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 +920 -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 +109 -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 +108 -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 +48 -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 +363 -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 +266 -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 +445 -0
  298. package/src/vite/router-discovery.ts +777 -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,397 @@
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
+ traceSource: "intercept-loader",
192
+ });
193
+
194
+ if (!shouldRevalidate) {
195
+ debugLog("intercept.loader", "skipped revalidation", {
196
+ loaderId: loader.$$id,
197
+ });
198
+ continue;
199
+ }
200
+ debugLog("intercept.loader", "revalidating", {
201
+ loaderId: loader.$$id,
202
+ stale,
203
+ });
204
+ }
205
+ }
206
+
207
+ loaderIds.push(loader.$$id);
208
+ loaderPromises.push(
209
+ deps.wrapLoaderPromise(
210
+ context.use(loader),
211
+ parentEntry,
212
+ segmentId,
213
+ context.pathname,
214
+ ),
215
+ );
216
+ }
217
+
218
+ const handlerResult =
219
+ typeof interceptEntry.handler === "function"
220
+ ? handleHandlerResult(interceptEntry.handler(context))
221
+ : interceptEntry.handler;
222
+
223
+ let layoutElement: ReactNode | undefined;
224
+ if (interceptEntry.layout) {
225
+ if (typeof interceptEntry.layout === "function") {
226
+ const layoutResult = await interceptEntry.layout(context);
227
+ if (layoutResult instanceof Response) {
228
+ throw layoutResult;
229
+ }
230
+ layoutElement = layoutResult;
231
+ } else {
232
+ layoutElement = interceptEntry.layout;
233
+ }
234
+ }
235
+
236
+ let component: ReactNode;
237
+ let loaderDataPromise: Promise<any[]> | any[] | undefined;
238
+
239
+ if (interceptEntry.loading && loaderPromises.length > 0) {
240
+ component =
241
+ handlerResult instanceof Promise
242
+ ? handlerResult
243
+ : (Promise.resolve(handlerResult) as ReactNode);
244
+ loaderDataPromise = Promise.all(loaderPromises);
245
+ } else if (loaderPromises.length > 0) {
246
+ loaderDataPromise = await Promise.all(loaderPromises);
247
+ component =
248
+ handlerResult instanceof Promise ? await handlerResult : handlerResult;
249
+ } else {
250
+ component =
251
+ interceptEntry.loading && handlerResult instanceof Promise
252
+ ? handlerResult
253
+ : handlerResult instanceof Promise
254
+ ? await handlerResult
255
+ : handlerResult;
256
+ }
257
+
258
+ const interceptSegment = {
259
+ id: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
260
+ namespace: `intercept:${interceptEntry.routeName}`,
261
+ type: "parallel" as const,
262
+ index: 0,
263
+ component,
264
+ loading: interceptEntry.loading === false ? null : interceptEntry.loading,
265
+ layout: layoutElement,
266
+ params,
267
+ slot: interceptEntry.slotName,
268
+ belongsToRoute,
269
+ parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
270
+ loaderDataPromise,
271
+ loaderIds: loaderIds.length > 0 ? loaderIds : undefined,
272
+ };
273
+ segments.push(interceptSegment);
274
+
275
+ return segments;
276
+ }
277
+
278
+ /**
279
+ * Resolve only the loaders for a cached intercept segment.
280
+ * Used on intercept cache hit to get fresh loader data while keeping cached component/layout.
281
+ */
282
+ export async function resolveInterceptLoadersOnly<TEnv>(
283
+ interceptEntry: InterceptEntry,
284
+ parentEntry: EntryData,
285
+ params: Record<string, string>,
286
+ context: HandlerContext<any, TEnv>,
287
+ belongsToRoute: boolean,
288
+ deps: SegmentResolutionDeps<TEnv>,
289
+ revalidationContext: {
290
+ clientSegmentIds: Set<string>;
291
+ prevParams: Record<string, string>;
292
+ request: Request;
293
+ prevUrl: URL;
294
+ nextUrl: URL;
295
+ routeKey: string;
296
+ actionContext?: {
297
+ actionId?: string;
298
+ actionUrl?: URL;
299
+ actionResult?: any;
300
+ formData?: FormData;
301
+ };
302
+ stale?: boolean;
303
+ },
304
+ ): Promise<{
305
+ loaderDataPromise: Promise<any[]> | any[];
306
+ loaderIds: string[];
307
+ } | null> {
308
+ if (interceptEntry.loader.length === 0) {
309
+ return null;
310
+ }
311
+
312
+ const loaderPromises: Promise<any>[] = [];
313
+ const loaderIds: string[] = [];
314
+
315
+ const {
316
+ clientSegmentIds,
317
+ prevParams,
318
+ request,
319
+ prevUrl,
320
+ nextUrl,
321
+ routeKey,
322
+ actionContext,
323
+ stale,
324
+ } = revalidationContext;
325
+
326
+ for (let i = 0; i < interceptEntry.loader.length; i++) {
327
+ const { loader, revalidate: loaderRevalidateFns } =
328
+ interceptEntry.loader[i];
329
+ const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
330
+
331
+ const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
332
+ if (clientSegmentIds.has(interceptSegmentId)) {
333
+ const dummySegment: ResolvedSegment = {
334
+ id: segmentId,
335
+ namespace: `intercept:${interceptEntry.routeName}`,
336
+ type: "loader",
337
+ index: i,
338
+ component: null,
339
+ params,
340
+ loaderId: loader.$$id,
341
+ belongsToRoute,
342
+ };
343
+
344
+ const shouldRevalidate = await evaluateRevalidation({
345
+ segment: dummySegment,
346
+ prevParams,
347
+ getPrevSegment: null,
348
+ request,
349
+ prevUrl,
350
+ nextUrl,
351
+ revalidations: loaderRevalidateFns.map((fn, j) => ({
352
+ name: `intercept-loader-revalidate${j}`,
353
+ fn,
354
+ })),
355
+ routeKey,
356
+ context,
357
+ actionContext,
358
+ stale,
359
+ traceSource: "intercept-loader",
360
+ });
361
+
362
+ if (!shouldRevalidate) {
363
+ debugLog("intercept.loader", "skipped on cache hit", {
364
+ loaderId: loader.$$id,
365
+ });
366
+ continue;
367
+ }
368
+ debugLog("intercept.loader", "revalidating on cache hit", {
369
+ loaderId: loader.$$id,
370
+ stale,
371
+ });
372
+ }
373
+
374
+ loaderIds.push(loader.$$id);
375
+ loaderPromises.push(
376
+ deps.wrapLoaderPromise(
377
+ context.use(loader),
378
+ parentEntry,
379
+ segmentId,
380
+ context.pathname,
381
+ ),
382
+ );
383
+ }
384
+
385
+ if (loaderPromises.length === 0) {
386
+ return null;
387
+ }
388
+
389
+ // Match fresh-path semantics: only defer (no await) when loading is truthy.
390
+ // `loading: false` means "no loading UI, await loaders before render" —
391
+ // same as the fresh path's `if (interceptEntry.loading && ...)` check.
392
+ const loaderDataPromise = interceptEntry.loading
393
+ ? Promise.all(loaderPromises)
394
+ : await Promise.all(loaderPromises);
395
+
396
+ return { loaderDataPromise, loaderIds };
397
+ }
@@ -0,0 +1,236 @@
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
+ routerId?: string;
18
+ }
19
+
20
+ // Detect lazy includes in handler result and create placeholder entries
21
+ // Lazy includes are IncludeItem with lazy: true and _lazyContext
22
+ // Moved to outer scope so it can be reused by evaluateLazyEntry for nested includes
23
+ export function findLazyIncludes<TEnv = any>(
24
+ items: AllUseItems[],
25
+ ): Array<{
26
+ prefix: string;
27
+ patterns: UrlPatterns<TEnv>;
28
+ context: {
29
+ urlPrefix: string;
30
+ namePrefix: string | undefined;
31
+ parent: unknown;
32
+ rootScoped?: boolean;
33
+ };
34
+ }> {
35
+ const lazyItems: Array<{
36
+ prefix: string;
37
+ patterns: UrlPatterns<TEnv>;
38
+ context: {
39
+ urlPrefix: string;
40
+ namePrefix: string | undefined;
41
+ parent: unknown;
42
+ rootScoped?: boolean;
43
+ };
44
+ }> = [];
45
+
46
+ for (const item of items) {
47
+ if (!item) continue;
48
+ if (item.type === "include") {
49
+ const includeItem = item as IncludeItem;
50
+ if (includeItem.lazy === true && includeItem._lazyContext) {
51
+ lazyItems.push({
52
+ prefix: includeItem.prefix,
53
+ patterns: includeItem.patterns as UrlPatterns<TEnv>,
54
+ context: includeItem._lazyContext,
55
+ });
56
+ }
57
+ }
58
+ // Recursively check nested items (in layouts, etc.)
59
+ if ((item as any).uses && Array.isArray((item as any).uses)) {
60
+ lazyItems.push(...findLazyIncludes((item as any).uses));
61
+ }
62
+ }
63
+
64
+ return lazyItems;
65
+ }
66
+
67
+ /**
68
+ * Evaluate a lazy entry's patterns and populate its routes
69
+ * This runs the lazy patterns handler and updates the entry in-place
70
+ * Also detects nested lazy includes and registers them as new entries
71
+ */
72
+ export function evaluateLazyEntry<TEnv = any>(
73
+ entry: RouteEntry<TEnv>,
74
+ deps: LazyEvalDeps<TEnv>,
75
+ ): void {
76
+ if (!entry.lazy || entry.lazyEvaluated || !entry.lazyPatterns) {
77
+ return;
78
+ }
79
+
80
+ // Check for pre-computed routes from build-time data.
81
+ // Only leaf nodes (no nested includes) are precomputed, so entries with
82
+ // nested lazy includes fall through to the handler below.
83
+ // When multiple entries share the same staticPrefix (e.g., several
84
+ // include("/", ...) calls), the precomputed data merges all their routes
85
+ // into one entry. Assigning that merged set to the first matching entry
86
+ // causes findMatch to pick the wrong handler for routes belonging to a
87
+ // different include. Skip the shortcut when the prefix is shared.
88
+ const currentPrecomputed = deps.getPrecomputedByPrefix();
89
+ if (currentPrecomputed) {
90
+ const routes = currentPrecomputed.get(entry.staticPrefix);
91
+ if (routes) {
92
+ const prefixIsShared =
93
+ deps.routesEntries.filter((e) => e.staticPrefix === entry.staticPrefix)
94
+ .length > 1;
95
+ if (!prefixIsShared) {
96
+ entry.lazyEvaluated = true;
97
+ entry.routes = routes as ResolvedRouteMap<any>;
98
+ for (const [name, pattern] of Object.entries(routes)) {
99
+ deps.mergedRouteMap[name] = pattern;
100
+ }
101
+ registerRouteMap(deps.mergedRouteMap);
102
+ return;
103
+ }
104
+ }
105
+ }
106
+
107
+ // Mark as evaluated immediately to prevent concurrent evaluation.
108
+ // JS is single-threaded but handlers.handler() could theoretically yield,
109
+ // and the while-loop in findMatch retries after evaluation.
110
+ entry.lazyEvaluated = true;
111
+
112
+ const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
113
+ const lazyContext = entry.lazyContext;
114
+
115
+ // Create a new context for evaluating the lazy patterns
116
+ const manifest = new Map<string, EntryData>();
117
+ const patterns = new Map<string, string>();
118
+ const patternsByPrefix = new Map<string, Map<string, string>>();
119
+ const trailingSlashMap = new Map<string, TrailingSlashMode>();
120
+
121
+ // Capture the handler result to detect nested lazy includes
122
+ let handlerResult: AllUseItems[] = [];
123
+
124
+ // Merge captured counters from include() to maintain consistent
125
+ // shortCode indices with sibling entries from pattern extraction
126
+ const lazyCounters: Record<string, number> = {};
127
+ if (lazyContext && (lazyContext as any).counters) {
128
+ const captured = (lazyContext as any).counters as Record<string, number>;
129
+ for (const [key, value] of Object.entries(captured)) {
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: (lazyContext?.parent as EntryData | null) ?? null,
142
+ counters: lazyCounters,
143
+ cacheProfiles: (lazyContext as any)?.cacheProfiles,
144
+ rootScoped: (lazyContext as any)?.rootScoped,
145
+ },
146
+ () => {
147
+ // Run the lazy patterns handler with the original context prefixes
148
+ // The prefix comes from the IncludeItem stored in lazyPatterns
149
+ const includePrefix = (entry as any)._lazyPrefix || "";
150
+ const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
151
+
152
+ if (fullPrefix || lazyContext?.namePrefix) {
153
+ runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
154
+ handlerResult = lazyPatterns.handler() as AllUseItems[];
155
+ });
156
+ } else {
157
+ handlerResult = lazyPatterns.handler() as AllUseItems[];
158
+ }
159
+ },
160
+ );
161
+
162
+ // Populate the entry's routes from the patterns
163
+ const routesObject: Record<string, string> = {};
164
+ for (const [name, pattern] of patterns.entries()) {
165
+ routesObject[name] = pattern;
166
+ // Also add to merged route map for reverse() support
167
+ const existingPattern = deps.mergedRouteMap[name];
168
+ if (existingPattern !== undefined && existingPattern !== pattern) {
169
+ console.warn(
170
+ `[@rangojs/router] Route name conflict: "${name}" already maps to "${existingPattern}", ` +
171
+ `overwriting with "${pattern}" (from lazy include). Use unique route names to avoid this.`,
172
+ );
173
+ }
174
+ deps.mergedRouteMap[name] = pattern;
175
+ }
176
+
177
+ // Update the entry in-place
178
+ entry.routes = routesObject as ResolvedRouteMap<any>;
179
+
180
+ // Note: Do NOT clear lazyPatterns/lazyContext here.
181
+ // loadManifest() needs them on every request to re-run the handler
182
+ // in the correct AsyncLocalStorage context (Store.manifest).
183
+
184
+ // Update trailing slash config if available
185
+ if (trailingSlashMap.size > 0) {
186
+ entry.trailingSlash = Object.fromEntries(trailingSlashMap);
187
+ }
188
+
189
+ // Detect nested lazy includes and register them as new entries
190
+ const nestedLazyIncludes = findLazyIncludes(handlerResult);
191
+ for (const lazyInclude of nestedLazyIncludes) {
192
+ // Compute the full URL prefix (combining parent prefix if any)
193
+ const fullPrefix = lazyInclude.context.urlPrefix
194
+ ? lazyInclude.context.urlPrefix + lazyInclude.prefix
195
+ : lazyInclude.prefix;
196
+
197
+ const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
198
+ prefix: "",
199
+ staticPrefix: extractStaticPrefix(fullPrefix),
200
+ routes: {} as ResolvedRouteMap<any>, // Empty until first match
201
+ trailingSlash: entry.trailingSlash,
202
+ handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
203
+ mountIndex: deps.nextMountIndex(),
204
+ routerId: deps.routerId,
205
+ // Lazy evaluation fields
206
+ lazy: true,
207
+ lazyPatterns: lazyInclude.patterns,
208
+ lazyContext: lazyInclude.context,
209
+ lazyEvaluated: false,
210
+ // Store the include prefix for evaluation
211
+ _lazyPrefix: lazyInclude.prefix,
212
+ };
213
+ // Insert nested lazy entry before any entry whose staticPrefix is a
214
+ // prefix of (but shorter than) this lazy entry's staticPrefix.
215
+ // This ensures more specific lazy includes are matched before
216
+ // less specific eager entries (e.g., "/href/nested" before "/href/:id").
217
+ const nestedPrefix = nestedEntry.staticPrefix;
218
+ let insertIndex = deps.routesEntries.length;
219
+ if (nestedPrefix) {
220
+ for (let i = 0; i < deps.routesEntries.length; i++) {
221
+ const existing = deps.routesEntries[i]!;
222
+ if (
223
+ nestedPrefix.startsWith(existing.staticPrefix) &&
224
+ nestedPrefix.length > existing.staticPrefix.length
225
+ ) {
226
+ insertIndex = i;
227
+ break;
228
+ }
229
+ }
230
+ }
231
+ deps.routesEntries.splice(insertIndex, 0, nestedEntry);
232
+ }
233
+
234
+ // Re-register route map for runtime reverse() usage
235
+ registerRouteMap(deps.mergedRouteMap);
236
+ }