@rangojs/router 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/bin/rango.js +4 -3
  2. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  3. package/dist/types/browser/partial-update.d.ts +6 -4
  4. package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
  5. package/dist/types/browser/react/context.d.ts +3 -2
  6. package/dist/types/browser/types.d.ts +5 -1
  7. package/dist/types/build/route-types/scan-filter.d.ts +7 -1
  8. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  9. package/dist/types/cache/document-cache.d.ts +2 -10
  10. package/dist/types/context-var.d.ts +12 -0
  11. package/dist/types/errors.d.ts +10 -0
  12. package/dist/types/handles/pushed-by.d.ts +8 -0
  13. package/dist/types/index.d.ts +4 -0
  14. package/dist/types/index.rsc.d.ts +2 -0
  15. package/dist/types/response-utils.d.ts +7 -0
  16. package/dist/types/reverse.d.ts +8 -8
  17. package/dist/types/route-definition/rewrite.d.ts +56 -0
  18. package/dist/types/route-map-builder.d.ts +7 -2
  19. package/dist/types/router/basename.d.ts +7 -0
  20. package/dist/types/router/handler-context.d.ts +1 -5
  21. package/dist/types/router/internal-params.d.ts +8 -0
  22. package/dist/types/router/middleware-types.d.ts +15 -0
  23. package/dist/types/router/revalidation.d.ts +1 -2
  24. package/dist/types/router/route-trie-builder.d.ts +10 -0
  25. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  26. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
  27. package/dist/types/rsc/full-payload.d.ts +11 -0
  28. package/dist/types/rsc/handler.d.ts +33 -0
  29. package/dist/types/rsc/helpers.d.ts +8 -0
  30. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  31. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  32. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  33. package/dist/types/server/request-context.d.ts +39 -2
  34. package/dist/types/server/rewrite-trail.d.ts +49 -0
  35. package/dist/types/server/slot-accessors.d.ts +29 -0
  36. package/dist/types/testing/index.d.ts +1 -1
  37. package/dist/types/testing/render-route.d.ts +8 -0
  38. package/dist/types/testing/run-middleware.d.ts +20 -0
  39. package/dist/types/testing/serve-shell-request.d.ts +2 -1
  40. package/dist/types/thenable-status.d.ts +6 -0
  41. package/dist/types/types/request-scope.d.ts +7 -0
  42. package/dist/types/vite/discovery/state.d.ts +12 -9
  43. package/dist/vite/index.js +164 -103
  44. package/package.json +1 -1
  45. package/skills/caching/SKILL.md +12 -0
  46. package/skills/catalog.json +1 -1
  47. package/skills/client-urls/SKILL.md +84 -0
  48. package/skills/hooks/SKILL.md +11 -6
  49. package/skills/hooks/data.md +3 -1
  50. package/skills/loader/SKILL.md +7 -1
  51. package/skills/middleware/SKILL.md +183 -2
  52. package/skills/ppr/SKILL.md +9 -0
  53. package/skills/rango/SKILL.md +1 -1
  54. package/skills/server-actions/SKILL.md +27 -0
  55. package/skills/testing/client-components.md +1 -0
  56. package/skills/testing/middleware.md +31 -10
  57. package/skills/testing/response-routes.md +1 -0
  58. package/skills/testing/serve-shell-request.md +19 -1
  59. package/skills/use-cache/SKILL.md +4 -2
  60. package/src/__augment-tests__/augmented.check.ts +20 -0
  61. package/src/browser/event-controller.ts +3 -0
  62. package/src/browser/merge-segment-loaders.ts +31 -18
  63. package/src/browser/navigation-bridge.ts +19 -3
  64. package/src/browser/partial-update.ts +27 -12
  65. package/src/browser/react/Link.tsx +7 -9
  66. package/src/browser/react/NavigationProvider.tsx +46 -26
  67. package/src/browser/react/context.ts +3 -2
  68. package/src/browser/react/use-router.ts +2 -4
  69. package/src/browser/server-action-bridge.ts +6 -0
  70. package/src/browser/types.ts +5 -1
  71. package/src/build/route-types/scan-filter.ts +11 -4
  72. package/src/cache/document-cache-policy.ts +131 -0
  73. package/src/cache/document-cache.ts +12 -119
  74. package/src/context-var.ts +32 -2
  75. package/src/errors.ts +19 -0
  76. package/src/handles/pushed-by.ts +26 -0
  77. package/src/index.rsc.ts +8 -0
  78. package/src/index.ts +7 -0
  79. package/src/response-utils.ts +19 -0
  80. package/src/reverse.ts +17 -14
  81. package/src/route-definition/redirect.ts +2 -15
  82. package/src/route-definition/rewrite.ts +146 -0
  83. package/src/route-map-builder.ts +14 -11
  84. package/src/router/basename.ts +20 -0
  85. package/src/router/handler-context.ts +3 -13
  86. package/src/router/intercept-resolution.ts +29 -12
  87. package/src/router/internal-params.ts +16 -0
  88. package/src/router/loader-resolution.ts +2 -0
  89. package/src/router/match-middleware/background-revalidation.ts +11 -9
  90. package/src/router/match-middleware/cache-lookup.ts +20 -15
  91. package/src/router/match-middleware/cache-store.ts +9 -11
  92. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  93. package/src/router/middleware-types.ts +16 -0
  94. package/src/router/middleware.ts +127 -49
  95. package/src/router/revalidation.ts +1 -1
  96. package/src/router/route-trie-builder.ts +28 -1
  97. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  98. package/src/router/segment-resolution/mask-nested.ts +8 -4
  99. package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
  100. package/src/router/segment-resolution/revalidation.ts +57 -33
  101. package/src/rsc/full-payload.ts +37 -5
  102. package/src/rsc/handler.ts +82 -55
  103. package/src/rsc/helpers.ts +24 -0
  104. package/src/rsc/loader-fetch.ts +79 -24
  105. package/src/rsc/response-cache-serve.ts +18 -2
  106. package/src/rsc/response-route-handler.ts +4 -7
  107. package/src/rsc/rewrite-passes.ts +203 -0
  108. package/src/rsc/rsc-rendering.ts +4 -2
  109. package/src/rsc/ssr-setup.ts +11 -5
  110. package/src/server/cookie-parse.ts +36 -12
  111. package/src/server/cookie-store.ts +7 -3
  112. package/src/server/handle-store.ts +48 -17
  113. package/src/server/request-context.ts +154 -63
  114. package/src/server/request-headers.ts +4 -14
  115. package/src/server/rewrite-trail.ts +190 -0
  116. package/src/server/slot-accessors.ts +57 -0
  117. package/src/ssr/index.tsx +118 -16
  118. package/src/testing/dispatch.ts +138 -67
  119. package/src/testing/index.ts +1 -0
  120. package/src/testing/render-route.tsx +16 -3
  121. package/src/testing/run-loader.ts +2 -0
  122. package/src/testing/run-middleware.ts +56 -11
  123. package/src/testing/serve-shell-request.ts +15 -2
  124. package/src/thenable-status.ts +11 -0
  125. package/src/types/request-scope.ts +8 -0
  126. package/src/vite/discovery/state.ts +21 -13
  127. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  128. package/src/vite/router-discovery.ts +29 -27
package/dist/bin/rango.js CHANGED
@@ -216,6 +216,9 @@ import picomatch from "picomatch";
216
216
  function isUniversallySkippedScanDir(name) {
217
217
  return name === "node_modules" || name === "dist" || name === "coverage" || name.startsWith(".");
218
218
  }
219
+ function isSkippedSourceScanDir(name) {
220
+ return isUniversallySkippedScanDir(name) || name === "build";
221
+ }
219
222
  function findTsFiles(dir, filter) {
220
223
  const results = [];
221
224
  let entries;
@@ -230,9 +233,7 @@ function findTsFiles(dir, filter) {
230
233
  for (const entry of entries) {
231
234
  const fullPath = join(dir, entry.name);
232
235
  if (entry.isDirectory()) {
233
- if (isUniversallySkippedScanDir(entry.name) || entry.name === "build") {
234
- continue;
235
- }
236
+ if (isSkippedSourceScanDir(entry.name)) continue;
236
237
  results.push(...findTsFiles(fullPath, filter));
237
238
  } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
238
239
  if (filter && !filter(fullPath)) continue;
@@ -3,8 +3,11 @@ import type { ResolvedSegment } from "./types.js";
3
3
  * Merge partial loader data from server with cached loader data.
4
4
  *
5
5
  * During partial revalidation (stale or action), the server may return only
6
- * some loaders that pass the revalidation check. The component still needs
7
- * all loader data, so we merge fresh data with cached data.
6
+ * some loaders that pass the revalidation check, none when every one is held.
7
+ * The component still needs all loader data, so we merge fresh data with
8
+ * cached data. Two settled aggregates merge into the array itself: a fresh
9
+ * Promise.all over settled parts is a promise React has not read, and content
10
+ * on screen handed one suspends (docs/internal/suspense-contract.md, rule 2).
8
11
  *
9
12
  * @param fromServer - Segment returned from server with partial loaders
10
13
  * @param fromCache - Cached segment with full loader data
@@ -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, RscMetadata } from "./types.js";
5
+ import type { HandleData, 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
  /**
@@ -83,9 +83,11 @@ export type UpdateMode = {
83
83
  * when we send cached segment IDs to the server - if the server returns empty diff,
84
84
  * we use the same segments we told the server we have. */
85
85
  targetCacheSegments?: ResolvedSegment[];
86
- /** Cached handle data for the target URL. When server returns empty diff and we're
87
- * rendering from cache, this is passed to the UI to restore breadcrumbs etc. */
88
- targetCacheHandleData?: Record<string, Record<string, unknown[]>>;
86
+ /** The handle state of `targetCacheSegments`: the entry's pushes, and
87
+ * the live pushes of the page on screen for each segment taken from it.
88
+ * Read at commit; the page starts from it and the response's handles,
89
+ * when it has any, merge onto it (NavigationProvider). */
90
+ targetCacheHandleData?: () => HandleData;
89
91
  /** Source URL for intercept restore (popstate cache miss) */
90
92
  interceptSourceUrl?: string;
91
93
  /**
@@ -74,13 +74,17 @@ export interface NavigationProviderProps {
74
74
  * The document this provider hydrates (initBrowserApp; renderRoute with
75
75
  * `hydrate: true`), whose store EventController.lockHydration locked.
76
76
  * `settled` resolves once the stream has ended and React has revealed every
77
- * boundary. Until then location state is `undefined`, as SSR rendered it;
78
- * then one transition gives the entry's state and changes
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.
77
+ * boundary. Then a transition changes HydrationBarrierContext alone, and
78
+ * React commits it once every boundary has hydrated, one still suspended on
79
+ * the client (a lazy chunk, a deferred-hydration gate) included. Until that
80
+ * commit location state is `undefined`, as SSR rendered it, through any
81
+ * commit no payload follows (a shallow navigation), and the store is
82
+ * locked; after it the entry's state lands in a transition of its own
83
+ * and the store is released (handle data lands in that release
84
+ * notification). A navigation committed earlier releases the store before
85
+ * its update, for the whole hold when the navigation is held: an old-page
86
+ * boundary hydrating meanwhile reads the destination's values and can
87
+ * mismatch.
84
88
  * Absent for a provider mounted on the client.
85
89
  */
86
90
  hydration?: {
@@ -76,7 +76,8 @@ export declare const LocationStateContext: Context<LocationStateSnapshot>;
76
76
  * Read by no component. NavigationProvider changes its value in a transition so
77
77
  * that React hydrates every boundary still dehydrated beneath it before that
78
78
  * commit: React hydrates a dehydrated boundary first when a provider above it
79
- * changes, and nothing re-renders for a value nobody reads. See
80
- * NavigationProviderProps.hydration.
79
+ * changes, and nothing re-renders for a value nobody reads. It must stay
80
+ * unread and alone in its lane (docs/internal/execution-model.md, "The reveal
81
+ * barrier is alone in its lane"). See NavigationProviderProps.hydration.
81
82
  */
82
83
  export declare const HydrationBarrierContext: Context<number>;
@@ -80,7 +80,11 @@ export interface RscMetadata {
80
80
  * positions and must complete at the handler barrier.
81
81
  */
82
82
  handlesLate?: AsyncGenerator<HandleData, void, unknown>;
83
- /** Cached handle data (for back/forward navigation from cache) */
83
+ /**
84
+ * Handle data of the history-cache entry the update renders from (a
85
+ * back/forward restore, or a link to a cached page). It replaces the handle
86
+ * state, and `handles`, when present, merge onto it.
87
+ */
84
88
  cachedHandleData?: HandleData;
85
89
  /**
86
90
  * RSC version string from the server.
@@ -24,8 +24,14 @@ export declare function isUniversallySkippedScanDir(name: string): boolean;
24
24
  * files discovery would never find. A path outside `root` is false.
25
25
  */
26
26
  export declare function isInSkippedScanDir(root: string, absolutePath: string): boolean;
27
+ /**
28
+ * Directory names the source-file scans skip: findTsFiles() and the build
29
+ * loader pre-scan (expose-internal-ids.ts). The isUniversallySkippedScanDir()
30
+ * set plus `build`, which the router scans still enter.
31
+ */
32
+ export declare function isSkippedSourceScanDir(name: string): boolean;
27
33
  /**
28
34
  * Recursively find .ts/.tsx files under a directory, skipping the directories
29
- * isUniversallySkippedScanDir() names, `build`, and .gen. files.
35
+ * isSkippedSourceScanDir() names and .gen. files.
30
36
  */
31
37
  export declare function findTsFiles(dir: string, filter?: ScanFilter): string[];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Document cache storage policy: which responses the document cache stores.
3
+ * A leaf (types and the per-client signal only), so request-scoped code can
4
+ * ask whether a render will be stored (documentCacheStoresRender) without
5
+ * importing the cache middleware.
6
+ */
7
+ import type { RequestContext } from "../server/request-context.js";
8
+ export interface CacheDirectives {
9
+ sMaxAge?: number;
10
+ staleWhileRevalidate?: number;
11
+ }
12
+ /**
13
+ * Check if response should be cached based on Cache-Control headers
14
+ */
15
+ export declare function shouldCacheResponse(response: Response, requestCtx?: Pick<RequestContext, "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation">): CacheDirectives | null;
16
+ /**
17
+ * Whether this cache will store the response the current render produces, as
18
+ * far as the render can tell: the middleware runs it (a MISS or a stale
19
+ * refresh) and the response stub already opts in. payloadInitialTheme
20
+ * (rsc/full-payload.ts) then renders the no-cookie default theme (#978). A
21
+ * Cache-Control written after next() is not on the stub yet; for that render
22
+ * shouldCacheResponse refuses a payload marked `_payloadVisitorTheme`.
23
+ */
24
+ export declare function documentCacheStoresRender(reqCtx: Pick<RequestContext, "_documentCacheRender" | "res">): boolean;
@@ -13,16 +13,8 @@
13
13
  * 4. If miss → run handler, cache if response has cache headers
14
14
  */
15
15
  import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
16
- import { type RequestContext } from "../server/request-context.js";
17
- /**
18
- * Whether this cache will store the response the current render produces, as
19
- * far as the render can tell: the middleware runs it (a MISS or a stale
20
- * refresh) and the response stub already opts in. payloadInitialTheme
21
- * (rsc/full-payload.ts) then renders the no-cookie default theme (#978). A
22
- * Cache-Control written after next() is not on the stub yet; for that render
23
- * shouldCacheResponse refuses a payload marked `_payloadVisitorTheme`.
24
- */
25
- export declare function documentCacheStoresRender(reqCtx: Pick<RequestContext, "_documentCacheRender" | "res">): boolean;
16
+ import { documentCacheStoresRender } from "./document-cache-policy.js";
17
+ export { documentCacheStoresRender };
26
18
  export interface DocumentCacheOptions<TEnv = any> {
27
19
  /**
28
20
  * Skip caching for specific paths (e.g., API routes)
@@ -68,6 +68,18 @@ export declare function isNonCacheable(variables: any, keyOrVar: string | Contex
68
68
  * Accepts either a string key (legacy) or a ContextVar token (typed).
69
69
  */
70
70
  export declare function contextGet(variables: any, keyOrVar: string | ContextVar<any>): any;
71
+ /**
72
+ * Keys that must never be used as string variable names, nor as a fetchable
73
+ * loader's request params (rsc/loader-fetch.ts).
74
+ */
75
+ export declare const FORBIDDEN_KEYS: ReadonlySet<string>;
76
+ /**
77
+ * `target[key] = value` as an own data property, `__proto__` included: that
78
+ * assignment would call Object.prototype's setter and replace the target's
79
+ * prototype, which Flight refuses to serialize (#1109). Loader data parsed
80
+ * from JSON, cookie names and the like can carry the key.
81
+ */
82
+ export declare function assignOwn(target: Record<string, unknown>, key: string, value: unknown): void;
71
83
  export interface ContextSetOptions {
72
84
  /**
73
85
  * When false, marks this specific write as non-cacheable.
@@ -176,6 +176,16 @@ export declare class Skip extends Error {
176
176
  * Type guard to check if a thrown value is a Skip signal.
177
177
  */
178
178
  export declare function isSkip(value: unknown): value is Skip;
179
+ /**
180
+ * A request middleware rewrote (`throw rewrite()`) more than `maxHops` times,
181
+ * or to the URL it was on. Reported through `onError` (phase "routing") and
182
+ * answered with 508. `trail` lists the URLs, the visitor's first.
183
+ */
184
+ export declare class RewriteLoopError extends Error {
185
+ name: "RewriteLoopError";
186
+ readonly trail: readonly string[];
187
+ constructor(trail: readonly string[], maxHops: number);
188
+ }
179
189
  /**
180
190
  * Thrown by the partial updater when the server responds with a redirect payload
181
191
  * that carries location state. Caught by navigate() to re-navigate to the
@@ -5,6 +5,14 @@ export declare function pushedByKey(handleName: string): string;
5
5
  export declare function isPushedByKey(name: string): boolean;
6
6
  /** `data` without its pushedBy records, as a reader outside the router sees it. */
7
7
  export declare function withoutPushedBy(data: HandleData): HandleData;
8
+ /**
9
+ * `target` with the buckets of the segments in `ids` taken from `source`.
10
+ * A pushedBy record is a snapshot key like a handle's, so a bucket's values
11
+ * and the record of the runs that pushed them always come from the same
12
+ * snapshot. New containers; the value arrays are shared, as cloneHandleData
13
+ * (navigation-store.ts) shares them.
14
+ */
15
+ export declare function withSegmentHandles(target: HandleData, source: HandleData, ids: ReadonlySet<string>): HandleData;
8
16
  /**
9
17
  * The handle data after a partial response's snapshot `incoming` (each stream
10
18
  * yield holds everything the response pushed so far). A segment outside
@@ -32,6 +32,10 @@ export declare function createRouter(): never;
32
32
  * Error-throwing stub for server-only `redirect` function.
33
33
  */
34
34
  export declare function redirect(): never;
35
+ /**
36
+ * Error-throwing stub for server-only `rewrite` function.
37
+ */
38
+ export declare function rewrite(): never;
35
39
  export { createHandle, isHandle, type Handle } from "./handle.js";
36
40
  export { DEFAULT_DEFER_TIMEOUT_MS, type DeferOptions, type HandlePush, type HandlePushFn, } from "./defer.js";
37
41
  export { createVar, type ContextVar } from "./context-var.js";
@@ -14,6 +14,8 @@ export type { RangoOptions, SSRStreamMode, SSROptions, ResolveStreamingContext,
14
14
  export type { OriginCheckConfig, OriginCheckContext, OriginCheckPhase, } from "./rsc/origin-guard.js";
15
15
  export type { ShellCaptureDebug, ShellCaptureDebugEvent, } from "./rsc/shell-capture.js";
16
16
  export { createLoader, redirect, type RouteHelpers, type RouteHandlers, layout, cache, middleware, revalidate, loader, loading, parallel, intercept, errorBoundary, notFoundBoundary, transition, } from "./route-definition.js";
17
+ export { rewrite, type Rewrite, type RewriteFunction, } from "./route-definition/rewrite.js";
18
+ export { RewriteLoopError } from "./errors.js";
17
19
  export type { RouteUseItem, LayoutUseItem, AllUseItems, UseItems, HandlerUseItem, } from "./route-types.js";
18
20
  export { createHandle, isHandle, type Handle } from "./handle.js";
19
21
  export { DEFAULT_DEFER_TIMEOUT_MS, type DeferOptions, type HandlePush, type HandlePushFn, } from "./defer.js";
@@ -33,3 +33,10 @@ export declare function isWebSocketUpgradeResponse(response: Response): boolean;
33
33
  */
34
34
  export declare function appendVaryAccept(response: Response): void;
35
35
  export declare function isRedirectResponse(response: Response): boolean;
36
+ /**
37
+ * Set a header on a response the router hands to the host, skipping a
38
+ * WebSocket upgrade (its headers are not to be touched) and swallowing the
39
+ * TypeError of immutable headers (a passed-through platform Response):
40
+ * instrumentation and debug headers never fail a request.
41
+ */
42
+ export declare function trySetHeader(response: Response, name: string, value: string): void;
@@ -93,31 +93,31 @@ export type ReverseFunction<TRoutes> = {
93
93
  * reverse("typo") // ✗ Compile error (not in global routes)
94
94
  * ```
95
95
  */
96
- export type ScopedReverseFunction<TLocalRoutes, TGlobalRoutes = TLocalRoutes> = {
96
+ export type ScopedReverseFunction<TLocalRoutes, TGlobalRoutes = TLocalRoutes, TResult = string> = {
97
97
  /**
98
98
  * Global route without params
99
99
  */
100
- <TName extends keyof TGlobalRoutes & string>(name: IsEmptyObject<ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>> extends true ? TName : never): string;
100
+ <TName extends keyof TGlobalRoutes & string>(name: IsEmptyObject<ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>> extends true ? TName : never): TResult;
101
101
  /**
102
102
  * Global route with params
103
103
  */
104
- <TName extends keyof TGlobalRoutes & string>(name: TName, params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>): string;
104
+ <TName extends keyof TGlobalRoutes & string>(name: TName, params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>): TResult;
105
105
  /**
106
106
  * Global route with params and search
107
107
  */
108
- <TName extends keyof TGlobalRoutes & string>(name: TName, params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>, search: ResolveSearchSchema<ExtractSearchSchema<TGlobalRoutes, TName>>): string;
108
+ <TName extends keyof TGlobalRoutes & string>(name: TName, params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>, search: ResolveSearchSchema<ExtractSearchSchema<TGlobalRoutes, TName>>): TResult;
109
109
  /**
110
110
  * Dot-prefixed local route without params
111
111
  */
112
- <TName extends keyof TLocalRoutes & string>(name: IsEmptyObject<ExtractParams<RoutePatternFor<TLocalRoutes, TName>>> extends true ? `.${TName}` : never): string;
112
+ <TName extends keyof TLocalRoutes & string>(name: IsEmptyObject<ExtractParams<RoutePatternFor<TLocalRoutes, TName>>> extends true ? `.${TName}` : never): TResult;
113
113
  /**
114
114
  * Dot-prefixed local route with params
115
115
  */
116
- <TName extends keyof TLocalRoutes & string>(name: `.${TName}`, params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>): string;
116
+ <TName extends keyof TLocalRoutes & string>(name: `.${TName}`, params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>): TResult;
117
117
  /**
118
118
  * Dot-prefixed local route with params and search
119
119
  */
120
- <TName extends keyof TLocalRoutes & string>(name: `.${TName}`, params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>, search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>): string;
120
+ <TName extends keyof TLocalRoutes & string>(name: `.${TName}`, params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>, search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>): TResult;
121
121
  };
122
122
  /**
123
123
  * Reverse function that resolves global route names only: no include() scope,
@@ -147,7 +147,7 @@ export type ScopedReverseFunction<TLocalRoutes, TGlobalRoutes = TLocalRoutes> =
147
147
  * reverse(".post", { slug: "hello" }) // compile error: no include() scope
148
148
  * ```
149
149
  */
150
- export type GlobalReverseFunction<TGlobalRoutes> = string extends keyof TGlobalRoutes ? <TName extends string>(name: Exclude<TName, `.${string}`>, params?: Record<string, string>, search?: Record<string, unknown>) => string : ScopedReverseFunction<{}, TGlobalRoutes>;
150
+ export type GlobalReverseFunction<TGlobalRoutes, TResult = string> = string extends keyof TGlobalRoutes ? <TName extends string>(name: Exclude<TName, `.${string}`>, params?: Record<string, string>, search?: Record<string, unknown>) => TResult : ScopedReverseFunction<{}, TGlobalRoutes, TResult>;
151
151
  /**
152
152
  * Extract local routes type from UrlPatterns
153
153
  * Used with scopedReverse() to get the routes type from patterns
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `throw rewrite(target)`: middleware renders another route at the same URL.
3
+ *
4
+ * The value only: what middleware throws and how the router recognizes it.
5
+ * The pass loop that honors it lives in rsc/rewrite-passes.ts, the hop trail
6
+ * in server/rewrite-trail.ts.
7
+ */
8
+ import type { GlobalReverseFunction } from "../reverse.js";
9
+ import type { DefaultReverseRouteMap } from "../types/global-namespace.js";
10
+ /** The target as written: a path, or a global route name with its params. */
11
+ export type RewriteTarget = {
12
+ readonly path: string;
13
+ } | {
14
+ readonly name: string;
15
+ readonly params?: Record<string, string>;
16
+ readonly search?: Record<string, unknown>;
17
+ };
18
+ export declare function describeRewriteTarget(target: RewriteTarget): string;
19
+ /**
20
+ * The value `rewrite()` returns, thrown by middleware. An Error, so a throw
21
+ * where a rewrite is not supported fails loudly with the rule in its message.
22
+ */
23
+ export declare class Rewrite extends Error {
24
+ name: "Rewrite";
25
+ readonly target: RewriteTarget;
26
+ constructor(target: RewriteTarget);
27
+ }
28
+ /** Whether `value` is a Rewrite thrown by `rewrite()` (ctx.isRewrite). */
29
+ export declare function isRewrite(value: unknown): value is Rewrite;
30
+ /**
31
+ * `rewrite(path)` or `rewrite(name, params?, search?)`: a path in this app
32
+ * (basename-prefixed like `redirect()`, may carry `?search`), or a global
33
+ * route name with the params and search `ctx.reverse` takes.
34
+ */
35
+ export type RewriteFunction<TRoutes = DefaultReverseRouteMap> = ((path: `/${string}`) => Rewrite) & GlobalReverseFunction<TRoutes, Rewrite>;
36
+ /**
37
+ * Render another route at the same URL. Throw it from middleware, before
38
+ * `await next()`:
39
+ *
40
+ * ```ts
41
+ * router.use(async (ctx, next) => {
42
+ * if (!locked || ctx.pathname === "/maintenance") return next();
43
+ * throw rewrite("maintenance");
44
+ * });
45
+ * ```
46
+ *
47
+ * The request runs again as the target (global middleware, route
48
+ * middleware, render) with the same request context: values set before the
49
+ * throw stay readable. `ctx.url` is the target; `ctx.rewrittenFrom` is the
50
+ * visitor's URL. See the `/middleware` skill.
51
+ */
52
+ export declare const rewrite: RewriteFunction;
53
+ /** A dispatch site that cannot honor a rewrite. */
54
+ export type RewriteSite = "mutation" | "loader-fetch" | "intercept";
55
+ /** The error a rewrite becomes at a site that cannot honor it. */
56
+ export declare function rewriteNotSupported(rewrite: Rewrite, site: RewriteSite): Error;
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * See docs/manifests.md for the full data flow.
9
9
  */
10
+ import { type TrieNode } from "./router/route-trie-builder.js";
10
11
  /**
11
12
  * Register routes into the global route map.
12
13
  * Routes are merged with any existing registered routes.
@@ -71,10 +72,14 @@ export declare function clearAllRouterData(): void;
71
72
  export declare function setRouterManifest(routerId: string, manifest: Record<string, string>): void;
72
73
  /** @internal */
73
74
  export declare function getRouterManifest(routerId: string): Record<string, string> | undefined;
74
- export declare function setRouterTrie(routerId: string, trie: import("./build/route-trie.js").TrieNode): void;
75
+ /**
76
+ * Install a router's trie. A trie that arrives as JSON (the build manifest)
77
+ * or across the dev runner boundary gets null-prototype static maps first.
78
+ */
79
+ export declare function setRouterTrie(routerId: string, trie: TrieNode): void;
75
80
  export declare function markRouterTrieAuthoritative(routerId: string): void;
76
81
  export declare function isRouterTrieAuthoritative(routerId: string): boolean;
77
- export declare function getRouterTrie(routerId: string): import("./build/route-trie.js").TrieNode | undefined;
82
+ export declare function getRouterTrie(routerId: string): TrieNode | undefined;
78
83
  export declare function setRouterPrecomputedEntries(routerId: string, entries: Array<{
79
84
  staticPrefix: string;
80
85
  routes: Record<string, string>;
@@ -8,3 +8,10 @@
8
8
  * createRouter() accepts and observe the same redirect() prefixing).
9
9
  */
10
10
  export declare function normalizeBasename(basename?: string): string | undefined;
11
+ /**
12
+ * Prefix a root-relative path with the basename unless it already carries it
13
+ * (the basename followed by "/", "?", "#" or the end), so "/admin?tab=x" and
14
+ * "/admin#frag" under basename "/admin" are not prefixed twice. Shared by
15
+ * redirect(), rewrite() and useRouter().
16
+ */
17
+ export declare function prefixBasename(path: string, basename?: string): string;
@@ -10,11 +10,7 @@ import type { InternalHandlerContext } from "../types";
10
10
  * (createMiddlewareContext).
11
11
  */
12
12
  export declare function guardHeaderWrites(headers: Headers, assertWrite: (method: string | symbol) => void): Headers;
13
- /**
14
- * Strip internal _rsc* query params from a URL.
15
- * Returns a new URL with only user-facing params.
16
- */
17
- export declare function stripInternalParams(url: URL): URL;
13
+ export { stripInternalParams } from "./internal-params.js";
18
14
  /**
19
15
  * Create a reverse function for URL generation from route names.
20
16
  * Used by both HandlerContext and MiddlewareContext.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Strip internal _rsc* query params from a URL.
3
+ * Returns a new URL with only user-facing params.
4
+ *
5
+ * A leaf, so request-scoped modules (server/rewrite-trail.ts) can strip
6
+ * without importing the handler context's graph.
7
+ */
8
+ export declare function stripInternalParams(url: URL): URL;
@@ -4,6 +4,7 @@ import type { GlobalReverseFunction } from "../reverse.js";
4
4
  import type { Theme } from "../theme/types.js";
5
5
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
6
6
  import type { RequestScope } from "../types/request-scope.js";
7
+ import type { Rewrite } from "../route-definition/rewrite.js";
7
8
  type GetVariableFn = {
8
9
  <T>(contextVar: ContextVar<T>): T | undefined;
9
10
  <K extends keyof DefaultVars>(key: K): DefaultVars[K];
@@ -58,6 +59,20 @@ export interface MiddlewareContext<TEnv = any, TParams = Record<string, string |
58
59
  setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
59
60
  /** Global route names only; see {@link GlobalReverseFunction}. */
60
61
  reverse: GlobalReverseFunction<DefaultReverseRouteMap>;
62
+ /**
63
+ * Whether `value` is a `rewrite()` thrown downstream. A middleware that
64
+ * wraps `next()` in try/catch rethrows it, so the request re-dispatches:
65
+ *
66
+ * ```ts
67
+ * try {
68
+ * return await next();
69
+ * } catch (error) {
70
+ * if (ctx.isRewrite(error)) throw error;
71
+ * return new Response("Error", { status: 500 });
72
+ * }
73
+ * ```
74
+ */
75
+ isRewrite(value: unknown): value is Rewrite;
61
76
  }
62
77
  export type MiddlewareFn<TEnv = any, TParams = Record<string, string | undefined>> = (ctx: MiddlewareContext<TEnv, TParams>, next: () => Promise<Response>) => Response | void | Promise<Response | void>;
63
78
  export interface MiddlewareEntry<TEnv = any> {
@@ -9,7 +9,7 @@ import type { RevalidationTraceEntry } from "./logging.js";
9
9
  /**
10
10
  * Options for revalidation evaluation
11
11
  */
12
- interface EvaluateRevalidationOptions<TEnv> {
12
+ export interface EvaluateRevalidationOptions<TEnv> {
13
13
  /** Current segment to evaluate */
14
14
  segment: ResolvedSegment;
15
15
  /** Previous route params (from route match, not segment) */
@@ -61,4 +61,3 @@ interface EvaluateRevalidationOptions<TEnv> {
61
61
  * Optimized to use prevParams directly and avoid building previous segments
62
62
  */
63
63
  export declare function evaluateRevalidation<TEnv>(options: EvaluateRevalidationOptions<TEnv>): Promise<boolean>;
64
- export {};
@@ -82,3 +82,13 @@ export interface TrieNode {
82
82
  * @param onDemandRouteNames - Optional set of on-demand-eligible route names (sets leaf.od)
83
83
  */
84
84
  export declare function buildRouteTrie(routeManifest: Record<string, string>, routeToStaticPrefix: Record<string, string>, routeTrailingSlash?: Record<string, string>, prerenderRouteNames?: Set<string>, passthroughRouteNames?: Set<string>, responseTypeRoutes?: Record<string, string>, onDemandRouteNames?: Set<string>): TrieNode;
85
+ /**
86
+ * Give every static-children map in the trie a null prototype, in place, and
87
+ * return the trie (#1109). The walker indexes `node.s` with raw URL segments,
88
+ * so a map with Object.prototype would answer `/constructor` or `/__proto__`
89
+ * with an inherited member. The builder creates null-prototype maps; a trie
90
+ * that arrives as JSON (the build manifest) or across the dev runner boundary
91
+ * gets them here. Object.setPrototypeOf keeps a JSON-parsed map's fast
92
+ * properties, where Object.create(null) starts in dictionary mode.
93
+ */
94
+ export declare function nullPrototypeStaticChildren(node: TrieNode): TrieNode;
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Deliberately a LEAF module: loader-mask (the other natural home) imports
10
10
  * request-context, so any funnel importing the mask from there inherits that
11
- * graph. This module imports only is-thenable and react.
11
+ * graph. This module imports only react and two leaves, is-thenable and
12
+ * context-var.
12
13
  */
13
14
  import { type ReactElement } from "react";
14
15
  /**
@@ -36,7 +37,8 @@ export declare function isPlainDataObject(value: unknown): value is Record<strin
36
37
  * keyless child of a static children list (validated at creation) would warn
37
38
  * "Each child in a list should have a unique key" once cloned. Pass only the
38
39
  * props that change: cloneElement re-parents the element (owner) when its
39
- * config carries `ref`.
40
+ * config carries `ref`. cloneElement copies props by assignment, so a prop
41
+ * key named `__proto__` can't survive a clone; prop copies don't use assignOwn.
40
42
  */
41
43
  export declare function cloneElementWithProps(element: ReactElement, props: Record<string, unknown>): ReactElement;
42
44
  /**
@@ -80,11 +80,8 @@ export interface PrefetchDeferral {
80
80
  * not stored and not held. Empty outside a prefetch.
81
81
  */
82
82
  readonly skipped: ReadonlySet<string>;
83
- /**
84
- * One of `skipped` is a chain entry: nothing below it runs, so the response
85
- * has no content for it, a route cache() record included.
86
- */
87
- readonly skipsChain: boolean;
83
+ /** How this request may use the route's cache() record (recordUse). */
84
+ readonly record: RecordUse;
88
85
  /** Segment id of the handler unit this prefetch skipped. */
89
86
  deferredUnit?: string;
90
87
  /**
@@ -105,6 +102,30 @@ export interface PrefetchDeferral {
105
102
  */
106
103
  deferredLoaders?: Map<string, DeferredLoader>;
107
104
  }
105
+ /**
106
+ * How a request may use the route's cache() record, decided once per match
107
+ * (planPrefetchDeferral) for the cache middleware.
108
+ */
109
+ export interface RecordUse {
110
+ /**
111
+ * withCacheLookup reads it. False when this prefetch skips a chain unit
112
+ * (PrefetchDeferral.skipped): the unit takes everything below it, so the
113
+ * read would replay the record's handle pushes for segments the response
114
+ * does not carry. Decided from what the client holds, never per tree: a
115
+ * prefetch that holds the flagged layout reads and writes the record like
116
+ * any other request.
117
+ */
118
+ readonly read: boolean;
119
+ /** withCacheStore writes it. False where `read` is: the record would be incomplete. */
120
+ readonly write: boolean;
121
+ /**
122
+ * A background re-render of the whole route may write it: the stale-hit
123
+ * refresh (withBackgroundRevalidation) and the proactive write a partial
124
+ * response needs (withCacheStore). False for a fill: the re-render runs
125
+ * the handlers of segments the client holds, which a fill does not run.
126
+ */
127
+ readonly refresh: boolean;
128
+ }
108
129
  /** A loader this prefetch deferred: PrefetchDeferral.deferredLoaders. */
109
130
  export interface DeferredLoader {
110
131
  /**
@@ -157,6 +178,16 @@ export declare function planPrefetchDeferral(entries: readonly EntryData[], mode
157
178
  export declare function getPrefetchDeferral(ctx: HandlerContext<any, any> | undefined): PrefetchDeferral | undefined;
158
179
  /** The request is a fill: a held segment is skipped without consulting anything. */
159
180
  export declare function isFillRequest(ctx: HandlerContext<any, any>): boolean;
181
+ /**
182
+ * A fill's whole revalidation rule (R5): send a segment the client does not
183
+ * hold, keep one it holds, and consult nothing else (no revalidate()
184
+ * predicate, no params or search comparison, no trace entry). Undefined on
185
+ * any other request, where revalidation decides. `held` is the set the
186
+ * deciding site resolves with.
187
+ */
188
+ export declare function fillSends(ctx: HandlerContext<any, any>, segmentId: string, held: ReadonlySet<string>): boolean | undefined;
189
+ /** PrefetchDeferral.record, or all of it open when the match has no plan. */
190
+ export declare function recordUse(ctx: HandlerContext<any, any> | undefined): RecordUse;
160
191
  /**
161
192
  * Register a loader this prefetch deferred, with the segment that registers
162
193
  * it and how to start it there (PrefetchDeferral.deferredLoaders). The first
@@ -209,14 +240,6 @@ export declare function belowDeferredUnit(ctx: HandlerContext<any, any>, segment
209
240
  * ctx.rendered() guards (loader-resolution.ts).
210
241
  */
211
242
  export declare function firstUnitCandidate(plan: PrefetchDeferral): EntryData | undefined;
212
- /**
213
- * This prefetch skips a layout above the route cache's boundary, and with it
214
- * everything the record below holds: withCacheLookup and withCacheStore leave
215
- * the record alone for it. Decided per request, from what the client holds:
216
- * a prefetch of the same tree that holds the layout reads and writes the
217
- * record like any other request.
218
- */
219
- export declare function defersAboveRecord(ctx: HandlerContext<any, any>): boolean;
220
243
  /**
221
244
  * Record the handler unit this prefetch skipped. A `chain` entry takes what
222
245
  * the walk reaches after it (PrefetchDeferral.below); a slot is skipped
@@ -21,6 +21,17 @@ import type { Theme } from "../theme/types.js";
21
21
  * derived-context store.
22
22
  */
23
23
  export declare function buildFullPayload(m: MatchResult, ctx: HandlerContext<any>, url: URL, reqCtx: RequestContext<any>, handleStore: HandleStore): RscPayload;
24
+ /**
25
+ * The URL a render shows as the page's location (payload `pathname`, the SSR
26
+ * search and origin seed). For a request middleware rewrote, the visitor's
27
+ * URL, so the server markup and the hydrating client (which reads the address
28
+ * bar) agree on `usePathname()` and `useSearchParams()`. A render other
29
+ * visitors may be served shows `url`, the target, as a direct visit does: a
30
+ * shell capture, and a render the document cache stores (its key is the
31
+ * target). A render that shows the visitor's URL marks
32
+ * `_payloadVisitorLocation`, and the document cache does not store it.
33
+ */
34
+ export declare function renderedLocation(reqCtx: Pick<RequestContext<any>, "_variables" | "_shellCaptureRun" | "_documentCacheRender" | "_payloadVisitorLocation" | "res"> | undefined, url: URL): URL;
24
35
  /**
25
36
  * The theme a document payload carries as `metadata.initialTheme` (the
26
37
  * ThemeProvider's first state), read with no identity guard: it is the