@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefetch Policy
|
|
3
|
+
*
|
|
4
|
+
* Determines whether speculative prefetching should run for the current user.
|
|
5
|
+
* Honors browser reduced-data preferences when available.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type NavigatorWithConnection = Navigator & {
|
|
9
|
+
connection?: {
|
|
10
|
+
saveData?: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate on every call so runtime changes to Save-Data or
|
|
16
|
+
* prefers-reduced-data are respected immediately.
|
|
17
|
+
*/
|
|
18
|
+
export function shouldPrefetch(): boolean {
|
|
19
|
+
if (typeof window === "undefined") return false;
|
|
20
|
+
|
|
21
|
+
const nav =
|
|
22
|
+
typeof navigator !== "undefined"
|
|
23
|
+
? (navigator as NavigatorWithConnection)
|
|
24
|
+
: undefined;
|
|
25
|
+
|
|
26
|
+
if (nav?.connection?.saveData) return false;
|
|
27
|
+
|
|
28
|
+
if (typeof window.matchMedia === "function") {
|
|
29
|
+
try {
|
|
30
|
+
if (window.matchMedia("(prefers-reduced-data: reduce)").matches) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore unsupported query errors and allow prefetch.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** No-op, kept for test compatibility. */
|
|
42
|
+
export function resetPrefetchPolicy(): void {}
|
|
@@ -28,8 +28,12 @@ function startExecution(
|
|
|
28
28
|
executing.add(key);
|
|
29
29
|
abortController ??= new AbortController();
|
|
30
30
|
execute(abortController.signal).finally(() => {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Only decrement if this key wasn't already cleared by cancelAllPrefetches.
|
|
32
|
+
// Without this guard, cancelled tasks' .finally() would underflow active
|
|
33
|
+
// below zero, breaking the MAX_CONCURRENT guarantee.
|
|
34
|
+
if (executing.delete(key)) {
|
|
35
|
+
active--;
|
|
36
|
+
}
|
|
33
37
|
drain();
|
|
34
38
|
});
|
|
35
39
|
}
|
|
@@ -76,6 +80,9 @@ export function cancelAllPrefetches(): void {
|
|
|
76
80
|
|
|
77
81
|
queue.length = 0;
|
|
78
82
|
queued.clear();
|
|
83
|
+
// Clear executing before resetting active. In-flight .finally() callbacks
|
|
84
|
+
// check executing.delete(key) — if the key is gone, they skip decrementing,
|
|
85
|
+
// so active settles at 0 without underflow.
|
|
79
86
|
executing.clear();
|
|
80
|
-
|
|
87
|
+
active = 0;
|
|
81
88
|
}
|
|
@@ -31,11 +31,11 @@ export type LinkState =
|
|
|
31
31
|
| LocationStateEntry[]
|
|
32
32
|
| StateOrGetter<Record<string, unknown>>;
|
|
33
33
|
|
|
34
|
-
import { prefetchDirect, prefetchQueued } from "../prefetch
|
|
34
|
+
import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
|
|
35
35
|
import {
|
|
36
36
|
observeForPrefetch,
|
|
37
37
|
unobserveForPrefetch,
|
|
38
|
-
} from "../prefetch
|
|
38
|
+
} from "../prefetch/observer.js";
|
|
39
39
|
|
|
40
40
|
// Touch device detection for hybrid strategy.
|
|
41
41
|
// Checked once at module load (Link.tsx is "use client", runs only in browser).
|
|
@@ -235,31 +235,34 @@ export const Link: ForwardRefExoticComponent<
|
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// No navigation context (outside provider): fall back to native navigation.
|
|
239
|
+
if (!ctx?.navigate) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
238
243
|
// Prevent default and use SPA navigation
|
|
239
244
|
e.preventDefault();
|
|
240
245
|
// Stop propagation to prevent link-interceptor from also handling this
|
|
241
246
|
e.stopPropagation();
|
|
242
247
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
resolvedState = currentState;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
ctx.navigate(to, { replace, scroll, state: resolvedState });
|
|
248
|
+
const currentState = stateRef.current;
|
|
249
|
+
let resolvedState: unknown;
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
Array.isArray(currentState) &&
|
|
253
|
+
currentState.length > 0 &&
|
|
254
|
+
isLocationStateEntry(currentState[0])
|
|
255
|
+
) {
|
|
256
|
+
resolvedState = resolveLocationStateEntries(
|
|
257
|
+
currentState as LocationStateEntry[],
|
|
258
|
+
);
|
|
259
|
+
} else if (typeof currentState === "function") {
|
|
260
|
+
resolvedState = currentState();
|
|
261
|
+
} else if (currentState != null) {
|
|
262
|
+
resolvedState = currentState;
|
|
262
263
|
}
|
|
264
|
+
|
|
265
|
+
ctx.navigate(to, { replace, scroll, state: resolvedState });
|
|
263
266
|
},
|
|
264
267
|
[to, isExternal, reloadDocument, replace, scroll, ctx, onClick],
|
|
265
268
|
);
|
|
@@ -281,6 +284,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
281
284
|
|
|
282
285
|
let cancelled = false;
|
|
283
286
|
let unsubIdle: (() => void) | undefined;
|
|
287
|
+
let observedElement: Element | null = null;
|
|
284
288
|
|
|
285
289
|
const triggerPrefetch = () => {
|
|
286
290
|
if (cancelled) return;
|
|
@@ -311,6 +315,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
311
315
|
} else if (isViewport) {
|
|
312
316
|
const element = internalRef.current;
|
|
313
317
|
if (!element) return;
|
|
318
|
+
observedElement = element;
|
|
314
319
|
observeForPrefetch(element, () => {
|
|
315
320
|
scheduleWhenIdle(triggerPrefetch);
|
|
316
321
|
});
|
|
@@ -319,8 +324,8 @@ export const Link: ForwardRefExoticComponent<
|
|
|
319
324
|
return () => {
|
|
320
325
|
cancelled = true;
|
|
321
326
|
unsubIdle?.();
|
|
322
|
-
if (isViewport &&
|
|
323
|
-
unobserveForPrefetch(
|
|
327
|
+
if (isViewport && observedElement) {
|
|
328
|
+
unobserveForPrefetch(observedElement);
|
|
324
329
|
}
|
|
325
330
|
};
|
|
326
331
|
}, [resolvedStrategy, to, isExternal, ctx]);
|
|
@@ -22,8 +22,9 @@ import type { EventController } from "../event-controller.js";
|
|
|
22
22
|
import { RootErrorBoundary } from "../../root-error-boundary.js";
|
|
23
23
|
import type { HandleData } from "../types.js";
|
|
24
24
|
import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
25
|
+
import { NonceContext } from "./nonce-context.js";
|
|
25
26
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
26
|
-
import { cancelAllPrefetches } from "../prefetch
|
|
27
|
+
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Process handles from an async generator, updating the event controller
|
|
@@ -370,6 +371,13 @@ export function NavigationProvider({
|
|
|
370
371
|
);
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
// Match SSR tree shape: NonceContext.Provider is always present so
|
|
375
|
+
// hydration sees the same component tree. Value is undefined on the
|
|
376
|
+
// client — CSP nonces are a server-side HTML concern.
|
|
377
|
+
content = (
|
|
378
|
+
<NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
|
|
379
|
+
);
|
|
380
|
+
|
|
373
381
|
return (
|
|
374
382
|
<NavigationStoreContext.Provider value={contextValue}>
|
|
375
383
|
{content}
|
|
@@ -15,14 +15,10 @@ export { useParams } from "./use-params.js";
|
|
|
15
15
|
export { useAction, type TrackedActionState } from "./use-action.js";
|
|
16
16
|
|
|
17
17
|
// Segments state hook
|
|
18
|
-
export {
|
|
19
|
-
useSegments,
|
|
20
|
-
initSegmentsSync,
|
|
21
|
-
type SegmentsState,
|
|
22
|
-
} from "./use-segments.js";
|
|
18
|
+
export { useSegments, type SegmentsState } from "./use-segments.js";
|
|
23
19
|
|
|
24
20
|
// Handle data hook
|
|
25
|
-
export { useHandle
|
|
21
|
+
export { useHandle } from "./use-handle.js";
|
|
26
22
|
|
|
27
23
|
// Client cache controls hook
|
|
28
24
|
export {
|
|
@@ -79,7 +79,7 @@ export function createLocationState<TState>(
|
|
|
79
79
|
let _key: string | undefined;
|
|
80
80
|
|
|
81
81
|
function getKey(): string {
|
|
82
|
-
if (!_key && process.env.NODE_ENV
|
|
82
|
+
if (!_key && process.env.NODE_ENV === "development") {
|
|
83
83
|
throw new Error(
|
|
84
84
|
"[rsc-router] createLocationState key not set. " +
|
|
85
85
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context for CSP nonce propagation to client components during SSR.
|
|
5
|
+
*
|
|
6
|
+
* The SSR renderer wraps the tree with NonceContext.Provider so that
|
|
7
|
+
* client components (e.g. MetaTags) can apply nonces to inline scripts.
|
|
8
|
+
* On the browser side, no provider is needed — the default undefined
|
|
9
|
+
* is correct since CSP nonces are a server-side HTML concern.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createContext, useContext, type Context } from "react";
|
|
13
|
+
|
|
14
|
+
export const NonceContext: Context<string | undefined> = createContext<
|
|
15
|
+
string | undefined
|
|
16
|
+
>(undefined);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the CSP nonce during SSR. Returns undefined on the client.
|
|
20
|
+
*/
|
|
21
|
+
export function useNonce(): string | undefined {
|
|
22
|
+
return useContext(NonceContext);
|
|
23
|
+
}
|
|
@@ -127,6 +127,11 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
|
|
|
127
127
|
* const error = useAction(addToCart, state => state.error);
|
|
128
128
|
* ```
|
|
129
129
|
*
|
|
130
|
+
* @note The selector is expected to be stable for a given hook instance.
|
|
131
|
+
* This hook tracks one projection of one action. Changing selector semantics
|
|
132
|
+
* for the same action ID without a new action event is not a supported pattern;
|
|
133
|
+
* use separate useAction() subscriptions if you need different projections.
|
|
134
|
+
*
|
|
130
135
|
* @note Actions passed as props from server components lose their metadata
|
|
131
136
|
* during RSC serialization. Use a string action name or import directly.
|
|
132
137
|
*/
|
|
@@ -162,7 +167,10 @@ export function useAction<T>(
|
|
|
162
167
|
T | TrackedActionState
|
|
163
168
|
>(null!);
|
|
164
169
|
|
|
165
|
-
// Ref keeps the latest selector
|
|
170
|
+
// Ref keeps the latest selector for subscription callbacks without
|
|
171
|
+
// re-subscribing on every render. Selector changes themselves are not
|
|
172
|
+
// treated as a reactive input; this hook expects a stable selector and
|
|
173
|
+
// represents one subscription/projection for one action.
|
|
166
174
|
const selectorRef = useRef(selector);
|
|
167
175
|
selectorRef.current = selector;
|
|
168
176
|
|
|
@@ -12,17 +12,8 @@ import type { Handle } from "../../handle.js";
|
|
|
12
12
|
import { getCollectFn } from "../../handle.js";
|
|
13
13
|
import type { HandleData } from "../types.js";
|
|
14
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
|
-
import { filterSegmentOrder } from "./filter-segment-order.js";
|
|
16
15
|
import { shallowEqual } from "./shallow-equal.js";
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
* SSR module-level state.
|
|
20
|
-
* Populated by initHandleDataSync before React renders.
|
|
21
|
-
* Used by useState initializer during SSR.
|
|
22
|
-
*/
|
|
23
|
-
let ssrHandleData: HandleData = {};
|
|
24
|
-
let ssrSegmentOrder: string[] = [];
|
|
25
|
-
|
|
26
17
|
/**
|
|
27
18
|
* Resolve the collect function for a handle.
|
|
28
19
|
* Handle objects are plain { __brand, $$id } - collect is stored in the registry
|
|
@@ -76,18 +67,6 @@ function collectHandle<T, A>(
|
|
|
76
67
|
return collect(segmentArrays);
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
/**
|
|
80
|
-
* Initialize handle data synchronously for SSR.
|
|
81
|
-
* Called before rendering to populate state for useState initializer.
|
|
82
|
-
*
|
|
83
|
-
* @param data - Handle data from RSC payload
|
|
84
|
-
* @param matched - Segment order for reduction
|
|
85
|
-
*/
|
|
86
|
-
export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
87
|
-
ssrHandleData = data;
|
|
88
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
70
|
/**
|
|
92
71
|
* Hook to access collected handle data.
|
|
93
72
|
*
|
|
@@ -117,11 +96,10 @@ export function useHandle<T, A, S>(
|
|
|
117
96
|
): A | S {
|
|
118
97
|
const ctx = useContext(NavigationStoreContext);
|
|
119
98
|
|
|
120
|
-
// Initial state from
|
|
99
|
+
// Initial state from context event controller, or empty fallback without provider.
|
|
121
100
|
const [value, setValue] = useState<A | S>(() => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
|
|
101
|
+
if (!ctx) {
|
|
102
|
+
const collected = collectHandle(handle, {}, []);
|
|
125
103
|
return selector ? selector(collected) : collected;
|
|
126
104
|
}
|
|
127
105
|
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { shallowEqual } from "./shallow-equal.js";
|
|
6
|
-
import { getSsrParams } from "./use-segments.js";
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Hook to access the current route params.
|
|
@@ -31,9 +30,8 @@ export function useParams<T>(
|
|
|
31
30
|
const ctx = useContext(NavigationStoreContext);
|
|
32
31
|
|
|
33
32
|
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
return selector ? selector(ssrParams) : ssrParams;
|
|
33
|
+
if (!ctx) {
|
|
34
|
+
return selector ? selector({}) : {};
|
|
37
35
|
}
|
|
38
36
|
const params = ctx.eventController.getParams();
|
|
39
37
|
return selector ? selector(params) : params;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
|
-
import { getSsrPathname } from "./use-segments.js";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Hook to access the current pathname.
|
|
@@ -20,8 +19,8 @@ export function usePathname(): string {
|
|
|
20
19
|
const ctx = useContext(NavigationStoreContext);
|
|
21
20
|
|
|
22
21
|
const [pathname, setPathname] = useState<string>(() => {
|
|
23
|
-
if (
|
|
24
|
-
return
|
|
22
|
+
if (!ctx) {
|
|
23
|
+
return "/";
|
|
25
24
|
}
|
|
26
25
|
return (ctx.eventController.getState().location as URL).pathname;
|
|
27
26
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useContext, useMemo } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
|
-
import { prefetchDirect } from "../prefetch
|
|
5
|
+
import { prefetchDirect } from "../prefetch/fetch.js";
|
|
6
6
|
import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -41,7 +41,8 @@ export function useSearchParams(): ReadonlyURLSearchParams {
|
|
|
41
41
|
const nextSearch = location.searchParams.toString();
|
|
42
42
|
if (nextSearch !== prevSearch.current) {
|
|
43
43
|
prevSearch.current = nextSearch;
|
|
44
|
-
|
|
44
|
+
// Create a snapshot so callers cannot mutate the source URLSearchParams
|
|
45
|
+
setSearchParams(new URLSearchParams(nextSearch));
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
48
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useContext, useState, useEffect, useRef } from "react";
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
|
-
import { filterSegmentOrder } from "./filter-segment-order.js";
|
|
6
5
|
import { shallowEqual } from "./shallow-equal.js";
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -17,47 +16,6 @@ export interface SegmentsState {
|
|
|
17
16
|
location: URL;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
/**
|
|
21
|
-
* SSR module-level state.
|
|
22
|
-
* Populated by initSegmentsSync before React renders.
|
|
23
|
-
* Used by useState initializer during SSR.
|
|
24
|
-
*/
|
|
25
|
-
let ssrSegmentOrder: string[] = [];
|
|
26
|
-
let ssrPathname: string = "/";
|
|
27
|
-
let ssrParams: Record<string, string> = {};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Initialize segments data synchronously for SSR.
|
|
31
|
-
* Called before rendering to populate state for useState initializer.
|
|
32
|
-
*
|
|
33
|
-
* @param matched - Segment order from RSC metadata
|
|
34
|
-
* @param pathname - Current pathname
|
|
35
|
-
* @param params - Merged route params
|
|
36
|
-
*/
|
|
37
|
-
export function initSegmentsSync(
|
|
38
|
-
matched?: string[],
|
|
39
|
-
pathname?: string,
|
|
40
|
-
params?: Record<string, string>,
|
|
41
|
-
): void {
|
|
42
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
43
|
-
ssrPathname = pathname ?? "/";
|
|
44
|
-
ssrParams = params ?? {};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get SSR params for use-params hook initialization.
|
|
49
|
-
*/
|
|
50
|
-
export function getSsrParams(): Record<string, string> {
|
|
51
|
-
return ssrParams;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get SSR pathname for use-pathname hook initialization.
|
|
56
|
-
*/
|
|
57
|
-
export function getSsrPathname(): string {
|
|
58
|
-
return ssrPathname;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
19
|
/**
|
|
62
20
|
* Parse pathname into path segments
|
|
63
21
|
* /shop/products/123 → ["shop", "products", "123"]
|
|
@@ -80,18 +38,6 @@ function buildSegmentsState(
|
|
|
80
38
|
};
|
|
81
39
|
}
|
|
82
40
|
|
|
83
|
-
/**
|
|
84
|
-
* Build SSR state from module-level variables
|
|
85
|
-
*/
|
|
86
|
-
function buildSsrState(): SegmentsState {
|
|
87
|
-
const location = new URL(ssrPathname, "http://localhost");
|
|
88
|
-
return {
|
|
89
|
-
path: parsePathname(ssrPathname),
|
|
90
|
-
segmentIds: ssrSegmentOrder,
|
|
91
|
-
location,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
41
|
/**
|
|
96
42
|
* Hook to access current route segments with optional selector for performance
|
|
97
43
|
*
|
|
@@ -115,13 +61,14 @@ export function useSegments<T>(
|
|
|
115
61
|
): T | SegmentsState {
|
|
116
62
|
const ctx = useContext(NavigationStoreContext);
|
|
117
63
|
|
|
118
|
-
// Build initial state from
|
|
119
|
-
// Inlined rather than calling recompute() because the segmentsCache
|
|
120
|
-
//
|
|
64
|
+
// Build initial state from event controller when context exists.
|
|
65
|
+
// Inlined rather than calling recompute() because the segmentsCache ref
|
|
66
|
+
// is not yet initialized during the useState initializer.
|
|
121
67
|
const [state, setState] = useState<T | SegmentsState>(() => {
|
|
122
|
-
if (
|
|
123
|
-
const
|
|
124
|
-
|
|
68
|
+
if (!ctx) {
|
|
69
|
+
const fallbackLocation = new URL("/", "http://localhost");
|
|
70
|
+
const fallbackState = buildSegmentsState(fallbackLocation, []);
|
|
71
|
+
return selector ? selector(fallbackState) : fallbackState;
|
|
125
72
|
}
|
|
126
73
|
const location = ctx.eventController.getLocation();
|
|
127
74
|
const handleState = ctx.eventController.getHandleState();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
2
|
+
|
|
3
|
+
type HeaderResult = { url: string } | "blocked" | null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
|
|
7
|
+
* Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
|
|
8
|
+
*/
|
|
9
|
+
export function extractRscHeaderUrl(
|
|
10
|
+
response: Response,
|
|
11
|
+
header: string,
|
|
12
|
+
): HeaderResult {
|
|
13
|
+
const raw = response.headers.get(header);
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
const url = validateRedirectOrigin(raw, window.location.origin);
|
|
16
|
+
return url ? { url } : "blocked";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Empty 200 response that won't choke Flight parsing.
|
|
21
|
+
* Used when a header URL is blocked by origin validation.
|
|
22
|
+
*/
|
|
23
|
+
export function emptyResponse(): Response {
|
|
24
|
+
return new Response(null, { status: 200 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
|
+
* Returns a new Response with one branch; the other is consumed to detect
|
|
30
|
+
* end-of-stream, calling onComplete when done.
|
|
31
|
+
*
|
|
32
|
+
* If the response has no body, onComplete fires synchronously.
|
|
33
|
+
* If signal is provided, an abort cancels the tracking reader.
|
|
34
|
+
*/
|
|
35
|
+
export function teeWithCompletion(
|
|
36
|
+
response: Response,
|
|
37
|
+
onComplete: () => void,
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): Response {
|
|
40
|
+
if (!response.body) {
|
|
41
|
+
onComplete();
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [rscStream, trackingStream] = response.body.tee();
|
|
46
|
+
|
|
47
|
+
(async () => {
|
|
48
|
+
const reader = trackingStream.getReader();
|
|
49
|
+
const onAbort = signal ? reader.cancel.bind(reader) : undefined;
|
|
50
|
+
if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
|
|
51
|
+
try {
|
|
52
|
+
while (true) {
|
|
53
|
+
const { done } = await reader.read();
|
|
54
|
+
if (done) break;
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
if (onAbort) signal!.removeEventListener("abort", onAbort);
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
onComplete();
|
|
60
|
+
}
|
|
61
|
+
})().catch((error) => {
|
|
62
|
+
if (!signal?.aborted) {
|
|
63
|
+
console.error("[Browser] Error reading tracking stream:", error);
|
|
64
|
+
}
|
|
65
|
+
onComplete();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return new Response(rscStream, {
|
|
69
|
+
headers: response.headers,
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
});
|
|
73
|
+
}
|