@rangojs/router 0.25.0 → 0.26.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 (72) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  3. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  4. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  5. package/dist/types/cache/types.d.ts +8 -6
  6. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  7. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  8. package/dist/types/handles/meta.d.ts +5 -0
  9. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  10. package/dist/types/prerender/writable-store.d.ts +2 -3
  11. package/dist/types/router/router-interfaces.d.ts +0 -5
  12. package/dist/types/router/router-options.d.ts +0 -20
  13. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  14. package/dist/types/router-versions.d.ts +8 -6
  15. package/dist/types/rsc/shell-capture-constants.d.ts +4 -3
  16. package/dist/types/rsc/shell-capture.d.ts +16 -2
  17. package/dist/types/rsc/types.d.ts +0 -8
  18. package/dist/types/server/build-version-table.d.ts +10 -13
  19. package/dist/types/server/context.d.ts +26 -2
  20. package/dist/types/server/handle-store.d.ts +17 -6
  21. package/dist/types/server/request-context.d.ts +4 -3
  22. package/dist/types/testing/build-versions.d.ts +0 -3
  23. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  24. package/dist/types/testing/serve-shell-request.d.ts +1 -1
  25. package/dist/types/types/loader-types.d.ts +6 -1
  26. package/dist/vite/index.js +3 -8
  27. package/package.json +1 -1
  28. package/skills/cache-guide/SKILL.md +4 -8
  29. package/skills/caching/SKILL.md +3 -5
  30. package/skills/comparison/references/framework-comparison.md +1 -1
  31. package/skills/hooks/handle-and-actions.md +6 -6
  32. package/skills/host-router/SKILL.md +1 -1
  33. package/skills/loader/SKILL.md +10 -9
  34. package/skills/ppr/SKILL.md +60 -44
  35. package/skills/prerender/SKILL.md +3 -3
  36. package/skills/router-setup/SKILL.md +0 -2
  37. package/skills/testing/render-handler.md +17 -17
  38. package/skills/testing/serve-shell-request.md +1 -1
  39. package/skills/testing/server-actions.md +16 -16
  40. package/skills/vercel/SKILL.md +1 -1
  41. package/src/cache/cf/cf-cache-store.ts +4 -7
  42. package/src/cache/cf/cf-cache-types.ts +0 -17
  43. package/src/cache/handle-snapshot.ts +2 -2
  44. package/src/cache/types.ts +8 -6
  45. package/src/cache/vercel/vercel-cache-store.ts +2 -18
  46. package/src/handles/deferred-resolution.ts +3 -2
  47. package/src/handles/meta.ts +5 -0
  48. package/src/prerender/build-shell-capture.ts +1 -2
  49. package/src/prerender/writable-store.ts +2 -3
  50. package/src/route-definition/dsl-helpers.ts +14 -14
  51. package/src/router/manifest.ts +9 -2
  52. package/src/router/match-middleware/intercept-resolution.ts +2 -2
  53. package/src/router/router-interfaces.ts +0 -6
  54. package/src/router/router-options.ts +0 -21
  55. package/src/router/segment-resolution/loader-cache.ts +4 -3
  56. package/src/router/segment-resolution/mask-nested.ts +5 -3
  57. package/src/router-versions.ts +7 -7
  58. package/src/router.ts +3 -8
  59. package/src/rsc/handler.ts +3 -7
  60. package/src/rsc/shell-capture-constants.ts +4 -3
  61. package/src/rsc/shell-capture.ts +126 -51
  62. package/src/rsc/types.ts +0 -9
  63. package/src/server/build-version-table.ts +12 -24
  64. package/src/server/context.ts +64 -2
  65. package/src/server/handle-store.ts +77 -10
  66. package/src/server/request-context.ts +4 -3
  67. package/src/testing/build-versions.ts +0 -3
  68. package/src/testing/dispatch.ts +1 -3
  69. package/src/testing/internal/seed-vars.ts +4 -4
  70. package/src/testing/serve-shell-request.ts +2 -5
  71. package/src/types/loader-types.ts +6 -1
  72. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
@@ -25,33 +25,24 @@ let table: RouterVersionsTable | undefined = ROUTER_VERSIONS;
25
25
  /**
26
26
  * The versions a router serves with, in order:
27
27
  *
28
- * 1. `override`, a consumer-set `version` (createRouter or createRSCHandler):
29
- * that exact value for both.
30
- * 2. The router's entry in the build table, else the whole-build entry.
31
- * 3. `VERSION` for both: the dev stamp, bumped on every RSC module edit.
28
+ * 1. The router's entry in the build table, else the whole-build entry.
29
+ * 2. `VERSION` for both: the dev stamp, bumped on every RSC module edit.
32
30
  */
33
31
  export function resolveRouterVersions(
34
32
  routerId: string | undefined,
35
- override?: string,
36
33
  ): RouterVersions {
37
- return (
38
- resolveVersionsFrom(table, routerId, override) ?? uniformVersions(VERSION)
39
- );
34
+ return resolveVersionsFrom(table, routerId) ?? uniformVersions(VERSION);
40
35
  }
41
36
 
42
37
  /**
43
38
  * The version an on-demand prerender overlay key carries: the router's data
44
39
  * version, because the overlay stores segment data, as the segment cache does
45
40
  * (a client-only deploy keeps both). The write side (`router.prerender`,
46
- * router.ts) and the read side (rsc/handler.ts) both call this with
47
- * `createRouter({ version })`, the only override the trigger can see, so they
48
- * cannot disagree; a `createRSCHandler({ version })` does not move it.
41
+ * router.ts) and the read side (rsc/handler.ts) both call this, so they
42
+ * cannot disagree.
49
43
  */
50
- export function resolvePrerenderVersion(
51
- routerId: string | undefined,
52
- routerVersion: string | undefined,
53
- ): string {
54
- return resolveRouterVersions(routerId, routerVersion).data;
44
+ export function resolvePrerenderVersion(routerId: string | undefined): string {
45
+ return resolveRouterVersions(routerId).data;
55
46
  }
56
47
 
57
48
  /** The table gives some router a pair of its own. */
@@ -104,16 +95,13 @@ export type KeyVersion = keyof RouterVersions | null;
104
95
 
105
96
  /**
106
97
  * The `v/{version}/` prefix of a persistent store key, or "" for an
107
- * unversioned one. A store-level `version` is used for every versioned kind;
108
- * otherwise the version is the serving router's, read per operation because
109
- * the cache factory builds the store before the request context exists.
98
+ * unversioned one. The version is the serving router's, read per operation
99
+ * because the cache factory builds the store before the request context
100
+ * exists.
110
101
  */
111
- export function versionKeyPrefix(
112
- storeVersion: string | undefined,
113
- kind: KeyVersion,
114
- ): string {
102
+ export function versionKeyPrefix(kind: KeyVersion): string {
115
103
  if (kind === null) return "";
116
- const version = storeVersion ?? getCacheVersions()[kind];
104
+ const version = getCacheVersions()[kind];
117
105
  return version ? `v/${version}/` : "";
118
106
  }
119
107
 
@@ -130,6 +130,11 @@ export type EntryPropRender = {
130
130
  * on both DSL evaluations, whatever `ssr` did to `loading`.
131
131
  */
132
132
  loadingPrefetch?: false;
133
+ /**
134
+ * loading(fallback, { ssr: false }), on both DSL evaluations; a document
135
+ * then sets `loading` to false (applyDocumentModeLoading).
136
+ */
137
+ loadingSsr?: false;
133
138
  transition?: TransitionConfig;
134
139
  };
135
140
 
@@ -151,7 +156,8 @@ export type LoaderEntry = {
151
156
  * Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
152
157
  * per-isSSR (router/manifest.ts cache key), so the flag is already
153
158
  * request-mode-correct when resolveLoaders (fresh.ts) reads it and never
154
- * appears on navigation-lane entries.
159
+ * appears on navigation-lane entries. Set by documentModeLoader, which
160
+ * getDocumentModeEntry also applies to a lazy include's captured ancestors.
155
161
  */
156
162
  awaitBeforeFlush?: true;
157
163
  /**
@@ -388,7 +394,8 @@ interface HelperContext extends IncludeScopePosition {
388
394
  * different routers don't clobber each other (route-map-builder.ts). */
389
395
  routerId?: string;
390
396
  metrics?: MetricsStore;
391
- /** True when rendering for SSR (document requests) */
397
+ /** True when rendering for SSR (document requests). DSL decisions on it
398
+ * go through the document-mode helpers (documentModeLoader). */
392
399
  isSSR?: boolean;
393
400
  /** URL patterns map for path() routes (route name -> pattern) */
394
401
  patterns?: Map<string, string>;
@@ -893,6 +900,61 @@ export function getIsolatedLazyParent(
893
900
  };
894
901
  }
895
902
 
903
+ /*
904
+ * The DSL's document-only decisions. A document (isSSR) evaluation applies
905
+ * them in dsl-helpers.ts; getDocumentModeEntry applies the same ones to a
906
+ * chain built without isSSR. A new ctx.isSSR decision gets a helper here,
907
+ * called from both.
908
+ */
909
+
910
+ /** loader(Def, { ssr: false }): a document awaits it before the first flush. */
911
+ export function documentModeLoader(entry: LoaderEntry): LoaderEntry {
912
+ return entry.bake ? { ...entry, awaitBeforeFlush: true } : entry;
913
+ }
914
+
915
+ /** loading(fallback, { ssr: false }): a document renders no fallback. */
916
+ export function applyDocumentModeLoading(entry: EntryPropRender): void {
917
+ if (entry.loadingSsr === false) entry.loading = false;
918
+ }
919
+
920
+ const documentModeEntries = new WeakMap<EntryData, EntryData>();
921
+
922
+ /**
923
+ * `entry`, its ancestors, orphan layouts and parallels as a document (isSSR)
924
+ * DSL evaluation builds them, from an evaluation without isSSR.
925
+ *
926
+ * include() captures its parent chain on router.ts routes() or
927
+ * lazy-includes.ts runExpansion, neither of which has isSSR, and loadManifest
928
+ * reuses it for every request. Handed to a document as is, every ancestor
929
+ * outside the include kept its navigation-mode decisions (#1179). Intercepts
930
+ * stay as they are: a document never resolves one.
931
+ *
932
+ * Memoized per entry, so an ancestor several includes share maps to one copy.
933
+ */
934
+ export function getDocumentModeEntry(entry: EntryData): EntryData {
935
+ const known = documentModeEntries.get(entry);
936
+ if (known) return known;
937
+ const copy = { ...entry };
938
+ documentModeEntries.set(entry, copy);
939
+ documentModeEntries.set(copy, copy);
940
+ copy.loader = entry.loader.map(documentModeLoader);
941
+ applyDocumentModeLoading(copy);
942
+ copy.layout = entry.layout.map(getDocumentModeEntry);
943
+ const parallel: ParallelEntries = {};
944
+ for (const slot of Object.keys(entry.parallel) as `@${string}`[]) {
945
+ const slotEntry = entry.parallel[slot];
946
+ if (slotEntry) {
947
+ parallel[slot] = getDocumentModeEntry(slotEntry) as ParallelEntryData;
948
+ }
949
+ }
950
+ copy.parallel = parallel;
951
+ if (entry.parent) copy.parent = getDocumentModeEntry(entry.parent);
952
+ if (entry.orphanOwner) {
953
+ copy.orphanOwner = getDocumentModeEntry(entry.orphanOwner);
954
+ }
955
+ return copy;
956
+ }
957
+
896
958
  export function getParallelEntries(
897
959
  parallels: ParallelEntries | EntryData[] | undefined,
898
960
  ): ParallelEntryData[] {
@@ -4,6 +4,7 @@ import {
4
4
  isInsideLoaderScope,
5
5
  } from "./context.js";
6
6
  import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
7
+ import { isThenable } from "../handles/is-thenable.js";
7
8
  import { pushedByKey } from "../handles/pushed-by.js";
8
9
  import { defineSlot } from "./slot-accessors.js";
9
10
 
@@ -254,12 +255,14 @@ export interface HandleStore {
254
255
  ): void;
255
256
 
256
257
  /**
257
- * push() a record's copy of `loaderId`'s settled push that stands
258
- * (restoreHandles): the record supplies the loader's value too. From then
259
- * on this request drops every settled, thenable-free push the loader makes
260
- * (live, or replayed from a cache entry, anywhere inside its body), and
261
- * keeps the ones holding a thenable (holdsThenable: what the record could
262
- * not keep), placed among the restored copies in the loader's push order.
258
+ * push() a record's copy of `loaderId`'s push that stands (restoreHandles):
259
+ * the record supplies the loader's value too. From then on this request
260
+ * drops every thenable-free push the loader makes (live, or replayed from
261
+ * a cache entry, anywhere inside its body), and keeps the ones holding a
262
+ * thenable (holdsThenable: what the record could not keep), placed among
263
+ * the restored copies in the loader's push order. A top-level promise is
264
+ * judged by its settled value, as the capture judged it (#1182): dropped
265
+ * when it settles thenable-free or rejects, else kept once it settles.
263
266
  */
264
267
  pushRestored(
265
268
  handleName: string,
@@ -278,6 +281,16 @@ export interface HandleStore {
278
281
  */
279
282
  setRecordAuthority(authority: (loaderId: string) => RecordAuthority): void;
280
283
 
284
+ /**
285
+ * Capture only (shell-capture.ts deriveShellCaptureContext): the slot
286
+ * holding `value` becomes a DSL-loader push without an owner, so the
287
+ * record and the prelude leave it out (getDataForSegment's
288
+ * `excludeLoaderPushes`). The capture owns a bake-lane loader's promise
289
+ * when it is pushed, and calls this once the promise settled to a value
290
+ * that holds one (#1182).
291
+ */
292
+ unrecordPush(handleName: string, segmentId: string, value: unknown): void;
293
+
281
294
  /**
282
295
  * `loaderId`'s run in this request settled (resolved or rejected), its
283
296
  * pushes made (loader-resolution.ts createLoaderExecutor). Its placeholders
@@ -666,6 +679,39 @@ export function createHandleStore(): HandleStore {
666
679
  return after;
667
680
  }
668
681
 
682
+ // A restored loader's top-level promise push, judged by its settled value
683
+ // as push(await value) would be: thenable-free, the record's copy stands
684
+ // (the capture recorded that value, #1182); holding a promise, it is kept
685
+ // where the loader's pushes go, once it settles; rejected, it is dropped.
686
+ // Tracked on the auxiliary lane, so the late channel stays open for it.
687
+ function judgeRestoredPromise(
688
+ store: HandleStore,
689
+ handleName: string,
690
+ segmentId: string,
691
+ value: unknown,
692
+ loaderId: string,
693
+ tag: SlotTag,
694
+ ): void {
695
+ const judged = Promise.resolve(value).then(
696
+ (settled) => {
697
+ if (!holdsThenable(settled)) {
698
+ const existing = data[handleName]?.[segmentId];
699
+ if (existing) matchRestoredSlot(existing, loaderId);
700
+ return;
701
+ }
702
+ const values = ((data[handleName] ??= {})[segmentId] ??= []);
703
+ insertSlot(values, restoredInsertIndex(values, loaderId), value, tag);
704
+ if (tag.replayOf !== undefined) {
705
+ addPending(pendingReplays, tag.replayOf, values);
706
+ }
707
+ version++;
708
+ signalEmission();
709
+ },
710
+ () => {},
711
+ );
712
+ store.trackAuxiliary(judged);
713
+ }
714
+
669
715
  function replayPush(
670
716
  store: HandleStore,
671
717
  replay: NonNullable<typeof replaying>,
@@ -860,10 +906,22 @@ export function createHandleStore(): HandleStore {
860
906
  restoredBy = restoringLoader(replayOf);
861
907
  }
862
908
  }
863
- if (restoredBy !== undefined && !holdsThenable(value)) {
864
- const existing = data[handleName]?.[segmentId];
865
- if (existing) matchRestoredSlot(existing, restoredBy);
866
- return;
909
+ if (restoredBy !== undefined) {
910
+ if (isThenable(value)) {
911
+ judgeRestoredPromise(this, handleName, segmentId, value, restoredBy, {
912
+ loader: (loaderPush ?? isInsideLoaderScope()) || undefined,
913
+ replayOf,
914
+ liveOf: replayOf === undefined ? restoredBy : undefined,
915
+ owner,
916
+ by: getHandlePushOwner() ?? replayOf ?? owner,
917
+ });
918
+ return;
919
+ }
920
+ if (!holdsThenable(value)) {
921
+ const existing = data[handleName]?.[segmentId];
922
+ if (existing) matchRestoredSlot(existing, restoredBy);
923
+ return;
924
+ }
867
925
  }
868
926
 
869
927
  if (!data[handleName]) {
@@ -977,6 +1035,15 @@ export function createHandleStore(): HandleStore {
977
1035
  authority = of;
978
1036
  },
979
1037
 
1038
+ unrecordPush(handleName: string, segmentId: string, value: unknown): void {
1039
+ const values = data[handleName]?.[segmentId];
1040
+ if (!values) return;
1041
+ const i = values.indexOf(value);
1042
+ if (i < 0) return;
1043
+ const tags = tagsFor(values);
1044
+ tags[i] = { ...tags[i], loader: true, owner: undefined };
1045
+ },
1046
+
980
1047
  settleLoaderRun(loaderId: string): void {
981
1048
  if (completed) return;
982
1049
  const hole = isHole(loaderId);
@@ -368,9 +368,10 @@ export interface RequestContext<
368
368
 
369
369
  /**
370
370
  * @internal Set on a capture's derived context when a loader pushed a
371
- * value the record cannot keep (a deferred push, masked nested promises,
372
- * or a push outside a loader body): the loader records then carry
373
- * `runs: 1`, so a HIT still runs their bodies for those pushes.
371
+ * value the record cannot keep (masked nested promises, a pushed promise
372
+ * that settled to one, a live-lane loader's push, or a push outside a
373
+ * loader body): the loader records then carry `runs: 1`, so a HIT still
374
+ * runs their bodies for those pushes.
374
375
  */
375
376
  _shellCaptureUnrecordedLoaderPush?: true;
376
377
 
@@ -35,9 +35,6 @@ export interface BuildVersions extends RouterVersions {
35
35
  * Install the cache versions of a simulated build, or remove them with no
36
36
  * argument (routers serve unversioned again, the default in tests).
37
37
  *
38
- * A router that sets `createRouter({ version })` keeps that value for both
39
- * versions, exactly as in production.
40
- *
41
38
  * @example A deploy that changes the client only keeps cached data
42
39
  * ```ts
43
40
  * setBuildVersions({ data: "d1", document: "h1" });
@@ -167,8 +167,6 @@ import { applyStreamIdleTimeout } from "../rsc/stream-idle.js";
167
167
  interface DispatchableRouter<TEnv> {
168
168
  id?: string;
169
169
  routerId?: string;
170
- /** createRouter({ version }), the consumer-set version. */
171
- version?: string;
172
170
  routeMap: Record<string, unknown>;
173
171
  /** The routes() patterns the dev trie is rebuilt from (rsc/manifest-init.ts). */
174
172
  urlpatterns?: unknown;
@@ -448,7 +446,7 @@ export async function dispatch<TEnv = any>(
448
446
  cacheProfiles: router.cacheProfiles,
449
447
  // The store keys with the router's versions, resolved as the production
450
448
  // handler resolves them (rsc/handler.ts).
451
- versions: resolveRouterVersions(routerId, router.version),
449
+ versions: resolveRouterVersions(routerId),
452
450
  });
453
451
  // Wire background error reporting so cache degradation (reportCacheError ->
454
452
  // _reportBackgroundError) reaches the router's onError, mirroring the production
@@ -23,10 +23,10 @@ export interface StateCookieSeed {
23
23
  */
24
24
  routerId?: string;
25
25
  /**
26
- * The app's version, as `createRouter({ version })` would set it: the
27
- * rotated value's prefix (`{version}:{timestamp}`) and the version a cache
28
- * store keys with. Defaults to `"0"` for the prefix (resolved inside
29
- * createRequestContext) and to the build versions for the store.
26
+ * The app's version: the rotated value's prefix (`{version}:{timestamp}`)
27
+ * and the request's cache versions, which a store keys with. Defaults to
28
+ * `"0"` for the prefix (resolved inside createRequestContext) and to the
29
+ * build versions for the store.
30
30
  */
31
31
  version?: string;
32
32
  }
@@ -2,7 +2,7 @@
2
2
  * serveShellRequest — one request through the production PPR serve path.
3
3
  *
4
4
  * Builds the router's request handler as `router.fetch` does
5
- * (`createRSCHandler` with the router's `nonce`, `version` and `cache`
5
+ * (`createRSCHandler` with the router's `nonce` and `cache`
6
6
  * config) and serves one GET through it, so middleware, the shell serve path
7
7
  * and a MISS's background capture all run production code, on real Flight
8
8
  * (the vendored react-server-dom serializer). The call settles every
@@ -537,10 +537,7 @@ async function getHandler(
537
537
  const { resolveRouterVersions } =
538
538
  await import("../server/build-version-table.js");
539
539
  // What createRSCHandler would resolve for this router now.
540
- const { document: versions } = resolveRouterVersions(
541
- internal.id,
542
- internal.version,
543
- );
540
+ const { document: versions } = resolveRouterVersions(internal.id);
544
541
  const cached = byStore.get(cacheStore ?? router);
545
542
  if (cached?.versions === versions) return cached.handler;
546
543
 
@@ -189,7 +189,12 @@ export type LoaderFn<
189
189
  * deterministically precedes Response construction, where the streamed
190
190
  * default only wins that race opportunistically). Under ppr it is also the
191
191
  * BAKE lane: the loader executes at shell capture and its settled return is
192
- * shell material (nested promises stay live holes) — see the ppr docs.
192
+ * shell material (nested promises stay live holes), and so are its handle
193
+ * pushes, a pushed promise or `.defer()` included: the capture awaits it and
194
+ * every HIT serves the value it settled to (#1182). A pushed promise that
195
+ * rejects, or misses `ppr.captureTimeout`, keeps the route on MISS. A value
196
+ * that must stay per request belongs to a loader without `ssr: false` — see
197
+ * the ppr docs.
193
198
  *
194
199
  * Scoped per LOADER, not per segment: an `ssr: false` loader alongside a
195
200
  * deliberately streaming sibling awaits only itself, and the sibling keeps
@@ -231,11 +231,7 @@ export async function runShellPrerenderPhase(
231
231
  for (const [routerId, routerInstance] of registry) {
232
232
  if (typeof routerInstance.match !== "function") continue;
233
233
  // The build's table always holds the whole-build pair.
234
- const versions = resolveVersionsFrom(
235
- versionsTable,
236
- routerId,
237
- routerInstance.version,
238
- )!;
234
+ const versions = resolveVersionsFrom(versionsTable, routerId)!;
239
235
  try {
240
236
  const res = await captureMod.captureShellForBuild({
241
237
  router: routerInstance,