@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -45
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +2 -1
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +48 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +34 -0
- package/src/redirect-origin.ts +100 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +4 -14
- package/src/types/handler-context.ts +28 -9
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +2 -108
- package/src/vite/router-discovery.ts +9 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -125,10 +125,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
125
125
|
return {
|
|
126
126
|
emit(event: TelemetryEvent): void {
|
|
127
127
|
switch (event.type) {
|
|
128
|
-
// -----------------------------------------------------------------
|
|
129
|
-
// Request lifecycle
|
|
130
|
-
// -----------------------------------------------------------------
|
|
131
|
-
|
|
132
128
|
case "request.start": {
|
|
133
129
|
const span = tracer.startSpan("rango.request", {
|
|
134
130
|
attributes: {
|
|
@@ -169,10 +165,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
169
165
|
break;
|
|
170
166
|
}
|
|
171
167
|
|
|
172
|
-
// -----------------------------------------------------------------
|
|
173
|
-
// Loader lifecycle
|
|
174
|
-
// -----------------------------------------------------------------
|
|
175
|
-
|
|
176
168
|
case "loader.start": {
|
|
177
169
|
const span = tracer.startSpan("rango.loader", {
|
|
178
170
|
attributes: {
|
|
@@ -231,10 +223,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
231
223
|
break;
|
|
232
224
|
}
|
|
233
225
|
|
|
234
|
-
// -----------------------------------------------------------------
|
|
235
|
-
// Handler errors (instant span)
|
|
236
|
-
// -----------------------------------------------------------------
|
|
237
|
-
|
|
238
226
|
case "handler.error": {
|
|
239
227
|
const attrs: Record<string, string | number | boolean> = {
|
|
240
228
|
"rango.handled_by_boundary": event.handledByBoundary,
|
|
@@ -257,10 +245,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
257
245
|
break;
|
|
258
246
|
}
|
|
259
247
|
|
|
260
|
-
// -----------------------------------------------------------------
|
|
261
|
-
// Cache decision (instant span)
|
|
262
|
-
// -----------------------------------------------------------------
|
|
263
|
-
|
|
264
248
|
case "cache.decision": {
|
|
265
249
|
const attrs: Record<string, string | number | boolean> = {
|
|
266
250
|
"http.route": event.pathname,
|
|
@@ -277,10 +261,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
277
261
|
break;
|
|
278
262
|
}
|
|
279
263
|
|
|
280
|
-
// -----------------------------------------------------------------
|
|
281
|
-
// Revalidation decision (instant span)
|
|
282
|
-
// -----------------------------------------------------------------
|
|
283
|
-
|
|
284
264
|
case "revalidation.decision": {
|
|
285
265
|
const span = tracer.startSpan("rango.revalidation.decision", {
|
|
286
266
|
attributes: {
|
package/src/router/telemetry.ts
CHANGED
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
* - revalidation.decision (revalidation evaluation)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Event types
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
17
|
interface BaseEvent {
|
|
22
18
|
/** Monotonic timestamp from performance.now() */
|
|
23
19
|
timestamp: number;
|
|
@@ -239,10 +235,6 @@ export function formatCacheSignalHeader(
|
|
|
239
235
|
return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
|
|
240
236
|
}
|
|
241
237
|
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
// Sink interface
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
|
|
246
238
|
/**
|
|
247
239
|
* Telemetry sink receives structured lifecycle events from the router.
|
|
248
240
|
* Implement this interface to integrate with any observability backend.
|
|
@@ -253,10 +245,6 @@ export interface TelemetrySink {
|
|
|
253
245
|
emit(event: TelemetryEvent): void;
|
|
254
246
|
}
|
|
255
247
|
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// No-op singleton (zero-cost disabled state)
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
|
|
260
248
|
const noopSink: TelemetrySink = {
|
|
261
249
|
emit() {},
|
|
262
250
|
};
|
|
@@ -284,12 +272,6 @@ export function safeEmit(sink: TelemetrySink, event: TelemetryEvent): void {
|
|
|
284
272
|
}
|
|
285
273
|
}
|
|
286
274
|
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
// Request ID extraction (for span correlation)
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
// Per-request memoization so the same Request object always maps to the
|
|
292
|
-
// same ID. WeakMap allows GC when the Request is no longer referenced.
|
|
293
275
|
const requestIds = new WeakMap<Request, string>();
|
|
294
276
|
let telemetryRequestCounter = 0;
|
|
295
277
|
|
|
@@ -323,10 +305,6 @@ export function getRequestId(request: Request): string {
|
|
|
323
305
|
return id;
|
|
324
306
|
}
|
|
325
307
|
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
// Console sink (built-in, replaces ad-hoc console.log debug traces)
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
|
|
330
308
|
/**
|
|
331
309
|
* Built-in console sink that logs events in a structured format.
|
|
332
310
|
* Designed as the default sink for development / debugging.
|
package/src/router/timeout.ts
CHANGED
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
* a Promise.race mechanism, returning 504 on expiry.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Public types
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
9
|
export interface RouterTimeouts {
|
|
14
10
|
/** Timeout for server action execution (ms). */
|
|
15
11
|
actionMs?: number;
|
|
@@ -35,10 +31,6 @@ export type OnTimeoutCallback<TEnv = any> = (
|
|
|
35
31
|
ctx: TimeoutContext<TEnv>,
|
|
36
32
|
) => Response | Promise<Response>;
|
|
37
33
|
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Internal resolved form
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
34
|
export interface ResolvedTimeouts {
|
|
43
35
|
actionMs: number | undefined;
|
|
44
36
|
renderStartMs: number | undefined;
|
|
@@ -63,10 +55,6 @@ export function resolveTimeouts(
|
|
|
63
55
|
};
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Error class
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
58
|
export class RouterTimeoutError extends Error {
|
|
71
59
|
override name = "RouterTimeoutError" as const;
|
|
72
60
|
phase: TimeoutPhase;
|
|
@@ -81,10 +69,6 @@ export class RouterTimeoutError extends Error {
|
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Race helper
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
72
|
type TimeoutResult<T> =
|
|
89
73
|
| { result: T; timedOut: false }
|
|
90
74
|
| { timedOut: true; durationMs: number };
|
|
@@ -129,10 +113,6 @@ export async function withTimeout<T>(
|
|
|
129
113
|
}
|
|
130
114
|
}
|
|
131
115
|
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
// Default response
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
|
|
136
116
|
/**
|
|
137
117
|
* Create the default 504 response for a timed-out request.
|
|
138
118
|
* Includes `X-Rango-Timeout-Phase` header for observability.
|
|
@@ -15,10 +15,6 @@ export interface TrieMatchResult {
|
|
|
15
15
|
sp: string;
|
|
16
16
|
/** Matched route params */
|
|
17
17
|
params: Record<string, string>;
|
|
18
|
-
/** Optional param names declared on the route. Absent params are omitted
|
|
19
|
-
* from `params` (read as `undefined`), matching the
|
|
20
|
-
* `ExtractParams<"/:locale?/...">` type. */
|
|
21
|
-
optionalParams?: string[];
|
|
22
18
|
/** Redirect target if trailing slash requires it */
|
|
23
19
|
redirectTo?: string;
|
|
24
20
|
/** Route has pre-rendered data available */
|
|
@@ -43,14 +39,12 @@ export function tryTrieMatch(
|
|
|
43
39
|
): TrieMatchResult | null {
|
|
44
40
|
if (!trie) return null;
|
|
45
41
|
|
|
46
|
-
// Split pathname into segments, filtering empty strings from leading/trailing slashes
|
|
47
42
|
const pathnameHasTrailingSlash =
|
|
48
43
|
pathname.length > 1 && pathname.endsWith("/");
|
|
49
44
|
const normalizedPath = pathnameHasTrailingSlash
|
|
50
45
|
? pathname.slice(0, -1)
|
|
51
46
|
: pathname;
|
|
52
47
|
|
|
53
|
-
// Handle root path
|
|
54
48
|
if (normalizedPath === "" || normalizedPath === "/") {
|
|
55
49
|
if (trie.r) {
|
|
56
50
|
return validateAndBuild(
|
|
@@ -77,10 +71,8 @@ export function tryTrieMatch(
|
|
|
77
71
|
return null;
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
// Remove leading slash and split
|
|
81
74
|
const segments = normalizedPath.slice(1).split("/");
|
|
82
75
|
|
|
83
|
-
// Try exact match with normalized path (no trailing slash)
|
|
84
76
|
const result = walkTrie(trie, segments, 0, []);
|
|
85
77
|
if (result) {
|
|
86
78
|
return validateAndBuild(
|
|
@@ -102,8 +94,58 @@ interface WalkResult {
|
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
97
|
+
* Check a leaf's constraints (leaf.cv) against already-resolved named params.
|
|
98
|
+
* Empty/undefined values are exempt (optional params that were not bound).
|
|
99
|
+
*/
|
|
100
|
+
function constraintsSatisfied(
|
|
101
|
+
leaf: TrieLeaf,
|
|
102
|
+
params: Record<string, string>,
|
|
103
|
+
): boolean {
|
|
104
|
+
if (!leaf.cv) return true;
|
|
105
|
+
for (const paramName in leaf.cv) {
|
|
106
|
+
const allowed = leaf.cv[paramName]!;
|
|
107
|
+
const value = params[paramName];
|
|
108
|
+
if (value !== undefined && value !== "" && !allowed.includes(value)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Constraint check for a candidate terminal DURING the walk. Builds the named
|
|
117
|
+
* params from positional walk values (decoded the same way validateAndBuild
|
|
118
|
+
* does) and validates leaf.cv. Returning false lets walkTrie unwind to a
|
|
119
|
+
* lower-priority sibling instead of committing to a leaf that would only be
|
|
120
|
+
* rejected post-walk — that post-walk rejection is what forced the regex
|
|
121
|
+
* fallback (and its false "trie gap" R3 warning) for perfectly valid configs.
|
|
122
|
+
*/
|
|
123
|
+
function leafConstraintsPass(
|
|
124
|
+
leaf: TrieLeaf,
|
|
125
|
+
paramValues: string[],
|
|
126
|
+
wildcardValue: string | undefined,
|
|
127
|
+
): boolean {
|
|
128
|
+
if (!leaf.cv) return true;
|
|
129
|
+
const params: Record<string, string> = {};
|
|
130
|
+
if (leaf.pa) {
|
|
131
|
+
for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
|
|
132
|
+
params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (wildcardValue !== undefined && "pn" in leaf) {
|
|
136
|
+
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
137
|
+
safeDecodeURIComponent(wildcardValue);
|
|
138
|
+
}
|
|
139
|
+
return constraintsSatisfied(leaf, params);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Walk the trie by segments with priority: static > suffix-param > param >
|
|
144
|
+
* wildcard (Priority 1-4 below; matches the canonical M4 ordering in
|
|
145
|
+
* docs/internal/matching-and-lazy-discovery.md).
|
|
146
|
+
* Uses backtracking to try all possible matches. Per-leaf constraints are
|
|
147
|
+
* enforced at each candidate terminal so a constraint miss backtracks to a
|
|
148
|
+
* lower-priority sibling rather than aborting the whole match.
|
|
107
149
|
*/
|
|
108
150
|
function walkTrie(
|
|
109
151
|
node: TrieNode,
|
|
@@ -111,9 +153,8 @@ function walkTrie(
|
|
|
111
153
|
index: number,
|
|
112
154
|
paramValues: string[],
|
|
113
155
|
): WalkResult | null {
|
|
114
|
-
// All segments consumed: check for terminal
|
|
115
156
|
if (index === segments.length) {
|
|
116
|
-
if (node.r) {
|
|
157
|
+
if (node.r && leafConstraintsPass(node.r, paramValues, undefined)) {
|
|
117
158
|
return { leaf: node.r, paramValues: [...paramValues] };
|
|
118
159
|
}
|
|
119
160
|
// A wildcard at this node matches the bare prefix with an empty remainder
|
|
@@ -122,7 +163,7 @@ function walkTrie(
|
|
|
122
163
|
// so without this a request to the wildcard's own prefix misses the trie
|
|
123
164
|
// and the regex fallback emits a corrupt redirect. A static terminal
|
|
124
165
|
// (node.r) still wins.
|
|
125
|
-
if (node.w) {
|
|
166
|
+
if (node.w && leafConstraintsPass(node.w, paramValues, "")) {
|
|
126
167
|
return { leaf: node.w, paramValues: [...paramValues], wildcardValue: "" };
|
|
127
168
|
}
|
|
128
169
|
return null;
|
|
@@ -131,14 +172,15 @@ function walkTrie(
|
|
|
131
172
|
const segment = segments[index];
|
|
132
173
|
const staticChild = node.s?.[segment];
|
|
133
174
|
|
|
134
|
-
// Priority 1: Static match
|
|
135
175
|
if (staticChild) {
|
|
136
176
|
const result = walkTrie(staticChild, segments, index + 1, paramValues);
|
|
137
177
|
if (result) return result;
|
|
138
178
|
}
|
|
139
179
|
|
|
140
|
-
// Priority 2: Suffix-param match (e.g., :productId.html)
|
|
141
180
|
if (node.xp) {
|
|
181
|
+
// node.xp keys are pre-sorted longest-suffix-first at build time
|
|
182
|
+
// (route-trie.ts sortSuffixParams), so the first match is the most specific
|
|
183
|
+
// suffix: `/app.min.js` matches `:file.min.js` before `:file.js`.
|
|
142
184
|
for (const suffix in node.xp) {
|
|
143
185
|
if (segment.endsWith(suffix) && segment.length > suffix.length) {
|
|
144
186
|
const paramValue = segment.slice(0, -suffix.length);
|
|
@@ -155,7 +197,6 @@ function walkTrie(
|
|
|
155
197
|
}
|
|
156
198
|
}
|
|
157
199
|
|
|
158
|
-
// Priority 3: Param match
|
|
159
200
|
if (node.p) {
|
|
160
201
|
paramValues.push(segment);
|
|
161
202
|
const result = walkTrie(node.p.c, segments, index + 1, paramValues);
|
|
@@ -163,14 +204,15 @@ function walkTrie(
|
|
|
163
204
|
if (result) return result;
|
|
164
205
|
}
|
|
165
206
|
|
|
166
|
-
// Priority 4: Wildcard match (consumes rest)
|
|
167
207
|
if (node.w) {
|
|
168
208
|
const rest = joinRemainingSegments(segments, index);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
209
|
+
if (leafConstraintsPass(node.w, paramValues, rest)) {
|
|
210
|
+
return {
|
|
211
|
+
leaf: node.w,
|
|
212
|
+
paramValues: [...paramValues],
|
|
213
|
+
wildcardValue: rest,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
174
216
|
}
|
|
175
217
|
|
|
176
218
|
return null;
|
|
@@ -196,10 +238,6 @@ function validateAndBuild(
|
|
|
196
238
|
originalPathname: string,
|
|
197
239
|
pathnameHasTrailingSlash: boolean,
|
|
198
240
|
): TrieMatchResult | null {
|
|
199
|
-
// Build named params by zipping leaf.pa with positional paramValues.
|
|
200
|
-
// Params are URL-decoded at this boundary so ctx.params holds the values
|
|
201
|
-
// apps expect (matching Express/React Router) and round-trip cleanly
|
|
202
|
-
// through ctx.reverse.
|
|
203
241
|
const params: Record<string, string> = {};
|
|
204
242
|
if (leaf.pa) {
|
|
205
243
|
for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
|
|
@@ -207,31 +245,15 @@ function validateAndBuild(
|
|
|
207
245
|
}
|
|
208
246
|
}
|
|
209
247
|
|
|
210
|
-
// Add wildcard param (wildcard leaves have pn from TrieNode.w type)
|
|
211
248
|
if (wildcardValue !== undefined && "pn" in leaf) {
|
|
212
249
|
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
213
250
|
safeDecodeURIComponent(wildcardValue);
|
|
214
251
|
}
|
|
215
252
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (leaf.cv) {
|
|
219
|
-
for (const paramName in leaf.cv) {
|
|
220
|
-
const allowed = leaf.cv[paramName]!;
|
|
221
|
-
const value = params[paramName];
|
|
222
|
-
if (value !== undefined && value !== "" && !allowed.includes(value)) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
253
|
+
if (!constraintsSatisfied(leaf, params)) {
|
|
254
|
+
return null;
|
|
226
255
|
}
|
|
227
256
|
|
|
228
|
-
// Optional params that weren't matched are left absent from `params` so
|
|
229
|
-
// `ctx.params.locale` reads as `undefined`, matching the
|
|
230
|
-
// `ExtractParams<"/:locale?/...">` type (`{ locale?: string }`). Both
|
|
231
|
-
// internal consumers — the constraint check above and `reverse()` —
|
|
232
|
-
// already treat missing/undefined as the absent form.
|
|
233
|
-
|
|
234
|
-
// Trailing slash handling
|
|
235
257
|
const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
|
|
236
258
|
let redirectTo: string | undefined;
|
|
237
259
|
|
|
@@ -251,7 +273,6 @@ function validateAndBuild(
|
|
|
251
273
|
params,
|
|
252
274
|
};
|
|
253
275
|
|
|
254
|
-
if (leaf.op) result.optionalParams = leaf.op;
|
|
255
276
|
if (redirectTo) result.redirectTo = redirectTo;
|
|
256
277
|
if (leaf.pr) result.pr = true;
|
|
257
278
|
if (leaf.pt) result.pt = true;
|
package/src/router/types.ts
CHANGED
|
@@ -22,27 +22,11 @@ import type {
|
|
|
22
22
|
ShouldRevalidateFn,
|
|
23
23
|
} from "../types";
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* Result of resolving loaders with revalidation
|
|
27
|
-
* Contains both segments to render and all matched segment IDs
|
|
28
|
-
*/
|
|
29
|
-
export interface LoaderRevalidationResult {
|
|
30
|
-
segments: ResolvedSegment[];
|
|
31
|
-
matchedIds: string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Result of resolving segments with revalidation
|
|
36
|
-
* Contains both segments to render and all matched segment IDs
|
|
37
|
-
*/
|
|
38
25
|
export interface SegmentRevalidationResult {
|
|
39
26
|
segments: ResolvedSegment[];
|
|
40
27
|
matchedIds: string[];
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
/**
|
|
44
|
-
* Action context type for revalidation
|
|
45
|
-
*/
|
|
46
30
|
export type ActionContext = {
|
|
47
31
|
actionId?: string;
|
|
48
32
|
actionUrl?: URL;
|
|
@@ -50,23 +34,6 @@ export type ActionContext = {
|
|
|
50
34
|
formData?: FormData;
|
|
51
35
|
};
|
|
52
36
|
|
|
53
|
-
/**
|
|
54
|
-
* Dependencies passed to segment resolution functions
|
|
55
|
-
* These are created within createRouter and passed to extracted utilities
|
|
56
|
-
*/
|
|
57
|
-
export interface RouterDependencies<TEnv> {
|
|
58
|
-
findNearestErrorBoundary: (
|
|
59
|
-
entry: EntryData | null,
|
|
60
|
-
) => ReactNode | ErrorBoundaryHandler | null;
|
|
61
|
-
findNearestNotFoundBoundary: (
|
|
62
|
-
entry: EntryData | null,
|
|
63
|
-
) => ReactNode | NotFoundBoundaryHandler | null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Dependencies injected from createRouter closure into extracted segment resolution functions.
|
|
68
|
-
* These are the closure-bound helpers that cannot be imported directly.
|
|
69
|
-
*/
|
|
70
37
|
export interface SegmentResolutionDeps<TEnv = any> {
|
|
71
38
|
wrapLoaderPromise: <T>(
|
|
72
39
|
promise: Promise<T>,
|
|
@@ -108,21 +75,6 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
108
75
|
viewTransitionDefault?: "auto" | false;
|
|
109
76
|
}
|
|
110
77
|
|
|
111
|
-
/**
|
|
112
|
-
* Dependencies injected from createRouter closure into extracted intercept resolution functions.
|
|
113
|
-
*/
|
|
114
|
-
export interface InterceptResolutionDeps<TEnv = any> {
|
|
115
|
-
wrapLoaderPromise: SegmentResolutionDeps<TEnv>["wrapLoaderPromise"];
|
|
116
|
-
evaluateInterceptWhen: (
|
|
117
|
-
intercept: InterceptEntry,
|
|
118
|
-
selectorContext: InterceptSelectorContext | null,
|
|
119
|
-
isAction: boolean,
|
|
120
|
-
) => boolean;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Dependencies injected from createRouter closure into extracted match API functions.
|
|
125
|
-
*/
|
|
126
78
|
export interface MatchApiDeps<TEnv = any> {
|
|
127
79
|
findMatch: (pathname: string, ms?: any) => any;
|
|
128
80
|
getMetricsStore: () => any;
|
|
@@ -137,23 +89,13 @@ export interface MatchApiDeps<TEnv = any> {
|
|
|
137
89
|
getRouteMap: () => Record<string, string>;
|
|
138
90
|
}
|
|
139
91
|
|
|
140
|
-
/**
|
|
141
|
-
* Title descriptor types for template support
|
|
142
|
-
*/
|
|
143
92
|
export type TitleDescriptor =
|
|
144
93
|
| string
|
|
145
94
|
| { template: string; default: string } // For layouts - template applied to child titles
|
|
146
|
-
| { absolute: string };
|
|
95
|
+
| { absolute: string };
|
|
147
96
|
|
|
148
|
-
/**
|
|
149
|
-
* Unset descriptor to remove inherited meta
|
|
150
|
-
* Key format matches getMetaKey output: "title", "name:description", "property:og:image"
|
|
151
|
-
*/
|
|
152
97
|
export type UnsetDescriptor = { unset: string };
|
|
153
98
|
|
|
154
|
-
/**
|
|
155
|
-
* Base meta descriptor types (sync values)
|
|
156
|
-
*/
|
|
157
99
|
export type MetaDescriptorBase =
|
|
158
100
|
| { charSet: "utf-8" }
|
|
159
101
|
| { title: TitleDescriptor }
|
|
@@ -165,10 +107,6 @@ export type MetaDescriptorBase =
|
|
|
165
107
|
| UnsetDescriptor
|
|
166
108
|
| { [name: string]: unknown };
|
|
167
109
|
|
|
168
|
-
/**
|
|
169
|
-
* Meta descriptor that can be sync or async.
|
|
170
|
-
* Use Promise<MetaDescriptorBase> for streaming meta that resolves after initial render.
|
|
171
|
-
*/
|
|
172
110
|
export type MetaDescriptor = MetaDescriptorBase | Promise<MetaDescriptorBase>;
|
|
173
111
|
|
|
174
112
|
type LdJsonObject = { [Key in string]: LdJsonValue } & {
|
package/src/router/url-params.ts
CHANGED
|
@@ -25,11 +25,6 @@ export function safeDecodeURIComponent(raw: string): string {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// encodeURIComponent over-encodes for path segments. After running it,
|
|
29
|
-
// un-encode the pchar sub-delims + (`:` / `@`) so the resulting URL
|
|
30
|
-
// keeps human-readable characters that are legal in a path segment.
|
|
31
|
-
// Everything dangerous — `/ ? # %` and space/control/non-ASCII — stays
|
|
32
|
-
// encoded.
|
|
33
28
|
const PATH_SAFE_ESCAPES: Record<string, string> = {
|
|
34
29
|
"%3A": ":",
|
|
35
30
|
"%40": "@",
|
package/src/router.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { createCacheScope } from "./cache/cache-scope.js";
|
|
3
|
-
import {
|
|
4
|
-
setCacheProfiles,
|
|
5
|
-
resolveCacheProfiles,
|
|
6
|
-
} from "./cache/profile-registry.js";
|
|
3
|
+
import { resolveCacheProfiles } from "./cache/profile-registry.js";
|
|
7
4
|
import { isCachedFunction } from "./cache/taint.js";
|
|
8
5
|
import { assertClientComponent } from "./component-utils.js";
|
|
9
6
|
import { DefaultDocument } from "./components/DefaultDocument.js";
|
|
@@ -181,11 +178,10 @@ export function createRouter<TEnv = any>(
|
|
|
181
178
|
// Resolve telemetry sink (no-op when not configured)
|
|
182
179
|
const telemetry = resolveSink(telemetrySink);
|
|
183
180
|
|
|
184
|
-
// Resolve cache profiles: merge user config with guaranteed default
|
|
185
|
-
// This resolved map is
|
|
186
|
-
//
|
|
181
|
+
// Resolve cache profiles: merge user config with the guaranteed default
|
|
182
|
+
// profile. This resolved map is threaded onto each request context; the
|
|
183
|
+
// "use cache: <profile>" runtime path reads it request-scoped.
|
|
187
184
|
const resolvedCacheProfiles = resolveCacheProfiles(cacheProfilesOption);
|
|
188
|
-
setCacheProfiles(resolvedCacheProfiles);
|
|
189
185
|
|
|
190
186
|
// Source file: prefer Vite-injected path (zero cost), fall back to
|
|
191
187
|
// stack trace parsing for non-Vite environments (e.g. tests).
|
|
@@ -355,7 +351,6 @@ export function createRouter<TEnv = any>(
|
|
|
355
351
|
regex,
|
|
356
352
|
paramNames,
|
|
357
353
|
handler,
|
|
358
|
-
mountPrefix,
|
|
359
354
|
});
|
|
360
355
|
}
|
|
361
356
|
|
|
@@ -1058,8 +1053,10 @@ export function createRouter<TEnv = any>(
|
|
|
1058
1053
|
if (!handler) {
|
|
1059
1054
|
// Lazy import deferred to first request to avoid dev mode issues
|
|
1060
1055
|
const { createRSCHandler } = await import("./rsc/handler.js");
|
|
1061
|
-
// Cast:
|
|
1062
|
-
//
|
|
1056
|
+
// Cast: createRSCHandler receives `router as any`, which erases TEnv
|
|
1057
|
+
// and infers its handler as RouterRequestInput<unknown>. Re-narrow the
|
|
1058
|
+
// returned handler to RouterRequestInput<TEnv> so the call below stays
|
|
1059
|
+
// typed. (The handler already accepts (request, RouterRequestInput).)
|
|
1063
1060
|
handler = createRSCHandler({
|
|
1064
1061
|
router: router as any,
|
|
1065
1062
|
cache,
|
package/src/rsc/handler.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
interceptRedirectForPartial,
|
|
32
32
|
buildRouteMiddlewareEntries,
|
|
33
33
|
} from "./helpers.js";
|
|
34
|
+
import { guardOutgoingRedirect } from "./redirect-guard.js";
|
|
34
35
|
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
35
36
|
import {
|
|
36
37
|
handleResponseRoute,
|
|
@@ -292,12 +293,13 @@ export function createRSCHandler<
|
|
|
292
293
|
function createRedirectFlightResponse(
|
|
293
294
|
redirectUrl: string,
|
|
294
295
|
locationState?: Record<string, unknown>,
|
|
296
|
+
external?: boolean,
|
|
295
297
|
): Response {
|
|
296
298
|
const redirectPayload: RscPayload = {
|
|
297
299
|
metadata: {
|
|
298
300
|
pathname: redirectUrl,
|
|
299
301
|
segments: [],
|
|
300
|
-
redirect: { url: redirectUrl },
|
|
302
|
+
redirect: { url: redirectUrl, ...(external && { external: true }) },
|
|
301
303
|
...(locationState && { locationState }),
|
|
302
304
|
},
|
|
303
305
|
};
|
|
@@ -570,7 +572,12 @@ export function createRSCHandler<
|
|
|
570
572
|
response.headers.set("Server-Timing", fullTiming);
|
|
571
573
|
}
|
|
572
574
|
|
|
573
|
-
|
|
575
|
+
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
576
|
+
// middleware short-circuit, response-route) funnels through here, so
|
|
577
|
+
// guarding browser-followed (3xx) redirects once covers them all and any
|
|
578
|
+
// future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
|
|
579
|
+
// through untouched (validated client-side instead).
|
|
580
|
+
return guardOutgoingRedirect(response, url.origin, router.basename);
|
|
574
581
|
});
|
|
575
582
|
};
|
|
576
583
|
|
|
@@ -1017,10 +1024,19 @@ export function createRSCHandler<
|
|
|
1017
1024
|
} catch (error) {
|
|
1018
1025
|
// Check if middleware/handler returned Response
|
|
1019
1026
|
if (error instanceof Response) {
|
|
1027
|
+
// An action revalidation render is delivered to the client over the
|
|
1028
|
+
// same Flight-parsing path as a partial navigation, so a Response
|
|
1029
|
+
// thrown during it must be converted exactly like a partial one
|
|
1030
|
+
// (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
|
|
1031
|
+
// the no-middleware path returns the raw Response (the with-middleware
|
|
1032
|
+
// path is already covered by the isPartial || actionContinuation
|
|
1033
|
+
// guard below).
|
|
1034
|
+
const treatAsPartial = isPartial || actionContinuation != null;
|
|
1035
|
+
|
|
1020
1036
|
// During partial (client-side navigation), a 200 Response from a handler
|
|
1021
1037
|
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1022
1038
|
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1023
|
-
if (
|
|
1039
|
+
if (treatAsPartial && error.status === 200) {
|
|
1024
1040
|
console.warn(
|
|
1025
1041
|
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1026
1042
|
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
@@ -1034,7 +1050,7 @@ export function createRSCHandler<
|
|
|
1034
1050
|
});
|
|
1035
1051
|
}
|
|
1036
1052
|
|
|
1037
|
-
if (
|
|
1053
|
+
if (treatAsPartial) {
|
|
1038
1054
|
const intercepted = interceptRedirectForPartial(
|
|
1039
1055
|
error,
|
|
1040
1056
|
createRedirectFlightResponse,
|