@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

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 (278) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +57 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. package/src/browser/action-response-classifier.ts +0 -99
@@ -8,8 +8,67 @@ import {
8
8
  _getRequestContext,
9
9
  getLocationState,
10
10
  } from "../server/request-context.js";
11
+ import type { RequestContext } from "../server/request-context.js";
11
12
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
13
+ import { isRedirectResponse } from "../response-utils.js";
12
14
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
15
+ import { formatCacheSignalHeader } from "../router/telemetry.js";
16
+
17
+ /**
18
+ * DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
19
+ * match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
20
+ * header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
21
+ * attached — output is byte-identical to the default. Header mutation failures
22
+ * are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
23
+ */
24
+ function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
25
+ const signal = ctx._cacheSignal;
26
+ if (!signal || signal.length === 0) return;
27
+ try {
28
+ target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
29
+ } catch {
30
+ // Headers immutable — skip.
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Copy stub headers from the request context onto a target Headers instance:
36
+ * append Set-Cookie entries, set everything else only if absent. Header
37
+ * mutation failures are swallowed so the same logic works against Response
38
+ * headers that may be immutable (e.g. Cloudflare protocol-switch responses).
39
+ */
40
+ function applyStubHeaders(target: Headers, stub: Headers): void {
41
+ stub.forEach((value, name) => {
42
+ try {
43
+ if (name.toLowerCase() === "set-cookie") {
44
+ target.append(name, value);
45
+ } else if (!target.has(name)) {
46
+ target.set(name, value);
47
+ }
48
+ } catch {
49
+ // Headers immutable — skip.
50
+ }
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Drain ctx._onResponseCallbacks onto a response. Swapping the array before
56
+ * iteration prevents re-entrant registrations from double-firing and matches
57
+ * the contract that each callback runs at most once per request.
58
+ */
59
+ function drainOnResponseCallbacks(
60
+ ctx: RequestContext,
61
+ response: Response,
62
+ ): Response {
63
+ const callbacks = ctx._onResponseCallbacks;
64
+ if (callbacks.length === 0) return response;
65
+ ctx._onResponseCallbacks = [];
66
+ let result = response;
67
+ for (const callback of callbacks) {
68
+ result = callback(result) ?? result;
69
+ }
70
+ return result;
71
+ }
13
72
 
14
73
  /**
15
74
  * Check if a request body has content to decode
@@ -39,40 +98,24 @@ export function createResponseWithMergedHeaders(
39
98
  return new Response(body, init);
40
99
  }
41
100
 
42
- // Merge headers from stub response into the new response.
43
- // Delete Set-Cookie from the stub after consuming so that downstream
44
- // merge points (e.g. executeMiddleware) do not duplicate them.
101
+ // Delete Set-Cookie from the stub after consuming so downstream merge
102
+ // points (e.g. executeMiddleware) don't duplicate them.
45
103
  const mergedHeaders = new Headers(init.headers);
46
- ctx.res.headers.forEach((value, name) => {
47
- if (name.toLowerCase() === "set-cookie") {
48
- mergedHeaders.append(name, value);
49
- } else if (!mergedHeaders.has(name)) {
50
- // Only set if not already present in init.headers
51
- mergedHeaders.set(name, value);
52
- }
53
- });
104
+ applyStubHeaders(mergedHeaders, ctx.res.headers);
54
105
  ctx.res.headers.delete("set-cookie");
106
+ applyCacheSignalHeader(mergedHeaders, ctx);
55
107
 
56
- // Use ctx.res.status if it was set (e.g., 404 for notFound, 500 for error)
57
- // Otherwise use the status from init
108
+ // ctx.res.status overrides init.status when explicitly set (e.g. 404 for
109
+ // notFound, 500 for error). Default ctx.res.status is 200.
58
110
  const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
59
111
 
60
- let response = new Response(body, {
112
+ const response = new Response(body, {
61
113
  ...init,
62
114
  status,
63
115
  headers: mergedHeaders,
64
116
  });
65
117
 
66
- // Run onResponse callbacks - each can inspect/modify the response.
67
- // Drain the array so that downstream callers (e.g. finalizeResponse)
68
- // do not re-execute the same callbacks on this response.
69
- const callbacks = ctx._onResponseCallbacks;
70
- ctx._onResponseCallbacks = [];
71
- for (const callback of callbacks) {
72
- response = callback(response) ?? response;
73
- }
74
-
75
- return response;
118
+ return drainOnResponseCallbacks(ctx, response);
76
119
  }
77
120
 
78
121
  /**
@@ -122,10 +165,10 @@ export function interceptRedirectForPartial(
122
165
  locationState?: Record<string, unknown>,
123
166
  ) => Response,
124
167
  ): Response | null {
125
- const redirectUrl = response.headers.get("Location");
126
- if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
168
+ if (!isRedirectResponse(response)) {
127
169
  return null;
128
170
  }
171
+ const redirectUrl = response.headers.get("Location")!;
129
172
  const locationState = getLocationState();
130
173
  let intercepted: Response;
131
174
  if (locationState) {
@@ -175,24 +218,29 @@ export function buildRouteMiddlewareEntries<TEnv>(
175
218
  }
176
219
 
177
220
  /**
178
- * Run onResponse callbacks on an existing Response.
179
- *
180
- * Used for code paths that bypass createResponseWithMergedHeaders(), such as
181
- * middleware short-circuits where the Response is already constructed but
182
- * ctx.onResponse() callbacks still need to fire.
221
+ * Merge stub headers from the request context onto an existing Response in
222
+ * place, then drain onResponse callbacks. Used when a Response cannot flow
223
+ * through `new Response()` status 101 is outside the constructor's
224
+ * 200-599 range, and the Cloudflare-specific `webSocket` property would be
225
+ * lost on reconstruction.
183
226
  */
184
- export function finalizeResponse(response: Response): Response {
227
+ export function mergeStubHeadersAndFinalize(response: Response): Response {
185
228
  const ctx = _getRequestContext();
186
- if (!ctx || ctx._onResponseCallbacks.length === 0) {
187
- return response;
188
- }
229
+ if (!ctx) return response;
189
230
 
190
- // Drain the array so callbacks run at most once per request.
191
- const callbacks = ctx._onResponseCallbacks;
192
- ctx._onResponseCallbacks = [];
193
- let result = response;
194
- for (const callback of callbacks) {
195
- result = callback(result) ?? result;
196
- }
197
- return result;
231
+ applyStubHeaders(response.headers, ctx.res.headers);
232
+ ctx.res.headers.delete("set-cookie");
233
+
234
+ return drainOnResponseCallbacks(ctx, response);
235
+ }
236
+
237
+ /**
238
+ * Run onResponse callbacks on an existing Response. Used by code paths that
239
+ * bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
240
+ * but still need ctx.onResponse() callbacks to fire.
241
+ */
242
+ export function finalizeResponse(response: Response): Response {
243
+ const ctx = _getRequestContext();
244
+ if (!ctx) return response;
245
+ return drainOnResponseCallbacks(ctx, response);
198
246
  }
package/src/rsc/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RSC Router - RSC Entry Point
2
+ * Rango - RSC Entry Point
3
3
  *
4
4
  * This module provides RSC utilities for server-side rendering,
5
5
  * server actions, loader fetching, and progressive enhancement.
@@ -13,6 +13,7 @@ import {
13
13
  setRouteTrie,
14
14
  setRouterManifest,
15
15
  setRouterTrie,
16
+ setRouterPrecomputedEntries,
16
17
  } from "../route-map-builder.js";
17
18
 
18
19
  /**
@@ -36,47 +37,13 @@ export async function buildRouterTrieFromUrlpatterns(
36
37
  undefined,
37
38
  router.basename ? { urlPrefix: router.basename } : undefined,
38
39
  );
39
- if (
40
- generated._routeAncestry &&
41
- Object.keys(generated._routeAncestry).length > 0
42
- ) {
43
- const { buildRouteTrie } = await import("../build/route-trie.js");
44
- // Map each route to its include() staticPrefix so the trie
45
- // returns the correct sp for lazy entry lookup in findMatch.
46
- const routeToStaticPrefix: Record<string, string> = {};
47
- for (const name of Object.keys(generated.routeManifest)) {
48
- routeToStaticPrefix[name] = "";
49
- }
50
- // Override with prefix from include() entries so the trie
51
- // returns the correct sp for lazy entry lookup in findMatch.
52
- // Walk recursively to include routes in nested includes.
53
- if (generated.prefixTree) {
54
- const visitPrefixNode = (node: any): void => {
55
- const sp = node.staticPrefix || "";
56
- for (const route of node.routes || []) {
57
- routeToStaticPrefix[route] = sp;
58
- }
59
- for (const child of Object.values(node.children || {})) {
60
- visitPrefixNode(child);
61
- }
62
- };
63
- for (const node of Object.values(generated.prefixTree)) {
64
- visitPrefixNode(node);
65
- }
66
- }
67
- const trie = buildRouteTrie(
68
- generated.routeManifest,
69
- generated._routeAncestry,
70
- routeToStaticPrefix,
71
- generated.routeTrailingSlash,
72
- generated.prerenderRoutes
73
- ? new Set(generated.prerenderRoutes)
74
- : undefined,
75
- generated.passthroughRoutes
76
- ? new Set(generated.passthroughRoutes)
77
- : undefined,
78
- generated.responseTypeRoutes,
79
- );
40
+ // Build the trie through the SAME shared helper the production discovery uses
41
+ // (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
42
+ // serialized trie cannot drift. buildPerRouterTrie returns null when there
43
+ // are no routes.
44
+ const { buildPerRouterTrie } = await import("../build/route-trie.js");
45
+ const trie = buildPerRouterTrie(generated);
46
+ if (trie) {
80
47
  setRouterTrie(router.id, trie);
81
48
  // Set global trie only if not already set by another router
82
49
  if (!getRouteTrie()) {
@@ -84,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
84
51
  }
85
52
  }
86
53
  setRouterManifest(router.id, generated.routeManifest);
54
+
55
+ // Match the production discovery path: precompute leaf-include entries so the
56
+ // match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
57
+ // Without this, dev re-runs each matched leaf include's handler at match time
58
+ // (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
59
+ // match-time run is skipped and the handler runs once per first request.
60
+ // Identical route ownership to the handler path (the shortcut is guarded by
61
+ // the same prefixIsShared and #506 checks production uses).
62
+ const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
63
+ const precomputed: Array<{
64
+ staticPrefix: string;
65
+ routes: Record<string, string>;
66
+ }> = [];
67
+ flattenLeafEntries(
68
+ generated.prefixTree,
69
+ generated.routeManifest,
70
+ precomputed,
71
+ );
72
+ setRouterPrecomputedEntries(router.id, precomputed);
73
+
87
74
  // Merge into global manifest (needed for reverse/href across routers)
88
75
  const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
89
76
  setCachedManifest({ ...existing, ...generated.routeManifest });
@@ -9,11 +9,29 @@
9
9
  * navigations, bookmarks, and non-browser clients don't send Origin.
10
10
  */
11
11
 
12
+ import type { RequestPlan } from "../router/request-classification.js";
13
+
12
14
  /**
13
15
  * Request phase that triggered the origin check.
14
16
  */
15
17
  export type OriginCheckPhase = "action" | "loader" | "pe-form";
16
18
 
19
+ // Exhaustive over RequestPlan modes so a new mode must be classified here (the
20
+ // security gate) instead of silently falling through to no origin check.
21
+ export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
22
+ RequestPlan["mode"],
23
+ OriginCheckPhase | null
24
+ > = {
25
+ action: "action",
26
+ loader: "loader",
27
+ "pe-render": "pe-form",
28
+ "full-render": null,
29
+ "partial-render": null,
30
+ response: null,
31
+ redirect: null,
32
+ "version-mismatch": null,
33
+ };
34
+
17
35
  /**
18
36
  * Context passed to the originCheck callback.
19
37
  */
@@ -116,14 +134,15 @@ export async function checkRequestOrigin<TEnv = any>(
116
134
  // Disabled by explicit opt-out
117
135
  if (config === false) return null;
118
136
 
119
- // Default: built-in validation (config === true or undefined)
120
- if (config === true || config === undefined) {
121
- const allowed = defaultOriginCheck(request, url);
122
- if (allowed) return null;
123
- return createForbiddenResponse(request);
124
- }
137
+ // Default (true/undefined) becomes a callback returning boolean, so the
138
+ // Response|true|reject resolution below is written once.
139
+ const check: (
140
+ ctx: OriginCheckContext<TEnv>,
141
+ ) => boolean | Response | Promise<boolean | Response> =
142
+ config === true || config === undefined
143
+ ? () => defaultOriginCheck(request, url)
144
+ : config;
125
145
 
126
- // Custom function — build context and call
127
146
  const ctx: OriginCheckContext<TEnv> = {
128
147
  request,
129
148
  url,
@@ -133,9 +152,8 @@ export async function checkRequestOrigin<TEnv = any>(
133
152
  defaultCheck: () => defaultOriginCheck(request, url),
134
153
  };
135
154
 
136
- const result = await config(ctx);
155
+ const result = await check(ctx);
137
156
 
138
157
  if (result instanceof Response) return result;
139
- if (result === true) return null;
140
- return createForbiddenResponse(request);
158
+ return result === true ? null : createForbiddenResponse(request);
141
159
  }
@@ -248,6 +248,8 @@ export async function handleProgressiveEnhancement<TEnv>(
248
248
  segments: match.segments,
249
249
  matched: match.matched,
250
250
  diff: match.diff,
251
+ resolvedIds: match.resolvedIds,
252
+ params: match.params,
251
253
  isPartial: false,
252
254
  rootLayout: ctx.router.rootLayout,
253
255
  handles: handleStore.stream(),
@@ -353,6 +355,8 @@ async function renderPeErrorBoundary<TEnv>(
353
355
  segments: errorResult.segments,
354
356
  matched: errorResult.matched,
355
357
  diff: errorResult.diff,
358
+ resolvedIds: errorResult.resolvedIds,
359
+ params: errorResult.params,
356
360
  isPartial: false,
357
361
  isError: true,
358
362
  rootLayout: ctx.router.rootLayout,
@@ -1,37 +1,104 @@
1
1
  /**
2
- * Response Error Payload Builder
2
+ * Problem Details (RFC 9457) Builder
3
3
  *
4
- * Builds a ResponseError object from a caught error, controlling
5
- * what information is exposed based on error type and environment.
4
+ * Builds a problem+json error body from a caught error, controlling what
5
+ * information is exposed based on error type and environment.
6
6
  */
7
7
 
8
8
  import { RouterError } from "../errors.js";
9
- import type { ResponseError } from "../urls.js";
9
+ import type { ProblemDetails } from "../urls.js";
10
10
 
11
11
  /**
12
- * Build a ResponseError payload from a caught error.
13
- * RouterError messages are always exposed (developer-crafted).
12
+ * HTTP reason phrases for the problem `title` member. Inlined because the
13
+ * router targets edge/worker runtimes without node's `http.STATUS_CODES`;
14
+ * covers the full standard 4xx/5xx range, with a generic fallback for any
15
+ * non-standard status a handler might set.
16
+ */
17
+ const STATUS_PHRASES: Record<number, string> = {
18
+ 400: "Bad Request",
19
+ 401: "Unauthorized",
20
+ 402: "Payment Required",
21
+ 403: "Forbidden",
22
+ 404: "Not Found",
23
+ 405: "Method Not Allowed",
24
+ 406: "Not Acceptable",
25
+ 407: "Proxy Authentication Required",
26
+ 408: "Request Timeout",
27
+ 409: "Conflict",
28
+ 410: "Gone",
29
+ 411: "Length Required",
30
+ 412: "Precondition Failed",
31
+ 413: "Payload Too Large",
32
+ 414: "URI Too Long",
33
+ 415: "Unsupported Media Type",
34
+ 416: "Range Not Satisfiable",
35
+ 417: "Expectation Failed",
36
+ 418: "I'm a Teapot",
37
+ 421: "Misdirected Request",
38
+ 422: "Unprocessable Entity",
39
+ 423: "Locked",
40
+ 424: "Failed Dependency",
41
+ 425: "Too Early",
42
+ 426: "Upgrade Required",
43
+ 428: "Precondition Required",
44
+ 429: "Too Many Requests",
45
+ 431: "Request Header Fields Too Large",
46
+ 451: "Unavailable For Legal Reasons",
47
+ 500: "Internal Server Error",
48
+ 501: "Not Implemented",
49
+ 502: "Bad Gateway",
50
+ 503: "Service Unavailable",
51
+ 504: "Gateway Timeout",
52
+ 505: "HTTP Version Not Supported",
53
+ 506: "Variant Also Negotiates",
54
+ 507: "Insufficient Storage",
55
+ 508: "Loop Detected",
56
+ 510: "Not Extended",
57
+ 511: "Network Authentication Required",
58
+ };
59
+
60
+ function statusPhrase(status: number): string {
61
+ return STATUS_PHRASES[status] ?? "Error";
62
+ }
63
+
64
+ /**
65
+ * Build an RFC 9457 problem+json body from a caught error.
66
+ * RouterError messages/codes are always exposed (developer-crafted).
14
67
  * Standard Error messages are hidden in production.
68
+ *
69
+ * The `type` member is omitted in this phase: per RFC 9457 an absent `type` is
70
+ * treated as `"about:blank"` (no semantics beyond the HTTP status), so emitting
71
+ * it adds nothing. Per-route problem-type URIs arrive with the declared-errors
72
+ * map later. `code` is always present so consumers can branch on it
73
+ * (`"INTERNAL"` for non-RouterError failures).
15
74
  */
16
- export function createResponseErrorPayload(
75
+ export function createProblemDetails(
17
76
  error: unknown,
77
+ status: number,
18
78
  isDev: boolean,
19
- ): ResponseError {
79
+ ): ProblemDetails {
20
80
  if (error instanceof RouterError) {
21
81
  return {
22
- message: error.message,
82
+ title: statusPhrase(status),
83
+ status,
84
+ detail: error.message,
23
85
  code: error.code,
24
- ...(error.type ? { type: error.type } : {}),
25
86
  ...(isDev && error.stack ? { stack: error.stack } : {}),
26
87
  };
27
88
  }
28
89
  if (error instanceof Error) {
29
90
  return {
30
- message: isDev ? error.message : "Internal Server Error",
91
+ title: statusPhrase(status),
92
+ status,
93
+ detail: isDev ? error.message : "Internal Server Error",
94
+ code: "INTERNAL",
31
95
  ...(isDev && error.stack ? { stack: error.stack } : {}),
32
96
  };
33
97
  }
34
98
  return {
35
- message: isDev ? String(error) : "Internal Server Error",
99
+ title: statusPhrase(status),
100
+ status,
101
+ detail: isDev ? String(error) : "Internal Server Error",
102
+ code: "INTERNAL",
36
103
  };
37
104
  }
@@ -11,6 +11,7 @@ import { requireRequestContext } from "../server/request-context.js";
11
11
  import { contextGet } from "../context-var.js";
12
12
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
13
  import { traverseBack } from "../router/pattern-matching.js";
14
+ import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
14
15
  import { createCacheScope } from "../cache/cache-scope.js";
15
16
  import { executeMiddleware } from "../router/middleware.js";
16
17
  import {
@@ -20,13 +21,15 @@ import {
20
21
  import type { MiddlewareFn } from "../router/middleware.js";
21
22
  import type { EntryData } from "../server/context.js";
22
23
  import type { HandlerContext } from "./handler-context.js";
23
- import { createResponseErrorPayload } from "./response-error.js";
24
+ import { createProblemDetails } from "./response-error.js";
24
25
  import {
25
26
  createResponseWithMergedHeaders,
26
27
  finalizeResponse,
27
28
  isCacheableStatus,
28
29
  buildRouteMiddlewareEntries,
30
+ mergeStubHeadersAndFinalize,
29
31
  } from "./helpers.js";
32
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
30
33
 
31
34
  export interface ResponseRouteMatch {
32
35
  responseType: string;
@@ -78,10 +81,13 @@ export async function handleResponseRoute<TEnv>(
78
81
  env,
79
82
  searchParams: cleanUrl.searchParams,
80
83
  url: cleanUrl,
84
+ originalUrl: reqCtx.originalUrl,
81
85
  pathname: url.pathname,
82
86
  reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
83
87
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
84
88
  header: (name: string, value: string) => reqCtx.header(name, value),
89
+ waitUntil: reqCtx.waitUntil.bind(reqCtx),
90
+ executionContext: reqCtx.executionContext,
85
91
  _responseType: preview.responseType,
86
92
  };
87
93
  // Brand with taint symbol so "use cache" detects it as request-scoped
@@ -96,6 +102,12 @@ export async function handleResponseRoute<TEnv>(
96
102
  // so that stub headers (cookies, custom headers set via ctx.header()) are included.
97
103
  // Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
98
104
  const rewrapResponse = (result: Response) => {
105
+ // 204/205/304 are NOT short-circuited — they're valid for the Response
106
+ // constructor and must honor ctx.setStatus() overrides. Only upgrade
107
+ // responses (status 101 / `webSocket` property) bypass reconstruction.
108
+ if (isWebSocketUpgradeResponse(result)) {
109
+ return mergeStubHeadersAndFinalize(result);
110
+ }
99
111
  const headers = new Headers();
100
112
  result.headers.forEach((value, key) => {
101
113
  if (key.toLowerCase() === "set-cookie") {
@@ -110,37 +122,6 @@ export async function handleResponseRoute<TEnv>(
110
122
  });
111
123
  };
112
124
 
113
- // JSON response routes: wrap in { data } / { error } envelope
114
- if (preview.responseType === "json") {
115
- try {
116
- const result = await (preview.handler as Function)(responseHandlerCtx);
117
- if (result instanceof Response) {
118
- return rewrapResponse(result);
119
- }
120
- return createResponseWithMergedHeaders(
121
- JSON.stringify({ data: result }),
122
- {
123
- status: 200,
124
- headers: { "content-type": "application/json;charset=utf-8" },
125
- },
126
- );
127
- } catch (error) {
128
- handlerCtx.callOnError(error, "handler", errorCtx);
129
- const isDev = process.env.NODE_ENV !== "production";
130
- const status = error instanceof RouterError ? error.status : 500;
131
- return createResponseWithMergedHeaders(
132
- JSON.stringify({
133
- error: createResponseErrorPayload(error, isDev),
134
- }),
135
- {
136
- status,
137
- headers: { "content-type": "application/json;charset=utf-8" },
138
- },
139
- );
140
- }
141
- }
142
-
143
- // Non-JSON response routes: catch errors and return plain Response
144
125
  try {
145
126
  const result = await (preview.handler as Function)(responseHandlerCtx);
146
127
 
@@ -148,38 +129,51 @@ export async function handleResponseRoute<TEnv>(
148
129
  return rewrapResponse(result);
149
130
  }
150
131
 
151
- // Auto-wrap based on response type tag
152
- switch (preview.responseType) {
153
- case "text":
154
- return createResponseWithMergedHeaders(String(result), {
155
- status: 200,
156
- headers: { "content-type": "text/plain;charset=utf-8" },
157
- });
158
- case "html":
159
- return createResponseWithMergedHeaders(String(result), {
160
- status: 200,
161
- headers: { "content-type": "text/html;charset=utf-8" },
162
- });
163
- case "xml":
164
- return createResponseWithMergedHeaders(String(result), {
165
- status: 200,
166
- headers: { "content-type": "application/xml;charset=utf-8" },
167
- });
168
- case "md":
169
- return createResponseWithMergedHeaders(String(result), {
170
- status: 200,
171
- headers: { "content-type": "text/markdown;charset=utf-8" },
172
- });
173
- default:
174
- // image, stream, any -- must return Response
175
- throw new Error(
176
- `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
177
- );
132
+ // Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
133
+ if (preview.responseType === "json") {
134
+ return createResponseWithMergedHeaders(JSON.stringify(result), {
135
+ status: 200,
136
+ headers: { "content-type": "application/json;charset=utf-8" },
137
+ });
138
+ }
139
+
140
+ // Object.hasOwn (not truthiness) so prototype names like "toString" are not
141
+ // matched; image/stream/any are absent and fall through to the throw.
142
+ if (Object.hasOwn(RESPONSE_TYPE_MIME, preview.responseType)) {
143
+ return createResponseWithMergedHeaders(String(result), {
144
+ status: 200,
145
+ headers: {
146
+ "content-type": `${RESPONSE_TYPE_MIME[preview.responseType]};charset=utf-8`,
147
+ },
148
+ });
178
149
  }
150
+
151
+ throw new Error(
152
+ `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
153
+ );
179
154
  } catch (error) {
180
155
  handlerCtx.callOnError(error, "handler", errorCtx);
181
156
  const isDev = process.env.NODE_ENV !== "production";
182
- const status = error instanceof RouterError ? error.status : 500;
157
+ const derivedStatus = error instanceof RouterError ? error.status : 500;
158
+ // Resolve the effective status the same way createResponseWithMergedHeaders
159
+ // will (ctx.res.status override) so the problem body's status/title match
160
+ // the actual HTTP status — e.g. when a handler called ctx.setStatus()
161
+ // before throwing.
162
+ const status =
163
+ reqCtx.res.status !== 200 ? reqCtx.res.status : derivedStatus;
164
+
165
+ if (preview.responseType === "json") {
166
+ return createResponseWithMergedHeaders(
167
+ JSON.stringify(createProblemDetails(error, status, isDev)),
168
+ {
169
+ status,
170
+ headers: {
171
+ "content-type": "application/problem+json;charset=utf-8",
172
+ },
173
+ },
174
+ );
175
+ }
176
+
183
177
  const message =
184
178
  error instanceof RouterError
185
179
  ? error.message
@@ -196,7 +190,9 @@ export async function handleResponseRoute<TEnv>(
196
190
  // Wrap callHandler to append Vary: Accept on content-negotiated responses
197
191
  const callHandlerWithVary = async () => {
198
192
  const response = await callHandler();
199
- if (preview.negotiated) {
193
+ if (preview.negotiated && !isWebSocketUpgradeResponse(response)) {
194
+ // Skip Vary on upgrade responses: headers are semantically immutable
195
+ // on some runtimes, and Vary is meaningless for a 101 response.
200
196
  response.headers.append("Vary", "Accept");
201
197
  }
202
198
  return response;