@rangojs/router 0.24.4 → 0.25.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 +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
package/src/loader-redirect.tsx
CHANGED
|
@@ -19,9 +19,8 @@ import { resolveSameOriginRedirect } from "./redirect-origin.js";
|
|
|
19
19
|
*
|
|
20
20
|
* Own module (not route-content-wrapper.tsx) because BOTH consumers need it
|
|
21
21
|
* without a cycle: the StreamedLoaderErrorBoundary redirect branch (read-site
|
|
22
|
-
* throw path) and decodeLoaderResults (
|
|
23
|
-
*
|
|
24
|
-
* in the errorFallback slot instead).
|
|
22
|
+
* throw path) and decodeLoaderResults (the tree build's settled results, which
|
|
23
|
+
* cannot throw — it plants this element in the errorFallback slot instead).
|
|
25
24
|
*/
|
|
26
25
|
export function LoaderRedirect({
|
|
27
26
|
to,
|
package/src/outlet-context.ts
CHANGED
|
@@ -13,18 +13,19 @@ export interface OutletContextValue {
|
|
|
13
13
|
* $$id. A value may be a pending Promise (loader still streaming) or an
|
|
14
14
|
* already-settled result entry. useLoader `use()`es a pending value at the
|
|
15
15
|
* read site — implicit suspension to the nearest consumer Suspense boundary —
|
|
16
|
-
* and decodes via decodeLoaderEntry. Populated instead of `loaderData`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* and decodes via decodeLoaderEntry. Populated instead of `loaderData` for
|
|
17
|
+
* a route's or layout's loaders on every lane; forceAwait/action lanes hand
|
|
18
|
+
* settled entries. `loaderData` is a slot's decoded aggregate.
|
|
19
19
|
*/
|
|
20
20
|
loaderStreams?: Record<string, unknown>;
|
|
21
21
|
/**
|
|
22
22
|
* Loader $$ids on this segment that the render awaited before first flush
|
|
23
23
|
* (loader(Def, { ssr: false })). Dev-diagnostic input only: useLoader uses
|
|
24
24
|
* it to warn when a read still suspends during SSR on a render that awaited
|
|
25
|
-
* flagged loaders (ssr-suspension-warning.ts). Present only
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* flagged loaders (ssr-suspension-warning.ts). Present only where a
|
|
26
|
+
* segment carries flagged loaders (document and shell-capture renders, and
|
|
27
|
+
* a client lane that re-renders their segments); a settled read never
|
|
28
|
+
* suspends, so it never warns.
|
|
28
29
|
*/
|
|
29
30
|
awaitedLoaderIds?: readonly string[];
|
|
30
31
|
parent?: OutletContextValue | null;
|
package/src/prefetch-markers.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The wire markers of `prefetch: false`
|
|
3
|
-
* shared by the browser
|
|
4
|
-
* (router/segment-resolution/prefetch-deferral.ts requestKind
|
|
5
|
-
* free, like segment-fragments.ts:
|
|
6
|
-
*
|
|
2
|
+
* The wire markers of prefetches and of `prefetch: false`
|
|
3
|
+
* (docs/design/prefetch-false.md): shared by the browser and the server
|
|
4
|
+
* (router/segment-resolution/prefetch-deferral.ts requestKind,
|
|
5
|
+
* rsc/prefetch-render-error.ts). Dependency free, like segment-fragments.ts:
|
|
6
|
+
* the browser must not import the server modules that use them.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
|
|
@@ -11,3 +11,13 @@ export const PREFETCH_HEADER = "X-Rango-Prefetch";
|
|
|
11
11
|
|
|
12
12
|
/** Query param of a fill request (browser/navigation-client.ts). */
|
|
13
13
|
export const FILL_PARAM = "_rsc_fill";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The last row of a prefetch whose render reported an error
|
|
17
|
+
* (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
|
|
18
|
+
* dispatch, so the decoder skips it (pinned by
|
|
19
|
+
* src/__tests__/flight-unknown-hint.test.ts), and browser/response-adapter.ts
|
|
20
|
+
* reads the stream as not ended cleanly. Five bytes: teeWithCompletion keeps
|
|
21
|
+
* the last five.
|
|
22
|
+
*/
|
|
23
|
+
export const PREFETCH_FAILED_ROW = ":H_0\n";
|
package/src/reverse.ts
CHANGED
|
@@ -406,9 +406,9 @@ export function createReverse<TRoutes extends Record<string, string>>(
|
|
|
406
406
|
throw new Error(`Unknown route: ${name}`);
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
// Always substitute: a required param with no params object throws
|
|
410
|
+
// `Missing param` rather than returning the raw `:param` pattern.
|
|
411
|
+
let result = substitutePatternParams(pattern, params ?? {}, name);
|
|
412
412
|
|
|
413
413
|
// Append search params as query string
|
|
414
414
|
if (search) {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
useRevealAudit,
|
|
19
19
|
} from "./suspense-audit-react.js";
|
|
20
20
|
import { SETTLED_THENABLE } from "./thenable-status.js";
|
|
21
|
+
import { shallowEqual } from "./browser/react/shallow-equal.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Router-owned error boundary for read-site loader errors. segment-system
|
|
@@ -40,12 +41,12 @@ import { SETTLED_THENABLE } from "./thenable-status.js";
|
|
|
40
41
|
* Errors without any marker rethrow to the app's own boundaries.
|
|
41
42
|
*/
|
|
42
43
|
export class StreamedLoaderErrorBoundary extends Component<
|
|
43
|
-
{ children: ReactNode;
|
|
44
|
-
{ error: unknown;
|
|
44
|
+
{ children: ReactNode; reset: unknown[] },
|
|
45
|
+
{ error: unknown; reset: unknown[] }
|
|
45
46
|
> {
|
|
46
|
-
state: { error: unknown;
|
|
47
|
+
state: { error: unknown; reset: unknown[] } = {
|
|
47
48
|
error: null,
|
|
48
|
-
|
|
49
|
+
reset: this.props.reset,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
static getDerivedStateFromError(error: unknown): { error: unknown } {
|
|
@@ -54,20 +55,21 @@ export class StreamedLoaderErrorBoundary extends Component<
|
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* A caught marker (redirect, notFound, error fallback) belongs to ONE route
|
|
57
|
-
* + params
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* + params and ONE delivery of the segment's loader data. `reset` is the
|
|
59
|
+
* segment's id-params identity followed by each of its loaders' data; the
|
|
60
|
+
* error clears when any of them changes. Group-keyed segments
|
|
61
|
+
* (ResolvedSegment.clientGroup) keep this instance alive across in-group
|
|
62
|
+
* navigations, so a redirect caught for /legacy must not keep rendering
|
|
63
|
+
* LoaderRedirect for /state. A rerun hands a loader new data, a held loader
|
|
64
|
+
* keeps its own: a later action or navigation that re-ran the loader clears
|
|
65
|
+
* an error fallback left by an earlier failure, and a tree that re-ran
|
|
66
|
+
* nothing here keeps it.
|
|
62
67
|
*/
|
|
63
68
|
static getDerivedStateFromProps(
|
|
64
|
-
|
|
65
|
-
state: {
|
|
66
|
-
): { error:
|
|
67
|
-
|
|
68
|
-
return { error: null, resetKey: props.resetKey };
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
69
|
+
{ reset }: { reset: unknown[] },
|
|
70
|
+
state: { reset: unknown[] },
|
|
71
|
+
): { error: null; reset: unknown[] } | null {
|
|
72
|
+
return shallowEqual(reset, state.reset) ? null : { error: null, reset };
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
render(): ReactNode {
|
|
@@ -311,7 +313,7 @@ function AuditedLoaderResolver({
|
|
|
311
313
|
* Without streams, the pre-spike behavior is preserved: a pending aggregate
|
|
312
314
|
* resolves ABOVE via use() (parallel/intercept slots still take this path —
|
|
313
315
|
* ResolvedSegment carries only the aggregate for slots), and a resolved
|
|
314
|
-
* array decodes synchronously (forceAwait/action lanes
|
|
316
|
+
* array decodes synchronously (a slot on the forceAwait/action lanes).
|
|
315
317
|
*/
|
|
316
318
|
function LoaderResolver({
|
|
317
319
|
loaderDataPromise,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import {
|
|
12
12
|
getContext,
|
|
13
13
|
getNamePrefix,
|
|
14
|
+
getReverseScope,
|
|
14
15
|
getUrlPrefix,
|
|
15
16
|
requireDslContext,
|
|
16
17
|
stampStaticDefScope,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
validateInterceptUseItems,
|
|
53
54
|
} from "./resolve-handler-use.js";
|
|
54
55
|
import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
|
|
56
|
+
import { toShouldRevalidateFn } from "../router/revalidation.js";
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* Check if an item contains routes (directly or inside nested wrappers).
|
|
@@ -189,6 +191,10 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
|
189
191
|
const { store, ctx } = requireDslContext(
|
|
190
192
|
"revalidate() must be called inside urls()",
|
|
191
193
|
);
|
|
194
|
+
invariant(
|
|
195
|
+
typeof fn === "function" || typeof fn === "boolean",
|
|
196
|
+
`revalidate() expects a predicate or a boolean, got ${fn === null ? "null" : typeof fn}`,
|
|
197
|
+
);
|
|
192
198
|
|
|
193
199
|
// Attach to last entry in stack
|
|
194
200
|
const parent = ctx.parent;
|
|
@@ -196,7 +202,7 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
|
196
202
|
invariant(false, "No parent entry available for revalidate()");
|
|
197
203
|
}
|
|
198
204
|
const name = `$${store.getNextIndex("revalidate")}`;
|
|
199
|
-
parent.revalidate.push(fn);
|
|
205
|
+
parent.revalidate.push(toShouldRevalidateFn(fn));
|
|
200
206
|
return { name, type: "revalidate" } as RevalidateItem;
|
|
201
207
|
};
|
|
202
208
|
|
|
@@ -394,6 +400,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
394
400
|
cache: cacheConfig,
|
|
395
401
|
handler: RootLayout,
|
|
396
402
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
403
|
+
reverseScope: getReverseScope(),
|
|
397
404
|
} satisfies EntryData;
|
|
398
405
|
|
|
399
406
|
// Attach to parent's layout array (cache entries are structural like layouts)
|
|
@@ -433,6 +440,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
433
440
|
// Cache entries render like layouts (with Outlet as default handler)
|
|
434
441
|
handler: RootLayout, // RootLayout just renders <Outlet />
|
|
435
442
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
443
|
+
reverseScope: getReverseScope(),
|
|
436
444
|
} satisfies EntryData;
|
|
437
445
|
|
|
438
446
|
// Run children with cache entry as parent
|
|
@@ -522,6 +530,7 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
|
522
530
|
handler: RootLayout,
|
|
523
531
|
middleware: [...fns],
|
|
524
532
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
533
|
+
reverseScope: getReverseScope(),
|
|
525
534
|
} satisfies EntryData;
|
|
526
535
|
|
|
527
536
|
// Run children callback. If the second arg was actually a middleware fn
|
|
@@ -643,6 +652,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
643
652
|
parent: null, // Parallels don't participate in parent chain traversal
|
|
644
653
|
handler: unwrappedSlots,
|
|
645
654
|
...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
|
|
655
|
+
reverseScope: getReverseScope(),
|
|
646
656
|
...(hasStaticSlot
|
|
647
657
|
? {
|
|
648
658
|
isStaticPrerender: true as const,
|
|
@@ -777,12 +787,11 @@ const intercept = (
|
|
|
777
787
|
const prefixedRouteName =
|
|
778
788
|
isLocal && namePrefix ? `${namePrefix}.${bareRouteName}` : bareRouteName;
|
|
779
789
|
|
|
780
|
-
// Create intercept entry with its own loaders/
|
|
790
|
+
// Create intercept entry with its own loaders/when
|
|
781
791
|
const entry: InterceptEntry = {
|
|
782
792
|
slotName: slotName as `@${string}`,
|
|
783
793
|
routeName: prefixedRouteName,
|
|
784
794
|
handler,
|
|
785
|
-
middleware: [],
|
|
786
795
|
loader: [],
|
|
787
796
|
when: [], // Selector conditions for conditional interception
|
|
788
797
|
};
|
|
@@ -800,12 +809,12 @@ const intercept = (
|
|
|
800
809
|
const handlerUseFn = resolveHandlerUse(handler);
|
|
801
810
|
const mergedUse = mergeHandlerUse(handlerUseFn, useFn, null);
|
|
802
811
|
|
|
803
|
-
// Run merged use callback to collect loaders,
|
|
812
|
+
// Run merged use callback to collect loaders, loading, layout, etc.
|
|
804
813
|
if (mergedUse) {
|
|
805
814
|
// Capture layout() calls into a temporary array
|
|
806
815
|
const capturedLayouts: EntryData[] = [];
|
|
807
816
|
|
|
808
|
-
// Temporary parent so
|
|
817
|
+
// Temporary parent so loader() attaches to the intercept entry;
|
|
809
818
|
// the loading get/set accessor mirrors writes onto `entry`. It is a
|
|
810
819
|
// shallow spread of the enclosing entry, so every field a rejected helper
|
|
811
820
|
// writes to points at a throwaway here: a helper called but not returned
|
|
@@ -814,8 +823,12 @@ const intercept = (
|
|
|
814
823
|
// is a route.
|
|
815
824
|
//
|
|
816
825
|
// Why an intercept rejects these (validateInterceptUseItems):
|
|
826
|
+
// - middleware(): only the live render resolved the intercept's own
|
|
827
|
+
// middleware; a cache() hit or a Prerender() target skipped it, so a
|
|
828
|
+
// guard in it was bypassed. The target route's middleware runs on every
|
|
829
|
+
// pass, the modal's included.
|
|
817
830
|
// - revalidate(): an intercept only evaluates its loaders' revalidate()
|
|
818
|
-
// (router/intercept-resolution.ts).
|
|
831
|
+
// (loader segments of its slot, router/intercept-resolution.ts).
|
|
819
832
|
// - errorBoundary()/notFoundBoundary(): error lookup walks EntryData
|
|
820
833
|
// parent chains (router/error-handling.ts), which never include an
|
|
821
834
|
// InterceptEntry; intercept loader errors resolve against the declaring
|
|
@@ -824,7 +837,7 @@ const intercept = (
|
|
|
824
837
|
// cache() scope, with its own "intercept:" key (cache/cache-scope.ts).
|
|
825
838
|
const tempParent = {
|
|
826
839
|
...ctx.parent,
|
|
827
|
-
middleware:
|
|
840
|
+
middleware: [],
|
|
828
841
|
loader: entry.loader,
|
|
829
842
|
layout: capturedLayouts,
|
|
830
843
|
revalidate: [],
|
|
@@ -833,6 +846,8 @@ const intercept = (
|
|
|
833
846
|
parallel: {},
|
|
834
847
|
intercept: [],
|
|
835
848
|
cache: undefined,
|
|
849
|
+
// Its loaders resolve `.name` like the intercept handler: from the route.
|
|
850
|
+
reverseScope: undefined,
|
|
836
851
|
get loading() {
|
|
837
852
|
return entry.loading;
|
|
838
853
|
},
|
|
@@ -925,6 +940,10 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
925
940
|
...(optionsGiven?.ssr === false &&
|
|
926
941
|
ctx.isSSR && { awaitBeforeFlush: true as const }),
|
|
927
942
|
...(optionsGiven?.prefetch === false && { prefetch: false as const }),
|
|
943
|
+
// The declaring layout's / parallel's / cache()'s scope. A route's (and
|
|
944
|
+
// an intercept's) loaders have none: they resolve from the route's name,
|
|
945
|
+
// like the handler.
|
|
946
|
+
...(ctx.parent.reverseScope && { reverseScope: ctx.parent.reverseScope }),
|
|
928
947
|
};
|
|
929
948
|
|
|
930
949
|
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
@@ -1156,6 +1175,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1156
1175
|
parent: ctx.parent,
|
|
1157
1176
|
handler: unwrappedHandler,
|
|
1158
1177
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
1178
|
+
reverseScope: getReverseScope(),
|
|
1159
1179
|
...(isStatic
|
|
1160
1180
|
? {
|
|
1161
1181
|
isStaticPrerender: true as const,
|
|
@@ -183,7 +183,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
183
183
|
* // With loaders and revalidation (revalidate() goes on the loader;
|
|
184
184
|
* // an intercept-level revalidate() throws)
|
|
185
185
|
* intercept("@modal", "card", () => <CardModal />, () => [
|
|
186
|
-
* loader(CardModalLoader, () => [revalidate(
|
|
186
|
+
* loader(CardModalLoader, () => [revalidate(false)]),
|
|
187
187
|
* ])
|
|
188
188
|
*
|
|
189
189
|
* // Conditional activation via the config object's `when` selector
|
|
@@ -288,6 +288,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
288
288
|
* revalidate(({ defaultShouldRevalidate }) =>
|
|
289
289
|
* ({ defaultShouldRevalidate: true })
|
|
290
290
|
* )
|
|
291
|
+
*
|
|
292
|
+
* // Shorthand for a constant hard decision: revalidate(() => false)
|
|
293
|
+
* revalidate(false)
|
|
291
294
|
* ```
|
|
292
295
|
* Not valid directly inside intercept() use() (throws at definition time):
|
|
293
296
|
* intercepts only revalidate their loaders, so attach it there with
|
|
@@ -298,8 +301,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
298
301
|
* for downstream revalidators),
|
|
299
302
|
* - or nothing / `null` / `undefined` (defer — leaves the suggestion
|
|
300
303
|
* unchanged and continues to the next revalidator).
|
|
304
|
+
* Or `true` / `false`: shorthand for `() => true` / `() => false`. A
|
|
305
|
+
* defer has no shorthand; write the function.
|
|
301
306
|
*/
|
|
302
|
-
revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
|
|
307
|
+
revalidate: (fn: ShouldRevalidateFn<any, TEnv> | boolean) => RevalidateItem;
|
|
303
308
|
/**
|
|
304
309
|
* Attach a data loader to the current route/layout
|
|
305
310
|
* ```typescript
|
|
@@ -93,14 +93,7 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
|
|
|
93
93
|
]),
|
|
94
94
|
// intercept() checks its whole merged use() (explicit and handler.use)
|
|
95
95
|
// against this set via validateInterceptUseItems below.
|
|
96
|
-
intercept: new Set([
|
|
97
|
-
"middleware",
|
|
98
|
-
"loader",
|
|
99
|
-
"loading",
|
|
100
|
-
"layout",
|
|
101
|
-
"route",
|
|
102
|
-
"transition",
|
|
103
|
-
]),
|
|
96
|
+
intercept: new Set(["loader", "loading", "layout", "route", "transition"]),
|
|
104
97
|
// LoaderUseItem — only revalidate + cache can attach to a loader entry
|
|
105
98
|
loader: new Set(["revalidate", "cache"]),
|
|
106
99
|
};
|
|
@@ -113,6 +106,8 @@ const INTERCEPT_BOUNDARY_HINT =
|
|
|
113
106
|
* hold them is documented at intercept() in dsl-helpers.ts.
|
|
114
107
|
*/
|
|
115
108
|
const INTERCEPT_REJECTION_HINTS: Record<string, string> = {
|
|
109
|
+
middleware:
|
|
110
|
+
"move it to the route's middleware() (it runs for the modal too) or into the loaders",
|
|
116
111
|
revalidate:
|
|
117
112
|
"attach it to the intercept's loader: loader(YourLoader, () => [revalidate(...)])",
|
|
118
113
|
errorBoundary: INTERCEPT_BOUNDARY_HINT,
|
|
@@ -134,7 +129,7 @@ function rejectInterceptItem(
|
|
|
134
129
|
|
|
135
130
|
/**
|
|
136
131
|
* Validate every item an intercept's merged use() returned (explicit use()
|
|
137
|
-
* and handler.use) against MOUNT_SITE_ALLOWED_TYPES.intercept, plus
|
|
132
|
+
* and handler.use) against MOUNT_SITE_ALLOWED_TYPES.intercept, plus three
|
|
138
133
|
* structural rules:
|
|
139
134
|
* - a nested layout() is only the modal chrome (intercept() keeps its
|
|
140
135
|
* handler), so it may not carry use() items of its own;
|
|
@@ -142,13 +137,20 @@ function rejectInterceptItem(
|
|
|
142
137
|
* it was not returned; its orphan form re-parents the following siblings
|
|
143
138
|
* onto that entry, so it is rejected the same way. Inside a path, cache()
|
|
144
139
|
* writes `sinks.cache` instead (dsl-helpers.ts cache()).
|
|
140
|
+
* - middleware on a `capturedLayouts` entry is rejected like `sinks.middleware`:
|
|
141
|
+
* it is an unreturned middleware(fn, () => [...]) or one called inside the
|
|
142
|
+
* chrome layout's use(), and nothing would run it.
|
|
145
143
|
*/
|
|
146
144
|
export function validateInterceptUseItems(
|
|
147
145
|
items: AllUseItems[],
|
|
148
|
-
capturedLayouts: readonly {
|
|
146
|
+
capturedLayouts: readonly {
|
|
147
|
+
type: string;
|
|
148
|
+
middleware?: readonly unknown[];
|
|
149
|
+
}[],
|
|
149
150
|
slotName: string,
|
|
150
151
|
routeName: string,
|
|
151
152
|
sinks: {
|
|
153
|
+
readonly middleware: readonly unknown[];
|
|
152
154
|
readonly revalidate: readonly unknown[];
|
|
153
155
|
readonly errorBoundary: readonly unknown[];
|
|
154
156
|
readonly notFoundBoundary: readonly unknown[];
|
|
@@ -206,6 +208,17 @@ export function validateInterceptUseItems(
|
|
|
206
208
|
routeName,
|
|
207
209
|
);
|
|
208
210
|
}
|
|
211
|
+
if (
|
|
212
|
+
sinks.middleware.length > 0 ||
|
|
213
|
+
capturedLayouts.some((l) => l.middleware?.length)
|
|
214
|
+
) {
|
|
215
|
+
rejectInterceptItem(
|
|
216
|
+
"middleware()",
|
|
217
|
+
INTERCEPT_REJECTION_HINTS.middleware!,
|
|
218
|
+
slotName,
|
|
219
|
+
routeName,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
209
222
|
if (
|
|
210
223
|
capturedLayouts.some((l) => l.type === "cache") ||
|
|
211
224
|
sinks.cache !== undefined
|
|
@@ -27,11 +27,10 @@ export type RewriteTarget =
|
|
|
27
27
|
|
|
28
28
|
const REWRITE_RULE =
|
|
29
29
|
"rewrite() is for middleware: throw it from router.use() or route " +
|
|
30
|
-
"middleware (middleware() in urls()), before await next()
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"redirect(); for a mutation return a Response from middleware.";
|
|
30
|
+
"middleware (middleware() in urls()), before await next(). Loaders and " +
|
|
31
|
+
"route handlers can't rewrite: throw rewrite() from the route's " +
|
|
32
|
+
"middleware instead. Middleware in loader fetches can't either. In a " +
|
|
33
|
+
"server action use redirect().";
|
|
35
34
|
|
|
36
35
|
export function describeRewriteTarget(target: RewriteTarget): string {
|
|
37
36
|
return "path" in target
|
|
@@ -39,6 +38,17 @@ export function describeRewriteTarget(target: RewriteTarget): string {
|
|
|
39
38
|
: `rewrite(${JSON.stringify(target.name)})`;
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Which PPR shell a rewritten request reads and captures
|
|
43
|
+
* (`rewrite(...).shell()`); without a call, none.
|
|
44
|
+
* - `"visitor"`: its own, keyed by the visitor's URL and the target, captured
|
|
45
|
+
* with the visitor's location and `ctx.rewrittenFrom`.
|
|
46
|
+
* - `"target"`: the target's shared shell. `usePathname()` reads the target's
|
|
47
|
+
* path on a HIT, and values set before the throw are baked in: for a page
|
|
48
|
+
* that renders neither (a lock or promo page).
|
|
49
|
+
*/
|
|
50
|
+
export type RewriteShell = "visitor" | "target";
|
|
51
|
+
|
|
42
52
|
/**
|
|
43
53
|
* The value `rewrite()` returns, thrown by middleware. An Error, so a throw
|
|
44
54
|
* where a rewrite is not supported fails loudly with the rule in its message.
|
|
@@ -46,11 +56,60 @@ export function describeRewriteTarget(target: RewriteTarget): string {
|
|
|
46
56
|
export class Rewrite extends Error {
|
|
47
57
|
name = "Rewrite" as const;
|
|
48
58
|
readonly target: RewriteTarget;
|
|
59
|
+
/** The `shell()` choice; undefined: no shell. */
|
|
60
|
+
readonly shellAudience: RewriteShell | undefined;
|
|
61
|
+
/** `disallowMutations()`: a mutation is refused, not moved, before its action runs. */
|
|
62
|
+
readonly mutationsDisallowed: boolean;
|
|
49
63
|
|
|
50
|
-
constructor(
|
|
64
|
+
constructor(
|
|
65
|
+
target: RewriteTarget,
|
|
66
|
+
shellAudience?: RewriteShell,
|
|
67
|
+
mutationsDisallowed = false,
|
|
68
|
+
) {
|
|
51
69
|
super(`${describeRewriteTarget(target)}: ${REWRITE_RULE}`);
|
|
52
70
|
Object.setPrototypeOf(this, Rewrite.prototype);
|
|
53
71
|
this.target = target;
|
|
72
|
+
this.shellAudience = shellAudience;
|
|
73
|
+
this.mutationsDisallowed = mutationsDisallowed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A copy that picks the PPR shell of a `ppr` target ({@link RewriteShell}),
|
|
78
|
+
* so a rewrite kept in a module constant keeps its own choice:
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* throw rewrite("item", { id }).shell("visitor");
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
shell(audience: RewriteShell): Rewrite {
|
|
85
|
+
if (audience !== "visitor" && audience !== "target") {
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
`${describeRewriteTarget(this.target)}.shell() takes "visitor" or "target", not ${JSON.stringify(audience)}.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return new Rewrite(this.target, audience, this.mutationsDisallowed);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A copy for a lock: global middleware's rewrite of a mutation (a server
|
|
95
|
+
* action, a form post, any non-GET request) is an error before the action
|
|
96
|
+
* runs, instead of moving the mutation to the target. GET and HEAD rewrite
|
|
97
|
+
* as usual.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* throw rewrite("maintenance").disallowMutations();
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* Route middleware runs after the action, so there it cannot refuse it: the
|
|
104
|
+
* render moves to the target, dev warns, and production logs one error.
|
|
105
|
+
*/
|
|
106
|
+
disallowMutations(): Rewrite {
|
|
107
|
+
if (arguments.length > 0) {
|
|
108
|
+
throw new TypeError(
|
|
109
|
+
`${describeRewriteTarget(this.target)}.disallowMutations() takes no arguments.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return new Rewrite(this.target, this.shellAudience, true);
|
|
54
113
|
}
|
|
55
114
|
}
|
|
56
115
|
Object.defineProperty(Rewrite.prototype, REWRITE_BRAND, { value: true });
|
|
@@ -107,31 +166,39 @@ function createRewrite(
|
|
|
107
166
|
* ```ts
|
|
108
167
|
* router.use(async (ctx, next) => {
|
|
109
168
|
* if (!locked || ctx.pathname === "/maintenance") return next();
|
|
110
|
-
* throw rewrite("maintenance");
|
|
169
|
+
* throw rewrite("maintenance").disallowMutations();
|
|
111
170
|
* });
|
|
112
171
|
* ```
|
|
113
172
|
*
|
|
114
173
|
* The request runs again as the target (global middleware, route
|
|
115
174
|
* middleware, render) with the same request context: values set before the
|
|
116
175
|
* throw stay readable. `ctx.url` is the target; `ctx.rewrittenFrom` is the
|
|
117
|
-
* visitor's URL.
|
|
176
|
+
* visitor's URL. A mutation that global middleware rewrites moves to the
|
|
177
|
+
* target before its action runs, so the page's forms keep working; a lock
|
|
178
|
+
* refuses it with `.disallowMutations()`. A `ppr` target gets no shell unless
|
|
179
|
+
* `.shell()` picks one ({@link RewriteShell}). See the `/middleware` skill.
|
|
118
180
|
*/
|
|
119
181
|
export const rewrite: RewriteFunction = createRewrite as RewriteFunction;
|
|
120
182
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
183
|
+
/**
|
|
184
|
+
* A dispatch site that cannot honor a rewrite. "mutation" is the refusal
|
|
185
|
+
* `.disallowMutations()` asked for, not a misuse.
|
|
186
|
+
*/
|
|
187
|
+
export type RewriteSite = "mutation" | "body-read" | "loader-fetch";
|
|
123
188
|
|
|
124
189
|
const SITE_MESSAGE: Record<RewriteSite, string> = {
|
|
125
190
|
mutation:
|
|
126
|
-
"
|
|
127
|
-
"or
|
|
128
|
-
|
|
191
|
+
"refused a mutation: .disallowMutations() was set, so a server action " +
|
|
192
|
+
"or form post (any request but GET and HEAD) fails before its action " +
|
|
193
|
+
"runs. To answer it instead, return a Response for mutations from " +
|
|
194
|
+
'middleware, e.g. new Response("Maintenance", { status: 503 }).',
|
|
195
|
+
"body-read":
|
|
196
|
+
"is not supported here: middleware read the request body before it " +
|
|
197
|
+
"threw it, and the request moves to the target with its body for the " +
|
|
198
|
+
"action to read. Read a copy instead: ctx.request.clone().formData().",
|
|
129
199
|
"loader-fetch":
|
|
130
|
-
"middleware threw it during a loader fetch
|
|
131
|
-
"useRefreshLoaders). Return a Response there.",
|
|
132
|
-
intercept:
|
|
133
|
-
"intercept middleware threw it. Rewrite from router.use() or the " +
|
|
134
|
-
"route's middleware(), or return a Response.",
|
|
200
|
+
"is not supported here: middleware threw it during a loader fetch " +
|
|
201
|
+
"(useFetchLoader, load(), useRefreshLoaders). Return a Response there.",
|
|
135
202
|
};
|
|
136
203
|
|
|
137
204
|
/** The error a rewrite becomes at a site that cannot honor it. */
|
|
@@ -140,7 +207,7 @@ export function rewriteNotSupported(
|
|
|
140
207
|
site: RewriteSite,
|
|
141
208
|
): Error {
|
|
142
209
|
return new Error(
|
|
143
|
-
`${describeRewriteTarget(rewrite.target)}
|
|
210
|
+
`${describeRewriteTarget(rewrite.target)} ${SITE_MESSAGE[site]}`,
|
|
144
211
|
{ cause: rewrite },
|
|
145
212
|
);
|
|
146
213
|
}
|
package/src/route-types.ts
CHANGED
|
@@ -160,7 +160,7 @@ export type IncludeItem = {
|
|
|
160
160
|
* include cannot collide with siblings declared outside it.
|
|
161
161
|
*/
|
|
162
162
|
includeScope?: string;
|
|
163
|
-
};
|
|
163
|
+
} & import("./server/context.js").IncludeScopePosition;
|
|
164
164
|
};
|
|
165
165
|
|
|
166
166
|
/**
|
|
@@ -221,7 +221,6 @@ export type ParallelUseItem =
|
|
|
221
221
|
| NotFoundBoundaryItem
|
|
222
222
|
| TransitionItem;
|
|
223
223
|
export type InterceptUseItem =
|
|
224
|
-
| MiddlewareItem
|
|
225
224
|
| LoaderItem
|
|
226
225
|
| LoadingItem
|
|
227
226
|
| LayoutItem
|
package/src/router/find-match.ts
CHANGED
|
@@ -21,6 +21,30 @@ function cloneMatchResult<TEnv>(
|
|
|
21
21
|
return r ? { ...r, params: { ...r.params } } : null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function describeUnownedTrieRoute(
|
|
25
|
+
pathname: string,
|
|
26
|
+
routeKey: string,
|
|
27
|
+
sp: string,
|
|
28
|
+
entries: RouteEntry[],
|
|
29
|
+
): string {
|
|
30
|
+
const includes = entries
|
|
31
|
+
.filter((e) => e.staticPrefix === sp)
|
|
32
|
+
.map((e) => {
|
|
33
|
+
const prefix = (e as { _lazyPrefix?: string })._lazyPrefix;
|
|
34
|
+
if (prefix === undefined) return "root routes";
|
|
35
|
+
const names = e.lazyContext?.namePrefix;
|
|
36
|
+
return `include("${prefix}")${names ? ` naming "${names}.*"` : ""}`;
|
|
37
|
+
});
|
|
38
|
+
return (
|
|
39
|
+
`[@rangojs/router] The route trie matched "${pathname}" to route ` +
|
|
40
|
+
`"${routeKey}" under static prefix "${sp}", but no include there ` +
|
|
41
|
+
`registered that name at runtime (checked: ${includes.join(", ")}). ` +
|
|
42
|
+
`Build discovery and the runtime named this route differently, so it ` +
|
|
43
|
+
`cannot render. If this follows a route edit, reload the page; if it ` +
|
|
44
|
+
`persists, please report it with your route configuration.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export interface FindMatchDeps<TEnv = any> {
|
|
25
49
|
routesEntries: RouteEntry<TEnv>[];
|
|
26
50
|
// Returns a Promise only when it had to resolve an async include provider
|
|
@@ -100,7 +124,7 @@ export function createFindMatch<TEnv = any>(
|
|
|
100
124
|
// this loop may evaluate a candidate that is NOT the route's owner.
|
|
101
125
|
// Remember the failure and keep scanning: if another candidate owns
|
|
102
126
|
// the route, this sibling's failure is correctly isolated (its
|
|
103
|
-
//
|
|
127
|
+
// in-flight import was cleared, so a later request retries). But if NO
|
|
104
128
|
// candidate ends up owning it, the failing one WAS the owner — a
|
|
105
129
|
// real, trie-matched route whose module failed to import — and we
|
|
106
130
|
// rethrow below as a 5xx rather than silently 404ing (see finding 3:
|
|
@@ -130,6 +154,18 @@ export function createFindMatch<TEnv = any>(
|
|
|
130
154
|
throw candidateError;
|
|
131
155
|
}
|
|
132
156
|
|
|
157
|
+
// No candidate owns the trie's key (#1104): dev throws, production keeps the 404 fallback.
|
|
158
|
+
if (!entry && fallbackEntry && process.env.NODE_ENV !== "production") {
|
|
159
|
+
throw new Error(
|
|
160
|
+
describeUnownedTrieRoute(
|
|
161
|
+
pathname,
|
|
162
|
+
trieResult.routeKey,
|
|
163
|
+
trieResult.sp,
|
|
164
|
+
deps.routesEntries,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
133
169
|
if (!entry) entry = fallbackEntry;
|
|
134
170
|
|
|
135
171
|
if (!entry) {
|