@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc

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 (285) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +277 -99
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2779 -1064
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +155 -6
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +716 -0
  37. package/skills/typesafety/SKILL.md +329 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +102 -16
  50. package/src/browser/navigation-client.ts +164 -59
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +21 -37
  53. package/src/browser/partial-update.ts +139 -38
  54. package/src/browser/prefetch/cache.ts +175 -15
  55. package/src/browser/prefetch/fetch.ts +180 -33
  56. package/src/browser/prefetch/queue.ts +123 -20
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +81 -9
  60. package/src/browser/react/NavigationProvider.tsx +110 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +191 -74
  74. package/src/browser/scroll-restoration.ts +41 -14
  75. package/src/browser/segment-reconciler.ts +36 -9
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +57 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +278 -88
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-runtime.ts +15 -11
  92. package/src/cache/cache-scope.ts +76 -49
  93. package/src/cache/cf/cf-cache-store.ts +501 -18
  94. package/src/cache/cf/index.ts +5 -1
  95. package/src/cache/document-cache.ts +17 -7
  96. package/src/cache/index.ts +1 -0
  97. package/src/cache/taint.ts +55 -0
  98. package/src/client.rsc.tsx +3 -0
  99. package/src/client.tsx +94 -238
  100. package/src/context-var.ts +72 -2
  101. package/src/debug.ts +2 -2
  102. package/src/decode-loader-results.ts +36 -0
  103. package/src/errors.ts +30 -1
  104. package/src/handle.ts +65 -12
  105. package/src/host/index.ts +2 -2
  106. package/src/host/router.ts +129 -57
  107. package/src/host/types.ts +31 -2
  108. package/src/host/utils.ts +1 -1
  109. package/src/href-client.ts +140 -20
  110. package/src/index.rsc.ts +12 -5
  111. package/src/index.ts +61 -11
  112. package/src/loader-store.ts +500 -0
  113. package/src/loader.rsc.ts +2 -5
  114. package/src/loader.ts +3 -10
  115. package/src/missing-id-error.ts +68 -0
  116. package/src/outlet-context.ts +1 -1
  117. package/src/prerender/store.ts +5 -4
  118. package/src/prerender.ts +141 -80
  119. package/src/response-utils.ts +37 -0
  120. package/src/reverse.ts +65 -15
  121. package/src/route-content-wrapper.tsx +6 -28
  122. package/src/route-definition/dsl-helpers.ts +435 -260
  123. package/src/route-definition/helper-factories.ts +29 -139
  124. package/src/route-definition/helpers-types.ts +110 -34
  125. package/src/route-definition/index.ts +3 -0
  126. package/src/route-definition/redirect.ts +11 -3
  127. package/src/route-definition/resolve-handler-use.ts +155 -0
  128. package/src/route-definition/use-item-types.ts +32 -0
  129. package/src/route-map-builder.ts +7 -1
  130. package/src/route-types.ts +37 -41
  131. package/src/router/basename.ts +14 -0
  132. package/src/router/content-negotiation.ts +113 -1
  133. package/src/router/error-handling.ts +1 -1
  134. package/src/router/find-match.ts +4 -2
  135. package/src/router/handler-context.ts +77 -38
  136. package/src/router/intercept-resolution.ts +15 -22
  137. package/src/router/lazy-includes.ts +12 -9
  138. package/src/router/loader-resolution.ts +174 -22
  139. package/src/router/logging.ts +5 -2
  140. package/src/router/manifest.ts +31 -16
  141. package/src/router/match-api.ts +128 -192
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/background-revalidation.ts +30 -2
  144. package/src/router/match-middleware/cache-lookup.ts +136 -106
  145. package/src/router/match-middleware/cache-store.ts +54 -10
  146. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  147. package/src/router/match-middleware/segment-resolution.ts +61 -5
  148. package/src/router/match-result.ts +125 -10
  149. package/src/router/metrics.ts +7 -2
  150. package/src/router/middleware-types.ts +21 -34
  151. package/src/router/middleware.ts +103 -90
  152. package/src/router/navigation-snapshot.ts +182 -0
  153. package/src/router/pattern-matching.ts +101 -17
  154. package/src/router/prerender-match.ts +110 -10
  155. package/src/router/preview-match.ts +32 -102
  156. package/src/router/request-classification.ts +286 -0
  157. package/src/router/revalidation.ts +58 -2
  158. package/src/router/route-snapshot.ts +245 -0
  159. package/src/router/router-context.ts +6 -1
  160. package/src/router/router-interfaces.ts +77 -28
  161. package/src/router/router-options.ts +76 -11
  162. package/src/router/router-registry.ts +2 -5
  163. package/src/router/segment-resolution/fresh.ts +223 -24
  164. package/src/router/segment-resolution/helpers.ts +29 -24
  165. package/src/router/segment-resolution/loader-cache.ts +1 -0
  166. package/src/router/segment-resolution/revalidation.ts +466 -285
  167. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  168. package/src/router/segment-wrappers.ts +2 -0
  169. package/src/router/substitute-pattern-params.ts +56 -0
  170. package/src/router/telemetry.ts +99 -0
  171. package/src/router/trie-matching.ts +18 -13
  172. package/src/router/types.ts +9 -0
  173. package/src/router/url-params.ts +49 -0
  174. package/src/router.ts +91 -23
  175. package/src/rsc/handler-context.ts +2 -2
  176. package/src/rsc/handler.ts +440 -381
  177. package/src/rsc/helpers.ts +91 -43
  178. package/src/rsc/index.ts +1 -1
  179. package/src/rsc/loader-fetch.ts +23 -3
  180. package/src/rsc/manifest-init.ts +5 -1
  181. package/src/rsc/origin-guard.ts +28 -10
  182. package/src/rsc/progressive-enhancement.ts +18 -2
  183. package/src/rsc/response-route-handler.ts +46 -53
  184. package/src/rsc/rsc-rendering.ts +41 -48
  185. package/src/rsc/runtime-warnings.ts +9 -10
  186. package/src/rsc/server-action.ts +25 -37
  187. package/src/rsc/ssr-setup.ts +18 -2
  188. package/src/rsc/types.ts +17 -3
  189. package/src/search-params.ts +4 -4
  190. package/src/segment-content-promise.ts +67 -0
  191. package/src/segment-loader-promise.ts +122 -0
  192. package/src/segment-system.tsx +219 -67
  193. package/src/serialize.ts +243 -0
  194. package/src/server/context.ts +277 -61
  195. package/src/server/cookie-store.ts +28 -4
  196. package/src/server/handle-store.ts +19 -0
  197. package/src/server/loader-registry.ts +9 -8
  198. package/src/server/request-context.ts +204 -60
  199. package/src/ssr/index.tsx +9 -1
  200. package/src/static-handler.ts +19 -7
  201. package/src/testing/cache-status.ts +166 -0
  202. package/src/testing/collect-handle.ts +63 -0
  203. package/src/testing/dispatch.ts +440 -0
  204. package/src/testing/dom.entry.ts +22 -0
  205. package/src/testing/e2e/fixture.ts +154 -0
  206. package/src/testing/e2e/index.ts +149 -0
  207. package/src/testing/e2e/matchers.ts +51 -0
  208. package/src/testing/e2e/page-helpers.ts +272 -0
  209. package/src/testing/e2e/parity.ts +306 -0
  210. package/src/testing/e2e/server.ts +183 -0
  211. package/src/testing/flight-matchers.ts +104 -0
  212. package/src/testing/flight-runtime.d.ts +21 -0
  213. package/src/testing/flight.entry.ts +22 -0
  214. package/src/testing/flight.ts +182 -0
  215. package/src/testing/generated-routes.ts +223 -0
  216. package/src/testing/index.ts +106 -0
  217. package/src/testing/internal/context.ts +255 -0
  218. package/src/testing/render-route.tsx +565 -0
  219. package/src/testing/run-loader.ts +296 -0
  220. package/src/testing/run-middleware.ts +179 -0
  221. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  222. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  223. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  224. package/src/testing/vitest-stubs/version.ts +5 -0
  225. package/src/testing/vitest.ts +183 -0
  226. package/src/types/cache-types.ts +4 -4
  227. package/src/types/global-namespace.ts +39 -26
  228. package/src/types/handler-context.ts +194 -72
  229. package/src/types/index.ts +1 -0
  230. package/src/types/loader-types.ts +41 -15
  231. package/src/types/request-scope.ts +126 -0
  232. package/src/types/route-entry.ts +19 -1
  233. package/src/types/segments.ts +37 -1
  234. package/src/urls/include-helper.ts +34 -67
  235. package/src/urls/index.ts +0 -3
  236. package/src/urls/path-helper-types.ts +50 -9
  237. package/src/urls/path-helper.ts +63 -63
  238. package/src/urls/pattern-types.ts +48 -19
  239. package/src/urls/response-types.ts +25 -22
  240. package/src/urls/type-extraction.ts +26 -116
  241. package/src/urls/urls-function.ts +1 -5
  242. package/src/use-loader.tsx +487 -44
  243. package/src/vite/debug.ts +185 -0
  244. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  245. package/src/vite/discovery/discover-routers.ts +105 -51
  246. package/src/vite/discovery/discovery-errors.ts +194 -0
  247. package/src/vite/discovery/gate-state.ts +171 -0
  248. package/src/vite/discovery/prerender-collection.ts +188 -93
  249. package/src/vite/discovery/route-types-writer.ts +40 -84
  250. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  251. package/src/vite/discovery/state.ts +46 -6
  252. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  253. package/src/vite/index.ts +6 -0
  254. package/src/vite/plugin-types.ts +111 -72
  255. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  256. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  257. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  258. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  259. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  260. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  261. package/src/vite/plugins/expose-action-id.ts +55 -33
  262. package/src/vite/plugins/expose-id-utils.ts +24 -8
  263. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  264. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  265. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  266. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  267. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  268. package/src/vite/plugins/performance-tracks.ts +92 -0
  269. package/src/vite/plugins/refresh-cmd.ts +88 -26
  270. package/src/vite/plugins/use-cache-transform.ts +65 -50
  271. package/src/vite/plugins/version-injector.ts +39 -23
  272. package/src/vite/plugins/version-plugin.ts +72 -3
  273. package/src/vite/plugins/virtual-entries.ts +2 -2
  274. package/src/vite/rango.ts +265 -226
  275. package/src/vite/router-discovery.ts +920 -137
  276. package/src/vite/utils/ast-handler-extract.ts +15 -15
  277. package/src/vite/utils/banner.ts +4 -4
  278. package/src/vite/utils/bundle-analysis.ts +4 -2
  279. package/src/vite/utils/client-chunks.ts +190 -0
  280. package/src/vite/utils/forward-user-plugins.ts +193 -0
  281. package/src/vite/utils/manifest-utils.ts +21 -5
  282. package/src/vite/utils/package-resolution.ts +41 -1
  283. package/src/vite/utils/prerender-utils.ts +38 -5
  284. package/src/vite/utils/shared-utils.ts +109 -27
  285. package/src/browser/action-response-classifier.ts +0 -99
@@ -1,8 +1,9 @@
1
- import { dirname, join, basename, resolve } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import {
4
4
  generateRouteTypesSource,
5
- buildCombinedRouteMapForRouterFile,
5
+ genFileTsPath,
6
+ resolveSearchSchemas,
6
7
  } from "./generate-route-types.ts";
7
8
  import { isAutoGeneratedRouteName } from "../route-name.js";
8
9
 
@@ -175,25 +176,13 @@ export async function discoverAndWriteRouteTypes(
175
176
  );
176
177
  }
177
178
 
178
- // Search schema fallback: runtime manifest may omit search schema metadata
179
- // in some module-runner flows. Fall back to static source parsing.
180
- if (!routeSearchSchemas || Object.keys(routeSearchSchemas).length === 0) {
181
- const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
182
- if (Object.keys(staticParsed.searchSchemas).length > 0) {
183
- const filtered: Record<string, Record<string, string>> = {};
184
- for (const name of Object.keys(routeManifest)) {
185
- const schema = staticParsed.searchSchemas[name];
186
- if (schema) filtered[name] = schema;
187
- }
188
- if (Object.keys(filtered).length > 0) {
189
- routeSearchSchemas = filtered;
190
- }
191
- }
192
- }
179
+ routeSearchSchemas = resolveSearchSchemas(
180
+ Object.keys(routeManifest),
181
+ routeSearchSchemas,
182
+ sourceFile,
183
+ );
193
184
 
194
- const routerDir = dirname(sourceFile);
195
- const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
196
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
185
+ const outPath = genFileTsPath(sourceFile);
197
186
 
198
187
  const source = generateRouteTypesSource(
199
188
  routeManifest,
@@ -214,11 +214,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
214
214
  bgStopCapture = c.stop;
215
215
  }
216
216
 
217
- // Stamp tainted args and RequestContext so request-scoped
218
- // reads (cookies, headers) and side effects (ctx.set, etc.)
219
- // throw inside background revalidation, same as the miss path.
220
- // Uses ref-counted stamp/unstamp so overlapping executions
221
- // sharing the same ctx don't clear each other's guards.
217
+ // Stamp tainted ARGS only not requestCtx. The args stamp guards
218
+ // direct ctx method calls (ctx.set, ctx.header, ctx.onResponse, etc.)
219
+ // which is sufficient for correctness.
220
+ //
221
+ // We intentionally skip stamping requestCtx here because:
222
+ // 1. runBackground starts the async task synchronously (before the
223
+ // first await), so stampCacheExec would pollute the shared
224
+ // requestCtx while the foreground pipeline is still running.
225
+ // This causes assertNotInsideCacheExec to fire when cache-store
226
+ // later calls requestCtx.onResponse().
227
+ // 2. requestCtx methods are closure-bound to the original ctx, so
228
+ // neither Object.create() nor a proxy can isolate the stamp.
229
+ // 3. The foreground miss path already stamps requestCtx and catches
230
+ // cookies()/headers() misuse on first execution. The background
231
+ // re-runs the same function with the same request.
222
232
  const bgTaintedArgs: unknown[] = [];
223
233
  for (const arg of args) {
224
234
  if (isTainted(arg)) {
@@ -226,9 +236,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
226
236
  bgTaintedArgs.push(arg);
227
237
  }
228
238
  }
229
- if (requestCtx) {
230
- stampCacheExec(requestCtx as object);
231
- }
232
239
 
233
240
  try {
234
241
  const freshResult = await fn.apply(this, args);
@@ -249,9 +256,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
249
256
  for (const arg of bgTaintedArgs) {
250
257
  unstampCacheExec(arg as object);
251
258
  }
252
- if (requestCtx) {
253
- unstampCacheExec(requestCtx as object);
254
- }
255
259
  // Restore original handle store
256
260
  if (originalHandleStore && requestCtx) {
257
261
  requestCtx._handleStore = originalHandleStore;
@@ -73,7 +73,7 @@ function getDefaultRouteCacheKey(
73
73
  isIntercept?: boolean,
74
74
  ): string {
75
75
  const ctx = getRequestContext();
76
- const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
76
+ const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
77
77
  const searchParams = ctx?.url.searchParams;
78
78
  const host = ctx?.url.host ?? "localhost";
79
79
 
@@ -187,6 +187,32 @@ export class CacheScope {
187
187
  return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
188
188
  }
189
189
 
190
+ /**
191
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
192
+ * operation) when the predicate returns false or throws; returns true when
193
+ * there is no condition or no request context to evaluate it against.
194
+ */
195
+ private conditionAllows(op: "read" | "write"): boolean {
196
+ if (this.config === false || !this.config.condition) return true;
197
+ const requestCtx = getRequestContext();
198
+ if (!requestCtx) return true;
199
+ try {
200
+ if (!this.config.condition(requestCtx)) {
201
+ debugCacheLog(
202
+ `[CacheScope] condition returned false, skipping cache ${op}`,
203
+ );
204
+ return false;
205
+ }
206
+ return true;
207
+ } catch (error) {
208
+ console.error(
209
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
210
+ error,
211
+ );
212
+ return false;
213
+ }
214
+ }
215
+
190
216
  /**
191
217
  * Lookup cached segments for a route (single cache entry per request).
192
218
  * Returns { segments, shouldRevalidate } or null if cache miss.
@@ -204,27 +230,7 @@ export class CacheScope {
204
230
  shouldRevalidate: boolean;
205
231
  } | null> {
206
232
  if (!this.enabled) return null;
207
-
208
- // Evaluate condition — skip cache read when condition returns false
209
- if (this.config !== false && this.config.condition) {
210
- const requestCtx = getRequestContext();
211
- if (requestCtx) {
212
- try {
213
- if (!this.config.condition(requestCtx)) {
214
- debugCacheLog(
215
- `[CacheScope] condition returned false, skipping cache read`,
216
- );
217
- return null;
218
- }
219
- } catch (error) {
220
- console.error(
221
- `[CacheScope] condition function threw, skipping cache read:`,
222
- error,
223
- );
224
- return null;
225
- }
226
- }
227
- }
233
+ if (!this.conditionAllows("read")) return null;
228
234
 
229
235
  const store = this.getStore();
230
236
  if (!store) return null;
@@ -284,27 +290,7 @@ export class CacheScope {
284
290
  isIntercept?: boolean,
285
291
  ): Promise<void> {
286
292
  if (!this.enabled || segments.length === 0) return;
287
-
288
- // Evaluate condition — skip cache write when condition returns false
289
- if (this.config !== false && this.config.condition) {
290
- const conditionCtx = getRequestContext();
291
- if (conditionCtx) {
292
- try {
293
- if (!this.config.condition(conditionCtx)) {
294
- debugCacheLog(
295
- `[CacheScope] condition returned false, skipping cache write`,
296
- );
297
- return;
298
- }
299
- } catch (error) {
300
- console.error(
301
- `[CacheScope] condition function threw, skipping cache write:`,
302
- error,
303
- );
304
- return;
305
- }
306
- }
307
- }
293
+ if (!this.conditionAllows("write")) return;
308
294
 
309
295
  const store = this.getStore();
310
296
  if (!store) return;
@@ -326,24 +312,61 @@ export class CacheScope {
326
312
  const key = await this.resolveKey(pathname, params, isIntercept);
327
313
 
328
314
  // Check if this is a partial request (navigation) vs document request
329
- const isPartial = requestCtx.url.searchParams.has("_rsc_partial");
315
+ const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
316
+
317
+ if (INTERNAL_RANGO_DEBUG) {
318
+ debugCacheLog(
319
+ `[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
320
+ );
321
+ }
330
322
 
331
323
  requestCtx.waitUntil(async () => {
324
+ if (INTERNAL_RANGO_DEBUG) {
325
+ debugCacheLog(
326
+ `[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
327
+ );
328
+ }
329
+
332
330
  await handleStore.settled;
333
331
 
334
- // For document requests: only cache if ALL segments have components (complete render)
335
- // For partial requests: null components are expected (client already has them)
332
+ if (INTERNAL_RANGO_DEBUG) {
333
+ debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
334
+ }
335
+
336
+ // For document requests: only cache if layout segments have components
337
+ // (complete render). Parallel and route segments may legitimately have
338
+ // null components — UI-less @meta parallels return null, and void route
339
+ // handlers produce null when the UI lives in parallel slots/layouts.
340
+ // Partial requests always allow null components (client already has them).
336
341
  if (!isPartial) {
337
- const hasAllComponents = nonLoaderSegments.every(
338
- (s) => s.component !== null,
342
+ const hasIncompleteLayouts = nonLoaderSegments.some(
343
+ (s) => s.component === null && s.type === "layout",
339
344
  );
340
- if (!hasAllComponents) return;
345
+ if (hasIncompleteLayouts) {
346
+ const nullSegments = nonLoaderSegments
347
+ .filter((s) => s.component === null && s.type === "layout")
348
+ .map((s) => s.id);
349
+ const error = new Error(
350
+ `[CacheScope] Cache write skipped: layout segments have null components ` +
351
+ `(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
352
+ `layout handlers must return JSX for document requests to be cacheable.`,
353
+ );
354
+ error.name = "CacheScopeInvariantError";
355
+ console.error(error.message);
356
+ return;
357
+ }
341
358
  }
342
359
 
343
360
  // Collect handle data for non-loader segments only
344
361
  const handles = captureHandles(nonLoaderSegments, handleStore);
345
362
 
346
363
  try {
364
+ if (INTERNAL_RANGO_DEBUG) {
365
+ debugCacheLog(
366
+ `[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
367
+ );
368
+ }
369
+
347
370
  // Serialize non-loader segments only
348
371
  const serializedSegments = await serializeSegments(nonLoaderSegments);
349
372
 
@@ -353,6 +376,10 @@ export class CacheScope {
353
376
  expiresAt: Date.now() + ttl * 1000,
354
377
  };
355
378
 
379
+ if (INTERNAL_RANGO_DEBUG) {
380
+ debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
381
+ }
382
+
356
383
  await store.set(key, data, ttl, swr);
357
384
 
358
385
  if (INTERNAL_RANGO_DEBUG) {