@rangojs/router 0.0.0-experimental.8a4d0430 → 0.0.0-experimental.8bd1b239

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 (152) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +76 -18
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +853 -435
  5. package/package.json +17 -16
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +45 -4
  8. package/skills/handler-use/SKILL.md +362 -0
  9. package/skills/intercept/SKILL.md +20 -0
  10. package/skills/layout/SKILL.md +22 -0
  11. package/skills/links/SKILL.md +3 -1
  12. package/skills/loader/SKILL.md +53 -43
  13. package/skills/middleware/SKILL.md +34 -3
  14. package/skills/migrate-nextjs/SKILL.md +560 -0
  15. package/skills/migrate-react-router/SKILL.md +764 -0
  16. package/skills/parallel/SKILL.md +185 -0
  17. package/skills/prerender/SKILL.md +110 -68
  18. package/skills/rango/SKILL.md +24 -22
  19. package/skills/route/SKILL.md +55 -0
  20. package/skills/router-setup/SKILL.md +87 -2
  21. package/skills/typesafety/SKILL.md +10 -0
  22. package/src/__internal.ts +1 -1
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/navigation-bridge.ts +40 -15
  26. package/src/browser/navigation-client.ts +142 -57
  27. package/src/browser/navigation-store.ts +43 -8
  28. package/src/browser/navigation-transaction.ts +11 -9
  29. package/src/browser/partial-update.ts +94 -17
  30. package/src/browser/prefetch/cache.ts +82 -12
  31. package/src/browser/prefetch/fetch.ts +98 -27
  32. package/src/browser/prefetch/policy.ts +6 -0
  33. package/src/browser/prefetch/queue.ts +92 -20
  34. package/src/browser/prefetch/resource-ready.ts +77 -0
  35. package/src/browser/react/Link.tsx +76 -9
  36. package/src/browser/react/NavigationProvider.tsx +40 -4
  37. package/src/browser/react/context.ts +7 -2
  38. package/src/browser/react/use-handle.ts +9 -58
  39. package/src/browser/react/use-navigation.ts +11 -10
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +134 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +36 -9
  44. package/src/browser/server-action-bridge.ts +8 -6
  45. package/src/browser/types.ts +36 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +3 -0
  48. package/src/build/route-trie.ts +50 -24
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +223 -74
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.tsx +84 -230
  60. package/src/context-var.ts +72 -2
  61. package/src/debug.ts +2 -2
  62. package/src/handle.ts +40 -0
  63. package/src/index.rsc.ts +3 -1
  64. package/src/index.ts +46 -6
  65. package/src/prerender/store.ts +5 -4
  66. package/src/prerender.ts +138 -77
  67. package/src/reverse.ts +25 -1
  68. package/src/route-definition/dsl-helpers.ts +224 -37
  69. package/src/route-definition/helpers-types.ts +67 -19
  70. package/src/route-definition/index.ts +3 -0
  71. package/src/route-definition/redirect.ts +11 -3
  72. package/src/route-definition/resolve-handler-use.ts +149 -0
  73. package/src/route-map-builder.ts +7 -1
  74. package/src/route-types.ts +11 -0
  75. package/src/router/content-negotiation.ts +100 -1
  76. package/src/router/find-match.ts +4 -2
  77. package/src/router/handler-context.ts +82 -23
  78. package/src/router/intercept-resolution.ts +11 -4
  79. package/src/router/lazy-includes.ts +4 -1
  80. package/src/router/loader-resolution.ts +156 -21
  81. package/src/router/logging.ts +5 -2
  82. package/src/router/manifest.ts +9 -3
  83. package/src/router/match-api.ts +124 -189
  84. package/src/router/match-middleware/background-revalidation.ts +30 -2
  85. package/src/router/match-middleware/cache-lookup.ts +94 -17
  86. package/src/router/match-middleware/cache-store.ts +53 -10
  87. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  88. package/src/router/match-middleware/segment-resolution.ts +61 -5
  89. package/src/router/match-result.ts +104 -10
  90. package/src/router/metrics.ts +6 -1
  91. package/src/router/middleware-types.ts +6 -8
  92. package/src/router/middleware.ts +4 -6
  93. package/src/router/navigation-snapshot.ts +182 -0
  94. package/src/router/prerender-match.ts +110 -10
  95. package/src/router/preview-match.ts +30 -102
  96. package/src/router/request-classification.ts +310 -0
  97. package/src/router/route-snapshot.ts +245 -0
  98. package/src/router/router-context.ts +6 -1
  99. package/src/router/router-interfaces.ts +36 -4
  100. package/src/router/router-options.ts +37 -11
  101. package/src/router/segment-resolution/fresh.ts +198 -20
  102. package/src/router/segment-resolution/helpers.ts +29 -24
  103. package/src/router/segment-resolution/loader-cache.ts +1 -0
  104. package/src/router/segment-resolution/revalidation.ts +438 -300
  105. package/src/router/segment-wrappers.ts +2 -0
  106. package/src/router/types.ts +1 -0
  107. package/src/router.ts +59 -6
  108. package/src/rsc/handler.ts +472 -372
  109. package/src/rsc/loader-fetch.ts +23 -3
  110. package/src/rsc/manifest-init.ts +5 -1
  111. package/src/rsc/progressive-enhancement.ts +14 -2
  112. package/src/rsc/rsc-rendering.ts +12 -1
  113. package/src/rsc/server-action.ts +8 -0
  114. package/src/rsc/ssr-setup.ts +2 -2
  115. package/src/rsc/types.ts +9 -1
  116. package/src/segment-content-promise.ts +67 -0
  117. package/src/segment-loader-promise.ts +122 -0
  118. package/src/segment-system.tsx +109 -23
  119. package/src/server/context.ts +140 -14
  120. package/src/server/handle-store.ts +19 -0
  121. package/src/server/loader-registry.ts +9 -8
  122. package/src/server/request-context.ts +185 -19
  123. package/src/ssr/index.tsx +4 -0
  124. package/src/static-handler.ts +18 -6
  125. package/src/types/cache-types.ts +4 -4
  126. package/src/types/handler-context.ts +137 -33
  127. package/src/types/loader-types.ts +36 -9
  128. package/src/types/route-entry.ts +8 -1
  129. package/src/types/segments.ts +2 -0
  130. package/src/urls/path-helper-types.ts +39 -6
  131. package/src/urls/path-helper.ts +48 -13
  132. package/src/urls/pattern-types.ts +12 -0
  133. package/src/urls/response-types.ts +16 -6
  134. package/src/use-loader.tsx +77 -5
  135. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  136. package/src/vite/discovery/discover-routers.ts +5 -1
  137. package/src/vite/discovery/prerender-collection.ts +128 -74
  138. package/src/vite/discovery/state.ts +13 -6
  139. package/src/vite/index.ts +4 -0
  140. package/src/vite/plugin-types.ts +51 -79
  141. package/src/vite/plugins/expose-action-id.ts +1 -3
  142. package/src/vite/plugins/expose-id-utils.ts +12 -0
  143. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  144. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  145. package/src/vite/plugins/performance-tracks.ts +88 -0
  146. package/src/vite/plugins/refresh-cmd.ts +88 -26
  147. package/src/vite/plugins/version-plugin.ts +13 -1
  148. package/src/vite/rango.ts +163 -211
  149. package/src/vite/router-discovery.ts +178 -45
  150. package/src/vite/utils/banner.ts +3 -3
  151. package/src/vite/utils/prerender-utils.ts +37 -5
  152. package/src/vite/utils/shared-utils.ts +3 -2
@@ -0,0 +1,149 @@
1
+ import type { AllUseItems } from "../route-types.js";
2
+ import { isPrerenderHandler, isPassthroughHandler } from "../prerender.js";
3
+ import { isStaticHandler } from "../static-handler.js";
4
+
5
+ /**
6
+ * Extract the .use callback from any handler shape.
7
+ *
8
+ * Checks definition brands first (objects with __brand), then plain functions.
9
+ * ReactNode handlers return undefined (no .use possible).
10
+ */
11
+ export function resolveHandlerUse(handler: unknown): (() => any[]) | undefined {
12
+ if (handler == null) return undefined;
13
+
14
+ // Check branded definitions first — they're objects but also have typeof "object"
15
+ if (isPassthroughHandler(handler)) {
16
+ return (handler as any).use;
17
+ }
18
+ if (isPrerenderHandler(handler)) {
19
+ return (handler as any).use;
20
+ }
21
+ if (isStaticHandler(handler)) {
22
+ return (handler as any).use;
23
+ }
24
+ // Plain handler function
25
+ if (typeof handler === "function") {
26
+ return (handler as any).use;
27
+ }
28
+ // ReactNode or other — no .use
29
+ return undefined;
30
+ }
31
+
32
+ /**
33
+ * Allowed item types per mount site.
34
+ * Mirrors the RouteUseItem / ParallelUseItem / InterceptUseItem / LayoutUseItem unions
35
+ * from route-types.ts for runtime validation.
36
+ */
37
+ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
38
+ path: new Set([
39
+ "layout",
40
+ "parallel",
41
+ "intercept",
42
+ "middleware",
43
+ "revalidate",
44
+ "loader",
45
+ "loading",
46
+ "errorBoundary",
47
+ "notFoundBoundary",
48
+ "cache",
49
+ "transition",
50
+ ]),
51
+ // Response routes (path.json, path.text, etc.) — mirrors ResponseRouteUseItem
52
+ response: new Set(["middleware", "cache"]),
53
+ route: new Set([
54
+ "layout",
55
+ "parallel",
56
+ "intercept",
57
+ "middleware",
58
+ "revalidate",
59
+ "loader",
60
+ "loading",
61
+ "errorBoundary",
62
+ "notFoundBoundary",
63
+ "cache",
64
+ "transition",
65
+ ]),
66
+ // layout allows AllUseItems — no validation needed, but included for completeness
67
+ layout: new Set([
68
+ "layout",
69
+ "route",
70
+ "middleware",
71
+ "revalidate",
72
+ "parallel",
73
+ "intercept",
74
+ "loader",
75
+ "loading",
76
+ "errorBoundary",
77
+ "notFoundBoundary",
78
+ "cache",
79
+ "transition",
80
+ "include",
81
+ ]),
82
+ parallel: new Set([
83
+ "revalidate",
84
+ "loader",
85
+ "loading",
86
+ "errorBoundary",
87
+ "notFoundBoundary",
88
+ "transition",
89
+ ]),
90
+ intercept: new Set([
91
+ "middleware",
92
+ "revalidate",
93
+ "loader",
94
+ "loading",
95
+ "errorBoundary",
96
+ "notFoundBoundary",
97
+ "layout",
98
+ "route",
99
+ "when",
100
+ "transition",
101
+ ]),
102
+ };
103
+
104
+ /**
105
+ * Validate that items from handler.use() are valid for the given mount site.
106
+ * Throws a descriptive error if any item is not allowed.
107
+ */
108
+ export function validateHandlerUseItems(
109
+ items: AllUseItems[],
110
+ mountSite: string,
111
+ ): void {
112
+ const allowed = MOUNT_SITE_ALLOWED_TYPES[mountSite];
113
+ if (!allowed) return;
114
+ for (const item of items) {
115
+ if (item == null) continue;
116
+ if (!allowed.has((item as any).type)) {
117
+ throw new Error(
118
+ `handler.use() returned ${(item as any).type}() which is not valid inside ${mountSite}(). ` +
119
+ `Allowed types: ${[...allowed].join(", ")}.`,
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Create a merged use callback from handler.use and explicit use.
127
+ * handler.use items come first (defaults), explicit items second (overrides).
128
+ * Returns undefined if both are absent.
129
+ */
130
+ export function mergeHandlerUse(
131
+ handlerUse: (() => any[]) | undefined,
132
+ explicitUse: (() => any[]) | undefined,
133
+ mountSite: string,
134
+ ): (() => any[]) | undefined {
135
+ if (!handlerUse && !explicitUse) return undefined;
136
+ if (!handlerUse) return explicitUse;
137
+ if (!explicitUse) {
138
+ return () => {
139
+ const items = handlerUse().flat(3);
140
+ validateHandlerUseItems(items, mountSite);
141
+ return items;
142
+ };
143
+ }
144
+ return () => {
145
+ const hItems = handlerUse().flat(3);
146
+ validateHandlerUseItems(hItems, mountSite);
147
+ return [...hItems, ...explicitUse()];
148
+ };
149
+ }
@@ -199,7 +199,13 @@ export function registerRouterManifestLoader(
199
199
  }
200
200
 
201
201
  export async function ensureRouterManifest(routerId: string): Promise<void> {
202
- if (perRouterManifestMap.has(routerId)) return;
202
+ // Check both manifest AND trie. The virtual module's setRouterManifest()
203
+ // pre-sets the manifest at startup, but the per-router trie is only
204
+ // available from the lazy loader. Without this, the lazy loader never
205
+ // runs and findMatch falls back to the global merged trie — which
206
+ // contains routes from ALL routers and breaks multi-router setups.
207
+ if (perRouterManifestMap.has(routerId) && perRouterTrieMap.has(routerId))
208
+ return;
203
209
  const loader = routerManifestLoaders.get(routerId);
204
210
  if (loader) {
205
211
  const mod = await loader();
@@ -257,3 +257,14 @@ export type LoaderUseItem = RevalidateItem | CacheItem;
257
257
  * runtime via .flat(3).
258
258
  */
259
259
  export type UseItems<T> = (T | readonly T[])[];
260
+
261
+ /**
262
+ * Union of all items that handler.use() may return.
263
+ * A handler doesn't know its mount site at definition time, so the type
264
+ * is intentionally broad — validation happens per-mount-site at runtime.
265
+ */
266
+ export type HandlerUseItem =
267
+ | RouteUseItem
268
+ | LayoutUseItem
269
+ | ParallelUseItem
270
+ | InterceptUseItem;
@@ -2,10 +2,18 @@
2
2
  * Content Negotiation Utilities
3
3
  *
4
4
  * Pure functions for HTTP Accept header parsing and response type matching.
5
- * Used by createRouter's previewMatch for content negotiation between
5
+ * Used by previewMatch and classifyRequest for content negotiation between
6
6
  * RSC routes and response routes (JSON, text, image, stream, etc.).
7
7
  */
8
8
 
9
+ import type { EntryData } from "../server/context.js";
10
+ import type { CollectedMiddleware } from "./middleware-types.js";
11
+ import { collectRouteMiddleware } from "./middleware.js";
12
+ import { loadManifest } from "./manifest.js";
13
+ import { traverseBack } from "./pattern-matching.js";
14
+ import type { RouteMatchResult } from "./pattern-matching.js";
15
+ import type { RouteSnapshot } from "./route-snapshot.js";
16
+
9
17
  // Response type -> MIME type used for Accept header matching
10
18
  export const RESPONSE_TYPE_MIME: Record<string, string> = {
11
19
  json: "application/json",
@@ -114,3 +122,94 @@ export function pickNegotiateVariant(
114
122
  // No match -- use first candidate as default
115
123
  return candidates[0]!;
116
124
  }
125
+
126
+ /**
127
+ * Result of content negotiation for a route with negotiate variants.
128
+ */
129
+ export interface NegotiationResult {
130
+ /** The winning response type */
131
+ responseType: string;
132
+ /** Handler function for the winning variant */
133
+ handler: Function;
134
+ /** Manifest entry for the winning variant (may differ from primary) */
135
+ manifestEntry: EntryData;
136
+ /** Route middleware for the winning variant */
137
+ routeMiddleware: CollectedMiddleware[];
138
+ /** Always true — negotiation occurred */
139
+ negotiated: true;
140
+ }
141
+
142
+ /**
143
+ * Perform content negotiation for a route with negotiate variants.
144
+ *
145
+ * Returns a NegotiationResult when a response route wins negotiation.
146
+ * Returns null when RSC wins or no negotiation is needed.
147
+ *
148
+ * Shared by previewMatch and classifyRequest to avoid duplicating
149
+ * the candidate-building and variant-loading logic.
150
+ */
151
+ export async function negotiateRoute(
152
+ request: Request,
153
+ pathname: string,
154
+ snapshot: RouteSnapshot,
155
+ ): Promise<NegotiationResult | null> {
156
+ const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
157
+ if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
158
+ return null;
159
+ }
160
+
161
+ const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
162
+
163
+ // Build candidate list preserving definition order.
164
+ const variants = matched.negotiateVariants;
165
+ let candidates: Array<{ routeKey: string; responseType: string }>;
166
+ if (responseType) {
167
+ candidates = [...variants, { routeKey: matched.routeKey, responseType }];
168
+ } else {
169
+ const rscCandidate = {
170
+ routeKey: matched.routeKey,
171
+ responseType: RSC_RESPONSE_TYPE,
172
+ };
173
+ candidates = matched.rscFirst
174
+ ? [rscCandidate, ...variants]
175
+ : [...variants, rscCandidate];
176
+ }
177
+
178
+ const variant = pickNegotiateVariant(acceptEntries, candidates);
179
+
180
+ // RSC won negotiation
181
+ if (variant.responseType === RSC_RESPONSE_TYPE) {
182
+ return null;
183
+ }
184
+
185
+ // Primary response-type won — use existing manifest entry and middleware
186
+ if (responseType && variant.routeKey === matched.routeKey) {
187
+ return {
188
+ responseType,
189
+ handler: manifestEntry.handler as Function,
190
+ manifestEntry,
191
+ routeMiddleware,
192
+ negotiated: true,
193
+ };
194
+ }
195
+
196
+ // Different variant won — load its manifest entry
197
+ const negotiateEntry = await loadManifest(
198
+ matched.entry,
199
+ variant.routeKey,
200
+ pathname,
201
+ undefined,
202
+ false,
203
+ );
204
+ const variantMiddleware = collectRouteMiddleware(
205
+ traverseBack(negotiateEntry),
206
+ matched.params,
207
+ );
208
+ return {
209
+ responseType: variant.responseType,
210
+ handler: negotiateEntry.handler as Function,
211
+ manifestEntry: negotiateEntry,
212
+ routeMiddleware: variantMiddleware,
213
+ negotiated: true,
214
+ };
215
+ }
@@ -52,8 +52,10 @@ export function createFindMatch<TEnv = any>(
52
52
  : undefined;
53
53
 
54
54
  // Phase 1: Try trie match (O(path_length))
55
- // Prefer per-router trie (isolated) over global trie (merged).
56
- const routeTrie = getRouterTrie(deps.routerId) ?? getRouteTrie();
55
+ // Only use the per-router trie. The global trie merges routes from ALL
56
+ // routers and must not be used — in multi-router setups (host routing)
57
+ // overlapping paths like "/" would match the wrong app's route.
58
+ const routeTrie = getRouterTrie(deps.routerId);
57
59
  if (routeTrie) {
58
60
  const trieStart = performance.now();
59
61
  const trieResult = tryTrieMatch(routeTrie, pathname);
@@ -8,7 +8,13 @@ import type { HandlerContext, InternalHandlerContext } from "../types";
8
8
  import { _getRequestContext } from "../server/request-context.js";
9
9
  import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
10
  import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
- import { contextGet, contextSet } from "../context-var.js";
11
+ import {
12
+ contextGet,
13
+ contextSet,
14
+ isNonCacheable,
15
+ type ContextSetOptions,
16
+ } from "../context-var.js";
17
+ import { isInsideCacheScope } from "../server/context.js";
12
18
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
13
19
  import { isAutoGeneratedRouteName } from "../route-name.js";
14
20
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
@@ -108,9 +114,9 @@ function createPrerenderPassthroughFn(
108
114
  }
109
115
  if (!isPassthroughRoute) {
110
116
  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.",
117
+ "ctx.passthrough() is only available on routes wrapped with " +
118
+ "Passthrough(). Remove the passthrough() call or wrap the " +
119
+ "Prerender definition with Passthrough(prerenderDef, liveHandler).",
114
120
  );
115
121
  }
116
122
  return PRERENDER_PASSTHROUGH;
@@ -160,9 +166,27 @@ export function createReverseFunction(
160
166
  : hrefParams;
161
167
 
162
168
  // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
169
+ // Optional params (:param?) are omitted when not provided
163
170
  if (effectiveParams) {
171
+ let hadOmittedOptional = false;
172
+ // First pass: optional params (trailing ?)
164
173
  result = result.replace(
165
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
174
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
175
+ (_, key) => {
176
+ const value = effectiveParams[key];
177
+ // Empty string is treated as omitted — the trie matcher fills
178
+ // unmatched optional params with "" (not undefined), so reverse
179
+ // must collapse those segments instead of leaving empty slots.
180
+ if (value === undefined || value === "") {
181
+ hadOmittedOptional = true;
182
+ return "";
183
+ }
184
+ return encodeURIComponent(value);
185
+ },
186
+ );
187
+ // Second pass: required params (no trailing ?)
188
+ result = result.replace(
189
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
166
190
  (_, key) => {
167
191
  const value = effectiveParams[key];
168
192
  if (value === undefined) {
@@ -171,6 +195,13 @@ export function createReverseFunction(
171
195
  return encodeURIComponent(value);
172
196
  },
173
197
  );
198
+ // Clean up slashes only when an optional param was actually omitted,
199
+ // so intentional trailing-slash patterns like "/blog/" are preserved.
200
+ if (hadOmittedOptional) {
201
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
202
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
203
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
204
+ }
174
205
  }
175
206
 
176
207
  // Append search params as query string
@@ -201,7 +232,7 @@ export function createHandlerContext<TEnv>(
201
232
  // Get variables from request context - this is the unified context
202
233
  // shared between middleware and route handlers
203
234
  const requestContext = _getRequestContext();
204
- const variables: any = requestContext?.var ?? {};
235
+ const variables: any = requestContext?._variables ?? {};
205
236
 
206
237
  // If route has a search schema, parse URLSearchParams into typed object
207
238
  const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
@@ -213,7 +244,7 @@ export function createHandlerContext<TEnv>(
213
244
  const stubResponse =
214
245
  requestContext?.res ?? new Response(null, { status: 200 });
215
246
 
216
- // Guard mutating Headers methods so they throw inside "use cache" functions.
247
+ // Guard mutating Headers methods so they throw inside "use cache" or cache() scope.
217
248
  // Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
218
249
  // is stamped by cache-runtime, not the shared request context.
219
250
  const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
@@ -225,6 +256,13 @@ export function createHandlerContext<TEnv>(
225
256
  if (MUTATING_HEADERS_METHODS.has(prop as string)) {
226
257
  return (...args: any[]) => {
227
258
  assertNotInsideCacheExec(ctx, "headers");
259
+ if (isInsideCacheScope()) {
260
+ throw new Error(
261
+ `ctx.headers.${String(prop)}() cannot be called inside a cache() boundary. ` +
262
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
263
+ `Move header mutations to a middleware or layout outside the cache() scope.`,
264
+ );
265
+ }
228
266
  return value.apply(target, args);
229
267
  };
230
268
  }
@@ -237,6 +275,7 @@ export function createHandlerContext<TEnv>(
237
275
  ctx = {
238
276
  params,
239
277
  build: false,
278
+ dev: false,
240
279
  request,
241
280
  searchParams,
242
281
  search: searchSchema ? resolvedSearchParams : {},
@@ -244,14 +283,24 @@ export function createHandlerContext<TEnv>(
244
283
  url,
245
284
  originalUrl: new URL(request.url),
246
285
  env: bindings,
247
- var: variables,
248
- get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as HandlerContext<
249
- any,
250
- TEnv
251
- >["get"],
252
- set: ((keyOrVar: any, value: any) => {
286
+ _variables: variables,
287
+ get: ((keyOrVar: any) => {
288
+ // Read-time guard: non-cacheable var inside cache() → throw.
289
+ // Works for both ContextVar tokens and string keys.
290
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
291
+ throw new Error(
292
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
293
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
294
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
295
+ );
296
+ }
297
+ return contextGet(variables, keyOrVar);
298
+ }) as HandlerContext<any, TEnv>["get"],
299
+ set: ((keyOrVar: any, value: any, options?: ContextSetOptions) => {
253
300
  assertNotInsideCacheExec(ctx, "set");
254
- contextSet(variables, keyOrVar, value);
301
+ // Write is dumb: store value + non-cacheable metadata.
302
+ // Enforcement happens at read time via ctx.get().
303
+ contextSet(variables, keyOrVar, value, options);
255
304
  }) as HandlerContext<any, TEnv>["set"],
256
305
  res: stubResponse, // Stub response for setting headers
257
306
  headers: guardedHeaders, // Guarded shorthand for res.headers
@@ -297,7 +346,7 @@ export function createHandlerContext<TEnv>(
297
346
  *
298
347
  * Returns an InternalHandlerContext where params, pathname, url, searchParams,
299
348
  * search, reverse, and use(handle) work. Request-time properties
300
- * (request, env, headers, cookies, var, get, set, res) throw with a clear error.
349
+ * (request, env, headers, cookies, get, set, res) throw with a clear error.
301
350
  */
302
351
  export function createPrerenderContext<TEnv>(
303
352
  params: Record<string, string>,
@@ -306,6 +355,8 @@ export function createPrerenderContext<TEnv>(
306
355
  routeName?: string,
307
356
  buildVars?: Record<string, any>,
308
357
  isPassthroughRoute?: boolean,
358
+ buildEnv?: TEnv,
359
+ devMode?: boolean,
309
360
  ): InternalHandlerContext<any, TEnv> {
310
361
  const syntheticUrl = new URL(`http://prerender${pathname}`);
311
362
  const variables = buildVars ?? {};
@@ -320,6 +371,7 @@ export function createPrerenderContext<TEnv>(
320
371
  return {
321
372
  params,
322
373
  build: true,
374
+ dev: devMode ?? false,
323
375
  get request(): Request {
324
376
  return throwUnavailable("request");
325
377
  },
@@ -329,11 +381,13 @@ export function createPrerenderContext<TEnv>(
329
381
  url: syntheticUrl,
330
382
  originalUrl: syntheticUrl,
331
383
  get env(): TEnv {
332
- return throwUnavailable("env");
333
- },
334
- get var(): any {
335
- return throwUnavailable("var");
384
+ if (buildEnv !== undefined) return buildEnv;
385
+ throw new Error(
386
+ "ctx.env is not available during pre-rendering. " +
387
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
388
+ );
336
389
  },
390
+ _variables: variables,
337
391
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
338
392
  set: ((keyOrVar: any, value: any) => {
339
393
  contextSet(variables, keyOrVar, value);
@@ -379,6 +433,8 @@ export function createPrerenderContext<TEnv>(
379
433
  export function createStaticContext<TEnv>(
380
434
  routeMap: Record<string, string>,
381
435
  routeName?: string,
436
+ buildEnv?: TEnv,
437
+ devMode?: boolean,
382
438
  ): InternalHandlerContext<any, TEnv> {
383
439
  const variables: Record<string, any> = {};
384
440
 
@@ -394,6 +450,7 @@ export function createStaticContext<TEnv>(
394
450
  return throwUnavailable("params");
395
451
  },
396
452
  build: true,
453
+ dev: devMode ?? false,
397
454
  get request(): Request {
398
455
  return throwUnavailable("request");
399
456
  },
@@ -413,11 +470,13 @@ export function createStaticContext<TEnv>(
413
470
  return throwUnavailable("originalUrl");
414
471
  },
415
472
  get env(): TEnv {
416
- return throwUnavailable("env");
417
- },
418
- get var(): any {
419
- return throwUnavailable("var");
473
+ if (buildEnv !== undefined) return buildEnv;
474
+ throw new Error(
475
+ "ctx.env is not available in Static() handlers. " +
476
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
477
+ );
420
478
  },
479
+ _variables: variables,
421
480
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
422
481
  set: ((keyOrVar: any, value: any) => {
423
482
  contextSet(variables, keyOrVar, value);
@@ -11,7 +11,11 @@ import type {
11
11
  InterceptEntry,
12
12
  InterceptSelectorContext,
13
13
  } from "../server/context";
14
- import type { HandlerContext, ResolvedSegment } from "../types";
14
+ import type {
15
+ HandlerContext,
16
+ InternalHandlerContext,
17
+ ResolvedSegment,
18
+ } from "../types";
15
19
  import { evaluateRevalidation } from "./revalidation.js";
16
20
  import { getRequestContext } from "../server/request-context.js";
17
21
  import { executeInterceptMiddleware } from "./middleware.js";
@@ -20,6 +24,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
20
24
  import { handleHandlerResult } from "./segment-resolution.js";
21
25
  import type { SegmentResolutionDeps } from "./types.js";
22
26
  import { debugLog } from "./logging.js";
27
+ import { runInsideLoaderScope } from "../server/context.js";
23
28
 
24
29
  /**
25
30
  * Check if an intercept's when conditions are satisfied.
@@ -133,7 +138,7 @@ export async function resolveInterceptEntry<TEnv>(
133
138
  context.request,
134
139
  context.env,
135
140
  params,
136
- context.var as Record<string, any>,
141
+ (context as InternalHandlerContext<any, TEnv>)._variables,
137
142
  requestCtx.res,
138
143
  createReverseFunction(getGlobalRouteMap()),
139
144
  );
@@ -188,6 +193,7 @@ export async function resolveInterceptEntry<TEnv>(
188
193
  context,
189
194
  actionContext,
190
195
  stale,
196
+ traceSource: "intercept-loader",
191
197
  });
192
198
 
193
199
  if (!shouldRevalidate) {
@@ -206,7 +212,7 @@ export async function resolveInterceptEntry<TEnv>(
206
212
  loaderIds.push(loader.$$id);
207
213
  loaderPromises.push(
208
214
  deps.wrapLoaderPromise(
209
- context.use(loader),
215
+ runInsideLoaderScope(() => context.use(loader)),
210
216
  parentEntry,
211
217
  segmentId,
212
218
  context.pathname,
@@ -355,6 +361,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
355
361
  context,
356
362
  actionContext,
357
363
  stale,
364
+ traceSource: "intercept-loader",
358
365
  });
359
366
 
360
367
  if (!shouldRevalidate) {
@@ -372,7 +379,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
372
379
  loaderIds.push(loader.$$id);
373
380
  loaderPromises.push(
374
381
  deps.wrapLoaderPromise(
375
- context.use(loader),
382
+ runInsideLoaderScope(() => context.use(loader)),
376
383
  parentEntry,
377
384
  segmentId,
378
385
  context.pathname,
@@ -4,6 +4,7 @@ import {
4
4
  EntryData,
5
5
  RSCRouterContext,
6
6
  runWithPrefixes,
7
+ getIsolatedLazyParent,
7
8
  } from "../server/context";
8
9
  import type { UrlPatterns } from "../urls.js";
9
10
  import type { AllUseItems, IncludeItem } from "../route-types.js";
@@ -14,6 +15,7 @@ export interface LazyEvalDeps<TEnv = any> {
14
15
  mergedRouteMap: Record<string, string>;
15
16
  nextMountIndex: () => number;
16
17
  getPrecomputedByPrefix: () => Map<string, Record<string, string>> | null;
18
+ routerId?: string;
17
19
  }
18
20
 
19
21
  // Detect lazy includes in handler result and create placeholder entries
@@ -137,7 +139,7 @@ export function evaluateLazyEntry<TEnv = any>(
137
139
  patternsByPrefix,
138
140
  trailingSlash: trailingSlashMap,
139
141
  namespace: "lazy",
140
- parent: (lazyContext?.parent as EntryData | null) ?? null,
142
+ parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
141
143
  counters: lazyCounters,
142
144
  cacheProfiles: (lazyContext as any)?.cacheProfiles,
143
145
  rootScoped: (lazyContext as any)?.rootScoped,
@@ -200,6 +202,7 @@ export function evaluateLazyEntry<TEnv = any>(
200
202
  trailingSlash: entry.trailingSlash,
201
203
  handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
202
204
  mountIndex: deps.nextMountIndex(),
205
+ routerId: deps.routerId,
203
206
  // Lazy evaluation fields
204
207
  lazy: true,
205
208
  lazyPatterns: lazyInclude.patterns,