@rangojs/router 0.0.0-experimental.56cb65a7 → 0.0.0-experimental.57005a2b
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 +716 -237
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +55 -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 +37 -5
- package/src/browser/navigation-client.ts +98 -46
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +51 -6
- package/src/browser/prefetch/cache.ts +16 -6
- package/src/browser/prefetch/fetch.ts +68 -6
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +67 -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/segment-reconciler.ts +36 -14
- 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-trie.ts +50 -24
- 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-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +84 -230
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +46 -6
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +25 -1
- package/src/route-definition/dsl-helpers.ts +194 -32
- package/src/route-definition/helpers-types.ts +67 -19
- 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 +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +82 -23
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +5 -5
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/manifest.ts +12 -7
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +74 -14
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +93 -9
- 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-context.ts +1 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +106 -18
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +129 -27
- package/src/router/types.ts +1 -0
- package/src/router.ts +54 -5
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +10 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/context.ts +76 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +175 -15
- 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 +137 -33
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- 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 +77 -5
- 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 +128 -74
- 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/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- 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 +178 -37
- package/src/vite/utils/prerender-utils.ts +37 -5
- 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
|
}
|
|
@@ -344,7 +357,7 @@ export async function resolveSegment<TEnv>(
|
|
|
344
357
|
namespace: entry.id,
|
|
345
358
|
type: "route",
|
|
346
359
|
index: 0,
|
|
347
|
-
component,
|
|
360
|
+
component: component ?? null,
|
|
348
361
|
loading: entry.loading === false ? null : entry.loading,
|
|
349
362
|
transition: entry.transition,
|
|
350
363
|
params,
|
|
@@ -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,30 @@ 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
|
+
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
423
|
+
for (const s of inheritedLoaders) {
|
|
424
|
+
s._inherited = true;
|
|
425
|
+
}
|
|
426
|
+
segments.push(...inheritedLoaders);
|
|
427
|
+
}
|
|
388
428
|
}
|
|
389
429
|
|
|
390
430
|
// Handler-first: orphan layout handler executes before its parallels
|
|
@@ -580,6 +620,13 @@ export async function resolveAllSegments<TEnv>(
|
|
|
580
620
|
} catch {}
|
|
581
621
|
|
|
582
622
|
for (const entry of entries) {
|
|
623
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
624
|
+
// can guard non-cacheable variable reads. Also guards response-level
|
|
625
|
+
// side effects (headers.set). Persists for all descendant entries.
|
|
626
|
+
if (entry.type === "cache") {
|
|
627
|
+
const store = RSCRouterContext.getStore();
|
|
628
|
+
if (store) store.insideCacheScope = true;
|
|
629
|
+
}
|
|
583
630
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
584
631
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
585
632
|
entry,
|
|
@@ -624,20 +671,37 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
624
671
|
deps: SegmentResolutionDeps<TEnv>,
|
|
625
672
|
): Promise<ResolvedSegment[]> {
|
|
626
673
|
const loaderSegments: ResolvedSegment[] = [];
|
|
674
|
+
const seenIds = new Set<string>();
|
|
627
675
|
|
|
628
676
|
async function collectEntryLoaders(
|
|
629
677
|
entry: EntryData,
|
|
630
678
|
belongsToRoute: boolean,
|
|
631
679
|
shortCodeOverride?: string,
|
|
632
680
|
): Promise<void> {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
681
|
+
// Skip if all loaders from this entry have already been resolved
|
|
682
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
683
|
+
const entryLoaders = entry.loader ?? [];
|
|
684
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
685
|
+
const allAlreadySeen =
|
|
686
|
+
entryLoaders.length > 0 &&
|
|
687
|
+
entryLoaders.every((le, i) =>
|
|
688
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
689
|
+
);
|
|
690
|
+
if (!allAlreadySeen) {
|
|
691
|
+
const segments = await resolveLoaders(
|
|
692
|
+
entry,
|
|
693
|
+
context,
|
|
694
|
+
belongsToRoute,
|
|
695
|
+
deps,
|
|
696
|
+
shortCodeOverride,
|
|
697
|
+
);
|
|
698
|
+
for (const seg of segments) {
|
|
699
|
+
if (!seenIds.has(seg.id)) {
|
|
700
|
+
seenIds.add(seg.id);
|
|
701
|
+
loaderSegments.push(seg);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
641
705
|
|
|
642
706
|
const seenParallelEntryIds = new Set<string>();
|
|
643
707
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -649,6 +713,30 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
649
713
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
650
714
|
for (const layoutEntry of entry.layout) {
|
|
651
715
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
716
|
+
// Inherit route loaders for orphan layouts with parallels.
|
|
717
|
+
// Resolve directly — do NOT re-enter collectEntryLoaders with the
|
|
718
|
+
// route entry, as that would re-iterate route.layout and loop.
|
|
719
|
+
if (
|
|
720
|
+
entry.type === "route" &&
|
|
721
|
+
entry.loader &&
|
|
722
|
+
entry.loader.length > 0 &&
|
|
723
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
724
|
+
) {
|
|
725
|
+
const inherited = await resolveLoaders(
|
|
726
|
+
entry,
|
|
727
|
+
context,
|
|
728
|
+
childBelongsToRoute,
|
|
729
|
+
deps,
|
|
730
|
+
layoutEntry.shortCode,
|
|
731
|
+
);
|
|
732
|
+
for (const seg of inherited) {
|
|
733
|
+
if (!seenIds.has(seg.id)) {
|
|
734
|
+
seenIds.add(seg.id);
|
|
735
|
+
seg._inherited = true;
|
|
736
|
+
loaderSegments.push(seg);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
652
740
|
}
|
|
653
741
|
}
|
|
654
742
|
|
|
@@ -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)) {
|
|
@@ -296,6 +319,39 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
296
319
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
297
320
|
for (const layoutEntry of entry.layout) {
|
|
298
321
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
322
|
+
// Inherit route loaders for orphan layouts with parallels.
|
|
323
|
+
// Resolve directly — do NOT re-enter collectEntryLoaders with the
|
|
324
|
+
// route entry, as that would re-iterate route.layout and loop.
|
|
325
|
+
if (
|
|
326
|
+
entry.type === "route" &&
|
|
327
|
+
entry.loader &&
|
|
328
|
+
entry.loader.length > 0 &&
|
|
329
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
330
|
+
) {
|
|
331
|
+
const inherited = await resolveLoadersWithRevalidation(
|
|
332
|
+
entry,
|
|
333
|
+
context,
|
|
334
|
+
childBelongsToRoute,
|
|
335
|
+
clientSegmentIds,
|
|
336
|
+
prevParams,
|
|
337
|
+
request,
|
|
338
|
+
prevUrl,
|
|
339
|
+
nextUrl,
|
|
340
|
+
routeKey,
|
|
341
|
+
deps,
|
|
342
|
+
actionContext,
|
|
343
|
+
layoutEntry.shortCode,
|
|
344
|
+
stale,
|
|
345
|
+
);
|
|
346
|
+
for (const seg of inherited.segments) {
|
|
347
|
+
if (!seenIds.has(seg.id)) {
|
|
348
|
+
seenIds.add(seg.id);
|
|
349
|
+
seg._inherited = true;
|
|
350
|
+
allLoaderSegments.push(seg);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
allMatchedIds.push(...inherited.matchedIds);
|
|
354
|
+
}
|
|
299
355
|
}
|
|
300
356
|
}
|
|
301
357
|
|
|
@@ -665,13 +721,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
665
721
|
return staticComponent;
|
|
666
722
|
}
|
|
667
723
|
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
724
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
725
|
+
// the build handler. At build time (context.build === true), always
|
|
726
|
+
// use the build handler from routeEntry.handler.
|
|
727
|
+
const handler =
|
|
728
|
+
!context.build && routeEntry.liveHandler
|
|
729
|
+
? routeEntry.liveHandler
|
|
730
|
+
: routeEntry.handler;
|
|
668
731
|
if (!routeEntry.loading) {
|
|
669
|
-
const result = handleHandlerResult(await
|
|
732
|
+
const result = handleHandlerResult(await handler(context));
|
|
670
733
|
doneHandler();
|
|
671
734
|
return result;
|
|
672
735
|
}
|
|
673
736
|
if (!actionContext) {
|
|
674
|
-
const result = handleHandlerResult(
|
|
737
|
+
const result = handleHandlerResult(handler(context));
|
|
675
738
|
if (result instanceof Promise) {
|
|
676
739
|
result.finally(doneHandler).catch(() => {});
|
|
677
740
|
const tracked = deps.trackHandler(result, {
|
|
@@ -694,9 +757,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
694
757
|
debugLog("segment.action", "resolving action route with awaited value", {
|
|
695
758
|
entryId: entry.id,
|
|
696
759
|
});
|
|
697
|
-
const actionResult = handleHandlerResult(
|
|
698
|
-
await routeEntry.handler(context),
|
|
699
|
-
);
|
|
760
|
+
const actionResult = handleHandlerResult(await handler(context));
|
|
700
761
|
doneHandler();
|
|
701
762
|
return {
|
|
702
763
|
content: Promise.resolve(actionResult),
|
|
@@ -705,10 +766,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
705
766
|
() => null,
|
|
706
767
|
);
|
|
707
768
|
|
|
769
|
+
// Normalize void handlers (undefined) to null so the reconciler's
|
|
770
|
+
// component === null checks work consistently for both void and explicit null.
|
|
708
771
|
const resolvedComponent =
|
|
709
772
|
component && typeof component === "object" && "content" in component
|
|
710
|
-
? (component as { content: ReactNode }).content
|
|
711
|
-
: component;
|
|
773
|
+
? ((component as { content: ReactNode }).content ?? null)
|
|
774
|
+
: (component ?? null);
|
|
712
775
|
|
|
713
776
|
const segment: ResolvedSegment = {
|
|
714
777
|
id: entry.shortCode,
|
|
@@ -810,6 +873,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
810
873
|
deps,
|
|
811
874
|
actionContext,
|
|
812
875
|
stale,
|
|
876
|
+
entry,
|
|
813
877
|
);
|
|
814
878
|
segments.push(...orphanResult.segments);
|
|
815
879
|
matchedIds.push(...orphanResult.matchedIds);
|
|
@@ -921,6 +985,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
921
985
|
deps: SegmentResolutionDeps<TEnv>,
|
|
922
986
|
actionContext?: ActionContext,
|
|
923
987
|
stale?: boolean,
|
|
988
|
+
/** Parent route entry — its loaders are inherited so parallel slots can access them. */
|
|
989
|
+
parentRouteEntry?: EntryData,
|
|
924
990
|
): Promise<SegmentRevalidationResult> {
|
|
925
991
|
invariant(
|
|
926
992
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -948,6 +1014,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
948
1014
|
segments.push(...loaderResult.segments);
|
|
949
1015
|
matchedIds.push(...loaderResult.matchedIds);
|
|
950
1016
|
|
|
1017
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
1018
|
+
// can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
|
|
1019
|
+
if (
|
|
1020
|
+
parentRouteEntry &&
|
|
1021
|
+
parentRouteEntry.loader &&
|
|
1022
|
+
parentRouteEntry.loader.length > 0 &&
|
|
1023
|
+
Object.keys(orphan.parallel).length > 0
|
|
1024
|
+
) {
|
|
1025
|
+
const inheritedResult = await resolveLoadersWithRevalidation(
|
|
1026
|
+
parentRouteEntry,
|
|
1027
|
+
context,
|
|
1028
|
+
belongsToRoute,
|
|
1029
|
+
clientSegmentIds,
|
|
1030
|
+
prevParams,
|
|
1031
|
+
request,
|
|
1032
|
+
prevUrl,
|
|
1033
|
+
nextUrl,
|
|
1034
|
+
routeKey,
|
|
1035
|
+
deps,
|
|
1036
|
+
actionContext,
|
|
1037
|
+
orphan.shortCode,
|
|
1038
|
+
stale,
|
|
1039
|
+
);
|
|
1040
|
+
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
1041
|
+
for (const s of inheritedResult.segments) {
|
|
1042
|
+
s._inherited = true;
|
|
1043
|
+
}
|
|
1044
|
+
segments.push(...inheritedResult.segments);
|
|
1045
|
+
matchedIds.push(...inheritedResult.matchedIds);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
951
1048
|
// Handler-first: resolve orphan layout handler before its parallels
|
|
952
1049
|
// so ctx.set() values are visible to parallel children.
|
|
953
1050
|
matchedIds.push(orphan.shortCode);
|
|
@@ -1034,6 +1131,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1034
1131
|
);
|
|
1035
1132
|
|
|
1036
1133
|
if (!resolvedParallelEntries.has(parallelEntry.id)) {
|
|
1134
|
+
// shortCodeOverride must match the parent layout, not the parallel entry.
|
|
1037
1135
|
const loaderResult = await resolveLoadersWithRevalidation(
|
|
1038
1136
|
parallelEntry,
|
|
1039
1137
|
context,
|
|
@@ -1046,7 +1144,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1046
1144
|
routeKey,
|
|
1047
1145
|
deps,
|
|
1048
1146
|
actionContext,
|
|
1049
|
-
|
|
1147
|
+
orphan.shortCode,
|
|
1050
1148
|
stale,
|
|
1051
1149
|
);
|
|
1052
1150
|
segments.push(...loaderResult.segments);
|
|
@@ -1229,6 +1327,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1229
1327
|
}
|
|
1230
1328
|
|
|
1231
1329
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1330
|
+
if (entry.type === "cache") {
|
|
1331
|
+
const store = RSCRouterContext.getStore();
|
|
1332
|
+
if (store) store.insideCacheScope = true;
|
|
1333
|
+
}
|
|
1232
1334
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1233
1335
|
const resolved = await resolveWithErrorBoundary(
|
|
1234
1336
|
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
|
|