@rangojs/router 0.0.0-experimental.78a48627 → 0.0.0-experimental.79

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 (147) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +138 -50
  3. package/dist/vite/index.js +853 -435
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +16 -17
  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 +37 -5
  26. package/src/browser/navigation-client.ts +142 -57
  27. package/src/browser/navigation-store.ts +43 -8
  28. package/src/browser/partial-update.ts +63 -22
  29. package/src/browser/prefetch/cache.ts +73 -11
  30. package/src/browser/prefetch/fetch.ts +98 -27
  31. package/src/browser/prefetch/queue.ts +92 -20
  32. package/src/browser/prefetch/resource-ready.ts +77 -0
  33. package/src/browser/react/Link.tsx +76 -9
  34. package/src/browser/react/NavigationProvider.tsx +16 -7
  35. package/src/browser/react/context.ts +7 -2
  36. package/src/browser/react/use-handle.ts +9 -58
  37. package/src/browser/react/use-router.ts +21 -8
  38. package/src/browser/rsc-router.tsx +134 -59
  39. package/src/browser/scroll-restoration.ts +21 -18
  40. package/src/browser/segment-reconciler.ts +36 -9
  41. package/src/browser/server-action-bridge.ts +8 -6
  42. package/src/browser/types.ts +27 -5
  43. package/src/build/generate-manifest.ts +6 -6
  44. package/src/build/generate-route-types.ts +3 -0
  45. package/src/build/route-trie.ts +50 -24
  46. package/src/build/route-types/include-resolution.ts +8 -1
  47. package/src/build/route-types/router-processing.ts +223 -74
  48. package/src/build/route-types/scan-filter.ts +8 -1
  49. package/src/cache/cache-runtime.ts +15 -11
  50. package/src/cache/cache-scope.ts +48 -7
  51. package/src/cache/cf/cf-cache-store.ts +453 -11
  52. package/src/cache/cf/index.ts +5 -1
  53. package/src/cache/document-cache.ts +17 -7
  54. package/src/cache/index.ts +1 -0
  55. package/src/cache/taint.ts +55 -0
  56. package/src/client.tsx +84 -230
  57. package/src/context-var.ts +72 -2
  58. package/src/debug.ts +2 -2
  59. package/src/handle.ts +40 -0
  60. package/src/index.rsc.ts +3 -1
  61. package/src/index.ts +46 -6
  62. package/src/prerender/store.ts +5 -4
  63. package/src/prerender.ts +138 -77
  64. package/src/reverse.ts +25 -1
  65. package/src/route-definition/dsl-helpers.ts +224 -37
  66. package/src/route-definition/helpers-types.ts +67 -19
  67. package/src/route-definition/index.ts +3 -0
  68. package/src/route-definition/redirect.ts +11 -3
  69. package/src/route-definition/resolve-handler-use.ts +149 -0
  70. package/src/route-types.ts +18 -0
  71. package/src/router/content-negotiation.ts +100 -1
  72. package/src/router/handler-context.ts +82 -23
  73. package/src/router/intercept-resolution.ts +9 -4
  74. package/src/router/lazy-includes.ts +7 -6
  75. package/src/router/loader-resolution.ts +156 -21
  76. package/src/router/logging.ts +1 -1
  77. package/src/router/manifest.ts +28 -15
  78. package/src/router/match-api.ts +124 -189
  79. package/src/router/match-middleware/background-revalidation.ts +30 -2
  80. package/src/router/match-middleware/cache-lookup.ts +94 -17
  81. package/src/router/match-middleware/cache-store.ts +53 -10
  82. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  83. package/src/router/match-middleware/segment-resolution.ts +60 -5
  84. package/src/router/match-result.ts +104 -10
  85. package/src/router/metrics.ts +6 -1
  86. package/src/router/middleware-types.ts +6 -8
  87. package/src/router/middleware.ts +4 -6
  88. package/src/router/navigation-snapshot.ts +182 -0
  89. package/src/router/prerender-match.ts +110 -10
  90. package/src/router/preview-match.ts +30 -102
  91. package/src/router/request-classification.ts +310 -0
  92. package/src/router/route-snapshot.ts +245 -0
  93. package/src/router/router-context.ts +1 -0
  94. package/src/router/router-interfaces.ts +36 -4
  95. package/src/router/router-options.ts +37 -11
  96. package/src/router/segment-resolution/fresh.ts +198 -20
  97. package/src/router/segment-resolution/helpers.ts +29 -24
  98. package/src/router/segment-resolution/loader-cache.ts +1 -0
  99. package/src/router/segment-resolution/revalidation.ts +433 -296
  100. package/src/router/types.ts +1 -0
  101. package/src/router.ts +55 -6
  102. package/src/rsc/handler.ts +472 -372
  103. package/src/rsc/loader-fetch.ts +23 -3
  104. package/src/rsc/manifest-init.ts +5 -1
  105. package/src/rsc/progressive-enhancement.ts +14 -2
  106. package/src/rsc/rsc-rendering.ts +10 -1
  107. package/src/rsc/server-action.ts +8 -0
  108. package/src/rsc/ssr-setup.ts +2 -2
  109. package/src/rsc/types.ts +9 -1
  110. package/src/segment-content-promise.ts +67 -0
  111. package/src/segment-loader-promise.ts +122 -0
  112. package/src/segment-system.tsx +109 -23
  113. package/src/server/context.ts +166 -17
  114. package/src/server/handle-store.ts +19 -0
  115. package/src/server/loader-registry.ts +9 -8
  116. package/src/server/request-context.ts +185 -19
  117. package/src/ssr/index.tsx +4 -0
  118. package/src/static-handler.ts +18 -6
  119. package/src/types/cache-types.ts +4 -4
  120. package/src/types/handler-context.ts +137 -33
  121. package/src/types/loader-types.ts +36 -9
  122. package/src/types/route-entry.ts +12 -1
  123. package/src/types/segments.ts +2 -0
  124. package/src/urls/include-helper.ts +24 -14
  125. package/src/urls/path-helper-types.ts +39 -6
  126. package/src/urls/path-helper.ts +48 -13
  127. package/src/urls/pattern-types.ts +12 -0
  128. package/src/urls/response-types.ts +16 -6
  129. package/src/use-loader.tsx +77 -5
  130. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  131. package/src/vite/discovery/discover-routers.ts +5 -1
  132. package/src/vite/discovery/prerender-collection.ts +128 -74
  133. package/src/vite/discovery/state.ts +13 -6
  134. package/src/vite/index.ts +4 -0
  135. package/src/vite/plugin-types.ts +51 -79
  136. package/src/vite/plugins/expose-action-id.ts +1 -3
  137. package/src/vite/plugins/expose-id-utils.ts +12 -0
  138. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  139. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  140. package/src/vite/plugins/performance-tracks.ts +88 -0
  141. package/src/vite/plugins/refresh-cmd.ts +88 -26
  142. package/src/vite/plugins/version-plugin.ts +13 -1
  143. package/src/vite/rango.ts +163 -211
  144. package/src/vite/router-discovery.ts +178 -45
  145. package/src/vite/utils/banner.ts +3 -3
  146. package/src/vite/utils/prerender-utils.ts +37 -5
  147. 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
+ }
@@ -176,6 +176,13 @@ export type IncludeItem = {
176
176
  >;
177
177
  /** Root scope flag for dot-local reverse resolution */
178
178
  rootScoped?: boolean;
179
+ /**
180
+ * Positional include scope token composed from the parent scope plus this
181
+ * include's sibling index (`${parentScope}I${idx}`). Applied to direct-
182
+ * descendant shortCodes during lazy evaluation so routes inside the
183
+ * include cannot collide with siblings declared outside it.
184
+ */
185
+ includeScope?: string;
179
186
  };
180
187
  [IncludeBrand]: void;
181
188
  };
@@ -257,3 +264,14 @@ export type LoaderUseItem = RevalidateItem | CacheItem;
257
264
  * runtime via .flat(3).
258
265
  */
259
266
  export type UseItems<T> = (T | readonly T[])[];
267
+
268
+ /**
269
+ * Union of all items that handler.use() may return.
270
+ * A handler doesn't know its mount site at definition time, so the type
271
+ * is intentionally broad — validation happens per-mount-site at runtime.
272
+ */
273
+ export type HandlerUseItem =
274
+ | RouteUseItem
275
+ | LayoutUseItem
276
+ | ParallelUseItem
277
+ | 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
+ }
@@ -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
  );
@@ -207,7 +212,7 @@ export async function resolveInterceptEntry<TEnv>(
207
212
  loaderIds.push(loader.$$id);
208
213
  loaderPromises.push(
209
214
  deps.wrapLoaderPromise(
210
- context.use(loader),
215
+ runInsideLoaderScope(() => context.use(loader)),
211
216
  parentEntry,
212
217
  segmentId,
213
218
  context.pathname,
@@ -374,7 +379,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
374
379
  loaderIds.push(loader.$$id);
375
380
  loaderPromises.push(
376
381
  deps.wrapLoaderPromise(
377
- context.use(loader),
382
+ runInsideLoaderScope(() => context.use(loader)),
378
383
  parentEntry,
379
384
  segmentId,
380
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";
@@ -124,9 +125,8 @@ export function evaluateLazyEntry<TEnv = any>(
124
125
  // Merge captured counters from include() to maintain consistent
125
126
  // shortCode indices with sibling entries from pattern extraction
126
127
  const lazyCounters: Record<string, number> = {};
127
- if (lazyContext && (lazyContext as any).counters) {
128
- const captured = (lazyContext as any).counters as Record<string, number>;
129
- for (const [key, value] of Object.entries(captured)) {
128
+ if (lazyContext?.counters) {
129
+ for (const [key, value] of Object.entries(lazyContext.counters)) {
130
130
  lazyCounters[key] = value;
131
131
  }
132
132
  }
@@ -138,10 +138,11 @@ export function evaluateLazyEntry<TEnv = any>(
138
138
  patternsByPrefix,
139
139
  trailingSlash: trailingSlashMap,
140
140
  namespace: "lazy",
141
- parent: (lazyContext?.parent as EntryData | null) ?? null,
141
+ parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
142
142
  counters: lazyCounters,
143
- cacheProfiles: (lazyContext as any)?.cacheProfiles,
144
- rootScoped: (lazyContext as any)?.rootScoped,
143
+ cacheProfiles: lazyContext?.cacheProfiles,
144
+ rootScoped: lazyContext?.rootScoped,
145
+ includeScope: lazyContext?.includeScope,
145
146
  },
146
147
  () => {
147
148
  // Run the lazy patterns handler with the original context prefixes