@rangojs/router 0.22.0 → 0.23.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 (180) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  7. package/dist/types/browser/partial-update.d.ts +34 -5
  8. package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
  9. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  10. package/dist/types/browser/response-adapter.d.ts +7 -0
  11. package/dist/types/browser/types.d.ts +19 -9
  12. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  13. package/dist/types/build/generate-route-types.d.ts +1 -1
  14. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  15. package/dist/types/build/route-types/scan-filter.d.ts +16 -2
  16. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  17. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  18. package/dist/types/client-urls/client-urls.d.ts +1 -1
  19. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  20. package/dist/types/client-urls/navigation.d.ts +0 -37
  21. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  22. package/dist/types/client-urls/server-projection.d.ts +29 -1
  23. package/dist/types/client-urls/types.d.ts +34 -69
  24. package/dist/types/client.d.ts +0 -1
  25. package/dist/types/errors.d.ts +5 -0
  26. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  27. package/dist/types/handles/pushed-by.d.ts +23 -0
  28. package/dist/types/loader-store.d.ts +8 -0
  29. package/dist/types/prefetch-markers.d.ts +11 -0
  30. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  31. package/dist/types/router/is-action.d.ts +2 -1
  32. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  33. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
  34. package/dist/types/server/context.d.ts +21 -1
  35. package/dist/types/server/handle-store.d.ts +9 -1
  36. package/dist/types/server/request-context.d.ts +14 -1
  37. package/dist/types/server.d.ts +1 -0
  38. package/dist/types/testing/flight.entry.d.ts +1 -1
  39. package/dist/types/testing/index.d.ts +3 -3
  40. package/dist/types/testing/internal/action-id.d.ts +10 -1
  41. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  42. package/dist/types/testing/internal/test-action.d.ts +8 -0
  43. package/dist/types/testing/run-loader.d.ts +2 -1
  44. package/dist/types/testing/run-revalidate.d.ts +74 -0
  45. package/dist/types/testing/run-transition-when.d.ts +2 -3
  46. package/dist/types/testing/serve-shell-request.d.ts +38 -0
  47. package/dist/types/testing/vitest.d.ts +11 -0
  48. package/dist/types/types/boundaries.d.ts +9 -0
  49. package/dist/types/types/handler-context.d.ts +14 -0
  50. package/dist/types/types/loader-types.d.ts +30 -0
  51. package/dist/types/types/segments.d.ts +21 -4
  52. package/dist/types/urls/path-helper-types.d.ts +25 -4
  53. package/dist/types/urls/type-extraction.d.ts +6 -2
  54. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  55. package/dist/types/vite/discovery/state.d.ts +11 -4
  56. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  57. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  58. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  59. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  60. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  61. package/dist/types/vite/router-discovery.d.ts +10 -0
  62. package/dist/vite/index.js +843 -309
  63. package/package.json +2 -2
  64. package/skills/caching/SKILL.md +42 -5
  65. package/skills/catalog.json +2 -2
  66. package/skills/client-urls/SKILL.md +113 -24
  67. package/skills/cloudflare/SKILL.md +14 -9
  68. package/skills/hooks/SKILL.md +11 -3
  69. package/skills/links/SKILL.md +24 -0
  70. package/skills/loader/SKILL.md +192 -1
  71. package/skills/migrate-nextjs/SKILL.md +2 -2
  72. package/skills/migrate-react-router/SKILL.md +10 -7
  73. package/skills/migrate-react-router/data-and-actions.md +1 -1
  74. package/skills/ppr/SKILL.md +13 -0
  75. package/skills/prerender/SKILL.md +22 -8
  76. package/skills/rango/SKILL.md +12 -6
  77. package/skills/route/SKILL.md +139 -0
  78. package/skills/testing/SKILL.md +3 -2
  79. package/skills/testing/client-components.md +1 -1
  80. package/skills/testing/predicates.md +67 -32
  81. package/skills/testing/serve-shell-request.md +88 -18
  82. package/skills/testing/setup.md +2 -2
  83. package/skills/typesafety/route-types.md +2 -2
  84. package/skills/view-transitions/SKILL.md +26 -0
  85. package/src/browser/defer.ts +82 -0
  86. package/src/browser/document-revealed.ts +52 -0
  87. package/src/browser/event-controller.ts +43 -32
  88. package/src/browser/navigation-bridge.ts +64 -14
  89. package/src/browser/navigation-client.ts +48 -36
  90. package/src/browser/navigation-store.ts +13 -0
  91. package/src/browser/navigation-transaction.ts +25 -8
  92. package/src/browser/partial-update.ts +528 -135
  93. package/src/browser/prefetch/fetch.ts +2 -1
  94. package/src/browser/react/NavigationProvider.tsx +47 -28
  95. package/src/browser/react/filter-segment-order.ts +13 -2
  96. package/src/browser/response-adapter.ts +20 -0
  97. package/src/browser/rsc-router.tsx +17 -45
  98. package/src/browser/server-action-bridge.ts +19 -63
  99. package/src/browser/types.ts +19 -9
  100. package/src/browser/validate-redirect-origin.ts +16 -0
  101. package/src/build/generate-route-types.ts +1 -0
  102. package/src/build/route-types/router-processing.ts +3 -6
  103. package/src/build/route-types/scan-filter.ts +37 -10
  104. package/src/cache/cf/cf-cache-store.ts +59 -21
  105. package/src/cache/cf/cf-cache-types.ts +24 -2
  106. package/src/cache/document-cache.ts +22 -1
  107. package/src/client-urls/client-urls.ts +22 -6
  108. package/src/client-urls/hoisted-revalidate.ts +25 -0
  109. package/src/client-urls/navigation.ts +0 -96
  110. package/src/client-urls/revalidate-registry.ts +30 -0
  111. package/src/client-urls/server-projection.ts +151 -65
  112. package/src/client-urls/types.ts +39 -71
  113. package/src/client.tsx +9 -5
  114. package/src/errors.ts +15 -0
  115. package/src/handles/deferred-resolution.ts +28 -5
  116. package/src/handles/pushed-by.ts +122 -0
  117. package/src/loader-store.ts +18 -7
  118. package/src/prefetch-markers.ts +13 -0
  119. package/src/route-content-wrapper.tsx +18 -4
  120. package/src/route-definition/dsl-helpers.ts +27 -4
  121. package/src/route-definition/helpers-types.ts +27 -1
  122. package/src/router/intercept-resolution.ts +6 -4
  123. package/src/router/is-action.ts +2 -1
  124. package/src/router/loader-resolution.ts +83 -4
  125. package/src/router/match-api.ts +36 -1
  126. package/src/router/match-middleware/background-revalidation.ts +11 -1
  127. package/src/router/match-middleware/cache-lookup.ts +29 -6
  128. package/src/router/match-middleware/cache-store.ts +17 -1
  129. package/src/router/match-result.ts +14 -2
  130. package/src/router/navigation-snapshot.ts +2 -1
  131. package/src/router/segment-resolution/fresh.ts +22 -18
  132. package/src/router/segment-resolution/loader-cache.ts +2 -1
  133. package/src/router/segment-resolution/loader-mask.ts +5 -3
  134. package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
  135. package/src/router/segment-resolution/revalidation.ts +169 -60
  136. package/src/rsc/rsc-rendering.ts +50 -7
  137. package/src/rsc/shell-capture.ts +2 -1
  138. package/src/segment-loader-promise.ts +23 -20
  139. package/src/segment-system.tsx +60 -26
  140. package/src/server/context.ts +39 -5
  141. package/src/server/handle-store.ts +104 -8
  142. package/src/server/loader-registry.ts +6 -1
  143. package/src/server/request-context.ts +21 -1
  144. package/src/server.ts +4 -0
  145. package/src/testing/action-form.ts +2 -11
  146. package/src/testing/e2e/page-helpers.ts +3 -1
  147. package/src/testing/flight.entry.ts +1 -0
  148. package/src/testing/index.ts +6 -3
  149. package/src/testing/internal/action-id.ts +16 -1
  150. package/src/testing/internal/client-urls-projection.ts +70 -0
  151. package/src/testing/internal/test-action.ts +25 -0
  152. package/src/testing/run-loader.ts +2 -1
  153. package/src/testing/run-revalidate.ts +142 -0
  154. package/src/testing/run-transition-when.ts +7 -8
  155. package/src/testing/serve-shell-request.ts +116 -6
  156. package/src/testing/vitest.ts +67 -1
  157. package/src/types/boundaries.ts +12 -0
  158. package/src/types/handler-context.ts +14 -0
  159. package/src/types/loader-types.ts +30 -0
  160. package/src/types/segments.ts +26 -4
  161. package/src/urls/path-helper-types.ts +27 -5
  162. package/src/urls/type-extraction.ts +30 -10
  163. package/src/use-loader.tsx +18 -13
  164. package/src/vite/discovery/client-urls-projection.ts +20 -21
  165. package/src/vite/discovery/discover-routers.ts +94 -26
  166. package/src/vite/discovery/state.ts +20 -4
  167. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  168. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  169. package/src/vite/plugins/hoist/literal.ts +417 -0
  170. package/src/vite/plugins/hoist/scope.ts +370 -0
  171. package/src/vite/plugins/hoist/source.ts +84 -0
  172. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  173. package/src/vite/rango.ts +2 -0
  174. package/src/vite/router-discovery.ts +93 -32
  175. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  176. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  177. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  178. package/src/client-urls/revalidate-chain.ts +0 -83
  179. package/src/client-urls/revalidation-protocol.ts +0 -56
  180. package/src/testing/run-client-revalidate.ts +0 -92
package/dist/bin/rango.js CHANGED
@@ -210,9 +210,12 @@ var init_codegen = __esm({
210
210
  });
211
211
 
212
212
  // src/build/route-types/scan-filter.ts
213
- import { join, relative } from "node:path";
213
+ import { isAbsolute, join, relative } from "node:path";
214
214
  import { readdirSync } from "node:fs";
215
215
  import picomatch from "picomatch";
216
+ function isUniversallySkippedScanDir(name) {
217
+ return name === "node_modules" || name === "dist" || name === "coverage" || name.startsWith(".");
218
+ }
216
219
  function findTsFiles(dir, filter) {
217
220
  const results = [];
218
221
  let entries;
@@ -227,8 +230,9 @@ function findTsFiles(dir, filter) {
227
230
  for (const entry of entries) {
228
231
  const fullPath = join(dir, entry.name);
229
232
  if (entry.isDirectory()) {
230
- if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist" || entry.name === "build" || entry.name === "coverage")
233
+ if (isUniversallySkippedScanDir(entry.name) || entry.name === "build") {
231
234
  continue;
235
+ }
232
236
  results.push(...findTsFiles(fullPath, filter));
233
237
  } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
234
238
  if (filter && !filter(fullPath)) continue;
@@ -1032,7 +1036,7 @@ function isRoutableSourceFile(name) {
1032
1036
  return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
1033
1037
  }
1034
1038
  function isExcludedScanDir(name) {
1035
- return name === "node_modules" || name === "dist" || name === "coverage" || name === "__tests__" || name === "__mocks__" || name.startsWith(".");
1039
+ return isUniversallySkippedScanDir(name) || name === "__tests__" || name === "__mocks__";
1036
1040
  }
1037
1041
  function findCallSiteFilesRecursive(dir, pattern, patternG, stopAtMatchDir, filter, results) {
1038
1042
  let entries;
@@ -1443,6 +1447,7 @@ var init_router_processing = __esm({
1443
1447
  "src/build/route-types/router-processing.ts"() {
1444
1448
  "use strict";
1445
1449
  init_codegen();
1450
+ init_scan_filter();
1446
1451
  init_source_scan();
1447
1452
  init_include_resolution();
1448
1453
  init_per_module_writer();
@@ -243,6 +243,22 @@ function resolveFromRouter(spec) {
243
243
  }
244
244
  }
245
245
  var rsdServerEdgePath = resolveFromRouter(RSD_SERVER_EDGE_SPEC);
246
+ var CLIENT_URLS_SOURCE_QUERY = "rango-client-urls-source";
247
+ var CLIENT_ENTRY_RSC_PATH = here("src/client.rsc.tsx").replaceAll(
248
+ "\\",
249
+ "/"
250
+ );
251
+ var CLIENT_URLS_INSTALLER_PATH = here(
252
+ "src/testing/internal/client-urls-projection.ts"
253
+ ).replaceAll("\\", "/");
254
+ function clientUrlsProjectionInstall(id) {
255
+ const source = `${id}${id.includes("?") ? "&" : "?"}${CLIENT_URLS_SOURCE_QUERY}`;
256
+ return `
257
+ import * as $$RangoClientUrlsSource from ${JSON.stringify(source)};
258
+ import { installClientUrlsProjection as $$rangoInstallClientUrlsProjection } from ${JSON.stringify(CLIENT_URLS_INSTALLER_PATH)};
259
+ $$rangoInstallClientUrlsProjection(${JSON.stringify(`${id}#default`)}, $$RangoClientUrlsSource);
260
+ `;
261
+ }
246
262
  function rangoTestAliases(opts = {}) {
247
263
  const aliases = [
248
264
  // Real impls (index.rsc.ts) for the bare specifier ONLY — exact regex so
@@ -290,8 +306,19 @@ function rangoTestConfig(opts = {}) {
290
306
  function rangoUseClientTransform() {
291
307
  return {
292
308
  name: "rango:testing-use-client",
309
+ resolveId: {
310
+ // Before Vite's resolver, which resolves the bare specifier first.
311
+ order: "pre",
312
+ handler(source, importer) {
313
+ if (source === "@rangojs/router/client" && importer?.includes(CLIENT_URLS_SOURCE_QUERY)) {
314
+ return CLIENT_ENTRY_RSC_PATH;
315
+ }
316
+ return void 0;
317
+ }
318
+ },
293
319
  async transform(code, id) {
294
320
  if (id.includes("/node_modules/")) return void 0;
321
+ if (id.includes(CLIENT_URLS_SOURCE_QUERY)) return void 0;
295
322
  if (!code.includes("use client")) return void 0;
296
323
  const { parseAstAsync } = await import("vite");
297
324
  const { hasDirective, transformDirectiveProxyExport } = await import("@vitejs/plugin-rsc/transforms");
@@ -308,12 +335,15 @@ function rangoUseClientTransform() {
308
335
  runtime: (name) => `$$RangoRSD.registerClientReference(() => { throw new Error("client reference " + ${JSON.stringify(name)} + " is not callable on the server"); }, ${JSON.stringify(id)}, ${JSON.stringify(name)})`
309
336
  });
310
337
  if (!result) return void 0;
311
- const { output } = result;
338
+ const { output, exportNames } = result;
312
339
  const rsdHref = rsdServerEdgePath ? pathToFileURL(rsdServerEdgePath).href : RSD_SERVER_EDGE_SPEC;
313
340
  output.prepend(
314
341
  `import * as $$RangoRSD from ${JSON.stringify(rsdHref)};
315
342
  `
316
343
  );
344
+ if (code.includes("clientUrls(") && exportNames.includes("default")) {
345
+ output.append(clientUrlsProjectionInstall(id));
346
+ }
317
347
  return {
318
348
  code: output.toString(),
319
349
  map: output.generateMap({ hires: true })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * One hole of a `prefetch: false` adoption (browser/partial-update.ts
3
+ * armHoles): the promise every reader of the hole waits on, and what the
4
+ * router knows of it. The router sets `status` and `value` here; it never
5
+ * writes React's thenable fields on `promise`.
6
+ *
7
+ * Resolve-only: a hole is filled or released (releaseHoles), never rejected.
8
+ * A failure arrives as a fulfilled value (a loader's error envelope, an
9
+ * error segment's node).
10
+ */
11
+ export declare class Defer<T> {
12
+ status: "pending" | "fulfilled";
13
+ value: T | undefined;
14
+ /** A reader has handed React `promise` (readDefer). */
15
+ read?: boolean;
16
+ /**
17
+ * Resolved with a value the router holds (a node), the hole is
18
+ * "fulfilled" at once. Resolved with a stream (a Flight chunk), its promise
19
+ * follows the stream, and so does the hole: "fulfilled" with the stream's
20
+ * value once it arrives, so a reader that mounts after that reads the value
21
+ * as it would read the chunk Flight marked. A rejected stream leaves the
22
+ * hole pending; its promise carries the rejection.
23
+ */
24
+ resolve: (value: T) => void;
25
+ readonly promise: Promise<T>;
26
+ }
27
+ /**
28
+ * The read rule, for a component that reads a hole (Suspender, useLoader).
29
+ * A hole that has fulfilled and that no reader has handed React is its
30
+ * value: use() of a promise React has not seen suspends an urgent render
31
+ * once, and the boundary shows its fallback. Once a reader has handed React
32
+ * the promise, every read is use() of that promise: React has marked it and
33
+ * returns its value at once, and a replay of the reader must hand use() the
34
+ * thenable it read before ("A component was suspended by an uncached
35
+ * promise"). Either way use() is called once, as at every useLoader read.
36
+ */
37
+ export declare function readDefer<T>(hole: Defer<T>): T;
38
+ /**
39
+ * A hole as the loader-streams convention has it: its promise while pending,
40
+ * its value once fulfilled. Anything else is returned as it is. For an
41
+ * aggregate, the pending-stream registry, and a node React reads itself (a
42
+ * segment or slot with no `loading()`): a router component there would be a
43
+ * fiber the filled page's node does not have, and the next render from the
44
+ * filled entry would remount the segment. React suspends such a read on the
45
+ * component above it, which commits nothing until the hole fulfils, so the
46
+ * value can stand where React read the promise.
47
+ */
48
+ export declare function asStream(value: unknown): unknown;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Resolves once the document has finished parsing and no Suspense boundary of
3
+ * it is unrevealed.
4
+ *
5
+ * Parsing ends with the stream, but React reveals a streamed boundary in
6
+ * batches up to 300 ms later (Fizz's $RC marks it `$~` and reveals it from a
7
+ * timer), so the boundary is still pending at DOMContentLoaded. A transition
8
+ * then would make React client-render it over its server HTML.
9
+ *
10
+ * A reveal rewrites the same comment's data, or React removes the node; a
11
+ * boundary nested in revealed content joins the document only then, so it
12
+ * resolves only once a full walk finds none. Polls with setTimeout, not
13
+ * requestAnimationFrame: a hidden tab never runs rAF, which would hold late
14
+ * handle pushes and location state until the tab is shown again. A tab hidden
15
+ * from load still waits: Fizz reveals boundaries from a rAF-gated script.
16
+ */
17
+ export declare function awaitDocumentRevealed(): Promise<void>;
@@ -178,7 +178,13 @@ export interface EventController {
178
178
  startNavigation(url: string, options?: NavigateOptionsInternal & {
179
179
  skipLoadingState?: boolean;
180
180
  }): NavigationHandle;
181
- abortNavigation(): void;
181
+ /**
182
+ * Abort the pending navigation, and the committed one whose stream is
183
+ * still open. `pendingOnly` leaves the committed one alone: a page
184
+ * restored from the back/forward cache is not being left
185
+ * (navigation-bridge.ts handlePageShow).
186
+ */
187
+ abortNavigation(pendingOnly?: boolean): void;
182
188
  startAction(actionId: string, args: unknown[],
183
189
  /** Originating history entry key; scopes location-state arbitration. */
184
190
  cohort?: string): ActionHandle;
@@ -65,6 +65,24 @@ interface BoundCommitOverrides {
65
65
  /** No payload follows this commit (CommitOptions.treeless). */
66
66
  treeless?: boolean;
67
67
  }
68
+ /**
69
+ * What a commit decided about scroll, in the shape the update that follows
70
+ * carries it (NavigationUpdate.scroll). A navigation always decides, and "do
71
+ * not scroll" is a decision (`<Link scroll={false}>`, an intercept): it
72
+ * replaces a pending one. A commit that is not a navigation decides nothing:
73
+ * an action refetch into the entry on screen (`storeOnly`) and a cache-only
74
+ * commit return no `scroll`, and their update leaves a pending decision alone
75
+ * (react/NavigationProvider.tsx).
76
+ *
77
+ * Scar tissue: those two used to answer `scroll: false`. An action refetch
78
+ * that landed before React committed a navigation then replaced the
79
+ * navigation's pending scroll with "do not scroll".
80
+ */
81
+ export interface CommitResult {
82
+ scroll?: {
83
+ enabled: boolean | undefined;
84
+ };
85
+ }
68
86
  /**
69
87
  * Bound transaction with pre-configured commit options (without segmentIds/segments)
70
88
  */
@@ -80,19 +98,15 @@ export interface BoundTransaction {
80
98
  readonly transitionGatedOff?: boolean;
81
99
  /** Start streaming and get a token to end it when the stream completes */
82
100
  startStreaming(): StreamingToken;
83
- /** Commit the navigation. Returns the effective scroll option for the caller to handle. */
84
- commit(segmentIds: string[], segments: ResolvedSegment[], overrides?: BoundCommitOverrides): {
85
- scroll?: boolean;
86
- };
101
+ /** Commit the navigation. Returns its scroll decision, for the update. */
102
+ commit(segmentIds: string[], segments: ResolvedSegment[], overrides?: BoundCommitOverrides): CommitResult;
87
103
  }
88
104
  /**
89
105
  * Navigation transaction for managing state during navigation
90
106
  * Uses the event controller handle for lifecycle management
91
107
  */
92
108
  interface NavigationTransaction extends Disposable {
93
- commit(options: CommitOptions): {
94
- scroll?: boolean;
95
- };
109
+ commit(options: CommitOptions): CommitResult;
96
110
  with(options: Omit<CommitOptions, "segmentIds" | "segments">): BoundTransaction;
97
111
  /** The navigation handle from the event controller */
98
112
  handle: NavigationHandle;
@@ -2,7 +2,7 @@ import type { NavigationStore, NavigationClient, UpdateSubscriber, ResolvedSegme
2
2
  import type { ReactNode } from "react";
3
3
  import type { RenderSegmentsOptions } from "../segment-system.js";
4
4
  import type { BoundTransaction } from "./navigation-transaction.js";
5
- import type { NavigationUpdate } from "./types.js";
5
+ import type { NavigationUpdate, RscMetadata } from "./types.js";
6
6
  import type { LoaderStore } from "../loader-store.js";
7
7
  import { type TransitionWhenActionInput } from "./transition-when.js";
8
8
  /**
@@ -30,6 +30,31 @@ export interface PartialUpdateConfig {
30
30
  renderSegments: (segments: ResolvedSegment[], options?: RenderSegmentsOptions) => Promise<ReactNode> | ReactNode;
31
31
  /** RSC version getter — returns the current version (may change after HMR) */
32
32
  getVersion?: () => string | undefined;
33
+ /**
34
+ * A fill (`prefetch: false`) lands after its navigation has committed: the
35
+ * transaction has recorded the entry and handed React the tree, and the
36
+ * fill hands React no update that would carry what its response holds to
37
+ * the stores. The bridge that owns navigation supplies the writes, the
38
+ * ones its own updates make. Without them a redirect is a document
39
+ * navigation and the rest is dropped.
40
+ */
41
+ fill?: {
42
+ redirect(url: string, state?: Record<string, unknown>): void;
43
+ /** The location state the server set, to the entry and its readers. */
44
+ locationState(state: Record<string, unknown>): void;
45
+ /** What the response's handlers pushed, to the handle store. */
46
+ handles(metadata: RscMetadata): void;
47
+ /**
48
+ * Deliver the navigation state to its readers now. A page built with
49
+ * holes commits in the task of its click, before the notification of the
50
+ * click has gone out (event-controller.ts makeDebouncedNotifier), and
51
+ * React holds that commit where a hole is read with no boundary: without
52
+ * this useNavigation() reads idle, on the page being left, for the whole
53
+ * wait. Delivered first, `loading` is on screen when the commit is
54
+ * handed over, and stays while React holds it, as for a plain click.
55
+ */
56
+ flush(): void;
57
+ };
33
58
  }
34
59
  /**
35
60
  * Options that can override the pre-configured commit settings
@@ -64,12 +89,16 @@ export type UpdateMode = {
64
89
  /** Source URL for intercept restore (popstate cache miss) */
65
90
  interceptSourceUrl?: string;
66
91
  /**
67
- * The bridge already presented an optimistic clientUrls() destination
68
- * for this navigation: transition-lane commits add
92
+ * The optimistic clientUrls() destination the bridge presented for
93
+ * this navigation. Transition-lane commits add
69
94
  * OPTIMISTIC_COMMIT_TRANSITION_TYPE so router <ViewTransition>
70
- * boundaries do not animate the identical repaint.
95
+ * boundaries do not animate the identical repaint, and `clear()` runs
96
+ * once the canonical tree is handed to React: `useOutlet().pending`
97
+ * ends with that commit, not with a `prefetch: false` fill after it.
71
98
  */
72
- optimisticPresented?: boolean;
99
+ presentation?: {
100
+ clear(): void;
101
+ };
73
102
  /**
74
103
  * A router.refresh() (or cross-tab refresh) of the current URL:
75
104
  * transition({ when }) sees kind "revalidate", not "replace".
@@ -1,8 +1,15 @@
1
1
  import { type ReactNode } from "react";
2
- import type { NavigationStore, NavigationUpdate, NavigationBridge } from "../types.js";
2
+ import type { NavigationStore, NavigationUpdate, NavigationBridge, RscMetadata } from "../types.js";
3
3
  import type { EventController } from "../event-controller.js";
4
4
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
5
5
  import { type AppShellRef } from "../app-shell.js";
6
+ /**
7
+ * Read the handle stream of `metadata` into the event controller and the
8
+ * entry on screen. The provider's own call for every payload it is handed,
9
+ * and the one write for a response that hands React nothing (a fill,
10
+ * browser/partial-update.ts fillHoles).
11
+ */
12
+ export declare function consumeHandles(metadata: RscMetadata, eventController: EventController, store: NavigationStore): void;
6
13
  /**
7
14
  * Props for NavigationProvider
8
15
  */
@@ -69,8 +76,11 @@ export interface NavigationProviderProps {
69
76
  * `settled` resolves once the stream has ended and React has revealed every
70
77
  * boundary. Until then location state is `undefined`, as SSR rendered it;
71
78
  * then one transition gives the entry's state and changes
72
- * HydrationBarrierContext, and the store is released after that commit. A
73
- * navigation committed earlier releases the store before its update.
79
+ * HydrationBarrierContext, and the store is released after that commit
80
+ * (handle data lands in that release notification). A navigation committed
81
+ * earlier releases the store before its update, for the whole hold when the
82
+ * navigation is held: an old-page boundary hydrating meanwhile reads
83
+ * the destination's values and can mismatch.
74
84
  * Absent for a provider mounted on the client.
75
85
  */
76
86
  hydration?: {
@@ -1,3 +1,10 @@
1
+ /**
2
+ * A loader sub-id carries a `D${index}.` marker ("M0L0D1.user"). Non-global,
3
+ * so `.test` and `.replace` are stateless.
4
+ */
5
+ export declare const LOADER_SEGMENT_ID: RegExp;
6
+ /** True for a loader sub-id ("M0L0D1.user"), false for a layout/route/slot id. */
7
+ export declare function isLoaderSegmentId(id: string): boolean;
1
8
  /**
2
9
  * Build the handle-collection segment order from a raw `matched` list.
3
10
  *
@@ -6,6 +6,13 @@ type HeaderResult = {
6
6
  * Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
7
7
  */
8
8
  export declare function extractRscHeaderUrl(response: Response, header: string): HeaderResult;
9
+ /**
10
+ * Run `request` with a signal that `signal` aborts until the request settles,
11
+ * and only until then. A response that has arrived streams to its end:
12
+ * aborting a Flight body mid-read rejects every chunk a mounted tree may be
13
+ * reading. Aborted, the request rejects with `signal.reason`.
14
+ */
15
+ export declare function abortUntilResponse<T>(signal: AbortSignal | undefined, request: (signal: AbortSignal | undefined) => Promise<T>): Promise<T>;
9
16
  /**
10
17
  * Empty 200 response that won't choke Flight parsing.
11
18
  * Used when a header URL is blocked by origin validation.
@@ -277,7 +277,12 @@ export interface HistoryEntryMemory {
277
277
  export interface NavigationUpdate {
278
278
  root: ReactNode | Promise<ReactNode>;
279
279
  metadata: RscMetadata;
280
- /** Scroll behavior to apply after React commits this update */
280
+ /**
281
+ * The scroll decision of the navigation transaction this update commits,
282
+ * applied after React commits it. Absent on every update that is not a
283
+ * navigation's (server action, error, `prefetch: false` fill): such an
284
+ * update says nothing about scroll, and leaves a pending decision alone.
285
+ */
281
286
  scroll?: {
282
287
  /** For back/forward: restore saved position */
283
288
  restore?: boolean;
@@ -537,6 +542,12 @@ export interface NavigationStore {
537
542
  * probe and write into one historyCache scan for the per-yield streaming path.
538
543
  */
539
544
  updateCacheHandleDataIfOwned(historyKey: string, handleData: HandleData, ownerInstance: number, stale?: boolean, handlesPending?: boolean): void;
545
+ /**
546
+ * The history keys whose entries show the page `historyKey`'s entry shows:
547
+ * that entry and the copies a shallow navigation made of it, which share its
548
+ * segments array. `[historyKey]` when it has no entry.
549
+ */
550
+ getPageHistoryKeys(historyKey: string): string[];
540
551
  markHistoryCacheStale(): void;
541
552
  markCacheAsStaleAndBroadcast(): void;
542
553
  clearHistoryCache(): void;
@@ -558,13 +569,6 @@ export interface FetchPartialOptions {
558
569
  signal?: AbortSignal;
559
570
  /** If true, this is a stale cache revalidation request - server should force revalidators */
560
571
  staleRevalidation?: boolean;
561
- /**
562
- * Encoded client-run per-loader revalidation decisions
563
- * (clientUrls revalidate() predicates executed in the browser); sent as
564
- * X-Rango-Client-Reval and honored only by materialized client-urls loader
565
- * stubs. Null/absent = locked server defaults.
566
- */
567
- clientRevalidation?: string | null;
568
572
  interceptSourceUrl?: string;
569
573
  /** RSC version for cache invalidation detection */
570
574
  version?: string;
@@ -572,6 +576,12 @@ export interface FetchPartialOptions {
572
576
  routerId?: string;
573
577
  /** If true, this is an HMR refetch - server should invalidate manifest cache */
574
578
  hmr?: boolean;
579
+ /**
580
+ * The fill request of an adopted prefetch that carried deferred segments
581
+ * (`prefetch: false`): marked `_rsc_fill`, never answered from or written
582
+ * to a prefetch cache or the HTTP cache, and it cancels no prefetch.
583
+ */
584
+ fill?: boolean;
575
585
  }
576
586
  /**
577
587
  * Result of a partial fetch including stream completion tracking
@@ -644,7 +654,7 @@ export interface NavigationBridgeConfig {
644
654
  export type { ResolvedSegment };
645
655
  /**
646
656
  * Token for tracking an active stream.
647
- * Call end() when the stream completes.
657
+ * Call end() when the stream completes. Only the first call counts.
648
658
  */
649
659
  export interface StreamingToken {
650
660
  end(): void;
@@ -26,3 +26,14 @@ export declare function validateRedirectOrigin(url: string, currentOrigin: strin
26
26
  * @returns The normalized URL string on success, or null if blocked.
27
27
  */
28
28
  export declare function validateExternalRedirect(url: string, currentOrigin: string): string | null;
29
+ /**
30
+ * The target of a redirect a Flight payload carries (`metadata.redirect`), or
31
+ * null when the client refuses it. An explicit off-host one
32
+ * (`redirect(url, { external: true })`) is scheme-validated only:
33
+ * `external` waives the same-origin check the app opted out of, not scheme
34
+ * safety. Every other one must stay on this origin.
35
+ */
36
+ export declare function validatePayloadRedirect(redirect: {
37
+ url: string;
38
+ external?: boolean;
39
+ }): string | null;
@@ -1,7 +1,7 @@
1
1
  export { extractParamsFromPattern, formatRouteEntry, } from "./route-types/param-extraction.js";
2
2
  export { extractRoutesFromSource } from "./route-types/ast-route-extraction.js";
3
3
  export { generatePerModuleTypesSource, generateRouteTypesSource, } from "./route-types/codegen.js";
4
- export { DEFAULT_EXCLUDE_PATTERNS, type ScanFilter, createScanFilter, findTsFiles, } from "./route-types/scan-filter.js";
4
+ export { DEFAULT_EXCLUDE_PATTERNS, type ScanFilter, createScanFilter, findTsFiles, isInSkippedScanDir, } from "./route-types/scan-filter.js";
5
5
  export { writePerModuleRouteTypes, writePerModuleRouteTypesForFile, } from "./route-types/per-module-writer.js";
6
6
  export { type UnresolvableReason, type UnresolvableInclude, extractIncludesWithDiagnostics, } from "./route-types/include-resolution.js";
7
7
  export { extractUrlsFromRouter, extractBasenameFromRouter, type UrlsExtractionResult, buildCombinedRouteMapForRouterFile, detectUnresolvableIncludes, detectUnresolvableIncludesForUrlsFile, findNestedRouterConflict, formatNestedRouterConflictError, findRouterFiles, findHostRouterFiles, writeCombinedRouteTypes, genFileTsPath, resolveSearchSchemas, } from "./route-types/router-processing.js";
@@ -1,4 +1,4 @@
1
- import type { ScanFilter } from "./scan-filter.js";
1
+ import { type ScanFilter } from "./scan-filter.js";
2
2
  import { type UnresolvableInclude } from "./include-resolution.js";
3
3
  export declare function findNestedRouterConflict(routerFiles: string[]): {
4
4
  ancestor: string;
@@ -11,7 +11,21 @@ export declare function createScanFilter(root: string, opts: {
11
11
  exclude?: string[];
12
12
  }): ScanFilter | undefined;
13
13
  /**
14
- * Recursively find .ts/.tsx files under a directory, skipping node_modules
15
- * and .gen. files.
14
+ * Directory names no discovery scan enters: findTsFiles() and the router scans
15
+ * (findRouterFiles/findHostRouterFiles, router-processing.ts) all build their
16
+ * skip rule on this, so a file below one is never discovered. Dot-directories
17
+ * cover a nested git worktree, a tool cache, or a test's working copy of
18
+ * another app.
19
+ */
20
+ export declare function isUniversallySkippedScanDir(name: string): boolean;
21
+ /**
22
+ * Whether a file under `root` sits in a directory every discovery scan skips
23
+ * (isUniversallySkippedScanDir). The dev route-file watcher uses it to ignore
24
+ * files discovery would never find. A path outside `root` is false.
25
+ */
26
+ export declare function isInSkippedScanDir(root: string, absolutePath: string): boolean;
27
+ /**
28
+ * Recursively find .ts/.tsx files under a directory, skipping the directories
29
+ * isUniversallySkippedScanDir() names, `build`, and .gen. files.
16
30
  */
17
31
  export declare function findTsFiles(dir: string, filter?: ScanFilter): string[];
@@ -53,6 +53,7 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
53
53
  private readonly tagPurge?;
54
54
  private readonly tagInvalidationTtl?;
55
55
  private readonly tagCacheTtl;
56
+ private readonly kvFallThrough;
56
57
  constructor(options: CFCacheStoreOptions<TEnv>);
57
58
  /**
58
59
  * Warn about a namespace-scoped misconfiguration once per namespace per
@@ -350,7 +351,10 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
350
351
  * dropped and read from the store (SWR recapture scheduling and each mode's
351
352
  * invalidation semantics stay with the store read). That read keeps the
352
353
  * caller's `tagHints`: cfTagHints may have evicted the key while the memo
353
- * still holds it.
354
+ * still holds it. A shell a tag marker rejects is dropped and read from
355
+ * the store too, which may hold a capture from after the marker (see
356
+ * get(), "Tag invalidation"), unless the request's own mask rejected it
357
+ * (declinesKvFallThrough).
354
358
  */
355
359
  private readMemoizedShell;
356
360
  /**
@@ -577,6 +581,17 @@ export declare class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<T
577
581
  * @internal
578
582
  */
579
583
  private isL1Invalidated;
584
+ /**
585
+ * Whether a rejected L1 copy skips the KV read: always unless the
586
+ * `kvFallThrough` option is on, and with it on when this request's own
587
+ * updateTag()/revalidateTag() (its mask, request-tag-mask.ts) rejects an
588
+ * entry tagged at `taggedAt`. KV holds a newer entry only if something
589
+ * rendered after this request's invalidation, while the request re-renders
590
+ * every entry it masked, so the read would add one KV get per masked entry
591
+ * to that re-render.
592
+ * @internal
593
+ */
594
+ private declinesKvFallThrough;
580
595
  /**
581
596
  * Synthetic Cache API request for a tag's L1-cached invalidation marker.
582
597
  * Unversioned for the same reason as tagMarkerKey; the cache namespace
@@ -51,8 +51,11 @@ export interface CFCacheReadDebugEvent {
51
51
  * - kv-timeout: the L2/KV read budget fired (read abandoned, NOT a genuine
52
52
  * absence -- distinct from kv-miss so a degradation signal is separable)
53
53
  * - tag-invalidated: a live L1/KV entry whose cache tags were invalidated
54
- * after it was written -- treated as a miss so the next render re-populates
55
- * it (the tag-invalidation read path, distinct from a plain miss)
54
+ * after it was written -- never served (the tag-invalidation read path,
55
+ * distinct from a plain miss). With `kvFallThrough` on, an L1 copy then
56
+ * reads L2/KV, so a kv-* event follows unless the request invalidated the
57
+ * tag itself; from L2/KV it is a miss and the next render re-populates
58
+ * the entry
56
59
  * - error: the read threw
57
60
  */
58
61
  outcome: "l1-fresh" | "l1-stale-revalidate" | "l1-revalidating-guarded" | "match-timeout" | "match-error" | "body-timeout" | "body-error" | "non-200" | "tag-invalidated" | "l1-miss" | "kv-fresh" | "kv-stale" | "kv-stale-suppressed" | "kv-miss" | "kv-timeout" | "error";
@@ -335,6 +338,24 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
335
338
  * <= 0) to disable and always await KV.
336
339
  */
337
340
  kvReadTimeoutMs?: number;
341
+ /**
342
+ * After `updateTag()`/`revalidateTag()`, a location whose own L1 copy a tag
343
+ * marker rejects reads KV next, and serves the entry another location (or a
344
+ * `router.prerender()` warm) wrote after the invalidation, instead of
345
+ * rendering again. Covers `cache()` records, `"use cache"` results, cached
346
+ * responses and `ppr` shells. The request that invalidated the tag still
347
+ * re-renders its own entries without the read.
348
+ *
349
+ * Cost: one KV get per rejected copy per location after each invalidation
350
+ * (150 ms added wait, measured in #1069 locally at 50 ms per KV read on a
351
+ * document with a shell, a record and one `"use cache"` result, every copy
352
+ * rejected; not measured on a deployed worker), paid even when KV holds only
353
+ * the old twin. Worth it where re-rendering costs more than a KV read
354
+ * (several locations, expensive renders). Needs `kv`; ignored without it.
355
+ *
356
+ * Defaults to false: a rejected L1 copy is a miss and the location renders.
357
+ */
358
+ kvFallThrough?: boolean;
338
359
  /**
339
360
  * Per-isolate memos ({@link StoreMemoOptions}), one set per isolate and
340
361
  * shared by every CFCacheStore in it. `shellMs` (default 2000) and
@@ -2,4 +2,4 @@ import type { ClientUrlBuilder, ClientUrlPatterns } from "./types.js";
2
2
  import type { ExtractRoutes } from "../urls/type-extraction.js";
3
3
  import type { ClientUrlItems } from "./types.js";
4
4
  export declare function clientUrls<const TItems extends ClientUrlItems>(builder: ClientUrlBuilder<TItems>): ClientUrlPatterns<ExtractRoutes<TItems>>;
5
- export type { ClientRevalidateArgs, ClientRevalidateFn, ClientTransitionConfig, ClientUrlBuilder, ClientUrlHelpers, ClientUrlInterceptRecord, ClientUrlItem, ClientUrlItemInput, ClientUrlItems, ClientLayoutFn, ClientPathOptions, ClientUrlLoaderRecord, ClientPathFn, ClientUrlPatterns, ClientUrlRouteRecord, ClientUrlUse, } from "./types.js";
5
+ export type { ClientTransitionConfig, ClientUrlBuilder, ClientUrlHelpers, ClientUrlInterceptRecord, ClientUrlItem, ClientUrlItemInput, ClientUrlItems, ClientLayoutFn, ClientPathOptions, ClientUrlLoaderRecord, ClientPathFn, ClientUrlPatterns, ClientUrlRouteRecord, ClientUrlUse, } from "./types.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The value the build leaves in place of a clientUrls() `revalidate()`
3
+ * predicate in the browser and SSR copies of a "use client" module
4
+ * (vite/plugins/client-urls-revalidate-hoist.ts): the predicate itself moves
5
+ * to a server module, registered under the same content key
6
+ * (revalidate-registry.ts). The projection carries the key; the materialized
7
+ * loader stub calls the registered predicate.
8
+ */
9
+ export declare const HOISTED_REVALIDATE_KEY = "$$rangoRevalidate";
10
+ export interface HoistedRevalidateRef {
11
+ readonly $$rangoRevalidate: string;
12
+ }
13
+ export declare function isHoistedRevalidateRef(value: unknown): value is HoistedRevalidateRef;
@@ -46,41 +46,4 @@ export declare function registerClientUrlGroup(definition: ClientUrlPatterns, mo
46
46
  export declare function beginClientUrlNavigation(targetUrl: URL, signal: AbortSignal,
47
47
  /** Builds the entry state; called only when the group presents the target. */
48
48
  buildState?: () => unknown, decideTransition?: (destination: ClientUrlDestination) => boolean): ClientUrlNavigationPresentation | null;
49
- /**
50
- * Run the held clientUrls route's per-loader revalidate() predicates and
51
- * encode their decisions for the request header. Predicates are CLIENT code
52
- * (declared in the "use client" definition module); only decisions cross the
53
- * wire — the synthesized per-loader revalidate() on every materialized stub
54
- * (server-projection.ts) reads them back by loader $$id.
55
- *
56
- * Decisions exist only for the loaders the client currently HOLDS: the local
57
- * match of currentUrl. They matter when the server would re-evaluate those
58
- * held loader segments — same-route param/search navs, actions, and stale
59
- * restores. Cross-route targets render new segments unconditionally, so a
60
- * decision would be ignored; we still compute against the current route since
61
- * only its held segments are addressable.
62
- *
63
- * Returns null when no group is active, the current location is not a client
64
- * route, or no predicate produced a decision that differs from the locked
65
- * default — the server then applies defaults, which is also the behavior for
66
- * requests that cannot carry decisions (no-JS, PE, prefetch, document loads).
67
- */
68
- export declare function collectClientRevalidationDecisions(options: {
69
- currentUrl: URL;
70
- nextUrl: URL;
71
- /**
72
- * True only when the decisions ride the action POST itself — the one
73
- * request the server evaluates with actionContext (locked default true).
74
- * Action-triggered refetch GETs (partial-update terminals) pass false:
75
- * the server gives those navigation defaults, and the delta gate below
76
- * must diff against the default the SERVER will use, or a force decision
77
- * on the refetch would be silently swallowed as "equals default".
78
- */
79
- actionRequest: boolean;
80
- /** Action TRUTH for the predicates' isAction() matcher; may be true on
81
- * refetch GETs where actionRequest is false. */
82
- isAction: boolean;
83
- actionId?: string;
84
- stale: boolean;
85
- }): string | null;
86
49
  export declare function clearClientUrlNavigationRegistry(): void;
@@ -0,0 +1,4 @@
1
+ import type { ShouldRevalidateFn } from "../types.js";
2
+ export declare function registerClientUrlRevalidators(fns: Readonly<Record<string, ShouldRevalidateFn<any, any>>>): void;
3
+ export declare function getClientUrlRevalidator(key: string): ShouldRevalidateFn<any, any> | undefined;
4
+ export declare function clearClientUrlRevalidators(): void;