@rangojs/router 0.0.0-experimental.133 → 0.0.0-experimental.135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +7 -2
- package/dist/vite/index.js +41 -27
- package/package.json +23 -24
- package/skills/composability/SKILL.md +0 -1
- package/skills/handler-use/SKILL.md +7 -7
- package/skills/intercept/SKILL.md +38 -13
- package/skills/loader/SKILL.md +10 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/migrate-react-router/SKILL.md +144 -1
- package/skills/prerender/SKILL.md +20 -17
- package/skills/router-setup/SKILL.md +1 -2
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/render-handler.md +15 -14
- package/skills/use-cache/SKILL.md +11 -0
- package/skills/view-transitions/SKILL.md +43 -0
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +27 -1
- package/src/browser/navigation-store.ts +82 -8
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +43 -3
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +32 -4
- package/src/browser/react/NavigationProvider.tsx +195 -4
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/types.ts +32 -1
- package/src/cache/cache-runtime.ts +26 -5
- package/src/cache/document-cache.ts +17 -1
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/handles/MetaTags.tsx +6 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +6 -4
- package/src/internal-debug.ts +11 -8
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +38 -39
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-types.ts +0 -5
- package/src/router/match-api.ts +5 -1
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/segment-resolution/fresh.ts +4 -0
- package/src/router/segment-resolution/loader-cache.ts +14 -2
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/rsc/progressive-enhancement.ts +56 -2
- package/src/rsc/rsc-rendering.ts +7 -2
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/segment-system.tsx +59 -8
- package/src/server/context.ts +13 -0
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +52 -3
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/segments.ts +100 -0
- package/src/urls/path-helper-types.ts +10 -7
- package/src/urls/urls-function.ts +0 -1
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +28 -9
- package/src/vite/router-discovery.ts +8 -13
- package/src/network-error-thrower.tsx +0 -18
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
encodeReply,
|
|
21
21
|
createClientTemporaryReferenceSet,
|
|
22
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getRequestContext,
|
|
25
|
+
runWithRequestContext,
|
|
26
|
+
} from "../server/request-context.js";
|
|
24
27
|
import { isUnderTestRunner } from "../runtime-env.js";
|
|
25
28
|
import {
|
|
26
29
|
isTainted,
|
|
@@ -307,7 +310,13 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
307
310
|
}
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
|
|
313
|
+
// foregroundOnAction (opt-in; see CacheProfile.foregroundOnAction): during an
|
|
314
|
+
// action's revalidation render, a stale entry falls through to the foreground
|
|
315
|
+
// miss path below instead of SWR. The flag is set by revalidateAfterAction.
|
|
316
|
+
const foregroundOnActionRevalidation =
|
|
317
|
+
requestCtx?._inActionRevalidation === true &&
|
|
318
|
+
profile.foregroundOnAction === true;
|
|
319
|
+
if (cached?.shouldRevalidate && !foregroundOnActionRevalidation) {
|
|
311
320
|
// Stale hit: return stale value, revalidate in background
|
|
312
321
|
try {
|
|
313
322
|
const result = await serveCached(cached);
|
|
@@ -316,8 +325,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
316
325
|
// live response or throw LateHandlePushError on the completed store.
|
|
317
326
|
// Same isolation pattern as route-level background-revalidation.ts.
|
|
318
327
|
runBackground(requestCtx, async () => {
|
|
319
|
-
//
|
|
320
|
-
//
|
|
328
|
+
// The closure-captured requestCtx is reused for the framework's own
|
|
329
|
+
// reads (handle store swap, error reporting) AND, below, to
|
|
330
|
+
// re-establish the request-context ALS around the user fn. ALS context
|
|
331
|
+
// may be gone inside waitUntil: on workerd a waitUntil task runs
|
|
332
|
+
// detached from the request's I/O context, so getRequestContext()
|
|
333
|
+
// inside the cached body would otherwise throw.
|
|
321
334
|
let originalHandleStore:
|
|
322
335
|
| ReturnType<typeof createHandleStore>
|
|
323
336
|
| undefined;
|
|
@@ -360,7 +373,15 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
360
373
|
}
|
|
361
374
|
|
|
362
375
|
try {
|
|
363
|
-
|
|
376
|
+
// Re-establish the request-context ALS so a "use cache" body that
|
|
377
|
+
// reads the ambient getRequestContext() (e.g.
|
|
378
|
+
// getRequestContext().env.ApiKey) resolves during the background
|
|
379
|
+
// revalidation instead of throwing "called outside of a request
|
|
380
|
+
// context". runWithRequestContext sets the store for fn's
|
|
381
|
+
// synchronous kickoff; its async continuations inherit it.
|
|
382
|
+
const scoped = runWithRequestContext(requestCtx, () =>
|
|
383
|
+
runWithCacheTagScope(() => fn.apply(this, args)),
|
|
384
|
+
);
|
|
364
385
|
const freshResult = await scoped.result;
|
|
365
386
|
bgStopCapture?.();
|
|
366
387
|
// Merge profile/DSL tags with runtime cacheTag() tags, read after
|
|
@@ -15,6 +15,7 @@ import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
|
15
15
|
import { hasPerClientSignal } from "../browser/cookie-name.js";
|
|
16
16
|
import {
|
|
17
17
|
getRequestContext,
|
|
18
|
+
runWithRequestContext,
|
|
18
19
|
type RequestContext,
|
|
19
20
|
} from "../server/request-context.js";
|
|
20
21
|
import { mayNeedSSR } from "../rsc/ssr-setup.js";
|
|
@@ -330,7 +331,13 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
330
331
|
|
|
331
332
|
runBackground(requestCtx, async () => {
|
|
332
333
|
try {
|
|
333
|
-
|
|
334
|
+
// Re-establish the request-context ALS around the background
|
|
335
|
+
// re-render: next() re-runs the full handler pipeline, and on
|
|
336
|
+
// workerd a waitUntil task runs detached from the request's I/O
|
|
337
|
+
// context, so a handler/component reading getRequestContext() would
|
|
338
|
+
// otherwise throw. Same fix as the route-level/use-cache background
|
|
339
|
+
// revalidation paths.
|
|
340
|
+
const fresh = await runWithRequestContext(requestCtx, () => next());
|
|
334
341
|
const directives = shouldCacheResponse(fresh);
|
|
335
342
|
|
|
336
343
|
if (directives && fresh.body) {
|
|
@@ -348,10 +355,15 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
348
355
|
log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
|
|
349
356
|
}
|
|
350
357
|
} catch (error) {
|
|
358
|
+
// Pass requestCtx explicitly: this runs in a detached waitUntil task
|
|
359
|
+
// where the ALS context is gone, so onError only fires if we hand it
|
|
360
|
+
// the captured context (reportCacheError falls back to _getRequestContext
|
|
361
|
+
// otherwise, which is null here).
|
|
351
362
|
reportCacheError(
|
|
352
363
|
error,
|
|
353
364
|
"cache-write",
|
|
354
365
|
"[DocumentCache] revalidation",
|
|
366
|
+
requestCtx,
|
|
355
367
|
);
|
|
356
368
|
}
|
|
357
369
|
});
|
|
@@ -401,10 +413,14 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
401
413
|
collectRequestTags(requestCtx),
|
|
402
414
|
);
|
|
403
415
|
} catch (error) {
|
|
416
|
+
// Detached waitUntil task — pass the captured requestCtx so onError
|
|
417
|
+
// fires even though the ALS context is gone (see the revalidation
|
|
418
|
+
// catch above).
|
|
404
419
|
reportCacheError(
|
|
405
420
|
error,
|
|
406
421
|
"cache-write",
|
|
407
422
|
"[DocumentCache] cache write",
|
|
423
|
+
requestCtx,
|
|
408
424
|
);
|
|
409
425
|
}
|
|
410
426
|
});
|
|
@@ -15,6 +15,21 @@ export interface CacheProfile {
|
|
|
15
15
|
swr?: number;
|
|
16
16
|
/** Default cache tags for invalidation */
|
|
17
17
|
tags?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* When true, a stale entry encountered during a server action's revalidation
|
|
20
|
+
* render is re-executed in the FOREGROUND (the action response reflects the
|
|
21
|
+
* refreshed value) instead of being served stale + revalidated in the
|
|
22
|
+
* background. Only the store write is deferred. Use this for mutation-related
|
|
23
|
+
* cached data that an action should refresh immediately; ordinary navigations
|
|
24
|
+
* always keep SWR. Default false: incidental TTL staleness must not turn an
|
|
25
|
+
* unrelated action into a synchronous cache-refresh barrier — for strong
|
|
26
|
+
* read-your-own-writes after a mutation, prefer updateTag() (a hard purge, so
|
|
27
|
+
* the action's own re-render is a fresh foreground miss).
|
|
28
|
+
*
|
|
29
|
+
* Honored only by `"use cache"` functions; route-level `cache()` and cached
|
|
30
|
+
* loaders ignore it (use updateTag() there).
|
|
31
|
+
*/
|
|
32
|
+
foregroundOnAction?: boolean;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
|
|
@@ -36,6 +36,14 @@ export interface ReadThroughItemConfig<T> {
|
|
|
36
36
|
key: string;
|
|
37
37
|
/** Execute the underlying function/loader on miss or revalidation */
|
|
38
38
|
execute: () => Promise<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Optional wrapper applied to execute() ONLY on the background
|
|
41
|
+
* stale-revalidation path (not the foreground miss, where the caller's context
|
|
42
|
+
* is already established). Used to re-establish the request-context ALS, which
|
|
43
|
+
* a detached waitUntil task loses on workerd. Defaults to calling execute()
|
|
44
|
+
* directly.
|
|
45
|
+
*/
|
|
46
|
+
wrapBackground?: (run: () => Promise<T>) => Promise<T>;
|
|
39
47
|
/** Serialize result for storage. Return null to skip caching. */
|
|
40
48
|
serialize: (data: T) => Promise<string | null>;
|
|
41
49
|
/** Deserialize cached value back to the original type */
|
|
@@ -77,6 +85,7 @@ export async function readThroughItem<T>(
|
|
|
77
85
|
onMiss,
|
|
78
86
|
onCached,
|
|
79
87
|
host,
|
|
88
|
+
wrapBackground,
|
|
80
89
|
} = config;
|
|
81
90
|
|
|
82
91
|
// Cache lookup. An infra read failure (getItem) is reported by the store
|
|
@@ -106,7 +115,12 @@ export async function readThroughItem<T>(
|
|
|
106
115
|
host,
|
|
107
116
|
async () => {
|
|
108
117
|
try {
|
|
109
|
-
|
|
118
|
+
// Re-establish the caller's context (request-context ALS) for the
|
|
119
|
+
// detached background execution; the foreground miss below calls
|
|
120
|
+
// execute() directly since its context is already established.
|
|
121
|
+
const fresh = await (wrapBackground
|
|
122
|
+
? wrapBackground(execute)
|
|
123
|
+
: execute());
|
|
110
124
|
const serialized = await serialize(fresh);
|
|
111
125
|
if (serialized !== null) {
|
|
112
126
|
await setItem(key, serialized, storeOptions);
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -267,6 +267,12 @@ export function MetaTags(): React.ReactNode {
|
|
|
267
267
|
/>
|
|
268
268
|
)}
|
|
269
269
|
{descriptors.map((descriptor, index) => {
|
|
270
|
+
// A descriptor is only a Promise on the SSR/hydration path, where it is
|
|
271
|
+
// use()d to stream the tag into the document. On client navigation the
|
|
272
|
+
// store resolves deferred handle values before applying them (see
|
|
273
|
+
// processHandles), so MetaTags only ever receives resolved descriptors
|
|
274
|
+
// there and never suspends — which would otherwise revert the committed
|
|
275
|
+
// route (MetaTags lives in <head>, above the route's <Suspense>).
|
|
270
276
|
if (isPromise(descriptor)) {
|
|
271
277
|
return (
|
|
272
278
|
<AsyncMetaTag
|
package/src/index.rsc.ts
CHANGED
|
@@ -65,6 +65,12 @@ export type {
|
|
|
65
65
|
ErrorPhase,
|
|
66
66
|
OnErrorContext,
|
|
67
67
|
OnErrorCallback,
|
|
68
|
+
// View transition types (also exported from the default entry; route
|
|
69
|
+
// definitions resolve to this react-server entry, so they must be here too)
|
|
70
|
+
TransitionConfig,
|
|
71
|
+
TransitionWhenFn,
|
|
72
|
+
TransitionWhenContext,
|
|
73
|
+
ViewTransitionClass,
|
|
68
74
|
} from "./types.js";
|
|
69
75
|
|
|
70
76
|
// Router options type (server-only, so import directly)
|
|
@@ -97,7 +103,6 @@ export {
|
|
|
97
103
|
loading,
|
|
98
104
|
parallel,
|
|
99
105
|
intercept,
|
|
100
|
-
when,
|
|
101
106
|
errorBoundary,
|
|
102
107
|
notFoundBoundary,
|
|
103
108
|
transition,
|
package/src/index.ts
CHANGED
|
@@ -82,7 +82,12 @@ export { createLoader } from "./loader.js";
|
|
|
82
82
|
|
|
83
83
|
// Route definition types (safe to import anywhere)
|
|
84
84
|
export type { RouteHelpers, RouteHandlers } from "./route-definition.js";
|
|
85
|
-
export type {
|
|
85
|
+
export type {
|
|
86
|
+
TransitionConfig,
|
|
87
|
+
TransitionWhenFn,
|
|
88
|
+
TransitionWhenContext,
|
|
89
|
+
ViewTransitionClass,
|
|
90
|
+
} from "./types.js";
|
|
86
91
|
|
|
87
92
|
// Composition types for reusable callback factories
|
|
88
93
|
export type {
|
|
@@ -279,9 +284,6 @@ export function parallel(): never {
|
|
|
279
284
|
export function intercept(): never {
|
|
280
285
|
throw serverOnlyStubError("intercept");
|
|
281
286
|
}
|
|
282
|
-
export function when(): never {
|
|
283
|
-
throw serverOnlyStubError("when");
|
|
284
|
-
}
|
|
285
287
|
export function errorBoundary(): never {
|
|
286
288
|
throw serverOnlyStubError("errorBoundary");
|
|
287
289
|
}
|
package/src/internal-debug.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Runtime fallback for non-Vite contexts (Node, tests, raw imports). In the Vite
|
|
2
|
+
// pipeline -- dev and build, every environment -- the router discovery plugin's
|
|
3
|
+
// transform replaces this module with the build-time-resolved flag (see
|
|
4
|
+
// vite/inject-client-debug.ts), so the CLIENT debug flag no longer depends on a
|
|
5
|
+
// `__RANGO_DEBUG__` define being delivered to the browser (which dev did only as
|
|
6
|
+
// an injected global whose presence varied across consumer setups).
|
|
7
|
+
//
|
|
8
|
+
// Keep this module to the single INTERNAL_RANGO_DEBUG export: the transform
|
|
9
|
+
// replaces the whole file, so a second export would be dropped from the bundle.
|
|
3
10
|
export const INTERNAL_RANGO_DEBUG: boolean =
|
|
4
|
-
typeof
|
|
5
|
-
|
|
6
|
-
: typeof process !== "undefined" &&
|
|
7
|
-
Boolean((process as any).env?.INTERNAL_RANGO_DEBUG);
|
|
8
|
-
|
|
9
|
-
declare const __RANGO_DEBUG__: boolean;
|
|
11
|
+
typeof process !== "undefined" &&
|
|
12
|
+
Boolean((process as any).env?.INTERNAL_RANGO_DEBUG);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface RenderErrorThrowerProps {
|
|
6
|
+
error: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client component that throws the given error during render, so the nearest
|
|
11
|
+
* error boundary catches it. Errors thrown during render are caught by error
|
|
12
|
+
* boundaries; async errors (rejected promises) are not -- which is why the
|
|
13
|
+
* navigation bridge funnels processing failures through this component instead
|
|
14
|
+
* of letting them surface as uncaught rejections.
|
|
15
|
+
*/
|
|
16
|
+
export function RenderErrorThrower({
|
|
17
|
+
error,
|
|
18
|
+
}: RenderErrorThrowerProps): ReactNode {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { Suspense, use } from "react";
|
|
4
|
-
import { invariant } from "./errors";
|
|
5
4
|
import { OutletProvider } from "./outlet-provider.js";
|
|
6
5
|
import type { ResolvedSegment } from "./types.js";
|
|
7
6
|
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
@@ -22,7 +21,9 @@ export function RouteContentWrapper({
|
|
|
22
21
|
fallback,
|
|
23
22
|
segmentId,
|
|
24
23
|
}: {
|
|
25
|
-
|
|
24
|
+
// Normally a pending promise (use() suspends -> fallback). forceAwait paths
|
|
25
|
+
// pass an already-resolved node so Suspender renders it without suspending.
|
|
26
|
+
content: Promise<ReactNode> | ReactNode;
|
|
26
27
|
fallback?: ReactNode;
|
|
27
28
|
segmentId?: string;
|
|
28
29
|
}): ReactNode {
|
|
@@ -41,9 +42,15 @@ const Suspender = ({
|
|
|
41
42
|
}: {
|
|
42
43
|
content: Promise<ReactNode> | ReactNode;
|
|
43
44
|
}): ReactNode => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Normally content is a pending promise -> use() suspends and the wrapping
|
|
46
|
+
// Suspense shows the loading() fallback. forceAwait paths (popstate,
|
|
47
|
+
// stale-revalidation, fully-prefetched nav) instead pass the ALREADY-RESOLVED
|
|
48
|
+
// node so first render does not suspend for a microtask and flash the loading()
|
|
49
|
+
// fallback on a NORMAL (non-transition) commit. The wrapper tree
|
|
50
|
+
// (RouteContentWrapper > Suspense > Suspender) is identical either way, so this
|
|
51
|
+
// preserves tree structure (see docs/tree-structure.md) — only whether use()
|
|
52
|
+
// suspends differs, exactly like LoaderResolver's resolved-data branch.
|
|
53
|
+
return content instanceof Promise ? use(content) : content;
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
/**
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type EntryPropSegments,
|
|
18
18
|
type HelperContext,
|
|
19
19
|
type InterceptEntry,
|
|
20
|
+
type InterceptConfig,
|
|
20
21
|
} from "../server/context";
|
|
21
22
|
import { invariant } from "../errors";
|
|
22
23
|
import { validateUserRouteName } from "../route-name.js";
|
|
@@ -36,7 +37,6 @@ import type {
|
|
|
36
37
|
ErrorBoundaryItem,
|
|
37
38
|
NotFoundBoundaryItem,
|
|
38
39
|
LayoutItem,
|
|
39
|
-
WhenItem,
|
|
40
40
|
CacheItem,
|
|
41
41
|
TransitionItem,
|
|
42
42
|
UseItems,
|
|
@@ -265,34 +265,6 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
|
|
|
265
265
|
return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
|
|
266
266
|
};
|
|
267
267
|
|
|
268
|
-
/**
|
|
269
|
-
* When helper - defines a condition for intercept activation
|
|
270
|
-
*
|
|
271
|
-
* Only valid inside intercept() use() callback. The when() function
|
|
272
|
-
* is captured by the intercept and stored in its `when` array.
|
|
273
|
-
* During soft navigation, all when() conditions must return true
|
|
274
|
-
* for the intercept to activate.
|
|
275
|
-
*/
|
|
276
|
-
const when: RouteHelpers<any, any>["when"] = (fn) => {
|
|
277
|
-
const { store, ctx } = requireDslContext(
|
|
278
|
-
"when() must be called inside intercept()",
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// The when() function needs to be captured by the intercept's tempParent
|
|
282
|
-
// which should have a `when` array. If not present, we're not inside intercept()
|
|
283
|
-
const parent = ctx.parent as any;
|
|
284
|
-
if (!parent || !("when" in parent)) {
|
|
285
|
-
invariant(
|
|
286
|
-
false,
|
|
287
|
-
"when() can only be used inside intercept() use() callback",
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const name = `$${store.getNextIndex("when")}`;
|
|
292
|
-
parent.when.push(fn);
|
|
293
|
-
return { name, type: "when" } as WhenItem;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
268
|
/**
|
|
297
269
|
* Cache helper - defines caching configuration for segments
|
|
298
270
|
*
|
|
@@ -701,8 +673,19 @@ const intercept = (
|
|
|
701
673
|
slotName: `@${string}`,
|
|
702
674
|
routeName: string,
|
|
703
675
|
handler: any,
|
|
676
|
+
configOrUse?: InterceptConfig | (() => any[]),
|
|
704
677
|
use?: () => any[],
|
|
705
678
|
) => {
|
|
679
|
+
// arg4 discrimination: a function is the use() callback (no config); an object
|
|
680
|
+
// is the config carrying `when`. With config given, the use() callback is
|
|
681
|
+
// arg5. Keeps the no-config form intercept(slot, route, handler, () => [...])
|
|
682
|
+
// working unchanged.
|
|
683
|
+
const config: InterceptConfig | undefined =
|
|
684
|
+
typeof configOrUse === "function" || configOrUse == null
|
|
685
|
+
? undefined
|
|
686
|
+
: configOrUse;
|
|
687
|
+
const useFn = typeof configOrUse === "function" ? configOrUse : use;
|
|
688
|
+
|
|
706
689
|
const { store, ctx } = requireDslContext(
|
|
707
690
|
"intercept() must be called inside urls()",
|
|
708
691
|
);
|
|
@@ -740,9 +723,17 @@ const intercept = (
|
|
|
740
723
|
when: [], // Selector conditions for conditional interception
|
|
741
724
|
};
|
|
742
725
|
|
|
726
|
+
// Conditional interception: `when` from the config object — a single selector
|
|
727
|
+
// or an array (ALL must return true to activate). Replaces the former when()
|
|
728
|
+
// use-item captured inside the callback.
|
|
729
|
+
if (config?.when) {
|
|
730
|
+
const selectors = Array.isArray(config.when) ? config.when : [config.when];
|
|
731
|
+
entry.when.push(...selectors);
|
|
732
|
+
}
|
|
733
|
+
|
|
743
734
|
// Merge handler.use defaults with explicit use
|
|
744
735
|
const handlerUseFn = resolveHandlerUse(handler);
|
|
745
|
-
const mergedUse = mergeHandlerUse(handlerUseFn,
|
|
736
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, useFn, "intercept");
|
|
746
737
|
|
|
747
738
|
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
748
739
|
if (mergedUse) {
|
|
@@ -759,7 +750,6 @@ const intercept = (
|
|
|
759
750
|
notFoundBoundary: entry.notFoundBoundary,
|
|
760
751
|
loader: entry.loader,
|
|
761
752
|
layout: capturedLayouts, // Capture layout() calls
|
|
762
|
-
when: entry.when, // Capture when() conditions
|
|
763
753
|
get loading() {
|
|
764
754
|
return entry.loading;
|
|
765
755
|
},
|
|
@@ -1114,7 +1104,6 @@ export {
|
|
|
1114
1104
|
revalidate,
|
|
1115
1105
|
parallel,
|
|
1116
1106
|
intercept,
|
|
1117
|
-
when,
|
|
1118
1107
|
errorBoundary,
|
|
1119
1108
|
notFoundBoundary,
|
|
1120
1109
|
route,
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
revalidate,
|
|
9
9
|
parallel,
|
|
10
10
|
intercept,
|
|
11
|
-
when,
|
|
12
11
|
errorBoundary,
|
|
13
12
|
notFoundBoundary,
|
|
14
13
|
route,
|
|
@@ -40,7 +39,6 @@ function buildRouteHelpers<T extends RouteDefinition, TEnv>(): RouteHelpers<
|
|
|
40
39
|
loading,
|
|
41
40
|
errorBoundary,
|
|
42
41
|
notFoundBoundary,
|
|
43
|
-
when,
|
|
44
42
|
cache,
|
|
45
43
|
transition,
|
|
46
44
|
} as unknown as RouteHelpers<T, TEnv>;
|
|
@@ -29,12 +29,10 @@ import type {
|
|
|
29
29
|
ParallelUseItem,
|
|
30
30
|
InterceptUseItem,
|
|
31
31
|
LoaderUseItem,
|
|
32
|
-
WhenItem,
|
|
33
32
|
CacheItem,
|
|
34
33
|
TransitionItem,
|
|
35
34
|
UseItems,
|
|
36
35
|
} from "../route-types.js";
|
|
37
|
-
import type { InterceptWhenFn } from "../server/context";
|
|
38
36
|
|
|
39
37
|
// Re-export route item types for backward compatibility
|
|
40
38
|
export type {
|
|
@@ -52,12 +50,12 @@ export type {
|
|
|
52
50
|
RouteUseItem,
|
|
53
51
|
ParallelUseItem,
|
|
54
52
|
InterceptUseItem,
|
|
55
|
-
WhenItem,
|
|
56
53
|
CacheItem,
|
|
57
54
|
} from "../route-types.js";
|
|
58
55
|
|
|
59
56
|
// Re-export intercept selector types for use in handlers
|
|
60
57
|
export type {
|
|
58
|
+
InterceptConfig,
|
|
61
59
|
InterceptSelectorContext,
|
|
62
60
|
InterceptSegmentsState,
|
|
63
61
|
InterceptWhenFn,
|
|
@@ -183,10 +181,26 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
183
181
|
* loader(CardModalLoader),
|
|
184
182
|
* revalidate(() => false),
|
|
185
183
|
* ])
|
|
184
|
+
*
|
|
185
|
+
* // Conditional activation via the config object's `when` selector
|
|
186
|
+
* intercept("@modal", "card", <CardModal />, {
|
|
187
|
+
* when: ({ from }) => from.pathname.startsWith("/board"),
|
|
188
|
+
* })
|
|
189
|
+
*
|
|
190
|
+
* // Config + other use-items: config is arg 4, use is arg 5
|
|
191
|
+
* intercept(
|
|
192
|
+
* "@modal",
|
|
193
|
+
* "card",
|
|
194
|
+
* <CardModal />,
|
|
195
|
+
* { when: ({ from }) => from.pathname.startsWith("/board") },
|
|
196
|
+
* () => [loader(CardDetailLoader)],
|
|
197
|
+
* )
|
|
186
198
|
* ```
|
|
187
199
|
* @param slotName - Named slot (prefixed with @) where intercept renders
|
|
188
200
|
* @param routeName - Route name to intercept
|
|
189
201
|
* @param handler - Component or handler for intercepted render
|
|
202
|
+
* @param config - Optional InterceptConfig (e.g. `{ when }`), or the use
|
|
203
|
+
* callback directly when there is no config
|
|
190
204
|
* @param use - Optional callback for loaders, middleware, revalidate, etc.
|
|
191
205
|
*/
|
|
192
206
|
intercept: {
|
|
@@ -195,6 +209,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
195
209
|
slotName: `@${string}`,
|
|
196
210
|
routeName: `.${K}`,
|
|
197
211
|
handler: ReactNode | Handler<ExtractRouteParams<T, K>, {}, TEnv>,
|
|
212
|
+
config?:
|
|
213
|
+
| import("../server/context.js").InterceptConfig<TEnv>
|
|
214
|
+
| (() => UseItems<InterceptUseItem>),
|
|
198
215
|
use?: () => UseItems<InterceptUseItem>,
|
|
199
216
|
): InterceptItem;
|
|
200
217
|
// Global: unprefixed, params inferred from global route map
|
|
@@ -202,6 +219,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
202
219
|
slotName: `@${string}`,
|
|
203
220
|
routeName: K,
|
|
204
221
|
handler: ReactNode | Handler<K, Rango.GeneratedRouteMap, TEnv>,
|
|
222
|
+
config?:
|
|
223
|
+
| import("../server/context.js").InterceptConfig<TEnv>
|
|
224
|
+
| (() => UseItems<InterceptUseItem>),
|
|
205
225
|
use?: () => UseItems<InterceptUseItem>,
|
|
206
226
|
): InterceptItem;
|
|
207
227
|
};
|
|
@@ -347,40 +367,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
347
367
|
notFoundBoundary: (
|
|
348
368
|
fallback: ReactNode | NotFoundBoundaryHandler,
|
|
349
369
|
) => NotFoundBoundaryItem;
|
|
350
|
-
/**
|
|
351
|
-
* Define a condition for when an intercept should activate
|
|
352
|
-
*
|
|
353
|
-
* Only valid inside intercept() use() callback. When multiple when() calls
|
|
354
|
-
* are present, ALL must return true for the intercept to activate.
|
|
355
|
-
* If no when() is defined, the intercept always activates on soft navigation.
|
|
356
|
-
*
|
|
357
|
-
* Context properties:
|
|
358
|
-
* - `from` - Source URL (where user is navigating from)
|
|
359
|
-
* - `to` - Destination URL (where user is navigating to)
|
|
360
|
-
* - `params` - Matched route params
|
|
361
|
-
* - `segments` - Client's current segments with `path` and `ids`
|
|
362
|
-
*
|
|
363
|
-
* ```typescript
|
|
364
|
-
* // Only intercept when coming from the board page
|
|
365
|
-
* intercept("@modal", "card", <CardModal />, () => [
|
|
366
|
-
* when(({ from }) => from.pathname.startsWith("/board")),
|
|
367
|
-
* loader(CardDetailLoader),
|
|
368
|
-
* ])
|
|
369
|
-
*
|
|
370
|
-
* // Use segments to check current route context
|
|
371
|
-
* intercept("@modal", "card", <CardModal />, () => [
|
|
372
|
-
* when(({ segments }) => segments.path[0] === "kanban"),
|
|
373
|
-
* ])
|
|
374
|
-
*
|
|
375
|
-
* // Multiple conditions (AND logic)
|
|
376
|
-
* intercept("@modal", "card", <CardModal />, () => [
|
|
377
|
-
* when(({ from }) => from.pathname.startsWith("/board")),
|
|
378
|
-
* when(({ segments }) => segments.ids.includes("kanban-layout")),
|
|
379
|
-
* ])
|
|
380
|
-
* ```
|
|
381
|
-
* @param fn - Selector function receiving navigation context, returns boolean
|
|
382
|
-
*/
|
|
383
|
-
when: (fn: InterceptWhenFn) => WhenItem;
|
|
384
370
|
/**
|
|
385
371
|
* Define cache configuration for segments
|
|
386
372
|
*
|
|
@@ -472,6 +458,13 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
472
458
|
* transition({}) is startTransition + ViewTransition under the default and
|
|
473
459
|
* startTransition only when the router sets viewTransition: false.
|
|
474
460
|
*
|
|
461
|
+
* Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
|
|
462
|
+
* request. It runs server-side AFTER the route handler (so it can read state
|
|
463
|
+
* the handler set via `ctx.get(...)`); returning false drops this transition
|
|
464
|
+
* for the request, so the navigation streams its loading() skeleton instead of
|
|
465
|
+
* holding. This is a post-handler predicate — distinct from intercept()'s
|
|
466
|
+
* match-time `when` config selector (`intercept(slot, route, Comp, { when })`).
|
|
467
|
+
*
|
|
475
468
|
* ```typescript
|
|
476
469
|
* // Attach to a single route
|
|
477
470
|
* path("/about", AboutPage, { name: "about" }, () => [
|
|
@@ -488,10 +481,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
488
481
|
* path("/product/:id", ProductPage, { name: "product" }, () => [
|
|
489
482
|
* transition({ viewTransition: false }),
|
|
490
483
|
* ])
|
|
484
|
+
*
|
|
485
|
+
* // Hold only when the handler decided to (post-handler predicate):
|
|
486
|
+
* path("/product/:id", ProductPage, { name: "product" }, () => [
|
|
487
|
+
* transition({ when: (ctx) => ctx.get(KeepScroll) === true }),
|
|
488
|
+
* ])
|
|
491
489
|
* ```
|
|
492
490
|
* @param config - ViewTransition configuration (enter, exit, update, share,
|
|
493
|
-
* default, name)
|
|
494
|
-
* boundary (createRouter({ viewTransition }) sets the app-wide default)
|
|
491
|
+
* default, name), `viewTransition: "auto" | false` to toggle the router
|
|
492
|
+
* boundary (createRouter({ viewTransition }) sets the app-wide default), and
|
|
493
|
+
* `when: (ctx) => boolean` to gate the transition per request post-handler
|
|
495
494
|
* @param children - Optional callback returning child routes to wrap
|
|
496
495
|
*/
|
|
497
496
|
transition: {
|
|
@@ -15,8 +15,8 @@ export type {
|
|
|
15
15
|
RouteUseItem,
|
|
16
16
|
ParallelUseItem,
|
|
17
17
|
InterceptUseItem,
|
|
18
|
-
WhenItem,
|
|
19
18
|
CacheItem,
|
|
19
|
+
InterceptConfig,
|
|
20
20
|
InterceptSelectorContext,
|
|
21
21
|
InterceptSegmentsState,
|
|
22
22
|
InterceptWhenFn,
|
|
@@ -30,7 +30,6 @@ export {
|
|
|
30
30
|
revalidate,
|
|
31
31
|
parallel,
|
|
32
32
|
intercept,
|
|
33
|
-
when,
|
|
34
33
|
errorBoundary,
|
|
35
34
|
notFoundBoundary,
|
|
36
35
|
loader,
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type { AllUseItems
|
|
1
|
+
import type { AllUseItems } from "../route-types.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The set of valid use-item `type` discriminants — the single runtime source of
|
|
5
5
|
* truth for "is this a well-formed use item?" shape validation.
|
|
6
6
|
*
|
|
7
7
|
* Declared via a `Record<...>` so that adding a member to the union without
|
|
8
|
-
* updating this map is a compile error.
|
|
9
|
-
* are valid inside intercept() even though WhenItem is not part of AllUseItems
|
|
10
|
-
* (it lives only in InterceptUseItem). This is shape validation only; per-mount-
|
|
8
|
+
* updating this map is a compile error. This is shape validation only; per-mount-
|
|
11
9
|
* site rules remain the narrower hand-written tables in resolve-handler-use.ts.
|
|
12
10
|
*/
|
|
13
|
-
const USE_ITEM_TYPES: Record<AllUseItems["type"]
|
|
11
|
+
const USE_ITEM_TYPES: Record<AllUseItems["type"], true> = {
|
|
14
12
|
layout: true,
|
|
15
13
|
route: true,
|
|
16
14
|
middleware: true,
|
|
@@ -21,7 +19,6 @@ const USE_ITEM_TYPES: Record<AllUseItems["type"] | WhenItem["type"], true> = {
|
|
|
21
19
|
loading: true,
|
|
22
20
|
errorBoundary: true,
|
|
23
21
|
notFoundBoundary: true,
|
|
24
|
-
when: true,
|
|
25
22
|
cache: true,
|
|
26
23
|
transition: true,
|
|
27
24
|
include: true,
|
package/src/route-types.ts
CHANGED
|
@@ -98,10 +98,6 @@ export type NotFoundBoundaryItem = {
|
|
|
98
98
|
type: "notFoundBoundary";
|
|
99
99
|
uses?: AllUseItems[];
|
|
100
100
|
};
|
|
101
|
-
export type WhenItem = {
|
|
102
|
-
name: string;
|
|
103
|
-
type: "when";
|
|
104
|
-
};
|
|
105
101
|
export type CacheItem = {
|
|
106
102
|
name: string;
|
|
107
103
|
type: "cache";
|
|
@@ -231,7 +227,6 @@ export type InterceptUseItem =
|
|
|
231
227
|
| NotFoundBoundaryItem
|
|
232
228
|
| LayoutItem
|
|
233
229
|
| RouteItem
|
|
234
|
-
| WhenItem
|
|
235
230
|
| TransitionItem;
|
|
236
231
|
export type LoaderUseItem = RevalidateItem | CacheItem;
|
|
237
232
|
|
package/src/router/match-api.ts
CHANGED
|
@@ -287,7 +287,11 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
287
287
|
});
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
setRequestContextPrevRouteKey(
|
|
290
|
+
setRequestContextPrevRouteKey(
|
|
291
|
+
nav.effectiveFromMatch?.routeKey,
|
|
292
|
+
nav.effectiveFromUrl,
|
|
293
|
+
nav.effectiveFromMatch?.params ?? nav.prevParams,
|
|
294
|
+
);
|
|
291
295
|
|
|
292
296
|
const interceptSelectorContext: InterceptSelectorContext = {
|
|
293
297
|
from: nav.effectiveFromUrl,
|