@rangojs/router 0.17.0 → 0.19.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The guarded `ctx.request.headers` (#976) and the router's raw reads.
3
+ *
4
+ * The Request the router hands to user code (handler, middleware and loader
5
+ * `ctx.request`, `getRequestContext().request`) carries an own `headers`
6
+ * getter that shadows the prototype's and runs the identity guard
7
+ * (cookie-store.ts guardRequestHeaders). A Proxy around the Request is not an
8
+ * option: `fetch(request)`, `new Request(request)` and `request.clone()` read
9
+ * the platform's internal slots, which a Proxy does not have. The own getter
10
+ * keeps the object a real Request, and those operations never call it.
11
+ */
12
+
13
+ /** The Headers each shadowed request's getter returns. */
14
+ const RAW_HEADERS = new WeakMap<Request, Headers>();
15
+
16
+ const INSPECT = Symbol.for("nodejs.util.inspect.custom");
17
+
18
+ /**
19
+ * Set while the platform or a request facade reads a shadowed request's
20
+ * headers for itself, not for user code (runUnguarded).
21
+ */
22
+ let unguarded = false;
23
+
24
+ function runUnguarded<T>(fn: () => T): T {
25
+ const outer = unguarded;
26
+ unguarded = true;
27
+ try {
28
+ return fn();
29
+ } finally {
30
+ unguarded = outer;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * A request's headers, past the guard. The rule: router code never reads a
36
+ * request's headers through the getter, only through this, so the guard sees
37
+ * user reads alone. Several router reads run inside the scopes the guard
38
+ * refuses (the state cookie rotation at a ppr capture, match's HMR marker,
39
+ * a store's cf-ray inside a "use cache" body, telemetry inside a loader
40
+ * fill); the rest follow the rule so a new call site cannot drift into one.
41
+ */
42
+ export function requestHeaders(request: Request): Headers {
43
+ return RAW_HEADERS.get(request) ?? request.headers;
44
+ }
45
+
46
+ /**
47
+ * Shadow `request.headers` with an own, non-enumerable getter that calls
48
+ * `beforeRead` and returns the request's own Headers (stable per request, so
49
+ * a header set through it stays visible to every reader). Idempotent. Leaves
50
+ * the request as is when `headers` is a non-configurable own property (a
51
+ * runtime that defines it per instance). Three inherited members are
52
+ * shadowed with it:
53
+ *
54
+ * - `clone()` returns a clone shadowed the same way. A plain clone was a
55
+ * Request without the getter, so `ctx.request.clone().headers` read the
56
+ * visitor's headers past the guard. (`new Request(ctx.request)` still
57
+ * returns a plain Request: the constructor cannot be intercepted.)
58
+ * - `_request`, on a lazy srvx NodeRequest (what @vitejs/plugin-rsc's Node
59
+ * dev and preview servers and the vercel-output launcher hand the
60
+ * router), runs unguarded: it builds the platform Request on first use
61
+ * from `this.headers`, for clone() and the body readers. Through the
62
+ * getter, a clone() inside a cache() boundary threw as a user read. It
63
+ * stays lazy, so a request whose body srvx reads from its buffered
64
+ * `rawBody` never builds one.
65
+ * - Node's util.inspect hook runs unguarded: undici's inspect reads
66
+ * `this.headers`, so `console.log(ctx.request)` inside a "use cache" body
67
+ * threw. A log is not a read into a shared entry.
68
+ */
69
+ export function shadowRequestHeaders(
70
+ request: Request,
71
+ beforeRead: () => void,
72
+ ): Request {
73
+ if (RAW_HEADERS.has(request)) return request;
74
+ const own = Object.getOwnPropertyDescriptor(request, "headers");
75
+ if (own && !own.configurable) return request;
76
+ const lazy = inheritedGetter(request, "_request");
77
+ if (lazy) {
78
+ Object.defineProperty(request, "_request", {
79
+ get(this: Request): unknown {
80
+ return runUnguarded(() => lazy.call(this));
81
+ },
82
+ configurable: true,
83
+ });
84
+ }
85
+ const raw = request.headers;
86
+ RAW_HEADERS.set(request, raw);
87
+ Object.defineProperty(request, "headers", {
88
+ get(): Headers {
89
+ if (!unguarded) beforeRead();
90
+ return raw;
91
+ },
92
+ enumerable: false,
93
+ configurable: true,
94
+ });
95
+ const clone = request.clone;
96
+ defineMethod(request, "clone", function (this: Request): Request {
97
+ return shadowRequestHeaders(clone.call(this), beforeRead);
98
+ });
99
+ const inspect = (request as unknown as Record<symbol, unknown>)[INSPECT];
100
+ if (typeof inspect === "function") {
101
+ defineMethod(
102
+ request,
103
+ INSPECT,
104
+ function (this: Request, ...args: unknown[]) {
105
+ return runUnguarded(() => inspect.apply(this, args));
106
+ },
107
+ );
108
+ }
109
+ return request;
110
+ }
111
+
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
+ /** The getter `key` resolves to on `target`'s prototype chain, if any. */
125
+ function inheritedGetter(
126
+ target: object,
127
+ key: string,
128
+ ): (() => unknown) | undefined {
129
+ for (let o = Object.getPrototypeOf(target); o; o = Object.getPrototypeOf(o)) {
130
+ const found = Object.getOwnPropertyDescriptor(o, key);
131
+ if (found) return found.get;
132
+ }
133
+ return undefined;
134
+ }
package/src/server.ts CHANGED
@@ -48,6 +48,10 @@ export {
48
48
  // Component utilities (used internally for server/client boundary checks)
49
49
  export { isClientComponent, assertClientComponent } from "./component-utils.js";
50
50
 
51
+ // transition({ when }) strict validation: the generated routes-manifest module
52
+ // enables it in the app's RSC realm (see transition-when-ref.ts).
53
+ export { enableTransitionWhenValidation } from "./transition-when-ref.js";
54
+
51
55
  // Client URL projection registry (Vite discovery/bootstrap bridge)
52
56
  export {
53
57
  clearClientUrlProjections,
package/src/ssr/index.tsx CHANGED
@@ -2,7 +2,11 @@ import React from "react";
2
2
  import { createSsrRootComponent, deserializeSsrPayload } from "./ssr-root.js";
3
3
  import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
4
4
  import { runWithPreinitNonce } from "./preinit-client-references.js";
5
- import { SHELL_CAPTURE_MAX_WAIT_MS } from "../rsc/shell-capture-constants.js";
5
+ import {
6
+ POST_QUIESCE_TASK_HOPS,
7
+ SHELL_CAPTURE_MAX_WAIT_MS,
8
+ } from "../rsc/shell-capture-constants.js";
9
+ import { isThenable } from "../handles/is-thenable.js";
6
10
  import type { ErrorPhase } from "../types.js";
7
11
  import type { HeadScriptsOption } from "../vite/plugin-types.js";
8
12
 
@@ -237,28 +241,64 @@ export interface SSRDependencies<TEnv = unknown> {
237
241
  }
238
242
 
239
243
  /**
240
- * Fixed number of macrotask hops between `quiesce` resolving and the abort. These
241
- * give React's fizz worker turns to flush the settled shell into the prelude and
242
- * mark still-pending boundaries as POSTPONED (rather than errored) before
243
- * controller.abort() lands. Not a wall-clock wait.
244
- *
245
- * Why 16 and not the original 2: under the REPLAY-ONLY capture model
246
- * (docs/design/ppr-shell-resume.md), the capture Flight render serializes ring-3
247
- * cached segments that are ALREADY serialized, so it emits the whole shell payload
248
- * in the first tick and the gate declares quiesce almost immediately (~a few ms).
249
- * On the old fresh-execution path the Flight dribbled out as handlers ran, so
250
- * Flight-quiet effectively meant "the shell has rendered" and 2 hops sufficed.
251
- *
252
- * Hops alone are NOT render-readiness: fizz cannot emit even <html> until the
253
- * payload root settles, which waits on every referenced client-module LOAD —
254
- * real module-runner I/O in dev (100ms+ cold), which no fixed count of near-
255
- * zero-cost task hops can buy. captureShellHTML therefore awaits the payload-
256
- * settled signal (SsrRootOptions.onPayloadSettled, deadline-bounded) between
257
- * quiesce and these hops; the hops then only flush the settled tree and mark
258
- * pending boundaries POSTPONED. Still task-based (masked loaders never emit,
259
- * so more hops never lets a hole settle). Bounded by maxWaitMs end to end.
244
+ * One set per running capture, collecting the client-reference module loads
245
+ * requested while it runs (captureClientLoads). The Flight client resolves a
246
+ * client component used as an element type lazily, so the payload settles
247
+ * while its module is still loading, and a shell that renders it outside any
248
+ * Suspense boundary cannot complete until the load does. On the
249
+ * cloudflare-basic build a cold load (the module, its imports, its CSS
250
+ * virtual) settled at +890.1ms, after the payload (+873.4ms) and 1ms before
251
+ * the abort, and uninstrumented builds lost that race (issue #949). The
252
+ * Flight client asks the loader again for every import row, even for a
253
+ * module another render is already loading, so a capture sees each load it
254
+ * needs; one a concurrent render requests meanwhile is collected too and is
255
+ * the same bounded I/O. The sets are per capture, their contents isolate-wide:
256
+ * a load that never settles can only hold the captures running when it was
257
+ * requested, each until its deadline.
260
258
  */
261
- const POST_QUIESCE_TASK_HOPS = 16;
259
+ const activeCaptureClientLoads = new Set<Set<PromiseLike<unknown>>>();
260
+
261
+ /** The loader wrapper {@link captureClientLoads} last installed. */
262
+ let recordingClientReferenceLoader: ((id: string) => unknown) | undefined;
263
+
264
+ /**
265
+ * Start collecting the client-reference loads requested during a capture;
266
+ * call the returned `stop` when the capture ends. Wraps plugin-rsc's SSR
267
+ * loader (`globalThis.__vite_rsc_client_require__`, which
268
+ * `__vite_rsc_require__` reads per call) on the first capture, so an isolate
269
+ * that never captures keeps the bare loader. Wraps again when another wrapper
270
+ * replaced this one (the build shell phase bridges hashed ids the same way,
271
+ * vite/discovery/shell-prerender-phase.ts). Collects nothing without the
272
+ * loader.
273
+ */
274
+ function captureClientLoads(): {
275
+ loads: Set<PromiseLike<unknown>>;
276
+ stop: () => void;
277
+ } {
278
+ const g = globalThis as {
279
+ __vite_rsc_client_require__?: (id: string) => unknown;
280
+ };
281
+ const load = g.__vite_rsc_client_require__;
282
+ if (typeof load === "function" && load !== recordingClientReferenceLoader) {
283
+ recordingClientReferenceLoader = (id) => {
284
+ const loading = load(id);
285
+ // plugin-rsc memoizes one promise per id and the Flight client stamps
286
+ // status "fulfilled" on it once loaded: a warm module is not collected.
287
+ if (
288
+ activeCaptureClientLoads.size > 0 &&
289
+ isThenable(loading) &&
290
+ (loading as { status?: string }).status !== "fulfilled"
291
+ ) {
292
+ for (const loads of activeCaptureClientLoads) loads.add(loading);
293
+ }
294
+ return loading;
295
+ };
296
+ g.__vite_rsc_client_require__ = recordingClientReferenceLoader;
297
+ }
298
+ const loads = new Set<PromiseLike<unknown>>();
299
+ activeCaptureClientLoads.add(loads);
300
+ return { loads, stop: () => activeCaptureClientLoads.delete(loads) };
301
+ }
262
302
 
263
303
  /**
264
304
  * Route an SSR error through the deps.onError notification callback with the
@@ -421,6 +461,13 @@ interface ShellCaptureOptions {
421
461
  * refuses to store the shell (issue #915).
422
462
  */
423
463
  onError?: (error: unknown) => void;
464
+ /**
465
+ * Called with React's errorInfo for each task still pending when the
466
+ * capture's own abort froze the prelude; its componentStack is computed
467
+ * when read. The dev no-shell warning prints the stacks: one of them is what
468
+ * suspended above <body> (issue #941).
469
+ */
470
+ onAbortedTask?: (errorInfo: { componentStack?: string } | undefined) => void;
424
471
  }
425
472
 
426
473
  /**
@@ -761,12 +808,13 @@ export function createShellCaptureHandler<TEnv = unknown>(
761
808
  const deadline = (await isDebuggerAttached())
762
809
  ? { promise: new Promise<void>(() => {}), cancel: () => {} }
763
810
  : createCancelableTimeout(maxWaitMs);
811
+ const clientLoads = captureClientLoads();
764
812
  try {
765
813
  // No nonce (nonce'd requests never reach capture); no formState.
766
- // payloadSettled: fires when the Flight payload root settles — i.e.
767
- // every client-module load the payload references completed and fizz
768
- // can actually emit the tree. The abort below gates on it (bounded by
769
- // the same deadline): Flight byte-quiet alone is NOT render-readiness.
814
+ // payloadSettled: fires when the Flight payload root settles and fizz
815
+ // can emit the tree. The abort below gates on it and on the
816
+ // client-reference loads in flight in the isolate (bounded by the same
817
+ // deadline): Flight byte-quiet alone is NOT render-readiness.
770
818
  let settlePayload!: () => void;
771
819
  const payloadSettled = new Promise<void>((resolve) => {
772
820
  settlePayload = resolve;
@@ -823,8 +871,9 @@ export function createShellCaptureHandler<TEnv = unknown>(
823
871
  // render errors are NOT our sentinel and still surface through
824
872
  // deps.onError, the same channel renderHTML uses. See
825
873
  // docs/design/ppr-shell-resume.md.
826
- onError: (error: unknown) => {
874
+ onError: (error: unknown, errorInfo?: { componentStack?: string }) => {
827
875
  if (error === abortReason) {
876
+ opts.onAbortedTask?.(errorInfo);
828
877
  return;
829
878
  }
830
879
  opts.onError?.(error);
@@ -846,25 +895,34 @@ export function createShellCaptureHandler<TEnv = unknown>(
846
895
  // shell that never goes quiet (a root postpone / hung handle).
847
896
  await Promise.race([opts.quiesce, deadline.promise]);
848
897
  // Then wait for fizz RENDER-READINESS, bounded by the same deadline:
849
- // the payload root settles only after every client-module load the
850
- // payload references completed (real module-runner I/O in dev; 100ms+
851
- // on a cold graph). Flight byte-quiet does NOT imply this — a fully
898
+ // the payload root settles, then the client-reference module loads the
899
+ // capture started settle — a client component used as an element type
900
+ // resolves lazily, after the payload (real module-runner I/O in dev;
901
+ // 100ms+ on a cold graph). Flight byte-quiet does NOT imply this — a fully
852
902
  // REPLAYED (prerendered) route's Flight stream finishes in ~1-3ms and
853
903
  // an abort taken on quiet-plus-task-hops alone landed BEFORE fizz could
854
904
  // emit <html>, freezing a zero-byte prelude: the eternal-MISS shape
855
905
  // this route class showed on every cold graph (dev cold boots, GH
856
906
  // runners) while ordinary routes — whose live handler execution keeps
857
- // Flight noisy long enough — never hit it. Masked-loader holes do not
858
- // block payload settlement (they postpone below the root), so this
859
- // await costs a genuinely hole-y shell nothing; a payload that NEVER
860
- // settles (hung handles) degrades at the deadline exactly as before.
861
- // A prerender that SETTLES first (early success or a hard rejection)
862
- // ends the wait immediately — fizz is already done either way.
907
+ // Flight noisy long enough — never hit it. Holes the payload carries
908
+ // block neither wait (masked live loaders and loader-nested promises postpone
909
+ // below the root and load no module; the Flight input stays frozen),
910
+ // so this await costs a genuinely hole-y shell nothing; a payload that
911
+ // NEVER settles (hung handles) degrades at the deadline exactly as
912
+ // before. A prerender that SETTLES first (early success or a hard
913
+ // rejection) ends the wait immediately — fizz is already done either
914
+ // way.
863
915
  const prerenderSettled = prerenderPromise.then(
864
916
  () => {},
865
917
  () => {},
866
918
  );
867
- await Promise.race([payloadSettled, prerenderSettled, deadline.promise]);
919
+ // The capture's loads all started when their import rows were parsed,
920
+ // before the Flight input froze.
921
+ await Promise.race([
922
+ payloadSettled.then(() => Promise.allSettled(clientLoads.loads)),
923
+ prerenderSettled,
924
+ deadline.promise,
925
+ ]);
868
926
  // Fixed task hops before the abort: give React's fizz worker turns to flush
869
927
  // the now-complete shell and mark the still-pending boundaries as POSTPONED
870
928
  // rather than errored. Deterministic (the byte set is already frozen and
@@ -923,6 +981,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
923
981
  };
924
982
  } finally {
925
983
  deadline.cancel();
984
+ clientLoads.stop();
926
985
  }
927
986
  };
928
987
  }
@@ -189,14 +189,15 @@ export interface SsrRootOptions {
189
189
  nonce?: string;
190
190
  /**
191
191
  * Fires once when the Flight payload root settles (resolve OR reject) — the
192
- * signal that every client-module load the payload references completed and
193
- * fizz can start emitting the tree. The capture pass gates its abort on
194
- * this: a fully REPLAYED (prerendered) route's Flight stream goes
195
- * byte-quiet in ~1-3ms, but fizz cannot render even <html> until the
196
- * module loads finish (real module-runner I/O in dev; 100ms+ on a cold
197
- * graph), so an abort gated on Flight quiet alone fires first and freezes
198
- * a zero-byte prelude. Masked-loader holes do NOT block this signal —
199
- * they postpone below the root.
192
+ * signal that fizz can start emitting the tree. A client component used as
193
+ * an element type resolves lazily, so its module load can still be in
194
+ * flight (captureShellHTML waits for those separately, issue #949). The
195
+ * capture pass gates its abort on this: a fully REPLAYED (prerendered)
196
+ * route's Flight stream goes byte-quiet in ~1-3ms, but fizz cannot render
197
+ * even <html> until the module loads finish (real module-runner I/O in dev;
198
+ * 100ms+ on a cold graph), so an abort gated on Flight quiet alone fires
199
+ * first and freezes a zero-byte prelude. Masked-loader holes do NOT block
200
+ * this signal — they postpone below the root.
200
201
  */
201
202
  onPayloadSettled?: () => void;
202
203
  /**
@@ -627,9 +627,9 @@ export async function dispatch<TEnv = any>(
627
627
  const manifestEntry = preview?.manifestEntry;
628
628
  if (manifestEntry) {
629
629
  // Lazy so the testing barrel's eager graph stays plugin-rsc-free (see the
630
- // import note above): the leaf takes createCacheScope/resolveCacheTags as
631
- // INJECTED deps so it never imports plugin-rsc; we hand it the lazily
632
- // imported pair here, only once a response route actually matched.
630
+ // import note above): the leaf takes createCacheScope as an INJECTED
631
+ // dep so it never imports plugin-rsc; we hand it the lazily imported
632
+ // builder here, only once a response route actually matched.
633
633
  const cacheScopeMod = await import("../cache/cache-scope.js");
634
634
  const { serveResponseRouteWithCache } =
635
635
  await import("../rsc/response-cache-serve.js");
@@ -645,10 +645,7 @@ export async function dispatch<TEnv = any>(
645
645
  responseType: responseType as string,
646
646
  url,
647
647
  executeHandler,
648
- deps: {
649
- createCacheScope: cacheScopeMod.createCacheScope,
650
- resolveCacheTags: cacheScopeMod.resolveCacheTags,
651
- },
648
+ deps: { createCacheScope: cacheScopeMod.createCacheScope },
652
649
  });
653
650
  if (cached !== undefined) return cached;
654
651
  }
@@ -53,6 +53,7 @@ export {
53
53
  SHELL_STATUS_HEADER,
54
54
  type PprReplayBypassReason,
55
55
  type PprReplayStatus,
56
+ type ShellCachePartition,
56
57
  type ShellStatus,
57
58
  type ShellStatusTarget,
58
59
  } from "../shell-status.js";
@@ -18,6 +18,11 @@
18
18
  * element tree, so you can assert typed prop fidelity across the client boundary
19
19
  * (a `Date` comes back a `Date`) and detect inlined-vs-island. Serialize +
20
20
  * deserialize only — no hydration/interaction (that is the e2e tier).
21
+ *
22
+ * `serveShellRequest` serves one request through the router's production
23
+ * handler, so a `ppr` route's MISS runs the real shell capture and a later
24
+ * request is a real HIT; only the HTML step is stubbed. `resetShellTestState`
25
+ * clears the per-isolate PPR state between tests.
21
26
  */
22
27
 
23
28
  export {
@@ -50,3 +55,12 @@ export type {
50
55
  RenderHandlerResult,
51
56
  StateCookieSeed,
52
57
  } from "./render-handler.js";
58
+
59
+ export {
60
+ serveShellRequest,
61
+ resetShellTestState,
62
+ } from "./serve-shell-request.js";
63
+ export type {
64
+ ServeShellRequestOptions,
65
+ ServeShellRequestResult,
66
+ } from "./serve-shell-request.js";
@@ -49,6 +49,8 @@ export type {
49
49
 
50
50
  export { runTransitionWhen } from "./run-transition-when.js";
51
51
  export type {
52
+ RunTransitionWhenAction,
53
+ RunTransitionWhenLocation,
52
54
  RunTransitionWhenOptions,
53
55
  RunTransitionWhenResult,
54
56
  } from "./run-transition-when.js";
@@ -91,12 +93,15 @@ export {
91
93
  export type {
92
94
  PprReplayBypassReason,
93
95
  PprReplayStatus,
96
+ ShellCachePartition,
94
97
  ShellStatus,
95
98
  ShellStatusTarget,
96
99
  } from "./shell-status.js";
97
100
 
98
101
  export { collectHandle } from "./collect-handle.js";
99
102
 
103
+ export { withLocationStateKey } from "./location-state-key.js";
104
+
100
105
  export {
101
106
  diffGeneratedRoutes,
102
107
  assertGeneratedRoutesMatch,
@@ -0,0 +1,25 @@
1
+ import { resolveActionRefId } from "../../router/is-action.js";
2
+
3
+ /**
4
+ * The id a testing primitive's `action` option names: a single imported
5
+ * server action (its `$id ?? $$id`, as the action boundary derives it) or a
6
+ * raw actionId string. A function without either id throws, naming the
7
+ * primitive.
8
+ */
9
+ export function resolveTestActionId(
10
+ action: ((...args: never[]) => unknown) | string | undefined,
11
+ primitive: string,
12
+ ): string | undefined {
13
+ if (action === undefined) return undefined;
14
+ if (typeof action === "string") return action;
15
+ const id = resolveActionRefId(action);
16
+ if (id === undefined) {
17
+ throw new Error(
18
+ `${primitive}: \`action\` must be a single imported server action ` +
19
+ "(carrying its build-injected id) or an actionId string. The passed " +
20
+ "function has no $id/$$id — outside a built app, pass the id string " +
21
+ 'your predicate should match (e.g. "src/actions/cart.ts#addToCart").',
22
+ );
23
+ }
24
+ return id;
25
+ }
@@ -250,7 +250,7 @@ export function snapshotRunEffects<TEnv>(ctx: RequestContext<TEnv>): {
250
250
  locationState: Record<string, unknown>;
251
251
  } {
252
252
  return {
253
- cookies: { ...ctx.cookies() },
253
+ cookies: ctx._readCookies(),
254
254
  locationState: resolveLocationStateEntries(ctx._locationState ?? []),
255
255
  };
256
256
  }
@@ -0,0 +1,45 @@
1
+ import {
2
+ peekLocationStateKey,
3
+ type LocationStateDefinition,
4
+ } from "../browser/react/location-state-shared.js";
5
+
6
+ // Typed history.state slots must carry this prefix: buildHistoryState spreads
7
+ // only `__rsc_ls_*` keys onto history.state (history-state.ts).
8
+ const KEY_PREFIX = "__rsc_ls_";
9
+
10
+ let syntheticKeyCounter = 0;
11
+
12
+ /**
13
+ * Assign a location-state key to a `createLocationState()` definition in a unit
14
+ * test that runs without the rango Vite plugin (which injects the key in dev
15
+ * and build). Outside production, a definition without a key throws on first
16
+ * use (`MyState(value)`, `useLocationState(MyState)`, `.read()`).
17
+ *
18
+ * - `withLocationStateKey(GridState, "GridState")` sets `__rsc_ls_GridState`
19
+ * (a name that already starts with `__rsc_ls_` is used as-is).
20
+ * - Without a name, a key already set (by the plugin or an earlier call) is
21
+ * kept; otherwise the definition gets a synthetic `__rsc_ls_test_<n>` key,
22
+ * which then stays on it.
23
+ *
24
+ * Returns the definition.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { withLocationStateKey } from "@rangojs/router/testing";
29
+ * import { GridState } from "../src/location-states";
30
+ *
31
+ * withLocationStateKey(GridState);
32
+ * ```
33
+ */
34
+ export function withLocationStateKey<
35
+ TDefinition extends LocationStateDefinition<any, any>,
36
+ >(definition: TDefinition, name?: string): TDefinition {
37
+ if (name !== undefined) {
38
+ definition.__rsc_ls_key = name.startsWith(KEY_PREFIX)
39
+ ? name
40
+ : KEY_PREFIX + name;
41
+ } else if (!peekLocationStateKey(definition)) {
42
+ definition.__rsc_ls_key = `${KEY_PREFIX}test_${syntheticKeyCounter++}`;
43
+ }
44
+ return definition;
45
+ }
@@ -338,7 +338,7 @@ export async function renderHandler<TEnv = any>(
338
338
  }
339
339
  });
340
340
 
341
- const cookies = { ...reqCtx.cookies() };
341
+ const cookies = reqCtx._readCookies();
342
342
  const responseSource = didThrow
343
343
  ? thrown
344
344
  : out instanceof Response