@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,251 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
+
4
+ // -- Revalidation trace types --
5
+
6
+ export interface RevalidationTraceEntry {
7
+ segmentId: string;
8
+ segmentType: string;
9
+ belongsToRoute: boolean;
10
+ source:
11
+ | "segment-resolution"
12
+ | "cache-hit"
13
+ | "loader"
14
+ | "parallel"
15
+ | "orphan-layout"
16
+ | "route-handler"
17
+ | "layout-handler"
18
+ | "intercept-loader";
19
+ defaultShouldRevalidate: boolean;
20
+ finalShouldRevalidate: boolean;
21
+ reason: string;
22
+ customRevalidators?: number;
23
+ }
24
+
25
+ export interface RevalidationTraceMeta {
26
+ method: string;
27
+ prevUrl: string;
28
+ nextUrl: string;
29
+ routeKey: string;
30
+ isAction: boolean;
31
+ stale?: boolean;
32
+ }
33
+
34
+ export interface RevalidationTrace {
35
+ meta: RevalidationTraceMeta;
36
+ entries: RevalidationTraceEntry[];
37
+ }
38
+
39
+ // -- Log context --
40
+
41
+ interface RouterLogContext {
42
+ requestId: string;
43
+ transactionId: string;
44
+ depth: number;
45
+ revalidationTrace?: RevalidationTrace;
46
+ }
47
+
48
+ interface RouterLogOptions {
49
+ request: Request;
50
+ transaction: string;
51
+ }
52
+
53
+ interface LogDetails {
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ const routerLogContext = new AsyncLocalStorage<RouterLogContext>();
58
+ const requestIds = new WeakMap<Request, string>();
59
+
60
+ let requestCounter = 0;
61
+ let transactionCounter = 0;
62
+
63
+ function nextId(prefix: string, counter: number): string {
64
+ return `${prefix}${counter.toString(36)}`;
65
+ }
66
+
67
+ function getHeaderRequestId(request: Request): string | null {
68
+ const candidate =
69
+ request.headers.get("x-rsc-router-request-id") ??
70
+ request.headers.get("x-request-id") ??
71
+ request.headers.get("cf-ray");
72
+ if (!candidate) return null;
73
+ const trimmed = candidate.trim();
74
+ return trimmed.length > 0 ? trimmed : null;
75
+ }
76
+
77
+ export function getOrCreateRequestId(request: Request): string {
78
+ const existing = requestIds.get(request);
79
+ if (existing) return existing;
80
+
81
+ const fromHeaders = getHeaderRequestId(request);
82
+ if (fromHeaders) {
83
+ requestIds.set(request, fromHeaders);
84
+ return fromHeaders;
85
+ }
86
+
87
+ requestCounter += 1;
88
+ const generated = nextId("req-", requestCounter);
89
+ requestIds.set(request, generated);
90
+ return generated;
91
+ }
92
+
93
+ export function runWithRouterLogContext<T>(
94
+ options: RouterLogOptions,
95
+ fn: () => T,
96
+ ): T {
97
+ if (!INTERNAL_RANGO_DEBUG) {
98
+ return fn();
99
+ }
100
+
101
+ const requestId = getOrCreateRequestId(options.request);
102
+ transactionCounter += 1;
103
+ const transactionId = `${options.transaction}-${nextId("tx-", transactionCounter)}`;
104
+
105
+ return routerLogContext.run(
106
+ {
107
+ requestId,
108
+ transactionId,
109
+ depth: 0,
110
+ },
111
+ fn,
112
+ );
113
+ }
114
+
115
+ export function withRouterLogScope<T>(
116
+ label: string,
117
+ fn: () => Promise<T>,
118
+ ): Promise<T>;
119
+ export function withRouterLogScope<T>(label: string, fn: () => T): T;
120
+ export function withRouterLogScope<T>(
121
+ label: string,
122
+ fn: () => Promise<T> | T,
123
+ ): Promise<T> | T {
124
+ const ctx = routerLogContext.getStore();
125
+ if (!INTERNAL_RANGO_DEBUG || !ctx) {
126
+ return fn();
127
+ }
128
+
129
+ debugLog(label, "start");
130
+
131
+ return routerLogContext.run({ ...ctx, depth: ctx.depth + 1 }, () => {
132
+ try {
133
+ const result = fn();
134
+ if (result && typeof (result as Promise<T>).then === "function") {
135
+ return (result as Promise<T>).then(
136
+ (value) => {
137
+ debugLog(label, "end");
138
+ return value;
139
+ },
140
+ (error) => {
141
+ debugLog(label, "error", { error: String(error) });
142
+ throw error;
143
+ },
144
+ );
145
+ }
146
+ debugLog(label, "end");
147
+ return result;
148
+ } catch (error) {
149
+ debugLog(label, "error", { error: String(error) });
150
+ throw error;
151
+ }
152
+ });
153
+ }
154
+
155
+ export function isRouterDebugEnabled(): boolean {
156
+ return INTERNAL_RANGO_DEBUG && !!routerLogContext.getStore();
157
+ }
158
+
159
+ function formatPrefix(scope: string): string {
160
+ const ctx = routerLogContext.getStore();
161
+ if (!ctx) return `[Router][${scope}]`;
162
+ const indent = " ".repeat(ctx.depth);
163
+ return `[Router][req:${ctx.requestId}][tx:${ctx.transactionId}] ${indent}[${scope}]`;
164
+ }
165
+
166
+ export function debugLog(
167
+ scope: string,
168
+ message: string,
169
+ details?: LogDetails,
170
+ ): void {
171
+ if (!isRouterDebugEnabled()) return;
172
+
173
+ const prefix = formatPrefix(scope);
174
+ if (details) {
175
+ console.log(`${prefix} ${message}`, details);
176
+ return;
177
+ }
178
+
179
+ console.log(`${prefix} ${message}`);
180
+ }
181
+
182
+ export function debugWarn(
183
+ scope: string,
184
+ message: string,
185
+ details?: LogDetails,
186
+ ): void {
187
+ if (!isRouterDebugEnabled()) return;
188
+
189
+ const prefix = formatPrefix(scope);
190
+ if (details) {
191
+ console.warn(`${prefix} ${message}`, details);
192
+ return;
193
+ }
194
+
195
+ console.warn(`${prefix} ${message}`);
196
+ }
197
+
198
+ // -- Revalidation trace helpers --
199
+
200
+ export function isTraceActive(): boolean {
201
+ if (!INTERNAL_RANGO_DEBUG) return false;
202
+ const ctx = routerLogContext.getStore();
203
+ return !!ctx?.revalidationTrace;
204
+ }
205
+
206
+ export function startRevalidationTrace(meta: RevalidationTraceMeta): void {
207
+ const ctx = routerLogContext.getStore();
208
+ if (!ctx || !INTERNAL_RANGO_DEBUG) return;
209
+ ctx.revalidationTrace = { meta, entries: [] };
210
+ }
211
+
212
+ export function pushRevalidationTraceEntry(
213
+ entry: RevalidationTraceEntry,
214
+ ): void {
215
+ const ctx = routerLogContext.getStore();
216
+ if (!ctx?.revalidationTrace) return;
217
+ ctx.revalidationTrace.entries.push(entry);
218
+ }
219
+
220
+ export function flushRevalidationTrace(): RevalidationTrace | null {
221
+ const ctx = routerLogContext.getStore();
222
+ if (!ctx?.revalidationTrace) return null;
223
+ const trace = ctx.revalidationTrace;
224
+ ctx.revalidationTrace = undefined;
225
+
226
+ if (trace.entries.length === 0) return trace;
227
+
228
+ const revalidated = trace.entries.filter((e) => e.finalShouldRevalidate);
229
+ const skipped = trace.entries.filter((e) => !e.finalShouldRevalidate);
230
+
231
+ debugLog("revalidation-trace", "flush", {
232
+ method: trace.meta.method,
233
+ routeKey: trace.meta.routeKey,
234
+ isAction: trace.meta.isAction,
235
+ stale: trace.meta.stale,
236
+ prevUrl: trace.meta.prevUrl,
237
+ nextUrl: trace.meta.nextUrl,
238
+ total: trace.entries.length,
239
+ revalidated: revalidated.length,
240
+ skipped: skipped.length,
241
+ entries: trace.entries.map((e) => ({
242
+ segmentId: e.segmentId,
243
+ type: e.segmentType,
244
+ source: e.source,
245
+ revalidate: e.finalShouldRevalidate,
246
+ reason: e.reason,
247
+ })),
248
+ });
249
+
250
+ return trace;
251
+ }
@@ -6,27 +6,86 @@
6
6
 
7
7
  import { invariant, RouteNotFoundError } from "../errors";
8
8
  import { createRouteHelpers } from "../route-definition";
9
- import { getContext, runWithPrefixes, type EntryData, type MetricsStore } from "../server/context";
9
+ import {
10
+ getContext,
11
+ runWithPrefixes,
12
+ getIsolatedLazyParent,
13
+ type EntryData,
14
+ type MetricsStore,
15
+ } from "../server/context";
10
16
  import MapRootLayout from "../server/root-layout";
11
17
  import type { RouteEntry } from "../types";
12
18
  import type { UrlPatterns } from "../urls";
19
+ import { VERSION } from "@rangojs/router:version";
20
+
21
+ // Module-level manifest cache: avoids re-executing DSL handler on every request.
22
+ // Handler execution is deterministic (components, loaders, middleware are module-level
23
+ // stable references), so the resulting EntryData tree can be safely cached and reused
24
+ // across requests within the same isolate.
25
+ //
26
+ // Cache is keyed by (VERSION, mountIndex, routeKey, isSSR). VERSION comes from the
27
+ // @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
28
+ // When VERSION changes, this module re-evaluates and the cache is recreated empty.
29
+ // Including VERSION in the key is additional defense against stale entries.
30
+ const manifestModuleCache = new Map<string, Map<string, EntryData>>();
13
31
 
14
32
  /**
15
33
  * Load manifest from route entry with AsyncLocalStorage context
16
34
  * Handles lazy imports, unwrapping, and validation
17
35
  *
18
- * Note: We don't cache manifests at the module level because includes are
19
- * lazily evaluated. Caching an incomplete manifest would cause cache misses
20
- * for routes in not-yet-evaluated includes.
36
+ * Results are cached at module level after first execution. Subsequent calls
37
+ * for the same (routeKey, isSSR) within the same isolate return cached data
38
+ * without re-executing the DSL handler.
39
+ */
40
+ /**
41
+ * Clear the module-level manifest cache.
42
+ * Called on HMR to ensure stale handler references are discarded.
21
43
  */
44
+ export function clearManifestCache(): void {
45
+ manifestModuleCache.clear();
46
+ }
47
+
22
48
  export async function loadManifest(
23
49
  entry: RouteEntry<any>,
24
50
  routeKey: string,
25
51
  path: string,
26
52
  metricsStore?: MetricsStore,
27
- isSSR?: boolean
53
+ isSSR?: boolean,
28
54
  ): Promise<EntryData> {
55
+ // Helper to push a metric entry
56
+ const pushMetric = metricsStore
57
+ ? (label: string, start: number) => {
58
+ metricsStore.metrics.push({
59
+ label,
60
+ duration: performance.now() - start,
61
+ startTime: start - metricsStore.requestStart,
62
+ });
63
+ }
64
+ : undefined;
65
+
29
66
  const mountIndex = entry.mountIndex;
67
+
68
+ // Check module-level cache (persists across requests within same isolate)
69
+ // Include routerId so multi-router setups (host routing) don't share cached
70
+ // EntryData across routers with overlapping mountIndex + routeKey combinations.
71
+ const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
72
+ const cached = manifestModuleCache.get(cacheKey);
73
+ if (cached) {
74
+ const cacheStart = performance.now();
75
+ // Set up Store for downstream consumers (segment resolution reads Store.manifest)
76
+ const Store = getContext().getOrCreateStore(routeKey);
77
+ Store.mountIndex = mountIndex;
78
+ Store.isSSR = isSSR;
79
+ if (metricsStore) Store.metrics = metricsStore;
80
+ // Restore cached manifest into Store
81
+ for (const [k, v] of cached) {
82
+ Store.manifest.set(k, v);
83
+ }
84
+ pushMetric?.("manifest:cache-hit", cacheStart);
85
+ return cached.get(routeKey)!;
86
+ }
87
+
88
+ const storeSetupStart = performance.now();
30
89
  const Store = getContext().getOrCreateStore(routeKey);
31
90
 
32
91
  // Set mount index in store for unique shortCode prefixes
@@ -40,20 +99,57 @@ export async function loadManifest(
40
99
  Store.metrics = metricsStore;
41
100
  }
42
101
 
102
+ pushMetric?.("manifest:store-setup", storeSetupStart);
103
+
43
104
  // Clear manifest before rebuilding to prevent stale entry mutations
105
+ const clearStart = performance.now();
44
106
  Store.manifest.clear();
107
+ pushMetric?.("manifest:clear", clearStart);
45
108
 
46
109
  try {
47
110
  // Include mountIndex in namespace to ensure unique cache keys per mount
48
- const namespaceWithMount = mountIndex !== undefined
49
- ? `#router.M${mountIndex}`
50
- : "#router";
111
+ const namespaceWithMount =
112
+ mountIndex !== undefined ? `#router.M${mountIndex}` : "#router";
51
113
 
52
114
  // For lazy entries, use the captured parent from include() context
53
115
  // This ensures routes are registered under the correct layout hierarchy
54
- const lazyContext = entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
55
- const parentForContext = lazyContext?.parent as EntryData | null ?? Store.parent;
116
+ const lazyContext =
117
+ entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
118
+ const parentForContext = lazyContext
119
+ ? getIsolatedLazyParent(
120
+ (lazyContext.parent as EntryData | null) ?? Store.parent,
121
+ )
122
+ : Store.parent;
123
+
124
+ // For lazy entries, merge captured counters from include() so the
125
+ // handler's entries get shortCode indices after sibling entries that
126
+ // were created during pattern extraction. This prevents shortCode
127
+ // collisions between lazy and non-lazy entries under the same parent
128
+ // (e.g., ArticlesLayout and BlogLayout both under NavLayout).
129
+ if (lazyContext && (lazyContext as any).counters) {
130
+ const captured = (lazyContext as any).counters as Record<string, number>;
131
+ for (const [key, value] of Object.entries(captured)) {
132
+ Store.counters[key] = Math.max(Store.counters[key] ?? 0, value);
133
+ }
134
+ }
56
135
 
136
+ // Propagate cache profiles for DSL-time cache("profileName") resolution.
137
+ // Non-lazy entries carry profiles directly; lazy entries carry them
138
+ // in the captured lazyContext from include() time.
139
+ const entryProfiles =
140
+ entry.cacheProfiles ?? (lazyContext as any)?.cacheProfiles;
141
+ if (entryProfiles) {
142
+ Store.cacheProfiles = entryProfiles;
143
+ }
144
+
145
+ // Propagate rootScoped from lazyContext so that routes inside
146
+ // nested { name: "sub" } under { name: "" } keep inherited root scope
147
+ // when the manifest is rebuilt on each request.
148
+ if (lazyContext && (lazyContext as any).rootScoped !== undefined) {
149
+ Store.rootScoped = (lazyContext as any).rootScoped;
150
+ }
151
+
152
+ const handlerExecStart = performance.now();
57
153
  const useItems = await getContext().runWithStore(
58
154
  Store,
59
155
  Store.namespace || namespaceWithMount,
@@ -62,25 +158,23 @@ export async function loadManifest(
62
158
  // Create helpers for lazy-loaded handlers that need them
63
159
  const helpers = createRouteHelpers();
64
160
 
65
- // For lazy entries, use lazyPatterns.handler() with proper prefixes
161
+ // For lazy entries, use lazyPatterns.handler() with proper prefixes.
162
+ // Do NOT wrap in MapRootLayout here: the captured parent chain from
163
+ // pattern extraction already includes the synthetic MapRootLayout
164
+ // parent, so adding another would create an extra level that does
165
+ // not exist in the non-lazy (root handler) path and would produce
166
+ // mismatched shortCodes.
66
167
  if (entry.lazy && entry.lazyPatterns) {
67
168
  const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
68
169
  const includePrefix = (entry as any)._lazyPrefix || "";
69
170
  const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
70
171
 
71
- // Wrap in root layout and run with prefixes
72
- const wrappedItems = helpers.layout(MapRootLayout, () => {
73
- if (fullPrefix || lazyContext?.namePrefix) {
74
- return runWithPrefixes(
75
- fullPrefix,
76
- lazyContext?.namePrefix,
77
- () => lazyPatterns.handler()
78
- );
79
- }
80
- return lazyPatterns.handler();
81
- });
82
-
83
- return [wrappedItems].flat(3);
172
+ if (fullPrefix || lazyContext?.namePrefix) {
173
+ return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
174
+ lazyPatterns.handler(),
175
+ );
176
+ }
177
+ return lazyPatterns.handler();
84
178
  }
85
179
 
86
180
  // Wrap handler execution in root layout so routes get correct parent
@@ -106,35 +200,60 @@ export async function loadManifest(
106
200
  "default" in load
107
201
  ) {
108
202
  // Promise<{ default: () => Array }> - e.g., dynamic import
109
- // Lazy-loaded handlers may need helpers (passed as optional arg)
203
+ if (typeof load.default !== "function") {
204
+ throw new Error(
205
+ `[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
206
+ `got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
207
+ );
208
+ }
110
209
  return (load.default as (h?: any) => any)(helpers);
111
210
  }
112
211
  if (typeof load === "function") {
113
212
  // Promise<() => Array>
114
213
  return (load as (h?: any) => any)(helpers);
115
214
  }
116
- // Promise<Array> - direct array from async handler
117
- return load;
215
+ // Reject unsupported async handler results. Supported shapes are:
216
+ // Promise<{ default: fn }> — dynamic import
217
+ // Promise<fn> — lazy function
218
+ // Direct Promise<Array> is not supported; use a function wrapper.
219
+ throw new Error(
220
+ `[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
221
+ `Lazy route handlers must resolve to a function or { default: fn }, ` +
222
+ `not a direct array. Wrap your handler: () => import('./urls') or ` +
223
+ `() => Promise.resolve((h) => [...])`,
224
+ );
118
225
  }
119
226
 
120
227
  // Inline handler - routes were registered with correct parent inside layout
121
228
  return [wrappedItems].flat(3);
122
- }
229
+ },
123
230
  );
231
+ pushMetric?.("manifest:handler-exec", handlerExecStart);
124
232
 
233
+ const validationStart = performance.now();
125
234
  invariant(
126
235
  useItems && useItems.length > 0,
127
- "Did not receive any handler from router.map()"
128
- );
129
- invariant(
130
- useItems.some((item: { type: string }) => item.type === "layout"),
131
- "Top-level handler must be a layout"
236
+ "Did not receive any handler from router.map()",
132
237
  );
238
+ // For non-lazy entries the root handler is wrapped in MapRootLayout,
239
+ // so the result always contains a layout item. Lazy entries run the
240
+ // included patterns handler directly (no MapRootLayout wrapper) so
241
+ // we skip this check -- the layout is in the captured parent chain.
242
+ if (!lazyContext) {
243
+ invariant(
244
+ useItems.some((item: { type: string }) => item.type === "layout"),
245
+ "Top-level handler must be a layout",
246
+ );
247
+ }
133
248
 
134
249
  invariant(
135
250
  Store.manifest.has(routeKey),
136
- `Route must be registered for ${routeKey}`
251
+ `Route must be registered for ${routeKey}`,
137
252
  );
253
+ pushMetric?.("manifest:validation", validationStart);
254
+
255
+ // Cache manifest for future requests in this isolate
256
+ manifestModuleCache.set(cacheKey, new Map(Store.manifest));
138
257
 
139
258
  return Store.manifest.get(routeKey)!;
140
259
  } catch (e) {
@@ -148,7 +267,7 @@ export async function loadManifest(
148
267
  routeKey,
149
268
  },
150
269
  },
151
- }
270
+ },
152
271
  );
153
272
  }
154
273
  }