@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/vite/index.js +2103 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +13 -8
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +66 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +238 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +33 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +29 -9
- package/src/browser/navigation-client.ts +99 -77
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +60 -40
- package/src/browser/prefetch/cache.ts +196 -49
- package/src/browser/prefetch/fetch.ts +203 -59
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +37 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +75 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +23 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +71 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +10 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +44 -30
- package/src/browser/types.ts +12 -2
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +45 -1
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +17 -5
- package/src/cache/cache-scope.ts +51 -49
- package/src/cache/cf/cf-cache-store.ts +502 -32
- package/src/cache/cf/index.ts +3 -0
- package/src/cache/handle-snapshot.ts +103 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/types.ts +10 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +4 -6
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +17 -8
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +9 -7
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/cache-lookup.ts +57 -95
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +40 -15
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +40 -37
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +51 -35
- package/src/router/router-options.ts +25 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +37 -25
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +58 -77
- package/src/rsc/helpers.ts +72 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +30 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -61
- package/src/rsc/rsc-rendering.ts +45 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +33 -39
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +57 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +17 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +72 -31
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +753 -104
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/cache/cf/index.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
export {
|
|
14
14
|
CFCacheStore,
|
|
15
15
|
type CFCacheStoreOptions,
|
|
16
|
+
type CFCacheDebug,
|
|
17
|
+
type CFCacheReadDebugEvent,
|
|
16
18
|
type KVNamespace,
|
|
17
19
|
} from "./cf-cache-store.js";
|
|
18
20
|
|
|
@@ -20,6 +22,7 @@ export {
|
|
|
20
22
|
export {
|
|
21
23
|
CACHE_STALE_AT_HEADER,
|
|
22
24
|
CACHE_STATUS_HEADER,
|
|
25
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
23
26
|
} from "./cf-cache-store.js";
|
|
24
27
|
|
|
25
28
|
// Internal exports (re-exported for backwards compatibility, marked @internal in source)
|
|
@@ -9,6 +9,109 @@
|
|
|
9
9
|
import type { ResolvedSegment } from "../types.js";
|
|
10
10
|
import type { HandleStore } from "../server/handle-store.js";
|
|
11
11
|
import type { SegmentHandleData } from "./types.js";
|
|
12
|
+
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bound on the background cache-write encode of handle data. A pushed handle
|
|
16
|
+
* value can be a Promise (request-context push-a-promise) or a Promise<ReactNode>
|
|
17
|
+
* (Breadcrumbs content), which the Flight encoder awaits while draining. The
|
|
18
|
+
* encode runs in waitUntil/runBackground, so a never-resolving handle value
|
|
19
|
+
* would otherwise pin a background slot indefinitely; on timeout the entry's
|
|
20
|
+
* handles coalesce to empty rather than hanging or poisoning the whole write.
|
|
21
|
+
*/
|
|
22
|
+
const HANDLE_ENCODE_TIMEOUT_MS = 5000;
|
|
23
|
+
|
|
24
|
+
type HandleRecord = Record<string, SegmentHandleData>;
|
|
25
|
+
|
|
26
|
+
// captureHandles builds a per-segment map keyed by every cached segment id, even
|
|
27
|
+
// segments that pushed nothing (their entry is an empty object). "No handle data"
|
|
28
|
+
// means no segment has any handle, in which case we skip the Flight encode and
|
|
29
|
+
// store an empty string — so the common handle-free route pays neither an encode
|
|
30
|
+
// on write nor a decode on every cache hit.
|
|
31
|
+
function hasHandleData(handles: HandleRecord): boolean {
|
|
32
|
+
for (const segId in handles) {
|
|
33
|
+
for (const _ in handles[segId]) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
|
|
39
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
40
|
+
const timeout = new Promise<T>((resolve) => {
|
|
41
|
+
timer = setTimeout(() => resolve(onTimeout), ms);
|
|
42
|
+
});
|
|
43
|
+
return Promise.race([
|
|
44
|
+
p.then(
|
|
45
|
+
(v) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
return v;
|
|
48
|
+
},
|
|
49
|
+
(e) => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
throw e;
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
timeout,
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Encode a captured handle map to a string for cache storage.
|
|
60
|
+
*
|
|
61
|
+
* Handle values can be Promises or React elements (e.g. Breadcrumbs `content`).
|
|
62
|
+
* JSON.stringify destroys those (Promise -> {}, ReactNode non-representable), so
|
|
63
|
+
* persisting the raw map silently corrupts non-scalar handle values on stores
|
|
64
|
+
* that serialize to JSON (the Cloudflare cache). Routing the map through the same
|
|
65
|
+
* RSC-Flight codec the segments/value already use awaits Promises and serializes
|
|
66
|
+
* React elements, so the stored field is a lossless, JSON-safe string. The
|
|
67
|
+
* in-memory store keeps the same string by reference, so both backends replay
|
|
68
|
+
* identical decoded values.
|
|
69
|
+
*/
|
|
70
|
+
export async function encodeHandles(handles: HandleRecord): Promise<string> {
|
|
71
|
+
// No handle was pushed anywhere — store an empty marker (decoded as "skip").
|
|
72
|
+
if (!hasHandleData(handles)) return "";
|
|
73
|
+
return encodeHandleValue(handles);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Decode a stored handle string back to a handle map. Returns null on any
|
|
78
|
+
* decode failure (e.g. a cross-version entry read under a pinned static
|
|
79
|
+
* version), so the caller can skip handle restore without discarding the
|
|
80
|
+
* otherwise-valid cached segments alongside it.
|
|
81
|
+
*/
|
|
82
|
+
export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
|
|
83
|
+
return decodeHandleValue<HandleRecord>(encoded);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Encode an arbitrary handle-data value to a Flight string. Used directly by the
|
|
88
|
+
* prerender/static pipeline, whose static path holds a single segment's
|
|
89
|
+
* `SegmentHandleData` (not a segId-keyed map). Bounded by the same timeout as
|
|
90
|
+
* encodeHandles; failure/timeout coalesces to "". The caller owns the empty
|
|
91
|
+
* check (an empty value still encodes to a non-empty Flight string, so skip the
|
|
92
|
+
* call when there is nothing to store).
|
|
93
|
+
*/
|
|
94
|
+
export async function encodeHandleValue(value: unknown): Promise<string> {
|
|
95
|
+
const encoded = await withTimeout(
|
|
96
|
+
serializeResult(value),
|
|
97
|
+
HANDLE_ENCODE_TIMEOUT_MS,
|
|
98
|
+
null,
|
|
99
|
+
);
|
|
100
|
+
return encoded ?? "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decode a Flight-encoded handle-data string. Returns null on any decode
|
|
105
|
+
* failure so the caller can skip handle restore without discarding valid
|
|
106
|
+
* cached/prerendered segments.
|
|
107
|
+
*/
|
|
108
|
+
export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
|
|
109
|
+
try {
|
|
110
|
+
return await deserializeResult<T>(encoded);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
12
115
|
|
|
13
116
|
/**
|
|
14
117
|
* Capture handle data for a set of segments from the handle store.
|
package/src/cache/index.ts
CHANGED
|
@@ -29,9 +29,12 @@ export { MemorySegmentCacheStore } from "./memory-segment-store.js";
|
|
|
29
29
|
export {
|
|
30
30
|
CFCacheStore,
|
|
31
31
|
type CFCacheStoreOptions,
|
|
32
|
+
type CFCacheDebug,
|
|
33
|
+
type CFCacheReadDebugEvent,
|
|
32
34
|
type KVNamespace,
|
|
33
35
|
CACHE_STALE_AT_HEADER,
|
|
34
36
|
CACHE_STATUS_HEADER,
|
|
37
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
35
38
|
} from "./cf/index.js";
|
|
36
39
|
|
|
37
40
|
// Cache scope
|
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
CacheGetResult,
|
|
13
13
|
CacheItemResult,
|
|
14
14
|
CacheItemOptions,
|
|
15
|
-
SegmentHandleData,
|
|
16
15
|
} from "./types.js";
|
|
17
16
|
import type { RequestContext } from "../server/request-context.js";
|
|
18
17
|
import {
|
|
@@ -56,7 +55,9 @@ interface CachedResponseEntry {
|
|
|
56
55
|
|
|
57
56
|
interface CachedItemEntry {
|
|
58
57
|
value: string;
|
|
59
|
-
|
|
58
|
+
/** RSC-encoded handle data (see handle-snapshot.ts encodeHandles). Stored as
|
|
59
|
+
* the encoded string by reference, identical to the JSON-serializing stores. */
|
|
60
|
+
handles?: string;
|
|
60
61
|
expiresAt: number;
|
|
61
62
|
staleAt: number;
|
|
62
63
|
}
|
package/src/cache/types.ts
CHANGED
|
@@ -175,8 +175,10 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
175
175
|
export interface CacheItemResult {
|
|
176
176
|
/** RSC-serialized return value */
|
|
177
177
|
value: string;
|
|
178
|
-
/**
|
|
179
|
-
|
|
178
|
+
/** RSC-encoded handle data captured during execution (breadcrumbs, metadata,
|
|
179
|
+
* etc.). Encoded via the Flight codec so Promise/ReactNode handle values
|
|
180
|
+
* survive JSON-serializing stores — see handle-snapshot.ts encodeHandles. */
|
|
181
|
+
handles?: string;
|
|
180
182
|
/** Whether the entry is stale and should be revalidated */
|
|
181
183
|
shouldRevalidate: boolean;
|
|
182
184
|
}
|
|
@@ -185,8 +187,8 @@ export interface CacheItemResult {
|
|
|
185
187
|
* Options for setItem() for function-level caching ("use cache").
|
|
186
188
|
*/
|
|
187
189
|
export interface CacheItemOptions {
|
|
188
|
-
/**
|
|
189
|
-
handles?:
|
|
190
|
+
/** RSC-encoded handle data to store alongside the value (see encodeHandles). */
|
|
191
|
+
handles?: string;
|
|
190
192
|
/** Time-to-live in seconds */
|
|
191
193
|
ttl?: number;
|
|
192
194
|
/** Stale-while-revalidate window in seconds */
|
|
@@ -227,8 +229,10 @@ export interface SerializedSegmentData {
|
|
|
227
229
|
export interface CachedEntryData {
|
|
228
230
|
/** Serialized segments for this entry */
|
|
229
231
|
segments: SerializedSegmentData[];
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
+
/** RSC-encoded handle data keyed by segment ID. Encoded via the Flight codec
|
|
233
|
+
* (see handle-snapshot.ts encodeHandles) so Promise/ReactNode handle values
|
|
234
|
+
* round-trip through JSON-serializing stores instead of being flattened. */
|
|
235
|
+
handles: string;
|
|
232
236
|
/** Expiration timestamp (ms since epoch) */
|
|
233
237
|
expiresAt: number;
|
|
234
238
|
}
|
package/src/client.rsc.tsx
CHANGED
|
@@ -78,6 +78,9 @@ export {
|
|
|
78
78
|
// Re-export useHref - it's a "use client" hook
|
|
79
79
|
export { useHref } from "./browser/react/use-href.js";
|
|
80
80
|
|
|
81
|
+
// Re-export useReverse - it's a "use client" hook
|
|
82
|
+
export { useReverse } from "./browser/react/use-reverse.js";
|
|
83
|
+
|
|
81
84
|
// Re-export useHandle - it's a "use client" hook
|
|
82
85
|
export { useHandle } from "./browser/react/use-handle.js";
|
|
83
86
|
|
package/src/client.tsx
CHANGED
|
@@ -21,6 +21,83 @@ import {
|
|
|
21
21
|
} from "./route-content-wrapper.js";
|
|
22
22
|
import { OutletProvider } from "./outlet-provider.js";
|
|
23
23
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
24
|
+
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render the content for a named parallel/intercept slot segment.
|
|
28
|
+
*
|
|
29
|
+
* Shared by Outlet (with `name` prop) and ParallelOutlet — both resolve a
|
|
30
|
+
* segment from context.parallel by slot name and then render it through the
|
|
31
|
+
* same layout/loader/mountPath wrapping pipeline.
|
|
32
|
+
*/
|
|
33
|
+
function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
|
|
34
|
+
if (!segment) return null;
|
|
35
|
+
|
|
36
|
+
const content: ReactNode =
|
|
37
|
+
segment.loading || segment.component instanceof Promise ? (
|
|
38
|
+
<RouteContentWrapper
|
|
39
|
+
content={getMemoizedContentPromise(segment.component)}
|
|
40
|
+
fallback={segment.loading}
|
|
41
|
+
segmentId={segment.id}
|
|
42
|
+
/>
|
|
43
|
+
) : (
|
|
44
|
+
(segment.component ?? null)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const hasOwnLoaders = !!(segment.loaderDataPromise && segment.loaderIds);
|
|
48
|
+
const loaderWrapped = hasOwnLoaders ? (
|
|
49
|
+
<LoaderBoundary
|
|
50
|
+
loaderDataPromise={segment.loaderDataPromise!}
|
|
51
|
+
loaderIds={segment.loaderIds!}
|
|
52
|
+
fallback={segment.loading}
|
|
53
|
+
outletKey={segment.id + "-loader"}
|
|
54
|
+
outletContent={null}
|
|
55
|
+
segment={segment}
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</LoaderBoundary>
|
|
59
|
+
) : null;
|
|
60
|
+
|
|
61
|
+
let result: ReactNode;
|
|
62
|
+
if (segment.layout) {
|
|
63
|
+
// Layout renders immediately; if loaders exist, the LoaderBoundary becomes
|
|
64
|
+
// the outlet content so layout's <Outlet /> suspends until loaders resolve.
|
|
65
|
+
result = (
|
|
66
|
+
<OutletProvider
|
|
67
|
+
content={hasOwnLoaders ? loaderWrapped : content}
|
|
68
|
+
segment={segment}
|
|
69
|
+
>
|
|
70
|
+
{segment.layout}
|
|
71
|
+
</OutletProvider>
|
|
72
|
+
);
|
|
73
|
+
} else if (hasOwnLoaders) {
|
|
74
|
+
// No layout but has loaders — wrap content with LoaderBoundary for useLoader context.
|
|
75
|
+
// Common for intercept routes that use useLoader without a custom layout.
|
|
76
|
+
result = loaderWrapped;
|
|
77
|
+
} else {
|
|
78
|
+
result = content;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (segment.mountPath) {
|
|
82
|
+
return (
|
|
83
|
+
<MountContextProvider value={segment.mountPath}>
|
|
84
|
+
{result}
|
|
85
|
+
</MountContextProvider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useSlotSegment(
|
|
93
|
+
context: OutletContextValue | null,
|
|
94
|
+
name: `@${string}` | undefined,
|
|
95
|
+
): ResolvedSegment | null {
|
|
96
|
+
return useMemo(() => {
|
|
97
|
+
if (!name || !context?.parallel) return null;
|
|
98
|
+
return context.parallel.find((seg) => seg.slot === name) ?? null;
|
|
99
|
+
}, [context, name]);
|
|
100
|
+
}
|
|
24
101
|
|
|
25
102
|
/**
|
|
26
103
|
* Outlet component - renders child content in layouts
|
|
@@ -61,95 +138,10 @@ import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
|
61
138
|
*/
|
|
62
139
|
export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
63
140
|
const context = useContext(OutletContext);
|
|
141
|
+
const namedSegment = useSlotSegment(context, name);
|
|
64
142
|
|
|
65
|
-
// If name provided, render parallel/intercept content for that slot
|
|
66
143
|
if (name) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!segment) return null;
|
|
70
|
-
|
|
71
|
-
// Determine the content to render
|
|
72
|
-
let content: ReactNode;
|
|
73
|
-
if (segment.loading || segment.component instanceof Promise) {
|
|
74
|
-
// Use RouteContentWrapper to handle Suspense wrapping properly
|
|
75
|
-
content = (
|
|
76
|
-
<RouteContentWrapper
|
|
77
|
-
content={
|
|
78
|
-
segment.component instanceof Promise
|
|
79
|
-
? segment.component
|
|
80
|
-
: Promise.resolve(segment.component)
|
|
81
|
-
}
|
|
82
|
-
fallback={segment.loading}
|
|
83
|
-
segmentId={segment.id}
|
|
84
|
-
/>
|
|
85
|
-
);
|
|
86
|
-
} else {
|
|
87
|
-
content = segment.component ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let result: ReactNode;
|
|
91
|
-
|
|
92
|
-
// If segment has a layout, wrap appropriately
|
|
93
|
-
if (segment.layout) {
|
|
94
|
-
// Check if this segment has loaders that need streaming
|
|
95
|
-
// The layout renders immediately, LoaderBoundary becomes the outlet content
|
|
96
|
-
// When layout renders <Outlet />, it gets the LoaderBoundary which suspends
|
|
97
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
98
|
-
const loaderAwareContent = (
|
|
99
|
-
<LoaderBoundary
|
|
100
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
101
|
-
loaderIds={segment.loaderIds}
|
|
102
|
-
fallback={segment.loading}
|
|
103
|
-
outletKey={segment.id + "-loader"}
|
|
104
|
-
outletContent={null}
|
|
105
|
-
segment={segment}
|
|
106
|
-
>
|
|
107
|
-
{content}
|
|
108
|
-
</LoaderBoundary>
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
result = (
|
|
112
|
-
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
113
|
-
{segment.layout}
|
|
114
|
-
</OutletProvider>
|
|
115
|
-
);
|
|
116
|
-
} else {
|
|
117
|
-
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
118
|
-
result = (
|
|
119
|
-
<OutletProvider content={content} segment={segment}>
|
|
120
|
-
{segment.layout}
|
|
121
|
-
</OutletProvider>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
125
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
126
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
127
|
-
result = (
|
|
128
|
-
<LoaderBoundary
|
|
129
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
130
|
-
loaderIds={segment.loaderIds}
|
|
131
|
-
fallback={segment.loading}
|
|
132
|
-
outletKey={segment.id + "-loader"}
|
|
133
|
-
outletContent={null}
|
|
134
|
-
segment={segment}
|
|
135
|
-
>
|
|
136
|
-
{content}
|
|
137
|
-
</LoaderBoundary>
|
|
138
|
-
);
|
|
139
|
-
} else {
|
|
140
|
-
result = content;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
144
|
-
if (segment.mountPath) {
|
|
145
|
-
return (
|
|
146
|
-
<MountContextProvider value={segment.mountPath}>
|
|
147
|
-
{result}
|
|
148
|
-
</MountContextProvider>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return result;
|
|
144
|
+
return renderSlotContent(namedSegment);
|
|
153
145
|
}
|
|
154
146
|
|
|
155
147
|
// Default: render child content
|
|
@@ -163,6 +155,7 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
163
155
|
|
|
164
156
|
return content;
|
|
165
157
|
}
|
|
158
|
+
|
|
166
159
|
/**
|
|
167
160
|
* ParallelOutlet component - renders content for a named parallel slot
|
|
168
161
|
*
|
|
@@ -187,94 +180,9 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
187
180
|
*/
|
|
188
181
|
export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
189
182
|
const context = useContext(OutletContext);
|
|
190
|
-
const segment =
|
|
191
|
-
if (!context?.parallel) return null;
|
|
192
|
-
return context.parallel.find((seg) => seg.slot === name) ?? null;
|
|
193
|
-
}, [context, name]);
|
|
194
|
-
|
|
195
|
-
if (!segment) return null;
|
|
196
|
-
|
|
197
|
-
// Determine the content to render
|
|
198
|
-
let content: ReactNode;
|
|
199
|
-
if (segment.loading || segment.component instanceof Promise) {
|
|
200
|
-
// Use RouteContentWrapper to handle Suspense wrapping properly
|
|
201
|
-
content = (
|
|
202
|
-
<RouteContentWrapper
|
|
203
|
-
content={
|
|
204
|
-
segment.component instanceof Promise
|
|
205
|
-
? segment.component
|
|
206
|
-
: Promise.resolve(segment.component)
|
|
207
|
-
}
|
|
208
|
-
fallback={segment.loading}
|
|
209
|
-
segmentId={segment.id}
|
|
210
|
-
/>
|
|
211
|
-
);
|
|
212
|
-
} else {
|
|
213
|
-
content = segment.component ?? null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
let result: ReactNode;
|
|
217
|
-
|
|
218
|
-
// If segment has a layout, wrap appropriately
|
|
219
|
-
if (segment.layout) {
|
|
220
|
-
// Check if this segment has loaders that need streaming
|
|
221
|
-
// The layout renders immediately, LoaderBoundary becomes the outlet content
|
|
222
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
223
|
-
const loaderAwareContent = (
|
|
224
|
-
<LoaderBoundary
|
|
225
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
226
|
-
loaderIds={segment.loaderIds}
|
|
227
|
-
fallback={segment.loading}
|
|
228
|
-
outletKey={segment.id + "-loader"}
|
|
229
|
-
outletContent={null}
|
|
230
|
-
segment={segment}
|
|
231
|
-
>
|
|
232
|
-
{content}
|
|
233
|
-
</LoaderBoundary>
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
result = (
|
|
237
|
-
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
238
|
-
{segment.layout}
|
|
239
|
-
</OutletProvider>
|
|
240
|
-
);
|
|
241
|
-
} else {
|
|
242
|
-
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
243
|
-
result = (
|
|
244
|
-
<OutletProvider content={content} segment={segment}>
|
|
245
|
-
{segment.layout}
|
|
246
|
-
</OutletProvider>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
250
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
251
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
252
|
-
result = (
|
|
253
|
-
<LoaderBoundary
|
|
254
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
255
|
-
loaderIds={segment.loaderIds}
|
|
256
|
-
fallback={segment.loading}
|
|
257
|
-
outletKey={segment.id + "-loader"}
|
|
258
|
-
outletContent={null}
|
|
259
|
-
segment={segment}
|
|
260
|
-
>
|
|
261
|
-
{content}
|
|
262
|
-
</LoaderBoundary>
|
|
263
|
-
);
|
|
264
|
-
} else {
|
|
265
|
-
result = content;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
269
|
-
if (segment.mountPath) {
|
|
270
|
-
return (
|
|
271
|
-
<MountContextProvider value={segment.mountPath}>
|
|
272
|
-
{result}
|
|
273
|
-
</MountContextProvider>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
183
|
+
const segment = useSlotSegment(context, name);
|
|
276
184
|
|
|
277
|
-
return
|
|
185
|
+
return renderSlotContent(segment);
|
|
278
186
|
}
|
|
279
187
|
|
|
280
188
|
// OutletProvider is defined in outlet-provider.tsx to break a circular
|
|
@@ -306,6 +214,7 @@ export function useOutlet(): ReactNode {
|
|
|
306
214
|
export {
|
|
307
215
|
useLoader,
|
|
308
216
|
useFetchLoader,
|
|
217
|
+
useRefreshLoaders,
|
|
309
218
|
type LoadFunction,
|
|
310
219
|
type UseLoaderResult,
|
|
311
220
|
type UseFetchLoaderResult,
|
|
@@ -501,37 +410,15 @@ export {
|
|
|
501
410
|
type LocationStateOptions,
|
|
502
411
|
} from "./browser/react/location-state.js";
|
|
503
412
|
|
|
504
|
-
// Type-safe href for client-side path validation
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
type PatternToPath,
|
|
509
|
-
type PathResponse,
|
|
510
|
-
} from "./href-client.js";
|
|
413
|
+
// Type-safe href for client-side path validation. The path and response types
|
|
414
|
+
// are ambient as `Rango.Path` / `Rango.PathResponse` (declared in
|
|
415
|
+
// href-client.ts) — no import needed.
|
|
416
|
+
export { href, type PatternToPath } from "./href-client.js";
|
|
511
417
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
* Type guard for checking if a response envelope contains an error.
|
|
517
|
-
*
|
|
518
|
-
* @example
|
|
519
|
-
* ```typescript
|
|
520
|
-
* const result: ResponseEnvelope<Product> = await fetch(url).then(r => r.json());
|
|
521
|
-
* if (isResponseError(result)) {
|
|
522
|
-
* console.log(result.error.message, result.error.code);
|
|
523
|
-
* return;
|
|
524
|
-
* }
|
|
525
|
-
* result.data // fully typed as Product
|
|
526
|
-
* ```
|
|
527
|
-
*/
|
|
528
|
-
export function isResponseError<T>(
|
|
529
|
-
result: import("./urls.js").ResponseEnvelope<T>,
|
|
530
|
-
): result is import("./urls.js").ResponseEnvelope<T> & {
|
|
531
|
-
error: import("./urls.js").ResponseError;
|
|
532
|
-
} {
|
|
533
|
-
return result.error !== undefined;
|
|
534
|
-
}
|
|
418
|
+
// Problem Details (RFC 9457) error body type for consuming JSON response routes.
|
|
419
|
+
// On a non-2xx response, `await res.json()` yields this shape; on success the
|
|
420
|
+
// body is the bare value (no envelope). Discriminate on `res.ok` / status.
|
|
421
|
+
export type { ProblemDetails } from "./urls.js";
|
|
535
422
|
|
|
536
423
|
// Mount context for include() scoped components
|
|
537
424
|
export { useMount } from "./browser/react/use-mount.js";
|
|
@@ -540,8 +427,12 @@ export { MountContext } from "./browser/react/mount-context.js";
|
|
|
540
427
|
// Mount-aware href hook - auto-prefixes paths with include() mount
|
|
541
428
|
export { useHref } from "./browser/react/use-href.js";
|
|
542
429
|
|
|
430
|
+
// Mount-aware reverse hook - resolves dot-prefixed names against an imported
|
|
431
|
+
// generated routes map (from a urls() module's .gen.ts).
|
|
432
|
+
export { useReverse } from "./browser/react/use-reverse.js";
|
|
433
|
+
|
|
543
434
|
// Type-safe scoped reverse function for scopedReverse<typeof patterns>()
|
|
544
|
-
export type { ScopedReverseFunction } from "./reverse.js";
|
|
435
|
+
export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
|
|
545
436
|
|
|
546
437
|
// Loader definition type - for typing loader props in client components
|
|
547
438
|
export type { LoaderDefinition } from "./types.js";
|
package/src/context-var.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* interface PaginationData { current: number; total: number }
|
|
13
13
|
* export const Pagination = createVar<PaginationData>();
|
|
14
14
|
*
|
|
15
|
-
* // Non-cacheable var — throws
|
|
15
|
+
* // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
|
|
16
16
|
* export const User = createVar<UserData>({ cache: false });
|
|
17
17
|
*
|
|
18
18
|
* // handler
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export interface ContextVar<T> {
|
|
27
27
|
readonly __brand: "context-var";
|
|
28
28
|
readonly key: symbol;
|
|
29
|
-
/** When false,
|
|
29
|
+
/** When false, ctx.get(var) throws inside a cache() boundary. */
|
|
30
30
|
readonly cache: boolean;
|
|
31
31
|
/** Phantom field to carry the type parameter. Never set at runtime. */
|
|
32
32
|
readonly __type?: T;
|
|
@@ -35,9 +35,9 @@ export interface ContextVar<T> {
|
|
|
35
35
|
export interface ContextVarOptions {
|
|
36
36
|
/**
|
|
37
37
|
* When false, marks this variable as non-cacheable.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Reading this var with ctx.get() inside a cache() boundary throws. Use for
|
|
39
|
+
* inherently request-specific data (user sessions, auth tokens, etc.) that
|
|
40
|
+
* must never be baked into cached segments.
|
|
41
41
|
*
|
|
42
42
|
* @default true
|
|
43
43
|
*/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { isLoaderDataResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Shared by segment-system (server) and LoaderResolver (client) so the
|
|
5
|
+
// legacy/ok/error-fallback/throw decode of resolved loader values lives once.
|
|
6
|
+
// Last failing loader wins errorFallback; an error without a fallback throws.
|
|
7
|
+
export function decodeLoaderResults(
|
|
8
|
+
resolvedData: any[],
|
|
9
|
+
loaderIds: string[],
|
|
10
|
+
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
11
|
+
const loaderData: Record<string, any> = {};
|
|
12
|
+
let errorFallback: ReactNode = null;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < loaderIds.length; i++) {
|
|
15
|
+
const id = loaderIds[i];
|
|
16
|
+
const result = resolvedData[i];
|
|
17
|
+
|
|
18
|
+
if (!isLoaderDataResult(result)) {
|
|
19
|
+
loaderData[id] = result;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
loaderData[id] = result.data;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (result.fallback) {
|
|
29
|
+
errorFallback = result.fallback;
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(result.error.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { loaderData, errorFallback };
|
|
36
|
+
}
|