@rangojs/router 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -44,7 +44,11 @@ import type {
|
|
|
44
44
|
UseItems,
|
|
45
45
|
} from "../route-types.js";
|
|
46
46
|
import type { RouteHelpers } from "./helpers-types.js";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
resolveHandlerUse,
|
|
49
|
+
mergeHandlerUse,
|
|
50
|
+
validateInterceptUseItems,
|
|
51
|
+
} from "./resolve-handler-use.js";
|
|
48
52
|
import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -131,16 +135,37 @@ const isOrphan = (result: AllUseItems[]): boolean =>
|
|
|
131
135
|
* pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
|
|
132
136
|
* docs/tree-structure.md) and push it onto the parent's layout[] so it renders
|
|
133
137
|
* as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
|
|
134
|
-
* validation and registers inline.
|
|
138
|
+
* validation and registers inline. `orphanOwner` keeps the owner reachable for
|
|
139
|
+
* boundary lookup only (router/error-handling.ts).
|
|
135
140
|
*/
|
|
136
141
|
const attachOrphanSibling = (
|
|
137
142
|
parent: EntryData | null,
|
|
138
143
|
entry: EntryData,
|
|
139
144
|
): void => {
|
|
140
145
|
entry.parent = null;
|
|
141
|
-
if (parent && "layout" in parent)
|
|
146
|
+
if (parent && "layout" in parent) {
|
|
147
|
+
parent.layout.push(entry);
|
|
148
|
+
entry.orphanOwner = parent;
|
|
149
|
+
}
|
|
142
150
|
};
|
|
143
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The route whose use() list `parent` sits in: `parent` itself, or the route
|
|
154
|
+
* above a chain of layout-typed wrappers (layout(), middleware()/transition()
|
|
155
|
+
* wrapper form). Inside a path those wrappers never hold routes (path-in-path
|
|
156
|
+
* is rejected), so the route is the only cacheable unit there. An intercept()
|
|
157
|
+
* scope (its temporary parent: a spread of the declaring entry with an own
|
|
158
|
+
* `cache` sink) stops the walk, so its cache() stays rejected by
|
|
159
|
+
* validateInterceptUseItems instead of configuring the path around it.
|
|
160
|
+
*/
|
|
161
|
+
function enclosingRoute(parent: EntryData | null): EntryData | undefined {
|
|
162
|
+
for (let entry = parent; entry; entry = entry.parent) {
|
|
163
|
+
if (entry.type === "route") return entry;
|
|
164
|
+
if (entry.type !== "layout" || "cache" in entry) return undefined;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
144
169
|
/**
|
|
145
170
|
* Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
|
|
146
171
|
* entry that captures the attachments declared by a use() callback — restoring
|
|
@@ -179,7 +204,7 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
|
179
204
|
* When an error occurs during rendering of this segment or its children,
|
|
180
205
|
* the fallback will be rendered instead. The fallback can be:
|
|
181
206
|
* - A static ReactNode (e.g., <ErrorPage />)
|
|
182
|
-
* - A handler function that receives error
|
|
207
|
+
* - A handler function that receives `{ error }` (server fallbacks get no reset)
|
|
183
208
|
*
|
|
184
209
|
* Error boundaries catch errors from:
|
|
185
210
|
* - Middleware execution
|
|
@@ -195,11 +220,11 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
|
195
220
|
*
|
|
196
221
|
* // Or with handler for dynamic error UI:
|
|
197
222
|
* route("products.detail", ProductDetail, () => [
|
|
198
|
-
* errorBoundary(({ error
|
|
223
|
+
* errorBoundary(({ error }) => (
|
|
199
224
|
* <div>
|
|
200
225
|
* <h2>Product failed to load</h2>
|
|
201
226
|
* <p>{error.message}</p>
|
|
202
|
-
* <
|
|
227
|
+
* <a href="/products">Back to products</a>
|
|
203
228
|
* </div>
|
|
204
229
|
* )),
|
|
205
230
|
* ])
|
|
@@ -317,6 +342,30 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
317
342
|
const name = `$${cacheIndex}`;
|
|
318
343
|
const cacheConfig = { options };
|
|
319
344
|
|
|
345
|
+
// Among a path's children, cache() configures that path: the route entry
|
|
346
|
+
// carries the config, so buildEntriesAndCacheScope (router/route-snapshot.ts)
|
|
347
|
+
// makes the route the boundary and its own segments (handler, layouts,
|
|
348
|
+
// parallels) the cached unit. Both forms leave ctx.parent unchanged, so
|
|
349
|
+
// every sibling and wrapped item still attaches where it was. Issue #912: a
|
|
350
|
+
// cache entry here was never an ancestor of the route (no scope, no store
|
|
351
|
+
// write). The same holds inside a routeless layout()/middleware()/
|
|
352
|
+
// transition() wrapper in a path (issue #918): a route's record is
|
|
353
|
+
// all-or-nothing, so the path is the only thing such a cache() can cache.
|
|
354
|
+
const route = enclosingRoute(ctx.parent);
|
|
355
|
+
if (route && ctx.parent) {
|
|
356
|
+
route.cache = cacheConfig;
|
|
357
|
+
if (!children) return { name, type: "cache" } as CacheItem;
|
|
358
|
+
const uses = runAndValidateUseItems(
|
|
359
|
+
store,
|
|
360
|
+
`${ctx.namespace}.${cacheIndex}`,
|
|
361
|
+
ctx.parent,
|
|
362
|
+
children,
|
|
363
|
+
"cache",
|
|
364
|
+
"children",
|
|
365
|
+
);
|
|
366
|
+
return { name, type: "cache", uses } as CacheItem;
|
|
367
|
+
}
|
|
368
|
+
|
|
320
369
|
// If no children, create an orphan cache entry (like orphan layouts)
|
|
321
370
|
// This allows cache() to wrap subsequent siblings
|
|
322
371
|
if (!children) {
|
|
@@ -491,7 +540,19 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
|
491
540
|
"children",
|
|
492
541
|
);
|
|
493
542
|
|
|
494
|
-
if (isOrphan(result))
|
|
543
|
+
if (isOrphan(result)) {
|
|
544
|
+
// Its middleware runs for every route of the enclosing entry
|
|
545
|
+
// (collectRouteMiddleware walks orphans), so the wrapper scopes nothing;
|
|
546
|
+
// the flat form says what runs. Issue #918: the nested layout() was
|
|
547
|
+
// dropped at render.
|
|
548
|
+
invariant(
|
|
549
|
+
!result.some((item) => item?.type === "layout"),
|
|
550
|
+
`middleware(fn, () => [...]) with no routes inside cannot contain layout() [${namespace}]. ` +
|
|
551
|
+
"The middleware runs for every route of the enclosing path or layout either way; " +
|
|
552
|
+
"list them as siblings instead: middleware(fn), layout(...)",
|
|
553
|
+
);
|
|
554
|
+
attachOrphanSibling(ctx.parent, entry);
|
|
555
|
+
}
|
|
495
556
|
|
|
496
557
|
return {
|
|
497
558
|
name: namespace,
|
|
@@ -714,15 +775,12 @@ const intercept = (
|
|
|
714
775
|
const prefixedRouteName =
|
|
715
776
|
isLocal && namePrefix ? `${namePrefix}.${bareRouteName}` : bareRouteName;
|
|
716
777
|
|
|
717
|
-
// Create intercept entry with its own loaders/
|
|
778
|
+
// Create intercept entry with its own loaders/middleware/when
|
|
718
779
|
const entry: InterceptEntry = {
|
|
719
780
|
slotName: slotName as `@${string}`,
|
|
720
781
|
routeName: prefixedRouteName,
|
|
721
782
|
handler,
|
|
722
783
|
middleware: [],
|
|
723
|
-
revalidate: [],
|
|
724
|
-
errorBoundary: [],
|
|
725
|
-
notFoundBoundary: [],
|
|
726
784
|
loader: [],
|
|
727
785
|
when: [], // Selector conditions for conditional interception
|
|
728
786
|
};
|
|
@@ -735,25 +793,44 @@ const intercept = (
|
|
|
735
793
|
entry.when.push(...selectors);
|
|
736
794
|
}
|
|
737
795
|
|
|
738
|
-
// Merge handler.use defaults with explicit use
|
|
796
|
+
// Merge handler.use defaults with explicit use. mountSite null: the merged
|
|
797
|
+
// items are validated once below, so both sources get the same message.
|
|
739
798
|
const handlerUseFn = resolveHandlerUse(handler);
|
|
740
|
-
const mergedUse = mergeHandlerUse(handlerUseFn, useFn,
|
|
799
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, useFn, null);
|
|
741
800
|
|
|
742
|
-
// Run merged use callback to collect loaders,
|
|
801
|
+
// Run merged use callback to collect loaders, middleware, etc.
|
|
743
802
|
if (mergedUse) {
|
|
744
803
|
// Capture layout() calls into a temporary array
|
|
745
804
|
const capturedLayouts: EntryData[] = [];
|
|
746
805
|
|
|
747
|
-
// Temporary parent so middleware/loader
|
|
748
|
-
//
|
|
806
|
+
// Temporary parent so middleware/loader attach to the intercept entry;
|
|
807
|
+
// the loading get/set accessor mirrors writes onto `entry`. It is a
|
|
808
|
+
// shallow spread of the enclosing entry, so every field a rejected helper
|
|
809
|
+
// writes to points at a throwaway here: a helper called but not returned
|
|
810
|
+
// from use() never lands on the enclosing layout. cache() writes into
|
|
811
|
+
// `layout` (capturedLayouts), or into `cache` when the enclosing entry
|
|
812
|
+
// is a route.
|
|
813
|
+
//
|
|
814
|
+
// Why an intercept rejects these (validateInterceptUseItems):
|
|
815
|
+
// - revalidate(): an intercept only evaluates its loaders' revalidate()
|
|
816
|
+
// (router/intercept-resolution.ts).
|
|
817
|
+
// - errorBoundary()/notFoundBoundary(): error lookup walks EntryData
|
|
818
|
+
// parent chains (router/error-handling.ts), which never include an
|
|
819
|
+
// InterceptEntry; intercept loader errors resolve against the declaring
|
|
820
|
+
// entry's boundaries instead.
|
|
821
|
+
// - cache(): an intercept navigation is cached under the target route's
|
|
822
|
+
// cache() scope, with its own "intercept:" key (cache/cache-scope.ts).
|
|
749
823
|
const tempParent = {
|
|
750
824
|
...ctx.parent,
|
|
751
825
|
middleware: entry.middleware,
|
|
752
|
-
revalidate: entry.revalidate,
|
|
753
|
-
errorBoundary: entry.errorBoundary,
|
|
754
|
-
notFoundBoundary: entry.notFoundBoundary,
|
|
755
826
|
loader: entry.loader,
|
|
756
|
-
layout: capturedLayouts,
|
|
827
|
+
layout: capturedLayouts,
|
|
828
|
+
revalidate: [],
|
|
829
|
+
errorBoundary: [],
|
|
830
|
+
notFoundBoundary: [],
|
|
831
|
+
parallel: {},
|
|
832
|
+
intercept: [],
|
|
833
|
+
cache: undefined,
|
|
757
834
|
get loading() {
|
|
758
835
|
return entry.loading;
|
|
759
836
|
},
|
|
@@ -762,19 +839,29 @@ const intercept = (
|
|
|
762
839
|
},
|
|
763
840
|
};
|
|
764
841
|
|
|
765
|
-
const result =
|
|
766
|
-
mergedUse()?.flat(3),
|
|
842
|
+
const result = validateUseItems(
|
|
843
|
+
withParent(ctx, tempParent as EntryData, () => mergedUse()?.flat(3)),
|
|
844
|
+
namespace,
|
|
845
|
+
"intercept",
|
|
846
|
+
"use",
|
|
847
|
+
);
|
|
848
|
+
validateInterceptUseItems(
|
|
849
|
+
result,
|
|
850
|
+
capturedLayouts,
|
|
851
|
+
slotName,
|
|
852
|
+
routeName,
|
|
853
|
+
tempParent,
|
|
767
854
|
);
|
|
768
855
|
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
856
|
+
// The modal chrome is the first returned layout() item. Match it by id so
|
|
857
|
+
// an entry pushed into capturedLayouts without being returned is never
|
|
858
|
+
// taken as the chrome. It carries no use() items (validated above).
|
|
859
|
+
const chromeItem = result.find((item) => item?.type === "layout");
|
|
860
|
+
const chrome =
|
|
861
|
+
chromeItem && capturedLayouts.find((l) => l.id === chromeItem.name);
|
|
862
|
+
if (chrome) {
|
|
863
|
+
entry.layout = chrome.handler as ReactNode | Handler<any, any, any>;
|
|
775
864
|
}
|
|
776
|
-
|
|
777
|
-
validateUseItems(result, namespace, "intercept", "use");
|
|
778
865
|
}
|
|
779
866
|
|
|
780
867
|
ctx.parent.intercept.push(entry);
|
|
@@ -180,10 +180,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
180
180
|
* intercept("@modal", "card", () => <CardModal />),
|
|
181
181
|
* ])
|
|
182
182
|
*
|
|
183
|
-
* // With loaders and revalidation
|
|
183
|
+
* // With loaders and revalidation (revalidate() goes on the loader;
|
|
184
|
+
* // an intercept-level revalidate() throws)
|
|
184
185
|
* intercept("@modal", "card", () => <CardModal />, () => [
|
|
185
|
-
* loader(CardModalLoader),
|
|
186
|
-
* revalidate(() => false),
|
|
186
|
+
* loader(CardModalLoader, () => [revalidate(() => false)]),
|
|
187
187
|
* ])
|
|
188
188
|
*
|
|
189
189
|
* // Conditional activation via the config object's `when` selector
|
|
@@ -205,7 +205,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
205
205
|
* @param handler - Component or handler for intercepted render
|
|
206
206
|
* @param config - Optional InterceptConfig (e.g. `{ when }`), or the use
|
|
207
207
|
* callback directly when there is no config
|
|
208
|
-
* @param use - Optional callback for
|
|
208
|
+
* @param use - Optional callback for loader(), middleware(), loading(),
|
|
209
|
+
* transition() and a layout() modal chrome without use() items.
|
|
210
|
+
* revalidate() (goes on the loader), errorBoundary()/notFoundBoundary()
|
|
211
|
+
* (go on the enclosing layout) and cache() (goes on the target route)
|
|
212
|
+
* throw here.
|
|
209
213
|
*/
|
|
210
214
|
intercept: {
|
|
211
215
|
// Local: dot-prefixed, params inferred from local route definition
|
|
@@ -285,6 +289,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
285
289
|
* ({ defaultShouldRevalidate: true })
|
|
286
290
|
* )
|
|
287
291
|
* ```
|
|
292
|
+
* Not valid directly inside intercept() use() (throws at definition time):
|
|
293
|
+
* intercepts only revalidate their loaders, so attach it there with
|
|
294
|
+
* `loader(Def, () => [revalidate(...)])`.
|
|
288
295
|
* @param fn - Function returning either:
|
|
289
296
|
* - `boolean` (hard decision — short-circuits the chain),
|
|
290
297
|
* - `{ defaultShouldRevalidate: boolean }` (soft — updates the suggestion
|
|
@@ -352,16 +359,19 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
352
359
|
* ```typescript
|
|
353
360
|
* errorBoundary(<ErrorFallback />)
|
|
354
361
|
*
|
|
355
|
-
* // With dynamic error handler
|
|
356
|
-
* errorBoundary(({ error
|
|
362
|
+
* // With dynamic error handler (server fallback: receives only { error })
|
|
363
|
+
* errorBoundary(({ error }) => (
|
|
357
364
|
* <div>
|
|
358
365
|
* <h2>Something went wrong</h2>
|
|
359
366
|
* <p>{error.message}</p>
|
|
360
|
-
* <
|
|
367
|
+
* <a href="/">Go home</a>
|
|
361
368
|
* </div>
|
|
362
369
|
* ))
|
|
363
370
|
* ```
|
|
364
|
-
*
|
|
371
|
+
* Not valid inside intercept() use() (throws at definition time): an
|
|
372
|
+
* intercept has no boundary of its own, and its loader errors resolve
|
|
373
|
+
* against the enclosing layout/path boundary.
|
|
374
|
+
* @param fallback - Static JSX or handler receiving `{ error }`
|
|
365
375
|
*/
|
|
366
376
|
errorBoundary: (
|
|
367
377
|
fallback: ReactNode | ErrorBoundaryHandler,
|
|
@@ -379,6 +389,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
379
389
|
* </div>
|
|
380
390
|
* ))
|
|
381
391
|
* ```
|
|
392
|
+
* Not valid inside intercept() use() (throws at definition time): an
|
|
393
|
+
* intercept has no boundary of its own, and notFound() from its loaders
|
|
394
|
+
* resolves against the enclosing layout/path boundary.
|
|
382
395
|
* @param fallback - Static JSX or handler receiving not-found info
|
|
383
396
|
*/
|
|
384
397
|
notFoundBoundary: (
|
|
@@ -396,6 +409,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
396
409
|
* Note: Loaders are NOT cached by default. Use cache() inside loader()
|
|
397
410
|
* to explicitly opt-in to loader caching.
|
|
398
411
|
*
|
|
412
|
+
* Not valid inside intercept() use() (throws at definition time): an
|
|
413
|
+
* intercept navigation is cached under the target route's cache() scope.
|
|
414
|
+
*
|
|
399
415
|
* ```typescript
|
|
400
416
|
* // Using app-level defaults (ttl inherited from store.defaults)
|
|
401
417
|
* cache(() => [
|
|
@@ -430,6 +446,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
430
446
|
* route("archive/:year"), // uses KV store
|
|
431
447
|
* ])
|
|
432
448
|
*
|
|
449
|
+
* // Among a path's children: caches that path (handler, layouts, parallels)
|
|
450
|
+
* path("/product/:id", ProductPage, { name: "product" }, () => [
|
|
451
|
+
* cache({ ttl: 60 }),
|
|
452
|
+
* layout(<ProductChrome />), // cached with the path
|
|
453
|
+
* ])
|
|
454
|
+
*
|
|
433
455
|
* // Opt-in loader caching
|
|
434
456
|
* route("product/:id", ProductHandler, () => [
|
|
435
457
|
* loader(ProductLoader), // NOT cached (default)
|
|
@@ -459,7 +481,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
459
481
|
* content while the new loader resolves instead of flashing the route's
|
|
460
482
|
* loading() skeleton (see segment-system.tsx inTransitionScope). Held
|
|
461
483
|
* useLoader readers of the re-run loaders report isLoading: true until
|
|
462
|
-
* the new data commits (use-loader.tsx).
|
|
484
|
+
* the new data commits (use-loader.tsx). The pin is per loader family
|
|
485
|
+
* (`$$id`), not per segment — a layout reader of the same createLoader
|
|
486
|
+
* the child is re-running is flagged too. This is also the precondition
|
|
463
487
|
* for any view-transition animation.
|
|
464
488
|
* 2. <ViewTransition> (experimental React only): the segment content is also
|
|
465
489
|
* wrapped in React's <ViewTransition>, so the held swap cross-fades/morphs.
|
|
@@ -91,13 +91,12 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
|
|
|
91
91
|
"notFoundBoundary",
|
|
92
92
|
"transition",
|
|
93
93
|
]),
|
|
94
|
+
// intercept() checks its whole merged use() (explicit and handler.use)
|
|
95
|
+
// against this set via validateInterceptUseItems below.
|
|
94
96
|
intercept: new Set([
|
|
95
97
|
"middleware",
|
|
96
|
-
"revalidate",
|
|
97
98
|
"loader",
|
|
98
99
|
"loading",
|
|
99
|
-
"errorBoundary",
|
|
100
|
-
"notFoundBoundary",
|
|
101
100
|
"layout",
|
|
102
101
|
"route",
|
|
103
102
|
"transition",
|
|
@@ -106,6 +105,120 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
|
|
|
106
105
|
loader: new Set(["revalidate", "cache"]),
|
|
107
106
|
};
|
|
108
107
|
|
|
108
|
+
const INTERCEPT_BOUNDARY_HINT =
|
|
109
|
+
"put it on the enclosing layout or path; it handles the intercept's handler and loader errors";
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Where each item intercept() rejects goes instead. Why an intercept cannot
|
|
113
|
+
* hold them is documented at intercept() in dsl-helpers.ts.
|
|
114
|
+
*/
|
|
115
|
+
const INTERCEPT_REJECTION_HINTS: Record<string, string> = {
|
|
116
|
+
revalidate:
|
|
117
|
+
"attach it to the intercept's loader: loader(YourLoader, () => [revalidate(...)])",
|
|
118
|
+
errorBoundary: INTERCEPT_BOUNDARY_HINT,
|
|
119
|
+
notFoundBoundary: INTERCEPT_BOUNDARY_HINT,
|
|
120
|
+
cache:
|
|
121
|
+
'put cache() on the target route (intercept navigations get their own "intercept:" cache key) or use "use cache" in the handler',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function rejectInterceptItem(
|
|
125
|
+
what: string,
|
|
126
|
+
hint: string,
|
|
127
|
+
slotName: string,
|
|
128
|
+
routeName: string,
|
|
129
|
+
): never {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`${what} is not valid inside intercept("${slotName}", "${routeName}") use() (including the handler's .use): ${hint}.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate every item an intercept's merged use() returned (explicit use()
|
|
137
|
+
* and handler.use) against MOUNT_SITE_ALLOWED_TYPES.intercept, plus two
|
|
138
|
+
* structural rules:
|
|
139
|
+
* - a nested layout() is only the modal chrome (intercept() keeps its
|
|
140
|
+
* handler), so it may not carry use() items of its own;
|
|
141
|
+
* - a cache entry in `capturedLayouts` means cache() ran in the scope even if
|
|
142
|
+
* it was not returned; its orphan form re-parents the following siblings
|
|
143
|
+
* onto that entry, so it is rejected the same way. Inside a path, cache()
|
|
144
|
+
* writes `sinks.cache` instead (dsl-helpers.ts cache()).
|
|
145
|
+
*/
|
|
146
|
+
export function validateInterceptUseItems(
|
|
147
|
+
items: AllUseItems[],
|
|
148
|
+
capturedLayouts: readonly { type: string }[],
|
|
149
|
+
slotName: string,
|
|
150
|
+
routeName: string,
|
|
151
|
+
sinks: {
|
|
152
|
+
readonly revalidate: readonly unknown[];
|
|
153
|
+
readonly errorBoundary: readonly unknown[];
|
|
154
|
+
readonly notFoundBoundary: readonly unknown[];
|
|
155
|
+
readonly intercept: readonly unknown[];
|
|
156
|
+
readonly parallel: Readonly<Record<string, unknown>>;
|
|
157
|
+
/** Set by cache() when the intercept is declared inside a path. */
|
|
158
|
+
readonly cache?: unknown;
|
|
159
|
+
},
|
|
160
|
+
): void {
|
|
161
|
+
const allowed = MOUNT_SITE_ALLOWED_TYPES.intercept!;
|
|
162
|
+
for (const item of items) {
|
|
163
|
+
if (item == null) continue;
|
|
164
|
+
const type = (item as { type: string }).type;
|
|
165
|
+
if (!allowed.has(type)) {
|
|
166
|
+
rejectInterceptItem(
|
|
167
|
+
`${type}()`,
|
|
168
|
+
INTERCEPT_REJECTION_HINTS[type] ??
|
|
169
|
+
`allowed items are ${[...allowed].join(", ")}`,
|
|
170
|
+
slotName,
|
|
171
|
+
routeName,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (type === "layout" && (item as { uses?: unknown[] }).uses?.length) {
|
|
175
|
+
rejectInterceptItem(
|
|
176
|
+
"layout() with its own use() items",
|
|
177
|
+
"put the modal chrome in the layout component and attach loaders to the intercept itself",
|
|
178
|
+
slotName,
|
|
179
|
+
routeName,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// A rejected helper called without being returned still wrote into its
|
|
184
|
+
// throwaway field on the temporary parent; reject it the same way.
|
|
185
|
+
for (const type of [
|
|
186
|
+
"revalidate",
|
|
187
|
+
"errorBoundary",
|
|
188
|
+
"notFoundBoundary",
|
|
189
|
+
"intercept",
|
|
190
|
+
] as const) {
|
|
191
|
+
if (sinks[type].length > 0) {
|
|
192
|
+
rejectInterceptItem(
|
|
193
|
+
`${type}()`,
|
|
194
|
+
INTERCEPT_REJECTION_HINTS[type] ??
|
|
195
|
+
`allowed items are ${[...allowed].join(", ")}`,
|
|
196
|
+
slotName,
|
|
197
|
+
routeName,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (Object.keys(sinks.parallel).length > 0) {
|
|
202
|
+
rejectInterceptItem(
|
|
203
|
+
"parallel()",
|
|
204
|
+
`allowed items are ${[...allowed].join(", ")}`,
|
|
205
|
+
slotName,
|
|
206
|
+
routeName,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (
|
|
210
|
+
capturedLayouts.some((l) => l.type === "cache") ||
|
|
211
|
+
sinks.cache !== undefined
|
|
212
|
+
) {
|
|
213
|
+
rejectInterceptItem(
|
|
214
|
+
"cache()",
|
|
215
|
+
INTERCEPT_REJECTION_HINTS.cache!,
|
|
216
|
+
slotName,
|
|
217
|
+
routeName,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
109
222
|
/**
|
|
110
223
|
* Validate that items from handler.use() are valid for the given mount site.
|
|
111
224
|
* Throws a descriptive error if any item is not allowed.
|
|
@@ -135,7 +248,7 @@ export function validateHandlerUseItems(
|
|
|
135
248
|
export function mergeHandlerUse(
|
|
136
249
|
handlerUse: (() => any[]) | undefined,
|
|
137
250
|
explicitUse: (() => any[]) | undefined,
|
|
138
|
-
mountSite: string,
|
|
251
|
+
mountSite: string | null,
|
|
139
252
|
): (() => any[]) | undefined {
|
|
140
253
|
if (!handlerUse && !explicitUse) return undefined;
|
|
141
254
|
// Validation asymmetry (intentional, pre-1.0): only handler.use() items are
|
|
@@ -144,17 +257,19 @@ export function mergeHandlerUse(
|
|
|
144
257
|
// branch here and the merged branch, so a structurally-valid-but-prohibited
|
|
145
258
|
// item (e.g. middleware() inside a parallel slot) is not rejected at this seam.
|
|
146
259
|
// Documented rather than enforced for now; revisit before 1.0 (#569).
|
|
260
|
+
// intercept() is the exception: it passes mountSite null and validates the
|
|
261
|
+
// merged items itself (validateInterceptUseItems).
|
|
147
262
|
if (!handlerUse) return explicitUse;
|
|
148
263
|
if (!explicitUse) {
|
|
149
264
|
return () => {
|
|
150
265
|
const items = handlerUse().flat(3);
|
|
151
|
-
validateHandlerUseItems(items, mountSite);
|
|
266
|
+
if (mountSite) validateHandlerUseItems(items, mountSite);
|
|
152
267
|
return items;
|
|
153
268
|
};
|
|
154
269
|
}
|
|
155
270
|
return () => {
|
|
156
271
|
const hItems = handlerUse().flat(3);
|
|
157
|
-
validateHandlerUseItems(hItems, mountSite);
|
|
272
|
+
if (mountSite) validateHandlerUseItems(hItems, mountSite);
|
|
158
273
|
return [...hItems, ...explicitUse()];
|
|
159
274
|
};
|
|
160
275
|
}
|
package/src/route-types.ts
CHANGED
|
@@ -222,11 +222,8 @@ export type ParallelUseItem =
|
|
|
222
222
|
| TransitionItem;
|
|
223
223
|
export type InterceptUseItem =
|
|
224
224
|
| MiddlewareItem
|
|
225
|
-
| RevalidateItem
|
|
226
225
|
| LoaderItem
|
|
227
226
|
| LoadingItem
|
|
228
|
-
| ErrorBoundaryItem
|
|
229
|
-
| NotFoundBoundaryItem
|
|
230
227
|
| LayoutItem
|
|
231
228
|
| RouteItem
|
|
232
229
|
| TransitionItem;
|
|
@@ -5,8 +5,9 @@ import MapRootLayout from "../server/root-layout.js";
|
|
|
5
5
|
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Build a serialized manifest from
|
|
9
|
-
* Used by the `debugManifest()` method on the router instance.
|
|
8
|
+
* Build a serialized manifest from the eager route entries for debug
|
|
9
|
+
* inspection. Used by the `debugManifest()` method on the router instance.
|
|
10
|
+
* include() children are not expanded (lazy placeholders are skipped).
|
|
10
11
|
*/
|
|
11
12
|
export async function buildDebugManifest<TEnv = any>(
|
|
12
13
|
routesEntries: RouteEntry<TEnv>[],
|
|
@@ -14,6 +15,13 @@ export async function buildDebugManifest<TEnv = any>(
|
|
|
14
15
|
const manifest = new Map<string, EntryData>();
|
|
15
16
|
|
|
16
17
|
for (const entry of routesEntries) {
|
|
18
|
+
// Lazy include placeholders are not mounts: a top-level one carries the
|
|
19
|
+
// parent urls() handler (router.ts routes()), so re-running it re-registers
|
|
20
|
+
// the parent's routes into the shared manifest (path() duplicate-name
|
|
21
|
+
// invariant). A nested one (lazy-includes.ts runExpansion()) is only valid
|
|
22
|
+
// under its captured include context.
|
|
23
|
+
if (entry.lazy) continue;
|
|
24
|
+
|
|
17
25
|
const Store = {
|
|
18
26
|
manifest,
|
|
19
27
|
namespace: `debug.M${entry.mountIndex}`,
|
|
@@ -105,31 +105,67 @@ export function invokeOnError<TEnv = any>(
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* First non-undefined `pick(orphan)` over the routeless entries in
|
|
110
|
+
* `entry.layout` and, recursively, in theirs: depth-first in render order
|
|
111
|
+
* (resolveOrphanLayout in segment-resolution/fresh.ts). Since #922 a routeless
|
|
112
|
+
* entry nested in another renders (a layout() after a bare cache() marker, a
|
|
113
|
+
* layout() in a routeless wrapper); a one-level scan missed the boundaries and
|
|
114
|
+
* intercepts it declares (#926).
|
|
115
|
+
*
|
|
116
|
+
* `skip` is the entry an upward walk came from. Only a bare cache() marker is
|
|
117
|
+
* both in its layout's layout[] and in the chain, and its subtree was already
|
|
118
|
+
* scanned at its chain position.
|
|
119
|
+
*/
|
|
120
|
+
export function findInOrphans<T>(
|
|
121
|
+
entry: EntryData,
|
|
122
|
+
pick: (orphan: EntryData) => T | undefined,
|
|
123
|
+
skip?: EntryData | null,
|
|
124
|
+
): T | undefined {
|
|
125
|
+
for (const orphan of entry.layout ?? []) {
|
|
126
|
+
if (orphan === skip) continue;
|
|
127
|
+
const found = pick(orphan) ?? findInOrphans(orphan, pick);
|
|
128
|
+
if (found !== undefined) return found;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `findInOrphans` pick: the orphan when it declares an errorBoundary(). */
|
|
134
|
+
export const withErrorBoundary = (entry: EntryData): EntryData | undefined =>
|
|
135
|
+
entry.errorBoundary && entry.errorBoundary.length > 0 ? entry : undefined;
|
|
136
|
+
|
|
137
|
+
const withNotFoundBoundary = (entry: EntryData): EntryData | undefined =>
|
|
138
|
+
entry.notFoundBoundary && entry.notFoundBoundary.length > 0
|
|
139
|
+
? entry
|
|
140
|
+
: undefined;
|
|
141
|
+
|
|
108
142
|
/**
|
|
109
143
|
* Find the nearest error boundary by walking up the entry chain
|
|
110
144
|
* Also checks sibling layouts (orphan layouts) for error boundaries
|
|
111
145
|
* Returns the first fallback found, or the default error boundary if configured
|
|
146
|
+
*
|
|
147
|
+
* An orphan's parent is null, so from an orphan the walk continues at its
|
|
148
|
+
* orphanOwner (EntryData). Without that step, errors from an intercept declared
|
|
149
|
+
* in a routeless layout, or from that layout's loaders, stopped at the orphan
|
|
150
|
+
* and skipped every ancestor boundary (#898).
|
|
112
151
|
*/
|
|
113
152
|
export function findNearestErrorBoundary(
|
|
114
153
|
entry: EntryData | null,
|
|
115
154
|
defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler,
|
|
116
155
|
): ReactNode | ErrorBoundaryHandler | null {
|
|
117
156
|
let current: EntryData | null = entry;
|
|
157
|
+
let prev: EntryData | null = null;
|
|
118
158
|
|
|
119
159
|
while (current) {
|
|
120
160
|
if (current.errorBoundary && current.errorBoundary.length > 0) {
|
|
121
161
|
return current.errorBoundary[current.errorBoundary.length - 1];
|
|
122
162
|
}
|
|
123
163
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
|
|
127
|
-
return orphan.errorBoundary[orphan.errorBoundary.length - 1];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
164
|
+
const orphan = findInOrphans(current, withErrorBoundary, prev);
|
|
165
|
+
if (orphan) return orphan.errorBoundary[orphan.errorBoundary.length - 1];
|
|
131
166
|
|
|
132
|
-
|
|
167
|
+
prev = current;
|
|
168
|
+
current = current.parent ?? current.orphanOwner ?? null;
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
// Return default error boundary if configured
|
|
@@ -145,6 +181,7 @@ export function findNearestNotFoundBoundary(
|
|
|
145
181
|
defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler,
|
|
146
182
|
): ReactNode | NotFoundBoundaryHandler | null {
|
|
147
183
|
let current: EntryData | null = entry;
|
|
184
|
+
let prev: EntryData | null = null;
|
|
148
185
|
|
|
149
186
|
while (current) {
|
|
150
187
|
if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
|
|
@@ -154,15 +191,14 @@ export function findNearestNotFoundBoundary(
|
|
|
154
191
|
// Check orphan layouts mirroring findNearestErrorBoundary: notFoundBoundary
|
|
155
192
|
// attaches identically (onto parent.notFoundBoundary), and an orphan layout
|
|
156
193
|
// (parent=null) is reachable only via this scan. First sibling is "outer".
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
|
|
161
|
-
}
|
|
162
|
-
}
|
|
194
|
+
const orphan = findInOrphans(current, withNotFoundBoundary, prev);
|
|
195
|
+
if (orphan) {
|
|
196
|
+
return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
|
|
163
197
|
}
|
|
164
198
|
|
|
165
|
-
|
|
199
|
+
// Orphan: continue at its owner, as findNearestErrorBoundary does.
|
|
200
|
+
prev = current;
|
|
201
|
+
current = current.parent ?? current.orphanOwner ?? null;
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
// Return default notFound boundary if configured
|