@rangojs/router 0.22.1 → 0.24.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 +10 -4
- 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/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- 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 +24 -10
- 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 +22 -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/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- 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/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- 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 +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- 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 +4 -4
- 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/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- 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 +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -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/request-scope.d.ts +7 -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 +14 -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 +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- 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 +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- 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 +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- 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 +45 -11
- 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-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- 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/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- 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/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -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/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- 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/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -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/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- 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/request-scope.ts +8 -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 +28 -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/expose-internal-ids.ts +2 -3
- 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 +116 -53
- 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,148 @@
|
|
|
1
|
+
import { LOADER_SEGMENT_ID } from "../browser/react/filter-segment-order.js";
|
|
2
|
+
import type { HandleData } from "../server/handle-store.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which run pushed each handle value, so a partial response replaces only the
|
|
6
|
+
* pushes of what it re-ran (#1103).
|
|
7
|
+
*
|
|
8
|
+
* A segment's handle bucket holds its handler's pushes and those of the
|
|
9
|
+
* loaders registered on it: a loader pushes under its owning segment's id
|
|
10
|
+
* (fresh.ts, revalidation.ts pin `_currentSegmentId`). A navigation or an
|
|
11
|
+
* action response decides each of them on its own: the handler re-runs and a
|
|
12
|
+
* loader whose revalidate() answers false keeps its data on the client. The
|
|
13
|
+
* response carries only what re-ran, so replacing the whole bucket with it
|
|
14
|
+
* dropped the pushes of the loader whose data the page still shows (the
|
|
15
|
+
* document title, a breadcrumb), and a loader re-run under a handler that did
|
|
16
|
+
* not re-run took the handler's pushes with it.
|
|
17
|
+
*
|
|
18
|
+
* The record rides in the snapshot itself, beside each handle, under a name
|
|
19
|
+
* no handle has: `data[pushedByKey(h)][segmentId][i]` is the $$id of the
|
|
20
|
+
* loader whose run pushed `data[h][segmentId][i]`, or null for the segment's
|
|
21
|
+
* handler. A bucket without a loader push has no entry. Inside the snapshot it
|
|
22
|
+
* crosses Flight and every clone and history-cache copy with the values;
|
|
23
|
+
* collectHandleData reads a handle by its $$id, so no collect sees it.
|
|
24
|
+
* resolveDeferredHandleValues keeps it aligned when it drops a value.
|
|
25
|
+
*/
|
|
26
|
+
const PUSHED_BY_SUFFIX = "\u0000pushedBy";
|
|
27
|
+
|
|
28
|
+
/** The snapshot key holding `handleName`'s pushedBy record. */
|
|
29
|
+
export function pushedByKey(handleName: string): string {
|
|
30
|
+
return handleName + PUSHED_BY_SUFFIX;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True for a pushedBy record's key, false for a handle's. */
|
|
34
|
+
export function isPushedByKey(name: string): boolean {
|
|
35
|
+
return name.endsWith(PUSHED_BY_SUFFIX);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `data` without its pushedBy records, as a reader outside the router sees it. */
|
|
39
|
+
export function withoutPushedBy(data: HandleData): HandleData {
|
|
40
|
+
const out: HandleData = {};
|
|
41
|
+
for (const name in data) {
|
|
42
|
+
if (!isPushedByKey(name)) out[name] = data[name];
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type PushedBy = readonly (string | null)[] | undefined;
|
|
48
|
+
|
|
49
|
+
const keysOf = (a: object | undefined, b: object | undefined): Set<string> =>
|
|
50
|
+
new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `target` with the buckets of the segments in `ids` taken from `source`.
|
|
54
|
+
* A pushedBy record is a snapshot key like a handle's, so a bucket's values
|
|
55
|
+
* and the record of the runs that pushed them always come from the same
|
|
56
|
+
* snapshot. New containers; the value arrays are shared, as cloneHandleData
|
|
57
|
+
* (navigation-store.ts) shares them.
|
|
58
|
+
*/
|
|
59
|
+
export function withSegmentHandles(
|
|
60
|
+
target: HandleData,
|
|
61
|
+
source: HandleData,
|
|
62
|
+
ids: ReadonlySet<string>,
|
|
63
|
+
): HandleData {
|
|
64
|
+
const out: HandleData = {};
|
|
65
|
+
for (const name of keysOf(target, source)) {
|
|
66
|
+
const buckets: Record<string, unknown[]> = {};
|
|
67
|
+
for (const [id, values] of Object.entries(target[name] ?? {})) {
|
|
68
|
+
if (!ids.has(id)) buckets[id] = values;
|
|
69
|
+
}
|
|
70
|
+
for (const [id, values] of Object.entries(source[name] ?? {})) {
|
|
71
|
+
if (ids.has(id)) buckets[id] = values;
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(buckets).length > 0) out[name] = buckets;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The handle data after a partial response's snapshot `incoming` (each stream
|
|
80
|
+
* yield holds everything the response pushed so far). A segment outside
|
|
81
|
+
* `segmentOrder` loses its buckets. In the others a value stands for the run
|
|
82
|
+
* that pushed it: the segment's handler, or a loader `matched` registers on
|
|
83
|
+
* the segment (`${shortCode}D${index}.${$$id}`, a loader's pushes being filed
|
|
84
|
+
* under its shortCode; an id it does not register, a dependency or a loader a
|
|
85
|
+
* handler read, counts as the handler's). A run that has values in the
|
|
86
|
+
* response, or ran for it (`resolvedIds`: the handlers and loaders that ran),
|
|
87
|
+
* replaces its values with the ones it delivered, nothing when it delivered
|
|
88
|
+
* none. The other runs keep theirs in place, and the response's values take
|
|
89
|
+
* the position of the first value they replace (the end when they replace
|
|
90
|
+
* none). New containers throughout: an object a reader was handed (a history
|
|
91
|
+
* entry's data) is never written to.
|
|
92
|
+
*/
|
|
93
|
+
export function mergePartialHandleData(
|
|
94
|
+
current: HandleData,
|
|
95
|
+
incoming: HandleData,
|
|
96
|
+
segmentOrder: readonly string[],
|
|
97
|
+
matched: readonly string[],
|
|
98
|
+
resolvedIds: readonly string[] | undefined,
|
|
99
|
+
): HandleData {
|
|
100
|
+
// A run's key: a segment's id for its handler, `${segmentId}\0${$$id}` for
|
|
101
|
+
// a loader (its segment id with the `D${index}.` marker replaced).
|
|
102
|
+
const runKey = (id: string): string =>
|
|
103
|
+
id.replace(LOADER_SEGMENT_ID, "\u0000");
|
|
104
|
+
const inOrder = new Set(segmentOrder);
|
|
105
|
+
const registered = new Set(matched.map(runKey));
|
|
106
|
+
const ran = new Set(resolvedIds?.map(runKey));
|
|
107
|
+
const out: HandleData = {};
|
|
108
|
+
for (const name of keysOf(current, incoming)) {
|
|
109
|
+
if (isPushedByKey(name)) continue;
|
|
110
|
+
const byName = pushedByKey(name);
|
|
111
|
+
const buckets: Record<string, unknown[]> = (out[name] = {});
|
|
112
|
+
for (const segmentId of keysOf(current[name], incoming[name])) {
|
|
113
|
+
if (!inOrder.has(segmentId)) continue;
|
|
114
|
+
const run = (by: PushedBy, i: number): string => {
|
|
115
|
+
const key = `${segmentId}\u0000${by?.[i]}`;
|
|
116
|
+
return registered.has(key) ? key : segmentId;
|
|
117
|
+
};
|
|
118
|
+
const currentBy = current[byName]?.[segmentId] as PushedBy;
|
|
119
|
+
const incomingValues = incoming[name]?.[segmentId] ?? [];
|
|
120
|
+
const incomingBy = incoming[byName]?.[segmentId] as PushedBy;
|
|
121
|
+
const delivered = new Set(
|
|
122
|
+
incomingValues.map((_, i) => run(incomingBy, i)),
|
|
123
|
+
);
|
|
124
|
+
const values: unknown[] = [];
|
|
125
|
+
const by: (string | null)[] = [];
|
|
126
|
+
let at = -1;
|
|
127
|
+
(current[name]?.[segmentId] ?? []).forEach((value, i) => {
|
|
128
|
+
const key = run(currentBy, i);
|
|
129
|
+
if (delivered.has(key) || ran.has(key)) {
|
|
130
|
+
if (at < 0) at = values.length;
|
|
131
|
+
} else {
|
|
132
|
+
values.push(value);
|
|
133
|
+
by.push(currentBy?.[i] ?? null);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (at < 0) at = values.length;
|
|
137
|
+
values.splice(at, 0, ...incomingValues);
|
|
138
|
+
by.splice(
|
|
139
|
+
at,
|
|
140
|
+
0,
|
|
141
|
+
...incomingValues.map((_, i) => incomingBy?.[i] ?? null),
|
|
142
|
+
);
|
|
143
|
+
if (values.length > 0) buckets[segmentId] = values;
|
|
144
|
+
if (by.some((run) => run !== null)) (out[byName] ??= {})[segmentId] = by;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
package/src/index.rsc.ts
CHANGED
|
@@ -119,6 +119,14 @@ export {
|
|
|
119
119
|
transition,
|
|
120
120
|
} from "./route-definition.js";
|
|
121
121
|
|
|
122
|
+
// Render another route at the same URL from middleware (throw rewrite()).
|
|
123
|
+
export {
|
|
124
|
+
rewrite,
|
|
125
|
+
type Rewrite,
|
|
126
|
+
type RewriteFunction,
|
|
127
|
+
} from "./route-definition/rewrite.js";
|
|
128
|
+
export { RewriteLoopError } from "./errors.js";
|
|
129
|
+
|
|
122
130
|
// Composition types for reusable callback factories
|
|
123
131
|
export type {
|
|
124
132
|
RouteUseItem,
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,13 @@ export function redirect(): never {
|
|
|
155
155
|
throw serverOnlyStubError("redirect");
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Error-throwing stub for server-only `rewrite` function.
|
|
160
|
+
*/
|
|
161
|
+
export function rewrite(): never {
|
|
162
|
+
throw serverOnlyStubError("rewrite");
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
// Handle API (universal - works on both server and client)
|
|
159
166
|
export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
160
167
|
export {
|
package/src/loader-store.ts
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { unwrapsSynchronously } from "./thenable-status.js";
|
|
39
|
+
import { asStream } from "./browser/defer.js";
|
|
39
40
|
|
|
40
41
|
/** The loader-segment shape announcePendingStreams reads (ResolvedSegment). */
|
|
41
42
|
export interface PendingStreamSource {
|
|
@@ -538,14 +539,23 @@ export class LoaderStore {
|
|
|
538
539
|
this.pendingStreams.set(loaderId, set);
|
|
539
540
|
}
|
|
540
541
|
set.add(stream);
|
|
541
|
-
const release = () =>
|
|
542
|
-
const current = this.pendingStreams.get(loaderId);
|
|
543
|
-
if (!current || !current.delete(stream)) return;
|
|
544
|
-
if (current.size === 0) this.pendingStreams.delete(loaderId);
|
|
545
|
-
};
|
|
542
|
+
const release = () => this.releasePendingStream(loaderId, stream);
|
|
546
543
|
stream.then(release, release);
|
|
547
544
|
}
|
|
548
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Stop tracking a stream. trackPendingStream does it as the stream settles;
|
|
548
|
+
* an abandoned fill's hole (browser/partial-update.ts releaseHoles) never
|
|
549
|
+
* settles, and
|
|
550
|
+
* left tracked it would pin the family's held readers on every later
|
|
551
|
+
* transition.
|
|
552
|
+
*/
|
|
553
|
+
releasePendingStream(loaderId: string, stream: Promise<unknown>): void {
|
|
554
|
+
const current = this.pendingStreams.get(loaderId);
|
|
555
|
+
if (!current || !current.delete(stream)) return;
|
|
556
|
+
if (current.size === 0) this.pendingStreams.delete(loaderId);
|
|
557
|
+
}
|
|
558
|
+
|
|
549
559
|
/** True while any navigation stream for `loaderId` is still pending. */
|
|
550
560
|
isStreamPending(loaderId: string): boolean {
|
|
551
561
|
return this.pendingStreams.has(loaderId);
|
|
@@ -566,12 +576,13 @@ export class LoaderStore {
|
|
|
566
576
|
*/
|
|
567
577
|
announcePendingStreams(segments: readonly PendingStreamSource[]): void {
|
|
568
578
|
for (const segment of segments) {
|
|
579
|
+
const data = asStream(segment.loaderData);
|
|
569
580
|
if (
|
|
570
581
|
segment.type === "loader" &&
|
|
571
582
|
segment.loaderId &&
|
|
572
|
-
|
|
583
|
+
data instanceof Promise
|
|
573
584
|
) {
|
|
574
|
-
this.trackPendingStream(segment.loaderId,
|
|
585
|
+
this.trackPendingStream(segment.loaderId, data);
|
|
575
586
|
}
|
|
576
587
|
}
|
|
577
588
|
for (const loaderId of this.pendingStreams.keys()) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire markers of `prefetch: false` (docs/design/prefetch-false.md):
|
|
3
|
+
* shared by the browser that sends them and the server that reads them
|
|
4
|
+
* (router/segment-resolution/prefetch-deferral.ts requestKind). Dependency
|
|
5
|
+
* free, like segment-fragments.ts: the browser must not import the server
|
|
6
|
+
* module that reads them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
10
|
+
export const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
11
|
+
|
|
12
|
+
/** Query param of a fill request (browser/navigation-client.ts). */
|
|
13
|
+
export const FILL_PARAM = "_rsc_fill";
|
package/src/response-utils.ts
CHANGED
|
@@ -60,3 +60,22 @@ export function isRedirectResponse(response: Response): boolean {
|
|
|
60
60
|
Boolean(response.headers.get("Location"))
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set a header on a response the router hands to the host, skipping a
|
|
66
|
+
* WebSocket upgrade (its headers are not to be touched) and swallowing the
|
|
67
|
+
* TypeError of immutable headers (a passed-through platform Response):
|
|
68
|
+
* instrumentation and debug headers never fail a request.
|
|
69
|
+
*/
|
|
70
|
+
export function trySetHeader(
|
|
71
|
+
response: Response,
|
|
72
|
+
name: string,
|
|
73
|
+
value: string,
|
|
74
|
+
): void {
|
|
75
|
+
if (isWebSocketUpgradeResponse(response)) return;
|
|
76
|
+
try {
|
|
77
|
+
response.headers.set(name, value);
|
|
78
|
+
} catch {
|
|
79
|
+
// Immutable headers: drop the header, never the response.
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/reverse.ts
CHANGED
|
@@ -149,6 +149,7 @@ export type ReverseFunction<TRoutes> = {
|
|
|
149
149
|
export type ScopedReverseFunction<
|
|
150
150
|
TLocalRoutes,
|
|
151
151
|
TGlobalRoutes = TLocalRoutes,
|
|
152
|
+
TResult = string,
|
|
152
153
|
> = {
|
|
153
154
|
/**
|
|
154
155
|
* Global route without params
|
|
@@ -159,7 +160,7 @@ export type ScopedReverseFunction<
|
|
|
159
160
|
> extends true
|
|
160
161
|
? TName
|
|
161
162
|
: never,
|
|
162
|
-
):
|
|
163
|
+
): TResult;
|
|
163
164
|
|
|
164
165
|
/**
|
|
165
166
|
* Global route with params
|
|
@@ -167,7 +168,7 @@ export type ScopedReverseFunction<
|
|
|
167
168
|
<TName extends keyof TGlobalRoutes & string>(
|
|
168
169
|
name: TName,
|
|
169
170
|
params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
|
|
170
|
-
):
|
|
171
|
+
): TResult;
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
174
|
* Global route with params and search
|
|
@@ -176,7 +177,7 @@ export type ScopedReverseFunction<
|
|
|
176
177
|
name: TName,
|
|
177
178
|
params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
|
|
178
179
|
search: ResolveSearchSchema<ExtractSearchSchema<TGlobalRoutes, TName>>,
|
|
179
|
-
):
|
|
180
|
+
): TResult;
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
183
|
* Dot-prefixed local route without params
|
|
@@ -187,7 +188,7 @@ export type ScopedReverseFunction<
|
|
|
187
188
|
> extends true
|
|
188
189
|
? `.${TName}`
|
|
189
190
|
: never,
|
|
190
|
-
):
|
|
191
|
+
): TResult;
|
|
191
192
|
|
|
192
193
|
/**
|
|
193
194
|
* Dot-prefixed local route with params
|
|
@@ -195,7 +196,7 @@ export type ScopedReverseFunction<
|
|
|
195
196
|
<TName extends keyof TLocalRoutes & string>(
|
|
196
197
|
name: `.${TName}`,
|
|
197
198
|
params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
198
|
-
):
|
|
199
|
+
): TResult;
|
|
199
200
|
|
|
200
201
|
/**
|
|
201
202
|
* Dot-prefixed local route with params and search
|
|
@@ -204,7 +205,7 @@ export type ScopedReverseFunction<
|
|
|
204
205
|
name: `.${TName}`,
|
|
205
206
|
params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
206
207
|
search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
|
|
207
|
-
):
|
|
208
|
+
): TResult;
|
|
208
209
|
};
|
|
209
210
|
|
|
210
211
|
/**
|
|
@@ -235,14 +236,16 @@ export type ScopedReverseFunction<
|
|
|
235
236
|
* reverse(".post", { slug: "hello" }) // compile error: no include() scope
|
|
236
237
|
* ```
|
|
237
238
|
*/
|
|
238
|
-
export type GlobalReverseFunction<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
239
|
+
export type GlobalReverseFunction<
|
|
240
|
+
TGlobalRoutes,
|
|
241
|
+
TResult = string,
|
|
242
|
+
> = string extends keyof TGlobalRoutes
|
|
243
|
+
? <TName extends string>(
|
|
244
|
+
name: Exclude<TName, `.${string}`>,
|
|
245
|
+
params?: Record<string, string>,
|
|
246
|
+
search?: Record<string, unknown>,
|
|
247
|
+
) => TResult
|
|
248
|
+
: ScopedReverseFunction<{}, TGlobalRoutes, TResult>;
|
|
246
249
|
|
|
247
250
|
/**
|
|
248
251
|
* Extract local routes type from UrlPatterns
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
LOADER_REDIRECT,
|
|
11
11
|
} from "./decode-loader-results.js";
|
|
12
12
|
import { LoaderRedirect } from "./loader-redirect.js";
|
|
13
|
+
import { Defer, readDefer } from "./browser/defer.js";
|
|
13
14
|
import type { BoundaryAudit } from "./suspense-audit.js";
|
|
14
15
|
import {
|
|
15
16
|
AuditedFallback,
|
|
16
17
|
useBoundaryAudit,
|
|
17
18
|
useRevealAudit,
|
|
18
19
|
} from "./suspense-audit-react.js";
|
|
20
|
+
import { SETTLED_THENABLE } from "./thenable-status.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Router-owned error boundary for read-site loader errors. segment-system
|
|
@@ -141,7 +143,7 @@ export function AuditedRouteContent({
|
|
|
141
143
|
const audit = useBoundaryAudit(
|
|
142
144
|
`content:${segmentId}`,
|
|
143
145
|
segmentId ?? "",
|
|
144
|
-
content,
|
|
146
|
+
content instanceof Defer ? content.promise : content,
|
|
145
147
|
);
|
|
146
148
|
return (
|
|
147
149
|
<Suspense
|
|
@@ -174,8 +176,16 @@ const Suspender = ({
|
|
|
174
176
|
// A pending promise suspends here and the wrapping Suspense shows the
|
|
175
177
|
// loading() fallback; a node renders without suspending. The wrapper tree
|
|
176
178
|
// (RouteContentWrapper > Suspense > Suspender) is identical either way (see
|
|
177
|
-
// docs/tree-structure.md), like LoaderResolver's resolved-data branch.
|
|
178
|
-
|
|
179
|
+
// docs/tree-structure.md), like LoaderResolver's resolved-data branch. A
|
|
180
|
+
// `prefetch: false` hole is read here, by the read rule, so the filled
|
|
181
|
+
// page's node later takes the same place. The content can be a pending
|
|
182
|
+
// promise in one tree and its node in the next: use() at this place on
|
|
183
|
+
// every render, as LoaderResolver does.
|
|
184
|
+
return content instanceof Defer
|
|
185
|
+
? (readDefer(content) as ReactNode)
|
|
186
|
+
: content instanceof Promise
|
|
187
|
+
? use(content)
|
|
188
|
+
: (use(SETTLED_THENABLE), content);
|
|
179
189
|
};
|
|
180
190
|
|
|
181
191
|
/**
|
|
@@ -329,10 +339,14 @@ function LoaderResolver({
|
|
|
329
339
|
);
|
|
330
340
|
}
|
|
331
341
|
|
|
342
|
+
// A slot's aggregate can be a pending promise on one render and its arrived
|
|
343
|
+
// array on the next (segment-loader-promise.ts): use() at this place on
|
|
344
|
+
// every render, or React reports a component that suspended in use() and
|
|
345
|
+
// then finished without it (the useLoader pattern, use-loader.tsx).
|
|
332
346
|
const resolvedData =
|
|
333
347
|
loaderDataPromise instanceof Promise
|
|
334
348
|
? use(loaderDataPromise)
|
|
335
|
-
: loaderDataPromise;
|
|
349
|
+
: (use(SETTLED_THENABLE), loaderDataPromise);
|
|
336
350
|
|
|
337
351
|
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
338
352
|
resolvedData,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type InterceptConfig,
|
|
23
23
|
type LoaderEntry,
|
|
24
24
|
} from "../server/context";
|
|
25
|
-
import { invariant } from "../errors";
|
|
25
|
+
import { assertBooleanOption, invariant } from "../errors";
|
|
26
26
|
import { validateUserRouteName } from "../route-name.js";
|
|
27
27
|
import { isCachedFunction } from "../cache/taint.js";
|
|
28
28
|
import { RangoContext } from "../server/context";
|
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
TransitionItem,
|
|
45
45
|
UseItems,
|
|
46
46
|
} from "../route-types.js";
|
|
47
|
+
import { isRenderableLoading } from "../types/boundaries.js";
|
|
47
48
|
import type { RouteHelpers } from "./helpers-types.js";
|
|
48
49
|
import {
|
|
49
50
|
resolveHandlerUse,
|
|
@@ -899,9 +900,15 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
899
900
|
(optionsGiven as { stream?: unknown } | undefined)?.stream === undefined,
|
|
900
901
|
"loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
|
|
901
902
|
);
|
|
902
|
-
|
|
903
|
-
optionsGiven?.ssr
|
|
904
|
-
|
|
903
|
+
assertBooleanOption(
|
|
904
|
+
optionsGiven?.ssr,
|
|
905
|
+
"loader() ssr",
|
|
906
|
+
"stream on every render",
|
|
907
|
+
);
|
|
908
|
+
assertBooleanOption(
|
|
909
|
+
optionsGiven?.prefetch,
|
|
910
|
+
"loader() prefetch",
|
|
911
|
+
"run the loader in prefetches",
|
|
905
912
|
);
|
|
906
913
|
|
|
907
914
|
const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
|
|
@@ -917,6 +924,7 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
917
924
|
...(optionsGiven?.ssr === false && { bake: true as const }),
|
|
918
925
|
...(optionsGiven?.ssr === false &&
|
|
919
926
|
ctx.isSSR && { awaitBeforeFlush: true as const }),
|
|
927
|
+
...(optionsGiven?.prefetch === false && { prefetch: false as const }),
|
|
920
928
|
};
|
|
921
929
|
|
|
922
930
|
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
@@ -969,6 +977,12 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
969
977
|
invariant(false, "No parent entry available for loading()");
|
|
970
978
|
}
|
|
971
979
|
|
|
980
|
+
assertBooleanOption(
|
|
981
|
+
options?.prefetch,
|
|
982
|
+
"loading() prefetch",
|
|
983
|
+
"render what the fallback covers in prefetches",
|
|
984
|
+
);
|
|
985
|
+
|
|
972
986
|
// Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
|
|
973
987
|
const resolved =
|
|
974
988
|
typeof component === "function" ? (component as () => any)() : component;
|
|
@@ -980,6 +994,15 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
980
994
|
parent.loading = resolved;
|
|
981
995
|
}
|
|
982
996
|
|
|
997
|
+
// The flag means "what this fallback covers": without a fallback to show
|
|
998
|
+
// there is nothing to defer behind. Recorded from `resolved`, not
|
|
999
|
+
// `parent.loading`, so the document evaluation (ssr: false) agrees with
|
|
1000
|
+
// the navigation one. Last loading() wins, like `loading` itself.
|
|
1001
|
+
parent.loadingPrefetch =
|
|
1002
|
+
options?.prefetch === false && isRenderableLoading(resolved)
|
|
1003
|
+
? false
|
|
1004
|
+
: undefined;
|
|
1005
|
+
|
|
983
1006
|
const name = `$${store.getNextIndex("loading")}`;
|
|
984
1007
|
return { name, type: "loading" } as LoadingItem;
|
|
985
1008
|
};
|
|
@@ -328,6 +328,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
328
328
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
329
329
|
* ```
|
|
330
330
|
*
|
|
331
|
+
* Pass `{ prefetch: false }` to keep the loader out of prefetches: a
|
|
332
|
+
* prefetch skips it and the click that adopts the prefetch fetches it in
|
|
333
|
+
* one follow-up request (see {@link LoaderOptions}). Only on a segment the
|
|
334
|
+
* client does not have yet: a loader the client already holds runs in the
|
|
335
|
+
* prefetch when it revalidates.
|
|
336
|
+
*
|
|
337
|
+
* ```typescript
|
|
338
|
+
* loader(ReviewsLoader, { prefetch: false }),
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
331
341
|
* @param loaderDef - Loader created with createLoader()
|
|
332
342
|
* @param optionsOrUse - Delivery options, or the use() callback when passing none
|
|
333
343
|
* @param use - Optional callback for loader-specific revalidation rules
|
|
@@ -345,14 +355,30 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
345
355
|
*
|
|
346
356
|
* // Skip loading on SSR, only show on client navigation
|
|
347
357
|
* loading(<Skeleton />, { ssr: false })
|
|
358
|
+
*
|
|
359
|
+
* // Keep everything behind this fallback out of prefetches
|
|
360
|
+
* loading(<Skeleton />, { prefetch: false })
|
|
348
361
|
* ```
|
|
349
362
|
* @param component - The loading UI to show during navigation
|
|
350
363
|
* @param options - Configuration options
|
|
351
364
|
* @param options.ssr - If false, skip showing loading on document requests (SSR)
|
|
365
|
+
* @param options.prefetch - If false, the flag applies when this entry's
|
|
366
|
+
* segment is new to the client. A prefetch then skips the whole unit: the
|
|
367
|
+
* handler, its loaders, its orphan layouts and parallel slots and, for a
|
|
368
|
+
* layout, every deeper entry; the click that adopts the prefetch shows
|
|
369
|
+
* this fallback and fetches them in one follow-up request, which reads
|
|
370
|
+
* cache() and "use cache" as any request does (a prefetch reads neither
|
|
371
|
+
* for skipped work). A segment the client already holds is never
|
|
372
|
+
* deferred: a prefetch renders it as usual when it revalidates, and what
|
|
373
|
+
* is below it follows its own flags. To keep a child out of prefetches
|
|
374
|
+
* while moving inside a section, flag the child's own loading(). An entry
|
|
375
|
+
* whose handler output is stored (cache(), ppr, Prerender/Static) is
|
|
376
|
+
* served as usual and only its loaders are skipped. Loaders behind the
|
|
377
|
+
* fallback cannot call ctx.rendered().
|
|
352
378
|
*/
|
|
353
379
|
loading: (
|
|
354
380
|
component: ReactNode | (() => ReactNode),
|
|
355
|
-
options?: { ssr?: boolean },
|
|
381
|
+
options?: { ssr?: boolean; prefetch?: boolean },
|
|
356
382
|
) => LoadingItem;
|
|
357
383
|
/**
|
|
358
384
|
* Attach an error boundary to catch errors in this segment and children
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
attachRedirectState,
|
|
9
9
|
markExternalRedirect,
|
|
10
10
|
} from "../redirect-origin.js";
|
|
11
|
+
import { prefixBasename } from "../router/basename.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Create a soft redirect Response for middleware short-circuit
|
|
@@ -84,21 +85,7 @@ export function redirect(
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// Auto-prefix root-relative URLs with basename for app-local redirects.
|
|
87
|
-
|
|
88
|
-
// separator, a query, a fragment, or end-of-string, so "/admin?tab=x" and
|
|
89
|
-
// "/admin#frag" are not double-prefixed into "/admin/admin?tab=x".
|
|
90
|
-
const bn = _getRequestContext()?._basename;
|
|
91
|
-
let resolvedUrl = url;
|
|
92
|
-
if (
|
|
93
|
-
bn &&
|
|
94
|
-
url.startsWith("/") &&
|
|
95
|
-
url !== bn &&
|
|
96
|
-
!url.startsWith(bn + "/") &&
|
|
97
|
-
!url.startsWith(bn + "?") &&
|
|
98
|
-
!url.startsWith(bn + "#")
|
|
99
|
-
) {
|
|
100
|
-
resolvedUrl = url === "/" ? bn : bn + url;
|
|
101
|
-
}
|
|
88
|
+
const resolvedUrl = prefixBasename(url, _getRequestContext()?._basename);
|
|
102
89
|
|
|
103
90
|
const headers: Record<string, string> = {
|
|
104
91
|
Location: resolvedUrl,
|