@rangojs/router 0.0.0-experimental.83 → 0.0.0-experimental.8332dbe4
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 +112 -17
- package/dist/vite/index.js +1197 -454
- package/package.json +4 -2
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +30 -2
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +25 -0
- package/skills/layout/SKILL.md +2 -0
- package/skills/links/SKILL.md +234 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/migrate-react-router/SKILL.md +4 -0
- package/skills/parallel/SKILL.md +9 -0
- package/skills/rango/SKILL.md +2 -0
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +24 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +9 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +113 -6
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +44 -10
- package/src/browser/prefetch/cache.ts +16 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/NavigationProvider.tsx +64 -16
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/use-params.ts +8 -5
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +5 -1
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -39
- package/src/route-definition/dsl-helpers.ts +16 -3
- package/src/route-definition/helpers-types.ts +6 -1
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/router/handler-context.ts +21 -41
- package/src/router/lazy-includes.ts +1 -1
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-result.ts +21 -2
- package/src/router/middleware-types.ts +14 -25
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +8 -0
- package/src/router/segment-resolution/revalidation.ts +128 -100
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +11 -10
- package/src/rsc/rsc-rendering.ts +3 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-system.tsx +60 -9
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +17 -0
- package/src/urls/response-types.ts +2 -10
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +48 -1
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +516 -486
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +498 -52
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TrieNode, TrieLeaf } from "../build/route-trie.js";
|
|
9
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
9
10
|
|
|
10
11
|
export interface TrieMatchResult {
|
|
11
12
|
/** Route name */
|
|
@@ -14,7 +15,9 @@ export interface TrieMatchResult {
|
|
|
14
15
|
sp: string;
|
|
15
16
|
/** Matched route params */
|
|
16
17
|
params: Record<string, string>;
|
|
17
|
-
/** Optional param names
|
|
18
|
+
/** Optional param names declared on the route. Absent params are omitted
|
|
19
|
+
* from `params` (read as `undefined`), matching the
|
|
20
|
+
* `ExtractParams<"/:locale?/...">` type. */
|
|
18
21
|
optionalParams?: string[];
|
|
19
22
|
/** Ancestry shortCodes for layout pruning */
|
|
20
23
|
ancestry: string[];
|
|
@@ -173,20 +176,25 @@ function validateAndBuild(
|
|
|
173
176
|
originalPathname: string,
|
|
174
177
|
pathnameHasTrailingSlash: boolean,
|
|
175
178
|
): TrieMatchResult | null {
|
|
176
|
-
// Build named params by zipping leaf.pa with positional paramValues
|
|
179
|
+
// Build named params by zipping leaf.pa with positional paramValues.
|
|
180
|
+
// Params are URL-decoded at this boundary so ctx.params holds the values
|
|
181
|
+
// apps expect (matching Express/React Router) and round-trip cleanly
|
|
182
|
+
// through ctx.reverse.
|
|
177
183
|
const params: Record<string, string> = {};
|
|
178
184
|
if (leaf.pa) {
|
|
179
185
|
for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
|
|
180
|
-
params[leaf.pa[i]] = paramValues[i];
|
|
186
|
+
params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
// Add wildcard param (wildcard leaves have pn from TrieNode.w type)
|
|
185
191
|
if (wildcardValue !== undefined && "pn" in leaf) {
|
|
186
|
-
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
192
|
+
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
193
|
+
safeDecodeURIComponent(wildcardValue);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
// Validate constraints
|
|
196
|
+
// Validate constraints against decoded values so constraint lists can be
|
|
197
|
+
// written in decoded form (e.g. ["en-GB", "en US"]).
|
|
190
198
|
if (leaf.cv) {
|
|
191
199
|
for (const paramName in leaf.cv) {
|
|
192
200
|
const allowed = leaf.cv[paramName]!;
|
|
@@ -197,14 +205,11 @@ function validateAndBuild(
|
|
|
197
205
|
}
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
+
// Optional params that weren't matched are left absent from `params` so
|
|
209
|
+
// `ctx.params.locale` reads as `undefined`, matching the
|
|
210
|
+
// `ExtractParams<"/:locale?/...">` type (`{ locale?: string }`). Both
|
|
211
|
+
// internal consumers — the constraint check above and `reverse()` —
|
|
212
|
+
// already treat missing/undefined as the absent form.
|
|
208
213
|
|
|
209
214
|
// Trailing slash handling
|
|
210
215
|
const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL param encode/decode at the route boundary.
|
|
3
|
+
*
|
|
4
|
+
* Extraction (decode): regex/trie matchers keep param values URL-encoded;
|
|
5
|
+
* `safeDecodeURIComponent` turns them back into raw strings so `ctx.params`
|
|
6
|
+
* matches the contract apps expect (Express/React Router/Fastify/Koa) and
|
|
7
|
+
* round-trips through reverse stay stable. Malformed %-encoding is
|
|
8
|
+
* preserved as-is so a broken URL doesn't crash matching.
|
|
9
|
+
*
|
|
10
|
+
* Reversal (encode): `encodePathSegment` escapes only what RFC 3986
|
|
11
|
+
* requires for a path segment — `/`, `?`, `#`, space, control chars,
|
|
12
|
+
* non-ASCII — and leaves pchar sub-delims (`@ : $ & + , ; =` and friends)
|
|
13
|
+
* readable. `encodeURIComponent` over-encodes for path segments, which
|
|
14
|
+
* makes generated URLs harder for humans to read in the address bar
|
|
15
|
+
* (e.g. mailbox IDs like `ivo@example.com` would become
|
|
16
|
+
* `ivo%40example.com` even though `@` is path-legal).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export function safeDecodeURIComponent(raw: string): string {
|
|
20
|
+
if (raw === "" || raw.indexOf("%") === -1) return raw;
|
|
21
|
+
try {
|
|
22
|
+
return decodeURIComponent(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
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
|
+
const PATH_SAFE_ESCAPES: Record<string, string> = {
|
|
34
|
+
"%3A": ":",
|
|
35
|
+
"%40": "@",
|
|
36
|
+
"%24": "$",
|
|
37
|
+
"%26": "&",
|
|
38
|
+
"%2B": "+",
|
|
39
|
+
"%2C": ",",
|
|
40
|
+
"%3B": ";",
|
|
41
|
+
"%3D": "=",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function encodePathSegment(value: string): string {
|
|
45
|
+
return encodeURIComponent(value).replace(
|
|
46
|
+
/%(?:3A|40|24|26|2B|2C|3B|3D)/gi,
|
|
47
|
+
(match) => PATH_SAFE_ESCAPES[match.toUpperCase()] ?? match,
|
|
48
|
+
);
|
|
49
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -22,8 +22,7 @@ import type { UrlPatterns } from "./urls.js";
|
|
|
22
22
|
import type { UrlBuilder } from "./urls/pattern-types.js";
|
|
23
23
|
import { urls } from "./urls.js";
|
|
24
24
|
import {
|
|
25
|
-
EntryData,
|
|
26
|
-
InterceptSelectorContext,
|
|
25
|
+
type EntryData,
|
|
27
26
|
getContext,
|
|
28
27
|
RSCRouterContext,
|
|
29
28
|
type MetricsStore,
|
package/src/rsc/handler.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
interceptRedirectForPartial,
|
|
32
32
|
buildRouteMiddlewareEntries,
|
|
33
33
|
} from "./helpers.js";
|
|
34
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
34
35
|
import {
|
|
35
36
|
handleResponseRoute,
|
|
36
37
|
type ResponseRouteMatch,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
getRouterTrie,
|
|
57
58
|
} from "../route-map-builder.js";
|
|
58
59
|
import type { HandlerContext } from "./handler-context.js";
|
|
60
|
+
import type { SegmentCacheStore } from "../cache/types.js";
|
|
59
61
|
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
60
62
|
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
61
63
|
import {
|
|
@@ -352,7 +354,7 @@ export function createRSCHandler<
|
|
|
352
354
|
// Resolve cache store configuration
|
|
353
355
|
// Priority: options.cache (handler override) > router.cache (router default)
|
|
354
356
|
// Store is enabled only if: config provided, enabled, and no ?__no_cache query param
|
|
355
|
-
let cacheStore
|
|
357
|
+
let cacheStore: SegmentCacheStore | undefined;
|
|
356
358
|
const cacheOption = options.cache ?? router.cache;
|
|
357
359
|
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
358
360
|
const cacheConfig =
|
|
@@ -533,7 +535,9 @@ export function createRSCHandler<
|
|
|
533
535
|
}
|
|
534
536
|
|
|
535
537
|
const fullTiming = timingParts.join(", ");
|
|
536
|
-
if (fullTiming
|
|
538
|
+
if (fullTiming && !isWebSocketUpgradeResponse(response)) {
|
|
539
|
+
response.headers.set("Server-Timing", fullTiming);
|
|
540
|
+
}
|
|
537
541
|
|
|
538
542
|
return response;
|
|
539
543
|
});
|
|
@@ -804,7 +808,7 @@ export function createRSCHandler<
|
|
|
804
808
|
);
|
|
805
809
|
}
|
|
806
810
|
const response = responseOutcome.result;
|
|
807
|
-
if (plan.negotiated) {
|
|
811
|
+
if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
808
812
|
response.headers.append("Vary", "Accept");
|
|
809
813
|
}
|
|
810
814
|
return response;
|
|
@@ -1014,7 +1018,7 @@ export function createRSCHandler<
|
|
|
1014
1018
|
nonce,
|
|
1015
1019
|
);
|
|
1016
1020
|
}
|
|
1017
|
-
if (negotiated) {
|
|
1021
|
+
if (negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
1018
1022
|
response.headers.append("Vary", "Accept");
|
|
1019
1023
|
}
|
|
1020
1024
|
return response;
|
|
@@ -248,6 +248,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
248
248
|
segments: match.segments,
|
|
249
249
|
matched: match.matched,
|
|
250
250
|
diff: match.diff,
|
|
251
|
+
resolvedIds: match.resolvedIds,
|
|
251
252
|
params: match.params,
|
|
252
253
|
isPartial: false,
|
|
253
254
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -354,6 +355,7 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
354
355
|
segments: errorResult.segments,
|
|
355
356
|
matched: errorResult.matched,
|
|
356
357
|
diff: errorResult.diff,
|
|
358
|
+
resolvedIds: errorResult.resolvedIds,
|
|
357
359
|
params: errorResult.params,
|
|
358
360
|
isPartial: false,
|
|
359
361
|
isError: true,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
buildRouteMiddlewareEntries,
|
|
29
29
|
mergeStubHeadersAndFinalize,
|
|
30
30
|
} from "./helpers.js";
|
|
31
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
31
32
|
|
|
32
33
|
export interface ResponseRouteMatch {
|
|
33
34
|
responseType: string;
|
|
@@ -79,10 +80,13 @@ export async function handleResponseRoute<TEnv>(
|
|
|
79
80
|
env,
|
|
80
81
|
searchParams: cleanUrl.searchParams,
|
|
81
82
|
url: cleanUrl,
|
|
83
|
+
originalUrl: reqCtx.originalUrl,
|
|
82
84
|
pathname: url.pathname,
|
|
83
85
|
reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
|
|
84
86
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
85
87
|
header: (name: string, value: string) => reqCtx.header(name, value),
|
|
88
|
+
waitUntil: reqCtx.waitUntil.bind(reqCtx),
|
|
89
|
+
executionContext: reqCtx.executionContext,
|
|
86
90
|
_responseType: preview.responseType,
|
|
87
91
|
};
|
|
88
92
|
// Brand with taint symbol so "use cache" detects it as request-scoped
|
|
@@ -97,17 +101,12 @@ export async function handleResponseRoute<TEnv>(
|
|
|
97
101
|
// so that stub headers (cookies, custom headers set via ctx.header()) are included.
|
|
98
102
|
// Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
|
|
99
103
|
const rewrapResponse = (result: Response) => {
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// `webSocket` property
|
|
103
|
-
|
|
104
|
-
// constructor and must honor ctx.setStatus() overrides.
|
|
105
|
-
const hasWebSocket =
|
|
106
|
-
(result as unknown as { webSocket?: unknown }).webSocket != null;
|
|
107
|
-
if (hasWebSocket || result.status === 101) {
|
|
104
|
+
// 204/205/304 are NOT short-circuited — they're valid for the Response
|
|
105
|
+
// constructor and must honor ctx.setStatus() overrides. Only upgrade
|
|
106
|
+
// responses (status 101 / `webSocket` property) bypass reconstruction.
|
|
107
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
108
108
|
return mergeStubHeadersAndFinalize(result);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
110
|
const headers = new Headers();
|
|
112
111
|
result.headers.forEach((value, key) => {
|
|
113
112
|
if (key.toLowerCase() === "set-cookie") {
|
|
@@ -208,7 +207,9 @@ export async function handleResponseRoute<TEnv>(
|
|
|
208
207
|
// Wrap callHandler to append Vary: Accept on content-negotiated responses
|
|
209
208
|
const callHandlerWithVary = async () => {
|
|
210
209
|
const response = await callHandler();
|
|
211
|
-
if (preview.negotiated) {
|
|
210
|
+
if (preview.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
211
|
+
// Skip Vary on upgrade responses: headers are semantically immutable
|
|
212
|
+
// on some runtimes, and Vary is meaningless for a 101 response.
|
|
212
213
|
response.headers.append("Vary", "Accept");
|
|
213
214
|
}
|
|
214
215
|
return response;
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -59,6 +59,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
59
59
|
segments: match.segments,
|
|
60
60
|
matched: match.matched,
|
|
61
61
|
diff: match.diff,
|
|
62
|
+
resolvedIds: match.resolvedIds,
|
|
62
63
|
params: match.params,
|
|
63
64
|
isPartial: false,
|
|
64
65
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -81,6 +82,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
81
82
|
segments: result.segments,
|
|
82
83
|
matched: result.matched,
|
|
83
84
|
diff: result.diff,
|
|
85
|
+
resolvedIds: result.resolvedIds,
|
|
84
86
|
params: result.params,
|
|
85
87
|
isPartial: true,
|
|
86
88
|
slots: result.slots,
|
|
@@ -144,6 +146,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
144
146
|
segments: match.segments,
|
|
145
147
|
matched: match.matched,
|
|
146
148
|
diff: match.diff,
|
|
149
|
+
resolvedIds: match.resolvedIds,
|
|
147
150
|
params: match.params,
|
|
148
151
|
isPartial: false,
|
|
149
152
|
rootLayout: ctx.router.rootLayout,
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -213,6 +213,7 @@ export async function executeServerAction<TEnv>(
|
|
|
213
213
|
isPartial: true,
|
|
214
214
|
matched: errorResult.matched,
|
|
215
215
|
diff: errorResult.diff,
|
|
216
|
+
resolvedIds: errorResult.resolvedIds,
|
|
216
217
|
params: errorResult.params,
|
|
217
218
|
isError: true,
|
|
218
219
|
handles: handleStore.stream(),
|
|
@@ -324,6 +325,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
324
325
|
isPartial: true,
|
|
325
326
|
matched: matchResult.matched,
|
|
326
327
|
diff: matchResult.diff,
|
|
328
|
+
resolvedIds: matchResult.resolvedIds,
|
|
327
329
|
params: matchResult.params,
|
|
328
330
|
slots: matchResult.slots,
|
|
329
331
|
handles: handleStore.stream(),
|
package/src/rsc/types.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface RscPayload {
|
|
|
26
26
|
isError?: boolean;
|
|
27
27
|
matched?: string[];
|
|
28
28
|
diff?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* All segment ids re-resolved on the server, including null-component
|
|
31
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
32
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
33
|
+
*/
|
|
34
|
+
resolvedIds?: string[];
|
|
29
35
|
/** Merged route params from the matched route */
|
|
30
36
|
params?: Record<string, string>;
|
|
31
37
|
slots?: Record<string, SlotState>;
|
package/src/segment-system.tsx
CHANGED
|
@@ -131,6 +131,47 @@ export interface RenderSegmentsOptions {
|
|
|
131
131
|
rootLayout?: ComponentType<RootLayoutProps>;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function createViewTransitionBoundary(
|
|
135
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
136
|
+
children: ReactNode,
|
|
137
|
+
): ReactNode {
|
|
138
|
+
return createElement(ReactViewTransition, {
|
|
139
|
+
...transition,
|
|
140
|
+
children,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wrapDefaultOutletContent(
|
|
145
|
+
content: ReactNode,
|
|
146
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
147
|
+
): ReactNode {
|
|
148
|
+
if (!React.isValidElement(content)) {
|
|
149
|
+
return createViewTransitionBoundary(transition, content);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const props = content.props as any;
|
|
153
|
+
|
|
154
|
+
if (content.type === MountContextProvider) {
|
|
155
|
+
return React.cloneElement(content, {
|
|
156
|
+
children: wrapDefaultOutletContent(props.children, transition),
|
|
157
|
+
} as any);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (content.type === OutletProvider && props.segment?.type === "layout") {
|
|
161
|
+
return React.cloneElement(content, {
|
|
162
|
+
content: wrapDefaultOutletContent(props.content, transition),
|
|
163
|
+
} as any);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (content.type === LoaderBoundary && props.segment?.type === "layout") {
|
|
167
|
+
return React.cloneElement(content, {
|
|
168
|
+
outletContent: wrapDefaultOutletContent(props.outletContent, transition),
|
|
169
|
+
} as any);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return createViewTransitionBoundary(transition, content);
|
|
173
|
+
}
|
|
174
|
+
|
|
134
175
|
/**
|
|
135
176
|
* Render segments into a React tree with proper layout nesting
|
|
136
177
|
*
|
|
@@ -273,17 +314,27 @@ export async function renderSegments(
|
|
|
273
314
|
// in transitions without adding custom animation classes. Named element-level
|
|
274
315
|
// <ViewTransition> components inside (with name/share props) morph independently
|
|
275
316
|
// from the parent's default cross-fade.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
317
|
+
//
|
|
318
|
+
// For layouts, wrap the outlet content (what `<Outlet />` renders) rather
|
|
319
|
+
// than the layout component itself. Parallel slots like `<ParallelOutlet
|
|
320
|
+
// name="@modal" />` read from a separate context channel and end up as
|
|
321
|
+
// siblings of the VT in the rendered tree, so modal mounts don't trigger a
|
|
322
|
+
// subtree update on the layout-level VT — which would otherwise make
|
|
323
|
+
// React's commit walker fire `document.startViewTransition` and apply
|
|
324
|
+
// view-transition-names to the underlying main subtree (cover/title/etc.).
|
|
325
|
+
let outletContent: ReactNode =
|
|
285
326
|
node.segment.type === "layout" ? content : null;
|
|
286
327
|
|
|
328
|
+
const transition = node.segment.transition;
|
|
329
|
+
|
|
330
|
+
if (ReactViewTransition && transition) {
|
|
331
|
+
if (node.segment.type === "layout") {
|
|
332
|
+
outletContent = wrapDefaultOutletContent(outletContent, transition);
|
|
333
|
+
} else {
|
|
334
|
+
nodeContent = createViewTransitionBoundary(transition, nodeContent);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
287
338
|
// Prepare loader data if there are loaders
|
|
288
339
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
289
340
|
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
@@ -37,6 +37,8 @@ import { track, type MetricsStore } from "./context.js";
|
|
|
37
37
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
38
38
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
39
39
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
40
|
+
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
|
|
41
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
40
42
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
41
43
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
42
44
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
@@ -58,22 +60,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
|
58
60
|
export interface RequestContext<
|
|
59
61
|
TEnv = DefaultEnv,
|
|
60
62
|
TParams = Record<string, string>,
|
|
61
|
-
> {
|
|
62
|
-
/** Platform bindings (Cloudflare env, etc.) */
|
|
63
|
-
env: TEnv;
|
|
64
|
-
/** Original HTTP request */
|
|
65
|
-
request: Request;
|
|
66
|
-
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
67
|
-
url: URL;
|
|
68
|
-
/**
|
|
69
|
-
* The original request URL with all parameters intact, including
|
|
70
|
-
* internal `_rsc*` transport params.
|
|
71
|
-
*/
|
|
72
|
-
originalUrl: URL;
|
|
73
|
-
/** URL pathname */
|
|
74
|
-
pathname: string;
|
|
75
|
-
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
76
|
-
searchParams: URLSearchParams;
|
|
63
|
+
> extends RequestScope<TEnv> {
|
|
77
64
|
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
78
65
|
_variables: Record<string, any>;
|
|
79
66
|
/** Get a variable set by middleware */
|
|
@@ -159,20 +146,6 @@ export interface RequestContext<
|
|
|
159
146
|
import("../cache/profile-registry.js").CacheProfile
|
|
160
147
|
>;
|
|
161
148
|
|
|
162
|
-
/**
|
|
163
|
-
* Schedule work to run after the response is sent.
|
|
164
|
-
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
165
|
-
* On Node.js, runs as fire-and-forget.
|
|
166
|
-
*
|
|
167
|
-
* @example
|
|
168
|
-
* ```typescript
|
|
169
|
-
* ctx.waitUntil(async () => {
|
|
170
|
-
* await cacheStore.set(key, data, ttl);
|
|
171
|
-
* });
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
waitUntil(fn: () => Promise<void>): void;
|
|
175
|
-
|
|
176
149
|
/**
|
|
177
150
|
* Register a callback to run when the response is created.
|
|
178
151
|
* Callbacks are sync and receive the response. They can:
|
|
@@ -498,13 +471,7 @@ export function requireRequestContext<
|
|
|
498
471
|
return getRequestContext<TEnv>();
|
|
499
472
|
}
|
|
500
473
|
|
|
501
|
-
|
|
502
|
-
* Cloudflare Workers ExecutionContext (subset we need)
|
|
503
|
-
*/
|
|
504
|
-
export interface ExecutionContext {
|
|
505
|
-
waitUntil(promise: Promise<any>): void;
|
|
506
|
-
passThroughOnException(): void;
|
|
507
|
-
}
|
|
474
|
+
export type { ExecutionContext };
|
|
508
475
|
|
|
509
476
|
/**
|
|
510
477
|
* Options for creating a request context
|
|
@@ -768,16 +735,14 @@ export function createRequestContext<TEnv>(
|
|
|
768
735
|
|
|
769
736
|
waitUntil(fn: () => Promise<void>): void {
|
|
770
737
|
if (executionContext?.waitUntil) {
|
|
771
|
-
// Cloudflare Workers: use native waitUntil
|
|
772
738
|
executionContext.waitUntil(fn());
|
|
773
739
|
} else {
|
|
774
|
-
|
|
775
|
-
fn().catch((err) =>
|
|
776
|
-
console.error("[waitUntil] Background task failed:", err),
|
|
777
|
-
);
|
|
740
|
+
fireAndForgetWaitUntil(fn);
|
|
778
741
|
}
|
|
779
742
|
},
|
|
780
743
|
|
|
744
|
+
executionContext,
|
|
745
|
+
|
|
781
746
|
_onResponseCallbacks: [],
|
|
782
747
|
|
|
783
748
|
onResponse(callback: (response: Response) => Response): void {
|
|
@@ -1043,7 +1008,10 @@ export function createUseFunction<TEnv>(
|
|
|
1043
1008
|
search: (ctx as any).search ?? {},
|
|
1044
1009
|
pathname: ctx.pathname,
|
|
1045
1010
|
url: ctx.url,
|
|
1011
|
+
originalUrl: ctx.originalUrl,
|
|
1046
1012
|
env: ctx.env as any,
|
|
1013
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
1014
|
+
executionContext: ctx.executionContext,
|
|
1047
1015
|
get: ctx.get as any,
|
|
1048
1016
|
use: (<TDep, TDepParams = any>(
|
|
1049
1017
|
dep: LoaderDefinition<TDep, TDepParams>,
|
package/src/ssr/index.tsx
CHANGED
|
@@ -162,9 +162,13 @@ function createSsrEventController(opts: {
|
|
|
162
162
|
}): EventController {
|
|
163
163
|
const location = new URL(opts.pathname, "http://localhost");
|
|
164
164
|
let params = opts.params ?? {};
|
|
165
|
+
const rawMatched = opts.matched ?? [];
|
|
165
166
|
const handleState = {
|
|
166
167
|
data: opts.handleData ?? {},
|
|
167
|
-
segmentOrder: filterSegmentOrder(
|
|
168
|
+
segmentOrder: filterSegmentOrder(rawMatched),
|
|
169
|
+
routeSegmentIds: rawMatched.filter(
|
|
170
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
171
|
+
),
|
|
168
172
|
};
|
|
169
173
|
const state: DerivedNavigationState = {
|
|
170
174
|
state: "idle",
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from "./route-config.js";
|
|
21
21
|
import type { LoaderDefinition } from "./loader-types.js";
|
|
22
22
|
import type { UseItems, HandlerUseItem } from "../route-types.js";
|
|
23
|
+
import type { RequestScope } from "./request-scope.js";
|
|
23
24
|
|
|
24
25
|
// Re-export MiddlewareFn for internal/advanced use
|
|
25
26
|
export type { MiddlewareFn } from "../router/middleware.js";
|
|
@@ -195,7 +196,7 @@ export type HandlerContext<
|
|
|
195
196
|
TEnv = DefaultEnv,
|
|
196
197
|
TSearch extends SearchSchema = {},
|
|
197
198
|
TRouteMap = never,
|
|
198
|
-
> = {
|
|
199
|
+
> = RequestScope<TEnv> & {
|
|
199
200
|
/**
|
|
200
201
|
* Route parameters extracted from the URL pattern.
|
|
201
202
|
* Type-safe when using Handler<"/path/:param"> or Handler<{ param: string }>.
|
|
@@ -215,44 +216,11 @@ export type HandlerContext<
|
|
|
215
216
|
* changing build semantics (e.g., skip expensive operations in dev).
|
|
216
217
|
*/
|
|
217
218
|
dev: boolean;
|
|
218
|
-
/**
|
|
219
|
-
* The original incoming Request object (transport URL intact).
|
|
220
|
-
* Use `ctx.url` / `ctx.searchParams` for application logic — those have
|
|
221
|
-
* internal `_rsc*` params stripped. `ctx.request` preserves the raw URL
|
|
222
|
-
* for cases where you need original headers, method, or body.
|
|
223
|
-
*/
|
|
224
|
-
request: Request;
|
|
225
|
-
/**
|
|
226
|
-
* Query parameters from the URL (system params like `_rsc*` are filtered).
|
|
227
|
-
* Always a standard URLSearchParams instance.
|
|
228
|
-
*/
|
|
229
|
-
searchParams: URLSearchParams;
|
|
230
219
|
/**
|
|
231
220
|
* Typed search parameters parsed from URL query string via the route's
|
|
232
221
|
* search schema. Empty object when no schema is defined.
|
|
233
222
|
*/
|
|
234
223
|
search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
|
|
235
|
-
/**
|
|
236
|
-
* The pathname portion of the request URL.
|
|
237
|
-
*/
|
|
238
|
-
pathname: string;
|
|
239
|
-
/**
|
|
240
|
-
* The full URL object (with internal `_rsc*` params stripped).
|
|
241
|
-
* Use this for application logic — routing, link generation, display.
|
|
242
|
-
*/
|
|
243
|
-
url: URL;
|
|
244
|
-
/**
|
|
245
|
-
* The original request URL with all parameters intact, including
|
|
246
|
-
* internal `_rsc*` transport params. Use `ctx.url` for application
|
|
247
|
-
* logic — this is only needed for advanced cases like debugging
|
|
248
|
-
* or custom cache keying.
|
|
249
|
-
*/
|
|
250
|
-
originalUrl: URL;
|
|
251
|
-
/**
|
|
252
|
-
* Platform bindings (DB, KV, secrets, etc.).
|
|
253
|
-
* Access resources like `ctx.env.DB`, `ctx.env.KV`.
|
|
254
|
-
*/
|
|
255
|
-
env: TEnv;
|
|
256
224
|
/**
|
|
257
225
|
* Type-safe getter for middleware variables.
|
|
258
226
|
* Preferred way to read middleware-injected variables.
|
|
@@ -503,13 +471,16 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
503
471
|
* **Return Types:**
|
|
504
472
|
* - `boolean` - Hard decision: immediately returns this value (short-circuits)
|
|
505
473
|
* - `{ defaultShouldRevalidate: boolean }` - Soft decision: updates suggestion for next revalidator
|
|
474
|
+
* - `void` / `null` / `undefined` - Defer to the current suggestion (no opinion); the
|
|
475
|
+
* loop continues to the next revalidator without changing the running default
|
|
506
476
|
*
|
|
507
477
|
* **Execution Flow:**
|
|
508
478
|
* 1. Start with built-in `defaultShouldRevalidate` (true if params changed)
|
|
509
479
|
* 2. Execute global revalidators first, then route-specific
|
|
510
480
|
* 3. Hard decision (boolean): stop immediately and use that value
|
|
511
481
|
* 4. Soft decision (object): update suggestion and continue to next revalidator
|
|
512
|
-
* 5.
|
|
482
|
+
* 5. Defer (`void` / `null` / `undefined`): leave suggestion unchanged and continue
|
|
483
|
+
* 6. If no hard decision was returned: use the final running suggestion
|
|
513
484
|
*
|
|
514
485
|
* @param args.currentParams - Previous route params (generic by default, can be narrowed)
|
|
515
486
|
* @param args.currentUrl - Previous URL
|
|
@@ -521,7 +492,8 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
521
492
|
* @param args.formData - Form data from action (future support)
|
|
522
493
|
* @param args.formMethod - HTTP method from action (future support)
|
|
523
494
|
*
|
|
524
|
-
* @returns Hard decision (boolean)
|
|
495
|
+
* @returns Hard decision (boolean), soft suggestion (object), or defer
|
|
496
|
+
* (`void` / `null` / `undefined`) to keep the running suggestion as-is.
|
|
525
497
|
*
|
|
526
498
|
* @example
|
|
527
499
|
* ```typescript
|
|
@@ -546,8 +518,9 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
546
518
|
* a segment (layout, route, parallel slot, or loader) should be re-rendered.
|
|
547
519
|
*
|
|
548
520
|
* Return `true` to re-render, `false` to skip (keep client's current version),
|
|
549
|
-
*
|
|
550
|
-
* downstream
|
|
521
|
+
* `{ defaultShouldRevalidate: boolean }` to update the running suggestion for
|
|
522
|
+
* downstream revalidators, or nothing (`void` / `null` / `undefined`) to defer
|
|
523
|
+
* to the current suggestion without changing it.
|
|
551
524
|
*
|
|
552
525
|
* @example
|
|
553
526
|
* ```ts
|
|
@@ -647,7 +620,7 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
|
647
620
|
* action that may have mutated backend state.
|
|
648
621
|
*/
|
|
649
622
|
stale?: boolean;
|
|
650
|
-
}) => boolean | { defaultShouldRevalidate: boolean };
|
|
623
|
+
}) => boolean | { defaultShouldRevalidate: boolean } | null | void;
|
|
651
624
|
|
|
652
625
|
// MiddlewareFn is imported from "../router/middleware.js" and re-exported
|
|
653
626
|
|
|
@@ -3,11 +3,13 @@ import type { Handle } from "../handle.js";
|
|
|
3
3
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
4
4
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
5
5
|
import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
|
|
6
|
+
import type { UseItems, LoaderUseItem } from "../route-types.js";
|
|
6
7
|
import type {
|
|
7
8
|
DefaultEnv,
|
|
8
9
|
DefaultReverseRouteMap,
|
|
9
10
|
DefaultVars,
|
|
10
11
|
} from "./global-namespace.js";
|
|
12
|
+
import type { RequestScope } from "./request-scope.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Context passed to loader functions during execution
|
|
@@ -39,7 +41,7 @@ export type LoaderContext<
|
|
|
39
41
|
TEnv = DefaultEnv,
|
|
40
42
|
TBody = unknown,
|
|
41
43
|
TSearch extends SearchSchema = {},
|
|
42
|
-
> = {
|
|
44
|
+
> = RequestScope<TEnv> & {
|
|
43
45
|
params: TParams;
|
|
44
46
|
/**
|
|
45
47
|
* Route params extracted from the URL pattern match (server-side only).
|
|
@@ -48,12 +50,7 @@ export type LoaderContext<
|
|
|
48
50
|
* resource scoping.
|
|
49
51
|
*/
|
|
50
52
|
routeParams: Record<string, string>;
|
|
51
|
-
request: Request;
|
|
52
|
-
searchParams: URLSearchParams;
|
|
53
53
|
search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
|
|
54
|
-
pathname: string;
|
|
55
|
-
url: URL;
|
|
56
|
-
env: TEnv;
|
|
57
54
|
get: {
|
|
58
55
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
59
56
|
} & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
|
|
@@ -207,4 +204,6 @@ export type LoaderDefinition<
|
|
|
207
204
|
__brand: "loader";
|
|
208
205
|
$$id: string; // Injected by Vite plugin (exposeInternalIds) - unique identifier
|
|
209
206
|
fn?: LoaderFn<T, TParams, any>; // Optional - server-side only, stored in registry for RSC
|
|
207
|
+
/** Composable default DSL items merged when the loader is mounted. */
|
|
208
|
+
use?: () => UseItems<LoaderUseItem>;
|
|
210
209
|
};
|