@rangojs/router 0.0.0-experimental.56cb65a7 → 0.0.0-experimental.57
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 +128 -46
- package/dist/vite/index.js +211 -47
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +67 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +16 -3
- package/src/browser/navigation-client.ts +64 -40
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +37 -4
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +44 -8
- package/src/browser/react/NavigationProvider.tsx +13 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +3 -0
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +9 -1
- package/src/router/handler-context.ts +36 -17
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +9 -2
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +50 -7
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-result.ts +11 -5
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +2 -5
- package/src/router/prerender-match.ts +2 -2
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +25 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +47 -16
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +50 -21
- package/src/router/types.ts +1 -0
- package/src/router.ts +41 -4
- package/src/rsc/handler.ts +11 -2
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/rsc/types.ts +8 -1
- package/src/server/context.ts +36 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +50 -12
- package/src/ssr/index.tsx +3 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +125 -31
- package/src/types/loader-types.ts +4 -5
- package/src/urls/pattern-types.ts +12 -0
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/rango.ts +17 -1
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { NonceProvider } from "../rsc/types.js";
|
|
9
9
|
import type { ExecutionContext } from "../server/request-context.js";
|
|
10
10
|
import type { UrlPatterns } from "../urls.js";
|
|
11
|
+
import type { UrlBuilder } from "../urls/pattern-types.js";
|
|
11
12
|
import type { NamedRouteEntry } from "./content-negotiation.js";
|
|
12
13
|
import type { TelemetrySink } from "./telemetry.js";
|
|
13
14
|
import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
@@ -95,6 +96,28 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
95
96
|
*/
|
|
96
97
|
$$sourceFile?: string;
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* URL prefix applied to all routes registered with this router.
|
|
101
|
+
*
|
|
102
|
+
* Useful when the app is served under a sub-path (e.g. `/admin` or `/v2`).
|
|
103
|
+
* All `path()` patterns are automatically prefixed and `reverse()` returns
|
|
104
|
+
* full paths including the basename. Route names are NOT prefixed.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const router = createRouter({
|
|
109
|
+
* basename: "/admin",
|
|
110
|
+
* }).routes(({ path }) => [
|
|
111
|
+
* path("/", Dashboard, { name: "home" }), // matches /admin
|
|
112
|
+
* path("/users", Users, { name: "users" }), // matches /admin/users
|
|
113
|
+
* ]);
|
|
114
|
+
*
|
|
115
|
+
* router.reverse("home"); // "/admin"
|
|
116
|
+
* router.reverse("users"); // "/admin/users"
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
basename?: string;
|
|
120
|
+
|
|
98
121
|
/**
|
|
99
122
|
* Enable performance metrics collection
|
|
100
123
|
* When enabled, metrics are output to console and available via Server-Timing header
|
|
@@ -337,25 +360,28 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
337
360
|
/**
|
|
338
361
|
* URL patterns to register with the router.
|
|
339
362
|
*
|
|
340
|
-
*
|
|
341
|
-
* directly
|
|
363
|
+
* Accepts either a `UrlPatterns` object from `urls()` or a builder function
|
|
364
|
+
* directly (urls() is called implicitly).
|
|
342
365
|
*
|
|
343
366
|
* @example
|
|
344
367
|
* ```typescript
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* const urlpatterns = urls(({ path, layout }) => [
|
|
348
|
-
* path("/", HomePage, { name: "home" }),
|
|
349
|
-
* path("/about", AboutPage, { name: "about" }),
|
|
350
|
-
* ]);
|
|
351
|
-
*
|
|
352
|
-
* const router = createRouter<AppEnv>({
|
|
368
|
+
* // With urls()
|
|
369
|
+
* createRouter<AppEnv>({
|
|
353
370
|
* document: Document,
|
|
354
371
|
* urls: urlpatterns,
|
|
355
372
|
* });
|
|
373
|
+
*
|
|
374
|
+
* // With builder function
|
|
375
|
+
* createRouter<AppEnv>({
|
|
376
|
+
* document: Document,
|
|
377
|
+
* urls: ({ path }) => [
|
|
378
|
+
* path("/", HomePage, { name: "home" }),
|
|
379
|
+
* path("/about", AboutPage, { name: "about" }),
|
|
380
|
+
* ],
|
|
381
|
+
* });
|
|
356
382
|
* ```
|
|
357
383
|
*/
|
|
358
|
-
urls?: UrlPatterns<TEnv, any>;
|
|
384
|
+
urls?: UrlPatterns<TEnv, any> | UrlBuilder<TEnv>;
|
|
359
385
|
|
|
360
386
|
/**
|
|
361
387
|
* Injected by the Vite transform at compile time.
|
|
@@ -30,7 +30,11 @@ import {
|
|
|
30
30
|
} from "./helpers.js";
|
|
31
31
|
import { getRouterContext } from "../router-context.js";
|
|
32
32
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
track,
|
|
35
|
+
RSCRouterContext,
|
|
36
|
+
runInsideLoaderScope,
|
|
37
|
+
} from "../../server/context.js";
|
|
34
38
|
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
// Streamed handler telemetry
|
|
@@ -100,9 +104,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
100
104
|
|
|
101
105
|
if (!loadingDisabled) {
|
|
102
106
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
103
|
-
|
|
104
|
-
// RSC/SSR stream consumption, after the perf timeline is logged.
|
|
105
|
-
return loaderEntries.map((loaderEntry, i) => {
|
|
107
|
+
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
106
108
|
const { loader } = loaderEntry;
|
|
107
109
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
108
110
|
return {
|
|
@@ -114,7 +116,9 @@ export async function resolveLoaders<TEnv>(
|
|
|
114
116
|
params: ctx.params,
|
|
115
117
|
loaderId: loader.$$id,
|
|
116
118
|
loaderData: deps.wrapLoaderPromise(
|
|
117
|
-
|
|
119
|
+
runInsideLoaderScope(() =>
|
|
120
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
121
|
+
),
|
|
118
122
|
entry,
|
|
119
123
|
segmentId,
|
|
120
124
|
ctx.pathname,
|
|
@@ -122,14 +126,17 @@ export async function resolveLoaders<TEnv>(
|
|
|
122
126
|
belongsToRoute,
|
|
123
127
|
};
|
|
124
128
|
});
|
|
129
|
+
|
|
130
|
+
return segments;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
128
134
|
// settled promises so handlers don't stream loading placeholders.
|
|
129
|
-
// We can measure actual execution time here since we await all loaders.
|
|
130
135
|
const pendingLoaderData = loaderEntries.map((loaderEntry) => {
|
|
131
136
|
const start = performance.now();
|
|
132
|
-
const promise =
|
|
137
|
+
const promise = runInsideLoaderScope(() =>
|
|
138
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
139
|
+
);
|
|
133
140
|
return { promise, start, loaderId: loaderEntry.loader.$$id };
|
|
134
141
|
});
|
|
135
142
|
await Promise.all(pendingLoaderData.map((p) => p.promise));
|
|
@@ -344,7 +351,7 @@ export async function resolveSegment<TEnv>(
|
|
|
344
351
|
namespace: entry.id,
|
|
345
352
|
type: "route",
|
|
346
353
|
index: 0,
|
|
347
|
-
component,
|
|
354
|
+
component: component ?? null,
|
|
348
355
|
loading: entry.loading === false ? null : entry.loading,
|
|
349
356
|
transition: entry.transition,
|
|
350
357
|
params,
|
|
@@ -580,6 +587,13 @@ export async function resolveAllSegments<TEnv>(
|
|
|
580
587
|
} catch {}
|
|
581
588
|
|
|
582
589
|
for (const entry of entries) {
|
|
590
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
591
|
+
// can guard non-cacheable variable reads. Also guards response-level
|
|
592
|
+
// side effects (headers.set). Persists for all descendant entries.
|
|
593
|
+
if (entry.type === "cache") {
|
|
594
|
+
const store = RSCRouterContext.getStore();
|
|
595
|
+
if (store) store.insideCacheScope = true;
|
|
596
|
+
}
|
|
583
597
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
584
598
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
585
599
|
entry,
|
|
@@ -624,20 +638,37 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
624
638
|
deps: SegmentResolutionDeps<TEnv>,
|
|
625
639
|
): Promise<ResolvedSegment[]> {
|
|
626
640
|
const loaderSegments: ResolvedSegment[] = [];
|
|
641
|
+
const seenIds = new Set<string>();
|
|
627
642
|
|
|
628
643
|
async function collectEntryLoaders(
|
|
629
644
|
entry: EntryData,
|
|
630
645
|
belongsToRoute: boolean,
|
|
631
646
|
shortCodeOverride?: string,
|
|
632
647
|
): Promise<void> {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
648
|
+
// Skip if all loaders from this entry have already been resolved
|
|
649
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
650
|
+
const entryLoaders = entry.loader ?? [];
|
|
651
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
652
|
+
const allAlreadySeen =
|
|
653
|
+
entryLoaders.length > 0 &&
|
|
654
|
+
entryLoaders.every((le, i) =>
|
|
655
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
656
|
+
);
|
|
657
|
+
if (!allAlreadySeen) {
|
|
658
|
+
const segments = await resolveLoaders(
|
|
659
|
+
entry,
|
|
660
|
+
context,
|
|
661
|
+
belongsToRoute,
|
|
662
|
+
deps,
|
|
663
|
+
shortCodeOverride,
|
|
664
|
+
);
|
|
665
|
+
for (const seg of segments) {
|
|
666
|
+
if (!seenIds.has(seg.id)) {
|
|
667
|
+
seenIds.add(seg.id);
|
|
668
|
+
loaderSegments.push(seg);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
641
672
|
|
|
642
673
|
const seenParallelEntryIds = new Set<string>();
|
|
643
674
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Error boundary segment creation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { createElement, type ReactNode } from "react";
|
|
12
12
|
import { DataNotFoundError } from "../../errors";
|
|
13
13
|
import {
|
|
14
14
|
createErrorInfo,
|
|
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
|
|
|
180
180
|
|
|
181
181
|
if (error instanceof DataNotFoundError) {
|
|
182
182
|
const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
|
|
183
|
+
// Fall back to router's notFound component, then a plain default
|
|
184
|
+
const notFoundOption = deps.notFoundComponent;
|
|
185
|
+
const defaultFallback =
|
|
186
|
+
typeof notFoundOption === "function"
|
|
187
|
+
? notFoundOption({ pathname: pathname ?? "" })
|
|
188
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
189
|
+
const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
+
const notFoundInfo = createNotFoundInfo(
|
|
192
|
+
error,
|
|
193
|
+
entry.shortCode,
|
|
194
|
+
entry.type,
|
|
195
|
+
pathname,
|
|
196
|
+
);
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
reportError(true, {
|
|
199
|
+
notFound: true,
|
|
200
|
+
message: notFoundInfo.message,
|
|
201
|
+
});
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
debugLog("segment", "notFound boundary handled error", {
|
|
204
|
+
segmentId: entry.shortCode,
|
|
205
|
+
message: notFoundInfo.message,
|
|
206
|
+
});
|
|
201
207
|
|
|
202
|
-
|
|
208
|
+
setResponseStatus(404);
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
210
|
+
return createNotFoundSegment(
|
|
211
|
+
notFoundInfo,
|
|
212
|
+
effectiveNotFoundFallback,
|
|
213
|
+
entry,
|
|
214
|
+
params,
|
|
215
|
+
);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
const fallback = deps.findNearestErrorBoundary(entry);
|
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
} from "./helpers.js";
|
|
42
42
|
import { getRouterContext } from "../router-context.js";
|
|
43
43
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
track,
|
|
46
|
+
RSCRouterContext,
|
|
47
|
+
runInsideLoaderScope,
|
|
48
|
+
} from "../../server/context.js";
|
|
45
49
|
|
|
46
50
|
// ---------------------------------------------------------------------------
|
|
47
51
|
// Telemetry helpers
|
|
@@ -232,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
232
236
|
params: ctx.params,
|
|
233
237
|
loaderId: loader.$$id,
|
|
234
238
|
loaderData: deps.wrapLoaderPromise(
|
|
235
|
-
|
|
239
|
+
runInsideLoaderScope(() =>
|
|
240
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
241
|
+
),
|
|
236
242
|
entry,
|
|
237
243
|
segmentId,
|
|
238
244
|
ctx.pathname,
|
|
@@ -262,29 +268,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
262
268
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
263
269
|
const allLoaderSegments: ResolvedSegment[] = [];
|
|
264
270
|
const allMatchedIds: string[] = [];
|
|
271
|
+
const seenIds = new Set<string>();
|
|
265
272
|
|
|
266
273
|
async function collectEntryLoaders(
|
|
267
274
|
entry: EntryData,
|
|
268
275
|
belongsToRoute: boolean,
|
|
269
276
|
shortCodeOverride?: string,
|
|
270
277
|
): Promise<void> {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
// Skip if all loaders from this entry have already been resolved
|
|
279
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
280
|
+
const loaderEntries = entry.loader ?? [];
|
|
281
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
282
|
+
const allAlreadySeen =
|
|
283
|
+
loaderEntries.length > 0 &&
|
|
284
|
+
loaderEntries.every((le, i) =>
|
|
285
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
286
|
+
);
|
|
287
|
+
if (!allAlreadySeen) {
|
|
288
|
+
const { segments, matchedIds } = await resolveLoadersWithRevalidation(
|
|
289
|
+
entry,
|
|
290
|
+
context,
|
|
291
|
+
belongsToRoute,
|
|
292
|
+
clientSegmentIds,
|
|
293
|
+
prevParams,
|
|
294
|
+
request,
|
|
295
|
+
prevUrl,
|
|
296
|
+
nextUrl,
|
|
297
|
+
routeKey,
|
|
298
|
+
deps,
|
|
299
|
+
actionContext,
|
|
300
|
+
shortCodeOverride,
|
|
301
|
+
stale,
|
|
302
|
+
);
|
|
303
|
+
for (const seg of segments) {
|
|
304
|
+
if (!seenIds.has(seg.id)) {
|
|
305
|
+
seenIds.add(seg.id);
|
|
306
|
+
allLoaderSegments.push(seg);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
allMatchedIds.push(...matchedIds);
|
|
310
|
+
}
|
|
288
311
|
|
|
289
312
|
const seenParallelEntryIds = new Set<string>();
|
|
290
313
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -705,10 +728,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
705
728
|
() => null,
|
|
706
729
|
);
|
|
707
730
|
|
|
731
|
+
// Normalize void handlers (undefined) to null so the reconciler's
|
|
732
|
+
// component === null checks work consistently for both void and explicit null.
|
|
708
733
|
const resolvedComponent =
|
|
709
734
|
component && typeof component === "object" && "content" in component
|
|
710
|
-
? (component as { content: ReactNode }).content
|
|
711
|
-
: component;
|
|
735
|
+
? ((component as { content: ReactNode }).content ?? null)
|
|
736
|
+
: (component ?? null);
|
|
712
737
|
|
|
713
738
|
const segment: ResolvedSegment = {
|
|
714
739
|
id: entry.shortCode,
|
|
@@ -1229,6 +1254,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1229
1254
|
}
|
|
1230
1255
|
|
|
1231
1256
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1257
|
+
if (entry.type === "cache") {
|
|
1258
|
+
const store = RSCRouterContext.getStore();
|
|
1259
|
+
if (store) store.insideCacheScope = true;
|
|
1260
|
+
}
|
|
1232
1261
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1233
1262
|
const resolved = await resolveWithErrorBoundary(
|
|
1234
1263
|
nonParallelEntry,
|
package/src/router/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
96
96
|
findNearestNotFoundBoundary: (
|
|
97
97
|
entry: EntryData | null,
|
|
98
98
|
) => ReactNode | NotFoundBoundaryHandler | null;
|
|
99
|
+
notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
|
|
99
100
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
100
101
|
}
|
|
101
102
|
|
package/src/router.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import MapRootLayout from "./server/root-layout.js";
|
|
20
20
|
import type { AllUseItems } from "./route-types.js";
|
|
21
21
|
import type { UrlPatterns } from "./urls.js";
|
|
22
|
+
import type { UrlBuilder } from "./urls/pattern-types.js";
|
|
23
|
+
import { urls } from "./urls.js";
|
|
22
24
|
import {
|
|
23
25
|
EntryData,
|
|
24
26
|
InterceptSelectorContext,
|
|
@@ -133,6 +135,7 @@ export function createRouter<TEnv = any>(
|
|
|
133
135
|
const {
|
|
134
136
|
id: userProvidedId,
|
|
135
137
|
$$id: injectedId,
|
|
138
|
+
basename: basenameOption,
|
|
136
139
|
debugPerformance = false,
|
|
137
140
|
document: documentOption,
|
|
138
141
|
defaultErrorBoundary,
|
|
@@ -158,6 +161,13 @@ export function createRouter<TEnv = any>(
|
|
|
158
161
|
originCheck: originCheckOption,
|
|
159
162
|
} = options;
|
|
160
163
|
|
|
164
|
+
// Normalize basename: ensure leading slash, strip trailing slash.
|
|
165
|
+
// A bare "/" is equivalent to no basename.
|
|
166
|
+
const basename =
|
|
167
|
+
basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
|
|
168
|
+
? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
|
|
169
|
+
: undefined;
|
|
170
|
+
|
|
161
171
|
// Resolve telemetry sink (no-op when not configured)
|
|
162
172
|
const telemetry = resolveSink(telemetrySink);
|
|
163
173
|
|
|
@@ -526,6 +536,7 @@ export function createRouter<TEnv = any>(
|
|
|
526
536
|
trackHandler,
|
|
527
537
|
findNearestErrorBoundary,
|
|
528
538
|
findNearestNotFoundBoundary,
|
|
539
|
+
notFoundComponent: notFound,
|
|
529
540
|
callOnError,
|
|
530
541
|
};
|
|
531
542
|
|
|
@@ -658,8 +669,15 @@ export function createRouter<TEnv = any>(
|
|
|
658
669
|
const router: RSCRouterInternal<TEnv, {}> = {
|
|
659
670
|
__brand: RSC_ROUTER_BRAND,
|
|
660
671
|
id: routerId,
|
|
672
|
+
basename,
|
|
673
|
+
|
|
674
|
+
routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
|
|
675
|
+
// Wrap builder functions in urls() automatically
|
|
676
|
+
const urlPatterns: UrlPatterns<TEnv> =
|
|
677
|
+
typeof patternsOrBuilder === "function"
|
|
678
|
+
? (urls(patternsOrBuilder) as UrlPatterns<TEnv>)
|
|
679
|
+
: patternsOrBuilder;
|
|
661
680
|
|
|
662
|
-
routes(urlPatterns: UrlPatterns<TEnv>): any {
|
|
663
681
|
// Store reference for runtime manifest generation
|
|
664
682
|
storedUrlPatterns = urlPatterns;
|
|
665
683
|
const currentMountIndex = mountIndex++;
|
|
@@ -707,6 +725,10 @@ export function createRouter<TEnv = any>(
|
|
|
707
725
|
counters: {},
|
|
708
726
|
mountIndex: currentMountIndex,
|
|
709
727
|
cacheProfiles: resolvedCacheProfiles,
|
|
728
|
+
// basename sets the initial URL prefix so all path() patterns
|
|
729
|
+
// are registered with the prefix (e.g. "/admin" + "/users" = "/admin/users").
|
|
730
|
+
// No namePrefix — route names stay unprefixed.
|
|
731
|
+
...(basename ? { urlPrefix: basename } : {}),
|
|
710
732
|
},
|
|
711
733
|
() => {
|
|
712
734
|
handlerResult = urlPatterns.handler() as AllUseItems[];
|
|
@@ -855,8 +877,18 @@ export function createRouter<TEnv = any>(
|
|
|
855
877
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
856
878
|
middleware?: MiddlewareFn<TEnv>,
|
|
857
879
|
): any {
|
|
858
|
-
//
|
|
859
|
-
|
|
880
|
+
// Auto-prefix pattern with basename so router-level middleware
|
|
881
|
+
// patterns are router-relative (e.g. "/users/*" matches "/app/users/*").
|
|
882
|
+
if (basename && typeof patternOrMiddleware === "string") {
|
|
883
|
+
const pattern = patternOrMiddleware;
|
|
884
|
+
const prefixed =
|
|
885
|
+
pattern === "/*" || pattern === "*"
|
|
886
|
+
? `${basename}/*`
|
|
887
|
+
: `${basename}${pattern}`;
|
|
888
|
+
addMiddleware(prefixed, middleware, null);
|
|
889
|
+
} else {
|
|
890
|
+
addMiddleware(patternOrMiddleware, middleware, null);
|
|
891
|
+
}
|
|
860
892
|
return router;
|
|
861
893
|
},
|
|
862
894
|
|
|
@@ -957,6 +989,9 @@ export function createRouter<TEnv = any>(
|
|
|
957
989
|
// Expose source file for per-router type generation
|
|
958
990
|
__sourceFile,
|
|
959
991
|
|
|
992
|
+
// Expose basename for runtime manifest generation
|
|
993
|
+
__basename: basename,
|
|
994
|
+
|
|
960
995
|
// RSC request handler (lazily created on first call)
|
|
961
996
|
fetch: (() => {
|
|
962
997
|
// Handler is created on first call and reused
|
|
@@ -998,7 +1033,9 @@ export function createRouter<TEnv = any>(
|
|
|
998
1033
|
RouterRegistry.set(routerId, router);
|
|
999
1034
|
|
|
1000
1035
|
// If urls option was provided, auto-register them
|
|
1001
|
-
if (urlsOption) {
|
|
1036
|
+
if (typeof urlsOption === "function") {
|
|
1037
|
+
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1038
|
+
} else if (urlsOption) {
|
|
1002
1039
|
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1003
1040
|
}
|
|
1004
1041
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
runWithRequestContext,
|
|
15
15
|
setRequestContextParams,
|
|
16
16
|
requireRequestContext,
|
|
17
|
+
getRequestContext,
|
|
17
18
|
createRequestContext,
|
|
18
19
|
} from "../server/request-context.js";
|
|
19
20
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
20
|
-
|
|
21
21
|
import type {
|
|
22
22
|
RscPayload,
|
|
23
23
|
CreateRSCHandlerOptions,
|
|
@@ -452,6 +452,9 @@ export function createRSCHandler<
|
|
|
452
452
|
// - Server components during rendering
|
|
453
453
|
// - Error boundaries
|
|
454
454
|
// - Streaming
|
|
455
|
+
// Store basename on request context (scoped per-request via existing ALS)
|
|
456
|
+
requestContext._basename = router.basename;
|
|
457
|
+
|
|
455
458
|
return runWithRequestContext(requestContext, async () => {
|
|
456
459
|
// Core handler logic (wrapped by middleware)
|
|
457
460
|
const coreHandler = async (): Promise<Response> => {
|
|
@@ -840,7 +843,11 @@ export function createRSCHandler<
|
|
|
840
843
|
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
841
844
|
actionContinuation?: ActionContinuation,
|
|
842
845
|
): Promise<Response> {
|
|
843
|
-
|
|
846
|
+
// App switch detection: if the client's routerId doesn't match this
|
|
847
|
+
// router, downgrade to a full render so the entire tree is replaced.
|
|
848
|
+
const clientRouterId = url.searchParams.get("_rsc_rid");
|
|
849
|
+
const isAppSwitch = !!(clientRouterId && clientRouterId !== router.id);
|
|
850
|
+
const isPartial = url.searchParams.has("_rsc_partial") && !isAppSwitch;
|
|
844
851
|
const isAction =
|
|
845
852
|
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
846
853
|
|
|
@@ -1025,6 +1032,8 @@ export function createRSCHandler<
|
|
|
1025
1032
|
const payload: RscPayload = {
|
|
1026
1033
|
metadata: {
|
|
1027
1034
|
pathname: url.pathname,
|
|
1035
|
+
routerId: router.id,
|
|
1036
|
+
basename: router.basename,
|
|
1028
1037
|
segments: [notFoundSegment],
|
|
1029
1038
|
matched: [],
|
|
1030
1039
|
diff: [],
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
31
|
): Promise<void> {
|
|
32
32
|
const { generateManifestFull } =
|
|
33
33
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
34
|
+
const generated = generateManifestFull(
|
|
35
|
+
router.urlpatterns,
|
|
36
|
+
undefined,
|
|
37
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
|
+
);
|
|
35
39
|
if (
|
|
36
40
|
generated._routeAncestry &&
|
|
37
41
|
Object.keys(generated._routeAncestry).length > 0
|
|
@@ -243,6 +243,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
243
243
|
const payload: RscPayload = {
|
|
244
244
|
metadata: {
|
|
245
245
|
pathname: url.pathname,
|
|
246
|
+
routerId: ctx.router.id,
|
|
247
|
+
basename: ctx.router.basename,
|
|
246
248
|
segments: match.segments,
|
|
247
249
|
matched: match.matched,
|
|
248
250
|
diff: match.diff,
|
|
@@ -342,6 +344,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
342
344
|
const payload: RscPayload = {
|
|
343
345
|
metadata: {
|
|
344
346
|
pathname: url.pathname,
|
|
347
|
+
routerId: ctx.router.id,
|
|
348
|
+
basename: ctx.router.basename,
|
|
345
349
|
segments: errorResult.segments,
|
|
346
350
|
matched: errorResult.matched,
|
|
347
351
|
diff: errorResult.diff,
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -54,6 +54,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
54
54
|
payload = {
|
|
55
55
|
metadata: {
|
|
56
56
|
pathname: url.pathname,
|
|
57
|
+
routerId: ctx.router.id,
|
|
58
|
+
basename: ctx.router.basename,
|
|
57
59
|
segments: match.segments,
|
|
58
60
|
matched: match.matched,
|
|
59
61
|
diff: match.diff,
|
|
@@ -75,6 +77,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
75
77
|
payload = {
|
|
76
78
|
metadata: {
|
|
77
79
|
pathname: url.pathname,
|
|
80
|
+
routerId: ctx.router.id,
|
|
78
81
|
segments: result.segments,
|
|
79
82
|
matched: result.matched,
|
|
80
83
|
diff: result.diff,
|
|
@@ -136,6 +139,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
136
139
|
|
|
137
140
|
metadata: {
|
|
138
141
|
pathname: url.pathname,
|
|
142
|
+
routerId: ctx.router.id,
|
|
143
|
+
basename: ctx.router.basename,
|
|
139
144
|
segments: match.segments,
|
|
140
145
|
matched: match.matched,
|
|
141
146
|
diff: match.diff,
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -208,6 +208,7 @@ export async function executeServerAction<TEnv>(
|
|
|
208
208
|
const payload: RscPayload = {
|
|
209
209
|
metadata: {
|
|
210
210
|
pathname: url.pathname,
|
|
211
|
+
routerId: ctx.router.id,
|
|
211
212
|
segments: errorResult.segments,
|
|
212
213
|
isPartial: true,
|
|
213
214
|
matched: errorResult.matched,
|
|
@@ -314,6 +315,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
314
315
|
const payload: RscPayload = {
|
|
315
316
|
metadata: {
|
|
316
317
|
pathname: url.pathname,
|
|
318
|
+
routerId: ctx.router.id,
|
|
317
319
|
segments: matchResult.segments,
|
|
318
320
|
isPartial: true,
|
|
319
321
|
matched: matchResult.matched,
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function getSSRSetup<TEnv>(
|
|
|
77
77
|
url: URL,
|
|
78
78
|
metricsStore: MetricsStore | undefined,
|
|
79
79
|
): Promise<SSRSetup> {
|
|
80
|
-
const early = _getRequestContext()?.
|
|
80
|
+
const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
|
|
81
81
|
| Promise<SSRSetup>
|
|
82
82
|
| undefined;
|
|
83
83
|
if (early) return early;
|
package/src/rsc/types.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface RscPayload {
|
|
|
19
19
|
metadata?: {
|
|
20
20
|
pathname: string;
|
|
21
21
|
segments: ResolvedSegment[];
|
|
22
|
+
/** Router instance ID. When this changes between navigations, the client
|
|
23
|
+
* discards cached segments and does a full tree replacement (app switch). */
|
|
24
|
+
routerId?: string;
|
|
22
25
|
isPartial?: boolean;
|
|
23
26
|
isError?: boolean;
|
|
24
27
|
matched?: string[];
|
|
@@ -38,6 +41,8 @@ export interface RscPayload {
|
|
|
38
41
|
themeConfig?: ResolvedThemeConfig | null;
|
|
39
42
|
/** Initial theme from cookie (for SSR hydration) */
|
|
40
43
|
initialTheme?: Theme;
|
|
44
|
+
/** URL prefix for all routes (from createRouter({ basename })). */
|
|
45
|
+
basename?: string;
|
|
41
46
|
/** Whether connection warmup is enabled */
|
|
42
47
|
warmupEnabled?: boolean;
|
|
43
48
|
/** Server-side redirect with optional state (for partial requests) */
|
|
@@ -63,7 +68,9 @@ export interface RSCDependencies {
|
|
|
63
68
|
*/
|
|
64
69
|
renderToReadableStream: <T>(
|
|
65
70
|
payload: T,
|
|
66
|
-
options?: {
|
|
71
|
+
options?: {
|
|
72
|
+
temporaryReferences?: unknown;
|
|
73
|
+
},
|
|
67
74
|
) => ReadableStream<Uint8Array>;
|
|
68
75
|
|
|
69
76
|
/**
|