@rangojs/router 0.23.0 → 0.24.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 (128) hide show
  1. package/dist/bin/rango.js +4 -3
  2. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  3. package/dist/types/browser/partial-update.d.ts +6 -4
  4. package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
  5. package/dist/types/browser/react/context.d.ts +3 -2
  6. package/dist/types/browser/types.d.ts +5 -1
  7. package/dist/types/build/route-types/scan-filter.d.ts +7 -1
  8. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  9. package/dist/types/cache/document-cache.d.ts +2 -10
  10. package/dist/types/context-var.d.ts +12 -0
  11. package/dist/types/errors.d.ts +10 -0
  12. package/dist/types/handles/pushed-by.d.ts +8 -0
  13. package/dist/types/index.d.ts +4 -0
  14. package/dist/types/index.rsc.d.ts +2 -0
  15. package/dist/types/response-utils.d.ts +7 -0
  16. package/dist/types/reverse.d.ts +8 -8
  17. package/dist/types/route-definition/rewrite.d.ts +56 -0
  18. package/dist/types/route-map-builder.d.ts +7 -2
  19. package/dist/types/router/basename.d.ts +7 -0
  20. package/dist/types/router/handler-context.d.ts +1 -5
  21. package/dist/types/router/internal-params.d.ts +8 -0
  22. package/dist/types/router/middleware-types.d.ts +15 -0
  23. package/dist/types/router/revalidation.d.ts +1 -2
  24. package/dist/types/router/route-trie-builder.d.ts +10 -0
  25. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  26. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
  27. package/dist/types/rsc/full-payload.d.ts +11 -0
  28. package/dist/types/rsc/handler.d.ts +33 -0
  29. package/dist/types/rsc/helpers.d.ts +8 -0
  30. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  31. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  32. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  33. package/dist/types/server/request-context.d.ts +39 -2
  34. package/dist/types/server/rewrite-trail.d.ts +49 -0
  35. package/dist/types/server/slot-accessors.d.ts +29 -0
  36. package/dist/types/testing/index.d.ts +1 -1
  37. package/dist/types/testing/render-route.d.ts +8 -0
  38. package/dist/types/testing/run-middleware.d.ts +20 -0
  39. package/dist/types/testing/serve-shell-request.d.ts +2 -1
  40. package/dist/types/thenable-status.d.ts +6 -0
  41. package/dist/types/types/request-scope.d.ts +7 -0
  42. package/dist/types/vite/discovery/state.d.ts +12 -9
  43. package/dist/vite/index.js +164 -103
  44. package/package.json +1 -1
  45. package/skills/caching/SKILL.md +12 -0
  46. package/skills/catalog.json +1 -1
  47. package/skills/client-urls/SKILL.md +84 -0
  48. package/skills/hooks/SKILL.md +11 -6
  49. package/skills/hooks/data.md +3 -1
  50. package/skills/loader/SKILL.md +7 -1
  51. package/skills/middleware/SKILL.md +183 -2
  52. package/skills/ppr/SKILL.md +9 -0
  53. package/skills/rango/SKILL.md +1 -1
  54. package/skills/server-actions/SKILL.md +27 -0
  55. package/skills/testing/client-components.md +1 -0
  56. package/skills/testing/middleware.md +31 -10
  57. package/skills/testing/response-routes.md +1 -0
  58. package/skills/testing/serve-shell-request.md +19 -1
  59. package/skills/use-cache/SKILL.md +4 -2
  60. package/src/__augment-tests__/augmented.check.ts +20 -0
  61. package/src/browser/event-controller.ts +3 -0
  62. package/src/browser/merge-segment-loaders.ts +31 -18
  63. package/src/browser/navigation-bridge.ts +19 -3
  64. package/src/browser/partial-update.ts +27 -12
  65. package/src/browser/react/Link.tsx +7 -9
  66. package/src/browser/react/NavigationProvider.tsx +46 -26
  67. package/src/browser/react/context.ts +3 -2
  68. package/src/browser/react/use-router.ts +2 -4
  69. package/src/browser/server-action-bridge.ts +6 -0
  70. package/src/browser/types.ts +5 -1
  71. package/src/build/route-types/scan-filter.ts +11 -4
  72. package/src/cache/document-cache-policy.ts +131 -0
  73. package/src/cache/document-cache.ts +12 -119
  74. package/src/context-var.ts +32 -2
  75. package/src/errors.ts +19 -0
  76. package/src/handles/pushed-by.ts +26 -0
  77. package/src/index.rsc.ts +8 -0
  78. package/src/index.ts +7 -0
  79. package/src/response-utils.ts +19 -0
  80. package/src/reverse.ts +17 -14
  81. package/src/route-definition/redirect.ts +2 -15
  82. package/src/route-definition/rewrite.ts +146 -0
  83. package/src/route-map-builder.ts +14 -11
  84. package/src/router/basename.ts +20 -0
  85. package/src/router/handler-context.ts +3 -13
  86. package/src/router/intercept-resolution.ts +29 -12
  87. package/src/router/internal-params.ts +16 -0
  88. package/src/router/loader-resolution.ts +2 -0
  89. package/src/router/match-middleware/background-revalidation.ts +11 -9
  90. package/src/router/match-middleware/cache-lookup.ts +20 -15
  91. package/src/router/match-middleware/cache-store.ts +9 -11
  92. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  93. package/src/router/middleware-types.ts +16 -0
  94. package/src/router/middleware.ts +127 -49
  95. package/src/router/revalidation.ts +1 -1
  96. package/src/router/route-trie-builder.ts +28 -1
  97. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  98. package/src/router/segment-resolution/mask-nested.ts +8 -4
  99. package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
  100. package/src/router/segment-resolution/revalidation.ts +57 -33
  101. package/src/rsc/full-payload.ts +37 -5
  102. package/src/rsc/handler.ts +82 -55
  103. package/src/rsc/helpers.ts +24 -0
  104. package/src/rsc/loader-fetch.ts +79 -24
  105. package/src/rsc/response-cache-serve.ts +18 -2
  106. package/src/rsc/response-route-handler.ts +4 -7
  107. package/src/rsc/rewrite-passes.ts +203 -0
  108. package/src/rsc/rsc-rendering.ts +4 -2
  109. package/src/rsc/ssr-setup.ts +11 -5
  110. package/src/server/cookie-parse.ts +36 -12
  111. package/src/server/cookie-store.ts +7 -3
  112. package/src/server/handle-store.ts +48 -17
  113. package/src/server/request-context.ts +154 -63
  114. package/src/server/request-headers.ts +4 -14
  115. package/src/server/rewrite-trail.ts +190 -0
  116. package/src/server/slot-accessors.ts +57 -0
  117. package/src/ssr/index.tsx +118 -16
  118. package/src/testing/dispatch.ts +138 -67
  119. package/src/testing/index.ts +1 -0
  120. package/src/testing/render-route.tsx +16 -3
  121. package/src/testing/run-loader.ts +2 -0
  122. package/src/testing/run-middleware.ts +56 -11
  123. package/src/testing/serve-shell-request.ts +15 -2
  124. package/src/thenable-status.ts +11 -0
  125. package/src/types/request-scope.ts +8 -0
  126. package/src/vite/discovery/state.ts +21 -13
  127. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  128. package/src/vite/router-discovery.ts +29 -27
@@ -5,6 +5,7 @@ import {
5
5
  } from "./context.js";
6
6
  import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
7
7
  import { pushedByKey } from "../handles/pushed-by.js";
8
+ import { defineSlot } from "./slot-accessors.js";
8
9
 
9
10
  /**
10
11
  * Handle data structure: handleName -> segmentId -> entries[]
@@ -394,6 +395,34 @@ export interface HandleStore {
394
395
  ): void;
395
396
  }
396
397
 
398
+ /**
399
+ * settled and fullySettled: shared descriptors (slot-accessors.ts) that call
400
+ * the store's closure in SETTLEMENT_SLOT with the lane they wait for.
401
+ */
402
+ const SETTLEMENT_SLOT: unique symbol = Symbol("rango.handleStore.settlement");
403
+
404
+ type SettlementSlot = { [SETTLEMENT_SLOT]: (full: boolean) => Promise<void> };
405
+
406
+ const SETTLEMENT_ACCESSORS: Record<
407
+ "settled" | "fullySettled",
408
+ PropertyDescriptor
409
+ > = {
410
+ settled: {
411
+ get(this: SettlementSlot): Promise<void> {
412
+ return this[SETTLEMENT_SLOT](false);
413
+ },
414
+ enumerable: true,
415
+ configurable: true,
416
+ },
417
+ fullySettled: {
418
+ get(this: SettlementSlot): Promise<void> {
419
+ return this[SETTLEMENT_SLOT](true);
420
+ },
421
+ enumerable: true,
422
+ configurable: true,
423
+ },
424
+ };
425
+
397
426
  /**
398
427
  * Create a new HandleStore instance.
399
428
  *
@@ -774,7 +803,9 @@ export function createHandleStore(): HandleStore {
774
803
  });
775
804
  }
776
805
 
777
- return {
806
+ // Data members only: the accessors are SETTLEMENT_ACCESSORS.
807
+ const store: Omit<HandleStore, "settled" | "fullySettled"> &
808
+ ThisType<HandleStore> = {
778
809
  track<T>(promise: Promise<T>): Promise<T> {
779
810
  inflightCount++;
780
811
  return trackSettlement(promise, () => {
@@ -793,22 +824,6 @@ export function createHandleStore(): HandleStore {
793
824
  sealInternal();
794
825
  },
795
826
 
796
- get settled(): Promise<void> {
797
- if (sealed && inflightCount === 0) return Promise.resolve();
798
- return new Promise<void>((resolve) => {
799
- drainWaiters.push(resolve);
800
- });
801
- },
802
-
803
- get fullySettled(): Promise<void> {
804
- if (sealed && inflightCount === 0 && auxInflightCount === 0) {
805
- return Promise.resolve();
806
- }
807
- return new Promise<void>((resolve) => {
808
- fullDrainWaiters.push(resolve);
809
- });
810
- },
811
-
812
827
  push(
813
828
  handleName: string,
814
829
  segmentId: string,
@@ -1213,4 +1228,20 @@ export function createHandleStore(): HandleStore {
1213
1228
  signalEmission();
1214
1229
  },
1215
1230
  };
1231
+ // settled: the handler lane drained; fullySettled: the auxiliary lane too.
1232
+ defineSlot(store, SETTLEMENT_SLOT, (full: boolean): Promise<void> => {
1233
+ if (sealed && inflightCount === 0 && (!full || auxInflightCount === 0)) {
1234
+ return Promise.resolve();
1235
+ }
1236
+ return new Promise<void>((resolve) => {
1237
+ (full ? fullDrainWaiters : drainWaiters).push(resolve);
1238
+ });
1239
+ });
1240
+ Object.defineProperty(store, "settled", SETTLEMENT_ACCESSORS.settled);
1241
+ Object.defineProperty(
1242
+ store,
1243
+ "fullySettled",
1244
+ SETTLEMENT_ACCESSORS.fullySettled,
1245
+ );
1246
+ return store as HandleStore;
1216
1247
  }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
14
  import { parseCookiesFromHeader } from "./cookie-parse.js";
15
+ import { defineHidden, defineSlot, slotAccessor } from "./slot-accessors.js";
15
16
  import type { CacheErrorCategory } from "../cache/cache-error.js";
16
17
  import type { CookieOptions } from "../router/middleware.js";
17
18
  import {
@@ -31,6 +32,7 @@ import type {
31
32
  import type { Handle } from "../handle.js";
32
33
  import {
33
34
  type ContextVar,
35
+ assignOwn,
34
36
  contextGet,
35
37
  contextSet,
36
38
  isNonCacheable,
@@ -94,6 +96,10 @@ import {
94
96
  type ExecutionStart,
95
97
  } from "../cache/invalidation-order.js";
96
98
  import { invariant } from "../errors.js";
99
+ import {
100
+ defineRequestRewrittenFrom,
101
+ defineRewrittenFrom,
102
+ } from "./rewrite-trail.js";
97
103
  import { isAutoGeneratedRouteName } from "../route-name.js";
98
104
 
99
105
  export interface RenderForegroundCursor {
@@ -729,6 +735,14 @@ export interface RequestContext<
729
735
  */
730
736
  _requestKind?: "prefetch" | "fill";
731
737
 
738
+ /**
739
+ * @internal Set when a render shows the visitor's URL of a rewritten
740
+ * request: the payload or SSR seed (renderedLocation, rsc/full-payload.ts)
741
+ * or a `ctx.rewrittenFrom` read (server/rewrite-trail.ts). The document
742
+ * cache does not store that response: it is keyed by the target.
743
+ */
744
+ _payloadVisitorLocation?: boolean;
745
+
732
746
  /**
733
747
  * Attach location state entries to the current response.
734
748
  *
@@ -1030,6 +1044,7 @@ export type PublicRequestContext<
1030
1044
  | "_payloadVisitorTheme"
1031
1045
  | "_payloadDeferred"
1032
1046
  | "_requestKind"
1047
+ | "_payloadVisitorLocation"
1033
1048
  | "_locationState"
1034
1049
  | "_routeName"
1035
1050
  | "_prevRouteKey"
@@ -1087,6 +1102,60 @@ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
1087
1102
  // AsyncLocalStorage instance for request context
1088
1103
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
1089
1104
 
1105
+ // The request context's accessors: shared descriptors (slot-accessors.ts).
1106
+ const RES_SLOT: unique symbol = Symbol("rango.requestContext.res");
1107
+ const RENDER_BARRIER_SLOT: unique symbol = Symbol(
1108
+ "rango.requestContext.renderBarrier",
1109
+ );
1110
+
1111
+ const RES_DESCRIPTOR: PropertyDescriptor = {
1112
+ ...slotAccessor(RES_SLOT, true),
1113
+ set(_: Response): void {
1114
+ throw new Error(
1115
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
1116
+ );
1117
+ },
1118
+ };
1119
+
1120
+ // getRequestContext().theme is the visitor's cookie, guarded like cookies()
1121
+ // (#971); before it was a plain read that a ppr shell, a cache() entry or a
1122
+ // "use cache" entry stored for every visitor. Non-enumerable like the
1123
+ // handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
1124
+ // context (React dev debug info) must not count as a read. The guard reads
1125
+ // the ambient context, as cookies() does, and falls back to the receiver.
1126
+ const REQUEST_THEME_DESCRIPTOR: PropertyDescriptor = {
1127
+ get(this: RequestContext<any>): Theme | undefined {
1128
+ return readGuardedTheme(this, "getRequestContext().theme");
1129
+ },
1130
+ enumerable: false,
1131
+ configurable: true,
1132
+ };
1133
+
1134
+ // A fresh context enumerates _renderBarrier like its other fields (the
1135
+ // fetchable-loader ctx spreads it, loader-fetch.ts); a derived one never did.
1136
+ const RENDER_BARRIER_DESCRIPTOR: PropertyDescriptor = slotAccessor(
1137
+ RENDER_BARRIER_SLOT,
1138
+ true,
1139
+ );
1140
+ const DERIVED_RENDER_BARRIER_DESCRIPTOR: PropertyDescriptor = {
1141
+ ...RENDER_BARRIER_DESCRIPTOR,
1142
+ enumerable: false,
1143
+ };
1144
+
1145
+ /**
1146
+ * Define `ctx.res` as an own accessor that reads `get`. Assigning it throws
1147
+ * the read-only error. A context derived from another (Object.create) gets
1148
+ * its own response this way; copying the descriptor would read the
1149
+ * prototype's slot.
1150
+ */
1151
+ export function bindRes(
1152
+ ctx: RequestContext<any, any>,
1153
+ get: () => Response,
1154
+ ): void {
1155
+ defineSlot(ctx, RES_SLOT, get);
1156
+ Object.defineProperty(ctx, "res", RES_DESCRIPTOR);
1157
+ }
1158
+
1090
1159
  /**
1091
1160
  * Capture the current async context (RequestContext._runAtRequestEntry), taken
1092
1161
  * on handler entry before any router scope, so a nested dispatch (a
@@ -1416,10 +1485,10 @@ export function createRequestContext<TEnv>(
1416
1485
  }
1417
1486
  const result: Record<string, string> = {};
1418
1487
  for (const key of Object.keys(parsed)) {
1419
- if (!deleted.has(key)) result[key] = parsed[key];
1488
+ if (!deleted.has(key)) assignOwn(result, key, parsed[key]);
1420
1489
  }
1421
1490
  for (const [k, v] of mutations) {
1422
- if (v !== null) result[k] = v;
1491
+ if (v !== null) assignOwn(result, k, v);
1423
1492
  }
1424
1493
  return result;
1425
1494
  };
@@ -1465,7 +1534,11 @@ export function createRequestContext<TEnv>(
1465
1534
  const cleanUrl = stripInternalParams(url);
1466
1535
  const originalUrl = new URL(request.url);
1467
1536
 
1468
- const ctx: RequestContext<TEnv> = {
1537
+ // Data members only: the accessors and hidden members are defined after it.
1538
+ const fields: Omit<
1539
+ RequestContext<TEnv>,
1540
+ "res" | "_renderBarrier" | "_readTheme" | "_readCookie" | "_readCookies"
1541
+ > = {
1469
1542
  env,
1470
1543
  request,
1471
1544
  url: cleanUrl,
@@ -1496,15 +1569,6 @@ export function createRequestContext<TEnv>(
1496
1569
  }) as RequestContext<TEnv>["set"],
1497
1570
  params: {} as Record<string, string>,
1498
1571
 
1499
- get res(): Response {
1500
- return stubResponse;
1501
- },
1502
- set res(_: Response) {
1503
- throw new Error(
1504
- "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
1505
- );
1506
- },
1507
-
1508
1572
  // The visitor's cookies, guarded like cookies() (#976): before, a plain
1509
1573
  // read that a ppr shell, a cache() entry or a "use cache" entry stored
1510
1574
  // for every visitor.
@@ -1518,10 +1582,6 @@ export function createRequestContext<TEnv>(
1518
1582
  return effectiveCookies();
1519
1583
  },
1520
1584
 
1521
- // Non-enumerable, defined after this literal.
1522
- _readCookie: effectiveCookie,
1523
- _readCookies: effectiveCookies,
1524
-
1525
1585
  setCookie(name: string, value: string, options?: CookieOptions): void {
1526
1586
  assertNotInsideCacheExec(ctx, "setCookie");
1527
1587
  stubResponse.headers.append(
@@ -1710,8 +1770,6 @@ export function createRequestContext<TEnv>(
1710
1770
  this._onResponseCallbacks.push(callback);
1711
1771
  },
1712
1772
 
1713
- // theme: a guarded, non-enumerable getter defined after this literal.
1714
- _readTheme: getTheme,
1715
1773
  setTheme: themeConfig
1716
1774
  ? (theme: Theme) => {
1717
1775
  assertNotInsideCacheExec(ctx, "setTheme");
@@ -1737,14 +1795,15 @@ export function createRequestContext<TEnv>(
1737
1795
  _renderDiagnosticsEnabled: undefined,
1738
1796
  _handlerStart: undefined,
1739
1797
 
1740
- _renderBarrier: null as any,
1741
1798
  _resolveRenderBarrier: null as any,
1742
1799
  _renderBarrierSegmentOrder: undefined,
1743
1800
 
1744
1801
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
1745
1802
  };
1803
+ const ctx = fields as RequestContext<TEnv>;
1746
1804
 
1747
- wireRenderBarrier(ctx, handleStore);
1805
+ bindRes(ctx, () => stubResponse);
1806
+ wireRenderBarrier(ctx, handleStore, RENDER_BARRIER_DESCRIPTOR);
1748
1807
 
1749
1808
  ctx.use = createUseFunction({
1750
1809
  handleStore,
@@ -1752,41 +1811,70 @@ export function createRequestContext<TEnv>(
1752
1811
  getContext: () => ctx,
1753
1812
  });
1754
1813
 
1755
- // getRequestContext().theme is the visitor's cookie, guarded like cookies()
1756
- // (#971); before it was a plain read that a ppr shell, a cache() entry or a
1757
- // "use cache" entry stored for every visitor. Non-enumerable like the
1758
- // handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
1759
- // context (React dev debug info) must not count as a read. The guard reads
1760
- // the ambient context, as cookies() does, and falls back to the receiver.
1761
- // _readTheme is the unguarded read: non-enumerable too, so a spread copy of
1762
- // the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
1763
- // The same holds for the unguarded cookie reads (#976).
1764
- Object.defineProperties(ctx, {
1765
- theme: {
1766
- get(this: RequestContext<TEnv>): Theme | undefined {
1767
- return readGuardedTheme(this, "getRequestContext().theme");
1768
- },
1769
- enumerable: false,
1770
- configurable: true,
1771
- },
1772
- _readTheme: { enumerable: false },
1773
- _readCookie: { enumerable: false },
1774
- _readCookies: { enumerable: false },
1775
- });
1814
+ // theme: REQUEST_THEME_DESCRIPTOR. _readTheme is the unguarded read:
1815
+ // non-enumerable too, so a spread copy of the context (the fetchable-loader
1816
+ // ctx, loader-fetch.ts) cannot carry it. The same holds for the unguarded
1817
+ // cookie reads (#976). Added here, not flipped from enumerable: changing an
1818
+ // existing property's attributes costs V8 a map copy per request.
1819
+ Object.defineProperty(ctx, "theme", REQUEST_THEME_DESCRIPTOR);
1820
+ defineRequestRewrittenFrom(ctx);
1821
+ defineHidden(ctx, "_readTheme", getTheme);
1822
+ defineHidden(ctx, "_readCookie", effectiveCookie);
1823
+ defineHidden(ctx, "_readCookies", effectiveCookies);
1776
1824
 
1777
1825
  (ctx as any)[NOCACHE_SYMBOL] = true;
1778
1826
  // Non-enumerable: a self-reference would make the context circular for
1779
1827
  // JSON.stringify, spreads and React's dev debug-info serialization of props.
1780
- Object.defineProperty(ctx, "_requestRoot", {
1781
- value: ctx,
1782
- enumerable: false,
1783
- configurable: true,
1784
- writable: true,
1785
- });
1828
+ defineHidden(ctx, "_requestRoot", ctx);
1786
1829
  ctx._requestStart = executionStart();
1787
1830
  return ctx;
1788
1831
  }
1789
1832
 
1833
+ /**
1834
+ * Move a request context to a rewrite target for the next pass of the
1835
+ * request (rsc/rewrite-passes.ts, `throw rewrite()`). The context object
1836
+ * stays the same, so what the visitor's pass set is still there: variables
1837
+ * (the rewrite trail among them), the response stub (status, headers,
1838
+ * cookies), location state, theme, `dynamic()`, metrics, request tags,
1839
+ * onResponse callbacks, the handle store.
1840
+ *
1841
+ * Moved to the target: the request (`rawUrl`, the visitor's `_rsc*` params
1842
+ * included), url, originalUrl, pathname, searchParams and the request kind.
1843
+ * Cleared for the target's match: params, route name, reverse, the
1844
+ * classified route, the request mode, and the document cache's render mark
1845
+ * (its middleware runs again for the target). The loader memo behind
1846
+ * `getRequestContext().use()` is new: a loader that middleware started in
1847
+ * the visitor's pass read the visitor's URL. A pass never renders before it
1848
+ * rewrites: router/middleware.ts refuses a rewrite thrown after a next()
1849
+ * unless that next() rejected with a Rewrite (a downstream middleware
1850
+ * rewrote before its own next()), so there is no render state to reset.
1851
+ */
1852
+ export function rebaseRequestContext(
1853
+ ctx: RequestContext<any>,
1854
+ request: Request,
1855
+ rawUrl: URL,
1856
+ ): void {
1857
+ const cleanUrl = stripInternalParams(rawUrl);
1858
+ ctx.request = guardRequestHeaders(request);
1859
+ ctx.url = cleanUrl;
1860
+ ctx.originalUrl = rawUrl;
1861
+ ctx.pathname = cleanUrl.pathname;
1862
+ ctx.searchParams = cleanUrl.searchParams;
1863
+ ctx._requestKind = requestKind(request, rawUrl);
1864
+ ctx.params = {};
1865
+ ctx._routeName = undefined;
1866
+ ctx.routeName = undefined;
1867
+ ctx.reverse = createReverseFunction(getGlobalRouteMap(), undefined, {});
1868
+ ctx._classifiedRoute = undefined;
1869
+ ctx._requestMode = undefined;
1870
+ ctx._documentCacheRender = undefined;
1871
+ ctx.use = createUseFunction({
1872
+ handleStore: ctx._handleStore,
1873
+ loaderPromises: new Map(),
1874
+ getContext: () => ctx,
1875
+ });
1876
+ }
1877
+
1790
1878
  /**
1791
1879
  * Wire a fresh render barrier onto `ctx`, closure-bound to THIS ctx and THIS
1792
1880
  * handle store. Called by createRequestContext for every fresh context, and by
@@ -1803,10 +1891,14 @@ export function createRequestContext<TEnv>(
1803
1891
  * read the foreground's handle snapshot; the capture's fresh `_handleStore`
1804
1892
  * was invisible, so foreground per-request handle data could bake into the
1805
1893
  * shared shell.
1894
+ *
1895
+ * A derived context's `_renderBarrier` is non-enumerable (the default
1896
+ * descriptor); a fresh context's is enumerable like its other fields.
1806
1897
  */
1807
1898
  export function wireRenderBarrier(
1808
1899
  ctx: RequestContext<any, any>,
1809
1900
  handleStore: HandleStore,
1901
+ descriptor: PropertyDescriptor = DERIVED_RENDER_BARRIER_DESCRIPTOR,
1810
1902
  ): void {
1811
1903
  // Reset the whole barrier family as OWN properties. No-op for a fresh
1812
1904
  // context; for the derived capture context this shadows the foreground's
@@ -1853,25 +1945,23 @@ export function wireRenderBarrier(
1853
1945
  }
1854
1946
  if (resolveBarrier) resolveBarrier();
1855
1947
  };
1948
+ defineSlot(ctx, RENDER_BARRIER_SLOT, (): Promise<void> => {
1949
+ const p = barrierResolved
1950
+ ? Promise.resolve()
1951
+ : new Promise<void>((resolve) => {
1952
+ resolveBarrier = resolve;
1953
+ });
1954
+ Object.defineProperty(ctx, "_renderBarrier", {
1955
+ value: p,
1956
+ writable: false,
1957
+ configurable: false,
1958
+ });
1959
+ return p;
1960
+ });
1856
1961
  // defineProperty, not assignment: on a derived context the prototype's
1857
1962
  // _renderBarrier may already be a non-writable data property (the getter
1858
1963
  // pins it after first access), which would reject a plain assignment.
1859
- Object.defineProperty(ctx, "_renderBarrier", {
1860
- get() {
1861
- const p = barrierResolved
1862
- ? Promise.resolve()
1863
- : new Promise<void>((resolve) => {
1864
- resolveBarrier = resolve;
1865
- });
1866
- Object.defineProperty(ctx, "_renderBarrier", {
1867
- value: p,
1868
- writable: false,
1869
- configurable: false,
1870
- });
1871
- return p;
1872
- },
1873
- configurable: true,
1874
- });
1964
+ Object.defineProperty(ctx, "_renderBarrier", descriptor);
1875
1965
  }
1876
1966
 
1877
1967
  // Capture the Max-Age value so it can be parsed numerically. A leading zero
@@ -2103,6 +2193,7 @@ export function createUseFunction<TEnv>(
2103
2193
  );
2104
2194
  },
2105
2195
  };
2196
+ defineRewrittenFrom(loaderCtx, ctx);
2106
2197
  // "use cache" keys it by route, like the request ctx it reads (#940).
2107
2198
  (loaderCtx as any)[NOCACHE_SYMBOL] = true;
2108
2199
 
@@ -10,6 +10,8 @@
10
10
  * keeps the object a real Request, and those operations never call it.
11
11
  */
12
12
 
13
+ import { defineHidden } from "./slot-accessors.js";
14
+
13
15
  /** The Headers each shadowed request's getter returns. */
14
16
  const RAW_HEADERS = new WeakMap<Request, Headers>();
15
17
 
@@ -93,12 +95,12 @@ export function shadowRequestHeaders(
93
95
  configurable: true,
94
96
  });
95
97
  const clone = request.clone;
96
- defineMethod(request, "clone", function (this: Request): Request {
98
+ defineHidden(request, "clone", function (this: Request): Request {
97
99
  return shadowRequestHeaders(clone.call(this), beforeRead);
98
100
  });
99
101
  const inspect = (request as unknown as Record<symbol, unknown>)[INSPECT];
100
102
  if (typeof inspect === "function") {
101
- defineMethod(
103
+ defineHidden(
102
104
  request,
103
105
  INSPECT,
104
106
  function (this: Request, ...args: unknown[]) {
@@ -109,18 +111,6 @@ export function shadowRequestHeaders(
109
111
  return request;
110
112
  }
111
113
 
112
- function defineMethod(
113
- target: object,
114
- key: PropertyKey,
115
- value: (...args: never[]) => unknown,
116
- ): void {
117
- Object.defineProperty(target, key, {
118
- value,
119
- configurable: true,
120
- writable: true,
121
- });
122
- }
123
-
124
114
  /** The getter `key` resolves to on `target`'s prototype chain, if any. */
125
115
  function inheritedGetter(
126
116
  target: object,
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The URLs a rewritten request moved through (`throw rewrite()`), and what
3
+ * each context sees of them.
4
+ *
5
+ * One source: the trail lives in the request's variables under a token no
6
+ * user code can reach, so a middleware cannot reset the loop count. The
7
+ * visitor's URL is its first entry (`ctx.rewrittenFrom`); the hops are its
8
+ * length minus one.
9
+ */
10
+
11
+ import { contextGet, contextSet, createVar } from "../context-var.js";
12
+ import { RewriteLoopError } from "../errors.js";
13
+ import { documentCacheStoresRender } from "../cache/document-cache-policy.js";
14
+ import { guardIdentityRead, type IdentityReadWording } from "./context.js";
15
+ import { stripInternalParams } from "../router/internal-params.js";
16
+ import type { RequestContext } from "./request-context.js";
17
+
18
+ /** Most rewrites one request may take; the next one is a RewriteLoopError. */
19
+ export const MAX_REWRITE_HOPS = 5;
20
+
21
+ /** Clean hrefs, the visitor's first. */
22
+ const REWRITE_TRAIL = createVar<readonly string[]>();
23
+
24
+ type TrailContext = Pick<RequestContext<any>, "_variables">;
25
+
26
+ function pathKey(url: URL): string {
27
+ return url.pathname + url.search;
28
+ }
29
+
30
+ /** The trail of a rewritten request; undefined when nothing rewrote. */
31
+ export function rewriteTrail(
32
+ reqCtx: TrailContext | undefined,
33
+ ): readonly string[] | undefined {
34
+ return reqCtx?._variables
35
+ ? (contextGet(reqCtx._variables, REWRITE_TRAIL) as
36
+ | readonly string[]
37
+ | undefined)
38
+ : undefined;
39
+ }
40
+
41
+ /**
42
+ * Count one hop from `from` to `to` (clean URLs) and return its number.
43
+ * Throws RewriteLoopError past {@link MAX_REWRITE_HOPS}, or when `to` is the
44
+ * URL the request is on (pathname and search).
45
+ */
46
+ export function recordRewriteHop(
47
+ reqCtx: TrailContext,
48
+ from: URL,
49
+ to: URL,
50
+ ): number {
51
+ const trail = [...(rewriteTrail(reqCtx) ?? [from.href]), to.href];
52
+ const hops = trail.length - 1;
53
+ if (pathKey(to) === pathKey(from) || hops > MAX_REWRITE_HOPS) {
54
+ throw new RewriteLoopError(
55
+ trail.map((href) => pathKey(new URL(href))),
56
+ MAX_REWRITE_HOPS,
57
+ );
58
+ }
59
+ contextSet(reqCtx._variables, REWRITE_TRAIL, trail);
60
+ return hops;
61
+ }
62
+
63
+ type AudienceContext = Pick<
64
+ RequestContext<any>,
65
+ "_shellCaptureRun" | "_documentCacheRender" | "res"
66
+ >;
67
+
68
+ /**
69
+ * A render other visitors may be served: a PPR shell capture, or a render
70
+ * the document cache stores (its response opted in before next()). It shows
71
+ * what a direct visit to its URL shows: no visitor theme, no visitor URL.
72
+ */
73
+ export function rendersForSharedAudience(reqCtx: AudienceContext): boolean {
74
+ return reqCtx._shellCaptureRun === true || documentCacheStoresRender(reqCtx);
75
+ }
76
+
77
+ /**
78
+ * The URL the visitor is on: the first URL of a rewritten request, else
79
+ * `url` with the internal `_rsc*` params stripped. A reload the router asks
80
+ * the browser for points here.
81
+ */
82
+ export function visitorUrl(reqCtx: TrailContext | undefined, url: URL): URL {
83
+ const first = rewriteTrail(reqCtx)?.[0];
84
+ return first ? new URL(first) : stripInternalParams(url);
85
+ }
86
+
87
+ const REWRITTEN_FROM_FIX =
88
+ "It is the visitor's URL, which the shared entry would serve to every " +
89
+ "visitor. Read it in middleware and pass what the page needs through a " +
90
+ "variable the cache key covers.";
91
+
92
+ const REWRITTEN_FROM_READ: IdentityReadWording = {
93
+ verb: "read",
94
+ fix: {
95
+ useCache:
96
+ "ctx.rewrittenFrom is not in the cache key. Read it before the cached " +
97
+ "function and pass it in as an argument.",
98
+ cacheScope: REWRITTEN_FROM_FIX,
99
+ capture: REWRITTEN_FROM_FIX,
100
+ warning: REWRITTEN_FROM_FIX,
101
+ },
102
+ };
103
+
104
+ /**
105
+ * Who reads `rewrittenFrom`: "render" for the contexts a render reads
106
+ * (handler, loader, response route, getRequestContext()), "middleware" for a
107
+ * middleware ctx, which runs once per request and is never stored.
108
+ */
109
+ export type RewrittenFromReader = "render" | "middleware";
110
+
111
+ /**
112
+ * The read behind every `ctx.rewrittenFrom`. Guarded like cookies(): inside
113
+ * a `cache()` boundary or a `"use cache"` function it throws, and a loader's
114
+ * own `cache()` fill records it. A render other visitors may be served sees
115
+ * `undefined`, as on a direct visit; any other render that sees the visitor's
116
+ * URL marks `_payloadVisitorLocation`, so the document cache does not store
117
+ * it.
118
+ */
119
+ function readRewrittenFrom(
120
+ reqCtx: RequestContext<any> | undefined,
121
+ reader: RewrittenFromReader,
122
+ ): string | undefined {
123
+ if (!reqCtx) return undefined;
124
+ if (reader === "render" && rendersForSharedAudience(reqCtx)) {
125
+ return undefined;
126
+ }
127
+ guardIdentityRead(reqCtx, "ctx.rewrittenFrom", REWRITTEN_FROM_READ);
128
+ const href = rewriteTrail(reqCtx)?.[0];
129
+ if (href && reader === "render") reqCtx._payloadVisitorLocation = true;
130
+ return href;
131
+ }
132
+
133
+ /** Each context's own copy of the URL, kept while the visitor URL holds. */
134
+ const COPIES = new WeakMap<object, URL>();
135
+ /** The request context a handler/loader/middleware ctx reads through. */
136
+ const SOURCES = new WeakMap<object, RequestContext<any> | undefined>();
137
+ const READERS = new WeakMap<object, RewrittenFromReader>();
138
+
139
+ function copyFor(holder: object, href: string | undefined): URL | undefined {
140
+ if (!href) return undefined;
141
+ const memo = COPIES.get(holder);
142
+ if (memo && memo.href === href) return memo;
143
+ const copy = new URL(href);
144
+ COPIES.set(holder, copy);
145
+ return copy;
146
+ }
147
+
148
+ /**
149
+ * Non-enumerable, like `ctx.theme`: serializing a ctx (React's dev debug
150
+ * info) must not count as a read.
151
+ */
152
+ const SCOPED_DESCRIPTOR: PropertyDescriptor = {
153
+ get(this: object): URL | undefined {
154
+ const href = readRewrittenFrom(
155
+ SOURCES.get(this),
156
+ READERS.get(this) ?? "render",
157
+ );
158
+ return copyFor(this, href);
159
+ },
160
+ enumerable: false,
161
+ configurable: true,
162
+ };
163
+
164
+ const REQUEST_DESCRIPTOR: PropertyDescriptor = {
165
+ get(this: RequestContext<any>): URL | undefined {
166
+ return copyFor(this, readRewrittenFrom(this, "render"));
167
+ },
168
+ enumerable: false,
169
+ configurable: true,
170
+ };
171
+
172
+ /**
173
+ * Define `ctx.rewrittenFrom` on a handler, loader, response-route or
174
+ * middleware ctx, read through `source` (the request context it belongs
175
+ * to). Each ctx gets its own copy of the URL.
176
+ */
177
+ export function defineRewrittenFrom(
178
+ ctx: object,
179
+ source: RequestContext<any> | undefined,
180
+ reader: RewrittenFromReader = "render",
181
+ ): void {
182
+ SOURCES.set(ctx, source);
183
+ if (reader !== "render") READERS.set(ctx, reader);
184
+ Object.defineProperty(ctx, "rewrittenFrom", SCOPED_DESCRIPTOR);
185
+ }
186
+
187
+ /** Define `rewrittenFrom` on a request context, read through itself. */
188
+ export function defineRequestRewrittenFrom(ctx: RequestContext<any>): void {
189
+ Object.defineProperty(ctx, "rewrittenFrom", REQUEST_DESCRIPTOR);
190
+ }