@rangojs/router 0.22.1 → 0.23.0
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 +8 -3
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +19 -9
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +16 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +14 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -3
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +38 -0
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +11 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +37 -10
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache.ts +22 -1
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +20 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +93 -32
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
projectClientUrlsModule,
|
|
3
|
+
setClientUrlProjection,
|
|
4
|
+
type ClientUrlProjection,
|
|
5
|
+
} from "../../client-urls/server-projection.js";
|
|
6
|
+
import { isHoistedRevalidateRef } from "../../client-urls/hoisted-revalidate.js";
|
|
7
|
+
import { registerClientUrlRevalidators } from "../../client-urls/revalidate-registry.js";
|
|
8
|
+
import type { ClientUrlPatterns } from "../../client-urls/types.js";
|
|
9
|
+
import type { ShouldRevalidateFn } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Install the projection of a `"use client"` clientUrls() module under its
|
|
13
|
+
* client reference id, as discovery installs it for an app
|
|
14
|
+
* (discoverClientUrlProjections in vite/discovery/client-urls-projection.ts):
|
|
15
|
+
* the same projection step, into the same registry include() materializes
|
|
16
|
+
* from. rangoUseClientTransform (testing/vitest.ts) appends a call to every
|
|
17
|
+
* such module, passing the module's own source evaluated without the
|
|
18
|
+
* transform, so a router under test mounts the group with no setup.
|
|
19
|
+
*
|
|
20
|
+
* A Vitest project runs no hoist plugin, so the source's revalidate()
|
|
21
|
+
* predicates are plain functions. Each is registered under a key named for
|
|
22
|
+
* its reference, route and loader, as the plugin's generated module registers
|
|
23
|
+
* its own, and the projection carries the keys: materialization attaches them
|
|
24
|
+
* and the server runs them, as in an app.
|
|
25
|
+
*/
|
|
26
|
+
export function installClientUrlsProjection(
|
|
27
|
+
referenceId: string,
|
|
28
|
+
source: Readonly<Record<string, unknown>>,
|
|
29
|
+
): void {
|
|
30
|
+
const projection = projectClientUrlsModule(source);
|
|
31
|
+
if (!projection) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Cannot install the client URL projection for reference "${referenceId}": ` +
|
|
34
|
+
'the module default export must be created by clientUrls() and have __brand === "client-urls"',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
setClientUrlProjection(
|
|
38
|
+
referenceId,
|
|
39
|
+
withRevalidateKeys(
|
|
40
|
+
referenceId,
|
|
41
|
+
source.default as ClientUrlPatterns,
|
|
42
|
+
projection,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function withRevalidateKeys(
|
|
48
|
+
referenceId: string,
|
|
49
|
+
definition: ClientUrlPatterns,
|
|
50
|
+
projection: ClientUrlProjection,
|
|
51
|
+
): ClientUrlProjection {
|
|
52
|
+
const fns: Record<string, ShouldRevalidateFn<any, any>> = {};
|
|
53
|
+
const routes = projection.routes.map((route) => {
|
|
54
|
+
const record = definition.routes.find(({ id }) => id === route.id);
|
|
55
|
+
const loaderRevalidate = (record?.loaders ?? []).map(
|
|
56
|
+
({ revalidate }, loaderIndex) =>
|
|
57
|
+
revalidate.map((entry, index) => {
|
|
58
|
+
if (isHoistedRevalidateRef(entry)) return entry.$$rangoRevalidate;
|
|
59
|
+
const key = `${referenceId}:${route.id}:${loaderIndex}:${index}`;
|
|
60
|
+
fns[key] = entry;
|
|
61
|
+
return key;
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
return loaderRevalidate.some((keys) => keys.length > 0)
|
|
65
|
+
? { ...route, loaderRevalidate }
|
|
66
|
+
: route;
|
|
67
|
+
});
|
|
68
|
+
registerClientUrlRevalidators(fns);
|
|
69
|
+
return { ...projection, routes };
|
|
70
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as RSDServer from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
|
|
2
|
+
import { registerServerModule } from "./server-modules.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register `action` under its reference id, `<module id>#<export name>`,
|
|
6
|
+
* tagged as a server reference like a built "use server" export so it keeps
|
|
7
|
+
* its `$$id`, where the plugin-rsc stub's decodeAction / loadServerAction
|
|
8
|
+
* resolve it. `primitive` names the caller in the error for a malformed id.
|
|
9
|
+
* Loads the react-server build: callers run under the react-server condition.
|
|
10
|
+
*/
|
|
11
|
+
export function registerTestAction(
|
|
12
|
+
action: (...args: any[]) => unknown,
|
|
13
|
+
id: string,
|
|
14
|
+
primitive: string,
|
|
15
|
+
): void {
|
|
16
|
+
const [moduleId, name] = id.split("#");
|
|
17
|
+
if (!moduleId || !name) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`${primitive}: id must be "<module id>#<export name>", received ${JSON.stringify(id)}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
registerServerModule(moduleId, {
|
|
23
|
+
[name]: RSDServer.registerServerReference(action, moduleId, name),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* - `ctx.reverse()` throws unless `routeMap` is provided (it does NOT fall back
|
|
21
21
|
* to the global route map — that would leak whichever routes another test
|
|
22
22
|
* registered).
|
|
23
|
-
* - `ctx.
|
|
23
|
+
* - `ctx.get(handle)` follows the production rule: reading a handle before
|
|
24
24
|
* `await ctx.rendered()` throws (pass `rendered` to mock the barrier).
|
|
25
|
+
* `ctx.use(handle)` is the push and needs no barrier.
|
|
25
26
|
* - `use cache` functions only cache (and only fire their taint/profile guards)
|
|
26
27
|
* when a `cacheStore` is provided — without one, registerCachedFunction
|
|
27
28
|
* bypasses (it checks for a store first). Pass `cacheStore`/`cacheProfiles`
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runRevalidate — unit-test revalidate() predicates: a urls() segment's, or a
|
|
3
|
+
* clientUrls() loader's (which the build moves to the server, where it runs
|
|
4
|
+
* the same way).
|
|
5
|
+
*
|
|
6
|
+
* Evaluates the predicate(s) through the router's own evaluateRevalidation
|
|
7
|
+
* (router/revalidation.ts) inside a seeded request context: the default
|
|
8
|
+
* decision, the argument object, boolean short-circuit, soft-verdict
|
|
9
|
+
* threading and the fail-open on a throw are the production code paths, not
|
|
10
|
+
* a re-implementation. Pass an array to test a chain in declaration order.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { evaluateRevalidation } from "../router/revalidation.js";
|
|
14
|
+
import { runWithRequestContext } from "../server/request-context.js";
|
|
15
|
+
import type { ResolvedSegment, ShouldRevalidateFn } from "../types.js";
|
|
16
|
+
import {
|
|
17
|
+
normalizeTestAction,
|
|
18
|
+
resolveTestActionId,
|
|
19
|
+
type ImportedAction,
|
|
20
|
+
} from "./internal/action-id.js";
|
|
21
|
+
import { createTestRequestContext } from "./internal/context.js";
|
|
22
|
+
import type { VarsInit } from "./internal/seed-vars.js";
|
|
23
|
+
import { toURL } from "./to-url.js";
|
|
24
|
+
|
|
25
|
+
type RevalidateArgs = Parameters<ShouldRevalidateFn<any, any>>[0];
|
|
26
|
+
|
|
27
|
+
/** The action fields a predicate receives on an action request. */
|
|
28
|
+
export interface RunRevalidateAction {
|
|
29
|
+
/** The triggering action: an imported server action or its actionId. */
|
|
30
|
+
ref?: ImportedAction | string;
|
|
31
|
+
/** `actionResult`: the action's return value. */
|
|
32
|
+
result?: unknown;
|
|
33
|
+
/** `formData`: the action's FormData argument. */
|
|
34
|
+
formData?: FormData;
|
|
35
|
+
/** `actionUrl`. Defaults to `currentUrl`. */
|
|
36
|
+
url?: string | URL;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options for {@link runRevalidate}. Defaults model a same-URL navigation
|
|
41
|
+
* with no action, for a route-owned loader segment.
|
|
42
|
+
*/
|
|
43
|
+
export interface RunRevalidateOptions<TEnv = any> {
|
|
44
|
+
/** The page being left. Defaults to `http://localhost/`. */
|
|
45
|
+
currentUrl?: string | URL;
|
|
46
|
+
/** The destination. Defaults to `currentUrl`. */
|
|
47
|
+
nextUrl?: string | URL;
|
|
48
|
+
currentParams?: Record<string, string>;
|
|
49
|
+
/** Defaults to `currentParams`. */
|
|
50
|
+
nextParams?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* The triggering action: an imported server action (id via
|
|
53
|
+
* `$id ?? $$id`), an actionId string, or `{ ref, result, formData, url }`.
|
|
54
|
+
* Makes the request a POST, as an action request is.
|
|
55
|
+
*/
|
|
56
|
+
action?: ImportedAction | string | RunRevalidateAction;
|
|
57
|
+
/** Overrides the request method (`POST` with `action`, else `GET`). */
|
|
58
|
+
method?: string;
|
|
59
|
+
/** The segment the predicate is attached to. Defaults to `"loader"`. */
|
|
60
|
+
segmentType?: RevalidateArgs["segmentType"];
|
|
61
|
+
/**
|
|
62
|
+
* The segment belongs to the matched route (a route's own loaders, a
|
|
63
|
+
* clientUrls() group loader). Defaults to true; false models a layout's
|
|
64
|
+
* loader, which a navigation keeps by default.
|
|
65
|
+
*/
|
|
66
|
+
belongsToRoute?: boolean;
|
|
67
|
+
/** `stale`: a history-entry background revalidation. */
|
|
68
|
+
stale?: boolean;
|
|
69
|
+
/** `toRouteName` (and `routeName`). */
|
|
70
|
+
routeName?: string;
|
|
71
|
+
/** `fromRouteName`. */
|
|
72
|
+
fromRouteName?: string;
|
|
73
|
+
/** Environment bindings of the seeded context. */
|
|
74
|
+
env?: TEnv;
|
|
75
|
+
/** Variables a middleware would have set, readable with `context.get()`. */
|
|
76
|
+
vars?: VarsInit;
|
|
77
|
+
/** `context`, in place of the seeded request context. */
|
|
78
|
+
context?: RevalidateArgs["context"];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DEFAULT_URL = "http://localhost/";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run one revalidate() predicate, or a chain in declaration order, the way
|
|
85
|
+
* the router does for a request. Resolves to the final decision: true
|
|
86
|
+
* re-runs the segment, false keeps the client's copy.
|
|
87
|
+
*/
|
|
88
|
+
export async function runRevalidate<TEnv = any>(
|
|
89
|
+
fn: ShouldRevalidateFn<any, TEnv> | readonly ShouldRevalidateFn<any, TEnv>[],
|
|
90
|
+
opts: RunRevalidateOptions<TEnv> = {},
|
|
91
|
+
): Promise<boolean> {
|
|
92
|
+
const currentUrl = toURL(opts.currentUrl, new URL(DEFAULT_URL));
|
|
93
|
+
const nextUrl = toURL(opts.nextUrl, currentUrl);
|
|
94
|
+
const currentParams = opts.currentParams ?? {};
|
|
95
|
+
const nextParams = opts.nextParams ?? currentParams;
|
|
96
|
+
const action = normalizeTestAction(opts.action);
|
|
97
|
+
const method = opts.method ?? (action ? "POST" : "GET");
|
|
98
|
+
const request = new Request(nextUrl, { method });
|
|
99
|
+
const { ctx } = createTestRequestContext<TEnv>({
|
|
100
|
+
env: opts.env,
|
|
101
|
+
request,
|
|
102
|
+
vars: opts.vars,
|
|
103
|
+
params: nextParams,
|
|
104
|
+
});
|
|
105
|
+
if (opts.fromRouteName !== undefined) ctx._prevRouteKey = opts.fromRouteName;
|
|
106
|
+
|
|
107
|
+
const segment: ResolvedSegment = {
|
|
108
|
+
id: "runRevalidate",
|
|
109
|
+
namespace: "runRevalidate",
|
|
110
|
+
type: opts.segmentType ?? "loader",
|
|
111
|
+
index: 0,
|
|
112
|
+
component: null,
|
|
113
|
+
params: nextParams,
|
|
114
|
+
belongsToRoute: opts.belongsToRoute ?? true,
|
|
115
|
+
};
|
|
116
|
+
const fns = Array.isArray(fn) ? fn : [fn];
|
|
117
|
+
return runWithRequestContext(ctx as never, () =>
|
|
118
|
+
evaluateRevalidation({
|
|
119
|
+
segment,
|
|
120
|
+
prevParams: currentParams,
|
|
121
|
+
getPrevSegment: null,
|
|
122
|
+
request,
|
|
123
|
+
prevUrl: currentUrl,
|
|
124
|
+
nextUrl,
|
|
125
|
+
revalidations: fns.map((entry, index) => ({
|
|
126
|
+
name: `runRevalidate#${index}`,
|
|
127
|
+
fn: entry,
|
|
128
|
+
})),
|
|
129
|
+
routeKey: opts.routeName ?? "",
|
|
130
|
+
context: (opts.context ?? ctx) as RevalidateArgs["context"],
|
|
131
|
+
actionContext: action
|
|
132
|
+
? {
|
|
133
|
+
actionId: resolveTestActionId(action.ref, "runRevalidate") ?? "",
|
|
134
|
+
actionUrl: toURL(action.url, currentUrl),
|
|
135
|
+
actionResult: action.result,
|
|
136
|
+
formData: action.formData,
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
stale: opts.stale ?? false,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
resolveNavigationState,
|
|
23
23
|
} from "../browser/history-state.js";
|
|
24
24
|
import { isLocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
normalizeTestAction,
|
|
27
|
+
resolveTestActionId,
|
|
28
|
+
type ImportedAction,
|
|
29
|
+
} from "./internal/action-id.js";
|
|
26
30
|
import type {
|
|
27
31
|
RouteLocation,
|
|
28
32
|
TransitionConfig,
|
|
@@ -31,8 +35,6 @@ import type {
|
|
|
31
35
|
TransitionWhenKind,
|
|
32
36
|
} from "../types/segments.js";
|
|
33
37
|
|
|
34
|
-
type ImportedAction = (...args: never[]) => unknown;
|
|
35
|
-
|
|
36
38
|
/** A navigation side: a URL, or a partial RouteLocation. */
|
|
37
39
|
export type RunTransitionWhenLocation =
|
|
38
40
|
| string
|
|
@@ -58,7 +60,7 @@ export interface RunTransitionWhenOptions {
|
|
|
58
60
|
/** The destination. Defaults to `from`; always `from` for "action" and "revalidate". */
|
|
59
61
|
to?: RunTransitionWhenLocation;
|
|
60
62
|
/**
|
|
61
|
-
* The triggering action (like
|
|
63
|
+
* The triggering action (like runRevalidate's): an imported server
|
|
62
64
|
* action (id via `$id ?? $$id`), an actionId string, or
|
|
63
65
|
* `{ ref, formData, result, error }`.
|
|
64
66
|
*/
|
|
@@ -105,10 +107,7 @@ export function runTransitionWhen(
|
|
|
105
107
|
whenOrConfig: TransitionWhenFn | TransitionConfig,
|
|
106
108
|
opts: RunTransitionWhenOptions = {},
|
|
107
109
|
): RunTransitionWhenResult {
|
|
108
|
-
const actionOption =
|
|
109
|
-
typeof opts.action === "function" || typeof opts.action === "string"
|
|
110
|
-
? { ref: opts.action }
|
|
111
|
-
: opts.action;
|
|
110
|
+
const actionOption = normalizeTestAction(opts.action);
|
|
112
111
|
const from = toLocation(opts.from, DEFAULT_URL);
|
|
113
112
|
const context = createTransitionWhenContext({
|
|
114
113
|
kind: opts.kind ?? (actionOption ? "action" : "push"),
|
|
@@ -57,7 +57,9 @@ import {
|
|
|
57
57
|
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
58
58
|
} from "../rsc/shell-capture-constants.js";
|
|
59
59
|
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../segment-fragments.js";
|
|
60
|
+
import { FILL_PARAM, PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
60
61
|
import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
|
|
62
|
+
import { withoutPushedBy } from "../handles/pushed-by.js";
|
|
61
63
|
import type { HandleData } from "../server/handle-store.js";
|
|
62
64
|
import { _getRequestContext } from "../server/request-context.js";
|
|
63
65
|
import {
|
|
@@ -88,8 +90,22 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
88
90
|
* of a document GET: from `from` (default `/`) with `segments` mounted
|
|
89
91
|
* (default none), advertising fragment passthrough. The replay decision is
|
|
90
92
|
* `result.replayStatus`.
|
|
93
|
+
*
|
|
94
|
+
* `prefetch: true` sends it as a `<Link>` prefetch does (the
|
|
95
|
+
* `X-Rango-Prefetch` header), so `prefetch: false` loaders and `loading()`
|
|
96
|
+
* entries are deferred: `result.readDeferred()` lists them. `fill: true`
|
|
97
|
+
* sends the request the browser follows an adopted prefetch with
|
|
98
|
+
* (`_rsc_fill`): pass the ids the prefetch delivered, without the deferred
|
|
99
|
+
* ones, as `segments`, and the same URL as `from`.
|
|
91
100
|
*/
|
|
92
|
-
partial?:
|
|
101
|
+
partial?:
|
|
102
|
+
| true
|
|
103
|
+
| {
|
|
104
|
+
from?: string;
|
|
105
|
+
segments?: readonly string[];
|
|
106
|
+
prefetch?: boolean;
|
|
107
|
+
fill?: boolean;
|
|
108
|
+
};
|
|
93
109
|
/**
|
|
94
110
|
* POST this form as a submit before hydration: the no-JS request the
|
|
95
111
|
* progressive-enhancement path runs the action for and re-renders the page
|
|
@@ -98,6 +114,30 @@ export interface ServeShellRequestOptions<TEnv = any> {
|
|
|
98
114
|
* Not with `partial`.
|
|
99
115
|
*/
|
|
100
116
|
form?: FormData;
|
|
117
|
+
/**
|
|
118
|
+
* Send the request a JS server action call makes from the page at `url`:
|
|
119
|
+
* a POST of `args` (encoded as the browser encodes them) for `ref`, with
|
|
120
|
+
* the page's mounted `segments` (default none), as
|
|
121
|
+
* browser/server-action-bridge.ts sends it. The response is the action's
|
|
122
|
+
* Flight payload (its return value and the re-rendered segments).
|
|
123
|
+
* Not with `partial` or `form`.
|
|
124
|
+
*/
|
|
125
|
+
action?: ServeShellRequestAction;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The JS server action call of {@link ServeShellRequestOptions.action}. */
|
|
129
|
+
export interface ServeShellRequestAction {
|
|
130
|
+
/** The server action. */
|
|
131
|
+
ref: (...args: any[]) => unknown;
|
|
132
|
+
/**
|
|
133
|
+
* Its reference id, `<module id>#<export name>`, as the build gives a
|
|
134
|
+
* "use server" export; registered so the handler loads `ref` by it.
|
|
135
|
+
*/
|
|
136
|
+
id: string;
|
|
137
|
+
/** The arguments the page calls it with. */
|
|
138
|
+
args?: readonly unknown[];
|
|
139
|
+
/** The segment ids the page holds (`_rsc_segments`). */
|
|
140
|
+
segments?: readonly string[];
|
|
101
141
|
}
|
|
102
142
|
|
|
103
143
|
/** A response's handle data, as the browser consumes it. */
|
|
@@ -157,6 +197,12 @@ export interface ServeShellRequestResult {
|
|
|
157
197
|
* values resolved. Undefined when no Flight was rendered.
|
|
158
198
|
*/
|
|
159
199
|
readHandles(): Promise<ShellRequestHandles | undefined>;
|
|
200
|
+
/**
|
|
201
|
+
* The ids of the segments `flight` marks deferred (a prefetch skipped
|
|
202
|
+
* them; the fill request renders them), in payload order. Empty when
|
|
203
|
+
* nothing was deferred, undefined when no Flight was rendered.
|
|
204
|
+
*/
|
|
205
|
+
readDeferred(): Promise<string[] | undefined>;
|
|
160
206
|
/**
|
|
161
207
|
* The form state the SSR render received (the `formState` render option),
|
|
162
208
|
* or undefined when it received none.
|
|
@@ -297,9 +343,22 @@ async function decodePayloadHandles(
|
|
|
297
343
|
for await (const data of metadata?.handles ?? []) hydration = data;
|
|
298
344
|
const late: HandleData[] = [];
|
|
299
345
|
for await (const data of metadata?.handlesLate ?? []) {
|
|
300
|
-
late.push(await resolveDeferredHandleValues(data));
|
|
346
|
+
late.push(withoutPushedBy(await resolveDeferredHandleValues(data)));
|
|
301
347
|
}
|
|
302
|
-
return {
|
|
348
|
+
return {
|
|
349
|
+
hydration: withoutPushedBy(await resolveDeferredHandleValues(hydration)),
|
|
350
|
+
late,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The ids of the segments a payload marks deferred. */
|
|
355
|
+
async function decodeDeferredIds(flight: string): Promise<string[]> {
|
|
356
|
+
const { metadata } = await decodePayload<{
|
|
357
|
+
metadata?: { segments?: Array<{ id: string; deferred?: boolean }> };
|
|
358
|
+
}>(flight);
|
|
359
|
+
return (metadata?.segments ?? [])
|
|
360
|
+
.filter((segment) => segment.deferred === true)
|
|
361
|
+
.map((segment) => segment.id);
|
|
303
362
|
}
|
|
304
363
|
|
|
305
364
|
/**
|
|
@@ -435,9 +494,46 @@ async function getHandler(
|
|
|
435
494
|
return handler;
|
|
436
495
|
}
|
|
437
496
|
|
|
438
|
-
function
|
|
497
|
+
async function buildActionRequest(
|
|
498
|
+
url: URL,
|
|
499
|
+
headers: Headers,
|
|
500
|
+
action: ServeShellRequestAction,
|
|
501
|
+
): Promise<Request> {
|
|
502
|
+
const [{ registerTestAction }] = await Promise.all([
|
|
503
|
+
import("./internal/test-action.js"),
|
|
504
|
+
import("./internal/flight-client-globals.js"),
|
|
505
|
+
]);
|
|
506
|
+
const { encodeReply } =
|
|
507
|
+
await import("@vitejs/plugin-rsc/vendor/react-server-dom/client.edge");
|
|
508
|
+
registerTestAction(action.ref, action.id, "serveShellRequest");
|
|
509
|
+
const target = new URL(url);
|
|
510
|
+
target.searchParams.set("_rsc_action", action.id);
|
|
511
|
+
target.searchParams.set("_rsc_segments", (action.segments ?? []).join(","));
|
|
512
|
+
headers.set("rsc-action", action.id);
|
|
513
|
+
if (!headers.has("X-RSC-Router-Client-Path")) {
|
|
514
|
+
headers.set("X-RSC-Router-Client-Path", url.href);
|
|
515
|
+
}
|
|
516
|
+
return new Request(target, {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers,
|
|
519
|
+
body: await encodeReply([...(action.args ?? [])]),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function buildRequest(
|
|
524
|
+
url: URL,
|
|
525
|
+
options: ServeShellRequestOptions,
|
|
526
|
+
): Promise<Request> {
|
|
439
527
|
const headers = new Headers(options.headers);
|
|
440
528
|
const partial = options.partial;
|
|
529
|
+
if (options.action) {
|
|
530
|
+
if (partial || options.form) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"serveShellRequest: action cannot be combined with partial or form",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return buildActionRequest(url, headers, options.action);
|
|
536
|
+
}
|
|
441
537
|
if (options.form) {
|
|
442
538
|
if (partial) {
|
|
443
539
|
throw new Error(
|
|
@@ -454,10 +550,21 @@ function buildRequest(url: URL, options: ServeShellRequestOptions): Request {
|
|
|
454
550
|
if (!headers.has("accept")) headers.set("accept", "text/html");
|
|
455
551
|
return new Request(url, { headers });
|
|
456
552
|
}
|
|
457
|
-
const {
|
|
553
|
+
const {
|
|
554
|
+
from = "/",
|
|
555
|
+
segments = [],
|
|
556
|
+
prefetch = false,
|
|
557
|
+
fill = false,
|
|
558
|
+
} = partial === true ? {} : partial;
|
|
458
559
|
const target = new URL(url);
|
|
459
560
|
target.searchParams.set("_rsc_partial", "true");
|
|
460
561
|
target.searchParams.set("_rsc_segments", segments.join(","));
|
|
562
|
+
// As the browser sends them: browser/navigation-client.ts (fill) and
|
|
563
|
+
// browser/prefetch/fetch.ts (prefetch).
|
|
564
|
+
if (fill) target.searchParams.set(FILL_PARAM, "1");
|
|
565
|
+
if (prefetch && !headers.has(PREFETCH_HEADER)) {
|
|
566
|
+
headers.set(PREFETCH_HEADER, "1");
|
|
567
|
+
}
|
|
461
568
|
if (!headers.has("X-RSC-Router-Client-Path")) {
|
|
462
569
|
headers.set("X-RSC-Router-Client-Path", new URL(from, url).href);
|
|
463
570
|
}
|
|
@@ -536,7 +643,7 @@ export async function serveShellRequest<TEnv = any>(
|
|
|
536
643
|
const target = new URL(url, "http://localhost");
|
|
537
644
|
await ensureRouterManifest(router.id);
|
|
538
645
|
const handler = await getHandler(router, options.cacheStore);
|
|
539
|
-
const request = buildRequest(target, options);
|
|
646
|
+
const request = await buildRequest(target, options);
|
|
540
647
|
const executionContext = createCollectingExecutionContext();
|
|
541
648
|
const { tasks } = executionContext;
|
|
542
649
|
const recorder: Recorder = {};
|
|
@@ -594,6 +701,9 @@ export async function serveShellRequest<TEnv = any>(
|
|
|
594
701
|
async readHandles() {
|
|
595
702
|
return flight ? decodePayloadHandles(flight) : undefined;
|
|
596
703
|
},
|
|
704
|
+
async readDeferred() {
|
|
705
|
+
return flight ? decodeDeferredIds(flight) : undefined;
|
|
706
|
+
},
|
|
597
707
|
formState: recorder.formState,
|
|
598
708
|
async readPayloadFormState() {
|
|
599
709
|
if (!flight) return undefined;
|
package/src/testing/vitest.ts
CHANGED
|
@@ -139,6 +139,37 @@ function resolveFromRouter(spec: string): string | undefined {
|
|
|
139
139
|
const rsdServerEdgePath: string | undefined =
|
|
140
140
|
resolveFromRouter(RSD_SERVER_EDGE_SPEC);
|
|
141
141
|
|
|
142
|
+
/** Marks the import of a clientUrls() module's untransformed source. */
|
|
143
|
+
const CLIENT_URLS_SOURCE_QUERY = "rango-client-urls-source";
|
|
144
|
+
|
|
145
|
+
/** `@rangojs/router/client` under the react-server condition. */
|
|
146
|
+
const CLIENT_ENTRY_RSC_PATH: string = here("src/client.rsc.tsx").replaceAll(
|
|
147
|
+
"\\",
|
|
148
|
+
"/",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Imported by absolute path so it shares the registry instance the router
|
|
153
|
+
* source under test materializes include() from.
|
|
154
|
+
*/
|
|
155
|
+
const CLIENT_URLS_INSTALLER_PATH: string = here(
|
|
156
|
+
"src/testing/internal/client-urls-projection.ts",
|
|
157
|
+
).replaceAll("\\", "/");
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Appended to a transformed `"use client"` clientUrls() module: install the
|
|
161
|
+
* projection of its own source under its default export's reference id
|
|
162
|
+
* (`<id>#default`, as registerClientReference composes it).
|
|
163
|
+
*/
|
|
164
|
+
function clientUrlsProjectionInstall(id: string): string {
|
|
165
|
+
const source = `${id}${id.includes("?") ? "&" : "?"}${CLIENT_URLS_SOURCE_QUERY}`;
|
|
166
|
+
return (
|
|
167
|
+
`\nimport * as $$RangoClientUrlsSource from ${JSON.stringify(source)};\n` +
|
|
168
|
+
`import { installClientUrlsProjection as $$rangoInstallClientUrlsProjection } from ${JSON.stringify(CLIENT_URLS_INSTALLER_PATH)};\n` +
|
|
169
|
+
`$$rangoInstallClientUrlsProjection(${JSON.stringify(`${id}#default`)}, $$RangoClientUrlsSource);\n`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
142
173
|
/**
|
|
143
174
|
* Build the `resolve.alias` entries a consumer's node/DOM Vitest project needs to
|
|
144
175
|
* import a real @rangojs/router app's router/loaders/middleware. Spread into a
|
|
@@ -241,6 +272,10 @@ export function rangoTestConfig(
|
|
|
241
272
|
/** A minimal Vite plugin shape (avoids a hard dependency on Vite's types). */
|
|
242
273
|
interface FlightTransformPlugin {
|
|
243
274
|
name: string;
|
|
275
|
+
resolveId: {
|
|
276
|
+
order: "pre";
|
|
277
|
+
handler(source: string, importer: string | undefined): string | undefined;
|
|
278
|
+
};
|
|
244
279
|
transform(
|
|
245
280
|
code: string,
|
|
246
281
|
id: string,
|
|
@@ -298,12 +333,39 @@ interface FlightTransformPlugin {
|
|
|
298
333
|
* by the module's absolute path (the boundary id), the export name becoming the
|
|
299
334
|
* boundary name. Modules without the directive (server components) are untouched,
|
|
300
335
|
* so `renderToFlightString` of pure leaf trees is unaffected.
|
|
336
|
+
*
|
|
337
|
+
* A `"use client"` module whose default export is a `clientUrls()` definition
|
|
338
|
+
* also installs its server projection, as the rango plugin's discovery does for
|
|
339
|
+
* an app: the module imports its own source without this transform and projects
|
|
340
|
+
* it under its client reference. A router that mounts the group
|
|
341
|
+
* (`include("/shop", shopUrls)` or `.routes(shopUrls)`) then serves its routes
|
|
342
|
+
* through `serveShellRequest` and `dispatch` with no setup.
|
|
301
343
|
*/
|
|
302
344
|
export function rangoUseClientTransform(): FlightTransformPlugin {
|
|
303
345
|
return {
|
|
304
346
|
name: "rango:testing-use-client",
|
|
347
|
+
resolveId: {
|
|
348
|
+
// Before Vite's resolver, which resolves the bare specifier first.
|
|
349
|
+
order: "pre",
|
|
350
|
+
handler(source, importer) {
|
|
351
|
+
// A clientUrls() module's source resolves the router's client entry
|
|
352
|
+
// as the RSC environment does (the react-server condition): this
|
|
353
|
+
// project resolves it to client.tsx, a "use client" module whose
|
|
354
|
+
// clientUrls is a client reference here. Both entries export the
|
|
355
|
+
// same clientUrls.
|
|
356
|
+
if (
|
|
357
|
+
source === "@rangojs/router/client" &&
|
|
358
|
+
importer?.includes(CLIENT_URLS_SOURCE_QUERY)
|
|
359
|
+
) {
|
|
360
|
+
return CLIENT_ENTRY_RSC_PATH;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
},
|
|
364
|
+
},
|
|
305
365
|
async transform(code, id) {
|
|
306
366
|
if (id.includes("/node_modules/")) return undefined;
|
|
367
|
+
// A clientUrls() module's own source, imported by its transformed copy.
|
|
368
|
+
if (id.includes(CLIENT_URLS_SOURCE_QUERY)) return undefined;
|
|
307
369
|
// Fast path: only parse modules that mention the directive.
|
|
308
370
|
if (!code.includes("use client")) return undefined;
|
|
309
371
|
const { parseAstAsync } = await import("vite");
|
|
@@ -329,7 +391,7 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
|
|
|
329
391
|
`${JSON.stringify(id)}, ${JSON.stringify(name)})`,
|
|
330
392
|
});
|
|
331
393
|
if (!result) return undefined;
|
|
332
|
-
const { output } = result;
|
|
394
|
+
const { output, exportNames } = result;
|
|
333
395
|
// Absolute file URL, not the bare specifier: the consumer's "use client"
|
|
334
396
|
// module cannot resolve @vitejs/plugin-rsc (it is a router dependency).
|
|
335
397
|
const rsdHref = rsdServerEdgePath
|
|
@@ -338,6 +400,10 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
|
|
|
338
400
|
output.prepend(
|
|
339
401
|
`import * as $$RangoRSD from ${JSON.stringify(rsdHref)};\n`,
|
|
340
402
|
);
|
|
403
|
+
// The modules discovery records (recordClientUrlsModule).
|
|
404
|
+
if (code.includes("clientUrls(") && exportNames.includes("default")) {
|
|
405
|
+
output.append(clientUrlsProjectionInstall(id));
|
|
406
|
+
}
|
|
341
407
|
return {
|
|
342
408
|
code: output.toString(),
|
|
343
409
|
map: output.generateMap({ hires: true }),
|
package/src/types/boundaries.ts
CHANGED
|
@@ -104,6 +104,18 @@ export function isLoaderDataResult(value: unknown): value is LoaderDataResult {
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* A loading skeleton is renderable only when it is a real ReactNode value;
|
|
109
|
+
* `false` counts as "not renderable". This is the three-term gate. The
|
|
110
|
+
* distinct two-term gate at the LoaderBoundary site (segment-system.tsx)
|
|
111
|
+
* deliberately treats `false` as "create a boundary without a
|
|
112
|
+
* RouteContentWrapper" (tree-structure.md), so it must NOT use this helper.
|
|
113
|
+
* Leaf module: shared by client (segment-system) and server (DSL, resolution).
|
|
114
|
+
*/
|
|
115
|
+
export function isRenderableLoading(loading: unknown): boolean {
|
|
116
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
export interface NotFoundInfo {
|
|
108
120
|
message: string;
|
|
109
121
|
segmentId: string;
|
|
@@ -492,6 +492,20 @@ export type InternalHandlerContext<
|
|
|
492
492
|
* setupLoaderAccess.
|
|
493
493
|
*/
|
|
494
494
|
_bindLoaderCacheTags?: (loaderId: string, tags: Set<string>) => void;
|
|
495
|
+
/**
|
|
496
|
+
* @internal This match's `prefetch: false` plan: what a prefetch defers,
|
|
497
|
+
* whether the request is a fill, and the loaders that cannot call
|
|
498
|
+
* ctx.rendered(). Set by match-api.ts when the tree declares a flag or the
|
|
499
|
+
* request is a fill. See router/segment-resolution/prefetch-deferral.ts.
|
|
500
|
+
*/
|
|
501
|
+
_prefetchDeferral?: import("../router/segment-resolution/prefetch-deferral.js").PrefetchDeferral;
|
|
502
|
+
/**
|
|
503
|
+
* @internal Whether a loader was started in this request: a run of it (a
|
|
504
|
+
* handler's or another loader's ctx.use()) or its cache() binding. A
|
|
505
|
+
* prefetch delivers such a loader instead of deferring it. See
|
|
506
|
+
* setupLoaderAccess.
|
|
507
|
+
*/
|
|
508
|
+
_loaderStarted?: (loaderId: string) => boolean;
|
|
495
509
|
};
|
|
496
510
|
|
|
497
511
|
/**
|
|
@@ -200,6 +200,36 @@ export type LoaderFn<
|
|
|
200
200
|
*/
|
|
201
201
|
export type LoaderOptions = {
|
|
202
202
|
ssr?: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* `prefetch: false` keeps this loader out of prefetches. A `<Link>`
|
|
205
|
+
* prefetch (a partial request with `X-Rango-Prefetch`) does not execute it;
|
|
206
|
+
* the payload marks it deferred. A document request and a navigation with
|
|
207
|
+
* no prefetch to adopt run it as usual.
|
|
208
|
+
*
|
|
209
|
+
* A deferred loader never blocks the navigation that adopts the prefetch:
|
|
210
|
+
* it behaves like a loader that is still streaming. Its own `<Suspense>`
|
|
211
|
+
* shows at once; if the nearest boundary is the route's `loading()`, that
|
|
212
|
+
* shows; with no boundary React holds the page until the fill returns. The
|
|
213
|
+
* fill is the one follow-up request that runs the loader.
|
|
214
|
+
*
|
|
215
|
+
* The fill reads through the loader's own `cache()` and any `"use cache"`
|
|
216
|
+
* function it calls. A prefetch reads neither, by intent: looking would
|
|
217
|
+
* mean running the loader on a miss.
|
|
218
|
+
*
|
|
219
|
+
* The flag applies only to a segment the client does not have yet. A
|
|
220
|
+
* loader the client already holds (the same route with new params, say)
|
|
221
|
+
* runs in the prefetch when it revalidates, exactly as without the flag:
|
|
222
|
+
* a click that adopts a prefetch is never worse than the same click with
|
|
223
|
+
* no prefetch at all.
|
|
224
|
+
*
|
|
225
|
+
* Limits: a handler that awaits the loader with `ctx.use()` makes it run in
|
|
226
|
+
* the prefetch anyway; the loader cannot call `ctx.rendered()` (it throws
|
|
227
|
+
* on every request); on a `ppr` route the flag is ignored for an
|
|
228
|
+
* `ssr: false` loader (the bake lane), with a dev warning. In the fill
|
|
229
|
+
* request only deferred work and middleware run, so values a held handler
|
|
230
|
+
* would have set with `ctx.set()` are not visible.
|
|
231
|
+
*/
|
|
232
|
+
prefetch?: boolean;
|
|
203
233
|
};
|
|
204
234
|
|
|
205
235
|
/**
|