@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,248 @@
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
+ defaultShouldRevalidate: boolean;
17
+ finalShouldRevalidate: boolean;
18
+ reason: string;
19
+ customRevalidators?: number;
20
+ }
21
+
22
+ export interface RevalidationTraceMeta {
23
+ method: string;
24
+ prevUrl: string;
25
+ nextUrl: string;
26
+ routeKey: string;
27
+ isAction: boolean;
28
+ stale?: boolean;
29
+ }
30
+
31
+ export interface RevalidationTrace {
32
+ meta: RevalidationTraceMeta;
33
+ entries: RevalidationTraceEntry[];
34
+ }
35
+
36
+ // -- Log context --
37
+
38
+ interface RouterLogContext {
39
+ requestId: string;
40
+ transactionId: string;
41
+ depth: number;
42
+ revalidationTrace?: RevalidationTrace;
43
+ }
44
+
45
+ interface RouterLogOptions {
46
+ request: Request;
47
+ transaction: string;
48
+ }
49
+
50
+ interface LogDetails {
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ const routerLogContext = new AsyncLocalStorage<RouterLogContext>();
55
+ const requestIds = new WeakMap<Request, string>();
56
+
57
+ let requestCounter = 0;
58
+ let transactionCounter = 0;
59
+
60
+ function nextId(prefix: string, counter: number): string {
61
+ return `${prefix}${counter.toString(36)}`;
62
+ }
63
+
64
+ function getHeaderRequestId(request: Request): string | null {
65
+ const candidate =
66
+ request.headers.get("x-rsc-router-request-id") ??
67
+ request.headers.get("x-request-id") ??
68
+ request.headers.get("cf-ray");
69
+ if (!candidate) return null;
70
+ const trimmed = candidate.trim();
71
+ return trimmed.length > 0 ? trimmed : null;
72
+ }
73
+
74
+ function getOrCreateRequestId(request: Request): string {
75
+ const existing = requestIds.get(request);
76
+ if (existing) return existing;
77
+
78
+ const fromHeaders = getHeaderRequestId(request);
79
+ if (fromHeaders) {
80
+ requestIds.set(request, fromHeaders);
81
+ return fromHeaders;
82
+ }
83
+
84
+ requestCounter += 1;
85
+ const generated = nextId("req-", requestCounter);
86
+ requestIds.set(request, generated);
87
+ return generated;
88
+ }
89
+
90
+ export function runWithRouterLogContext<T>(
91
+ options: RouterLogOptions,
92
+ fn: () => T,
93
+ ): T {
94
+ if (!INTERNAL_RANGO_DEBUG) {
95
+ return fn();
96
+ }
97
+
98
+ const requestId = getOrCreateRequestId(options.request);
99
+ transactionCounter += 1;
100
+ const transactionId = `${options.transaction}-${nextId("tx-", transactionCounter)}`;
101
+
102
+ return routerLogContext.run(
103
+ {
104
+ requestId,
105
+ transactionId,
106
+ depth: 0,
107
+ },
108
+ fn,
109
+ );
110
+ }
111
+
112
+ export function withRouterLogScope<T>(
113
+ label: string,
114
+ fn: () => Promise<T>,
115
+ ): Promise<T>;
116
+ export function withRouterLogScope<T>(label: string, fn: () => T): T;
117
+ export function withRouterLogScope<T>(
118
+ label: string,
119
+ fn: () => Promise<T> | T,
120
+ ): Promise<T> | T {
121
+ const ctx = routerLogContext.getStore();
122
+ if (!INTERNAL_RANGO_DEBUG || !ctx) {
123
+ return fn();
124
+ }
125
+
126
+ debugLog(label, "start");
127
+
128
+ return routerLogContext.run({ ...ctx, depth: ctx.depth + 1 }, () => {
129
+ try {
130
+ const result = fn();
131
+ if (result && typeof (result as Promise<T>).then === "function") {
132
+ return (result as Promise<T>).then(
133
+ (value) => {
134
+ debugLog(label, "end");
135
+ return value;
136
+ },
137
+ (error) => {
138
+ debugLog(label, "error", { error: String(error) });
139
+ throw error;
140
+ },
141
+ );
142
+ }
143
+ debugLog(label, "end");
144
+ return result;
145
+ } catch (error) {
146
+ debugLog(label, "error", { error: String(error) });
147
+ throw error;
148
+ }
149
+ });
150
+ }
151
+
152
+ export function isRouterDebugEnabled(): boolean {
153
+ return INTERNAL_RANGO_DEBUG && !!routerLogContext.getStore();
154
+ }
155
+
156
+ function formatPrefix(scope: string): string {
157
+ const ctx = routerLogContext.getStore();
158
+ if (!ctx) return `[Router][${scope}]`;
159
+ const indent = " ".repeat(ctx.depth);
160
+ return `[Router][req:${ctx.requestId}][tx:${ctx.transactionId}] ${indent}[${scope}]`;
161
+ }
162
+
163
+ export function debugLog(
164
+ scope: string,
165
+ message: string,
166
+ details?: LogDetails,
167
+ ): void {
168
+ if (!isRouterDebugEnabled()) return;
169
+
170
+ const prefix = formatPrefix(scope);
171
+ if (details) {
172
+ console.log(`${prefix} ${message}`, details);
173
+ return;
174
+ }
175
+
176
+ console.log(`${prefix} ${message}`);
177
+ }
178
+
179
+ export function debugWarn(
180
+ scope: string,
181
+ message: string,
182
+ details?: LogDetails,
183
+ ): void {
184
+ if (!isRouterDebugEnabled()) return;
185
+
186
+ const prefix = formatPrefix(scope);
187
+ if (details) {
188
+ console.warn(`${prefix} ${message}`, details);
189
+ return;
190
+ }
191
+
192
+ console.warn(`${prefix} ${message}`);
193
+ }
194
+
195
+ // -- Revalidation trace helpers --
196
+
197
+ export function isTraceActive(): boolean {
198
+ if (!INTERNAL_RANGO_DEBUG) return false;
199
+ const ctx = routerLogContext.getStore();
200
+ return !!ctx?.revalidationTrace;
201
+ }
202
+
203
+ export function startRevalidationTrace(meta: RevalidationTraceMeta): void {
204
+ const ctx = routerLogContext.getStore();
205
+ if (!ctx || !INTERNAL_RANGO_DEBUG) return;
206
+ ctx.revalidationTrace = { meta, entries: [] };
207
+ }
208
+
209
+ export function pushRevalidationTraceEntry(
210
+ entry: RevalidationTraceEntry,
211
+ ): void {
212
+ const ctx = routerLogContext.getStore();
213
+ if (!ctx?.revalidationTrace) return;
214
+ ctx.revalidationTrace.entries.push(entry);
215
+ }
216
+
217
+ export function flushRevalidationTrace(): RevalidationTrace | null {
218
+ const ctx = routerLogContext.getStore();
219
+ if (!ctx?.revalidationTrace) return null;
220
+ const trace = ctx.revalidationTrace;
221
+ ctx.revalidationTrace = undefined;
222
+
223
+ if (trace.entries.length === 0) return trace;
224
+
225
+ const revalidated = trace.entries.filter((e) => e.finalShouldRevalidate);
226
+ const skipped = trace.entries.filter((e) => !e.finalShouldRevalidate);
227
+
228
+ debugLog("revalidation-trace", "flush", {
229
+ method: trace.meta.method,
230
+ routeKey: trace.meta.routeKey,
231
+ isAction: trace.meta.isAction,
232
+ stale: trace.meta.stale,
233
+ prevUrl: trace.meta.prevUrl,
234
+ nextUrl: trace.meta.nextUrl,
235
+ total: trace.entries.length,
236
+ revalidated: revalidated.length,
237
+ skipped: skipped.length,
238
+ entries: trace.entries.map((e) => ({
239
+ segmentId: e.segmentId,
240
+ type: e.segmentType,
241
+ source: e.source,
242
+ revalidate: e.finalShouldRevalidate,
243
+ reason: e.reason,
244
+ })),
245
+ });
246
+
247
+ return trace;
248
+ }
@@ -6,38 +6,83 @@
6
6
 
7
7
  import { invariant, RouteNotFoundError } from "../errors";
8
8
  import { createRouteHelpers } from "../route-definition";
9
- import { getContext, type EntryData, type MetricsStore } from "../server/context";
9
+ import {
10
+ getContext,
11
+ runWithPrefixes,
12
+ type EntryData,
13
+ type MetricsStore,
14
+ } from "../server/context";
10
15
  import MapRootLayout from "../server/root-layout";
11
16
  import type { RouteEntry } from "../types";
17
+ import type { UrlPatterns } from "../urls";
18
+ import { VERSION } from "@rangojs/router:version";
12
19
 
13
- /**
14
- * Module-level cache for manifests per mount index.
15
- * Only used in production - dev mode skips caching for HMR support.
16
- */
17
- const manifestCache = new Map<number, Map<string, EntryData>>();
20
+ // Module-level manifest cache: avoids re-executing DSL handler on every request.
21
+ // Handler execution is deterministic (components, loaders, middleware are module-level
22
+ // stable references), so the resulting EntryData tree can be safely cached and reused
23
+ // across requests within the same isolate.
24
+ //
25
+ // Cache is keyed by (VERSION, mountIndex, routeKey, isSSR). VERSION comes from the
26
+ // @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
27
+ // When VERSION changes, this module re-evaluates and the cache is recreated empty.
28
+ // Including VERSION in the key is additional defense against stale entries.
29
+ const manifestModuleCache = new Map<string, Map<string, EntryData>>();
18
30
 
19
31
  /**
20
32
  * Load manifest from route entry with AsyncLocalStorage context
21
33
  * Handles lazy imports, unwrapping, and validation
34
+ *
35
+ * Results are cached at module level after first execution. Subsequent calls
36
+ * for the same (routeKey, isSSR) within the same isolate return cached data
37
+ * without re-executing the DSL handler.
38
+ */
39
+ /**
40
+ * Clear the module-level manifest cache.
41
+ * Called on HMR to ensure stale handler references are discarded.
22
42
  */
43
+ export function clearManifestCache(): void {
44
+ manifestModuleCache.clear();
45
+ }
46
+
23
47
  export async function loadManifest(
24
48
  entry: RouteEntry<any>,
25
49
  routeKey: string,
26
50
  path: string,
27
51
  metricsStore?: MetricsStore,
28
- isSSR?: boolean
52
+ isSSR?: boolean,
29
53
  ): Promise<EntryData> {
54
+ // Helper to push a metric entry
55
+ const pushMetric = metricsStore
56
+ ? (label: string, start: number) => {
57
+ metricsStore.metrics.push({
58
+ label,
59
+ duration: performance.now() - start,
60
+ startTime: start - metricsStore.requestStart,
61
+ });
62
+ }
63
+ : undefined;
64
+
30
65
  const mountIndex = entry.mountIndex;
31
- const isDev = process.env.NODE_ENV !== "production";
32
66
 
33
- // In production, check cache first
34
- if (!isDev) {
35
- const cachedManifest = manifestCache.get(mountIndex);
36
- if (cachedManifest && cachedManifest.has(routeKey)) {
37
- return cachedManifest.get(routeKey)!;
67
+ // Check module-level cache (persists across requests within same isolate)
68
+ const cacheKey = `${VERSION}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
69
+ const cached = manifestModuleCache.get(cacheKey);
70
+ if (cached) {
71
+ const cacheStart = performance.now();
72
+ // Set up Store for downstream consumers (segment resolution reads Store.manifest)
73
+ const Store = getContext().getOrCreateStore(routeKey);
74
+ Store.mountIndex = mountIndex;
75
+ Store.isSSR = isSSR;
76
+ if (metricsStore) Store.metrics = metricsStore;
77
+ // Restore cached manifest into Store
78
+ for (const [k, v] of cached) {
79
+ Store.manifest.set(k, v);
38
80
  }
81
+ pushMetric?.("manifest:cache-hit", cacheStart);
82
+ return cached.get(routeKey)!;
39
83
  }
40
84
 
85
+ const storeSetupStart = performance.now();
41
86
  const Store = getContext().getOrCreateStore(routeKey);
42
87
 
43
88
  // Set mount index in store for unique shortCode prefixes
@@ -51,23 +96,81 @@ export async function loadManifest(
51
96
  Store.metrics = metricsStore;
52
97
  }
53
98
 
99
+ pushMetric?.("manifest:store-setup", storeSetupStart);
100
+
54
101
  // Clear manifest before rebuilding to prevent stale entry mutations
102
+ const clearStart = performance.now();
55
103
  Store.manifest.clear();
104
+ pushMetric?.("manifest:clear", clearStart);
56
105
 
57
106
  try {
58
107
  // Include mountIndex in namespace to ensure unique cache keys per mount
59
- const namespaceWithMount = mountIndex !== undefined
60
- ? `#router.M${mountIndex}`
61
- : "#router";
108
+ const namespaceWithMount =
109
+ mountIndex !== undefined ? `#router.M${mountIndex}` : "#router";
110
+
111
+ // For lazy entries, use the captured parent from include() context
112
+ // This ensures routes are registered under the correct layout hierarchy
113
+ const lazyContext =
114
+ entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
115
+ const parentForContext =
116
+ (lazyContext?.parent as EntryData | null) ?? Store.parent;
117
+
118
+ // For lazy entries, merge captured counters from include() so the
119
+ // handler's entries get shortCode indices after sibling entries that
120
+ // were created during pattern extraction. This prevents shortCode
121
+ // collisions between lazy and non-lazy entries under the same parent
122
+ // (e.g., ArticlesLayout and BlogLayout both under NavLayout).
123
+ if (lazyContext && (lazyContext as any).counters) {
124
+ const captured = (lazyContext as any).counters as Record<string, number>;
125
+ for (const [key, value] of Object.entries(captured)) {
126
+ Store.counters[key] = Math.max(Store.counters[key] ?? 0, value);
127
+ }
128
+ }
62
129
 
130
+ // Propagate cache profiles for DSL-time cache("profileName") resolution.
131
+ // Non-lazy entries carry profiles directly; lazy entries carry them
132
+ // in the captured lazyContext from include() time.
133
+ const entryProfiles =
134
+ entry.cacheProfiles ?? (lazyContext as any)?.cacheProfiles;
135
+ if (entryProfiles) {
136
+ Store.cacheProfiles = entryProfiles;
137
+ }
138
+
139
+ // Propagate rootScoped from lazyContext so that routes inside
140
+ // nested { name: "sub" } under { name: "" } keep inherited root scope
141
+ // when the manifest is rebuilt on each request.
142
+ if (lazyContext && (lazyContext as any).rootScoped !== undefined) {
143
+ Store.rootScoped = (lazyContext as any).rootScoped;
144
+ }
145
+
146
+ const handlerExecStart = performance.now();
63
147
  const useItems = await getContext().runWithStore(
64
148
  Store,
65
149
  Store.namespace || namespaceWithMount,
66
- Store.parent,
150
+ parentForContext,
67
151
  async () => {
68
152
  // Create helpers for lazy-loaded handlers that need them
69
153
  const helpers = createRouteHelpers();
70
154
 
155
+ // For lazy entries, use lazyPatterns.handler() with proper prefixes.
156
+ // Do NOT wrap in MapRootLayout here: the captured parent chain from
157
+ // pattern extraction already includes the synthetic MapRootLayout
158
+ // parent, so adding another would create an extra level that does
159
+ // not exist in the non-lazy (root handler) path and would produce
160
+ // mismatched shortCodes.
161
+ if (entry.lazy && entry.lazyPatterns) {
162
+ const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
163
+ const includePrefix = (entry as any)._lazyPrefix || "";
164
+ const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
165
+
166
+ if (fullPrefix || lazyContext?.namePrefix) {
167
+ return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
168
+ lazyPatterns.handler(),
169
+ );
170
+ }
171
+ return lazyPatterns.handler();
172
+ }
173
+
71
174
  // Wrap handler execution in root layout so routes get correct parent
72
175
  // This ensures all routes are registered with the layout as their parent
73
176
  let promiseResult: Promise<any> | null = null;
@@ -91,40 +194,60 @@ export async function loadManifest(
91
194
  "default" in load
92
195
  ) {
93
196
  // Promise<{ default: () => Array }> - e.g., dynamic import
94
- // Lazy-loaded handlers may need helpers (passed as optional arg)
197
+ if (typeof load.default !== "function") {
198
+ throw new Error(
199
+ `[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
200
+ `got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
201
+ );
202
+ }
95
203
  return (load.default as (h?: any) => any)(helpers);
96
204
  }
97
205
  if (typeof load === "function") {
98
206
  // Promise<() => Array>
99
207
  return (load as (h?: any) => any)(helpers);
100
208
  }
101
- // Promise<Array> - direct array from async handler
102
- return load;
209
+ // Reject unsupported async handler results. Supported shapes are:
210
+ // Promise<{ default: fn }> — dynamic import
211
+ // Promise<fn> — lazy function
212
+ // Direct Promise<Array> is not supported; use a function wrapper.
213
+ throw new Error(
214
+ `[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
215
+ `Lazy route handlers must resolve to a function or { default: fn }, ` +
216
+ `not a direct array. Wrap your handler: () => import('./urls') or ` +
217
+ `() => Promise.resolve((h) => [...])`,
218
+ );
103
219
  }
104
220
 
105
221
  // Inline handler - routes were registered with correct parent inside layout
106
222
  return [wrappedItems].flat(3);
107
- }
223
+ },
108
224
  );
225
+ pushMetric?.("manifest:handler-exec", handlerExecStart);
109
226
 
227
+ const validationStart = performance.now();
110
228
  invariant(
111
229
  useItems && useItems.length > 0,
112
- "Did not receive any handler from router.map()"
113
- );
114
- invariant(
115
- useItems.some((item: { type: string }) => item.type === "layout"),
116
- "Top-level handler must be a layout"
230
+ "Did not receive any handler from router.map()",
117
231
  );
232
+ // For non-lazy entries the root handler is wrapped in MapRootLayout,
233
+ // so the result always contains a layout item. Lazy entries run the
234
+ // included patterns handler directly (no MapRootLayout wrapper) so
235
+ // we skip this check -- the layout is in the captured parent chain.
236
+ if (!lazyContext) {
237
+ invariant(
238
+ useItems.some((item: { type: string }) => item.type === "layout"),
239
+ "Top-level handler must be a layout",
240
+ );
241
+ }
118
242
 
119
243
  invariant(
120
244
  Store.manifest.has(routeKey),
121
- `Route must be registered for ${routeKey}`
245
+ `Route must be registered for ${routeKey}`,
122
246
  );
247
+ pushMetric?.("manifest:validation", validationStart);
123
248
 
124
- // Cache manifest in production after successful build
125
- if (!isDev) {
126
- manifestCache.set(mountIndex, new Map(Store.manifest));
127
- }
249
+ // Cache manifest for future requests in this isolate
250
+ manifestModuleCache.set(cacheKey, new Map(Store.manifest));
128
251
 
129
252
  return Store.manifest.get(routeKey)!;
130
253
  } catch (e) {
@@ -138,7 +261,7 @@ export async function loadManifest(
138
261
  routeKey,
139
262
  },
140
263
  },
141
- }
264
+ },
142
265
  );
143
266
  }
144
267
  }