@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bf1b128c

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 (182) hide show
  1. package/README.md +126 -38
  2. package/dist/bin/rango.js +138 -50
  3. package/dist/vite/index.js +2018 -732
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +7 -5
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/cache-guide/SKILL.md +32 -0
  9. package/skills/caching/SKILL.md +45 -4
  10. package/skills/handler-use/SKILL.md +362 -0
  11. package/skills/hooks/SKILL.md +28 -20
  12. package/skills/intercept/SKILL.md +20 -0
  13. package/skills/layout/SKILL.md +22 -0
  14. package/skills/links/SKILL.md +91 -17
  15. package/skills/loader/SKILL.md +119 -45
  16. package/skills/middleware/SKILL.md +34 -3
  17. package/skills/migrate-nextjs/SKILL.md +560 -0
  18. package/skills/migrate-react-router/SKILL.md +765 -0
  19. package/skills/parallel/SKILL.md +192 -0
  20. package/skills/prerender/SKILL.md +110 -68
  21. package/skills/rango/SKILL.md +24 -22
  22. package/skills/response-routes/SKILL.md +8 -0
  23. package/skills/route/SKILL.md +55 -0
  24. package/skills/router-setup/SKILL.md +87 -2
  25. package/skills/streams-and-websockets/SKILL.md +283 -0
  26. package/skills/typesafety/SKILL.md +13 -1
  27. package/src/__internal.ts +1 -1
  28. package/src/browser/app-shell.ts +52 -0
  29. package/src/browser/app-version.ts +14 -0
  30. package/src/browser/event-controller.ts +5 -0
  31. package/src/browser/navigation-bridge.ts +88 -9
  32. package/src/browser/navigation-client.ts +167 -59
  33. package/src/browser/navigation-store.ts +68 -9
  34. package/src/browser/navigation-transaction.ts +11 -9
  35. package/src/browser/partial-update.ts +109 -15
  36. package/src/browser/prefetch/cache.ts +175 -15
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/queue.ts +123 -20
  39. package/src/browser/prefetch/resource-ready.ts +77 -0
  40. package/src/browser/rango-state.ts +53 -13
  41. package/src/browser/react/Link.tsx +81 -9
  42. package/src/browser/react/NavigationProvider.tsx +89 -14
  43. package/src/browser/react/context.ts +7 -2
  44. package/src/browser/react/use-handle.ts +9 -58
  45. package/src/browser/react/use-navigation.ts +22 -2
  46. package/src/browser/react/use-params.ts +11 -1
  47. package/src/browser/react/use-router.ts +29 -9
  48. package/src/browser/rsc-router.tsx +168 -65
  49. package/src/browser/scroll-restoration.ts +36 -17
  50. package/src/browser/segment-reconciler.ts +36 -9
  51. package/src/browser/server-action-bridge.ts +8 -6
  52. package/src/browser/types.ts +49 -5
  53. package/src/build/generate-manifest.ts +6 -6
  54. package/src/build/generate-route-types.ts +3 -0
  55. package/src/build/route-trie.ts +50 -24
  56. package/src/build/route-types/include-resolution.ts +8 -1
  57. package/src/build/route-types/router-processing.ts +223 -74
  58. package/src/build/route-types/scan-filter.ts +8 -1
  59. package/src/cache/cache-runtime.ts +15 -11
  60. package/src/cache/cache-scope.ts +48 -7
  61. package/src/cache/cf/cf-cache-store.ts +455 -15
  62. package/src/cache/cf/index.ts +5 -1
  63. package/src/cache/document-cache.ts +17 -7
  64. package/src/cache/index.ts +1 -0
  65. package/src/cache/taint.ts +55 -0
  66. package/src/client.tsx +84 -230
  67. package/src/context-var.ts +72 -2
  68. package/src/debug.ts +2 -2
  69. package/src/handle.ts +40 -0
  70. package/src/index.rsc.ts +6 -1
  71. package/src/index.ts +49 -6
  72. package/src/outlet-context.ts +1 -1
  73. package/src/prerender/store.ts +5 -4
  74. package/src/prerender.ts +138 -77
  75. package/src/response-utils.ts +28 -0
  76. package/src/reverse.ts +27 -2
  77. package/src/route-definition/dsl-helpers.ts +240 -40
  78. package/src/route-definition/helpers-types.ts +73 -20
  79. package/src/route-definition/index.ts +3 -0
  80. package/src/route-definition/redirect.ts +11 -3
  81. package/src/route-definition/resolve-handler-use.ts +155 -0
  82. package/src/route-map-builder.ts +7 -1
  83. package/src/route-types.ts +18 -0
  84. package/src/router/content-negotiation.ts +100 -1
  85. package/src/router/find-match.ts +4 -2
  86. package/src/router/handler-context.ts +101 -25
  87. package/src/router/intercept-resolution.ts +11 -4
  88. package/src/router/lazy-includes.ts +10 -7
  89. package/src/router/loader-resolution.ts +159 -21
  90. package/src/router/logging.ts +5 -2
  91. package/src/router/manifest.ts +31 -16
  92. package/src/router/match-api.ts +127 -192
  93. package/src/router/match-middleware/background-revalidation.ts +30 -2
  94. package/src/router/match-middleware/cache-lookup.ts +94 -17
  95. package/src/router/match-middleware/cache-store.ts +53 -10
  96. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  97. package/src/router/match-middleware/segment-resolution.ts +61 -5
  98. package/src/router/match-result.ts +104 -10
  99. package/src/router/metrics.ts +6 -1
  100. package/src/router/middleware-types.ts +8 -30
  101. package/src/router/middleware.ts +58 -13
  102. package/src/router/navigation-snapshot.ts +182 -0
  103. package/src/router/pattern-matching.ts +60 -9
  104. package/src/router/prerender-match.ts +110 -10
  105. package/src/router/preview-match.ts +30 -102
  106. package/src/router/request-classification.ts +310 -0
  107. package/src/router/revalidation.ts +15 -1
  108. package/src/router/route-snapshot.ts +245 -0
  109. package/src/router/router-context.ts +6 -1
  110. package/src/router/router-interfaces.ts +36 -4
  111. package/src/router/router-options.ts +37 -11
  112. package/src/router/segment-resolution/fresh.ts +198 -20
  113. package/src/router/segment-resolution/helpers.ts +29 -24
  114. package/src/router/segment-resolution/loader-cache.ts +1 -0
  115. package/src/router/segment-resolution/revalidation.ts +452 -307
  116. package/src/router/segment-wrappers.ts +2 -0
  117. package/src/router/trie-matching.ts +10 -4
  118. package/src/router/types.ts +1 -0
  119. package/src/router/url-params.ts +49 -0
  120. package/src/router.ts +60 -8
  121. package/src/rsc/handler.ts +478 -374
  122. package/src/rsc/helpers.ts +69 -41
  123. package/src/rsc/loader-fetch.ts +23 -3
  124. package/src/rsc/manifest-init.ts +5 -1
  125. package/src/rsc/progressive-enhancement.ts +16 -2
  126. package/src/rsc/response-route-handler.ts +14 -1
  127. package/src/rsc/rsc-rendering.ts +17 -1
  128. package/src/rsc/server-action.ts +10 -0
  129. package/src/rsc/ssr-setup.ts +2 -2
  130. package/src/rsc/types.ts +9 -1
  131. package/src/segment-content-promise.ts +67 -0
  132. package/src/segment-loader-promise.ts +122 -0
  133. package/src/segment-system.tsx +109 -23
  134. package/src/server/context.ts +166 -17
  135. package/src/server/handle-store.ts +19 -0
  136. package/src/server/loader-registry.ts +9 -8
  137. package/src/server/request-context.ts +194 -60
  138. package/src/ssr/index.tsx +4 -0
  139. package/src/static-handler.ts +18 -6
  140. package/src/types/cache-types.ts +4 -4
  141. package/src/types/handler-context.ts +145 -68
  142. package/src/types/loader-types.ts +41 -15
  143. package/src/types/request-scope.ts +126 -0
  144. package/src/types/route-entry.ts +19 -1
  145. package/src/types/segments.ts +2 -0
  146. package/src/urls/include-helper.ts +24 -14
  147. package/src/urls/path-helper-types.ts +39 -6
  148. package/src/urls/path-helper.ts +48 -13
  149. package/src/urls/pattern-types.ts +12 -0
  150. package/src/urls/response-types.ts +18 -16
  151. package/src/use-loader.tsx +77 -5
  152. package/src/vite/debug.ts +184 -0
  153. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  154. package/src/vite/discovery/discover-routers.ts +36 -4
  155. package/src/vite/discovery/gate-state.ts +171 -0
  156. package/src/vite/discovery/prerender-collection.ts +175 -74
  157. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  158. package/src/vite/discovery/state.ts +13 -6
  159. package/src/vite/index.ts +4 -0
  160. package/src/vite/plugin-types.ts +51 -79
  161. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  162. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  163. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  164. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  165. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  166. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  167. package/src/vite/plugins/expose-action-id.ts +53 -31
  168. package/src/vite/plugins/expose-id-utils.ts +12 -0
  169. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  170. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  171. package/src/vite/plugins/expose-internal-ids.ts +563 -316
  172. package/src/vite/plugins/performance-tracks.ts +96 -0
  173. package/src/vite/plugins/refresh-cmd.ts +88 -26
  174. package/src/vite/plugins/use-cache-transform.ts +56 -43
  175. package/src/vite/plugins/version-injector.ts +37 -11
  176. package/src/vite/plugins/version-plugin.ts +13 -1
  177. package/src/vite/rango.ts +204 -217
  178. package/src/vite/router-discovery.ts +732 -94
  179. package/src/vite/utils/banner.ts +4 -4
  180. package/src/vite/utils/package-resolution.ts +41 -1
  181. package/src/vite/utils/prerender-utils.ts +37 -5
  182. package/src/vite/utils/shared-utils.ts +3 -2
@@ -7,6 +7,7 @@
7
7
  import type { RouteEntry, TrailingSlashMode } from "../types";
8
8
  import type { EntryData } from "../server/context";
9
9
  import { debugLog, isRouterDebugEnabled } from "./logging.js";
10
+ import { safeDecodeURIComponent } from "./url-params.js";
10
11
 
11
12
  /**
12
13
  * Parsed segment info
@@ -82,6 +83,13 @@ export interface CompiledPattern {
82
83
  paramNames: string[];
83
84
  optionalParams: Set<string>;
84
85
  hasTrailingSlash: boolean;
86
+ /**
87
+ * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
88
+ * Validated against the **decoded** param value after regex extraction so
89
+ * a URL like `/en%20GB` still matches `:lang(en GB)` — matching the trie
90
+ * path's behavior (trie-matching.ts:validateAndBuild).
91
+ */
92
+ constraints?: Record<string, string[]>;
85
93
  }
86
94
 
87
95
  // Module-level cache for compiled patterns. Route patterns are a finite set
@@ -142,6 +150,7 @@ export function compilePattern(pattern: string): CompiledPattern {
142
150
  const segments = parsePattern(normalizedPattern);
143
151
  const paramNames: string[] = [];
144
152
  const optionalParams = new Set<string>();
153
+ let constraints: Record<string, string[]> | undefined;
145
154
 
146
155
  let regexPattern = "";
147
156
 
@@ -152,11 +161,14 @@ export function compilePattern(pattern: string): CompiledPattern {
152
161
  } else if (segment.type === "param") {
153
162
  paramNames.push(segment.value);
154
163
  const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
155
- const valuePattern = segment.constraint
156
- ? `(${segment.constraint.map(escapeRegex).join("|")})`
157
- : segment.suffix
158
- ? "([^/]+?)"
159
- : "([^/]+)";
164
+ // Constrained params capture anything here; the allowed values are
165
+ // checked post-decode in findMatch so URL-encoded constraint values
166
+ // (e.g. `:lang(en GB)` via `/en%20GB`) still match.
167
+ const valuePattern = segment.suffix ? "([^/]+?)" : "([^/]+)";
168
+
169
+ if (segment.constraint) {
170
+ (constraints ??= {})[segment.value] = segment.constraint;
171
+ }
160
172
 
161
173
  if (segment.optional) {
162
174
  optionalParams.add(segment.value);
@@ -186,9 +198,33 @@ export function compilePattern(pattern: string): CompiledPattern {
186
198
  paramNames,
187
199
  optionalParams,
188
200
  hasTrailingSlash,
201
+ ...(constraints ? { constraints } : {}),
189
202
  };
190
203
  }
191
204
 
205
+ /**
206
+ * Validate decoded params against a compiled pattern's constraints.
207
+ * Returns false if any constrained param has a non-empty value not in the
208
+ * allowed list (empty-string = absent optional, which is allowed).
209
+ */
210
+ function satisfiesConstraints(
211
+ params: Record<string, string>,
212
+ constraints: Record<string, string[]> | undefined,
213
+ ): boolean {
214
+ if (!constraints) return true;
215
+ for (const name in constraints) {
216
+ const value = params[name];
217
+ if (
218
+ value !== undefined &&
219
+ value !== "" &&
220
+ !constraints[name].includes(value)
221
+ ) {
222
+ return false;
223
+ }
224
+ }
225
+ return true;
226
+ }
227
+
192
228
  /**
193
229
  * Escape special regex characters in a string
194
230
  */
@@ -392,8 +428,13 @@ export function findMatch<TEnv>(
392
428
  fullPattern = entry.prefix + pattern;
393
429
  }
394
430
 
395
- const { regex, paramNames, optionalParams, hasTrailingSlash } =
396
- getCompiledPattern(fullPattern);
431
+ const {
432
+ regex,
433
+ paramNames,
434
+ optionalParams,
435
+ hasTrailingSlash,
436
+ constraints,
437
+ } = getCompiledPattern(fullPattern);
397
438
 
398
439
  // Get trailing slash mode for this route (per-route config or pattern-based)
399
440
  const trailingSlashMode: TrailingSlashMode | undefined =
@@ -412,9 +453,15 @@ export function findMatch<TEnv>(
412
453
  if (match) {
413
454
  const params: Record<string, string> = {};
414
455
  paramNames.forEach((name, index) => {
415
- params[name] = match[index + 1] ?? "";
456
+ params[name] = safeDecodeURIComponent(match[index + 1] ?? "");
416
457
  });
417
458
 
459
+ // Validate constraints against decoded values; a failure falls
460
+ // through to the next route so other patterns can still match.
461
+ if (!satisfiesConstraints(params, constraints)) {
462
+ continue;
463
+ }
464
+
418
465
  if (effectiveDebug) {
419
466
  debugLog("findMatch", "matched route", {
420
467
  routeKey,
@@ -467,9 +514,13 @@ export function findMatch<TEnv>(
467
514
  if (altMatch) {
468
515
  const params: Record<string, string> = {};
469
516
  paramNames.forEach((name, index) => {
470
- params[name] = altMatch[index + 1] ?? "";
517
+ params[name] = safeDecodeURIComponent(altMatch[index + 1] ?? "");
471
518
  });
472
519
 
520
+ if (!satisfiesConstraints(params, constraints)) {
521
+ continue;
522
+ }
523
+
473
524
  // Determine redirect behavior based on mode
474
525
  if (trailingSlashMode === "ignore") {
475
526
  // Match without redirect
@@ -54,6 +54,9 @@ export async function matchForPrerender<TEnv = any>(
54
54
  deps: PrerenderMatchDeps<TEnv>,
55
55
  buildVars?: Record<string, any>,
56
56
  isPassthroughRoute?: boolean,
57
+ buildEnv?: TEnv,
58
+ /** Dev-only: check getParams() for passthrough routes to skip unknown params. */
59
+ devMode?: boolean,
57
60
  ): Promise<{
58
61
  segments: SerializedSegmentData[];
59
62
  handles: Record<string, SegmentHandleData>;
@@ -90,21 +93,106 @@ export async function matchForPrerender<TEnv = any>(
90
93
  entries.push(entry);
91
94
  }
92
95
 
96
+ // 3b. Dev-mode passthrough shortcut: if the route is a Passthrough route
97
+ // and has getParams(), check if the matched params are in the known list.
98
+ // In production, only known params are pre-rendered; unknown params fall
99
+ // through to the live handler. Mirror that behavior in dev mode to avoid
100
+ // rendering unknown params with build: true.
101
+ // Vars collected from getParams() probe — merged into render context below.
102
+ let devProbeBuildVars: Record<string, any> | undefined;
103
+
104
+ if (devMode && matchedPassthroughRoute) {
105
+ const routeEntry = entries.find(
106
+ (
107
+ e,
108
+ ): e is EntryData & {
109
+ type: "route";
110
+ prerenderDef: { getParams: (ctx: any) => Promise<any[]> | any[] };
111
+ } =>
112
+ e.type === "route" &&
113
+ !!(e as any).isPassthrough &&
114
+ !!(e as any).prerenderDef?.getParams,
115
+ );
116
+ if (routeEntry) {
117
+ try {
118
+ const probeBuildVars: Record<string, any> = {};
119
+ const knownParamsList = await routeEntry.prerenderDef.getParams({
120
+ build: true as const,
121
+ dev: true,
122
+ set: ((keyOrVar: any, value: any) => {
123
+ contextSet(probeBuildVars, keyOrVar, value);
124
+ }) as any,
125
+ reverse: createReverseFunction(deps.mergedRouteMap),
126
+ get env() {
127
+ if (buildEnv !== undefined) return buildEnv;
128
+ throw new Error(
129
+ "[rsc-router] ctx.env is not available during dev-mode getParams(). " +
130
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
131
+ );
132
+ },
133
+ });
134
+ // Compare only the keys returned by getParams — ignore mount params
135
+ // from include() prefixes that aren't part of the handler's params.
136
+ const isKnown = knownParamsList.some((known: Record<string, any>) => {
137
+ const knownKeys = Object.keys(known);
138
+ return knownKeys.every(
139
+ (k) => String(known[k]) === String(matchedParams[k]),
140
+ );
141
+ });
142
+ if (!isKnown) {
143
+ return {
144
+ segments: [],
145
+ handles: {},
146
+ routeName: matched.routeKey,
147
+ params: matchedParams,
148
+ passthrough: true as const,
149
+ };
150
+ }
151
+ // Preserve vars set by getParams() for the render context
152
+ if (
153
+ Object.keys(probeBuildVars).length > 0 ||
154
+ Object.getOwnPropertySymbols(probeBuildVars).length > 0
155
+ ) {
156
+ devProbeBuildVars = probeBuildVars;
157
+ }
158
+ } catch (err: any) {
159
+ // Mirror production semantics (prerender-collection.ts):
160
+ // Skip errors are intentional — treat as passthrough.
161
+ // All other errors propagate so dev surfaces them.
162
+ if (err?.name === "Skip") {
163
+ return {
164
+ segments: [],
165
+ handles: {},
166
+ routeName: matched.routeKey,
167
+ params: matchedParams,
168
+ passthrough: true as const,
169
+ };
170
+ }
171
+ throw err;
172
+ }
173
+ }
174
+ }
175
+
93
176
  // 4. Create handle store for collecting handle data
94
177
  const handleStore = createHandleStore();
95
178
 
96
179
  // 5. Create a minimal request context with the handle store
97
- // Shallow-copy getParams vars so each param set is independent
98
- const variables: Record<string, any> = buildVars ? { ...buildVars } : {};
180
+ // Shallow-copy getParams vars so each param set is independent.
181
+ // In dev mode, merge vars from the getParams() probe if the caller
182
+ // didn't provide buildVars (production passes them from expandPrerenderRoutes).
183
+ const effectiveBuildVars = buildVars ?? devProbeBuildVars;
184
+ const variables: Record<string, any> = effectiveBuildVars
185
+ ? { ...effectiveBuildVars }
186
+ : {};
99
187
  const stubRes = new Response(null, { status: 200 });
100
188
  const minimalRequestContext: RequestContext<TEnv> = {
101
- env: {} as TEnv,
189
+ env: buildEnv ?? ({} as TEnv),
102
190
  request: new Request("http://prerender" + pathname),
103
191
  url: new URL("http://prerender" + pathname),
104
192
  originalUrl: new URL("http://prerender" + pathname),
105
193
  pathname,
106
194
  searchParams: new URLSearchParams(),
107
- var: variables,
195
+ _variables: variables,
108
196
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
109
197
  set: ((keyOrVar: any, value: any) => {
110
198
  contextSet(variables, keyOrVar, value);
@@ -128,6 +216,8 @@ export async function matchForPrerender<TEnv = any>(
128
216
  _onResponseCallbacks: [],
129
217
  setLocationState() {},
130
218
  _locationState: undefined,
219
+ _renderBarrier: Promise.resolve(),
220
+ _resolveRenderBarrier: () => {},
131
221
  _reportedErrors: new WeakSet<object>(),
132
222
  reverse: createReverseFunction(
133
223
  deps.mergedRouteMap,
@@ -140,7 +230,7 @@ export async function matchForPrerender<TEnv = any>(
140
230
  return runWithRequestContext(minimalRequestContext, async () => {
141
231
  // 6. Create prerender context with synthetic URL.
142
232
  // Prerender handlers get params, pathname, url, searchParams, search,
143
- // reverse, and use(handle) but no request, env, headers, or cookies.
233
+ // reverse, use(handle), and optionally env (when buildEnv is configured).
144
234
  const buildCtx = createPrerenderContext<TEnv>(
145
235
  matchedParams,
146
236
  pathname,
@@ -148,6 +238,8 @@ export async function matchForPrerender<TEnv = any>(
148
238
  matched.routeKey,
149
239
  variables,
150
240
  matchedPassthroughRoute,
241
+ buildEnv,
242
+ devMode,
151
243
  );
152
244
 
153
245
  // 7. Wire use() for handles only (loaders throw)
@@ -320,6 +412,8 @@ export async function renderStaticSegment<TEnv = any>(
320
412
  handlerId: string,
321
413
  mergedRouteMap: Record<string, string>,
322
414
  routeName?: string,
415
+ buildEnv?: TEnv,
416
+ devMode?: boolean,
323
417
  ): Promise<{ encoded: string; handles: Record<string, unknown[]> } | null> {
324
418
  const syntheticUrl = new URL("http://prerender/");
325
419
  const syntheticRequest = new Request(syntheticUrl);
@@ -330,13 +424,13 @@ export async function renderStaticSegment<TEnv = any>(
330
424
  // Minimal request context so setupBuildUse can find the HandleStore
331
425
  const stubRes = new Response(null, { status: 200 });
332
426
  const minimalRequestContext: RequestContext<TEnv> = {
333
- env: {} as TEnv,
427
+ env: buildEnv ?? ({} as TEnv),
334
428
  request: syntheticRequest,
335
429
  url: syntheticUrl,
336
430
  originalUrl: syntheticUrl,
337
431
  pathname: "/",
338
432
  searchParams: syntheticUrl.searchParams,
339
- var: {},
433
+ _variables: {},
340
434
  get: () => undefined as any,
341
435
  set: () => {},
342
436
  params: {},
@@ -358,6 +452,8 @@ export async function renderStaticSegment<TEnv = any>(
358
452
  _onResponseCallbacks: [],
359
453
  setLocationState() {},
360
454
  _locationState: undefined,
455
+ _renderBarrier: Promise.resolve(),
456
+ _resolveRenderBarrier: () => {},
361
457
  _reportedErrors: new WeakSet<object>(),
362
458
  reverse: createReverseFunction(
363
459
  mergedRouteMap,
@@ -368,9 +464,13 @@ export async function renderStaticSegment<TEnv = any>(
368
464
  };
369
465
 
370
466
  return runWithRequestContext(minimalRequestContext, async () => {
371
- // Static handlers get only reverse and use(handle) no URL, params,
372
- // request, env, headers, or cookies.
373
- const buildCtx = createStaticContext<TEnv>(mergedRouteMap, routeName);
467
+ // Static handlers get only reverse, use(handle), and optionally env.
468
+ const buildCtx = createStaticContext<TEnv>(
469
+ mergedRouteMap,
470
+ routeName,
471
+ buildEnv,
472
+ devMode,
473
+ );
374
474
 
375
475
  // Set segment ID so handle pushes are keyed correctly
376
476
  (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
@@ -1,15 +1,9 @@
1
- import { loadManifest } from "./manifest.js";
2
- import { traverseBack } from "./pattern-matching.js";
3
- import { collectRouteMiddleware } from "./middleware.js";
4
- import {
5
- parseAcceptTypes,
6
- RSC_RESPONSE_TYPE,
7
- pickNegotiateVariant,
8
- } from "./content-negotiation.js";
1
+ import { negotiateRoute } from "./content-negotiation.js";
9
2
  import { runWithRouterLogContext, withRouterLogScope } from "./logging.js";
10
3
  import type { EntryData } from "../server/context";
11
4
  import type { RouteMatchResult } from "./pattern-matching.js";
12
5
  import type { MiddlewareFn } from "./middleware.js";
6
+ import { resolveRoute } from "./route-snapshot.js";
13
7
 
14
8
  export interface PreviewMatchDeps<TEnv = any> {
15
9
  findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
@@ -42,110 +36,44 @@ export async function previewMatch<TEnv = any>(
42
36
  const url = new URL(request.url);
43
37
  const pathname = url.pathname;
44
38
 
45
- // Quick route matching
46
- const matched = deps.findMatch(pathname);
47
- if (!matched) {
39
+ // Route resolution via snapshot (lite mode: skip entries/cacheScope
40
+ // since previewMatch only needs matched, manifestEntry, routeMiddleware,
41
+ // and responseType)
42
+ const result = await resolveRoute<TEnv>(pathname, {
43
+ findMatch: deps.findMatch,
44
+ lite: true,
45
+ });
46
+
47
+ if (!result) {
48
48
  return null;
49
49
  }
50
50
 
51
51
  // Skip redirect check - will be handled in full match
52
- if (matched.redirectTo) {
52
+ if (result.type === "redirect") {
53
53
  return { routeMiddleware: undefined };
54
54
  }
55
55
 
56
- // Load manifest (without segment resolution)
57
- const manifestEntry = await loadManifest(
58
- matched.entry,
59
- matched.routeKey,
60
- pathname,
61
- undefined, // No metrics store for preview
62
- false, // isSSR - doesn't matter for preview
63
- );
64
-
65
- // Collect route-level middleware from entry tree
66
- // Includes middleware from orphan layouts (inline layouts within routes)
67
- const routeMiddleware = collectRouteMiddleware(
68
- traverseBack(manifestEntry),
69
- matched.params,
70
- );
71
-
72
- // Check for response type (from trie match or manifest entry)
73
- const responseType =
74
- matched.responseType ||
75
- (manifestEntry.type === "route"
76
- ? manifestEntry.responseType
77
- : undefined);
78
-
79
- // Content negotiation: when negotiate variants exist, pick the best
80
- // handler based on the Accept header. Uses q-values and client order
81
- // as tiebreaker (matching Express/Hono behavior). RSC routes participate
82
- // as text/html candidates so browsers naturally get HTML without
83
- // special-casing.
84
- if (matched.negotiateVariants && matched.negotiateVariants.length > 0) {
85
- const acceptEntries = parseAcceptTypes(
86
- request.headers.get("accept") || "",
87
- );
88
-
89
- // Build candidate list preserving definition order.
90
- // For wildcard (*/*) and no-Accept fallback, the first candidate wins.
91
- const variants = matched.negotiateVariants;
92
- let candidates: Array<{ routeKey: string; responseType: string }>;
93
- if (responseType) {
94
- // Primary is response-type — include it as a candidate
95
- candidates = [
96
- ...variants,
97
- { routeKey: matched.routeKey, responseType },
98
- ];
99
- } else {
100
- // Primary is RSC — insert as text/html candidate in definition order
101
- const rscCandidate = {
102
- routeKey: matched.routeKey,
103
- responseType: RSC_RESPONSE_TYPE,
104
- };
105
- candidates = matched.rscFirst
106
- ? [rscCandidate, ...variants]
107
- : [...variants, rscCandidate];
108
- }
109
-
110
- const variant = pickNegotiateVariant(acceptEntries, candidates);
56
+ const snapshot = result.snapshot;
57
+ const { matched, manifestEntry, routeMiddleware, responseType } =
58
+ snapshot;
111
59
 
112
- // If the winner is RSC, fall through to default RSC handling
113
- if (variant.responseType === RSC_RESPONSE_TYPE) {
114
- // Fall through — RSC won negotiation
115
- } else if (responseType && variant.routeKey === matched.routeKey) {
116
- // Fall through — response-type primary won, already set
117
- } else {
118
- const negotiateEntry = await loadManifest(
119
- matched.entry,
120
- variant.routeKey,
121
- pathname,
122
- undefined,
123
- false,
124
- );
125
- // Recompute middleware from the selected variant's entry tree
126
- // since different variants can have different middleware chains.
127
- const variantMiddleware = collectRouteMiddleware(
128
- traverseBack(negotiateEntry),
129
- matched.params,
130
- );
131
- return {
132
- routeMiddleware:
133
- variantMiddleware.length > 0 ? variantMiddleware : undefined,
134
- responseType: variant.responseType,
135
- handler:
136
- negotiateEntry.type === "route"
137
- ? negotiateEntry.handler
138
- : undefined,
139
- params: matched.params,
140
- negotiated: true,
141
- manifestEntry: negotiateEntry,
142
- routeKey: matched.routeKey,
143
- };
144
- }
60
+ const negotiation = await negotiateRoute(request, pathname, snapshot);
61
+ if (negotiation) {
62
+ return {
63
+ routeMiddleware:
64
+ negotiation.routeMiddleware.length > 0
65
+ ? negotiation.routeMiddleware
66
+ : undefined,
67
+ responseType: negotiation.responseType,
68
+ handler: negotiation.handler,
69
+ params: matched.params,
70
+ negotiated: true,
71
+ manifestEntry: negotiation.manifestEntry,
72
+ routeKey: matched.routeKey,
73
+ };
145
74
  }
146
75
 
147
- // If we passed through the negotiation block (variants exist), mark as
148
- // negotiated so the handler sets Vary: Accept on the response.
76
+ // No negotiation or RSC won return default route info
149
77
  const hasVariants =
150
78
  matched.negotiateVariants && matched.negotiateVariants.length > 0;
151
79
  return {