@rangojs/router 0.23.0 → 0.24.1
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/client-urls/types.d.ts +8 -0
- 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 +86 -1
- 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/client-urls/client-root.tsx +3 -3
- package/src/client-urls/client-urls.ts +8 -1
- package/src/client-urls/types.ts +8 -0
- 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
|
@@ -154,12 +154,14 @@ interface LoadingItem extends ItemBase {
|
|
|
154
154
|
|
|
155
155
|
interface FlattenContext {
|
|
156
156
|
readonly layouts: readonly ComponentType[];
|
|
157
|
+
readonly layoutKeys: readonly number[];
|
|
157
158
|
readonly loaders: readonly ClientUrlLoaderRecord[];
|
|
158
159
|
readonly loading: ReactNode | undefined;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
const EMPTY_CONTEXT: FlattenContext = {
|
|
162
163
|
layouts: [],
|
|
164
|
+
layoutKeys: [],
|
|
163
165
|
loaders: [],
|
|
164
166
|
loading: undefined,
|
|
165
167
|
};
|
|
@@ -588,6 +590,7 @@ function applyConfig(
|
|
|
588
590
|
|
|
589
591
|
return {
|
|
590
592
|
layouts: context.layouts,
|
|
593
|
+
layoutKeys: context.layoutKeys,
|
|
591
594
|
loaders,
|
|
592
595
|
loading,
|
|
593
596
|
};
|
|
@@ -614,6 +617,7 @@ function addPathRecord(
|
|
|
614
617
|
options,
|
|
615
618
|
component: item.component,
|
|
616
619
|
layouts: Object.freeze([...routeContext.layouts]),
|
|
620
|
+
layoutKeys: Object.freeze([...routeContext.layoutKeys]),
|
|
617
621
|
loaders: Object.freeze([...routeContext.loaders]),
|
|
618
622
|
loading: routeContext.loading,
|
|
619
623
|
transition: transitionItem
|
|
@@ -625,6 +629,7 @@ function addPathRecord(
|
|
|
625
629
|
|
|
626
630
|
function flattenLayout(
|
|
627
631
|
item: LayoutItem,
|
|
632
|
+
ordinal: number,
|
|
628
633
|
context: FlattenContext,
|
|
629
634
|
routes: ClientUrlRouteRecord[],
|
|
630
635
|
): void {
|
|
@@ -633,6 +638,7 @@ function flattenLayout(
|
|
|
633
638
|
{
|
|
634
639
|
...context,
|
|
635
640
|
layouts: [...context.layouts, item.component],
|
|
641
|
+
layoutKeys: [...context.layoutKeys, ordinal],
|
|
636
642
|
},
|
|
637
643
|
item.children,
|
|
638
644
|
);
|
|
@@ -650,11 +656,12 @@ function flattenRoutes(
|
|
|
650
656
|
context: FlattenContext,
|
|
651
657
|
routes: ClientUrlRouteRecord[],
|
|
652
658
|
): void {
|
|
659
|
+
let layoutOrdinal = 0;
|
|
653
660
|
for (const item of items) {
|
|
654
661
|
if (item.type === "path") {
|
|
655
662
|
addPathRecord(item, context, routes);
|
|
656
663
|
} else if (item.type === "layout") {
|
|
657
|
-
flattenLayout(item, context, routes);
|
|
664
|
+
flattenLayout(item, layoutOrdinal++, context, routes);
|
|
658
665
|
}
|
|
659
666
|
}
|
|
660
667
|
}
|
package/src/client-urls/types.ts
CHANGED
|
@@ -105,6 +105,14 @@ export interface ClientUrlRouteRecord {
|
|
|
105
105
|
readonly options: Readonly<ClientPathOptions> | undefined;
|
|
106
106
|
readonly component: ComponentType;
|
|
107
107
|
readonly layouts: readonly ComponentType[];
|
|
108
|
+
/**
|
|
109
|
+
* One per entry in `layouts`: the layout() item's ordinal among its sibling
|
|
110
|
+
* layout()s, its React key. Routes under the same layout() share it, so
|
|
111
|
+
* ClientUrlsRoot keeps that layout's instance across them (#1124). Keys only
|
|
112
|
+
* need to be unique among siblings: a differing parent layout remounts the
|
|
113
|
+
* whole subtree anyway.
|
|
114
|
+
*/
|
|
115
|
+
readonly layoutKeys: readonly number[];
|
|
108
116
|
readonly loaders: readonly ClientUrlLoaderRecord[];
|
|
109
117
|
readonly loading: ReactNode | undefined;
|
|
110
118
|
readonly transition: Readonly<ClientTransitionConfig> | undefined;
|
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;
|