@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.
Files changed (59) hide show
  1. package/dist/types/cache/cf/cf-cache-store.d.ts +30 -16
  2. package/dist/types/cache/shell-snapshot.d.ts +2 -2
  3. package/dist/types/cache/types.d.ts +26 -7
  4. package/dist/types/client-urls/server-projection.d.ts +4 -4
  5. package/dist/types/client-urls/types.d.ts +13 -12
  6. package/dist/types/route-definition/helpers-types.d.ts +6 -5
  7. package/dist/types/router/segment-resolution/loader-mask.d.ts +1 -1
  8. package/dist/types/rsc/shell-capture.d.ts +14 -4
  9. package/dist/types/rsc/types.d.ts +8 -0
  10. package/dist/types/server/context.d.ts +1 -1
  11. package/dist/types/server/request-context.d.ts +1 -1
  12. package/dist/types/ssr/index.d.ts +16 -0
  13. package/dist/types/ssr/ssr-root.d.ts +5 -0
  14. package/dist/types/types/loader-types.d.ts +24 -22
  15. package/dist/types/urls/path-helper-types.d.ts +8 -7
  16. package/dist/vite/index.js +8 -5
  17. package/package.json +1 -1
  18. package/skills/breadcrumbs/SKILL.md +2 -2
  19. package/skills/catalog.json +2 -2
  20. package/skills/client-urls/SKILL.md +9 -8
  21. package/skills/cloudflare/SKILL.md +5 -3
  22. package/skills/comparison/references/framework-comparison.md +2 -2
  23. package/skills/hooks/data.md +1 -1
  24. package/skills/hooks/handle-and-actions.md +1 -1
  25. package/skills/loader/SKILL.md +29 -28
  26. package/skills/migrate-nextjs/SKILL.md +3 -3
  27. package/skills/migrate-react-router/component-migration.md +1 -1
  28. package/skills/migrate-react-router/data-and-actions.md +1 -1
  29. package/skills/migrate-react-router/route-mapping.md +1 -1
  30. package/skills/parallel/SKILL.md +1 -1
  31. package/skills/ppr/SKILL.md +12 -8
  32. package/skills/rango/SKILL.md +20 -20
  33. package/skills/router-setup/SKILL.md +1 -1
  34. package/skills/scripts/SKILL.md +1 -1
  35. package/skills/shell-manifest/SKILL.md +1 -1
  36. package/src/browser/react/Link.tsx +27 -4
  37. package/src/cache/cf/cf-cache-store.ts +118 -62
  38. package/src/cache/shell-snapshot.ts +2 -2
  39. package/src/cache/types.ts +27 -7
  40. package/src/client-urls/client-urls.ts +12 -9
  41. package/src/client-urls/server-projection.ts +6 -6
  42. package/src/client-urls/types.ts +13 -12
  43. package/src/route-definition/dsl-helpers.ts +7 -3
  44. package/src/route-definition/helpers-types.ts +6 -5
  45. package/src/router/loader-resolution.ts +3 -3
  46. package/src/router/segment-resolution/fresh.ts +2 -2
  47. package/src/router/segment-resolution/loader-cache.ts +1 -1
  48. package/src/router/segment-resolution/loader-mask.ts +1 -1
  49. package/src/rsc/rsc-rendering.ts +10 -0
  50. package/src/rsc/shell-build-manifest.ts +13 -6
  51. package/src/rsc/shell-capture.ts +32 -5
  52. package/src/rsc/ssr-setup.ts +4 -0
  53. package/src/rsc/types.ts +18 -2
  54. package/src/server/context.ts +1 -1
  55. package/src/server/request-context.ts +1 -1
  56. package/src/ssr/index.tsx +22 -2
  57. package/src/ssr/ssr-root.tsx +17 -1
  58. package/src/types/loader-types.ts +21 -19
  59. 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 the shell family is inert: getShell/putShell
261
- * are ONLY called for routes that declared the `ppr` path option, so firing
262
- * here (not in the constructor) scopes the warning to apps that actually
263
- * use PPR a KV-less CFCacheStore is a perfectly fine config otherwise.
264
- * Without it, the correctness-first fail-open (issue #651) is invisible:
265
- * every ppr route is a permanent MISS with zero diagnostics.
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 warnShellFamilyInertOnce;
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 its retention meets KV's
282
- * 60-second floor, KV. The shared write is registered with waitUntil and
283
- * awaited so invalidation rejection can be acknowledged to the capture
284
- * scheduler. Short-lived shells remain useful in L1 even though KV rejects
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
- * private envelope check (marker >= since, fail open).
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, or void for stores without acknowledgements.
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 (CFCacheStore without a KV namespace). scheduleShellCapture skips
170
- * captures whose only write target is inert — the background render would be
171
- * dead work that still occupies the per-isolate serialized capture queue
172
- * (a promise-heavy route bakes for seconds per MISS with nothing stored).
173
- * Absent/false means the family, when present, actually stores.
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, { stream:
31
- * "navigation" }); materialization passes the option through to the server
32
- * loader() so document renders await them before first flush. Absent (=
33
- * none) in projections serialized before stream support. */
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, { stream: "navigation" }): document renders await this loader
58
- * before first flush (see {@link LoaderOptions}). Projected into the server
59
- * tree, where the per-isSSR entry stamping applies — client navigations
60
- * stream regardless.
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 stream?: "navigation";
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 `{ stream: "navigation" }` to await this loader before first flush
105
- * on DOCUMENT requests (see {@link LoaderOptions}) the opt-in for loaders
106
- * whose data, handle pushes, or thrown notFound()/redirect() must be in the
107
- * SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
108
- * `ppr` group route the flag BAKES: the loader executes at shell capture
109
- * and its settled return freezes into the shell (nested promises stay
110
- * live holes).
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 `{ stream: "navigation" }` to await this loader before first flush on
212
- * DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders whose
213
- * data, handle pushes, or thrown notFound()/redirect() must be in the SSR'd
214
- * HTML. Per-loader: a dynamic sibling in the same segment keeps streaming.
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, { stream: "navigation" }, () => [cache()]),
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: a `stream: "navigation"`
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 CFCacheStore
130
- * without a KV namespace); nothing could store the result, so the
131
- * background render was not scheduled at all
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, { stream: "navigation" })), so its data, handle pushes, and
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, { stream: "navigation" })): segment resolution awaits these
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, { stream: "navigation" })` so the document render awaits it
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
- * Delivery mode for a DSL-registered loader: `loader(Def, { stream })`.
153
- *
154
- * Default (omitted): the loader streams on every render. Its data, its
155
- * `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws may land
156
- * AFTER the document Response is constructed, so none of them are guaranteed to
157
- * be in the SSR'd HTML.
158
- *
159
- * `"navigation"` narrows streaming to client navigations only: on a DOCUMENT
160
- * request the loader is awaited before first flush. `useLoader` still suspends,
161
- * but on an already-settled promise, so no fallback paints. This is the
162
- * SSR-completeness opt-in the name is about WHERE streaming still applies, not
163
- * about disabling it. Choose it when the loader feeds something that must exist
164
- * in the document: `<head>` meta via a handle, or a real 404 status (an awaited
165
- * `notFound()` deterministically precedes Response construction, where the
166
- * streamed default only wins that race opportunistically). It does NOT change
167
- * PPR capture behavior: capture renders mask loaders and skip this await.
168
- *
169
- * Scoped per LOADER, not per segment: a baked loader alongside a deliberately
170
- * dynamic sibling awaits only itself, and the sibling keeps streaming behind its
171
- * `loading()`/Suspense boundary.
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
- stream?: "navigation";
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 `{ stream: "navigation" }` to await this loader before first flush
152
- * on DOCUMENT requests (see {@link LoaderOptions}) the opt-in for loaders
153
- * whose data, handle pushes, or thrown notFound()/redirect() must be in the
154
- * SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
155
- * `ppr` route the flag BAKES: the loader executes at shell capture and
156
- * its settled return freezes into the shell (nested promises stay live
157
- * holes) — the pre-flush promise applied to the prelude.
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
  /**
@@ -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.7.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 !== window.location.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 isExternal = isExternalUrl(to);
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(({ stream }, index) => stream === "navigation" ? index : -1).filter((index) => index >= 0);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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, { stream: "navigation" })` —
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 `{ stream: "navigation" }` if it must be in the SSR'd document), and if
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).
@@ -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 stream:\"navigation\" 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.",
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 (stream:\"navigation\").",
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
  },