@rangojs/router 0.7.0 → 0.9.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/types/cache/cf/cf-cache-store.d.ts +30 -16
- package/dist/types/cache/shell-snapshot.d.ts +2 -2
- package/dist/types/cache/types.d.ts +26 -7
- package/dist/types/client-urls/server-projection.d.ts +4 -4
- package/dist/types/client-urls/types.d.ts +13 -12
- package/dist/types/route-definition/helpers-types.d.ts +6 -5
- package/dist/types/router/segment-resolution/loader-mask.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +14 -4
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/server/context.d.ts +1 -1
- package/dist/types/server/request-context.d.ts +1 -1
- package/dist/types/ssr/index.d.ts +16 -0
- package/dist/types/ssr/ssr-root.d.ts +5 -0
- package/dist/types/types/loader-types.d.ts +24 -22
- package/dist/types/urls/path-helper-types.d.ts +8 -7
- package/dist/vite/index.js +8 -5
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +2 -2
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +9 -8
- package/skills/cloudflare/SKILL.md +5 -3
- package/skills/comparison/references/framework-comparison.md +2 -2
- package/skills/hooks/data.md +1 -1
- package/skills/hooks/handle-and-actions.md +1 -1
- package/skills/loader/SKILL.md +29 -28
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/migrate-react-router/component-migration.md +1 -1
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/migrate-react-router/route-mapping.md +1 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +12 -8
- package/skills/rango/SKILL.md +20 -20
- package/skills/router-setup/SKILL.md +1 -1
- package/skills/scripts/SKILL.md +1 -1
- package/skills/shell-manifest/SKILL.md +1 -1
- package/src/browser/react/Link.tsx +27 -4
- package/src/cache/cf/cf-cache-store.ts +118 -62
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/types.ts +27 -7
- package/src/client-urls/client-urls.ts +12 -9
- package/src/client-urls/server-projection.ts +6 -6
- package/src/client-urls/types.ts +13 -12
- package/src/route-definition/dsl-helpers.ts +7 -3
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/router/loader-resolution.ts +3 -3
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/loader-cache.ts +1 -1
- package/src/router/segment-resolution/loader-mask.ts +1 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/shell-build-manifest.ts +13 -6
- package/src/rsc/shell-capture.ts +32 -5
- package/src/rsc/ssr-setup.ts +4 -0
- package/src/rsc/types.ts +18 -2
- package/src/server/context.ts +1 -1
- package/src/server/request-context.ts +1 -1
- package/src/ssr/index.tsx +22 -2
- package/src/ssr/ssr-root.tsx +17 -1
- package/src/types/loader-types.ts +21 -19
- package/src/urls/path-helper-types.ts +8 -7
|
@@ -30,8 +30,6 @@ import type { KVNamespace, CFCacheReadDebugEvent, CFCacheDebug, CFCacheStoreOpti
|
|
|
30
30
|
export type { KVNamespace, CFCacheReadDebugEvent, CFCacheDebug, CFCacheStoreOptions, };
|
|
31
31
|
export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
32
32
|
readonly supportsPassiveShellReads: true;
|
|
33
|
-
/** True when constructed without KV: the shell family no-ops (see ctor). */
|
|
34
|
-
readonly shellFamilyInert?: boolean;
|
|
35
33
|
readonly defaults?: CacheDefaults;
|
|
36
34
|
readonly keyGenerator?: (ctx: RequestContext<TEnv>, defaultKey: string) => string | Promise<string>;
|
|
37
35
|
private readonly namespace?;
|
|
@@ -43,6 +41,8 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
43
41
|
private readonly kvReadTimeoutMs;
|
|
44
42
|
private readonly debug?;
|
|
45
43
|
private readonly kv?;
|
|
44
|
+
/** True when constructed without KV: no durable tag history (see ctor). */
|
|
45
|
+
readonly tagHistoryInert?: boolean;
|
|
46
46
|
private readonly onRevalidateTag?;
|
|
47
47
|
private readonly tagPurge?;
|
|
48
48
|
private readonly tagInvalidationTtl?;
|
|
@@ -257,15 +257,26 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
257
257
|
*/
|
|
258
258
|
setItem(key: string, value: string, options?: CacheItemOptions): Promise<void>;
|
|
259
259
|
/**
|
|
260
|
-
* Warn once per isolate that
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
260
|
+
* Warn once per isolate that a TAGGED shell landed on a store with no
|
|
261
|
+
* eviction path for it: no KV (markers) and no tagPurge (purge-by-tag).
|
|
262
|
+
* The shell still caches and expires by ttl+swr, but updateTag()/
|
|
263
|
+
* revalidateTag() cannot reach it — silent staleness a consumer who tagged
|
|
264
|
+
* the route clearly did not intend. Fired from putShell (not the
|
|
265
|
+
* constructor) so an untagged edge-only ppr config stays warning-free.
|
|
266
266
|
* @internal
|
|
267
267
|
*/
|
|
268
|
-
private
|
|
268
|
+
private warnShellTagsNoEvictionOnce;
|
|
269
|
+
/**
|
|
270
|
+
* Generation gate for shell writes and the capture scheduler
|
|
271
|
+
* (isTagsInvalidatedSince). With KV it is the durable marker cascade.
|
|
272
|
+
* Without KV there are no markers: in purge mode the per-request memo is
|
|
273
|
+
* the only signal — a capture racing THIS request's updateTag() is still
|
|
274
|
+
* rejected (read-your-own-writes), while cross-request races are bounded
|
|
275
|
+
* by ttl+swr exactly like the data families' purge-mode writes. Without
|
|
276
|
+
* either, fail open (ttl/swr-only semantics, warned at putShell).
|
|
277
|
+
* @internal
|
|
278
|
+
*/
|
|
279
|
+
private isShellGenerationInvalidated;
|
|
269
280
|
/**
|
|
270
281
|
* Get a cached PPR shell entry from Cache API, falling through to KV and
|
|
271
282
|
* promoting a valid KV hit. Both tiers store one envelope so the prelude,
|
|
@@ -278,13 +289,14 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
278
289
|
shouldRevalidate?: boolean;
|
|
279
290
|
} | null>;
|
|
280
291
|
/**
|
|
281
|
-
* Store a PPR shell envelope in Cache API and, when
|
|
282
|
-
* 60-second floor, KV. The shared write is
|
|
283
|
-
* awaited so invalidation rejection can be
|
|
284
|
-
* scheduler. Short-lived shells remain useful
|
|
285
|
-
* them
|
|
292
|
+
* Store a PPR shell envelope in Cache API and, when KV is configured and
|
|
293
|
+
* the retention meets its 60-second floor, KV. The shared write is
|
|
294
|
+
* registered with waitUntil and awaited so invalidation rejection can be
|
|
295
|
+
* acknowledged to the capture scheduler. Short-lived shells remain useful
|
|
296
|
+
* in L1 even though KV rejects them; a KV-less store is L1-only by design
|
|
297
|
+
* (edge-only ppr — see the section comment).
|
|
286
298
|
*/
|
|
287
|
-
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
|
|
299
|
+
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | "uncacheable" | void>;
|
|
288
300
|
/** Rebuild the public shell entry from its validated storage envelope. */
|
|
289
301
|
private shellEnvelopeToEntry;
|
|
290
302
|
/** Build the Cache API representation of the coupled shell envelope. */
|
|
@@ -569,7 +581,9 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
|
|
|
569
581
|
* Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
|
|
570
582
|
* SAME KV markers used by runtime envelopes also evict immutable build shells
|
|
571
583
|
* and captures whose write races updateTag(). Thin public wrapper over the
|
|
572
|
-
*
|
|
584
|
+
* shell generation check (marker >= since, fail open); KV-less it degrades
|
|
585
|
+
* to the per-request memo in purge mode and to false otherwise — see
|
|
586
|
+
* isShellGenerationInvalidated.
|
|
573
587
|
*/
|
|
574
588
|
isTagsInvalidatedSince(tags: string[], sinceMs: number): Promise<boolean>;
|
|
575
589
|
invalidateTags(tags: string[]): Promise<void>;
|
|
@@ -90,7 +90,7 @@ export declare class RecordingShellStore<TEnv = unknown> implements SegmentCache
|
|
|
90
90
|
entry: ShellCacheEntry;
|
|
91
91
|
shouldRevalidate?: boolean;
|
|
92
92
|
} | null>;
|
|
93
|
-
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
|
|
93
|
+
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | "uncacheable" | void>;
|
|
94
94
|
invalidateTags(tags: string[]): Promise<void>;
|
|
95
95
|
}
|
|
96
96
|
/** True iff `store` is a RecordingShellStore (duck-typed across module copies). */
|
|
@@ -181,6 +181,6 @@ export declare class SeededShellStore<TEnv = unknown> implements SegmentCacheSto
|
|
|
181
181
|
entry: ShellCacheEntry;
|
|
182
182
|
shouldRevalidate?: boolean;
|
|
183
183
|
} | null>;
|
|
184
|
-
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
|
|
184
|
+
putShell(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | "uncacheable" | void>;
|
|
185
185
|
invalidateTags(tags: string[]): Promise<void>;
|
|
186
186
|
}
|
|
@@ -161,18 +161,37 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
161
161
|
* @param tags - Optional cache tags for invalidation (participates in
|
|
162
162
|
* invalidateTags via the same tag machinery as the item family)
|
|
163
163
|
* @returns `invalidated` when a generation marker rejected the write,
|
|
164
|
-
* `stored` when acknowledged,
|
|
164
|
+
* `stored` when acknowledged, `uncacheable` when the entry can NEVER be
|
|
165
|
+
* stored under the current configuration (every retry would refuse
|
|
166
|
+
* identically — the capture scheduler backs the key off instead of
|
|
167
|
+
* recapturing per MISS; CFCacheStore returns it for a tag set whose
|
|
168
|
+
* Cache-Tag header overflows in KV-less purge mode), or void for stores
|
|
169
|
+
* without acknowledgements.
|
|
165
170
|
*/
|
|
166
|
-
putShell?(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | void>;
|
|
171
|
+
putShell?(key: string, entry: ShellCacheEntry, ttlSeconds?: number, swrSeconds?: number, tags?: string[]): Promise<"stored" | "invalidated" | "uncacheable" | void>;
|
|
167
172
|
/**
|
|
168
173
|
* Declares the shell family present-but-inert: getShell/putShell exist but
|
|
169
|
-
* no-op (
|
|
170
|
-
* captures whose only write target is inert —
|
|
171
|
-
* dead work that still occupies the
|
|
172
|
-
* (a promise-heavy route bakes for
|
|
173
|
-
* Absent/false means the family,
|
|
174
|
+
* no-op (a custom store whose backing tier is conditionally unavailable).
|
|
175
|
+
* scheduleShellCapture skips captures whose only write target is inert —
|
|
176
|
+
* the background render would be dead work that still occupies the
|
|
177
|
+
* per-isolate serialized capture queue (a promise-heavy route bakes for
|
|
178
|
+
* seconds per MISS with nothing stored). Absent/false means the family,
|
|
179
|
+
* when present, actually stores. The built-in stores never declare it:
|
|
180
|
+
* CFCacheStore is L1-only without KV (edge-only ppr), not inert.
|
|
174
181
|
*/
|
|
175
182
|
shellFamilyInert?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Declares isTagsInvalidatedSince present-but-inert: the store implements
|
|
185
|
+
* the method but has no DURABLE invalidation history behind it (a KV-less
|
|
186
|
+
* CFCacheStore answers from the per-request memo at best). Runtime shells
|
|
187
|
+
* tolerate that — purge eviction plus ttl/swr bound their staleness — but
|
|
188
|
+
* a TAGGED build-manifest shell is immutable with no ttl of its own, so
|
|
189
|
+
* serving it on such a store would make updateTag() a permanent no-op for
|
|
190
|
+
* it. The build-shell read-through declines tagged entries on this flag
|
|
191
|
+
* (same declared-intent-cannot-be-honored doctrine as a store missing the
|
|
192
|
+
* method entirely). Absent/false means answers are durably backed.
|
|
193
|
+
*/
|
|
194
|
+
tagHistoryInert?: boolean;
|
|
176
195
|
/**
|
|
177
196
|
* Get a cached function result by key.
|
|
178
197
|
* Returns the serialized value, optional handle data, and staleness flag.
|
|
@@ -27,10 +27,10 @@ export interface ClientUrlProjectionRoute {
|
|
|
27
27
|
readonly options: ClientUrlProjectionOptions;
|
|
28
28
|
readonly loaderIds: readonly string[];
|
|
29
29
|
readonly hasLoading: boolean;
|
|
30
|
-
/** Indices into loaderIds of loaders declared loader(Def, {
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
/** Indices into loaderIds of loaders declared loader(Def, { ssr: false });
|
|
31
|
+
* materialization passes the option through to the server loader() so
|
|
32
|
+
* document renders await them before first flush. Absent (= none) in
|
|
33
|
+
* projections serialized before the option existed. */
|
|
34
34
|
readonly awaitedLoaderIndices?: readonly number[];
|
|
35
35
|
/** Data-only transition config (no `when` — server-tree only); absent in
|
|
36
36
|
* projections serialized before transition support. */
|
|
@@ -54,12 +54,12 @@ export interface ClientUrlLoaderRecord {
|
|
|
54
54
|
/** Client-run per-loader revalidation predicates; empty = locked defaults. */
|
|
55
55
|
readonly revalidate: readonly ClientRevalidateFn[];
|
|
56
56
|
/**
|
|
57
|
-
* loader(Def, {
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* loader(Def, { ssr: false }): document renders await this loader before
|
|
58
|
+
* first flush (see {@link LoaderOptions}). Projected into the server tree,
|
|
59
|
+
* where the per-isSSR entry stamping applies — client navigations stream
|
|
60
|
+
* regardless.
|
|
61
61
|
*/
|
|
62
|
-
readonly
|
|
62
|
+
readonly ssr?: false;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* The data-only subset of TransitionConfig a clientUrls() route may declare:
|
|
@@ -101,13 +101,14 @@ export interface ClientUrlHelpers {
|
|
|
101
101
|
* revalidate() only — a CLIENT-RUN per-loader predicate; its decision (not
|
|
102
102
|
* the function) is sent with the revalidation request.
|
|
103
103
|
*
|
|
104
|
-
* Pass `{
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* live
|
|
104
|
+
* Pass `{ ssr: false }` — the same knob as loading(fallback, { ssr:
|
|
105
|
+
* false }) — to await this loader before first flush on DOCUMENT requests
|
|
106
|
+
* (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
|
|
107
|
+
* pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
|
|
108
|
+
* Per-loader: a streaming sibling keeps streaming. Under a `ppr` group
|
|
109
|
+
* route the flag BAKES: the loader executes at shell capture and its
|
|
110
|
+
* settled return freezes into the shell (nested promises stay live
|
|
111
|
+
* holes).
|
|
111
112
|
*/
|
|
112
113
|
readonly loader: <TData>(definition: LoaderDefinition<TData>, optionsOrUse?: LoaderOptions | ClientUrlUse, use?: ClientUrlUse) => ClientUrlItem;
|
|
113
114
|
readonly loading: (component: ReactNode) => ClientUrlItem;
|
|
@@ -208,13 +208,14 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
208
208
|
* return <div>{data.name}</div>;
|
|
209
209
|
* }
|
|
210
210
|
* ```
|
|
211
|
-
* Pass `{
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
211
|
+
* Pass `{ ssr: false }` — the same knob as loading(fallback, { ssr:
|
|
212
|
+
* false }) — to await this loader before first flush on DOCUMENT requests
|
|
213
|
+
* (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
|
|
214
|
+
* pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
|
|
215
|
+
* Per-loader: a streaming sibling in the same segment keeps streaming.
|
|
215
216
|
*
|
|
216
217
|
* ```typescript
|
|
217
|
-
* loader(ProductLoader, {
|
|
218
|
+
* loader(ProductLoader, { ssr: false }, () => [cache()]),
|
|
218
219
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
219
220
|
* ```
|
|
220
221
|
*
|
|
@@ -30,7 +30,7 @@ export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
|
30
30
|
/**
|
|
31
31
|
* Entry-level lane input for an entry's loaders under PPR (the loading()
|
|
32
32
|
* value; docs/design/loader-container-bake.md). The CAPTURE decision itself
|
|
33
|
-
* is per LOADER in loader-cache.ts:
|
|
33
|
+
* is per LOADER in loader-cache.ts: an `ssr: false`
|
|
34
34
|
* (awaitBeforeFlush) loader BAKES at capture regardless of this value — the
|
|
35
35
|
* flag's document promise ("data in the HTML before first flush") maps to
|
|
36
36
|
* the frozen prelude — while every other loader is LIVE (masked at capture,
|
|
@@ -126,9 +126,10 @@ export interface ShellCaptureDebugEvent {
|
|
|
126
126
|
* the capture was not attempted
|
|
127
127
|
* - skip-capacity: the isolate capture queue is full; a later request may retry
|
|
128
128
|
* - skip-inert-store: the resolved store's shell family is missing or
|
|
129
|
-
* declared inert (SegmentCacheStore.shellFamilyInert — a
|
|
130
|
-
*
|
|
131
|
-
* background render was not
|
|
129
|
+
* declared inert (SegmentCacheStore.shellFamilyInert — a custom store
|
|
130
|
+
* whose backing tier is unavailable; built-in stores never declare it);
|
|
131
|
+
* nothing could store the result, so the background render was not
|
|
132
|
+
* scheduled at all
|
|
132
133
|
* - skip-queue-timeout: the capture waited past CAPTURE_QUEUE_WAIT_BUDGET_MS
|
|
133
134
|
* behind other captures and was dropped unrun (no backoff — the route is
|
|
134
135
|
* not doomed, the isolate was busy; a later request re-probes). Carries
|
|
@@ -160,7 +161,7 @@ export interface ShellCaptureDebugEvent {
|
|
|
160
161
|
/** A bake-lane loader settled into a shell that uses TTL/SWR-only invalidation. */
|
|
161
162
|
untaggedBake?: true;
|
|
162
163
|
/** Outcome reported by a store that supports shell-write acknowledgements. */
|
|
163
|
-
storeWrite?: "stored" | "invalidated";
|
|
164
|
+
storeWrite?: "stored" | "invalidated" | "uncacheable";
|
|
164
165
|
/** Consecutive failure count in the key's backoff entry, when one exists. */
|
|
165
166
|
backoffFailures?: number;
|
|
166
167
|
/** Ms remaining in the key's backoff window, when one exists. */
|
|
@@ -282,6 +283,15 @@ export interface ShellCaptureDescriptor {
|
|
|
282
283
|
* Omitted (build-time bare-pathname captures) = search-less, same as "".
|
|
283
284
|
*/
|
|
284
285
|
searchSeed?: string;
|
|
286
|
+
/**
|
|
287
|
+
* The scheduling request's origin, seeding the capture render's SSR store
|
|
288
|
+
* location. Shell keys are host-scoped, so the resume pass's request
|
|
289
|
+
* agrees modulo protocol drift; the seed keeps origin-dependent static
|
|
290
|
+
* markup (Link's data-external) identical across capture, resume, and
|
|
291
|
+
* browser hydration. Omitted (build-time host-agnostic captures) falls
|
|
292
|
+
* back to the internal host.
|
|
293
|
+
*/
|
|
294
|
+
originSeed?: string;
|
|
285
295
|
/**
|
|
286
296
|
* The RSC handler's build version (HandlerContext.version), stamped into the
|
|
287
297
|
* stored entry as ShellCacheEntry.buildVersion — the serve-side
|
|
@@ -175,6 +175,12 @@ export interface SSRRenderOptions {
|
|
|
175
175
|
* search; see ShellCaptureOptions.search in the SSR entry).
|
|
176
176
|
*/
|
|
177
177
|
search?: string;
|
|
178
|
+
/**
|
|
179
|
+
* The live request's origin (`url.origin`), seeding the SSR store
|
|
180
|
+
* location so origin-dependent markup (Link's data-external) agrees with
|
|
181
|
+
* the browser across hydration. Absent on host-agnostic build captures.
|
|
182
|
+
*/
|
|
183
|
+
origin?: string;
|
|
178
184
|
}
|
|
179
185
|
/**
|
|
180
186
|
* SSR module interface for HTML rendering
|
|
@@ -193,6 +199,7 @@ export interface SSRModule {
|
|
|
193
199
|
quiesce: Promise<void>;
|
|
194
200
|
maxWaitMs?: number;
|
|
195
201
|
search?: string;
|
|
202
|
+
origin?: string;
|
|
196
203
|
}) => Promise<{
|
|
197
204
|
prelude: Uint8Array;
|
|
198
205
|
postponed: string | null;
|
|
@@ -209,6 +216,7 @@ export interface SSRModule {
|
|
|
209
216
|
postponed: string | null;
|
|
210
217
|
nonce?: string;
|
|
211
218
|
search?: string;
|
|
219
|
+
origin?: string;
|
|
212
220
|
}) => Promise<ReadableStream<Uint8Array>>;
|
|
213
221
|
}
|
|
214
222
|
/**
|
|
@@ -84,7 +84,7 @@ export type LoaderEntry = {
|
|
|
84
84
|
cache?: EntryCacheConfig;
|
|
85
85
|
/**
|
|
86
86
|
* Document renders await this loader before segment resolution returns
|
|
87
|
-
* (loader(Def, {
|
|
87
|
+
* (loader(Def, { ssr: false })), so its data, handle pushes, and
|
|
88
88
|
* thrown notFound()/redirect() deterministically precede first flush.
|
|
89
89
|
* Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
|
|
90
90
|
* per-isSSR (router/manifest.ts cache key), so the flag is already
|
|
@@ -509,7 +509,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
|
|
|
509
509
|
_handlerLoaderDeps?: Set<string>;
|
|
510
510
|
/**
|
|
511
511
|
* @internal Loader IDs ($$id) whose entries carry `awaitBeforeFlush`
|
|
512
|
-
* (loader(Def, {
|
|
512
|
+
* (loader(Def, { ssr: false })): segment resolution awaits these
|
|
513
513
|
* before returning, so the render barrier cannot resolve until they settle.
|
|
514
514
|
* rendered() checks this set to fail fast — a flagged loader awaiting the
|
|
515
515
|
* barrier is a guaranteed cycle, not a race. Registered by resolveLoaders
|
|
@@ -90,6 +90,13 @@ export interface SSRRenderOptions {
|
|
|
90
90
|
* only (runtime captures own the search variants, seeded per key).
|
|
91
91
|
*/
|
|
92
92
|
search?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The live request's origin, seeding the SSR store location so
|
|
95
|
+
* origin-dependent markup (Link's data-external) agrees with the
|
|
96
|
+
* browser's window.location across hydration. Absent on the build-time
|
|
97
|
+
* prerender pass (host-agnostic bare captures).
|
|
98
|
+
*/
|
|
99
|
+
origin?: string;
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* SSR dependencies from external packages
|
|
@@ -184,6 +191,13 @@ interface ShellCaptureOptions {
|
|
|
184
191
|
* the shell's own key. MUST equal the resume pass's seed for the same key.
|
|
185
192
|
*/
|
|
186
193
|
search?: string;
|
|
194
|
+
/**
|
|
195
|
+
* The capture request's origin. Shell keys are host-scoped, so the resume
|
|
196
|
+
* pass's request agrees modulo protocol drift; seeding it keeps
|
|
197
|
+
* origin-dependent static markup (Link's data-external) identical across
|
|
198
|
+
* capture, resume, and browser hydration.
|
|
199
|
+
*/
|
|
200
|
+
origin?: string;
|
|
187
201
|
}
|
|
188
202
|
/**
|
|
189
203
|
* Result of a successful shell capture. `prelude` is the raw prelude bytes;
|
|
@@ -210,6 +224,8 @@ interface ShellResumeOptions {
|
|
|
210
224
|
* postponed holes.
|
|
211
225
|
*/
|
|
212
226
|
search?: string;
|
|
227
|
+
/** The HIT request's origin — same host as the capture's (key-scoped). */
|
|
228
|
+
origin?: string;
|
|
213
229
|
}
|
|
214
230
|
/**
|
|
215
231
|
* Create an SSR handler that converts RSC streams to HTML.
|
|
@@ -58,6 +58,11 @@ export interface SsrRootOptions {
|
|
|
58
58
|
* the resume tree matches the captured tree above the postponed holes.
|
|
59
59
|
*/
|
|
60
60
|
search?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Origin seeding the SSR store location (see createSsrEventController):
|
|
63
|
+
* live fizz passes the request's origin; capture/resume pass theirs.
|
|
64
|
+
*/
|
|
65
|
+
origin?: string;
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Build the closure component that deserializes the Flight payload, consumes
|
|
@@ -63,7 +63,7 @@ export type LoaderContext<TParams = Record<string, string | undefined>, TEnv = D
|
|
|
63
63
|
* later ones stream to the client and apply post-hydration (document lane)
|
|
64
64
|
* or progressively (navigation/action lanes). To guarantee a loader's
|
|
65
65
|
* handles are in the SSR'd document, register it as
|
|
66
|
-
* `loader(Def, {
|
|
66
|
+
* `loader(Def, { ssr: false })` so the document render awaits it
|
|
67
67
|
* (see {@link LoaderOptions}).
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
@@ -149,29 +149,31 @@ export type LoaderContext<TParams = Record<string, string | undefined>, TEnv = D
|
|
|
149
149
|
*/
|
|
150
150
|
export type LoaderFn<T, TParams = Record<string, string | undefined>, TEnv = DefaultEnv> = (ctx: LoaderContext<TParams, TEnv>) => Promise<T> | T;
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* Default (omitted): the loader streams on every render. Its data,
|
|
155
|
-
* `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws
|
|
156
|
-
* AFTER the document Response is constructed, so none of them are
|
|
157
|
-
* be in the SSR'd HTML.
|
|
158
|
-
*
|
|
159
|
-
* `
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* `
|
|
152
|
+
* SSR delivery for a DSL-registered loader: `loader(Def, { ssr })`.
|
|
153
|
+
*
|
|
154
|
+
* Default (omitted / `true`): the loader streams on every render. Its data,
|
|
155
|
+
* its `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws
|
|
156
|
+
* may land AFTER the document Response is constructed, so none of them are
|
|
157
|
+
* guaranteed to be in the SSR'd HTML.
|
|
158
|
+
*
|
|
159
|
+
* `ssr: false` turns SSR streaming off for this loader — the same knob
|
|
160
|
+
* `loading(fallback, { ssr: false })` is for the fallback: on a DOCUMENT
|
|
161
|
+
* request the loader is awaited before first flush, so no fallback paints for
|
|
162
|
+
* it (`useLoader` still suspends, but on an already-settled promise). Client
|
|
163
|
+
* navigations keep streaming it. This is the SSR-completeness opt-in. Choose
|
|
164
|
+
* it when the loader feeds something that must exist in the document:
|
|
165
|
+
* `<head>` meta via a handle, or a real 404 status (an awaited `notFound()`
|
|
166
|
+
* deterministically precedes Response construction, where the streamed
|
|
167
|
+
* default only wins that race opportunistically). Under ppr it is also the
|
|
168
|
+
* BAKE lane: the loader executes at shell capture and its settled return is
|
|
169
|
+
* shell material (nested promises stay live holes) — see the ppr docs.
|
|
170
|
+
*
|
|
171
|
+
* Scoped per LOADER, not per segment: an `ssr: false` loader alongside a
|
|
172
|
+
* deliberately streaming sibling awaits only itself, and the sibling keeps
|
|
173
|
+
* streaming behind its `loading()`/Suspense boundary.
|
|
172
174
|
*/
|
|
173
175
|
export type LoaderOptions = {
|
|
174
|
-
|
|
176
|
+
ssr?: boolean;
|
|
175
177
|
};
|
|
176
178
|
/**
|
|
177
179
|
* Options for fetchable loaders
|
|
@@ -148,13 +148,14 @@ export type PathHelpers<TEnv> = {
|
|
|
148
148
|
/**
|
|
149
149
|
* Attach a data loader to the current route/layout.
|
|
150
150
|
*
|
|
151
|
-
* Pass `{
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* holes) — the pre-flush
|
|
151
|
+
* Pass `{ ssr: false }` — the same knob as loading(fallback, { ssr:
|
|
152
|
+
* false }) — to await this loader before first flush on DOCUMENT requests
|
|
153
|
+
* (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
|
|
154
|
+
* pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
|
|
155
|
+
* Per-loader: a streaming sibling keeps streaming. Under a `ppr` route the
|
|
156
|
+
* flag BAKES: the loader executes at shell capture and its settled return
|
|
157
|
+
* freezes into the shell (nested promises stay live holes) — the pre-flush
|
|
158
|
+
* promise applied to the prelude.
|
|
158
159
|
*/
|
|
159
160
|
loader: <TData>(loaderDef: LoaderDefinition<TData>, optionsOrUse?: LoaderOptions | (() => LoaderUseItem[]), use?: () => LoaderUseItem[]) => LoaderItem;
|
|
160
161
|
/**
|
package/dist/vite/index.js
CHANGED
|
@@ -3685,7 +3685,7 @@ import { resolve } from "node:path";
|
|
|
3685
3685
|
// package.json
|
|
3686
3686
|
var package_default = {
|
|
3687
3687
|
name: "@rangojs/router",
|
|
3688
|
-
version: "0.
|
|
3688
|
+
version: "0.9.0",
|
|
3689
3689
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
3690
3690
|
keywords: [
|
|
3691
3691
|
"react",
|
|
@@ -8304,11 +8304,12 @@ init_loader();
|
|
|
8304
8304
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
8305
8305
|
var IGNORE_STRATEGY_CHANGES = (_listener) => () => {
|
|
8306
8306
|
};
|
|
8307
|
-
function isExternalUrl(href2) {
|
|
8307
|
+
function isExternalUrl(href2, origin) {
|
|
8308
8308
|
if (href2.startsWith("//")) return true;
|
|
8309
8309
|
if (href2.startsWith("http://") || href2.startsWith("https://")) {
|
|
8310
|
+
if (!origin) return false;
|
|
8310
8311
|
try {
|
|
8311
|
-
return new URL(href2).origin !==
|
|
8312
|
+
return new URL(href2).origin !== origin;
|
|
8312
8313
|
} catch {
|
|
8313
8314
|
return false;
|
|
8314
8315
|
}
|
|
@@ -8332,7 +8333,9 @@ var Link = forwardRef(function Link2({
|
|
|
8332
8333
|
...props
|
|
8333
8334
|
}, ref) {
|
|
8334
8335
|
const ctx = useContext14(NavigationStoreContext);
|
|
8335
|
-
const
|
|
8336
|
+
const storeLocation = ctx?.eventController.getState().location;
|
|
8337
|
+
const origin = storeLocation?.origin ?? (typeof window !== "undefined" ? window.location.origin : void 0);
|
|
8338
|
+
const isExternal = isExternalUrl(to, origin);
|
|
8336
8339
|
const resolvedTo = useMemo7(() => {
|
|
8337
8340
|
if (isExternal) return to;
|
|
8338
8341
|
const bn = ctx?.basename;
|
|
@@ -9018,7 +9021,7 @@ function serializeRoute(route2) {
|
|
|
9018
9021
|
}
|
|
9019
9022
|
return loader2.$$id;
|
|
9020
9023
|
});
|
|
9021
|
-
const awaitedLoaderIndices = route2.loaders.map(({
|
|
9024
|
+
const awaitedLoaderIndices = route2.loaders.map(({ ssr }, index) => ssr === false ? index : -1).filter((index) => index >= 0);
|
|
9022
9025
|
const transition2 = serializeTransition(route2);
|
|
9023
9026
|
return Object.freeze({
|
|
9024
9027
|
id: route2.id,
|
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@ Two pushes from one segment accumulate in push order (`Shop › product`).
|
|
|
82
82
|
Delivery follows the loader race model: a push that beats the handler barrier
|
|
83
83
|
is in the SSR'd document; a push after a slow fetch streams and applies
|
|
84
84
|
client-side (`useHandle` re-renders when it lands). To guarantee document
|
|
85
|
-
delivery, register the loader as `loader(Def, {
|
|
85
|
+
delivery, register the loader as `loader(Def, { ssr: false })` —
|
|
86
86
|
see `/loader` → "Writing Handles from Loaders".
|
|
87
87
|
|
|
88
88
|
## Async Content
|
|
@@ -170,7 +170,7 @@ swap in — no blank, no pending entry. If the slot times out to `else: null`/
|
|
|
170
170
|
undefined, the entry is simply dropped. Use `.defer()` only when even
|
|
171
171
|
`label`/`href` are unknown at handler time AND the producer is a component,
|
|
172
172
|
not a loader — a loader-produced crumb pushes directly from the loader body
|
|
173
|
-
(add `{
|
|
173
|
+
(add `{ ssr: false }` if it must be in the SSR'd document), and if
|
|
174
174
|
you know `label`/`href` and only the `content` is async, push a concrete item
|
|
175
175
|
with a `Promise` `content` field instead (the `content` field is a nested
|
|
176
176
|
promise you resolve with `use()` in your component; no `.defer()` needed).
|
package/skills/catalog.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"name": "client-urls",
|
|
38
|
-
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the
|
|
38
|
+
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a \"use client\" file work.",
|
|
39
39
|
"argumentHint": "[setup]",
|
|
40
40
|
"path": "skills/client-urls/SKILL.md"
|
|
41
41
|
},
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "loader",
|
|
140
|
-
"description": "Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (
|
|
140
|
+
"description": "Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (ssr:false).",
|
|
141
141
|
"argumentHint": "[loader]",
|
|
142
142
|
"path": "skills/loader/SKILL.md"
|
|
143
143
|
},
|