@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  } from "../server/context";
17
17
  import { invariant } from "../errors";
18
18
  import { isCachedFunction } from "../cache/taint.js";
19
- import { getCacheProfile } from "../cache/profile-registry.js";
19
+ import { RSCRouterContext } from "../server/context";
20
20
  import { isStaticHandler } from "../static-handler.js";
21
21
  import RootLayout from "../server/root-layout";
22
22
  import type {
@@ -227,7 +227,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
227
227
  children = undefined;
228
228
  } else if (typeof optionsOrChildren === "string") {
229
229
  // cache('profileName') or cache('profileName', () => [...])
230
- const profile = getCacheProfile(optionsOrChildren);
230
+ // Resolve from context-scoped profiles (set per-router via HelperContext).
231
+ const ctxStore = RSCRouterContext.getStore();
232
+ const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
231
233
  invariant(
232
234
  profile,
233
235
  `cache("${optionsOrChildren}"): unknown cache profile. ` +
@@ -245,7 +247,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
245
247
  children = maybeChildren;
246
248
  }
247
249
 
248
- const name = `$${store.getNextIndex("cache")}`;
250
+ // Allocate a single index for this cache() call (used in all paths)
251
+ const cacheIndex = store.getNextIndex("cache");
252
+ const name = `$${cacheIndex}`;
249
253
  const cacheConfig = { options };
250
254
 
251
255
  // If no children, create an orphan cache entry (like orphan layouts)
@@ -262,7 +266,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
262
266
 
263
267
  // Create orphan cache entry (like orphan layout)
264
268
  // Subsequent siblings in the same array will attach to this entry
265
- const namespace = `${ctx.namespace}.${store.getNextIndex("cache")}`;
269
+ const namespace = `${ctx.namespace}.${cacheIndex}`;
266
270
  const cacheUrlPrefix = getUrlPrefix();
267
271
 
268
272
  const entry = {
@@ -297,8 +301,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
297
301
  }
298
302
 
299
303
  // With children: create a cache entry (like layout with caching semantics)
300
- const cacheNextIndex = store.getNextIndex("cache");
301
- const namespace = `${ctx.namespace}.${cacheNextIndex}`;
304
+ const namespace = `${ctx.namespace}.${cacheIndex}`;
302
305
  const cacheShortCode = store.getShortCode("cache");
303
306
 
304
307
  const cacheUrlPrefix2 = getUrlPrefix();
@@ -1,6 +1,3 @@
1
- // Route definition
2
- export { route, type RouteDefinitionResult } from "./route-function.js";
3
-
4
1
  // Type definitions
5
2
  export type { RouteHelpers } from "./helpers-types.js";
6
3
  export type {
@@ -43,11 +43,16 @@ import {
43
43
  export function redirect(url: string, status?: number): Response;
44
44
  export function redirect(
45
45
  url: string,
46
- options: { status?: number; state?: LocationStateEntry[] },
46
+ options: {
47
+ status?: number;
48
+ state?: LocationStateEntry | LocationStateEntry[];
49
+ },
47
50
  ): Response;
48
51
  export function redirect(
49
52
  url: string,
50
- statusOrOptions?: number | { status?: number; state?: LocationStateEntry[] },
53
+ statusOrOptions?:
54
+ | number
55
+ | { status?: number; state?: LocationStateEntry | LocationStateEntry[] },
51
56
  ): Response {
52
57
  const status =
53
58
  typeof statusOrOptions === "number"
@@ -62,7 +67,14 @@ export function redirect(
62
67
 
63
68
  if (process.env.NODE_ENV !== "production") {
64
69
  const reqCtx = getRequestContext();
65
- if (reqCtx && !reqCtx.url.searchParams.has("_rsc_partial")) {
70
+ // Warn only on true full-page SSR loads. SPA partial requests and server
71
+ // actions both deliver state through Flight payloads, so suppress for those.
72
+ if (
73
+ reqCtx &&
74
+ !reqCtx.url.searchParams.has("_rsc_partial") &&
75
+ !reqCtx.request.headers.has("rsc-action") &&
76
+ !reqCtx.url.searchParams.has("_rsc_action")
77
+ ) {
66
78
  console.warn(
67
79
  `[Router] redirect() with state during a full-page (SSR) request to "${url}". ` +
68
80
  "Location state is only delivered during SPA navigations and will be lost on this request.",
@@ -128,15 +128,23 @@ const perRouterPrecomputedEntriesMap: Map<
128
128
  > = new Map();
129
129
 
130
130
  /**
131
- * Clear all per-router cached data (manifest, trie, precomputed entries).
131
+ * Clear all cached route data (global and per-router).
132
132
  * Called during HMR when route definitions change so the handler rebuilds
133
133
  * the trie from the updated router.urlpatterns on the next request.
134
+ *
135
+ * The virtual module calls this before repopulating with fresh data,
136
+ * preventing stale entries from removed routes from accumulating.
134
137
  */
135
138
  export function clearAllRouterData(): void {
139
+ globalRouteMap = {};
140
+ cachedManifest = null;
141
+ cachedPrecomputedEntries = null;
142
+ cachedRouteTrie = null;
143
+ rootScopeRoutes.clear();
144
+ globalSearchSchemas.clear();
136
145
  perRouterManifestMap.clear();
137
146
  perRouterTrieMap.clear();
138
147
  perRouterPrecomputedEntriesMap.clear();
139
- cachedRouteTrie = null;
140
148
  }
141
149
 
142
150
  export function setRouterManifest(
@@ -217,6 +225,34 @@ export function waitForManifestReady(): Promise<void> | null {
217
225
  return manifestReadyPromise;
218
226
  }
219
227
 
228
+ // ============================================================================
229
+ // Route Scope Registry
230
+ // ============================================================================
231
+
232
+ // Tracks whether each route is at root scope (no named include boundary above).
233
+ // Used by dot-local reverse resolution to decide whether bare-name fallback
234
+ // is allowed after scoped lookups are exhausted.
235
+ const rootScopeRoutes: Map<string, boolean> = new Map();
236
+
237
+ /**
238
+ * Register whether a route is at root scope.
239
+ * Called by path() during route evaluation.
240
+ */
241
+ export function registerRouteRootScope(
242
+ routeName: string,
243
+ rootScoped: boolean,
244
+ ): void {
245
+ rootScopeRoutes.set(routeName, rootScoped);
246
+ }
247
+
248
+ /**
249
+ * Check if a route is at root scope.
250
+ * Returns undefined if the route has not been registered (e.g. in unit tests).
251
+ */
252
+ export function isRouteRootScoped(routeName: string): boolean | undefined {
253
+ return rootScopeRoutes.get(routeName);
254
+ }
255
+
220
256
  // ============================================================================
221
257
  // Search Schema Registry
222
258
  // ============================================================================
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Route name utilities for filtering internal route names.
3
+ *
4
+ * Internal names stay active in the runtime manifest for matching and local
5
+ * reverse() resolution, but they must not leak into public APIs or generated
6
+ * route maps.
7
+ */
8
+
9
+ export const AUTO_GENERATED_ROUTE_PREFIX = "$path_";
10
+ export const INTERNAL_INCLUDE_SCOPE_PREFIX = "$prefix_";
11
+
12
+ const RESERVED_PREFIXES = [
13
+ AUTO_GENERATED_ROUTE_PREFIX,
14
+ INTERNAL_INCLUDE_SCOPE_PREFIX,
15
+ ] as const;
16
+
17
+ /**
18
+ * Check if a route name is internal.
19
+ * Internal names include:
20
+ * - unnamed path() routes like "$path__health" or "docs.$path__health"
21
+ * - hidden include scopes like "$prefix_0.index" or "blog.$prefix_1.post"
22
+ *
23
+ * User-defined names containing "$" (e.g. "docs.$admin") are valid and must
24
+ * be preserved.
25
+ */
26
+ export function isAutoGeneratedRouteName(name: string): boolean {
27
+ return name.split(".").some((segment) => {
28
+ return (
29
+ segment.startsWith(AUTO_GENERATED_ROUTE_PREFIX) ||
30
+ segment.startsWith(INTERNAL_INCLUDE_SCOPE_PREFIX)
31
+ );
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Validate that a user-provided route name does not collide with
37
+ * reserved internal prefixes. Checks every dot-separated segment,
38
+ * mirroring the same rule used by isAutoGeneratedRouteName().
39
+ *
40
+ * Throws with a clear message when a reserved prefix is detected.
41
+ */
42
+ export function validateUserRouteName(name: string): void {
43
+ for (const segment of name.split(".")) {
44
+ for (const prefix of RESERVED_PREFIXES) {
45
+ if (segment.startsWith(prefix)) {
46
+ throw new Error(
47
+ `Route name "${name}" contains segment "${segment}" which uses reserved internal prefix "${prefix}". ` +
48
+ `Choose a different name to avoid collision with auto-generated route names.`,
49
+ );
50
+ }
51
+ }
52
+ }
53
+ }
@@ -169,6 +169,13 @@ export type IncludeItem = {
169
169
  parent: unknown; // EntryData - avoid circular import
170
170
  /** Counter snapshot from pattern extraction for consistent shortCode indices */
171
171
  counters?: Record<string, number>;
172
+ /** Cache profiles for DSL-time cache("profileName") resolution */
173
+ cacheProfiles?: Record<
174
+ string,
175
+ import("./cache/profile-registry.js").CacheProfile
176
+ >;
177
+ /** Root scope flag for dot-local reverse resolution */
178
+ rootScoped?: boolean;
172
179
  };
173
180
  [IncludeBrand]: void;
174
181
  };
@@ -52,7 +52,7 @@ export function parseAcceptTypes(accept: string): AcceptEntry[] {
52
52
  for (let i = 0; i < parts.length; i++) {
53
53
  const part = parts[i]!;
54
54
  const segments = part.split(";");
55
- const mime = segments[0]!.trim();
55
+ const mime = segments[0]!.trim().toLowerCase();
56
56
  if (!mime) continue;
57
57
  let q = 1.0;
58
58
  for (let j = 1; j < segments.length; j++) {
@@ -45,10 +45,23 @@ export async function buildDebugManifest<TEnv = any>(
45
45
  if (promiseResult !== null) {
46
46
  const load = await (promiseResult as Promise<any>);
47
47
  if (load && typeof load === "object" && "default" in load) {
48
- const useItems = load.default;
49
- if (typeof useItems === "function") {
50
- useItems(helpers);
48
+ // Promise<{ default: fn }> — e.g. dynamic import
49
+ if (typeof load.default !== "function") {
50
+ throw new Error(
51
+ `[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
52
+ `got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
53
+ );
51
54
  }
55
+ load.default(helpers);
56
+ } else if (typeof load === "function") {
57
+ // Promise<fn>
58
+ load(helpers);
59
+ } else {
60
+ // Reject unsupported async handler results (same policy as manifest.ts)
61
+ throw new Error(
62
+ `[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
63
+ `Lazy route handlers must resolve to a function or { default: fn }.`,
64
+ );
52
65
  }
53
66
  }
54
67
  },
@@ -5,11 +5,13 @@
5
5
  */
6
6
 
7
7
  import type { HandlerContext, InternalHandlerContext } from "../types";
8
- import { getRequestContext } from "../server/request-context.js";
9
- import { getSearchSchema } from "../route-map-builder.js";
8
+ import { _getRequestContext } from "../server/request-context.js";
9
+ import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
10
  import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
11
  import { contextGet, contextSet } from "../context-var.js";
12
12
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
+ import { isAutoGeneratedRouteName } from "../route-name.js";
14
+ import { PRERENDER_PASSTHROUGH } from "../prerender.js";
13
15
 
14
16
  /**
15
17
  * Strip internal _rsc* query params from a URL.
@@ -28,11 +30,15 @@ export function stripInternalParams(url: URL): URL {
28
30
  /**
29
31
  * Resolve route name with namespace prefix support.
30
32
  * Supports local names (dot-prefixed) and absolute names (global lookup).
33
+ *
34
+ * @param rootScoped - Explicit override for root-scope check. When undefined,
35
+ * falls back to the global scope registry, then to a heuristic.
31
36
  */
32
37
  function resolveRouteName(
33
38
  name: string,
34
39
  routeMap: Record<string, string>,
35
40
  currentRoutePrefix?: string,
41
+ rootScoped?: boolean,
36
42
  ): string | undefined {
37
43
  // 1. Dot-prefixed (".article", ".author.posts") — local resolution only.
38
44
  // Resolves within the current include() scope using the mount prefix.
@@ -65,6 +71,23 @@ function resolveRouteName(
65
71
  }
66
72
  }
67
73
 
74
+ // Fallback: try bare name at root scope only.
75
+ // Routes inside { name: "" } mounts are at root scope — their dot-local
76
+ // names can fall back to bare names (e.g., "sub.detail" reaching "flatIndex").
77
+ // Routes inside named mounts (e.g., { name: "magazine" }) are NOT at root
78
+ // scope — dot-local must not leak into unrelated global names.
79
+ //
80
+ // Resolution order: explicit param > scope registry > heuristic.
81
+ const isRootScoped =
82
+ rootScoped ??
83
+ isRouteRootScoped(currentRoutePrefix) ??
84
+ !currentRoutePrefix.includes(".");
85
+ if (isRootScoped) {
86
+ if (routeMap[lookupName] !== undefined) {
87
+ return routeMap[lookupName];
88
+ }
89
+ }
90
+
68
91
  return undefined;
69
92
  }
70
93
 
@@ -73,6 +96,27 @@ function resolveRouteName(
73
96
  return routeMap[name];
74
97
  }
75
98
 
99
+ function createPrerenderPassthroughFn(
100
+ build: boolean,
101
+ isPassthroughRoute: boolean,
102
+ ): () => typeof PRERENDER_PASSTHROUGH {
103
+ return () => {
104
+ if (!build) {
105
+ throw new Error(
106
+ "ctx.passthrough() can only be called during build-time prerendering.",
107
+ );
108
+ }
109
+ if (!isPassthroughRoute) {
110
+ throw new Error(
111
+ "ctx.passthrough() is only available on routes declared with " +
112
+ "{ passthrough: true }. Remove the passthrough() call or add " +
113
+ "{ passthrough: true } to the Prerender options.",
114
+ );
115
+ }
116
+ return PRERENDER_PASSTHROUGH;
117
+ };
118
+ }
119
+
76
120
  /**
77
121
  * Create a reverse function for URL generation from route names.
78
122
  * Used by both HandlerContext and MiddlewareContext.
@@ -87,6 +131,7 @@ export function createReverseFunction(
87
131
  routeMap: Record<string, string>,
88
132
  currentRoutePrefix?: string,
89
133
  currentParams?: Record<string, string>,
134
+ rootScoped?: boolean,
90
135
  ): (
91
136
  name: string,
92
137
  hrefParams?: Record<string, string>,
@@ -94,7 +139,12 @@ export function createReverseFunction(
94
139
  ) => string {
95
140
  return (name, hrefParams, search) => {
96
141
  // Resolve route name with namespace support
97
- const pattern = resolveRouteName(name, routeMap, currentRoutePrefix);
142
+ const pattern = resolveRouteName(
143
+ name,
144
+ routeMap,
145
+ currentRoutePrefix,
146
+ rootScoped,
147
+ );
98
148
 
99
149
  if (pattern === undefined) {
100
150
  throw new Error(
@@ -109,10 +159,10 @@ export function createReverseFunction(
109
159
  ? { ...currentParams, ...hrefParams }
110
160
  : hrefParams;
111
161
 
112
- // Substitute params (strip constraint syntax: :param(a|b) -> value)
162
+ // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
113
163
  if (effectiveParams) {
114
164
  result = result.replace(
115
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?/g,
165
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
116
166
  (_, key) => {
117
167
  const value = effectiveParams[key];
118
168
  if (value === undefined) {
@@ -146,10 +196,11 @@ export function createHandlerContext<TEnv>(
146
196
  routeMap: Record<string, string> = {},
147
197
  routeName?: string,
148
198
  responseType?: string,
199
+ isPassthroughRoute: boolean = false,
149
200
  ): InternalHandlerContext<any, TEnv> {
150
201
  // Get variables from request context - this is the unified context
151
202
  // shared between middleware and route handlers
152
- const requestContext = getRequestContext();
203
+ const requestContext = _getRequestContext();
153
204
  const variables: any = requestContext?.var ?? {};
154
205
 
155
206
  // If route has a search schema, parse URLSearchParams into typed object
@@ -179,7 +230,6 @@ export function createHandlerContext<TEnv>(
179
230
  set: ((keyOrVar: any, value: any) => {
180
231
  contextSet(variables, keyOrVar, value);
181
232
  }) as HandlerContext<any, TEnv>["set"],
182
- _originalRequest: request, // Raw request for advanced use
183
233
  res: stubResponse, // Stub response for setting headers
184
234
  headers: stubResponse.headers, // Shorthand for res.headers
185
235
  // Placeholder use() - will be replaced with actual implementation during request
@@ -198,9 +248,21 @@ export function createHandlerContext<TEnv>(
198
248
  }
199
249
  requestContext.setLocationState(entries);
200
250
  },
201
- // Scoped reverse for URL generation (auto-fills current request params)
202
- reverse: createReverseFunction(routeMap, routeName, params),
251
+ routeName: (routeName && !isAutoGeneratedRouteName(routeName)
252
+ ? routeName
253
+ : undefined) as HandlerContext["routeName"],
254
+ // Scoped reverse for URL generation (auto-fills current request params).
255
+ // Resolve rootScoped eagerly so the reverse function is self-contained
256
+ // and does not depend on the global rootScopeRoutes registry at call time.
257
+ reverse: createReverseFunction(
258
+ routeMap,
259
+ routeName,
260
+ params,
261
+ routeName ? isRouteRootScoped(routeName) : undefined,
262
+ ),
263
+ passthrough: createPrerenderPassthroughFn(false, isPassthroughRoute),
203
264
  _responseType: responseType,
265
+ _routeName: routeName,
204
266
  };
205
267
  // Brand with taint symbol so "use cache" excludes ctx from cache keys
206
268
  (ctx as any)[NOCACHE_SYMBOL] = true;
@@ -220,6 +282,7 @@ export function createPrerenderContext<TEnv>(
220
282
  routeMap: Record<string, string>,
221
283
  routeName?: string,
222
284
  buildVars?: Record<string, any>,
285
+ isPassthroughRoute?: boolean,
223
286
  ): InternalHandlerContext<any, TEnv> {
224
287
  const syntheticUrl = new URL(`http://prerender${pathname}`);
225
288
  const variables = buildVars ?? {};
@@ -251,9 +314,6 @@ export function createPrerenderContext<TEnv>(
251
314
  set: ((keyOrVar: any, value: any) => {
252
315
  contextSet(variables, keyOrVar, value);
253
316
  }) as any,
254
- get _originalRequest(): Request {
255
- return throwUnavailable("request");
256
- },
257
317
  get res(): Response {
258
318
  return throwUnavailable("res");
259
319
  },
@@ -266,10 +326,23 @@ export function createPrerenderContext<TEnv>(
266
326
  },
267
327
  theme: undefined,
268
328
  setTheme: undefined,
329
+ routeName: (routeName && !isAutoGeneratedRouteName(routeName)
330
+ ? routeName
331
+ : undefined) as HandlerContext["routeName"],
269
332
  setLocationState: () => {
270
333
  throwUnavailable("setLocationState");
271
334
  },
272
- reverse: createReverseFunction(routeMap, routeName, params),
335
+ reverse: createReverseFunction(
336
+ routeMap,
337
+ routeName,
338
+ params,
339
+ routeName ? isRouteRootScoped(routeName) : undefined,
340
+ ),
341
+ passthrough: createPrerenderPassthroughFn(
342
+ true,
343
+ isPassthroughRoute === true,
344
+ ),
345
+ _routeName: routeName,
273
346
  } as InternalHandlerContext<any, TEnv>;
274
347
  }
275
348
 
@@ -322,9 +395,6 @@ export function createStaticContext<TEnv>(
322
395
  set: ((keyOrVar: any, value: any) => {
323
396
  contextSet(variables, keyOrVar, value);
324
397
  }) as any,
325
- get _originalRequest(): Request {
326
- return throwUnavailable("request");
327
- },
328
398
  get res(): Response {
329
399
  return throwUnavailable("res");
330
400
  },
@@ -337,9 +407,18 @@ export function createStaticContext<TEnv>(
337
407
  },
338
408
  theme: undefined,
339
409
  setTheme: undefined,
410
+ routeName: (routeName && !isAutoGeneratedRouteName(routeName)
411
+ ? routeName
412
+ : undefined) as HandlerContext["routeName"],
340
413
  setLocationState: () => {
341
414
  throwUnavailable("setLocationState");
342
415
  },
343
- reverse: createReverseFunction(routeMap, routeName),
416
+ reverse: createReverseFunction(
417
+ routeMap,
418
+ routeName,
419
+ undefined,
420
+ routeName ? isRouteRootScoped(routeName) : undefined,
421
+ ),
422
+ _routeName: routeName,
344
423
  } as InternalHandlerContext<any, TEnv>;
345
424
  }
@@ -384,10 +384,12 @@ export async function resolveInterceptLoadersOnly<TEnv>(
384
384
  return null;
385
385
  }
386
386
 
387
- const loaderDataPromise =
388
- interceptEntry.loading !== undefined
389
- ? Promise.all(loaderPromises)
390
- : await Promise.all(loaderPromises);
387
+ // Match fresh-path semantics: only defer (no await) when loading is truthy.
388
+ // `loading: false` means "no loading UI, await loaders before render" —
389
+ // same as the fresh path's `if (interceptEntry.loading && ...)` check.
390
+ const loaderDataPromise = interceptEntry.loading
391
+ ? Promise.all(loaderPromises)
392
+ : await Promise.all(loaderPromises);
391
393
 
392
394
  return { loaderDataPromise, loaderIds };
393
395
  }
@@ -28,6 +28,7 @@ export function findLazyIncludes<TEnv = any>(
28
28
  urlPrefix: string;
29
29
  namePrefix: string | undefined;
30
30
  parent: unknown;
31
+ rootScoped?: boolean;
31
32
  };
32
33
  }> {
33
34
  const lazyItems: Array<{
@@ -37,6 +38,7 @@ export function findLazyIncludes<TEnv = any>(
37
38
  urlPrefix: string;
38
39
  namePrefix: string | undefined;
39
40
  parent: unknown;
41
+ rootScoped?: boolean;
40
42
  };
41
43
  }> = [];
42
44
 
@@ -137,6 +139,8 @@ export function evaluateLazyEntry<TEnv = any>(
137
139
  namespace: "lazy",
138
140
  parent: (lazyContext?.parent as EntryData | null) ?? null,
139
141
  counters: lazyCounters,
142
+ cacheProfiles: (lazyContext as any)?.cacheProfiles,
143
+ rootScoped: (lazyContext as any)?.rootScoped,
140
144
  },
141
145
  () => {
142
146
  // Run the lazy patterns handler with the original context prefixes
@@ -22,7 +22,7 @@ import type { LoaderRevalidationResult, ActionContext } from "./types";
22
22
  import { isHandle, type Handle } from "../handle.js";
23
23
  import type { HandleStore } from "../server/handle-store.js";
24
24
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
25
- import { getRequestContext } from "../server/request-context.js";
25
+ import { _getRequestContext } from "../server/request-context.js";
26
26
  import { debugLog } from "./logging.js";
27
27
 
28
28
  /**
@@ -182,7 +182,7 @@ function createLoaderExecutor<TEnv>(
182
182
  callerLoaderId: string | null,
183
183
  ) => Promise<any> {
184
184
  // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
185
- const reqCtxRef = getRequestContext();
185
+ const reqCtxRef = _getRequestContext();
186
186
 
187
187
  // Dependency graph: loaderId -> set of loader IDs it directly depends on.
188
188
  const dependsOn = new Map<string, Set<string>>();
@@ -243,6 +243,7 @@ function createLoaderExecutor<TEnv>(
243
243
  const currentLoaderId = loader.$$id;
244
244
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
245
245
  params: ctx.params,
246
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
246
247
  request: ctx.request,
247
248
  searchParams: ctx.searchParams,
248
249
  search: (ctx as any).search,
@@ -251,12 +252,6 @@ function createLoaderExecutor<TEnv>(
251
252
  env: ctx.env,
252
253
  var: ctx.var,
253
254
  get: ctx.get,
254
- cookie(name: string) {
255
- return reqCtxRef?.cookie(name);
256
- },
257
- cookies() {
258
- return reqCtxRef?.cookies() ?? {};
259
- },
260
255
  use: <TDep, TDepParams = any>(
261
256
  dep: LoaderDefinition<TDep, TDepParams>,
262
257
  ): Promise<TDep> => {
@@ -267,7 +262,7 @@ function createLoaderExecutor<TEnv>(
267
262
  reverse: ctx.reverse as LoaderContext["reverse"],
268
263
  };
269
264
 
270
- const doneLoader = track(`loader:${loader.$$id}`);
265
+ const doneLoader = track(`loader:${loader.$$id}`, 2);
271
266
  const promise = Promise.resolve(
272
267
  loaderFn(loaderCtx as LoaderContext<any, TEnv>),
273
268
  ).finally(() => {
@@ -300,7 +295,7 @@ export function setupLoaderAccess<TEnv>(
300
295
  // can disrupt AsyncLocalStorage, causing getRequestContext() to return
301
296
  // undefined when handlers later call ctx.use(handle). Capturing early
302
297
  // ensures the store reference survives ALS disruption.
303
- const handleStoreRef = getRequestContext()?._handleStore;
298
+ const handleStoreRef = _getRequestContext()?._handleStore;
304
299
 
305
300
  const useLoader = createLoaderExecutor(ctx, loaderPromises);
306
301
 
@@ -342,7 +337,7 @@ export function setupLoaderAccess<TEnv>(
342
337
  */
343
338
  export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
344
339
  // Eagerly capture the HandleStore (same ALS protection as setupLoaderAccess).
345
- const handleStoreRef = getRequestContext()?._handleStore;
340
+ const handleStoreRef = _getRequestContext()?._handleStore;
346
341
 
347
342
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
348
343
  // Handle case: return a push function bound to the current segment