@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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 +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- 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 +45 -3
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- 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/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- 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/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 +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- 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 +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -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 +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- 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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- 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 +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ShouldRevalidateFn,
|
|
19
19
|
} from "../types.js";
|
|
20
20
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
21
|
+
import type { TelemetrySink } from "./telemetry.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Revalidation context passed to segment resolution
|
|
@@ -79,6 +80,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
79
80
|
routeMap?: Record<string, string>,
|
|
80
81
|
routeName?: string,
|
|
81
82
|
responseType?: string,
|
|
83
|
+
isPassthroughRoute?: boolean,
|
|
82
84
|
) => HandlerContext<any, TEnv>;
|
|
83
85
|
|
|
84
86
|
// Loader setup
|
|
@@ -181,6 +183,12 @@ export interface RouterContext<TEnv = any> {
|
|
|
181
183
|
context: HandlerContext<any, TEnv>;
|
|
182
184
|
actionContext?: any;
|
|
183
185
|
stale?: boolean;
|
|
186
|
+
traceSource?:
|
|
187
|
+
| "segment-resolution"
|
|
188
|
+
| "cache-hit"
|
|
189
|
+
| "loader"
|
|
190
|
+
| "parallel"
|
|
191
|
+
| "orphan-layout";
|
|
184
192
|
}) => Promise<boolean>;
|
|
185
193
|
|
|
186
194
|
// Request context
|
|
@@ -234,6 +242,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
234
242
|
nextUrl: URL,
|
|
235
243
|
routeKey: string,
|
|
236
244
|
actionContext?: any,
|
|
245
|
+
stale?: boolean,
|
|
237
246
|
) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
|
|
238
247
|
|
|
239
248
|
// Entry revalidation map
|
|
@@ -241,6 +250,12 @@ export interface RouterContext<TEnv = any> {
|
|
|
241
250
|
entries: EntryData[],
|
|
242
251
|
) => Map<string, { revalidate: ShouldRevalidateFn[] }>;
|
|
243
252
|
|
|
253
|
+
// Telemetry sink (optional, no-op when undefined)
|
|
254
|
+
telemetry?: TelemetrySink;
|
|
255
|
+
|
|
256
|
+
// Request ID for telemetry span correlation (set per-request in match handlers)
|
|
257
|
+
requestId?: string;
|
|
258
|
+
|
|
244
259
|
// Intercept loaders only (for cache hit + intercept scenarios)
|
|
245
260
|
resolveInterceptLoadersOnly?: (
|
|
246
261
|
intercept: InterceptEntry,
|
|
@@ -14,6 +14,7 @@ import type { MiddlewareEntry, MiddlewareFn } from "./middleware.js";
|
|
|
14
14
|
import { RSC_ROUTER_BRAND } from "./router-registry.js";
|
|
15
15
|
import type { RSCRouterOptions, RootLayoutProps } from "./router-options.js";
|
|
16
16
|
import type { DefaultVars } from "../types/global-namespace.js";
|
|
17
|
+
import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Options passed to router.fetch(), router.match(), and other request entrypoints.
|
|
@@ -246,6 +247,15 @@ export interface RSCRouterInternal<
|
|
|
246
247
|
*/
|
|
247
248
|
readonly themeConfig: import("../theme/types.js").ResolvedThemeConfig | null;
|
|
248
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Cache profiles for "use cache" per-request resolution.
|
|
252
|
+
* Always includes at least the "default" profile.
|
|
253
|
+
*/
|
|
254
|
+
readonly cacheProfiles: Record<
|
|
255
|
+
string,
|
|
256
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
257
|
+
>;
|
|
258
|
+
|
|
249
259
|
/**
|
|
250
260
|
* Cache-Control header value for prefetch responses.
|
|
251
261
|
* False means no browser caching of prefetch responses.
|
|
@@ -265,6 +275,16 @@ export interface RSCRouterInternal<
|
|
|
265
275
|
*/
|
|
266
276
|
readonly allowDebugManifest: boolean;
|
|
267
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Resolved timeout configuration (merged from shorthand + structured).
|
|
280
|
+
*/
|
|
281
|
+
readonly timeouts: ResolvedTimeouts;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Custom timeout response handler.
|
|
285
|
+
*/
|
|
286
|
+
readonly onTimeout?: OnTimeoutCallback<TEnv>;
|
|
287
|
+
|
|
268
288
|
/**
|
|
269
289
|
* App-level middleware entries
|
|
270
290
|
* These wrap the entire request/response cycle
|
|
@@ -286,6 +306,18 @@ export interface RSCRouterInternal<
|
|
|
286
306
|
*/
|
|
287
307
|
readonly urlpatterns?: UrlPatterns<TEnv, any>;
|
|
288
308
|
|
|
309
|
+
/**
|
|
310
|
+
* SSR configuration. resolveStreaming determines stream vs allReady
|
|
311
|
+
* per HTML request (undefined = always stream).
|
|
312
|
+
*/
|
|
313
|
+
readonly ssr?: import("./router-options.js").SSROptions<TEnv>;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Cross-origin request protection configuration.
|
|
317
|
+
* Default: true (enabled).
|
|
318
|
+
*/
|
|
319
|
+
readonly originCheck: import("../rsc/origin-guard.js").OriginCheckConfig<TEnv>;
|
|
320
|
+
|
|
289
321
|
/**
|
|
290
322
|
* Source file path where createRouter() was called.
|
|
291
323
|
* Set via Error.stack parsing at construction time.
|
|
@@ -307,6 +339,7 @@ export interface RSCRouterInternal<
|
|
|
307
339
|
pathname: string,
|
|
308
340
|
params: Record<string, string>,
|
|
309
341
|
buildVars?: Record<string, any>,
|
|
342
|
+
isPassthroughRoute?: boolean,
|
|
310
343
|
): Promise<{
|
|
311
344
|
segments: SerializedSegmentData[];
|
|
312
345
|
handles: Record<string, SegmentHandleData>;
|
|
@@ -314,6 +347,7 @@ export interface RSCRouterInternal<
|
|
|
314
347
|
params: Record<string, string>;
|
|
315
348
|
interceptSegments?: SerializedSegmentData[];
|
|
316
349
|
interceptHandles?: Record<string, SegmentHandleData>;
|
|
350
|
+
passthrough?: true;
|
|
317
351
|
} | null>;
|
|
318
352
|
|
|
319
353
|
/**
|
|
@@ -9,6 +9,58 @@ import type { NonceProvider } from "../rsc/types.js";
|
|
|
9
9
|
import type { ExecutionContext } from "../server/request-context.js";
|
|
10
10
|
import type { UrlPatterns } from "../urls.js";
|
|
11
11
|
import type { NamedRouteEntry } from "./content-negotiation.js";
|
|
12
|
+
import type { TelemetrySink } from "./telemetry.js";
|
|
13
|
+
import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SSR stream mode returned by resolveStreaming.
|
|
17
|
+
*
|
|
18
|
+
* - `"stream"` — start flushing HTML as soon as the shell is ready
|
|
19
|
+
* (default React SSR behavior via `renderToReadableStream`).
|
|
20
|
+
* - `"allReady"` — wait for every Suspense boundary to resolve before
|
|
21
|
+
* sending any bytes (equivalent to awaiting `stream.allReady`).
|
|
22
|
+
*/
|
|
23
|
+
export type SSRStreamMode = "stream" | "allReady";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Context passed to the resolveStreaming callback.
|
|
27
|
+
*/
|
|
28
|
+
export interface ResolveStreamingContext<TEnv = unknown> {
|
|
29
|
+
request: Request;
|
|
30
|
+
env: TEnv;
|
|
31
|
+
url: URL;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* SSR configuration options.
|
|
36
|
+
*/
|
|
37
|
+
export interface SSROptions<TEnv = unknown> {
|
|
38
|
+
/**
|
|
39
|
+
* Determine whether an HTML response should stream progressively or
|
|
40
|
+
* wait for full readiness before flushing.
|
|
41
|
+
*
|
|
42
|
+
* Called once per HTML request, before the HTML response is produced.
|
|
43
|
+
* Does NOT apply to RSC responses (`__rsc`, partial navigation, prefetch).
|
|
44
|
+
*
|
|
45
|
+
* Return `"stream"` (default) for progressive streaming or `"allReady"`
|
|
46
|
+
* to buffer the complete HTML before sending.
|
|
47
|
+
*
|
|
48
|
+
* @example Bot detection
|
|
49
|
+
* ```ts
|
|
50
|
+
* createRouter({
|
|
51
|
+
* ssr: {
|
|
52
|
+
* resolveStreaming: async ({ request, env }) => {
|
|
53
|
+
* const bot = await detectBot(request, env);
|
|
54
|
+
* return bot.isBot && !bot.supportsStreaming ? "allReady" : "stream";
|
|
55
|
+
* },
|
|
56
|
+
* },
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
resolveStreaming?: (
|
|
61
|
+
context: ResolveStreamingContext<TEnv>,
|
|
62
|
+
) => SSRStreamMode | Promise<SSRStreamMode>;
|
|
63
|
+
}
|
|
12
64
|
|
|
13
65
|
/**
|
|
14
66
|
* Props passed to the root layout component
|
|
@@ -384,4 +436,152 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
384
436
|
* @default true
|
|
385
437
|
*/
|
|
386
438
|
warmup?: boolean;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Shorthand timeout (ms) applied to both action execution and render start.
|
|
442
|
+
* Does NOT apply to streamIdleMs.
|
|
443
|
+
* Overridden by individual values in `timeouts`.
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```typescript
|
|
447
|
+
* createRouter({ timeout: 10_000 });
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
timeout?: number;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Structured timeout configuration per phase.
|
|
454
|
+
* Values here override the `timeout` shorthand.
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* createRouter({
|
|
459
|
+
* timeouts: {
|
|
460
|
+
* actionMs: 10_000,
|
|
461
|
+
* renderStartMs: 8_000,
|
|
462
|
+
* },
|
|
463
|
+
* });
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
timeouts?: RouterTimeouts;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Custom handler invoked when a timeout occurs.
|
|
470
|
+
* Receives context about which phase timed out and must return a Response.
|
|
471
|
+
* If not provided, returns a plain 504 with "Request timed out" body
|
|
472
|
+
* and X-Rango-Timeout-Phase header.
|
|
473
|
+
*
|
|
474
|
+
* If the callback throws, the default 504 response is used as fallback.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```typescript
|
|
478
|
+
* createRouter({
|
|
479
|
+
* timeout: 10_000,
|
|
480
|
+
* onTimeout: (ctx) => {
|
|
481
|
+
* return new Response(
|
|
482
|
+
* JSON.stringify({ error: "timeout", phase: ctx.phase }),
|
|
483
|
+
* { status: 504, headers: { "Content-Type": "application/json" } },
|
|
484
|
+
* );
|
|
485
|
+
* },
|
|
486
|
+
* });
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
onTimeout?: OnTimeoutCallback<TEnv>;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Telemetry sink for structured lifecycle events.
|
|
493
|
+
*
|
|
494
|
+
* When provided, the router emits events for request start/end,
|
|
495
|
+
* loader start/end/error, handler errors, cache decisions, and
|
|
496
|
+
* revalidation decisions.
|
|
497
|
+
*
|
|
498
|
+
* No-op when not configured (zero overhead).
|
|
499
|
+
*
|
|
500
|
+
* @example Console logging
|
|
501
|
+
* ```typescript
|
|
502
|
+
* import { createConsoleSink } from "@rangojs/router";
|
|
503
|
+
*
|
|
504
|
+
* const router = createRouter({
|
|
505
|
+
* telemetry: createConsoleSink(),
|
|
506
|
+
* });
|
|
507
|
+
* ```
|
|
508
|
+
*
|
|
509
|
+
* @example Custom sink
|
|
510
|
+
* ```typescript
|
|
511
|
+
* const router = createRouter({
|
|
512
|
+
* telemetry: {
|
|
513
|
+
* emit(event) {
|
|
514
|
+
* myTracer.record(event);
|
|
515
|
+
* },
|
|
516
|
+
* },
|
|
517
|
+
* });
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
telemetry?: TelemetrySink;
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* SSR configuration options.
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```typescript
|
|
527
|
+
* createRouter({
|
|
528
|
+
* ssr: {
|
|
529
|
+
* resolveStreaming: async ({ request, env }) => {
|
|
530
|
+
* const bot = await detectBot(request, env);
|
|
531
|
+
* return bot.isBot ? "allReady" : "stream";
|
|
532
|
+
* },
|
|
533
|
+
* },
|
|
534
|
+
* });
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
ssr?: SSROptions<TEnv>;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Cross-origin request protection for server actions, loader fetches,
|
|
541
|
+
* and progressive enhancement form submissions.
|
|
542
|
+
*
|
|
543
|
+
* When enabled, the router validates that the request's Origin header
|
|
544
|
+
* (or Referer fallback) matches the Host before executing actions,
|
|
545
|
+
* loaders, or PE submissions. Requests without Origin/Referer are
|
|
546
|
+
* allowed (same-origin navigations, non-browser clients).
|
|
547
|
+
*
|
|
548
|
+
* The built-in check compares Origin against the Host header and
|
|
549
|
+
* url.protocol. It does NOT trust X-Forwarded-Host/Proto headers
|
|
550
|
+
* (they are client-controllable without a trusted proxy). On standard
|
|
551
|
+
* deployments (Cloudflare Workers, Node behind nginx/caddy) the Host
|
|
552
|
+
* header is already set to the public-facing host by the platform or
|
|
553
|
+
* proxy. For non-standard proxy setups where Host differs from the
|
|
554
|
+
* public origin, use a custom function that reads the appropriate
|
|
555
|
+
* forwarded headers from your trusted proxy.
|
|
556
|
+
*
|
|
557
|
+
* - `true` (default) -- enable built-in origin validation
|
|
558
|
+
* - `false` -- disable
|
|
559
|
+
* - function -- full custom control with access to env, phase,
|
|
560
|
+
* and the built-in check via `ctx.defaultCheck()`
|
|
561
|
+
*
|
|
562
|
+
* The callback receives `OriginCheckContext` with `request`, `url`,
|
|
563
|
+
* `env`, `routerId`, `phase` ("action" | "loader" | "pe-form"),
|
|
564
|
+
* and `defaultCheck()`. Return `true` to allow, `false` for default
|
|
565
|
+
* 403 rejection, or a `Response` for custom rejection.
|
|
566
|
+
*
|
|
567
|
+
* @default true
|
|
568
|
+
*
|
|
569
|
+
* @example Trusted proxy with X-Forwarded-Host
|
|
570
|
+
* ```ts
|
|
571
|
+
* createRouter({
|
|
572
|
+
* originCheck({ request, url, env, defaultCheck }) {
|
|
573
|
+
* if (env.TRUST_PROXY) {
|
|
574
|
+
* const origin = request.headers.get("origin");
|
|
575
|
+
* if (!origin) return true;
|
|
576
|
+
* if (origin === "null") return false;
|
|
577
|
+
* const host = request.headers.get("x-forwarded-host")
|
|
578
|
+
* ?? request.headers.get("host") ?? url.host;
|
|
579
|
+
* return origin.toLowerCase() === `${url.protocol}//${host}`.toLowerCase();
|
|
580
|
+
* }
|
|
581
|
+
* return defaultCheck();
|
|
582
|
+
* },
|
|
583
|
+
* });
|
|
584
|
+
* ```
|
|
585
|
+
*/
|
|
586
|
+
originCheck?: import("../rsc/origin-guard.js").OriginCheckConfig<TEnv>;
|
|
387
587
|
}
|
|
@@ -22,6 +22,51 @@ import {
|
|
|
22
22
|
resolveLayoutComponent,
|
|
23
23
|
resolveWithErrorBoundary,
|
|
24
24
|
} from "./helpers.js";
|
|
25
|
+
import { getRouterContext } from "../router-context.js";
|
|
26
|
+
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Streamed handler telemetry
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Attach a fire-and-forget rejection observer to a streamed handler promise.
|
|
34
|
+
* React catches the actual error via its error boundary; this only emits
|
|
35
|
+
* the handler.error telemetry event.
|
|
36
|
+
*/
|
|
37
|
+
function observeStreamedHandler(
|
|
38
|
+
promise: Promise<ReactNode>,
|
|
39
|
+
segmentId: string,
|
|
40
|
+
segmentType: string,
|
|
41
|
+
pathname?: string,
|
|
42
|
+
routeKey?: string,
|
|
43
|
+
params?: Record<string, string>,
|
|
44
|
+
): void {
|
|
45
|
+
let routerCtx;
|
|
46
|
+
try {
|
|
47
|
+
routerCtx = getRouterContext();
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!routerCtx?.telemetry) return;
|
|
52
|
+
const sink = resolveSink(routerCtx.telemetry);
|
|
53
|
+
const reqId = routerCtx.requestId;
|
|
54
|
+
promise.catch((err: unknown) => {
|
|
55
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
56
|
+
safeEmit(sink, {
|
|
57
|
+
type: "handler.error",
|
|
58
|
+
timestamp: performance.now(),
|
|
59
|
+
requestId: reqId,
|
|
60
|
+
segmentId,
|
|
61
|
+
segmentType,
|
|
62
|
+
error: errorObj,
|
|
63
|
+
handledByBoundary: true,
|
|
64
|
+
pathname,
|
|
65
|
+
routeKey,
|
|
66
|
+
params,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
25
70
|
|
|
26
71
|
// ---------------------------------------------------------------------------
|
|
27
72
|
// Fresh path (full match, no revalidation)
|
|
@@ -128,19 +173,8 @@ export async function resolveSegment<TEnv>(
|
|
|
128
173
|
segments.push(...loaderSegments);
|
|
129
174
|
}
|
|
130
175
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
parallelEntry,
|
|
134
|
-
params,
|
|
135
|
-
context,
|
|
136
|
-
false,
|
|
137
|
-
entry.shortCode,
|
|
138
|
-
deps,
|
|
139
|
-
options,
|
|
140
|
-
);
|
|
141
|
-
segments.push(...parallelSegments);
|
|
142
|
-
}
|
|
143
|
-
|
|
176
|
+
// Handler-first: layout handler executes before its parallels and orphan
|
|
177
|
+
// layouts so that ctx.set() values are visible to all children.
|
|
144
178
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
145
179
|
entry.shortCode;
|
|
146
180
|
|
|
@@ -160,6 +194,20 @@ export async function resolveSegment<TEnv>(
|
|
|
160
194
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
161
195
|
});
|
|
162
196
|
|
|
197
|
+
for (const parallelEntry of entry.parallel) {
|
|
198
|
+
const parallelSegments = await resolveParallelEntry(
|
|
199
|
+
parallelEntry,
|
|
200
|
+
params,
|
|
201
|
+
context,
|
|
202
|
+
false,
|
|
203
|
+
entry.shortCode,
|
|
204
|
+
deps,
|
|
205
|
+
options,
|
|
206
|
+
routeKey,
|
|
207
|
+
);
|
|
208
|
+
segments.push(...parallelSegments);
|
|
209
|
+
}
|
|
210
|
+
|
|
163
211
|
for (const orphan of entry.layout) {
|
|
164
212
|
const orphanSegments = await resolveOrphanLayout(
|
|
165
213
|
orphan,
|
|
@@ -169,6 +217,7 @@ export async function resolveSegment<TEnv>(
|
|
|
169
217
|
false,
|
|
170
218
|
deps,
|
|
171
219
|
options,
|
|
220
|
+
routeKey,
|
|
172
221
|
);
|
|
173
222
|
segments.push(...orphanSegments);
|
|
174
223
|
}
|
|
@@ -194,8 +243,23 @@ export async function resolveSegment<TEnv>(
|
|
|
194
243
|
if (component === undefined) {
|
|
195
244
|
if (entry.loading) {
|
|
196
245
|
const result = handleHandlerResult(entry.handler(context));
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
if (result instanceof Promise) {
|
|
247
|
+
const tracked = deps.trackHandler(result, {
|
|
248
|
+
segmentId: entry.shortCode,
|
|
249
|
+
segmentType: entry.type,
|
|
250
|
+
});
|
|
251
|
+
observeStreamedHandler(
|
|
252
|
+
tracked,
|
|
253
|
+
entry.shortCode,
|
|
254
|
+
entry.type,
|
|
255
|
+
context.pathname,
|
|
256
|
+
routeKey,
|
|
257
|
+
params,
|
|
258
|
+
);
|
|
259
|
+
component = tracked;
|
|
260
|
+
} else {
|
|
261
|
+
component = result;
|
|
262
|
+
}
|
|
199
263
|
} else {
|
|
200
264
|
component = handleHandlerResult(await entry.handler(context));
|
|
201
265
|
}
|
|
@@ -210,6 +274,7 @@ export async function resolveSegment<TEnv>(
|
|
|
210
274
|
true,
|
|
211
275
|
deps,
|
|
212
276
|
options,
|
|
277
|
+
routeKey,
|
|
213
278
|
);
|
|
214
279
|
segments.push(...orphanSegments);
|
|
215
280
|
}
|
|
@@ -223,6 +288,7 @@ export async function resolveSegment<TEnv>(
|
|
|
223
288
|
entry.shortCode,
|
|
224
289
|
deps,
|
|
225
290
|
options,
|
|
291
|
+
routeKey,
|
|
226
292
|
);
|
|
227
293
|
segments.push(...parallelSegments);
|
|
228
294
|
}
|
|
@@ -257,6 +323,7 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
257
323
|
belongsToRoute: boolean,
|
|
258
324
|
deps: SegmentResolutionDeps<TEnv>,
|
|
259
325
|
options?: ResolveSegmentOptions,
|
|
326
|
+
routeKey?: string,
|
|
260
327
|
): Promise<ResolvedSegment[]> {
|
|
261
328
|
invariant(
|
|
262
329
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -274,19 +341,8 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
274
341
|
segments.push(...loaderSegments);
|
|
275
342
|
}
|
|
276
343
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
parallelEntry,
|
|
280
|
-
params,
|
|
281
|
-
context,
|
|
282
|
-
belongsToRoute,
|
|
283
|
-
orphan.shortCode,
|
|
284
|
-
deps,
|
|
285
|
-
options,
|
|
286
|
-
);
|
|
287
|
-
segments.push(...parallelSegments);
|
|
288
|
-
}
|
|
289
|
-
|
|
344
|
+
// Handler-first: orphan layout handler executes before its parallels
|
|
345
|
+
// so that ctx.set() values are visible to parallel children.
|
|
290
346
|
const component = await resolveLayoutComponent(orphan, context);
|
|
291
347
|
|
|
292
348
|
segments.push({
|
|
@@ -303,6 +359,20 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
303
359
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
304
360
|
});
|
|
305
361
|
|
|
362
|
+
for (const parallelEntry of orphan.parallel) {
|
|
363
|
+
const parallelSegments = await resolveParallelEntry(
|
|
364
|
+
parallelEntry,
|
|
365
|
+
params,
|
|
366
|
+
context,
|
|
367
|
+
belongsToRoute,
|
|
368
|
+
orphan.shortCode,
|
|
369
|
+
deps,
|
|
370
|
+
options,
|
|
371
|
+
routeKey,
|
|
372
|
+
);
|
|
373
|
+
segments.push(...parallelSegments);
|
|
374
|
+
}
|
|
375
|
+
|
|
306
376
|
return segments;
|
|
307
377
|
}
|
|
308
378
|
|
|
@@ -317,6 +387,7 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
317
387
|
parentShortCode: string,
|
|
318
388
|
deps: SegmentResolutionDeps<TEnv>,
|
|
319
389
|
options?: ResolveSegmentOptions,
|
|
390
|
+
routeKey?: string,
|
|
320
391
|
): Promise<ResolvedSegment[]> {
|
|
321
392
|
invariant(
|
|
322
393
|
parallelEntry.type === "parallel",
|
|
@@ -344,7 +415,23 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
344
415
|
if (hasLoadingFallback) {
|
|
345
416
|
const result =
|
|
346
417
|
typeof handler === "function" ? handler(context) : handler;
|
|
347
|
-
|
|
418
|
+
if (result instanceof Promise) {
|
|
419
|
+
const tracked = deps.trackHandler(result, {
|
|
420
|
+
segmentId: `${parentShortCode}.${slot}`,
|
|
421
|
+
segmentType: "parallel",
|
|
422
|
+
});
|
|
423
|
+
observeStreamedHandler(
|
|
424
|
+
tracked,
|
|
425
|
+
`${parentShortCode}.${slot}`,
|
|
426
|
+
"parallel",
|
|
427
|
+
context.pathname,
|
|
428
|
+
routeKey,
|
|
429
|
+
params,
|
|
430
|
+
);
|
|
431
|
+
component = tracked as ReactNode;
|
|
432
|
+
} else {
|
|
433
|
+
component = result as ReactNode;
|
|
434
|
+
}
|
|
348
435
|
} else {
|
|
349
436
|
component =
|
|
350
437
|
typeof handler === "function" ? await handler(context) : handler;
|
|
@@ -405,6 +492,12 @@ export async function resolveAllSegments<TEnv>(
|
|
|
405
492
|
safeRequest = context.request;
|
|
406
493
|
} catch {}
|
|
407
494
|
|
|
495
|
+
// Get telemetry sink from RouterContext (may not exist during prerendering)
|
|
496
|
+
let telemetry;
|
|
497
|
+
try {
|
|
498
|
+
telemetry = getRouterContext()?.telemetry;
|
|
499
|
+
} catch {}
|
|
500
|
+
|
|
408
501
|
for (const entry of entries) {
|
|
409
502
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
410
503
|
entry,
|
|
@@ -422,7 +515,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
422
515
|
),
|
|
423
516
|
(seg) => [seg],
|
|
424
517
|
deps,
|
|
425
|
-
{ request: safeRequest, url: context.url, routeKey },
|
|
518
|
+
{ request: safeRequest, url: context.url, routeKey, telemetry },
|
|
426
519
|
context.pathname,
|
|
427
520
|
);
|
|
428
521
|
// Deduplicate by segment ID. include() scopes can produce entries that
|
|
@@ -23,6 +23,8 @@ import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
|
|
|
23
23
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
24
24
|
import { debugLog } from "../logging.js";
|
|
25
25
|
import { tryStaticLookup } from "./static-store.js";
|
|
26
|
+
import type { TelemetrySink } from "../telemetry.js";
|
|
27
|
+
import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
|
|
26
28
|
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
// Handler result processing
|
|
@@ -116,6 +118,7 @@ export interface ErrorReportContext {
|
|
|
116
118
|
env?: any;
|
|
117
119
|
isPartial?: boolean;
|
|
118
120
|
requestStartTime?: number;
|
|
121
|
+
telemetry?: TelemetrySink;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/**
|
|
@@ -150,6 +153,22 @@ export function catchSegmentError<TEnv>(
|
|
|
150
153
|
metadata,
|
|
151
154
|
requestStartTime: report.requestStartTime,
|
|
152
155
|
});
|
|
156
|
+
if (report.telemetry) {
|
|
157
|
+
const errorObj =
|
|
158
|
+
error instanceof Error ? error : new Error(String(error));
|
|
159
|
+
safeEmit(resolveSink(report.telemetry), {
|
|
160
|
+
type: "handler.error",
|
|
161
|
+
timestamp: performance.now(),
|
|
162
|
+
requestId: report.request ? getRequestId(report.request) : undefined,
|
|
163
|
+
segmentId: entry.shortCode,
|
|
164
|
+
segmentType: entry.type,
|
|
165
|
+
error: errorObj,
|
|
166
|
+
handledByBoundary,
|
|
167
|
+
pathname,
|
|
168
|
+
routeKey: report.routeKey,
|
|
169
|
+
params,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
153
172
|
};
|
|
154
173
|
|
|
155
174
|
const setResponseStatus = (status: number) => {
|