@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.
- package/dist/bin/rango.js +4 -3
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/partial-update.d.ts +6 -4
- package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/types.d.ts +5 -1
- package/dist/types/build/route-types/scan-filter.d.ts +7 -1
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/handles/pushed-by.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/request-context.d.ts +39 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/serve-shell-request.d.ts +2 -1
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/vite/discovery/state.d.ts +12 -9
- package/dist/vite/index.js +164 -103
- package/package.json +1 -1
- package/skills/caching/SKILL.md +12 -0
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +84 -0
- package/skills/hooks/SKILL.md +11 -6
- package/skills/hooks/data.md +3 -1
- package/skills/loader/SKILL.md +7 -1
- package/skills/middleware/SKILL.md +183 -2
- package/skills/ppr/SKILL.md +9 -0
- package/skills/rango/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/client-components.md +1 -0
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +19 -1
- package/skills/use-cache/SKILL.md +4 -2
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/event-controller.ts +3 -0
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +19 -3
- package/src/browser/partial-update.ts +27 -12
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +46 -26
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/server-action-bridge.ts +6 -0
- package/src/browser/types.ts +5 -1
- package/src/build/route-types/scan-filter.ts +11 -4
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +12 -119
- package/src/context-var.ts +32 -2
- package/src/errors.ts +19 -0
- package/src/handles/pushed-by.ts +26 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +29 -12
- package/src/router/internal-params.ts +16 -0
- package/src/router/loader-resolution.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +11 -9
- package/src/router/match-middleware/cache-lookup.ts +20 -15
- package/src/router/match-middleware/cache-store.ts +9 -11
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
- package/src/router/segment-resolution/revalidation.ts +57 -33
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +4 -2
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +48 -17
- package/src/server/request-context.ts +154 -63
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/index.ts +1 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +2 -0
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/serve-shell-request.ts +15 -2
- package/src/thenable-status.ts +11 -0
- package/src/types/request-scope.ts +8 -0
- package/src/vite/discovery/state.ts +21 -13
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/router-discovery.ts +29 -27
package/src/context-var.ts
CHANGED
|
@@ -125,8 +125,38 @@ export function contextGet(
|
|
|
125
125
|
return variables[keyOrVar.key];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/**
|
|
129
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Keys that must never be used as string variable names, nor as a fetchable
|
|
130
|
+
* loader's request params (rsc/loader-fetch.ts).
|
|
131
|
+
*/
|
|
132
|
+
export const FORBIDDEN_KEYS: ReadonlySet<string> = new Set([
|
|
133
|
+
"__proto__",
|
|
134
|
+
"constructor",
|
|
135
|
+
"prototype",
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `target[key] = value` as an own data property, `__proto__` included: that
|
|
140
|
+
* assignment would call Object.prototype's setter and replace the target's
|
|
141
|
+
* prototype, which Flight refuses to serialize (#1109). Loader data parsed
|
|
142
|
+
* from JSON, cookie names and the like can carry the key.
|
|
143
|
+
*/
|
|
144
|
+
export function assignOwn(
|
|
145
|
+
target: Record<string, unknown>,
|
|
146
|
+
key: string,
|
|
147
|
+
value: unknown,
|
|
148
|
+
): void {
|
|
149
|
+
if (key === "__proto__") {
|
|
150
|
+
Object.defineProperty(target, key, {
|
|
151
|
+
value,
|
|
152
|
+
writable: true,
|
|
153
|
+
enumerable: true,
|
|
154
|
+
configurable: true,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
target[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
130
160
|
|
|
131
161
|
export interface ContextSetOptions {
|
|
132
162
|
/**
|
package/src/errors.ts
CHANGED
|
@@ -296,6 +296,25 @@ export function isSkip(value: unknown): value is Skip {
|
|
|
296
296
|
return value instanceof Skip;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
/**
|
|
300
|
+
* A request middleware rewrote (`throw rewrite()`) more than `maxHops` times,
|
|
301
|
+
* or to the URL it was on. Reported through `onError` (phase "routing") and
|
|
302
|
+
* answered with 508. `trail` lists the URLs, the visitor's first.
|
|
303
|
+
*/
|
|
304
|
+
export class RewriteLoopError extends Error {
|
|
305
|
+
name = "RewriteLoopError" as const;
|
|
306
|
+
readonly trail: readonly string[];
|
|
307
|
+
|
|
308
|
+
constructor(trail: readonly string[], maxHops: number) {
|
|
309
|
+
super(
|
|
310
|
+
`Infinite rewrite loop: ${trail.join(" -> ")}. A request is rewritten ` +
|
|
311
|
+
`at most ${maxHops} times, and never to the URL it is on.`,
|
|
312
|
+
);
|
|
313
|
+
Object.setPrototypeOf(this, RewriteLoopError.prototype);
|
|
314
|
+
this.trail = trail;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
299
318
|
/**
|
|
300
319
|
* Thrown by the partial updater when the server responds with a redirect payload
|
|
301
320
|
* that carries location state. Caught by navigate() to re-navigate to the
|
package/src/handles/pushed-by.ts
CHANGED
|
@@ -49,6 +49,32 @@ type PushedBy = readonly (string | null)[] | undefined;
|
|
|
49
49
|
const keysOf = (a: object | undefined, b: object | undefined): Set<string> =>
|
|
50
50
|
new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* `target` with the buckets of the segments in `ids` taken from `source`.
|
|
54
|
+
* A pushedBy record is a snapshot key like a handle's, so a bucket's values
|
|
55
|
+
* and the record of the runs that pushed them always come from the same
|
|
56
|
+
* snapshot. New containers; the value arrays are shared, as cloneHandleData
|
|
57
|
+
* (navigation-store.ts) shares them.
|
|
58
|
+
*/
|
|
59
|
+
export function withSegmentHandles(
|
|
60
|
+
target: HandleData,
|
|
61
|
+
source: HandleData,
|
|
62
|
+
ids: ReadonlySet<string>,
|
|
63
|
+
): HandleData {
|
|
64
|
+
const out: HandleData = {};
|
|
65
|
+
for (const name of keysOf(target, source)) {
|
|
66
|
+
const buckets: Record<string, unknown[]> = {};
|
|
67
|
+
for (const [id, values] of Object.entries(target[name] ?? {})) {
|
|
68
|
+
if (!ids.has(id)) buckets[id] = values;
|
|
69
|
+
}
|
|
70
|
+
for (const [id, values] of Object.entries(source[name] ?? {})) {
|
|
71
|
+
if (ids.has(id)) buckets[id] = values;
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(buckets).length > 0) out[name] = buckets;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
/**
|
|
53
79
|
* The handle data after a partial response's snapshot `incoming` (each stream
|
|
54
80
|
* yield holds everything the response pushed so far). A segment outside
|
package/src/index.rsc.ts
CHANGED
|
@@ -119,6 +119,14 @@ export {
|
|
|
119
119
|
transition,
|
|
120
120
|
} from "./route-definition.js";
|
|
121
121
|
|
|
122
|
+
// Render another route at the same URL from middleware (throw rewrite()).
|
|
123
|
+
export {
|
|
124
|
+
rewrite,
|
|
125
|
+
type Rewrite,
|
|
126
|
+
type RewriteFunction,
|
|
127
|
+
} from "./route-definition/rewrite.js";
|
|
128
|
+
export { RewriteLoopError } from "./errors.js";
|
|
129
|
+
|
|
122
130
|
// Composition types for reusable callback factories
|
|
123
131
|
export type {
|
|
124
132
|
RouteUseItem,
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,13 @@ export function redirect(): never {
|
|
|
155
155
|
throw serverOnlyStubError("redirect");
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Error-throwing stub for server-only `rewrite` function.
|
|
160
|
+
*/
|
|
161
|
+
export function rewrite(): never {
|
|
162
|
+
throw serverOnlyStubError("rewrite");
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
// Handle API (universal - works on both server and client)
|
|
159
166
|
export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
160
167
|
export {
|
package/src/response-utils.ts
CHANGED
|
@@ -60,3 +60,22 @@ export function isRedirectResponse(response: Response): boolean {
|
|
|
60
60
|
Boolean(response.headers.get("Location"))
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set a header on a response the router hands to the host, skipping a
|
|
66
|
+
* WebSocket upgrade (its headers are not to be touched) and swallowing the
|
|
67
|
+
* TypeError of immutable headers (a passed-through platform Response):
|
|
68
|
+
* instrumentation and debug headers never fail a request.
|
|
69
|
+
*/
|
|
70
|
+
export function trySetHeader(
|
|
71
|
+
response: Response,
|
|
72
|
+
name: string,
|
|
73
|
+
value: string,
|
|
74
|
+
): void {
|
|
75
|
+
if (isWebSocketUpgradeResponse(response)) return;
|
|
76
|
+
try {
|
|
77
|
+
response.headers.set(name, value);
|
|
78
|
+
} catch {
|
|
79
|
+
// Immutable headers: drop the header, never the response.
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/reverse.ts
CHANGED
|
@@ -149,6 +149,7 @@ export type ReverseFunction<TRoutes> = {
|
|
|
149
149
|
export type ScopedReverseFunction<
|
|
150
150
|
TLocalRoutes,
|
|
151
151
|
TGlobalRoutes = TLocalRoutes,
|
|
152
|
+
TResult = string,
|
|
152
153
|
> = {
|
|
153
154
|
/**
|
|
154
155
|
* Global route without params
|
|
@@ -159,7 +160,7 @@ export type ScopedReverseFunction<
|
|
|
159
160
|
> extends true
|
|
160
161
|
? TName
|
|
161
162
|
: never,
|
|
162
|
-
):
|
|
163
|
+
): TResult;
|
|
163
164
|
|
|
164
165
|
/**
|
|
165
166
|
* Global route with params
|
|
@@ -167,7 +168,7 @@ export type ScopedReverseFunction<
|
|
|
167
168
|
<TName extends keyof TGlobalRoutes & string>(
|
|
168
169
|
name: TName,
|
|
169
170
|
params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
|
|
170
|
-
):
|
|
171
|
+
): TResult;
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
174
|
* Global route with params and search
|
|
@@ -176,7 +177,7 @@ export type ScopedReverseFunction<
|
|
|
176
177
|
name: TName,
|
|
177
178
|
params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
|
|
178
179
|
search: ResolveSearchSchema<ExtractSearchSchema<TGlobalRoutes, TName>>,
|
|
179
|
-
):
|
|
180
|
+
): TResult;
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
183
|
* Dot-prefixed local route without params
|
|
@@ -187,7 +188,7 @@ export type ScopedReverseFunction<
|
|
|
187
188
|
> extends true
|
|
188
189
|
? `.${TName}`
|
|
189
190
|
: never,
|
|
190
|
-
):
|
|
191
|
+
): TResult;
|
|
191
192
|
|
|
192
193
|
/**
|
|
193
194
|
* Dot-prefixed local route with params
|
|
@@ -195,7 +196,7 @@ export type ScopedReverseFunction<
|
|
|
195
196
|
<TName extends keyof TLocalRoutes & string>(
|
|
196
197
|
name: `.${TName}`,
|
|
197
198
|
params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
198
|
-
):
|
|
199
|
+
): TResult;
|
|
199
200
|
|
|
200
201
|
/**
|
|
201
202
|
* Dot-prefixed local route with params and search
|
|
@@ -204,7 +205,7 @@ export type ScopedReverseFunction<
|
|
|
204
205
|
name: `.${TName}`,
|
|
205
206
|
params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
206
207
|
search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
|
|
207
|
-
):
|
|
208
|
+
): TResult;
|
|
208
209
|
};
|
|
209
210
|
|
|
210
211
|
/**
|
|
@@ -235,14 +236,16 @@ export type ScopedReverseFunction<
|
|
|
235
236
|
* reverse(".post", { slug: "hello" }) // compile error: no include() scope
|
|
236
237
|
* ```
|
|
237
238
|
*/
|
|
238
|
-
export type GlobalReverseFunction<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
239
|
+
export type GlobalReverseFunction<
|
|
240
|
+
TGlobalRoutes,
|
|
241
|
+
TResult = string,
|
|
242
|
+
> = string extends keyof TGlobalRoutes
|
|
243
|
+
? <TName extends string>(
|
|
244
|
+
name: Exclude<TName, `.${string}`>,
|
|
245
|
+
params?: Record<string, string>,
|
|
246
|
+
search?: Record<string, unknown>,
|
|
247
|
+
) => TResult
|
|
248
|
+
: ScopedReverseFunction<{}, TGlobalRoutes, TResult>;
|
|
246
249
|
|
|
247
250
|
/**
|
|
248
251
|
* Extract local routes type from UrlPatterns
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
attachRedirectState,
|
|
9
9
|
markExternalRedirect,
|
|
10
10
|
} from "../redirect-origin.js";
|
|
11
|
+
import { prefixBasename } from "../router/basename.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Create a soft redirect Response for middleware short-circuit
|
|
@@ -84,21 +85,7 @@ export function redirect(
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// Auto-prefix root-relative URLs with basename for app-local redirects.
|
|
87
|
-
|
|
88
|
-
// separator, a query, a fragment, or end-of-string, so "/admin?tab=x" and
|
|
89
|
-
// "/admin#frag" are not double-prefixed into "/admin/admin?tab=x".
|
|
90
|
-
const bn = _getRequestContext()?._basename;
|
|
91
|
-
let resolvedUrl = url;
|
|
92
|
-
if (
|
|
93
|
-
bn &&
|
|
94
|
-
url.startsWith("/") &&
|
|
95
|
-
url !== bn &&
|
|
96
|
-
!url.startsWith(bn + "/") &&
|
|
97
|
-
!url.startsWith(bn + "?") &&
|
|
98
|
-
!url.startsWith(bn + "#")
|
|
99
|
-
) {
|
|
100
|
-
resolvedUrl = url === "/" ? bn : bn + url;
|
|
101
|
-
}
|
|
88
|
+
const resolvedUrl = prefixBasename(url, _getRequestContext()?._basename);
|
|
102
89
|
|
|
103
90
|
const headers: Record<string, string> = {
|
|
104
91
|
Location: resolvedUrl,
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
|
|
9
|
+
import type { GlobalReverseFunction } from "../reverse.js";
|
|
10
|
+
import type { DefaultReverseRouteMap } from "../types/global-namespace.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cross-realm brand: a Vite dev server can load this module twice (the RSC
|
|
14
|
+
* and the SSR graph), so `instanceof` alone misses a Rewrite from the other
|
|
15
|
+
* copy.
|
|
16
|
+
*/
|
|
17
|
+
const REWRITE_BRAND: unique symbol = Symbol.for("rango.rewrite");
|
|
18
|
+
|
|
19
|
+
/** The target as written: a path, or a global route name with its params. */
|
|
20
|
+
export type RewriteTarget =
|
|
21
|
+
| { readonly path: string }
|
|
22
|
+
| {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly params?: Record<string, string>;
|
|
25
|
+
readonly search?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const REWRITE_RULE =
|
|
29
|
+
"rewrite() is for middleware: throw it from router.use() or route " +
|
|
30
|
+
"middleware (middleware() in urls()), before await next(), while it " +
|
|
31
|
+
"handles a GET or HEAD page request. Loaders and route handlers can't " +
|
|
32
|
+
"rewrite: throw rewrite() from the route's middleware instead. Intercept " +
|
|
33
|
+
"middleware and loader fetches can't either. In a server action use " +
|
|
34
|
+
"redirect(); for a mutation return a Response from middleware.";
|
|
35
|
+
|
|
36
|
+
export function describeRewriteTarget(target: RewriteTarget): string {
|
|
37
|
+
return "path" in target
|
|
38
|
+
? `rewrite(${JSON.stringify(target.path)})`
|
|
39
|
+
: `rewrite(${JSON.stringify(target.name)})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The value `rewrite()` returns, thrown by middleware. An Error, so a throw
|
|
44
|
+
* where a rewrite is not supported fails loudly with the rule in its message.
|
|
45
|
+
*/
|
|
46
|
+
export class Rewrite extends Error {
|
|
47
|
+
name = "Rewrite" as const;
|
|
48
|
+
readonly target: RewriteTarget;
|
|
49
|
+
|
|
50
|
+
constructor(target: RewriteTarget) {
|
|
51
|
+
super(`${describeRewriteTarget(target)}: ${REWRITE_RULE}`);
|
|
52
|
+
Object.setPrototypeOf(this, Rewrite.prototype);
|
|
53
|
+
this.target = target;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
Object.defineProperty(Rewrite.prototype, REWRITE_BRAND, { value: true });
|
|
57
|
+
|
|
58
|
+
/** Whether `value` is a Rewrite thrown by `rewrite()` (ctx.isRewrite). */
|
|
59
|
+
export function isRewrite(value: unknown): value is Rewrite {
|
|
60
|
+
return (
|
|
61
|
+
typeof value === "object" &&
|
|
62
|
+
value !== null &&
|
|
63
|
+
(value as { [REWRITE_BRAND]?: unknown })[REWRITE_BRAND] === true
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `rewrite(path)` or `rewrite(name, params?, search?)`: a path in this app
|
|
69
|
+
* (basename-prefixed like `redirect()`, may carry `?search`), or a global
|
|
70
|
+
* route name with the params and search `ctx.reverse` takes.
|
|
71
|
+
*/
|
|
72
|
+
export type RewriteFunction<TRoutes = DefaultReverseRouteMap> = ((
|
|
73
|
+
path: `/${string}`,
|
|
74
|
+
) => Rewrite) &
|
|
75
|
+
GlobalReverseFunction<TRoutes, Rewrite>;
|
|
76
|
+
|
|
77
|
+
function createRewrite(
|
|
78
|
+
target: string,
|
|
79
|
+
params?: Record<string, string>,
|
|
80
|
+
search?: Record<string, unknown>,
|
|
81
|
+
): Rewrite {
|
|
82
|
+
if (typeof target !== "string" || target === "") {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
'rewrite() takes a path starting with "/" or a global route name.',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (target.startsWith("/")) {
|
|
88
|
+
if (params !== undefined || search !== undefined) {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
`rewrite(${JSON.stringify(target)}) takes params only with a route name, not with a path.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return new Rewrite({ path: target });
|
|
94
|
+
}
|
|
95
|
+
if (target.startsWith(".")) {
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
`rewrite(${JSON.stringify(target)}): a rewrite resolves global route names only (no include() scope).`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return new Rewrite({ name: target, params, search });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render another route at the same URL. Throw it from middleware, before
|
|
105
|
+
* `await next()`:
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* router.use(async (ctx, next) => {
|
|
109
|
+
* if (!locked || ctx.pathname === "/maintenance") return next();
|
|
110
|
+
* throw rewrite("maintenance");
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* The request runs again as the target (global middleware, route
|
|
115
|
+
* middleware, render) with the same request context: values set before the
|
|
116
|
+
* throw stay readable. `ctx.url` is the target; `ctx.rewrittenFrom` is the
|
|
117
|
+
* visitor's URL. See the `/middleware` skill.
|
|
118
|
+
*/
|
|
119
|
+
export const rewrite: RewriteFunction = createRewrite as RewriteFunction;
|
|
120
|
+
|
|
121
|
+
/** A dispatch site that cannot honor a rewrite. */
|
|
122
|
+
export type RewriteSite = "mutation" | "loader-fetch" | "intercept";
|
|
123
|
+
|
|
124
|
+
const SITE_MESSAGE: Record<RewriteSite, string> = {
|
|
125
|
+
mutation:
|
|
126
|
+
"middleware threw it while handling a non-GET request (a server action " +
|
|
127
|
+
"or a form post). Return a Response for mutations instead, e.g. " +
|
|
128
|
+
'new Response("Maintenance", { status: 503 }).',
|
|
129
|
+
"loader-fetch":
|
|
130
|
+
"middleware threw it during a loader fetch (useFetchLoader, load(), " +
|
|
131
|
+
"useRefreshLoaders). Return a Response there.",
|
|
132
|
+
intercept:
|
|
133
|
+
"intercept middleware threw it. Rewrite from router.use() or the " +
|
|
134
|
+
"route's middleware(), or return a Response.",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** The error a rewrite becomes at a site that cannot honor it. */
|
|
138
|
+
export function rewriteNotSupported(
|
|
139
|
+
rewrite: Rewrite,
|
|
140
|
+
site: RewriteSite,
|
|
141
|
+
): Error {
|
|
142
|
+
return new Error(
|
|
143
|
+
`${describeRewriteTarget(rewrite.target)} is not supported here: ${SITE_MESSAGE[site]}`,
|
|
144
|
+
{ cause: rewrite },
|
|
145
|
+
);
|
|
146
|
+
}
|
package/src/route-map-builder.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* See docs/manifests.md for the full data flow.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
nullPrototypeStaticChildren,
|
|
13
|
+
type TrieNode,
|
|
14
|
+
} from "./router/route-trie-builder.js";
|
|
15
|
+
|
|
11
16
|
let globalRouteMap: Record<string, string> = {};
|
|
12
17
|
|
|
13
18
|
let cachedManifest: Record<string, string> | null = null;
|
|
@@ -86,8 +91,7 @@ export function clearCachedManifest(): void {
|
|
|
86
91
|
// precomputed entries so multi-router setups (e.g. site + admin via
|
|
87
92
|
// createHostRouter()) don't see each other's routes.
|
|
88
93
|
const perRouterManifestMap: Map<string, Record<string, string>> = new Map();
|
|
89
|
-
const perRouterTrieMap: Map<string,
|
|
90
|
-
new Map();
|
|
94
|
+
const perRouterTrieMap: Map<string, TrieNode> = new Map();
|
|
91
95
|
const perRouterPrecomputedEntriesMap: Map<
|
|
92
96
|
string,
|
|
93
97
|
Array<{ staticPrefix: string; routes: Record<string, string> }>
|
|
@@ -132,11 +136,12 @@ export function getRouterManifest(
|
|
|
132
136
|
return perRouterManifestMap.get(routerId);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Install a router's trie. A trie that arrives as JSON (the build manifest)
|
|
141
|
+
* or across the dev runner boundary gets null-prototype static maps first.
|
|
142
|
+
*/
|
|
143
|
+
export function setRouterTrie(routerId: string, trie: TrieNode): void {
|
|
144
|
+
perRouterTrieMap.set(routerId, nullPrototypeStaticChildren(trie));
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
// Routers whose trie came from the COMPLETE build manifest (deserialized via
|
|
@@ -156,9 +161,7 @@ export function isRouterTrieAuthoritative(routerId: string): boolean {
|
|
|
156
161
|
return authoritativeTrieRouters.has(routerId);
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
export function getRouterTrie(
|
|
160
|
-
routerId: string,
|
|
161
|
-
): import("./build/route-trie.js").TrieNode | undefined {
|
|
164
|
+
export function getRouterTrie(routerId: string): TrieNode | undefined {
|
|
162
165
|
return perRouterTrieMap.get(routerId);
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -199,7 +202,7 @@ export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
|
199
202
|
if (loader) {
|
|
200
203
|
const mod = await loader();
|
|
201
204
|
if (mod.trie) {
|
|
202
|
-
|
|
205
|
+
setRouterTrie(routerId, mod.trie);
|
|
203
206
|
// A trie serialized into the build manifest comes from complete
|
|
204
207
|
// discovery — misses are authoritative 404s (see find-match.ts).
|
|
205
208
|
markRouterTrieAuthoritative(routerId);
|
package/src/router/basename.ts
CHANGED
|
@@ -12,3 +12,23 @@ export function normalizeBasename(basename?: string): string | undefined {
|
|
|
12
12
|
const trimmed = basename.replace(/^\/+|\/+$/g, "");
|
|
13
13
|
return trimmed ? "/" + trimmed : undefined;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prefix a root-relative path with the basename unless it already carries it
|
|
18
|
+
* (the basename followed by "/", "?", "#" or the end), so "/admin?tab=x" and
|
|
19
|
+
* "/admin#frag" under basename "/admin" are not prefixed twice. Shared by
|
|
20
|
+
* redirect(), rewrite() and useRouter().
|
|
21
|
+
*/
|
|
22
|
+
export function prefixBasename(path: string, basename?: string): string {
|
|
23
|
+
if (
|
|
24
|
+
!basename ||
|
|
25
|
+
!path.startsWith("/") ||
|
|
26
|
+
path === basename ||
|
|
27
|
+
path.startsWith(basename + "/") ||
|
|
28
|
+
path.startsWith(basename + "?") ||
|
|
29
|
+
path.startsWith(basename + "#")
|
|
30
|
+
) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
return path === "/" ? basename : basename + path;
|
|
34
|
+
}
|
|
@@ -30,6 +30,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
|
30
30
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
31
31
|
import { substitutePatternParams } from "./substitute-pattern-params.js";
|
|
32
32
|
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
33
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
33
34
|
|
|
34
35
|
// Mutating Headers methods guarded so they throw inside "use cache" / cache()
|
|
35
36
|
// scope. Module-level constant (read-only, .has() lookups) so it is allocated
|
|
@@ -87,19 +88,7 @@ export function guardHeaderWrites(
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
* Strip internal _rsc* query params from a URL.
|
|
92
|
-
* Returns a new URL with only user-facing params.
|
|
93
|
-
*/
|
|
94
|
-
export function stripInternalParams(url: URL): URL {
|
|
95
|
-
const clean = new URL(url);
|
|
96
|
-
for (const key of [...clean.searchParams.keys()]) {
|
|
97
|
-
if (key.startsWith("_rsc")) {
|
|
98
|
-
clean.searchParams.delete(key);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return clean;
|
|
102
|
-
}
|
|
91
|
+
export { stripInternalParams } from "./internal-params.js";
|
|
103
92
|
|
|
104
93
|
/**
|
|
105
94
|
* Resolve route name with namespace prefix support.
|
|
@@ -362,6 +351,7 @@ export function createHandlerContext<TEnv>(
|
|
|
362
351
|
(ctx as { [THEME_REQUEST_CONTEXT]?: unknown })[THEME_REQUEST_CONTEXT] =
|
|
363
352
|
requestContext;
|
|
364
353
|
Object.defineProperty(ctx, "theme", THEME_DESCRIPTOR);
|
|
354
|
+
defineRewrittenFrom(ctx, requestContext);
|
|
365
355
|
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
366
356
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
367
357
|
return ctx;
|
|
@@ -262,13 +262,23 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
262
262
|
|
|
263
263
|
latchPprHeaderScopeForInterceptTarget(interceptEntry);
|
|
264
264
|
|
|
265
|
+
// The intercept segment's id: the tag scope its handler and layout record in
|
|
266
|
+
// (#957, cache-tag.ts runInSegmentTagScope) and the owner of its handler's
|
|
267
|
+
// and loaders' handle pushes, pinned before the loader kickoffs as
|
|
268
|
+
// fresh.ts/revalidation.ts pin theirs (createLoaderExecutor captures it at
|
|
269
|
+
// kickoff; a handler's ctx.use(Handle) reads it when called).
|
|
270
|
+
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
271
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
272
|
+
slotSegmentId;
|
|
273
|
+
const hasLoaders = interceptEntry.loader.length > 0;
|
|
274
|
+
|
|
265
275
|
const loaderPromises: Promise<any>[] = [];
|
|
266
276
|
const loaderIds: string[] = [];
|
|
267
277
|
|
|
268
278
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
269
279
|
const loaderEntry = interceptEntry.loader[i];
|
|
270
280
|
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
271
|
-
const segmentId = `${
|
|
281
|
+
const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
|
|
272
282
|
|
|
273
283
|
if (revalidationContext) {
|
|
274
284
|
const {
|
|
@@ -282,8 +292,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
282
292
|
stale,
|
|
283
293
|
} = revalidationContext;
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
295
|
+
if (clientSegmentIds.has(slotSegmentId)) {
|
|
287
296
|
const dummySegment: ResolvedSegment = {
|
|
288
297
|
id: segmentId,
|
|
289
298
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -345,9 +354,6 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
345
354
|
);
|
|
346
355
|
}
|
|
347
356
|
|
|
348
|
-
// The intercept segment's id, and the tag scope its handler and layout
|
|
349
|
-
// record in (#957, cache-tag.ts runInSegmentTagScope).
|
|
350
|
-
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
351
357
|
let handlerResult: ReactNode;
|
|
352
358
|
try {
|
|
353
359
|
const handler = interceptEntry.handler;
|
|
@@ -399,7 +405,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
399
405
|
const streamedComponent = (promise: Promise<unknown>): ReactNode =>
|
|
400
406
|
trackStreamedHandler(deps, promise, {
|
|
401
407
|
boundaryEntry: parentEntry,
|
|
402
|
-
segmentId:
|
|
408
|
+
segmentId: slotSegmentId,
|
|
403
409
|
segmentType: "parallel",
|
|
404
410
|
context,
|
|
405
411
|
routeKey: interceptEntry.routeName,
|
|
@@ -442,6 +448,13 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
442
448
|
}
|
|
443
449
|
}
|
|
444
450
|
|
|
451
|
+
// The aggregate lists the loaders that ran; empty, not absent, when the
|
|
452
|
+
// client holds the slot and all skipped. Absent replaces the client's
|
|
453
|
+
// aggregate, empty merges into it (merge-segment-loaders.ts).
|
|
454
|
+
if (hasLoaders && loaderPromises.length === 0) {
|
|
455
|
+
loaderDataPromise = [];
|
|
456
|
+
}
|
|
457
|
+
|
|
445
458
|
const interceptSegment = {
|
|
446
459
|
id: slotSegmentId,
|
|
447
460
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -455,7 +468,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
455
468
|
belongsToRoute,
|
|
456
469
|
parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
|
|
457
470
|
loaderDataPromise,
|
|
458
|
-
loaderIds:
|
|
471
|
+
loaderIds: hasLoaders ? loaderIds : undefined,
|
|
459
472
|
};
|
|
460
473
|
segments.push(interceptSegment);
|
|
461
474
|
|
|
@@ -498,6 +511,10 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
498
511
|
|
|
499
512
|
latchPprHeaderScopeForInterceptTarget(interceptEntry);
|
|
500
513
|
|
|
514
|
+
// The loaders' handle push owner, as in resolveInterceptEntry.
|
|
515
|
+
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
516
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
517
|
+
slotSegmentId;
|
|
501
518
|
const loaderPromises: Promise<any>[] = [];
|
|
502
519
|
const loaderIds: string[] = [];
|
|
503
520
|
|
|
@@ -515,10 +532,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
515
532
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
516
533
|
const loaderEntry = interceptEntry.loader[i];
|
|
517
534
|
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
518
|
-
const segmentId = `${
|
|
535
|
+
const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
|
|
519
536
|
|
|
520
|
-
|
|
521
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
537
|
+
if (clientSegmentIds.has(slotSegmentId)) {
|
|
522
538
|
const dummySegment: ResolvedSegment = {
|
|
523
539
|
id: segmentId,
|
|
524
540
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -581,8 +597,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
581
597
|
);
|
|
582
598
|
}
|
|
583
599
|
|
|
600
|
+
// Every loader held: the empty aggregate (see resolveInterceptEntry).
|
|
584
601
|
if (loaderPromises.length === 0) {
|
|
585
|
-
return
|
|
602
|
+
return { loaderDataPromise: [], loaderIds };
|
|
586
603
|
}
|
|
587
604
|
|
|
588
605
|
// Match fresh-path semantics: only defer (no await) when loading is truthy.
|
|
@@ -0,0 +1,16 @@
|
|
|
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 function stripInternalParams(url: URL): URL {
|
|
9
|
+
const clean = new URL(url);
|
|
10
|
+
for (const key of [...clean.searchParams.keys()]) {
|
|
11
|
+
if (key.startsWith("_rsc")) {
|
|
12
|
+
clean.searchParams.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return clean;
|
|
16
|
+
}
|