@rangojs/router 0.0.0-experimental.48 → 0.0.0-experimental.4ffa0f9b
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/README.md +76 -18
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +421 -121
- 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/prerender/SKILL.md +110 -68
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/__internal.ts +1 -1
- 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 +27 -5
- 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-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +6 -6
- 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/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +2 -56
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +42 -19
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +79 -23
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +121 -10
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +22 -7
- package/src/router/match-middleware/cache-store.ts +5 -0
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +65 -9
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +65 -7
- package/src/router/types.ts +1 -0
- package/src/router.ts +54 -5
- package/src/rsc/handler.ts +460 -368
- 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 +2 -2
- package/src/rsc/types.ts +8 -1
- package/src/server/context.ts +50 -1
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +134 -14
- package/src/ssr/index.tsx +3 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +37 -19
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +1 -1
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +19 -2
- package/src/vite/router-discovery.ts +153 -34
- package/src/vite/utils/prerender-utils.ts +18 -0
- 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));
|
|
@@ -277,9 +284,14 @@ export async function resolveSegment<TEnv>(
|
|
|
277
284
|
entry.shortCode,
|
|
278
285
|
);
|
|
279
286
|
if (component === undefined) {
|
|
287
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
288
|
+
// the build handler. At build time (context.build === true), always
|
|
289
|
+
// use the build handler from entry.handler.
|
|
290
|
+
const handler =
|
|
291
|
+
!context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
|
|
280
292
|
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
281
293
|
if (entry.loading) {
|
|
282
|
-
const result = handleHandlerResult(
|
|
294
|
+
const result = handleHandlerResult(handler(context));
|
|
283
295
|
if (result instanceof Promise) {
|
|
284
296
|
result.finally(doneRouteHandler).catch(() => {});
|
|
285
297
|
const tracked = deps.trackHandler(result, {
|
|
@@ -300,7 +312,7 @@ export async function resolveSegment<TEnv>(
|
|
|
300
312
|
component = result;
|
|
301
313
|
}
|
|
302
314
|
} else {
|
|
303
|
-
component = handleHandlerResult(await
|
|
315
|
+
component = handleHandlerResult(await handler(context));
|
|
304
316
|
doneRouteHandler();
|
|
305
317
|
}
|
|
306
318
|
}
|
|
@@ -315,6 +327,7 @@ export async function resolveSegment<TEnv>(
|
|
|
315
327
|
deps,
|
|
316
328
|
options,
|
|
317
329
|
routeKey,
|
|
330
|
+
entry,
|
|
318
331
|
);
|
|
319
332
|
segments.push(...orphanSegments);
|
|
320
333
|
}
|
|
@@ -370,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
370
383
|
deps: SegmentResolutionDeps<TEnv>,
|
|
371
384
|
options?: ResolveSegmentOptions,
|
|
372
385
|
routeKey?: string,
|
|
386
|
+
/** Parent route entry — its loaders are inherited by the layout so
|
|
387
|
+
* parallel slots inside this layout can access them via useLoader(). */
|
|
388
|
+
parentRouteEntry?: EntryData,
|
|
373
389
|
): Promise<ResolvedSegment[]> {
|
|
374
390
|
invariant(
|
|
375
391
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -385,6 +401,26 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
385
401
|
deps,
|
|
386
402
|
);
|
|
387
403
|
segments.push(...loaderSegments);
|
|
404
|
+
|
|
405
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
406
|
+
// can access them via useLoader(). Without this, the route's loaders
|
|
407
|
+
// are only in the route's OutletProvider (rendered as <Outlet /> content),
|
|
408
|
+
// which is a child — not a parent — of the layout's context.
|
|
409
|
+
if (
|
|
410
|
+
parentRouteEntry &&
|
|
411
|
+
parentRouteEntry.loader &&
|
|
412
|
+
parentRouteEntry.loader.length > 0 &&
|
|
413
|
+
Object.keys(orphan.parallel).length > 0
|
|
414
|
+
) {
|
|
415
|
+
const inheritedLoaders = await resolveLoaders(
|
|
416
|
+
parentRouteEntry,
|
|
417
|
+
context,
|
|
418
|
+
belongsToRoute,
|
|
419
|
+
deps,
|
|
420
|
+
orphan.shortCode,
|
|
421
|
+
);
|
|
422
|
+
segments.push(...inheritedLoaders);
|
|
423
|
+
}
|
|
388
424
|
}
|
|
389
425
|
|
|
390
426
|
// Handler-first: orphan layout handler executes before its parallels
|
|
@@ -580,6 +616,13 @@ export async function resolveAllSegments<TEnv>(
|
|
|
580
616
|
} catch {}
|
|
581
617
|
|
|
582
618
|
for (const entry of entries) {
|
|
619
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
620
|
+
// can guard non-cacheable variable reads. Also guards response-level
|
|
621
|
+
// side effects (headers.set). Persists for all descendant entries.
|
|
622
|
+
if (entry.type === "cache") {
|
|
623
|
+
const store = RSCRouterContext.getStore();
|
|
624
|
+
if (store) store.insideCacheScope = true;
|
|
625
|
+
}
|
|
583
626
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
584
627
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
585
628
|
entry,
|
|
@@ -666,6 +709,19 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
666
709
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
667
710
|
for (const layoutEntry of entry.layout) {
|
|
668
711
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
712
|
+
// Inherit route loaders for orphan layouts with parallels
|
|
713
|
+
if (
|
|
714
|
+
entry.type === "route" &&
|
|
715
|
+
entry.loader &&
|
|
716
|
+
entry.loader.length > 0 &&
|
|
717
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
718
|
+
) {
|
|
719
|
+
await collectEntryLoaders(
|
|
720
|
+
entry,
|
|
721
|
+
childBelongsToRoute,
|
|
722
|
+
layoutEntry.shortCode,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
669
725
|
}
|
|
670
726
|
}
|
|
671
727
|
|
|
@@ -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,
|
|
@@ -313,6 +319,19 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
313
319
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
314
320
|
for (const layoutEntry of entry.layout) {
|
|
315
321
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
322
|
+
// Inherit route loaders for orphan layouts with parallels
|
|
323
|
+
if (
|
|
324
|
+
entry.type === "route" &&
|
|
325
|
+
entry.loader &&
|
|
326
|
+
entry.loader.length > 0 &&
|
|
327
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
328
|
+
) {
|
|
329
|
+
await collectEntryLoaders(
|
|
330
|
+
entry,
|
|
331
|
+
childBelongsToRoute,
|
|
332
|
+
layoutEntry.shortCode,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
316
335
|
}
|
|
317
336
|
}
|
|
318
337
|
|
|
@@ -682,13 +701,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
682
701
|
return staticComponent;
|
|
683
702
|
}
|
|
684
703
|
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
704
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
705
|
+
// the build handler. At build time (context.build === true), always
|
|
706
|
+
// use the build handler from routeEntry.handler.
|
|
707
|
+
const handler =
|
|
708
|
+
!context.build && routeEntry.liveHandler
|
|
709
|
+
? routeEntry.liveHandler
|
|
710
|
+
: routeEntry.handler;
|
|
685
711
|
if (!routeEntry.loading) {
|
|
686
|
-
const result = handleHandlerResult(await
|
|
712
|
+
const result = handleHandlerResult(await handler(context));
|
|
687
713
|
doneHandler();
|
|
688
714
|
return result;
|
|
689
715
|
}
|
|
690
716
|
if (!actionContext) {
|
|
691
|
-
const result = handleHandlerResult(
|
|
717
|
+
const result = handleHandlerResult(handler(context));
|
|
692
718
|
if (result instanceof Promise) {
|
|
693
719
|
result.finally(doneHandler).catch(() => {});
|
|
694
720
|
const tracked = deps.trackHandler(result, {
|
|
@@ -711,9 +737,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
711
737
|
debugLog("segment.action", "resolving action route with awaited value", {
|
|
712
738
|
entryId: entry.id,
|
|
713
739
|
});
|
|
714
|
-
const actionResult = handleHandlerResult(
|
|
715
|
-
await routeEntry.handler(context),
|
|
716
|
-
);
|
|
740
|
+
const actionResult = handleHandlerResult(await handler(context));
|
|
717
741
|
doneHandler();
|
|
718
742
|
return {
|
|
719
743
|
content: Promise.resolve(actionResult),
|
|
@@ -829,6 +853,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
829
853
|
deps,
|
|
830
854
|
actionContext,
|
|
831
855
|
stale,
|
|
856
|
+
entry,
|
|
832
857
|
);
|
|
833
858
|
segments.push(...orphanResult.segments);
|
|
834
859
|
matchedIds.push(...orphanResult.matchedIds);
|
|
@@ -940,6 +965,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
940
965
|
deps: SegmentResolutionDeps<TEnv>,
|
|
941
966
|
actionContext?: ActionContext,
|
|
942
967
|
stale?: boolean,
|
|
968
|
+
/** Parent route entry — its loaders are inherited so parallel slots can access them. */
|
|
969
|
+
parentRouteEntry?: EntryData,
|
|
943
970
|
): Promise<SegmentRevalidationResult> {
|
|
944
971
|
invariant(
|
|
945
972
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -967,6 +994,33 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
967
994
|
segments.push(...loaderResult.segments);
|
|
968
995
|
matchedIds.push(...loaderResult.matchedIds);
|
|
969
996
|
|
|
997
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
998
|
+
// can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
|
|
999
|
+
if (
|
|
1000
|
+
parentRouteEntry &&
|
|
1001
|
+
parentRouteEntry.loader &&
|
|
1002
|
+
parentRouteEntry.loader.length > 0 &&
|
|
1003
|
+
Object.keys(orphan.parallel).length > 0
|
|
1004
|
+
) {
|
|
1005
|
+
const inheritedResult = await resolveLoadersWithRevalidation(
|
|
1006
|
+
parentRouteEntry,
|
|
1007
|
+
context,
|
|
1008
|
+
belongsToRoute,
|
|
1009
|
+
clientSegmentIds,
|
|
1010
|
+
prevParams,
|
|
1011
|
+
request,
|
|
1012
|
+
prevUrl,
|
|
1013
|
+
nextUrl,
|
|
1014
|
+
routeKey,
|
|
1015
|
+
deps,
|
|
1016
|
+
actionContext,
|
|
1017
|
+
orphan.shortCode,
|
|
1018
|
+
stale,
|
|
1019
|
+
);
|
|
1020
|
+
segments.push(...inheritedResult.segments);
|
|
1021
|
+
matchedIds.push(...inheritedResult.matchedIds);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
970
1024
|
// Handler-first: resolve orphan layout handler before its parallels
|
|
971
1025
|
// so ctx.set() values are visible to parallel children.
|
|
972
1026
|
matchedIds.push(orphan.shortCode);
|
|
@@ -1248,6 +1302,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1248
1302
|
}
|
|
1249
1303
|
|
|
1250
1304
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1305
|
+
if (entry.type === "cache") {
|
|
1306
|
+
const store = RSCRouterContext.getStore();
|
|
1307
|
+
if (store) store.insideCacheScope = true;
|
|
1308
|
+
}
|
|
1251
1309
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1252
1310
|
const resolved = await resolveWithErrorBoundary(
|
|
1253
1311
|
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
|
|
|
@@ -614,6 +625,8 @@ export function createRouter<TEnv = any>(
|
|
|
614
625
|
params: Record<string, string>,
|
|
615
626
|
buildVars?: Record<string, any>,
|
|
616
627
|
isPassthroughRoute?: boolean,
|
|
628
|
+
buildEnv?: TEnv,
|
|
629
|
+
devMode?: boolean,
|
|
617
630
|
) {
|
|
618
631
|
return _matchForPrerender(
|
|
619
632
|
pathname,
|
|
@@ -621,6 +634,8 @@ export function createRouter<TEnv = any>(
|
|
|
621
634
|
prerenderDeps,
|
|
622
635
|
buildVars,
|
|
623
636
|
isPassthroughRoute,
|
|
637
|
+
buildEnv,
|
|
638
|
+
devMode,
|
|
624
639
|
);
|
|
625
640
|
}
|
|
626
641
|
|
|
@@ -628,12 +643,16 @@ export function createRouter<TEnv = any>(
|
|
|
628
643
|
handler: Function,
|
|
629
644
|
handlerId: string,
|
|
630
645
|
routeName?: string,
|
|
646
|
+
buildEnv?: TEnv,
|
|
647
|
+
devMode?: boolean,
|
|
631
648
|
) {
|
|
632
649
|
return _renderStaticSegment<TEnv>(
|
|
633
650
|
handler,
|
|
634
651
|
handlerId,
|
|
635
652
|
mergedRouteMap,
|
|
636
653
|
routeName,
|
|
654
|
+
buildEnv,
|
|
655
|
+
devMode,
|
|
637
656
|
);
|
|
638
657
|
}
|
|
639
658
|
|
|
@@ -658,8 +677,15 @@ export function createRouter<TEnv = any>(
|
|
|
658
677
|
const router: RSCRouterInternal<TEnv, {}> = {
|
|
659
678
|
__brand: RSC_ROUTER_BRAND,
|
|
660
679
|
id: routerId,
|
|
680
|
+
basename,
|
|
681
|
+
|
|
682
|
+
routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
|
|
683
|
+
// Wrap builder functions in urls() automatically
|
|
684
|
+
const urlPatterns: UrlPatterns<TEnv> =
|
|
685
|
+
typeof patternsOrBuilder === "function"
|
|
686
|
+
? (urls(patternsOrBuilder) as UrlPatterns<TEnv>)
|
|
687
|
+
: patternsOrBuilder;
|
|
661
688
|
|
|
662
|
-
routes(urlPatterns: UrlPatterns<TEnv>): any {
|
|
663
689
|
// Store reference for runtime manifest generation
|
|
664
690
|
storedUrlPatterns = urlPatterns;
|
|
665
691
|
const currentMountIndex = mountIndex++;
|
|
@@ -707,6 +733,10 @@ export function createRouter<TEnv = any>(
|
|
|
707
733
|
counters: {},
|
|
708
734
|
mountIndex: currentMountIndex,
|
|
709
735
|
cacheProfiles: resolvedCacheProfiles,
|
|
736
|
+
// basename sets the initial URL prefix so all path() patterns
|
|
737
|
+
// are registered with the prefix (e.g. "/admin" + "/users" = "/admin/users").
|
|
738
|
+
// No namePrefix — route names stay unprefixed.
|
|
739
|
+
...(basename ? { urlPrefix: basename } : {}),
|
|
710
740
|
},
|
|
711
741
|
() => {
|
|
712
742
|
handlerResult = urlPatterns.handler() as AllUseItems[];
|
|
@@ -726,7 +756,7 @@ export function createRouter<TEnv = any>(
|
|
|
726
756
|
if (entry.type === "route" && entry.isPrerender) {
|
|
727
757
|
if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
|
|
728
758
|
prerenderRouteKeys.add(name);
|
|
729
|
-
if (entry.
|
|
759
|
+
if (entry.isPassthrough === true) {
|
|
730
760
|
if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
|
|
731
761
|
passthroughRouteKeys.add(name);
|
|
732
762
|
}
|
|
@@ -855,8 +885,18 @@ export function createRouter<TEnv = any>(
|
|
|
855
885
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
856
886
|
middleware?: MiddlewareFn<TEnv>,
|
|
857
887
|
): any {
|
|
858
|
-
//
|
|
859
|
-
|
|
888
|
+
// Auto-prefix pattern with basename so router-level middleware
|
|
889
|
+
// patterns are router-relative (e.g. "/users/*" matches "/app/users/*").
|
|
890
|
+
if (basename && typeof patternOrMiddleware === "string") {
|
|
891
|
+
const pattern = patternOrMiddleware;
|
|
892
|
+
const prefixed =
|
|
893
|
+
pattern === "/*" || pattern === "*"
|
|
894
|
+
? `${basename}/*`
|
|
895
|
+
: `${basename}${pattern}`;
|
|
896
|
+
addMiddleware(prefixed, middleware, null);
|
|
897
|
+
} else {
|
|
898
|
+
addMiddleware(patternOrMiddleware, middleware, null);
|
|
899
|
+
}
|
|
860
900
|
return router;
|
|
861
901
|
},
|
|
862
902
|
|
|
@@ -957,6 +997,9 @@ export function createRouter<TEnv = any>(
|
|
|
957
997
|
// Expose source file for per-router type generation
|
|
958
998
|
__sourceFile,
|
|
959
999
|
|
|
1000
|
+
// Expose basename for runtime manifest generation
|
|
1001
|
+
__basename: basename,
|
|
1002
|
+
|
|
960
1003
|
// RSC request handler (lazily created on first call)
|
|
961
1004
|
fetch: (() => {
|
|
962
1005
|
// Handler is created on first call and reused
|
|
@@ -990,6 +1033,10 @@ export function createRouter<TEnv = any>(
|
|
|
990
1033
|
};
|
|
991
1034
|
})(),
|
|
992
1035
|
|
|
1036
|
+
// Low-level route matching for request classification
|
|
1037
|
+
findMatch: (pathname: string, metricsStore?: any) =>
|
|
1038
|
+
findMatch(pathname, metricsStore),
|
|
1039
|
+
|
|
993
1040
|
// Debug utility for manifest inspection
|
|
994
1041
|
debugManifest: () => buildDebugManifest<TEnv>(routesEntries),
|
|
995
1042
|
};
|
|
@@ -998,7 +1045,9 @@ export function createRouter<TEnv = any>(
|
|
|
998
1045
|
RouterRegistry.set(routerId, router);
|
|
999
1046
|
|
|
1000
1047
|
// If urls option was provided, auto-register them
|
|
1001
|
-
if (urlsOption) {
|
|
1048
|
+
if (typeof urlsOption === "function") {
|
|
1049
|
+
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1050
|
+
} else if (urlsOption) {
|
|
1002
1051
|
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1003
1052
|
}
|
|
1004
1053
|
|