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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -4,7 +4,12 @@
4
4
  * Utility functions for RSC request handling.
5
5
  */
6
6
 
7
- import { getRequestContext } from "../server/request-context.js";
7
+ import {
8
+ _getRequestContext,
9
+ getLocationState,
10
+ } from "../server/request-context.js";
11
+ import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
12
+ import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
8
13
 
9
14
  /**
10
15
  * Check if a request body has content to decode
@@ -29,12 +34,14 @@ export function createResponseWithMergedHeaders(
29
34
  body: BodyInit | null,
30
35
  init: ResponseInit,
31
36
  ): Response {
32
- const ctx = getRequestContext();
37
+ const ctx = _getRequestContext();
33
38
  if (!ctx) {
34
39
  return new Response(body, init);
35
40
  }
36
41
 
37
- // Merge headers from stub response into the new response
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.
38
45
  const mergedHeaders = new Headers(init.headers);
39
46
  ctx.res.headers.forEach((value, name) => {
40
47
  if (name.toLowerCase() === "set-cookie") {
@@ -44,6 +51,7 @@ export function createResponseWithMergedHeaders(
44
51
  mergedHeaders.set(name, value);
45
52
  }
46
53
  });
54
+ ctx.res.headers.delete("set-cookie");
47
55
 
48
56
  // Use ctx.res.status if it was set (e.g., 404 for notFound, 500 for error)
49
57
  // Otherwise use the status from init
@@ -55,8 +63,12 @@ export function createResponseWithMergedHeaders(
55
63
  headers: mergedHeaders,
56
64
  });
57
65
 
58
- // Run onResponse callbacks - each can inspect/modify the response
59
- for (const callback of ctx._onResponseCallbacks) {
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) {
60
72
  response = callback(response) ?? response;
61
73
  }
62
74
 
@@ -76,3 +88,111 @@ export function createSimpleRedirectResponse(redirectUrl: string): Response {
76
88
  headers: { "X-RSC-Redirect": redirectUrl },
77
89
  });
78
90
  }
91
+
92
+ /**
93
+ * Carry over headers from a source redirect Response to a wrapper Response.
94
+ * Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
95
+ * and appends Set-Cookie to avoid clobbering multiple cookie headers.
96
+ */
97
+ export function carryOverRedirectHeaders(
98
+ source: Response,
99
+ target: Response,
100
+ ): void {
101
+ source.headers.forEach((value, name) => {
102
+ const lower = name.toLowerCase();
103
+ if (lower === "location" || lower === "x-rsc-redirect") return;
104
+ if (lower === "set-cookie") {
105
+ target.headers.append(name, value);
106
+ } else if (!target.headers.has(name)) {
107
+ target.headers.set(name, value);
108
+ }
109
+ });
110
+ }
111
+
112
+ /**
113
+ * If a response is a 3xx redirect during a partial (client-side) request,
114
+ * intercept it and return a Flight-compatible redirect instead.
115
+ * fetch() auto-follows 3xx which would hit a URL that renders full HTML
116
+ * the client can't parse. Returns null if the response is not a redirect.
117
+ */
118
+ export function interceptRedirectForPartial(
119
+ response: Response,
120
+ createRedirectFlightResponse: (
121
+ redirectUrl: string,
122
+ locationState?: Record<string, unknown>,
123
+ ) => Response,
124
+ ): Response | null {
125
+ const redirectUrl = response.headers.get("Location");
126
+ if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
127
+ return null;
128
+ }
129
+ const locationState = getLocationState();
130
+ let intercepted: Response;
131
+ if (locationState) {
132
+ intercepted = createRedirectFlightResponse(
133
+ redirectUrl,
134
+ resolveLocationStateEntries(locationState),
135
+ );
136
+ } else {
137
+ intercepted = createSimpleRedirectResponse(redirectUrl);
138
+ }
139
+
140
+ carryOverRedirectHeaders(response, intercepted);
141
+
142
+ return intercepted;
143
+ }
144
+
145
+ /**
146
+ * Only cache successful responses. Non-200 statuses (errors, redirects) are
147
+ * not cached -- notFound() produces 500 in response routes, and explicit
148
+ * non-200 Responses are rare enough that caching them would be surprising.
149
+ */
150
+ export function isCacheableStatus(status: number): boolean {
151
+ return status === 200;
152
+ }
153
+
154
+ /**
155
+ * Convert route-level middleware entries to the format expected by
156
+ * executeMiddleware. Route middleware from previewMatch carries just
157
+ * { handler, params }; this wraps them in the full MiddlewareEntry shape.
158
+ */
159
+ export function buildRouteMiddlewareEntries<TEnv>(
160
+ routeMiddleware: Array<{
161
+ handler: MiddlewareFn;
162
+ params: Record<string, string>;
163
+ }>,
164
+ ): Array<{ entry: MiddlewareEntry<TEnv>; params: Record<string, string> }> {
165
+ return routeMiddleware.map((mw) => ({
166
+ entry: {
167
+ pattern: null,
168
+ regex: null,
169
+ paramNames: [],
170
+ handler: mw.handler,
171
+ mountPrefix: null,
172
+ } as MiddlewareEntry<TEnv>,
173
+ params: mw.params,
174
+ }));
175
+ }
176
+
177
+ /**
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.
183
+ */
184
+ export function finalizeResponse(response: Response): Response {
185
+ const ctx = _getRequestContext();
186
+ if (!ctx || ctx._onResponseCallbacks.length === 0) {
187
+ return response;
188
+ }
189
+
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;
198
+ }
package/src/rsc/index.ts CHANGED
@@ -29,28 +29,8 @@ export type {
29
29
  NonceProvider,
30
30
  } from "./types.js";
31
31
 
32
- // Re-export HandleStore types for consumers who need custom handling
33
- export {
34
- createHandleStore,
35
- type HandleStore,
36
- type HandleData,
37
- } from "../server/handle-store.js";
38
-
39
32
  // Re-export request context utilities for server-side access to env/request/params
40
33
  export {
41
34
  getRequestContext,
42
35
  requireRequestContext,
43
- setRequestContextParams,
44
36
  } from "../server/request-context.js";
45
-
46
- // Re-export cache store types and implementations
47
- export type {
48
- SegmentCacheStore,
49
- CachedEntryData,
50
- CachedEntryResult,
51
- SegmentCacheProvider,
52
- SegmentHandleData,
53
- } from "../cache/types.js";
54
-
55
- export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
56
- export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
@@ -14,9 +14,20 @@
14
14
  import { getLoaderLazy } from "../server/loader-registry.js";
15
15
  import { executeLoaderMiddleware } from "../router/middleware.js";
16
16
  import { requireRequestContext } from "../server/request-context.js";
17
- import { createReverseFunction } from "../router/handler-context.js";
18
- import { getGlobalRouteMap } from "../route-map-builder.js";
19
- import { createResponseWithMergedHeaders } from "./helpers.js";
17
+ import {
18
+ createReverseFunction,
19
+ stripInternalParams,
20
+ } from "../router/handler-context.js";
21
+ import {
22
+ getGlobalRouteMap,
23
+ getSearchSchema,
24
+ isRouteRootScoped,
25
+ } from "../route-map-builder.js";
26
+ import { parseSearchParams } from "../search-params.js";
27
+ import {
28
+ createResponseWithMergedHeaders,
29
+ finalizeResponse,
30
+ } from "./helpers.js";
20
31
  import type { HandlerContext } from "./handler-context.js";
21
32
 
22
33
  export async function handleLoaderFetch<TEnv>(
@@ -44,6 +55,15 @@ export async function handleLoaderFetch<TEnv>(
44
55
  );
45
56
  }
46
57
 
58
+ // Non-fetchable loaders are registered for SSR ctx.use() only.
59
+ // They must not be callable through the standalone _rsc_loader endpoint.
60
+ if (!registeredLoader.fetchable) {
61
+ return createResponseWithMergedHeaders(
62
+ `Loader "${loaderId}" is not fetchable`,
63
+ { status: 403 },
64
+ );
65
+ }
66
+
47
67
  // Parse params, body, and formData based on request method and content type
48
68
  let loaderParams: Record<string, string> = {};
49
69
  let loaderBody: unknown = undefined;
@@ -90,51 +110,73 @@ export async function handleLoaderFetch<TEnv>(
90
110
  }
91
111
  }
92
112
 
93
- // Execute the loader with middleware
113
+ // Execute the loader with middleware.
114
+ // finalizeResponse drains onResponse callbacks that middleware short-circuits
115
+ // may leave behind (executeLoaderMiddleware does not finalize them itself).
94
116
  try {
95
117
  const { fn, middleware } = registeredLoader;
96
118
 
97
- return await executeLoaderMiddleware(
98
- middleware,
99
- request,
100
- env,
101
- loaderParams,
102
- variables,
103
- async () => {
104
- const reqCtx = requireRequestContext();
105
- // Merge route params (from previewMatch) with explicit loader params.
106
- // Explicit params take precedence over route-matched params.
107
- const mergedParams = {
108
- ...(routeParams ?? {}),
109
- ...loaderParams,
110
- };
111
- const loaderCtx: any = {
112
- ...reqCtx,
113
- params: mergedParams,
114
- body: loaderBody,
115
- method: request.method,
116
- reverse: createReverseFunction(
117
- getGlobalRouteMap(),
118
- reqCtx._routeName,
119
- mergedParams,
120
- ),
121
- ...(loaderFormData ? { formData: loaderFormData } : {}),
122
- };
119
+ return finalizeResponse(
120
+ await executeLoaderMiddleware(
121
+ middleware,
122
+ request,
123
+ env,
124
+ loaderParams,
125
+ variables,
126
+ async () => {
127
+ const reqCtx = requireRequestContext();
128
+ // Merge route params (from previewMatch) with explicit loader params.
129
+ // Explicit params take precedence over route-matched params.
130
+ const resolvedRouteParams = routeParams ?? {};
131
+ const mergedParams = {
132
+ ...resolvedRouteParams,
133
+ ...loaderParams,
134
+ };
135
+ // Strip _rsc_* transport params so loaders see the same
136
+ // url/searchParams as during SSR/navigation.
137
+ const cleanUrl = stripInternalParams(url);
138
+ const cleanSearchParams = cleanUrl.searchParams;
139
+ const searchSchema = reqCtx._routeName
140
+ ? getSearchSchema(reqCtx._routeName)
141
+ : undefined;
142
+ const loaderCtx: any = {
143
+ ...reqCtx,
144
+ url: cleanUrl,
145
+ pathname: cleanUrl.pathname,
146
+ searchParams: cleanSearchParams,
147
+ search: searchSchema
148
+ ? parseSearchParams(cleanSearchParams, searchSchema)
149
+ : {},
150
+ params: mergedParams,
151
+ routeParams: resolvedRouteParams,
152
+ body: loaderBody,
153
+ method: request.method,
154
+ reverse: createReverseFunction(
155
+ getGlobalRouteMap(),
156
+ reqCtx._routeName,
157
+ mergedParams,
158
+ reqCtx._routeName
159
+ ? isRouteRootScoped(reqCtx._routeName)
160
+ : undefined,
161
+ ),
162
+ ...(loaderFormData ? { formData: loaderFormData } : {}),
163
+ };
123
164
 
124
- const result = await fn(loaderCtx);
165
+ const result = await fn(loaderCtx);
125
166
 
126
- interface LoaderPayload {
127
- loaderResult: unknown;
128
- }
129
- const loaderPayload: LoaderPayload = { loaderResult: result };
130
- const rscStream =
131
- ctx.renderToReadableStream<LoaderPayload>(loaderPayload);
167
+ interface LoaderPayload {
168
+ loaderResult: unknown;
169
+ }
170
+ const loaderPayload: LoaderPayload = { loaderResult: result };
171
+ const rscStream =
172
+ ctx.renderToReadableStream<LoaderPayload>(loaderPayload);
132
173
 
133
- return createResponseWithMergedHeaders(rscStream, {
134
- headers: { "content-type": "text/x-component;charset=utf-8" },
135
- });
136
- },
137
- createReverseFunction(ctx.getRequiredRouteMap()),
174
+ return createResponseWithMergedHeaders(rscStream, {
175
+ headers: { "content-type": "text/x-component;charset=utf-8" },
176
+ });
177
+ },
178
+ createReverseFunction(ctx.getRequiredRouteMap()),
179
+ ),
138
180
  );
139
181
  } catch (error) {
140
182
  const err = error instanceof Error ? error : new Error(String(error));
@@ -29,8 +29,9 @@ import {
29
29
  export async function buildRouterTrieFromUrlpatterns(
30
30
  router: any,
31
31
  ): Promise<void> {
32
- const { generateManifest } = await import("../build/generate-manifest.js");
33
- const generated = generateManifest(router.urlpatterns);
32
+ const { generateManifestFull } =
33
+ await import("../build/generate-manifest.js");
34
+ const generated = generateManifestFull(router.urlpatterns);
34
35
  if (
35
36
  generated._routeAncestry &&
36
37
  Object.keys(generated._routeAncestry).length > 0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Origin Guard
3
+ *
4
+ * Cross-origin request protection for server actions, loader fetches, and
5
+ * progressive enhancement form submissions. Validates that the Origin header
6
+ * (or Referer fallback) matches the request Host before executing.
7
+ *
8
+ * Requests without an Origin or Referer header are allowed — same-origin
9
+ * navigations, bookmarks, and non-browser clients don't send Origin.
10
+ */
11
+
12
+ /**
13
+ * Request phase that triggered the origin check.
14
+ */
15
+ export type OriginCheckPhase = "action" | "loader" | "pe-form";
16
+
17
+ /**
18
+ * Context passed to the originCheck callback.
19
+ */
20
+ export interface OriginCheckContext<TEnv = any> {
21
+ request: Request;
22
+ url: URL;
23
+ env: TEnv;
24
+ routerId: string;
25
+ phase: OriginCheckPhase;
26
+ /** Run the built-in conservative check (Origin/Referer vs Host + url.protocol). */
27
+ defaultCheck: () => boolean;
28
+ }
29
+
30
+ /**
31
+ * Configuration for the origin check.
32
+ *
33
+ * - `true` (default) — built-in conservative check
34
+ * - `false` — disabled
35
+ * - function — custom control; return true to allow, false to reject with
36
+ * default 403, or a Response for custom rejection
37
+ */
38
+ export type OriginCheckConfig<TEnv = any> =
39
+ | boolean
40
+ | ((
41
+ ctx: OriginCheckContext<TEnv>,
42
+ ) => boolean | Response | Promise<boolean | Response>);
43
+
44
+ /**
45
+ * Built-in conservative origin check.
46
+ * Compares Origin (or Referer fallback) against Host + url.protocol.
47
+ * Does NOT trust X-Forwarded-Host/Proto headers.
48
+ *
49
+ * Returns true to allow, false to reject.
50
+ */
51
+ export function defaultOriginCheck(request: Request, url: URL): boolean {
52
+ // 1. Read Origin header (present on all cross-origin requests and
53
+ // same-origin POST/PUT/PATCH/DELETE in modern browsers)
54
+ let requestOrigin = request.headers.get("origin");
55
+
56
+ // 2. Fallback to Referer if Origin is absent (some proxies strip it)
57
+ if (!requestOrigin) {
58
+ const referer = request.headers.get("referer");
59
+ if (referer) {
60
+ try {
61
+ requestOrigin = new URL(referer).origin;
62
+ } catch {
63
+ // Malformed referer — treat as absent
64
+ }
65
+ }
66
+ }
67
+
68
+ // 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
69
+ if (!requestOrigin) return true;
70
+
71
+ // "null" origin comes from privacy-sensitive contexts (data: URLs,
72
+ // sandboxed iframes, cross-origin redirects). Reject it.
73
+ if (requestOrigin === "null") return false;
74
+
75
+ // 4. Determine expected host from Host header or URL.
76
+ // X-Forwarded-Host/Proto are NOT used — they are client-controllable
77
+ // unless a trusted proxy strips them. On standard deployments (Cloudflare
78
+ // Workers, Node behind nginx/caddy) the Host header is already correct.
79
+ // For non-standard setups, use the custom function escape hatch.
80
+ const expectedHost = request.headers.get("host") || url.host;
81
+ const expectedProtocol = url.protocol;
82
+
83
+ // 5. Build expected origin and compare (case-insensitive)
84
+ const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
85
+
86
+ return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
87
+ }
88
+
89
+ function createForbiddenResponse(request: Request): Response {
90
+ const isDev = process.env.NODE_ENV !== "production";
91
+ const body = isDev
92
+ ? "Forbidden: Origin mismatch. The request origin does not match the server host. " +
93
+ `Set originCheck: false in createRouter() to disable this check. ` +
94
+ `(Origin: ${request.headers.get("origin") ?? "none"}, ` +
95
+ `Host: ${request.headers.get("host") ?? "none"})`
96
+ : "Forbidden";
97
+
98
+ return new Response(body, {
99
+ status: 403,
100
+ headers: { "X-Rango-Origin-Check": "failed" },
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Configuration-aware origin check dispatcher.
106
+ * Builds the OriginCheckContext and delegates to the configured check.
107
+ */
108
+ export async function checkRequestOrigin<TEnv = any>(
109
+ request: Request,
110
+ url: URL,
111
+ config: OriginCheckConfig<TEnv> | undefined,
112
+ env: TEnv,
113
+ routerId: string,
114
+ phase: OriginCheckPhase,
115
+ ): Promise<Response | null> {
116
+ // Disabled by explicit opt-out
117
+ if (config === false) return null;
118
+
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
+ }
125
+
126
+ // Custom function — build context and call
127
+ const ctx: OriginCheckContext<TEnv> = {
128
+ request,
129
+ url,
130
+ env,
131
+ routerId,
132
+ phase,
133
+ defaultCheck: () => defaultOriginCheck(request, url),
134
+ };
135
+
136
+ const result = await config(ctx);
137
+
138
+ if (result instanceof Response) return result;
139
+ if (result === true) return null;
140
+ return createForbiddenResponse(request);
141
+ }