@rangojs/router 0.0.0-experimental.126 → 0.0.0-experimental.128
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/dist/bin/rango.js +5 -1
- package/dist/vite/index.js +55 -40
- package/package.json +23 -19
- package/skills/observability/SKILL.md +39 -4
- package/skills/prerender/SKILL.md +30 -11
- package/skills/router-setup/SKILL.md +23 -3
- package/src/build/route-types/codegen.ts +12 -1
- package/src/cache/cache-scope.ts +20 -0
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +112 -0
- package/src/index.rsc.ts +19 -2
- package/src/index.ts +16 -1
- package/src/route-definition/dsl-helpers.ts +19 -0
- package/src/router/instrument.ts +440 -0
- package/src/router/loader-resolution.ts +15 -10
- package/src/router/match-middleware/cache-lookup.ts +9 -14
- package/src/router/match-middleware/cache-store.ts +12 -0
- package/src/router/middleware.ts +23 -2
- package/src/router/prerender-match.ts +5 -2
- package/src/router/router-context.ts +2 -1
- package/src/router/router-interfaces.ts +8 -0
- package/src/router/router-options.ts +58 -4
- package/src/router/segment-resolution/fresh.ts +15 -18
- package/src/router/segment-resolution/helpers.ts +6 -0
- package/src/router/segment-resolution/loader-cache.ts +5 -0
- package/src/router/segment-resolution/revalidation.ts +9 -18
- package/src/router/segment-wrappers.ts +3 -2
- package/src/router/telemetry-otel.ts +161 -179
- package/src/router/tracing.ts +203 -0
- package/src/router.ts +9 -0
- package/src/rsc/handler.ts +140 -134
- package/src/rsc/loader-fetch.ts +7 -1
- package/src/rsc/progressive-enhancement.ts +9 -2
- package/src/rsc/rsc-rendering.ts +38 -14
- package/src/rsc/server-action.ts +28 -7
- package/src/segment-system.tsx +4 -1
- package/src/server/request-context.ts +23 -5
- package/src/vite/discovery/prerender-collection.ts +26 -37
- package/src/vite/discovery/state.ts +6 -0
- package/src/vite/plugin-types.ts +25 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +10 -0
- package/src/vite/rango.ts +1 -0
- package/src/vite/router-discovery.ts +9 -3
- package/src/vite/utils/prerender-utils.ts +36 -0
|
@@ -17,6 +17,7 @@ import { setupBuildUse } from "./loader-resolution.js";
|
|
|
17
17
|
import { loadManifest } from "./manifest.js";
|
|
18
18
|
import { traverseBack } from "./pattern-matching.js";
|
|
19
19
|
import type { RouterContext } from "./router-context.js";
|
|
20
|
+
import type { ResolveSegmentOptions } from "./segment-resolution.js";
|
|
20
21
|
import { runWithRouterContext } from "./router-context.js";
|
|
21
22
|
import type { EntryData, InterceptEntry } from "../server/context";
|
|
22
23
|
import type {
|
|
@@ -40,7 +41,7 @@ export interface PrerenderMatchDeps<TEnv = any> {
|
|
|
40
41
|
params: Record<string, string>,
|
|
41
42
|
context: HandlerContext<any, TEnv>,
|
|
42
43
|
loaderPromises: Map<string, Promise<any>>,
|
|
43
|
-
options?:
|
|
44
|
+
options?: ResolveSegmentOptions,
|
|
44
45
|
) => Promise<ResolvedSegment[]>;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -257,7 +258,9 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
257
258
|
matchedParams,
|
|
258
259
|
buildCtx,
|
|
259
260
|
loaderPromises,
|
|
260
|
-
|
|
261
|
+
// throwOnError: a render failure (or `throw new Skip()`) must reach the
|
|
262
|
+
// build/dev caller, not be baked into a frozen error page (issue #587).
|
|
263
|
+
{ skipLoaders: true, throwOnError: true },
|
|
261
264
|
);
|
|
262
265
|
|
|
263
266
|
// 9. Detect passthrough sentinel: handler returned ctx.passthrough().
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "../types.js";
|
|
20
20
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
21
21
|
import type { TelemetrySink } from "./telemetry.js";
|
|
22
|
+
import type { ResolveSegmentOptions } from "./segment-resolution.js";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Revalidation context passed to segment resolution
|
|
@@ -195,7 +196,7 @@ export interface RouterContext<TEnv = any> {
|
|
|
195
196
|
params: Record<string, string>,
|
|
196
197
|
handlerContext: HandlerContext<any, TEnv>,
|
|
197
198
|
loaderPromises: Map<string, Promise<any>>,
|
|
198
|
-
options?:
|
|
199
|
+
options?: ResolveSegmentOptions,
|
|
199
200
|
) => Promise<ResolvedSegment[]>;
|
|
200
201
|
|
|
201
202
|
resolveAllSegmentsGenerator?: (
|
|
@@ -14,6 +14,7 @@ import { RSC_ROUTER_BRAND } from "./router-registry.js";
|
|
|
14
14
|
import type { RangoOptions, RootLayoutProps } from "./router-options.js";
|
|
15
15
|
import type { DefaultVars } from "../types/global-namespace.js";
|
|
16
16
|
import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
17
|
+
import type { ResolvedTracing } from "./tracing.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Options passed to router.fetch(), router.match(), and other request entrypoints.
|
|
@@ -320,6 +321,13 @@ export interface RangoInternal<
|
|
|
320
321
|
*/
|
|
321
322
|
readonly debugPerformance?: boolean;
|
|
322
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Resolved platform phase-span tracing (Cloudflare custom spans or OTel), or
|
|
326
|
+
* undefined when off. Threaded onto the request context and read at each
|
|
327
|
+
* traced phase.
|
|
328
|
+
*/
|
|
329
|
+
readonly tracing?: ResolvedTracing;
|
|
330
|
+
|
|
323
331
|
/**
|
|
324
332
|
* Whether ?__debug_manifest is allowed in production.
|
|
325
333
|
* Always enabled in development.
|
|
@@ -11,6 +11,7 @@ import type { UrlPatterns } from "../urls.js";
|
|
|
11
11
|
import type { UrlBuilder } from "../urls/pattern-types.js";
|
|
12
12
|
import type { NamedRouteEntry } from "./content-negotiation.js";
|
|
13
13
|
import type { TelemetrySink } from "./telemetry.js";
|
|
14
|
+
import type { RouterTracingConfig } from "./tracing.js";
|
|
14
15
|
import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -574,11 +575,14 @@ export interface RangoOptions<TEnv = any> {
|
|
|
574
575
|
onTimeout?: OnTimeoutCallback<TEnv>;
|
|
575
576
|
|
|
576
577
|
/**
|
|
577
|
-
* Telemetry sink for structured lifecycle
|
|
578
|
+
* Telemetry sink for structured, discrete lifecycle EVENTS: request
|
|
579
|
+
* start/end/error, loader start/end/error, handler errors, cache decisions,
|
|
580
|
+
* revalidation decisions, timeouts, origin rejections.
|
|
578
581
|
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
+
* This is the EVENT surface. Phase-duration SPANS (request/loader/render/ssr
|
|
583
|
+
* timing wired into a tracing backend) come from the separate `tracing`
|
|
584
|
+
* option below — a sink does not emit them, because async-context nesting
|
|
585
|
+
* cannot be faithfully reconstructed from after-the-fact start/end events.
|
|
582
586
|
*
|
|
583
587
|
* No-op when not configured (zero overhead).
|
|
584
588
|
*
|
|
@@ -591,6 +595,18 @@ export interface RangoOptions<TEnv = any> {
|
|
|
591
595
|
* });
|
|
592
596
|
* ```
|
|
593
597
|
*
|
|
598
|
+
* @example OpenTelemetry — pair the event sink with the tracing slot
|
|
599
|
+
* ```typescript
|
|
600
|
+
* import { createOTelTracing, createOTelSink } from "@rangojs/router";
|
|
601
|
+
* import { trace } from "@opentelemetry/api";
|
|
602
|
+
*
|
|
603
|
+
* const tracer = trace.getTracer("my-app");
|
|
604
|
+
* const router = createRouter({
|
|
605
|
+
* tracing: createOTelTracing(tracer), // phase spans
|
|
606
|
+
* telemetry: createOTelSink(tracer), // discrete-fact events
|
|
607
|
+
* });
|
|
608
|
+
* ```
|
|
609
|
+
*
|
|
594
610
|
* @example Custom sink
|
|
595
611
|
* ```typescript
|
|
596
612
|
* const router = createRouter({
|
|
@@ -604,6 +620,44 @@ export interface RangoOptions<TEnv = any> {
|
|
|
604
620
|
*/
|
|
605
621
|
telemetry?: TelemetrySink;
|
|
606
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Span tracing for the router's performance phases (request, middleware, action,
|
|
625
|
+
* loaders, render, ssr). Connects the same phases shown in the
|
|
626
|
+
* `debugPerformance` timeline to the host platform's tracing system. This is
|
|
627
|
+
* the SPAN surface (the `telemetry` option above is the event surface).
|
|
628
|
+
*
|
|
629
|
+
* Two factories produce a config, both for this slot:
|
|
630
|
+
* - `createOTelTracing(tracer)` from `@rangojs/router` — any platform with an
|
|
631
|
+
* OpenTelemetry SDK (including Node). Bridges the phases onto
|
|
632
|
+
* `tracer.startActiveSpan`.
|
|
633
|
+
* - `createCloudflareTracing()` from `@rangojs/router/cloudflare` — Cloudflare
|
|
634
|
+
* Workers native custom spans, alongside the automatic KV/D1/fetch spans.
|
|
635
|
+
*
|
|
636
|
+
* When tracing is unset — or off-platform (no OTel SDK / no Cloudflare tracing
|
|
637
|
+
* destination) — every span call falls through to the work directly, so the
|
|
638
|
+
* request behaves exactly as if tracing were off.
|
|
639
|
+
*
|
|
640
|
+
* @example OpenTelemetry
|
|
641
|
+
* ```typescript
|
|
642
|
+
* import { createOTelTracing } from "@rangojs/router";
|
|
643
|
+
* import { trace } from "@opentelemetry/api";
|
|
644
|
+
*
|
|
645
|
+
* const router = createRouter({
|
|
646
|
+
* tracing: createOTelTracing(trace.getTracer("my-app")),
|
|
647
|
+
* });
|
|
648
|
+
* ```
|
|
649
|
+
*
|
|
650
|
+
* @example Cloudflare
|
|
651
|
+
* ```typescript
|
|
652
|
+
* import { createCloudflareTracing } from "@rangojs/router/cloudflare";
|
|
653
|
+
*
|
|
654
|
+
* const router = createRouter({
|
|
655
|
+
* tracing: createCloudflareTracing({ spans: { ssr: false } }),
|
|
656
|
+
* });
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
tracing?: RouterTracingConfig;
|
|
660
|
+
|
|
607
661
|
/**
|
|
608
662
|
* SSR configuration options.
|
|
609
663
|
*
|
|
@@ -19,8 +19,6 @@ import type {
|
|
|
19
19
|
} from "../../types";
|
|
20
20
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
21
21
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
22
|
-
import { _getRequestContext } from "../../server/request-context.js";
|
|
23
|
-
import { appendMetric } from "../metrics.js";
|
|
24
22
|
import {
|
|
25
23
|
handleHandlerResult,
|
|
26
24
|
tryStaticHandler,
|
|
@@ -59,7 +57,6 @@ export async function resolveLoaders<TEnv>(
|
|
|
59
57
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
60
58
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
61
59
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
62
|
-
const ms = _getRequestContext()?._metricsStore;
|
|
63
60
|
|
|
64
61
|
if (!loadingDisabled) {
|
|
65
62
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
@@ -102,7 +99,6 @@ export async function resolveLoaders<TEnv>(
|
|
|
102
99
|
const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
|
|
103
100
|
const { loader } = loaderEntry;
|
|
104
101
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
105
|
-
const start = performance.now();
|
|
106
102
|
const wrapped = deps.wrapLoaderPromise(
|
|
107
103
|
runInsideLoaderScope(() =>
|
|
108
104
|
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
@@ -111,26 +107,17 @@ export async function resolveLoaders<TEnv>(
|
|
|
111
107
|
segmentId,
|
|
112
108
|
ctx.pathname,
|
|
113
109
|
);
|
|
114
|
-
return { wrapped,
|
|
110
|
+
return { wrapped, segmentId };
|
|
115
111
|
});
|
|
116
112
|
await Promise.all(pendingLoaderData.map((p) => p.wrapped));
|
|
117
113
|
|
|
118
114
|
return loaderEntries.map((loaderEntry, i) => {
|
|
119
115
|
const { loader } = loaderEntry;
|
|
120
116
|
const pending = pendingLoaderData[i]!;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const batchEnd = performance.now();
|
|
126
|
-
appendMetric(
|
|
127
|
-
ms,
|
|
128
|
-
`loader:${loader.$$id}`,
|
|
129
|
-
pending.start,
|
|
130
|
-
batchEnd - pending.start,
|
|
131
|
-
2,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
117
|
+
// The "loader:<id>" perf metric is recorded by observePhase at the single
|
|
118
|
+
// loader-metering site (useLoader, reached via ctx.use during
|
|
119
|
+
// resolveLoaderData), with the real per-loader duration rather than a
|
|
120
|
+
// Promise.all batch ceiling.
|
|
134
121
|
return {
|
|
135
122
|
id: pending.segmentId,
|
|
136
123
|
namespace: entry.id,
|
|
@@ -151,6 +138,15 @@ export async function resolveLoaders<TEnv>(
|
|
|
151
138
|
export interface ResolveSegmentOptions {
|
|
152
139
|
/** When true, skip resolveLoaders() calls (used for pre-rendering) */
|
|
153
140
|
skipLoaders?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* When true, a thrown render error is re-thrown instead of being converted
|
|
143
|
+
* into an error-boundary segment. Set only by the pre-render path so a
|
|
144
|
+
* build-time render failure (and a `throw new Skip()` inside a render fn)
|
|
145
|
+
* surfaces to the build instead of being silently baked into a frozen error
|
|
146
|
+
* page served as a 200 (issue #587). The live request path leaves this unset,
|
|
147
|
+
* so error boundaries keep catching at request time.
|
|
148
|
+
*/
|
|
149
|
+
throwOnError?: boolean;
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
/**
|
|
@@ -635,6 +631,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
635
631
|
deps,
|
|
636
632
|
{ request: safeRequest, url: context.url, routeKey, telemetry },
|
|
637
633
|
context.pathname,
|
|
634
|
+
options?.throwOnError,
|
|
638
635
|
);
|
|
639
636
|
doneEntry();
|
|
640
637
|
// Deduplicate by segment ID. include() scopes can produce entries that
|
|
@@ -284,11 +284,17 @@ export async function resolveWithErrorBoundary<TEnv, TResult>(
|
|
|
284
284
|
deps: SegmentResolutionDeps<TEnv>,
|
|
285
285
|
report?: ErrorReportContext,
|
|
286
286
|
pathname?: string,
|
|
287
|
+
throwOnError?: boolean,
|
|
287
288
|
): Promise<TResult> {
|
|
288
289
|
try {
|
|
289
290
|
return await resolveFn();
|
|
290
291
|
} catch (error) {
|
|
291
292
|
if (error instanceof Response) throw error;
|
|
293
|
+
// Pre-render surfaces render failures to the build instead of baking the
|
|
294
|
+
// error boundary as a frozen 200 (issue #587). A `throw new Skip()` in a
|
|
295
|
+
// render fn also propagates here so the build can skip that URL rather than
|
|
296
|
+
// bake its error page. The live request path leaves throwOnError unset.
|
|
297
|
+
if (throwOnError) throw error;
|
|
292
298
|
const segment = catchSegmentError(
|
|
293
299
|
error,
|
|
294
300
|
entry,
|
|
@@ -113,6 +113,11 @@ function getLoaderStore(
|
|
|
113
113
|
*
|
|
114
114
|
* When the LoaderEntry has no cache config, delegates directly to ctx.use(loader).
|
|
115
115
|
* When cached, checks store first and stores on miss via waitUntil.
|
|
116
|
+
*
|
|
117
|
+
* Loader metering is NOT done here — it lives at the ctx.use execution funnel
|
|
118
|
+
* (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
|
|
119
|
+
* so it emits no loader phase (the loader did not execute; the hit is only a
|
|
120
|
+
* LoaderCache debug log).
|
|
116
121
|
*/
|
|
117
122
|
export function resolveLoaderData<TEnv>(
|
|
118
123
|
loaderEntry: LoaderEntry,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "./helpers.js";
|
|
44
44
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
45
45
|
import { getRouterContext } from "../router-context.js";
|
|
46
|
-
import {
|
|
46
|
+
import { observeEvent } from "../instrument.js";
|
|
47
47
|
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
48
48
|
import {
|
|
49
49
|
track,
|
|
@@ -87,23 +87,14 @@ function emitRevalidationDecision(
|
|
|
87
87
|
routeKey: string,
|
|
88
88
|
shouldRevalidate: boolean,
|
|
89
89
|
): void {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
type: "revalidation.decision",
|
|
99
|
-
timestamp: performance.now(),
|
|
100
|
-
requestId: routerCtx.requestId,
|
|
101
|
-
segmentId,
|
|
102
|
-
pathname,
|
|
103
|
-
routeKey,
|
|
104
|
-
shouldRevalidate,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
90
|
+
observeEvent({
|
|
91
|
+
type: "revalidation.decision",
|
|
92
|
+
timestamp: performance.now(),
|
|
93
|
+
segmentId,
|
|
94
|
+
pathname,
|
|
95
|
+
routeKey,
|
|
96
|
+
shouldRevalidate,
|
|
97
|
+
});
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
// ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ShouldRevalidateFn,
|
|
6
6
|
} from "../types";
|
|
7
7
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
8
|
+
import type { ResolveSegmentOptions } from "./segment-resolution.js";
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
resolveAllSegments as _resolveAllSegments,
|
|
@@ -29,7 +30,7 @@ export interface SegmentWrappers<TEnv = any> {
|
|
|
29
30
|
params: Record<string, string>,
|
|
30
31
|
context: HandlerContext<any, TEnv>,
|
|
31
32
|
loaderPromises: Map<string, Promise<any>>,
|
|
32
|
-
options?:
|
|
33
|
+
options?: ResolveSegmentOptions,
|
|
33
34
|
) => Promise<ResolvedSegment[]>;
|
|
34
35
|
resolveLoadersOnly: (
|
|
35
36
|
entries: EntryData[],
|
|
@@ -123,7 +124,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
123
124
|
params: Record<string, string>,
|
|
124
125
|
context: HandlerContext<any, TEnv>,
|
|
125
126
|
loaderPromises: Map<string, Promise<any>>,
|
|
126
|
-
options?:
|
|
127
|
+
options?: ResolveSegmentOptions,
|
|
127
128
|
): ReturnType<typeof _resolveAllSegments> {
|
|
128
129
|
return _resolveAllSegments(
|
|
129
130
|
entries,
|