@rangojs/router 0.0.0-experimental.ede38110 → 0.0.0-experimental.f2d1a2f1
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 +50 -20
- package/dist/vite/index.js +353 -49
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +5 -3
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +28 -20
- package/skills/links/SKILL.md +88 -16
- package/skills/loader/SKILL.md +35 -2
- package/skills/migrate-react-router/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +3 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/navigation-bridge.ts +51 -2
- package/src/browser/navigation-client.ts +33 -10
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +20 -1
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +50 -11
- package/src/browser/react/use-navigation.ts +30 -11
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/types.ts +13 -0
- package/src/cache/cf/cf-cache-store.ts +5 -7
- 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 +3 -2
- package/src/route-definition/dsl-helpers.ts +16 -3
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/router/handler-context.ts +20 -3
- package/src/router/lazy-includes.ts +1 -1
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/match-api.ts +3 -3
- package/src/router/middleware-types.ts +2 -22
- package/src/router/middleware.ts +32 -4
- package/src/router/pattern-matching.ts +60 -9
- package/src/router/trie-matching.ts +10 -4
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +7 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/server/request-context.ts +10 -42
- package/src/types/handler-context.ts +2 -34
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/urls/response-types.ts +2 -10
- package/src/vite/debug.ts +86 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/performance-tracks.ts +4 -6
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +161 -23
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
|
@@ -98,25 +98,30 @@ export interface LinkProps extends Omit<
|
|
|
98
98
|
*/
|
|
99
99
|
prefetch?: PrefetchStrategy;
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
102
|
-
* When set, prefetch responses are cached independently of the current
|
|
103
|
-
* page URL, so navigating to the same target from different source pages
|
|
104
|
-
* reuses the cached prefetch.
|
|
101
|
+
* Opt-in override for the prefetch cache scope.
|
|
105
102
|
*
|
|
106
|
-
* -
|
|
107
|
-
*
|
|
108
|
-
*
|
|
103
|
+
* The default cache is source-agnostic: one shared entry per target,
|
|
104
|
+
* keyed on Rango state + target URL. This is correct for routes whose
|
|
105
|
+
* response shape doesn't depend on where the user navigates from.
|
|
106
|
+
*
|
|
107
|
+
* Set `":source"` when this Link's response would legitimately differ
|
|
108
|
+
* based on the source page — typically when the target route (or one
|
|
109
|
+
* of its layouts) uses a custom `revalidate()` handler that reads
|
|
110
|
+
* `currentUrl` / `currentParams`, and the wildcard entry would
|
|
111
|
+
* therefore serve the wrong diff to a navigation from a different
|
|
112
|
+
* source.
|
|
113
|
+
*
|
|
114
|
+
* Intercept responses are auto-scoped to the source via a server-side
|
|
115
|
+
* tag, so `":source"` is only needed for custom revalidation logic.
|
|
109
116
|
*
|
|
110
117
|
* @example
|
|
111
118
|
* ```tsx
|
|
112
|
-
* //
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* // Normalize — strip trailing page number from source URL
|
|
116
|
-
* <Link to="/page/3" prefetch="hover" prefetchKey={(from) => from.replace(/\/\d+$/, '')} />
|
|
119
|
+
* // Route uses a `revalidate()` that branches on currentUrl — opt in
|
|
120
|
+
* // so prefetches don't bleed across source pages.
|
|
121
|
+
* <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
|
|
117
122
|
* ```
|
|
118
123
|
*/
|
|
119
|
-
prefetchKey?:
|
|
124
|
+
prefetchKey?: ":source";
|
|
120
125
|
/**
|
|
121
126
|
* State to pass to history.pushState/replaceState.
|
|
122
127
|
* Accessible via useLocationState() hook.
|
|
@@ -28,6 +28,7 @@ import { NonceContext } from "./nonce-context.js";
|
|
|
28
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
29
29
|
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
30
|
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
31
|
+
import type { AppShellRef } from "../app-shell.js";
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Process handles from an async generator, updating the event controller
|
|
@@ -133,15 +134,23 @@ export interface NavigationProviderProps {
|
|
|
133
134
|
warmupEnabled?: boolean;
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
|
-
* App version from server payload
|
|
137
|
-
*
|
|
137
|
+
* App version from server payload.
|
|
138
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
138
139
|
*/
|
|
139
140
|
version?: string;
|
|
140
141
|
|
|
141
142
|
/**
|
|
142
143
|
* URL prefix for all routes (from createRouter({ basename })).
|
|
144
|
+
* Used only as a fallback when `appShellRef` is not supplied.
|
|
143
145
|
*/
|
|
144
146
|
basename?: string;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Live app-shell ref. When provided, the context's `basename` and `version`
|
|
150
|
+
* properties become live getters that track app-switch updates without
|
|
151
|
+
* invalidating the memoized context value.
|
|
152
|
+
*/
|
|
153
|
+
appShellRef?: AppShellRef;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
/**
|
|
@@ -175,6 +184,7 @@ export function NavigationProvider({
|
|
|
175
184
|
warmupEnabled,
|
|
176
185
|
version,
|
|
177
186
|
basename,
|
|
187
|
+
appShellRef,
|
|
178
188
|
}: NavigationProviderProps): ReactNode {
|
|
179
189
|
// Track current payload for rendering (this triggers re-renders)
|
|
180
190
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -196,18 +206,39 @@ export function NavigationProvider({
|
|
|
196
206
|
await bridge.refresh();
|
|
197
207
|
}, []);
|
|
198
208
|
|
|
199
|
-
// Context value is stable (store, eventController, navigate, refresh never
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
// Context value is stable (store, eventController, navigate, refresh never
|
|
210
|
+
// change). When an appShellRef is supplied, `basename` and `version` are
|
|
211
|
+
// installed as live getters so app-switch transitions (which update the ref)
|
|
212
|
+
// propagate to consumers without forcing a tree-wide rerender.
|
|
213
|
+
const contextValue = useMemo<NavigationStoreContextValue>(() => {
|
|
214
|
+
if (appShellRef) {
|
|
215
|
+
const value = {
|
|
216
|
+
store,
|
|
217
|
+
eventController,
|
|
218
|
+
navigate,
|
|
219
|
+
refresh,
|
|
220
|
+
} as NavigationStoreContextValue;
|
|
221
|
+
Object.defineProperty(value, "basename", {
|
|
222
|
+
configurable: true,
|
|
223
|
+
enumerable: true,
|
|
224
|
+
get: () => appShellRef.get().basename,
|
|
225
|
+
});
|
|
226
|
+
Object.defineProperty(value, "version", {
|
|
227
|
+
configurable: true,
|
|
228
|
+
enumerable: true,
|
|
229
|
+
get: () => appShellRef.get().version,
|
|
230
|
+
});
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
202
234
|
store,
|
|
203
235
|
eventController,
|
|
204
236
|
navigate,
|
|
205
237
|
refresh,
|
|
206
238
|
version,
|
|
207
239
|
basename,
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
);
|
|
240
|
+
};
|
|
241
|
+
}, []);
|
|
211
242
|
|
|
212
243
|
// Connection warmup: keep TLS alive after idle periods.
|
|
213
244
|
// After 60s of no user interaction, marks connection as "cold".
|
|
@@ -345,8 +376,12 @@ export function NavigationProvider({
|
|
|
345
376
|
metadata: update.metadata,
|
|
346
377
|
});
|
|
347
378
|
|
|
348
|
-
// Update route params
|
|
349
|
-
|
|
379
|
+
// Update route params. Only reset when the server actually sends a params
|
|
380
|
+
// map — an absent `params` field means "no change" (e.g., legacy action
|
|
381
|
+
// responses that omitted params). Explicit `{}` still clears correctly.
|
|
382
|
+
if (update.metadata.params !== undefined) {
|
|
383
|
+
eventController.setParams(update.metadata.params);
|
|
384
|
+
}
|
|
350
385
|
|
|
351
386
|
// Update handle data progressively as it streams in
|
|
352
387
|
if (update.metadata.handles) {
|
|
@@ -398,7 +433,11 @@ export function NavigationProvider({
|
|
|
398
433
|
// Build the content tree
|
|
399
434
|
let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
|
|
400
435
|
|
|
401
|
-
// Wrap with ThemeProvider when theme is enabled
|
|
436
|
+
// Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
|
|
437
|
+
// document-lifetime: its config comes from the initial load and does NOT
|
|
438
|
+
// swap on cross-app transitions, because the ThemeProvider sits above the
|
|
439
|
+
// segment tree and a smooth (no-reload) app switch cannot safely remount
|
|
440
|
+
// it. A new theme config only takes effect on a full document load.
|
|
402
441
|
if (themeConfig) {
|
|
403
442
|
content = (
|
|
404
443
|
<ThemeProvider config={themeConfig} initialTheme={initialTheme}>
|
|
@@ -53,6 +53,12 @@ export function useNavigation<T>(
|
|
|
53
53
|
});
|
|
54
54
|
const prevState = useRef(baseValue);
|
|
55
55
|
|
|
56
|
+
// Tracks whether the most recent setOptimisticValue call pinned the value
|
|
57
|
+
// to a non-idle state. Used to decide whether to emit a release update when
|
|
58
|
+
// returning to idle, so the optimistic store doesn't stay pinned if a
|
|
59
|
+
// parent transition (e.g. <Link> click) is still pending.
|
|
60
|
+
const optimisticPinnedRef = useRef(false);
|
|
61
|
+
|
|
56
62
|
// useOptimistic allows immediate updates during transitions/actions
|
|
57
63
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
58
64
|
|
|
@@ -78,17 +84,30 @@ export function useNavigation<T>(
|
|
|
78
84
|
if (!shallowEqual(nextSelected, prevState.current)) {
|
|
79
85
|
prevState.current = nextSelected;
|
|
80
86
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// Check if any actions are in progress for optimistic updates
|
|
88
|
+
const hasInflightActions =
|
|
89
|
+
ctx.eventController.getInflightActions().size > 0;
|
|
90
|
+
|
|
91
|
+
const shouldPin = hasInflightActions || publicState.state !== "idle";
|
|
92
|
+
|
|
93
|
+
if (shouldPin) {
|
|
94
|
+
// Pin the optimistic store so the loading value shows immediately
|
|
95
|
+
// even if a parent transition (e.g. <Link> click) defers the
|
|
96
|
+
// urgent setBaseValue commit.
|
|
97
|
+
startTransition(() => {
|
|
98
|
+
setOptimisticValue(nextSelected);
|
|
99
|
+
});
|
|
100
|
+
optimisticPinnedRef.current = true;
|
|
101
|
+
} else if (optimisticPinnedRef.current) {
|
|
102
|
+
// Release a previously-pinned optimistic value. Without this,
|
|
103
|
+
// useOptimistic keeps returning the stale loading value while
|
|
104
|
+
// any parent transition is still pending, even after baseValue
|
|
105
|
+
// flipped to idle.
|
|
106
|
+
startTransition(() => {
|
|
107
|
+
setOptimisticValue(nextSelected);
|
|
108
|
+
});
|
|
109
|
+
optimisticPinnedRef.current = false;
|
|
110
|
+
}
|
|
92
111
|
|
|
93
112
|
// Always update base state so UI reflects current state
|
|
94
113
|
setBaseValue(nextSelected);
|
|
@@ -16,11 +16,21 @@ import { shallowEqual } from "./shallow-equal.js";
|
|
|
16
16
|
* const params = useParams();
|
|
17
17
|
* // { productId: "123" }
|
|
18
18
|
*
|
|
19
|
+
* // Annotate the expected shape via a generic
|
|
20
|
+
* const { productId } = useParams<{ productId: string }>();
|
|
21
|
+
*
|
|
19
22
|
* // With selector
|
|
20
23
|
* const productId = useParams(p => p.productId);
|
|
21
24
|
* ```
|
|
22
25
|
*/
|
|
23
|
-
|
|
26
|
+
// `T extends object` (not `Record<string, string | undefined>`) so that
|
|
27
|
+
// interface shapes pass the constraint — interfaces lack an implicit
|
|
28
|
+
// index signature and would otherwise be rejected. The generic is a
|
|
29
|
+
// shape annotation, not a runtime check; the body always returns the
|
|
30
|
+
// underlying params map unchanged.
|
|
31
|
+
export function useParams<
|
|
32
|
+
T extends object = Record<string, string>,
|
|
33
|
+
>(): Readonly<T>;
|
|
24
34
|
export function useParams<T>(
|
|
25
35
|
selector: (params: Record<string, string>) => T,
|
|
26
36
|
): T;
|
|
@@ -13,6 +13,10 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
|
13
13
|
* useRouter() do not re-render on navigation state changes.
|
|
14
14
|
* For reactive navigation state, use useNavigation() instead.
|
|
15
15
|
*
|
|
16
|
+
* Methods read `basename` from the live context on each call so that
|
|
17
|
+
* cross-app navigation (app-switch) sees the current app's basename
|
|
18
|
+
* rather than the one captured at mount time.
|
|
19
|
+
*
|
|
16
20
|
* @example
|
|
17
21
|
* ```tsx
|
|
18
22
|
* const router = useRouter();
|
|
@@ -29,7 +33,10 @@ export function useRouter(): RouterInstance {
|
|
|
29
33
|
throw new Error("useRouter must be used within NavigationProvider");
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
// Stable reference: ctx is
|
|
36
|
+
// Stable reference: ctx itself is stable, and reads on each method call
|
|
37
|
+
// pick up live basename values from the context (backed by a live ref
|
|
38
|
+
// in NavigationProvider), so app-switch transitions are reflected without
|
|
39
|
+
// recreating this object.
|
|
33
40
|
return useMemo<RouterInstance>(() => {
|
|
34
41
|
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
35
42
|
function withBasename(url: string): string {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
isInterceptSegment,
|
|
29
29
|
splitInterceptSegments,
|
|
30
30
|
} from "./intercept-utils.js";
|
|
31
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
31
32
|
|
|
32
33
|
// Vite HMR types are provided by vite/client
|
|
33
34
|
|
|
@@ -114,6 +115,13 @@ export interface BrowserAppContext {
|
|
|
114
115
|
warmupEnabled?: boolean;
|
|
115
116
|
/** App version for prefetch version mismatch detection */
|
|
116
117
|
version?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Live app-shell ref. Cross-app navigations replace its contents so the
|
|
120
|
+
* NavigationProvider and renderSegments pick up the target app's
|
|
121
|
+
* rootLayout, basename, and version without consumer rerenders. Theme,
|
|
122
|
+
* warmup, and prefetch TTL are document-lifetime (see AppShell).
|
|
123
|
+
*/
|
|
124
|
+
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// Module-level state for the initialized app
|
|
@@ -204,13 +212,23 @@ export async function initBrowserApp(
|
|
|
204
212
|
// Create composable utilities
|
|
205
213
|
const client = createNavigationClient(deps);
|
|
206
214
|
|
|
207
|
-
//
|
|
208
|
-
|
|
215
|
+
// Capture the per-router app-shell so cross-app navigations can replace
|
|
216
|
+
// it atomically. rootLayout, basename, and version live here and are
|
|
217
|
+
// read through the ref at call time rather than closed over. Theme,
|
|
218
|
+
// warmup, and prefetch TTL are deliberately excluded — they are
|
|
219
|
+
// document-lifetime and stay stable across smooth cross-app transitions.
|
|
209
220
|
const version = initialPayload.metadata?.version;
|
|
221
|
+
const appShellRef = createAppShellRef({
|
|
222
|
+
routerId: initialPayload.metadata?.routerId,
|
|
223
|
+
rootLayout: initialPayload.metadata?.rootLayout,
|
|
224
|
+
basename: initialPayload.metadata?.basename,
|
|
225
|
+
version,
|
|
226
|
+
});
|
|
210
227
|
|
|
211
228
|
// Initialize the localStorage state key for cache invalidation.
|
|
212
|
-
//
|
|
213
|
-
|
|
229
|
+
// The build version busts cached prefetches on deploy; the routerId
|
|
230
|
+
// namespaces the key so sibling apps on the same origin don't collide.
|
|
231
|
+
initRangoState(version ?? "0", initialPayload.metadata?.routerId);
|
|
214
232
|
setAppVersion(version);
|
|
215
233
|
|
|
216
234
|
// Initialize the in-memory prefetch cache TTL from server config.
|
|
@@ -220,11 +238,17 @@ export async function initBrowserApp(
|
|
|
220
238
|
initPrefetchCache(prefetchCacheTTL);
|
|
221
239
|
}
|
|
222
240
|
|
|
223
|
-
// Create a bound renderSegments that
|
|
241
|
+
// Create a bound renderSegments that reads rootLayout through the shell
|
|
242
|
+
// ref. On app switch the ref is updated before the tree re-renders, so
|
|
243
|
+
// the new app's Document (rootLayout) replaces the previous one.
|
|
224
244
|
const renderSegments = (
|
|
225
245
|
segments: ResolvedSegment[],
|
|
226
246
|
options?: RenderSegmentsOptions,
|
|
227
|
-
) =>
|
|
247
|
+
) =>
|
|
248
|
+
baseRenderSegments(segments, {
|
|
249
|
+
...options,
|
|
250
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
251
|
+
});
|
|
228
252
|
|
|
229
253
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
230
254
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -256,6 +280,7 @@ export async function initBrowserApp(
|
|
|
256
280
|
onUpdate: (update) => store.emitUpdate(update),
|
|
257
281
|
renderSegments,
|
|
258
282
|
version: version,
|
|
283
|
+
appShellRef,
|
|
259
284
|
});
|
|
260
285
|
|
|
261
286
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -416,6 +441,7 @@ export async function initBrowserApp(
|
|
|
416
441
|
initialTheme: effectiveInitialTheme,
|
|
417
442
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
418
443
|
version,
|
|
444
|
+
appShellRef,
|
|
419
445
|
};
|
|
420
446
|
browserAppContext = context;
|
|
421
447
|
|
|
@@ -481,6 +507,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
481
507
|
initialTheme,
|
|
482
508
|
warmupEnabled,
|
|
483
509
|
version,
|
|
510
|
+
appShellRef,
|
|
484
511
|
} = getBrowserAppContext();
|
|
485
512
|
|
|
486
513
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -501,6 +528,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
501
528
|
warmupEnabled={warmupEnabled}
|
|
502
529
|
version={version}
|
|
503
530
|
basename={initialPayload.metadata?.basename}
|
|
531
|
+
appShellRef={appShellRef}
|
|
504
532
|
/>
|
|
505
533
|
);
|
|
506
534
|
}
|
package/src/browser/types.ts
CHANGED
|
@@ -427,6 +427,12 @@ export interface NavigationStore {
|
|
|
427
427
|
markCacheAsStale(): void;
|
|
428
428
|
markCacheAsStaleAndBroadcast(): void;
|
|
429
429
|
clearHistoryCache(): void;
|
|
430
|
+
/**
|
|
431
|
+
* Clear this tab's nav + prefetch caches without broadcasting or rotating
|
|
432
|
+
* shared state. Intended for app-switch transitions that affect only this
|
|
433
|
+
* tab's session.
|
|
434
|
+
*/
|
|
435
|
+
clearHistoryCacheLocal(): void;
|
|
430
436
|
broadcastCacheInvalidation(): void;
|
|
431
437
|
|
|
432
438
|
// Cross-tab refresh callback (set by navigation bridge)
|
|
@@ -542,6 +548,13 @@ export interface NavigationBridge {
|
|
|
542
548
|
registerLinkInterception(): () => void;
|
|
543
549
|
/** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
|
|
544
550
|
updateVersion(newVersion: string): void;
|
|
551
|
+
/**
|
|
552
|
+
* Replace the active app-shell snapshot (rootLayout, basename, version)
|
|
553
|
+
* atomically. Used on cross-app navigations when the response's routerId
|
|
554
|
+
* indicates the user entered a different app. Theme, warmup, and prefetch
|
|
555
|
+
* TTL are document-lifetime and not part of the shell.
|
|
556
|
+
*/
|
|
557
|
+
updateAppShell(next: import("./app-shell.js").AppShell): void;
|
|
545
558
|
}
|
|
546
559
|
|
|
547
560
|
/**
|
|
@@ -67,13 +67,11 @@ export const MAX_REVALIDATION_INTERVAL = 30;
|
|
|
67
67
|
// Types
|
|
68
68
|
// ============================================================================
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
passThroughOnException(): void;
|
|
76
|
-
}
|
|
70
|
+
// Re-exported from the canonical home so cf-cache-store consumers keep
|
|
71
|
+
// importing `ExecutionContext` from this module without a second interface
|
|
72
|
+
// drifting over time.
|
|
73
|
+
export type { ExecutionContext } from "../../types/request-scope.js";
|
|
74
|
+
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
77
|
* Minimal Cloudflare KV Namespace interface.
|
package/src/index.rsc.ts
CHANGED
|
@@ -172,6 +172,9 @@ export type { PublicRequestContext as RequestContext } from "./server/request-co
|
|
|
172
172
|
import type { PublicRequestContext } from "./server/request-context.js";
|
|
173
173
|
import type { DefaultEnv } from "./types/global-namespace.js";
|
|
174
174
|
|
|
175
|
+
// Shared base for every user-facing request context (mirrors index.ts).
|
|
176
|
+
export type { RequestScope, ExecutionContext } from "./types/request-scope.js";
|
|
177
|
+
|
|
175
178
|
export const getRequestContext: <
|
|
176
179
|
TEnv = DefaultEnv,
|
|
177
180
|
>() => PublicRequestContext<TEnv> = _getRequestContextInternal;
|
package/src/index.ts
CHANGED
|
@@ -264,6 +264,9 @@ export function transition(): never {
|
|
|
264
264
|
// Request context type (safe for client)
|
|
265
265
|
export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
|
|
266
266
|
|
|
267
|
+
// Shared base for every user-facing request context.
|
|
268
|
+
export type { RequestScope, ExecutionContext } from "./types/request-scope.js";
|
|
269
|
+
|
|
267
270
|
// Cookie store types (safe for client)
|
|
268
271
|
export type {
|
|
269
272
|
CookieStore,
|
package/src/outlet-context.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-neutral Response shape utilities.
|
|
3
|
+
*
|
|
4
|
+
* Kept at the src/ root so both `router/` and `rsc/` can depend on it
|
|
5
|
+
* without creating a cross-layer import cycle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True when a Response represents a WebSocket upgrade handoff and must not
|
|
10
|
+
* be reconstructed or mutated:
|
|
11
|
+
*
|
|
12
|
+
* - Status 101 (Switching Protocols) is outside the standard Response
|
|
13
|
+
* constructor's 200–599 range, so `new Response(body, { status: 101 })`
|
|
14
|
+
* throws RangeError on Node/undici and any spec-compliant runtime.
|
|
15
|
+
* - Cloudflare's workerd attaches a non-standard `webSocket` property on
|
|
16
|
+
* the upgrade Response (e.g. from `acceptWebSocket`/`handleWebSocketUpgrade`
|
|
17
|
+
* or the `agents` library's `routeAgentRequest`). That property is dropped
|
|
18
|
+
* by a `new Response(...)` copy, breaking the upgrade even on workerd
|
|
19
|
+
* where the status range is relaxed.
|
|
20
|
+
*
|
|
21
|
+
* Callers should short-circuit header/body merges for these responses.
|
|
22
|
+
*/
|
|
23
|
+
export function isWebSocketUpgradeResponse(response: Response): boolean {
|
|
24
|
+
return (
|
|
25
|
+
response.status === 101 ||
|
|
26
|
+
(response as unknown as { webSocket?: unknown }).webSocket != null
|
|
27
|
+
);
|
|
28
|
+
}
|
package/src/reverse.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtractParams } from "./types.js";
|
|
2
2
|
import type { SearchSchema, ResolveSearchSchema } from "./search-params.js";
|
|
3
3
|
import { serializeSearchParams } from "./search-params.js";
|
|
4
|
+
import { encodePathSegment } from "./router/url-params.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Sanitize prefix string by removing leading slash
|
|
@@ -318,7 +319,7 @@ export function createReverse<TRoutes extends Record<string, string>>(
|
|
|
318
319
|
hadOmittedOptional = true;
|
|
319
320
|
return "";
|
|
320
321
|
}
|
|
321
|
-
return
|
|
322
|
+
return encodePathSegment(value);
|
|
322
323
|
},
|
|
323
324
|
);
|
|
324
325
|
// Second pass: required params (no trailing ?)
|
|
@@ -329,7 +330,7 @@ export function createReverse<TRoutes extends Record<string, string>>(
|
|
|
329
330
|
if (value === undefined) {
|
|
330
331
|
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
331
332
|
}
|
|
332
|
-
return
|
|
333
|
+
return encodePathSegment(value);
|
|
333
334
|
},
|
|
334
335
|
);
|
|
335
336
|
// Clean up slashes only when an optional param was actually omitted,
|
|
@@ -304,6 +304,15 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
304
304
|
return { name: namespace, type: "cache" } as CacheItem;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
// Inside a loader() use() callback, only the direct form — cache()/cache(opts)/
|
|
308
|
+
// cache("profile") — writes cache config to the loader entry. The wrapper
|
|
309
|
+
// form creates a structural cache boundary with its own children scope, which
|
|
310
|
+
// has no effect on the loader and would silently no-op.
|
|
311
|
+
invariant(
|
|
312
|
+
!(ctx.parent && (ctx.parent as any).type === "loader"),
|
|
313
|
+
"cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
|
|
314
|
+
);
|
|
315
|
+
|
|
307
316
|
// With children: create a cache entry (like layout with caching semantics)
|
|
308
317
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
309
318
|
const cacheShortCode = store.getShortCode("cache");
|
|
@@ -750,8 +759,12 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
750
759
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
751
760
|
};
|
|
752
761
|
|
|
753
|
-
//
|
|
754
|
-
|
|
762
|
+
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
763
|
+
const handlerUseFn = resolveHandlerUse(loaderDef);
|
|
764
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "loader");
|
|
765
|
+
|
|
766
|
+
// If any use callback is in effect, run it to collect revalidation rules and cache config
|
|
767
|
+
if (mergedUse) {
|
|
755
768
|
// Temporarily set context for revalidate()/cache() calls to target this loader
|
|
756
769
|
const originalParent = ctx.parent;
|
|
757
770
|
// Create a temporary "parent" with type "loader" so cache() can detect it.
|
|
@@ -764,7 +777,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
764
777
|
};
|
|
765
778
|
ctx.parent = tempParent as EntryData;
|
|
766
779
|
|
|
767
|
-
const result =
|
|
780
|
+
const result = mergedUse()?.flat(3);
|
|
768
781
|
|
|
769
782
|
// Copy cache config only if cache() was called during the use() callback.
|
|
770
783
|
// The spread from originalParent may carry an inherited .cache from
|
|
@@ -21,6 +21,10 @@ export function resolveHandlerUse(handler: unknown): (() => any[]) | undefined {
|
|
|
21
21
|
if (isStaticHandler(handler)) {
|
|
22
22
|
return (handler as any).use;
|
|
23
23
|
}
|
|
24
|
+
// Loader definitions from createLoader() — branded objects with optional .use
|
|
25
|
+
if (typeof handler === "object" && (handler as any).__brand === "loader") {
|
|
26
|
+
return (handler as any).use;
|
|
27
|
+
}
|
|
24
28
|
// Plain handler function
|
|
25
29
|
if (typeof handler === "function") {
|
|
26
30
|
return (handler as any).use;
|
|
@@ -99,6 +103,8 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
|
|
|
99
103
|
"when",
|
|
100
104
|
"transition",
|
|
101
105
|
]),
|
|
106
|
+
// LoaderUseItem — only revalidate + cache can attach to a loader entry
|
|
107
|
+
loader: new Set(["revalidate", "cache"]),
|
|
102
108
|
};
|
|
103
109
|
|
|
104
110
|
/**
|
|
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
|
|
|
18
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
19
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
20
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
21
|
+
import { encodePathSegment } from "./url-params.js";
|
|
22
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Strip internal _rsc* query params from a URL.
|
|
@@ -181,7 +183,7 @@ export function createReverseFunction(
|
|
|
181
183
|
hadOmittedOptional = true;
|
|
182
184
|
return "";
|
|
183
185
|
}
|
|
184
|
-
return
|
|
186
|
+
return encodePathSegment(value);
|
|
185
187
|
},
|
|
186
188
|
);
|
|
187
189
|
// Second pass: required params (no trailing ?)
|
|
@@ -192,7 +194,7 @@ export function createReverseFunction(
|
|
|
192
194
|
if (value === undefined) {
|
|
193
195
|
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
194
196
|
}
|
|
195
|
-
return
|
|
197
|
+
return encodePathSegment(value);
|
|
196
198
|
},
|
|
197
199
|
);
|
|
198
200
|
// Clean up slashes only when an optional param was actually omitted,
|
|
@@ -281,8 +283,12 @@ export function createHandlerContext<TEnv>(
|
|
|
281
283
|
search: searchSchema ? resolvedSearchParams : {},
|
|
282
284
|
pathname,
|
|
283
285
|
url,
|
|
284
|
-
originalUrl: new URL(request.url),
|
|
286
|
+
originalUrl: requestContext?.originalUrl ?? new URL(request.url),
|
|
285
287
|
env: bindings,
|
|
288
|
+
waitUntil: requestContext
|
|
289
|
+
? requestContext.waitUntil.bind(requestContext)
|
|
290
|
+
: fireAndForgetWaitUntil,
|
|
291
|
+
executionContext: requestContext?.executionContext,
|
|
286
292
|
_variables: variables,
|
|
287
293
|
get: ((keyOrVar: any) => {
|
|
288
294
|
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
@@ -387,6 +393,12 @@ export function createPrerenderContext<TEnv>(
|
|
|
387
393
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
388
394
|
);
|
|
389
395
|
},
|
|
396
|
+
// Build-time prerender has no live request. waitUntil is a true no-op
|
|
397
|
+
// (running fn() here would fire side effects during build, which is
|
|
398
|
+
// incorrect — these are meant to outlive the live response).
|
|
399
|
+
// executionContext is absent for the same reason.
|
|
400
|
+
waitUntil: () => {},
|
|
401
|
+
executionContext: undefined,
|
|
390
402
|
_variables: variables,
|
|
391
403
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
392
404
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -476,6 +488,11 @@ export function createStaticContext<TEnv>(
|
|
|
476
488
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
477
489
|
);
|
|
478
490
|
},
|
|
491
|
+
// Static() handlers have no live request. waitUntil is a true no-op
|
|
492
|
+
// (running fn() here would fire side effects during build, which is
|
|
493
|
+
// incorrect). executionContext is absent for the same reason.
|
|
494
|
+
waitUntil: () => {},
|
|
495
|
+
executionContext: undefined,
|
|
479
496
|
_variables: variables,
|
|
480
497
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
481
498
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -266,7 +266,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
266
266
|
search: (ctx as any).search,
|
|
267
267
|
pathname: ctx.pathname,
|
|
268
268
|
url: ctx.url,
|
|
269
|
+
originalUrl: ctx.originalUrl,
|
|
269
270
|
env: ctx.env,
|
|
271
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
272
|
+
executionContext: ctx.executionContext,
|
|
270
273
|
get: ((keyOrVar: any) =>
|
|
271
274
|
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
272
275
|
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|