@rangojs/router 0.8.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 (51) hide show
  1. package/dist/types/client-urls/server-projection.d.ts +4 -4
  2. package/dist/types/client-urls/types.d.ts +13 -12
  3. package/dist/types/route-definition/helpers-types.d.ts +6 -5
  4. package/dist/types/router/segment-resolution/loader-mask.d.ts +1 -1
  5. package/dist/types/rsc/shell-capture.d.ts +9 -0
  6. package/dist/types/rsc/types.d.ts +8 -0
  7. package/dist/types/server/context.d.ts +1 -1
  8. package/dist/types/server/request-context.d.ts +1 -1
  9. package/dist/types/ssr/index.d.ts +16 -0
  10. package/dist/types/ssr/ssr-root.d.ts +5 -0
  11. package/dist/types/types/loader-types.d.ts +24 -22
  12. package/dist/types/urls/path-helper-types.d.ts +8 -7
  13. package/dist/vite/index.js +8 -5
  14. package/package.json +22 -21
  15. package/skills/breadcrumbs/SKILL.md +2 -2
  16. package/skills/catalog.json +2 -2
  17. package/skills/client-urls/SKILL.md +9 -8
  18. package/skills/comparison/references/framework-comparison.md +2 -2
  19. package/skills/hooks/data.md +1 -1
  20. package/skills/hooks/handle-and-actions.md +1 -1
  21. package/skills/loader/SKILL.md +29 -28
  22. package/skills/migrate-nextjs/SKILL.md +3 -3
  23. package/skills/migrate-react-router/component-migration.md +1 -1
  24. package/skills/migrate-react-router/data-and-actions.md +1 -1
  25. package/skills/migrate-react-router/route-mapping.md +1 -1
  26. package/skills/parallel/SKILL.md +1 -1
  27. package/skills/ppr/SKILL.md +1 -1
  28. package/skills/rango/SKILL.md +20 -20
  29. package/skills/router-setup/SKILL.md +1 -1
  30. package/skills/scripts/SKILL.md +1 -1
  31. package/skills/shell-manifest/SKILL.md +1 -1
  32. package/src/browser/react/Link.tsx +27 -4
  33. package/src/client-urls/client-urls.ts +12 -9
  34. package/src/client-urls/server-projection.ts +6 -6
  35. package/src/client-urls/types.ts +13 -12
  36. package/src/route-definition/dsl-helpers.ts +7 -3
  37. package/src/route-definition/helpers-types.ts +6 -5
  38. package/src/router/loader-resolution.ts +3 -3
  39. package/src/router/segment-resolution/fresh.ts +2 -2
  40. package/src/router/segment-resolution/loader-cache.ts +1 -1
  41. package/src/router/segment-resolution/loader-mask.ts +1 -1
  42. package/src/rsc/rsc-rendering.ts +10 -0
  43. package/src/rsc/shell-capture.ts +10 -0
  44. package/src/rsc/ssr-setup.ts +4 -0
  45. package/src/rsc/types.ts +18 -2
  46. package/src/server/context.ts +1 -1
  47. package/src/server/request-context.ts +1 -1
  48. package/src/ssr/index.tsx +22 -2
  49. package/src/ssr/ssr-root.tsx +17 -1
  50. package/src/types/loader-types.ts +21 -19
  51. package/src/urls/path-helper-types.ts +8 -7
@@ -155,6 +155,7 @@ function createShellCaptureDescriptor(
155
155
  ctx: HandlerContext<any>,
156
156
  key: string,
157
157
  searchSeed: string,
158
+ originSeed: string,
158
159
  pprConfig: ResolvedPprConfig,
159
160
  store: SegmentCacheStore<any>,
160
161
  navigationOnly?: true,
@@ -162,6 +163,11 @@ function createShellCaptureDescriptor(
162
163
  return {
163
164
  key,
164
165
  searchSeed,
166
+ // Derived from the same request URL as the key (the callers compute
167
+ // both next to each other, so they cannot drift): seeding the origin
168
+ // keeps origin-dependent static markup (Link's data-external)
169
+ // identical across capture, resume, and browser hydration.
170
+ originSeed,
165
171
  buildVersion: ctx.version,
166
172
  ttl: pprConfig.ttl,
167
173
  swr: pprConfig.swr,
@@ -519,6 +525,7 @@ function* shellServePlan<TEnv>(
519
525
  ctx,
520
526
  key,
521
527
  shellSearchSeed(url, reqCtx._searchParamsFilter),
528
+ url.origin,
522
529
  pprConfig,
523
530
  store,
524
531
  );
@@ -1004,6 +1011,7 @@ function scheduleNavigationShellCapture<TEnv>(
1004
1011
  partialCaptureKey ??
1005
1012
  buildNavigationShellKey(url, reqCtx._searchParamsFilter),
1006
1013
  shellSearchSeed(url, reqCtx._searchParamsFilter),
1014
+ url.origin,
1007
1015
  pprConfig,
1008
1016
  store,
1009
1017
  true,
@@ -1524,6 +1532,8 @@ function serveShellHit(
1524
1532
  // The shell key's own search — identical to the capture seed for
1525
1533
  // this key, so the resume tree matches the captured tree.
1526
1534
  search: shellSearchSeed(url, reqCtx._searchParamsFilter),
1535
+ // The HIT request's origin — same host as the capture's (key-scoped).
1536
+ origin: url.origin,
1527
1537
  }),
1528
1538
  );
1529
1539
  };
@@ -917,6 +917,15 @@ export interface ShellCaptureDescriptor {
917
917
  * Omitted (build-time bare-pathname captures) = search-less, same as "".
918
918
  */
919
919
  searchSeed?: string;
920
+ /**
921
+ * The scheduling request's origin, seeding the capture render's SSR store
922
+ * location. Shell keys are host-scoped, so the resume pass's request
923
+ * agrees modulo protocol drift; the seed keeps origin-dependent static
924
+ * markup (Link's data-external) identical across capture, resume, and
925
+ * browser hydration. Omitted (build-time host-agnostic captures) falls
926
+ * back to the internal host.
927
+ */
928
+ originSeed?: string;
920
929
  /**
921
930
  * The RSC handler's build version (HandlerContext.version), stamped into the
922
931
  * stored entry as ShellCacheEntry.buildVersion — the serve-side
@@ -1767,6 +1776,7 @@ async function captureAndStoreShell(
1767
1776
  // The shell key's own search seeds the capture render's store —
1768
1777
  // static-part search reads bake what the key names.
1769
1778
  search: capture.searchSeed,
1779
+ origin: capture.originSeed,
1770
1780
  }),
1771
1781
  );
1772
1782
  } catch (error) {
@@ -120,6 +120,10 @@ export function createSsrHtmlStage<TEnv>(
120
120
  ...options.render,
121
121
  streamMode,
122
122
  search: options.url.search,
123
+ // origin: same out-of-band channel — seeds the SSR store location
124
+ // so origin-dependent markup (Link's data-external) agrees with the
125
+ // browser's window.location across hydration.
126
+ origin: options.url.origin,
123
127
  }),
124
128
  ...(options.init && { init: options.init }),
125
129
  };
package/src/rsc/types.ts CHANGED
@@ -187,6 +187,12 @@ export interface SSRRenderOptions {
187
187
  * search; see ShellCaptureOptions.search in the SSR entry).
188
188
  */
189
189
  search?: string;
190
+ /**
191
+ * The live request's origin (`url.origin`), seeding the SSR store
192
+ * location so origin-dependent markup (Link's data-external) agrees with
193
+ * the browser across hydration. Absent on host-agnostic build captures.
194
+ */
195
+ origin?: string;
190
196
  }
191
197
 
192
198
  /**
@@ -208,7 +214,12 @@ export interface SSRModule {
208
214
  */
209
215
  captureShellHTML?: (
210
216
  rscStream: ReadableStream<Uint8Array>,
211
- options: { quiesce: Promise<void>; maxWaitMs?: number; search?: string },
217
+ options: {
218
+ quiesce: Promise<void>;
219
+ maxWaitMs?: number;
220
+ search?: string;
221
+ origin?: string;
222
+ },
212
223
  ) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
213
224
 
214
225
  /**
@@ -221,7 +232,12 @@ export interface SSRModule {
221
232
  */
222
233
  resumeShellHTML?: (
223
234
  rscStream: ReadableStream<Uint8Array>,
224
- options: { postponed: string | null; nonce?: string; search?: string },
235
+ options: {
236
+ postponed: string | null;
237
+ nonce?: string;
238
+ search?: string;
239
+ origin?: string;
240
+ },
225
241
  ) => Promise<ReadableStream<Uint8Array>>;
226
242
  }
227
243
 
@@ -108,7 +108,7 @@ export type LoaderEntry = {
108
108
  cache?: EntryCacheConfig;
109
109
  /**
110
110
  * Document renders await this loader before segment resolution returns
111
- * (loader(Def, { stream: "navigation" })), so its data, handle pushes, and
111
+ * (loader(Def, { ssr: false })), so its data, handle pushes, and
112
112
  * thrown notFound()/redirect() deterministically precede first flush.
113
113
  * Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
114
114
  * per-isSSR (router/manifest.ts cache key), so the flag is already
@@ -620,7 +620,7 @@ export interface RequestContext<
620
620
 
621
621
  /**
622
622
  * @internal Loader IDs ($$id) whose entries carry `awaitBeforeFlush`
623
- * (loader(Def, { stream: "navigation" })): segment resolution awaits these
623
+ * (loader(Def, { ssr: false })): segment resolution awaits these
624
624
  * before returning, so the render barrier cannot resolve until they settle.
625
625
  * rendered() checks this set to fail fast — a flagged loader awaiting the
626
626
  * barrier is a guaranteed cycle, not a race. Registered by resolveLoaders
package/src/ssr/index.tsx CHANGED
@@ -114,6 +114,14 @@ export interface SSRRenderOptions {
114
114
  * only (runtime captures own the search variants, seeded per key).
115
115
  */
116
116
  search?: string;
117
+
118
+ /**
119
+ * The live request's origin, seeding the SSR store location so
120
+ * origin-dependent markup (Link's data-external) agrees with the
121
+ * browser's window.location across hydration. Absent on the build-time
122
+ * prerender pass (host-agnostic bare captures).
123
+ */
124
+ origin?: string;
117
125
  }
118
126
 
119
127
  /**
@@ -363,6 +371,13 @@ interface ShellCaptureOptions {
363
371
  * the shell's own key. MUST equal the resume pass's seed for the same key.
364
372
  */
365
373
  search?: string;
374
+ /**
375
+ * The capture request's origin. Shell keys are host-scoped, so the resume
376
+ * pass's request agrees modulo protocol drift; seeding it keeps
377
+ * origin-dependent static markup (Link's data-external) identical across
378
+ * capture, resume, and browser hydration.
379
+ */
380
+ origin?: string;
366
381
  }
367
382
 
368
383
  /**
@@ -391,6 +406,8 @@ interface ShellResumeOptions {
391
406
  * postponed holes.
392
407
  */
393
408
  search?: string;
409
+ /** The HIT request's origin — same host as the capture's (key-scoped). */
410
+ origin?: string;
394
411
  }
395
412
 
396
413
  /**
@@ -472,7 +489,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
472
489
  rscStream: ReadableStream<Uint8Array>,
473
490
  options?: SSRRenderOptions,
474
491
  ): Promise<ReadableStream<Uint8Array>> {
475
- const { nonce, formState, streamMode, search } = options ?? {};
492
+ const { nonce, formState, streamMode, search, origin } = options ?? {};
476
493
 
477
494
  try {
478
495
  // Tee the stream:
@@ -490,6 +507,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
490
507
  // same key => same seed, so the resume tree matches the captured
491
508
  // tree while static-part search reads render what the key names.
492
509
  search,
510
+ origin,
493
511
  });
494
512
 
495
513
  // Get bootstrap script content
@@ -594,6 +612,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
594
612
  // The shell key's own search: static-part search reads render what
595
613
  // the key names, and the resume pass seeds the identical string.
596
614
  search: opts.search,
615
+ origin: opts.origin,
597
616
  });
598
617
 
599
618
  // Bootstrap load raced against the deadline. A load that never resolves
@@ -767,7 +786,7 @@ export function createShellResumeHandler<TEnv = unknown>(
767
786
  rscStream: ReadableStream<Uint8Array>,
768
787
  opts: ShellResumeOptions,
769
788
  ): Promise<ReadableStream<Uint8Array>> {
770
- const { postponed, nonce, search } = opts;
789
+ const { postponed, nonce, search, origin } = opts;
771
790
 
772
791
  try {
773
792
  if (postponed === null) {
@@ -800,6 +819,7 @@ export function createShellResumeHandler<TEnv = unknown>(
800
819
  // Same seed as the capture pass for this key: the resume tree must
801
820
  // match the captured tree above the holes, search reads included.
802
821
  search,
822
+ origin,
803
823
  });
804
824
 
805
825
  // EAGER injection (resume-only): the stored prelude — a complete document
@@ -70,13 +70,22 @@ function createSsrEventController(opts: {
70
70
  * names and both passes agree byte-for-byte.
71
71
  */
72
72
  search?: string;
73
+ /**
74
+ * Origin seeding the store location. Live fizz passes the request's real
75
+ * origin so origin-dependent markup (Link's data-external compares link
76
+ * origins against it) agrees with the browser's window.location; ppr
77
+ * capture/resume pass their request's origin (shell keys are host-scoped,
78
+ * so the two agree modulo protocol drift). Absent (build-time bare
79
+ * captures, tests) falls back to the historical internal host.
80
+ */
81
+ origin?: string;
73
82
  params?: Record<string, string>;
74
83
  handleData?: HandleData;
75
84
  matched?: string[];
76
85
  }): EventController {
77
86
  const location = new URL(
78
87
  `${opts.pathname}${opts.search ?? ""}`,
79
- "http://localhost",
88
+ opts.origin ?? "http://localhost",
80
89
  );
81
90
  let params = opts.params ?? {};
82
91
  const rawMatched = opts.matched ?? [];
@@ -162,6 +171,11 @@ export interface SsrRootOptions {
162
171
  * the resume tree matches the captured tree above the postponed holes.
163
172
  */
164
173
  search?: string;
174
+ /**
175
+ * Origin seeding the SSR store location (see createSsrEventController):
176
+ * live fizz passes the request's origin; capture/resume pass theirs.
177
+ */
178
+ origin?: string;
165
179
  }
166
180
 
167
181
  /**
@@ -188,6 +202,7 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
188
202
  nonce,
189
203
  onPayloadSettled,
190
204
  search,
205
+ origin,
191
206
  } = opts;
192
207
 
193
208
  let payload: Promise<RscPayload> | undefined;
@@ -238,6 +253,7 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
238
253
  eventController: createSsrEventController({
239
254
  pathname,
240
255
  search,
256
+ origin,
241
257
  params: resolved.metadata?.params,
242
258
  handleData,
243
259
  matched: resolved.metadata?.matched,
@@ -75,7 +75,7 @@ export type LoaderContext<
75
75
  * later ones stream to the client and apply post-hydration (document lane)
76
76
  * or progressively (navigation/action lanes). To guarantee a loader's
77
77
  * handles are in the SSR'd document, register it as
78
- * `loader(Def, { stream: "navigation" })` so the document render awaits it
78
+ * `loader(Def, { ssr: false })` so the document render awaits it
79
79
  * (see {@link LoaderOptions}).
80
80
  *
81
81
  * @example
@@ -174,29 +174,31 @@ export type LoaderFn<
174
174
  > = (ctx: LoaderContext<TParams, TEnv>) => Promise<T> | T;
175
175
 
176
176
  /**
177
- * Delivery mode for a DSL-registered loader: `loader(Def, { stream })`.
177
+ * SSR delivery for a DSL-registered loader: `loader(Def, { ssr })`.
178
178
  *
179
- * Default (omitted): the loader streams on every render. Its data, its
180
- * `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws may land
181
- * AFTER the document Response is constructed, so none of them are guaranteed to
182
- * be in the SSR'd HTML.
179
+ * Default (omitted / `true`): the loader streams on every render. Its data,
180
+ * its `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws
181
+ * may land AFTER the document Response is constructed, so none of them are
182
+ * guaranteed to be in the SSR'd HTML.
183
183
  *
184
- * `"navigation"` narrows streaming to client navigations only: on a DOCUMENT
185
- * request the loader is awaited before first flush. `useLoader` still suspends,
186
- * but on an already-settled promise, so no fallback paints. This is the
187
- * SSR-completeness opt-in the name is about WHERE streaming still applies, not
188
- * about disabling it. Choose it when the loader feeds something that must exist
189
- * in the document: `<head>` meta via a handle, or a real 404 status (an awaited
190
- * `notFound()` deterministically precedes Response construction, where the
191
- * streamed default only wins that race opportunistically). It does NOT change
192
- * PPR capture behavior: capture renders mask loaders and skip this await.
184
+ * `ssr: false` turns SSR streaming off for this loader the same knob
185
+ * `loading(fallback, { ssr: false })` is for the fallback: on a DOCUMENT
186
+ * request the loader is awaited before first flush, so no fallback paints for
187
+ * it (`useLoader` still suspends, but on an already-settled promise). Client
188
+ * navigations keep streaming it. This is the SSR-completeness opt-in. Choose
189
+ * it when the loader feeds something that must exist in the document:
190
+ * `<head>` meta via a handle, or a real 404 status (an awaited `notFound()`
191
+ * deterministically precedes Response construction, where the streamed
192
+ * default only wins that race opportunistically). Under ppr it is also the
193
+ * BAKE lane: the loader executes at shell capture and its settled return is
194
+ * shell material (nested promises stay live holes) — see the ppr docs.
193
195
  *
194
- * Scoped per LOADER, not per segment: a baked loader alongside a deliberately
195
- * dynamic sibling awaits only itself, and the sibling keeps streaming behind its
196
- * `loading()`/Suspense boundary.
196
+ * Scoped per LOADER, not per segment: an `ssr: false` loader alongside a
197
+ * deliberately streaming sibling awaits only itself, and the sibling keeps
198
+ * streaming behind its `loading()`/Suspense boundary.
197
199
  */
198
200
  export type LoaderOptions = {
199
- stream?: "navigation";
201
+ ssr?: boolean;
200
202
  };
201
203
 
202
204
  /**
@@ -347,13 +347,14 @@ export type PathHelpers<TEnv> = {
347
347
  /**
348
348
  * Attach a data loader to the current route/layout.
349
349
  *
350
- * Pass `{ stream: "navigation" }` to await this loader before first flush
351
- * on DOCUMENT requests (see {@link LoaderOptions}) the opt-in for loaders
352
- * whose data, handle pushes, or thrown notFound()/redirect() must be in the
353
- * SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
354
- * `ppr` route the flag BAKES: the loader executes at shell capture and
355
- * its settled return freezes into the shell (nested promises stay live
356
- * holes) — the pre-flush promise applied to the prelude.
350
+ * Pass `{ ssr: false }` the same knob as loading(fallback, { ssr:
351
+ * false }) to await this loader before first flush on DOCUMENT requests
352
+ * (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
353
+ * pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
354
+ * Per-loader: a streaming sibling keeps streaming. Under a `ppr` route the
355
+ * flag BAKES: the loader executes at shell capture and its settled return
356
+ * freezes into the shell (nested promises stay live holes) — the pre-flush
357
+ * promise applied to the prelude.
357
358
  */
358
359
  loader: <TData>(
359
360
  loaderDef: LoaderDefinition<TData>,