@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.
- package/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/rsc/helpers.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Utility functions for RSC request handling.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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";
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -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 {
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
params
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
reqCtx._routeName
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
165
|
+
const result = await fn(loaderCtx);
|
|
125
166
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
interface LoaderPayload {
|
|
168
|
+
loaderResult: unknown;
|
|
169
|
+
}
|
|
170
|
+
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
+
const rscStream =
|
|
172
|
+
ctx.renderToReadableStream<LoaderPayload>(loaderPayload);
|
|
132
173
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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));
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -29,8 +29,9 @@ import {
|
|
|
29
29
|
export async function buildRouterTrieFromUrlpatterns(
|
|
30
30
|
router: any,
|
|
31
31
|
): Promise<void> {
|
|
32
|
-
const {
|
|
33
|
-
|
|
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
|
+
}
|