@rangojs/router 0.33.1 → 0.34.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 (63) hide show
  1. package/dist/bin/{chunk-TTXC2U3G.js → chunk-4HHFQSDC.js} +10 -2
  2. package/dist/bin/{chunk-3B7CKBRO.js → chunk-MWOHBDPM.js} +9 -9
  3. package/dist/bin/{client-urls-projection-DQLWPDAT.js → client-urls-projection-4Q5EQDAL.js} +2 -2
  4. package/dist/bin/rango.js +1 -1
  5. package/dist/bin/{runtime-EPUDEHO4.js → runtime-6WLCJCN7.js} +1 -1
  6. package/dist/bin/{runtime-discovery-3HH6BP3K.js → runtime-discovery-2ZTYRZ3V.js} +2 -2
  7. package/dist/bin/{temp-rsc-server-JALTIWLT.js → temp-rsc-server-EUNKRFJD.js} +3 -3
  8. package/dist/testing/vitest.js +5 -0
  9. package/dist/types/browser/history-state.d.ts +21 -0
  10. package/dist/types/browser/navigation-transaction.d.ts +2 -0
  11. package/dist/types/client-urls/server-projection.d.ts +7 -0
  12. package/dist/types/client-urls/types.d.ts +13 -14
  13. package/dist/types/errors.d.ts +5 -0
  14. package/dist/types/loader-store.d.ts +11 -1
  15. package/dist/types/prefetch-markers.d.ts +13 -4
  16. package/dist/types/route-definition/helpers-types.d.ts +14 -10
  17. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +39 -21
  18. package/dist/types/server/context.d.ts +6 -5
  19. package/dist/types/testing/render-route.d.ts +21 -1
  20. package/dist/types/testing/serve-shell-request.d.ts +7 -7
  21. package/dist/types/testing/vitest-stubs/plugin-rsc-browser.d.ts +33 -0
  22. package/dist/types/types/loader-types.d.ts +10 -8
  23. package/dist/types/urls/path-helper-types.d.ts +9 -8
  24. package/dist/vite/index.js +30 -13
  25. package/package.json +1 -1
  26. package/skills/app-structure/large-app.md +3 -0
  27. package/skills/client-urls/SKILL.md +13 -10
  28. package/skills/hooks/data.md +5 -0
  29. package/skills/hooks/state.md +13 -11
  30. package/skills/intercept/SKILL.md +8 -4
  31. package/skills/links/SKILL.md +15 -12
  32. package/skills/loader/SKILL.md +52 -39
  33. package/skills/ppr/SKILL.md +1 -1
  34. package/skills/route/SKILL.md +15 -9
  35. package/skills/testing/SKILL.md +1 -0
  36. package/skills/testing/client-components.md +67 -19
  37. package/skills/testing/serve-shell-request.md +19 -7
  38. package/skills/ux-architecture/SKILL.md +16 -11
  39. package/src/browser/history-state.ts +48 -0
  40. package/src/browser/navigation-bridge.ts +12 -1
  41. package/src/browser/navigation-transaction.ts +14 -6
  42. package/src/browser/partial-update.ts +12 -4
  43. package/src/browser/prefetch/fetch.ts +3 -3
  44. package/src/client-urls/client-urls.ts +11 -7
  45. package/src/client-urls/server-projection.ts +21 -3
  46. package/src/client-urls/types.ts +13 -14
  47. package/src/errors.ts +17 -0
  48. package/src/loader-store.ts +41 -4
  49. package/src/prefetch-markers.ts +18 -4
  50. package/src/route-definition/dsl-helpers.ts +13 -6
  51. package/src/route-definition/helpers-types.ts +14 -10
  52. package/src/router/loader-resolution.ts +4 -4
  53. package/src/router/match-api.ts +1 -3
  54. package/src/router/segment-resolution/prefetch-deferral.ts +108 -50
  55. package/src/rsc/rsc-rendering.ts +1 -1
  56. package/src/server/context.ts +6 -5
  57. package/src/testing/render-route.tsx +122 -7
  58. package/src/testing/serve-shell-request.ts +7 -7
  59. package/src/testing/vitest-stubs/plugin-rsc-browser.ts +61 -0
  60. package/src/testing/vitest.ts +5 -0
  61. package/src/types/loader-types.ts +10 -8
  62. package/src/urls/path-helper-types.ts +9 -8
  63. package/src/use-loader.tsx +29 -7
@@ -11,6 +11,7 @@ import type {
11
11
  DefaultVars,
12
12
  } from "./global-namespace.js";
13
13
  import type { RequestScope } from "./request-scope.js";
14
+ import type { PrefetchOption } from "../prefetch-markers.js";
14
15
 
15
16
  /**
16
17
  * Context passed to loader functions during execution
@@ -207,13 +208,14 @@ export type LoaderFn<
207
208
  export type LoaderOptions = {
208
209
  ssr?: boolean;
209
210
  /**
210
- * `prefetch: false` keeps this loader out of speculative prefetches. A
211
- * viewport or render `<Link>` prefetch (a partial request with
212
- * `X-Rango-Prefetch: 1`) does not execute it; the payload marks it
213
- * deferred. A hover prefetch and `useRouter().prefetch()`
214
- * (`X-Rango-Prefetch: intent`) count as intent to click and run it, so a
215
- * click after a hover needs no follow-up request. A document request and a
216
- * navigation with no prefetch to adopt run it as usual.
211
+ * `prefetch: false` keeps this loader out of every `<Link>` prefetch: a
212
+ * prefetch (a partial request with `X-Rango-Prefetch`) does not execute
213
+ * it; the payload marks it deferred. `prefetch: "intent"` keeps it out of
214
+ * speculative prefetches only: a viewport or render prefetch
215
+ * (`X-Rango-Prefetch: 1`) defers it, and a hover prefetch or
216
+ * `useRouter().prefetch()` (`X-Rango-Prefetch: intent`) runs it, so a click
217
+ * after a hover needs no follow-up request for it. A document request and
218
+ * a navigation with no prefetch to adopt run it as usual.
217
219
  *
218
220
  * A deferred loader never blocks the navigation that adopts the prefetch:
219
221
  * it behaves like a loader that is still streaming. Its own `<Suspense>`
@@ -238,7 +240,7 @@ export type LoaderOptions = {
238
240
  * request only deferred work and middleware run, so values a held handler
239
241
  * would have set with `ctx.set()` are not visible.
240
242
  */
241
- prefetch?: boolean;
243
+ prefetch?: PrefetchOption;
242
244
  };
243
245
 
244
246
  /**
@@ -58,6 +58,7 @@ import type {
58
58
  } from "./pattern-types.js";
59
59
  import type { ExtractRoutes, ExtractResponses } from "./type-extraction.js";
60
60
  import type { ClientUrlPatterns } from "../client-urls/types.js";
61
+ import type { PrefetchOption } from "../prefetch-markers.js";
61
62
 
62
63
  /**
63
64
  * Base path function signature for defining routes with URL patterns.
@@ -364,10 +365,10 @@ export type PathHelpers<TEnv> = {
364
365
  * freezes into the shell (nested promises stay live holes) — the pre-flush
365
366
  * promise applied to the prelude.
366
367
  *
367
- * Pass `{ prefetch: false }` to keep the loader out of speculative
368
- * prefetches (viewport, render): such a prefetch skips it and the click
369
- * that adopts it fetches it in one follow-up request; a hover prefetch runs
370
- * it (see {@link LoaderOptions}). Only on a segment the
368
+ * Pass `{ prefetch: false }` to keep the loader out of prefetches: a
369
+ * prefetch skips it and the click that adopts it fetches it in one
370
+ * follow-up request. `{ prefetch: "intent" }` defers it in viewport and
371
+ * render prefetches only (see {@link LoaderOptions}). Only on a segment the
371
372
  * client does not have yet: a loader the client already holds runs in the
372
373
  * prefetch when it revalidates.
373
374
  */
@@ -381,9 +382,9 @@ export type PathHelpers<TEnv> = {
381
382
  * Attach a loading component to the current route/layout.
382
383
  *
383
384
  * `{ ssr: false }` skips the fallback on document requests. `{ prefetch:
384
- * false }` keeps what the fallback covers out of viewport and render
385
- * prefetches (a hover prefetch runs it), when this entry's segment is new
386
- * to the client: the entry's handler, its loaders,
385
+ * false }` keeps what the fallback covers out of prefetches (`"intent"`:
386
+ * viewport and render prefetches only, see {@link LoaderOptions}), when this
387
+ * entry's segment is new to the client: the entry's handler, its loaders,
387
388
  * its orphan layouts and parallel slots and, for a layout, every deeper
388
389
  * entry. The click that adopts the prefetch shows the fallback and fetches
389
390
  * them in one follow-up request, which reads cache() and "use cache" as
@@ -397,7 +398,7 @@ export type PathHelpers<TEnv> = {
397
398
  */
398
399
  loading: (
399
400
  component: ReactNode | (() => ReactNode),
400
- options?: { ssr?: boolean; prefetch?: boolean },
401
+ options?: { ssr?: boolean; prefetch?: PrefetchOption },
401
402
  ) => LoadingItem;
402
403
 
403
404
  /**
@@ -270,10 +270,15 @@ function useLoaderInternal<T>(
270
270
  const loaders: LoaderStore | undefined = useContext(NavigationStoreContext)
271
271
  ?.store?.loaders;
272
272
 
273
+ // Committed route data (updated by the reset effect below), read by the
274
+ // store when a refresh begins: the snapshot it produces belongs to it.
275
+ const prevContextDataRef = useRef(contextData);
276
+ const routeDataOf = useCallback(() => prevContextDataRef.current, []);
277
+
273
278
  const groupRefetch = useCallback(async (): Promise<void> => {
274
279
  if (!loaderId || !loaders) return;
275
280
  const requestId = loaders.reserveRequestId(bucketKey);
276
- loaders.beginRequest(bucketKey, requestId);
281
+ loaders.beginRequest(bucketKey, requestId, routeDataOf);
277
282
  try {
278
283
  const url = new URL(window.location.href);
279
284
  url.searchParams.set("_rsc_loader", loaderId);
@@ -316,6 +321,15 @@ function useLoaderInternal<T>(
316
321
  sharedState.bucketKey === bucketKey
317
322
  ? sharedState.snapshot
318
323
  : EMPTY_LOADER_SNAPSHOT;
324
+ // A route reader shows a refresh only when it was requested for this
325
+ // reader's route data: never another page's, nor one a navigation replaced
326
+ // (the rule the reset effect below applies when a mounted reader's data
327
+ // changes, here also for a reader that mounts).
328
+ const sharedValue =
329
+ sharedSnapshot.hasValue &&
330
+ (!hasContextData || sharedSnapshot.valueFor.has(contextData));
331
+ const sharedRequest =
332
+ !hasContextData || sharedSnapshot.requestFor.has(contextData);
319
333
  useEffect(() => {
320
334
  if (!loaders) return;
321
335
  const initial = loaders.getSnapshot(bucketKey);
@@ -343,6 +357,7 @@ function useLoaderInternal<T>(
343
357
  onStreamPending: hasContextData
344
358
  ? () => startTransition(() => setStreamPending(true))
345
359
  : undefined,
360
+ routeData: hasContextData ? routeDataOf : undefined,
346
361
  },
347
362
  );
348
363
  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional:
@@ -388,7 +403,6 @@ function useLoaderInternal<T>(
388
403
  // any pre-navigation load() promise that resolves later fails its gate
389
404
  // and is dropped — fixes the race where a stale fetch overwrites the
390
405
  // new route's context.
391
- const prevContextDataRef = useRef(contextData);
392
406
  useEffect(() => {
393
407
  if (prevContextDataRef.current !== contextData) {
394
408
  setLocalFetchedData({ has: false, value: undefined });
@@ -407,11 +421,14 @@ function useLoaderInternal<T>(
407
421
  // `has`/`hasValue` gate each level so a committed falsy value is not skipped.
408
422
  const data = localFetchedData.has
409
423
  ? localFetchedData.value
410
- : sharedSnapshot.hasValue
424
+ : sharedValue
411
425
  ? (sharedSnapshot.value as T | undefined)
412
426
  : contextData;
413
- const isLoading = localIsLoading || sharedSnapshot.isLoading || streamPending;
414
- const error = localError ?? sharedSnapshot.error;
427
+ const isLoading =
428
+ localIsLoading ||
429
+ (sharedRequest && sharedSnapshot.isLoading) ||
430
+ streamPending;
431
+ const error = localError ?? (sharedRequest ? sharedSnapshot.error : null);
415
432
 
416
433
  const throwOnError = options?.throwOnError ?? true;
417
434
 
@@ -446,7 +463,11 @@ function useLoaderInternal<T>(
446
463
  if (shared) {
447
464
  sharedRequestId = loaders.reserveRequestId(bucket);
448
465
  lastSharedRequestIdRef.current = sharedRequestId;
449
- loaders.beginRequest(bucket, sharedRequestId);
466
+ loaders.beginRequest(
467
+ bucket,
468
+ sharedRequestId,
469
+ hasContextDataRef.current ? routeDataOf : undefined,
470
+ );
450
471
  } else {
451
472
  localRequestId = ++localRequestIdRef.current;
452
473
  setLocalIsLoading(true);
@@ -559,12 +580,13 @@ function useLoaderInternal<T>(
559
580
  }
560
581
  }
561
582
  },
562
- [throwOnError, loaders],
583
+ [throwOnError, loaders, routeDataOf],
563
584
  );
564
585
 
565
586
  if (throwOnError) {
566
587
  if (localError) throw localError;
567
588
  if (
589
+ sharedRequest &&
568
590
  sharedSnapshot.error &&
569
591
  lastSharedRequestIdRef.current !== null &&
570
592
  sharedSnapshot.requestId === lastSharedRequestIdRef.current