@rangojs/router 0.17.0 → 0.18.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 (152) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/history-state.d.ts +16 -0
  4. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  5. package/dist/types/cache/cache-policy.d.ts +17 -0
  6. package/dist/types/cache/cache-scope.d.ts +98 -14
  7. package/dist/types/cache/cache-tag.d.ts +92 -7
  8. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  9. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  10. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  11. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  12. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  13. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  14. package/dist/types/cache/handle-snapshot.d.ts +4 -3
  15. package/dist/types/cache/index.d.ts +1 -0
  16. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  17. package/dist/types/cache/shell-memo.d.ts +178 -0
  18. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  19. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  20. package/dist/types/cache/types.d.ts +207 -43
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  22. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  23. package/dist/types/router/instrument.d.ts +8 -0
  24. package/dist/types/router/middleware-types.d.ts +6 -1
  25. package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
  26. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
  27. package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
  28. package/dist/types/rsc/full-payload.d.ts +14 -0
  29. package/dist/types/rsc/handler.d.ts +1 -1
  30. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  31. package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
  32. package/dist/types/rsc/shell-capture.d.ts +146 -15
  33. package/dist/types/rsc/shell-serve.d.ts +140 -17
  34. package/dist/types/rsc/types.d.ts +7 -0
  35. package/dist/types/server/context.d.ts +109 -15
  36. package/dist/types/server/cookie-store.d.ts +21 -0
  37. package/dist/types/server/request-context.d.ts +159 -33
  38. package/dist/types/ssr/index.d.ts +9 -0
  39. package/dist/types/ssr/ssr-root.d.ts +9 -8
  40. package/dist/types/testing/flight.entry.d.ts +7 -0
  41. package/dist/types/testing/run-loader.d.ts +18 -3
  42. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  43. package/dist/types/testing/shell-status.d.ts +23 -10
  44. package/dist/types/types/handler-context.d.ts +14 -1
  45. package/dist/types/urls/pattern-types.d.ts +22 -18
  46. package/dist/types/vite/router-discovery.d.ts +29 -2
  47. package/dist/vite/index.js +46 -9
  48. package/package.json +1 -1
  49. package/skills/breadcrumbs/SKILL.md +17 -7
  50. package/skills/cache-guide/SKILL.md +123 -51
  51. package/skills/caching/SKILL.md +253 -23
  52. package/skills/catalog.json +1 -1
  53. package/skills/client-urls/SKILL.md +11 -6
  54. package/skills/cloudflare/SKILL.md +15 -0
  55. package/skills/comparison/references/framework-comparison.md +44 -32
  56. package/skills/defer-hydration/SKILL.md +50 -30
  57. package/skills/deployment-caching/SKILL.md +5 -2
  58. package/skills/document-cache/SKILL.md +8 -2
  59. package/skills/handler-use/SKILL.md +2 -0
  60. package/skills/hooks/handle-and-actions.md +3 -1
  61. package/skills/hooks/state.md +3 -1
  62. package/skills/loader/SKILL.md +161 -39
  63. package/skills/middleware/SKILL.md +16 -9
  64. package/skills/migrate-nextjs/SKILL.md +67 -36
  65. package/skills/migrate-react-router/SKILL.md +2 -1
  66. package/skills/migrate-react-router/component-migration.md +3 -1
  67. package/skills/migrate-react-router/data-and-actions.md +3 -1
  68. package/skills/migrate-react-router/route-mapping.md +7 -4
  69. package/skills/observability/SKILL.md +64 -0
  70. package/skills/parallel/SKILL.md +13 -8
  71. package/skills/ppr/SKILL.md +655 -363
  72. package/skills/prerender/SKILL.md +27 -14
  73. package/skills/rango/SKILL.md +52 -33
  74. package/skills/route/SKILL.md +12 -7
  75. package/skills/router-setup/SKILL.md +4 -2
  76. package/skills/scripts/SKILL.md +8 -1
  77. package/skills/server-actions/SKILL.md +11 -0
  78. package/skills/shell-manifest/SKILL.md +21 -12
  79. package/skills/testing/SKILL.md +25 -21
  80. package/skills/testing/cache-prerender.md +15 -14
  81. package/skills/testing/loader.md +18 -1
  82. package/skills/testing/serve-shell-request.md +188 -0
  83. package/skills/theme/SKILL.md +52 -4
  84. package/skills/typesafety/env-and-bindings.md +10 -2
  85. package/skills/use-cache/SKILL.md +39 -16
  86. package/skills/vercel/SKILL.md +14 -2
  87. package/skills/view-transitions/SKILL.md +1 -1
  88. package/src/browser/cookie-name.ts +23 -0
  89. package/src/browser/history-state.ts +23 -0
  90. package/src/browser/rsc-router.tsx +5 -0
  91. package/src/cache/cache-key-utils.ts +22 -2
  92. package/src/cache/cache-policy.ts +22 -0
  93. package/src/cache/cache-runtime.ts +34 -7
  94. package/src/cache/cache-scope.ts +345 -33
  95. package/src/cache/cache-tag.ts +375 -11
  96. package/src/cache/cf/cf-base64.ts +36 -4
  97. package/src/cache/cf/cf-cache-constants.ts +12 -0
  98. package/src/cache/cf/cf-cache-store.ts +990 -348
  99. package/src/cache/cf/cf-cache-types.ts +32 -4
  100. package/src/cache/cf/cf-shell-frame.ts +258 -0
  101. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  102. package/src/cache/handle-snapshot.ts +4 -3
  103. package/src/cache/index.ts +2 -0
  104. package/src/cache/isolate-tag-memo.ts +308 -0
  105. package/src/cache/shell-memo.ts +341 -0
  106. package/src/cache/shell-snapshot.ts +217 -27
  107. package/src/cache/tag-invalidation.ts +104 -19
  108. package/src/cache/types.ts +211 -43
  109. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  110. package/src/prerender/build-shell-capture.ts +65 -11
  111. package/src/route-definition/dsl-helpers.ts +4 -3
  112. package/src/router/handler-context.ts +38 -4
  113. package/src/router/instrument.ts +54 -2
  114. package/src/router/intercept-resolution.ts +19 -7
  115. package/src/router/loader-resolution.ts +63 -32
  116. package/src/router/match-api.ts +28 -9
  117. package/src/router/match-handlers.ts +5 -0
  118. package/src/router/match-middleware/cache-lookup.ts +17 -0
  119. package/src/router/match-middleware/cache-store.ts +31 -11
  120. package/src/router/middleware-types.ts +6 -1
  121. package/src/router/middleware.ts +23 -4
  122. package/src/router/prerender-match.ts +6 -0
  123. package/src/router/segment-resolution/fresh.ts +18 -9
  124. package/src/router/segment-resolution/helpers.ts +4 -2
  125. package/src/router/segment-resolution/loader-cache.ts +227 -56
  126. package/src/router/segment-resolution/loader-snapshot.ts +203 -51
  127. package/src/router/segment-resolution/mask-nested.ts +136 -42
  128. package/src/router/segment-resolution/revalidation.ts +27 -10
  129. package/src/rsc/full-payload.ts +25 -1
  130. package/src/rsc/handler.ts +14 -2
  131. package/src/rsc/progressive-enhancement.ts +3 -2
  132. package/src/rsc/response-cache-serve.ts +9 -6
  133. package/src/rsc/rsc-rendering.ts +601 -181
  134. package/src/rsc/shell-build-manifest.ts +27 -9
  135. package/src/rsc/shell-capture-constants.ts +60 -6
  136. package/src/rsc/shell-capture.ts +1001 -250
  137. package/src/rsc/shell-serve.ts +316 -34
  138. package/src/rsc/types.ts +7 -0
  139. package/src/server/context.ts +219 -28
  140. package/src/server/cookie-store.ts +87 -62
  141. package/src/server/request-context.ts +263 -38
  142. package/src/ssr/index.tsx +96 -37
  143. package/src/ssr/ssr-root.tsx +9 -8
  144. package/src/testing/flight.entry.ts +14 -0
  145. package/src/testing/run-loader.ts +63 -5
  146. package/src/testing/serve-shell-request.ts +378 -0
  147. package/src/testing/shell-status.ts +34 -12
  148. package/src/theme/ThemeProvider.tsx +7 -6
  149. package/src/types/handler-context.ts +14 -1
  150. package/src/urls/pattern-types.ts +22 -18
  151. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  152. package/src/vite/router-discovery.ts +14 -4
@@ -107,6 +107,7 @@ import {
107
107
  createShellImplicitDocScope,
108
108
  type CacheScope,
109
109
  } from "../../cache/cache-scope.js";
110
+ import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
110
111
  import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
111
112
  import { paramsEqual } from "../params-util.js";
112
113
 
@@ -508,6 +509,15 @@ export function withCacheLookup<TEnv>(
508
509
  }
509
510
  }
510
511
 
512
+ // A document HIT tail replays the handler layer through the implicit doc
513
+ // scope and never runs a handler: without that scope (a context that lost
514
+ // the marker before the scope resolved), fail like a lookup miss instead
515
+ // of falling through to the handlers below.
516
+ const tailMarker = pipelineReqCtx?._shellImplicitCache;
517
+ if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
518
+ throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
519
+ }
520
+
511
521
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
512
522
  yield* source;
513
523
  if (ms) {
@@ -585,6 +595,13 @@ export function withCacheLookup<TEnv>(
585
595
  }
586
596
 
587
597
  if (!cacheResult) {
598
+ // A document shell HIT tail replays the handler layer from the entry's
599
+ // doc record and must never run a handler behind the committed prelude:
600
+ // a record that did not hit (it failed to decode, or the entry lost it)
601
+ // ends the tail here; serveShellHit degrades the response.
602
+ if (replayMarker?.docTail) {
603
+ throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
604
+ }
588
605
  yield* source;
589
606
  if (ms) {
590
607
  ms.metrics.push({
@@ -106,7 +106,11 @@ import {
106
106
  type RequestContext,
107
107
  } from "../../server/request-context.js";
108
108
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
109
- import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
109
+ import {
110
+ createShellImplicitDocScope,
111
+ resolveCacheTags,
112
+ } from "../../cache/cache-scope.js";
113
+ import { recordSegmentTags } from "../../cache/cache-tag.js";
110
114
  import { getRouterContext } from "../router-context.js";
111
115
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
112
116
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
@@ -316,9 +320,10 @@ export function withCacheStore<TEnv>(
316
320
  * store) on the no-eligible-snapshot path, so the lookup's own refusal is
317
321
  * still surfaced while the fallback has nothing to serve.
318
322
  *
319
- * Wrapped in requestCtx.waitUntil — during a capture that is the tracked-write
320
- * override, so captureAndStoreShell's settleWrites awaits the record before
321
- * draining the snapshot.
323
+ * Registered through requestCtx.onResponse, which a capture fires only after
324
+ * the handler pushes settle (settleCaptureRecord), and wrapped in
325
+ * requestCtx.waitUntil — during a capture that is the tracked-write override,
326
+ * so settleWrites awaits the record before the capture reads it.
322
327
  */
323
328
  function recordShellCaptureDocRecord<TEnv>(
324
329
  ctx: MatchContext<TEnv>,
@@ -336,12 +341,27 @@ function recordShellCaptureDocRecord<TEnv>(
336
341
  if (!scope.allowsCache("write")) return;
337
342
 
338
343
  const docScope = createShellImplicitDocScope(marker);
339
- requestCtx.waitUntil(() =>
340
- docScope.cacheRoute(
341
- ctx.pathname,
342
- ctx.matched.params,
343
- segments,
344
- ctx.isIntercept,
345
- ),
344
+ // The route scope's cache({ tags }) describe these segments too: on its
345
+ // HIT they arrive with the replayed record (recordSegmentTags in
346
+ // lookupRouteDetailed); on a fresh render they are recorded here, so the
347
+ // doc record, and the shell taking its tags, carries them either way.
348
+ recordSegmentTags(
349
+ segments.map((s) => s.id),
350
+ resolveCacheTags(scope.config, requestCtx),
351
+ requestCtx,
346
352
  );
353
+ // Through onResponse, like the implicit scope's own write: the capture
354
+ // fires these callbacks (settleCaptureRecord) only after the handler
355
+ // pushes settled, so the record's handle encode never races them.
356
+ requestCtx.onResponse((response) => {
357
+ requestCtx.waitUntil(() =>
358
+ docScope.cacheRoute(
359
+ ctx.pathname,
360
+ ctx.matched.params,
361
+ segments,
362
+ ctx.isIntercept,
363
+ ),
364
+ );
365
+ return response;
366
+ });
347
367
  }
@@ -68,7 +68,12 @@ export interface MiddlewareContext<
68
68
 
69
69
  debugPerformance(): void;
70
70
 
71
- theme?: Theme;
71
+ /**
72
+ * The visitor's theme (cookie or default); undefined without theme config.
73
+ * Guarded like `cookies()`: a read inside a `"use cache"` function throws
74
+ * (#971).
75
+ */
76
+ readonly theme?: Theme;
72
77
 
73
78
  setTheme?: (theme: Theme) => void;
74
79
 
@@ -31,6 +31,8 @@ import {
31
31
  } from "../cache/taint.js";
32
32
  import { getCacheExecScope } from "../cache/cache-exec-scope.js";
33
33
  import { assertNonCacheableReadAllowed } from "../server/context.js";
34
+ import { assertThemeReadAllowed } from "../server/cookie-store.js";
35
+ import type { Theme } from "../theme/types.js";
34
36
 
35
37
  // Re-export types consumed through this module's path.
36
38
  export type {
@@ -40,6 +42,25 @@ export type {
40
42
  MiddlewareFn,
41
43
  } from "./middleware-types.js";
42
44
 
45
+ /**
46
+ * Middleware ctx.theme: the visitor's theme cookie, guarded like the handler
47
+ * ctx.theme and cookies() (#971, #946), so a read inside a "use cache"
48
+ * function the middleware calls throws. Non-enumerable like the handler and
49
+ * request-context getters: a spread of the ctx (inside a "use cache"
50
+ * function, say) does not read it. One shared descriptor, so every ctx keeps
51
+ * the same property shape.
52
+ */
53
+ const MIDDLEWARE_THEME_DESCRIPTOR: PropertyDescriptor = {
54
+ get(): Theme | undefined {
55
+ const reqCtx = _getRequestContext();
56
+ if (!reqCtx?._themeConfig) return undefined;
57
+ assertThemeReadAllowed(reqCtx, "ctx.theme");
58
+ return reqCtx._readTheme();
59
+ },
60
+ enumerable: false,
61
+ configurable: true,
62
+ };
63
+
43
64
  const MIDDLEWARE_METRIC_DEPTH = 1;
44
65
  const POST_METRIC_MIN_DURATION_MS = 0.01;
45
66
 
@@ -320,10 +341,7 @@ export function createMiddlewareContext<TEnv>(
320
341
  responseHolder.response.headers.set(name, value);
321
342
  },
322
343
 
323
- get theme(): MiddlewareContext<TEnv>["theme"] {
324
- return _getRequestContext()?.theme;
325
- },
326
-
344
+ // theme: MIDDLEWARE_THEME_DESCRIPTOR, defined after this literal.
327
345
  get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
328
346
  return _getRequestContext()?.setTheme;
329
347
  },
@@ -361,6 +379,7 @@ export function createMiddlewareContext<TEnv>(
361
379
  }
362
380
  },
363
381
  };
382
+ Object.defineProperty(ctx, "theme", MIDDLEWARE_THEME_DESCRIPTOR);
364
383
  (ctx as any)[NOCACHE_SYMBOL] = true;
365
384
  return ctx;
366
385
  }
@@ -230,6 +230,7 @@ export async function matchForPrerender<TEnv = any>(
230
230
  _setStatus: () => {},
231
231
  _rotateStateCookie: () => {},
232
232
  _setKeepCacheDirective: () => {},
233
+ _setFreshReadsCookie: () => {},
233
234
  use: (() => {
234
235
  throw new Error("use() not available during pre-rendering");
235
236
  }) as any,
@@ -239,6 +240,8 @@ export async function matchForPrerender<TEnv = any>(
239
240
  waitUntil: () => {},
240
241
  onResponse: () => {},
241
242
  _onResponseCallbacks: [],
243
+ // No theme at build time: no request cookie.
244
+ _readTheme: () => undefined,
242
245
  setLocationState() {},
243
246
  _locationState: undefined,
244
247
  _renderBarrier: Promise.resolve(),
@@ -530,6 +533,7 @@ export async function renderStaticSegment<TEnv = any>(
530
533
  _setStatus: () => {},
531
534
  _rotateStateCookie: () => {},
532
535
  _setKeepCacheDirective: () => {},
536
+ _setFreshReadsCookie: () => {},
533
537
  use: (() => {
534
538
  throw new Error("use() not available during static pre-rendering");
535
539
  }) as any,
@@ -539,6 +543,8 @@ export async function renderStaticSegment<TEnv = any>(
539
543
  waitUntil: () => {},
540
544
  onResponse: () => {},
541
545
  _onResponseCallbacks: [],
546
+ // No theme at build time: no request cookie.
547
+ _readTheme: () => undefined,
542
548
  setLocationState() {},
543
549
  _locationState: undefined,
544
550
  _renderBarrier: Promise.resolve(),
@@ -37,7 +37,7 @@ import { applyViewTransitionDefault } from "./view-transition-default.js";
37
37
  import { _getRequestContext } from "../../server/request-context.js";
38
38
  import { getRouterContext } from "../router-context.js";
39
39
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
40
- import { observeHandler } from "../instrument.js";
40
+ import { observeSegmentHandler } from "../instrument.js";
41
41
  import {
42
42
  track,
43
43
  RangoContext,
@@ -173,9 +173,7 @@ export async function resolveLoaders<TEnv>(
173
173
  // bakes at capture regardless of the entry's loading() lane
174
174
  // (loader-cache.ts capture branch) and its HIT-tail seed
175
175
  // overlay needs the same key.
176
- bakeLane || loaderEntry.awaitBeforeFlush === true
177
- ? segmentId
178
- : null,
176
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
179
177
  ),
180
178
  ),
181
179
  entry,
@@ -223,7 +221,7 @@ export async function resolveLoaders<TEnv>(
223
221
  loaderEntry,
224
222
  ctx,
225
223
  ctx.pathname,
226
- bakeLane || loaderEntry.awaitBeforeFlush === true ? segmentId : null,
224
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
227
225
  ),
228
226
  ),
229
227
  entry,
@@ -396,7 +394,7 @@ export async function resolveSegment<TEnv>(
396
394
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
397
395
  if (entry.loading) {
398
396
  const result = handleHandlerResult(
399
- observeHandler(entry.id, handler, context),
397
+ observeSegmentHandler(entry.shortCode, entry.id, handler, context),
400
398
  );
401
399
  if (result instanceof Promise) {
402
400
  warnOnStreamedResponse(result, entry.id);
@@ -420,7 +418,12 @@ export async function resolveSegment<TEnv>(
420
418
  }
421
419
  } else {
422
420
  component = handleHandlerResult(
423
- await observeHandler(entry.id, handler, context),
421
+ await observeSegmentHandler(
422
+ entry.shortCode,
423
+ entry.id,
424
+ handler,
425
+ context,
426
+ ),
424
427
  );
425
428
  doneRouteHandler();
426
429
  }
@@ -670,7 +673,12 @@ export async function resolveParallelEntry<TEnv>(
670
673
  if (hasLoadingFallback) {
671
674
  const result =
672
675
  typeof handler === "function"
673
- ? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
676
+ ? observeSegmentHandler(
677
+ `${parentShortCode}.${slot}`,
678
+ `${parallelEntry.id}.${slot}`,
679
+ handler,
680
+ context,
681
+ )
674
682
  : handler;
675
683
  if (result instanceof Promise) {
676
684
  result.finally(doneParallelHandler).catch(() => {});
@@ -694,7 +702,8 @@ export async function resolveParallelEntry<TEnv>(
694
702
  } else {
695
703
  component =
696
704
  typeof handler === "function"
697
- ? await observeHandler(
705
+ ? await observeSegmentHandler(
706
+ `${parentShortCode}.${slot}`,
698
707
  `${parallelEntry.id}.${slot}`,
699
708
  handler,
700
709
  context,
@@ -29,7 +29,7 @@ import type {
29
29
  import type { SegmentResolutionDeps } from "../types.js";
30
30
  import { debugLog } from "../logging.js";
31
31
  import { tryStaticLookup } from "./static-store.js";
32
- import { observeHandler } from "../instrument.js";
32
+ import { observeSegmentHandler } from "../instrument.js";
33
33
  import type { TelemetrySink } from "../telemetry.js";
34
34
  import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
35
35
 
@@ -175,7 +175,9 @@ export async function resolveLayoutComponent<TEnv>(
175
175
  // by track("handler:<id>") at the call site). handleHandlerResult stays OUTSIDE
176
176
  // the span so a handler that returns a Response (redirect control flow, which it
177
177
  // rethrows) is not recorded as a span error — mirrors the route-handler sites.
178
- return handleHandlerResult(await observeHandler(entry.id, handler, context));
178
+ return handleHandlerResult(
179
+ await observeSegmentHandler(entry.shortCode, entry.id, handler, context),
180
+ );
179
181
  }
180
182
 
181
183
  // ---------------------------------------------------------------------------