@timber-js/app 0.2.0-alpha.198 → 0.2.0-alpha.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-dVDi1GQz.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-dVDi1GQz.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-Ph6luW4c.js → convention-lint-fRkwVwEH.js} +25 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-Ph6luW4c.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -20,11 +20,11 @@ import { createElement, Fragment } from 'react';
|
|
|
20
20
|
import { withSpan } from './tracing.ts';
|
|
21
21
|
import type { RouteMatch } from './pipeline.ts';
|
|
22
22
|
import type { ManifestSegmentNode } from './route-matcher.ts';
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
23
|
+
import { isDenySignal, DenySignal } from './primitives.ts';
|
|
24
|
+
import { isClientReference } from './utils/element-type.ts';
|
|
25
|
+
import { loadRouteModules, type MetadataSource, type AccessFn } from './route-module-loader.ts';
|
|
25
26
|
import { MetadataCollector, MetadataHead } from './metadata-collector.ts';
|
|
26
27
|
import { AccessGate } from './access-gate.tsx';
|
|
27
|
-
import { requestContextAls } from './als-registry.ts';
|
|
28
28
|
import { PageDenyBoundary, buildDenyPageChain, handleCaughtDeny } from './deny-boundary.ts';
|
|
29
29
|
import type { DenyPageEntry } from './deny-boundary.ts';
|
|
30
30
|
import {
|
|
@@ -42,115 +42,13 @@ import { SegmentOutlet } from '../client/segment-outlet.tsx';
|
|
|
42
42
|
import { TimberErrorBoundary } from '../client/error-boundary.tsx';
|
|
43
43
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.ts';
|
|
44
44
|
import type { InterceptionContext } from './pipeline.ts';
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
computeSegmentKeys,
|
|
49
|
-
computeSegmentTreePaths,
|
|
50
|
-
computeSlotKey,
|
|
51
|
-
} from '../routing/segment-keys.ts';
|
|
45
|
+
import type { ClientStateTree } from './state-tree-diff.ts';
|
|
46
|
+
import { computeSkippableSet } from './skippable-prefix.ts';
|
|
47
|
+
import { computeSegmentKeys, computeSegmentTreePaths } from '../routing/segment-keys.ts';
|
|
52
48
|
import { setFirstDeniedSegmentIndex } from './deny-boundary.ts';
|
|
53
49
|
import { loadModule } from './safe-load.ts';
|
|
50
|
+
import { replaceOutermostSegmentOutlet } from './route-element-helpers.ts';
|
|
54
51
|
|
|
55
|
-
/**
|
|
56
|
-
* Replace the outermost SegmentOutlet in a partial payload with its
|
|
57
|
-
* SegmentProvider child. Walks through AccessGate wrappers (which sit
|
|
58
|
-
* outside SegmentOutlet) to find the outlet.
|
|
59
|
-
*/
|
|
60
|
-
function replaceOutermostSegmentOutlet(
|
|
61
|
-
element: React.ReactElement,
|
|
62
|
-
replacement: React.ReactElement
|
|
63
|
-
): React.ReactElement {
|
|
64
|
-
if (element.type === SegmentOutlet) {
|
|
65
|
-
return replacement;
|
|
66
|
-
}
|
|
67
|
-
// AccessGate and MetadataCollector wrap outside SegmentOutlet — recurse through them.
|
|
68
|
-
const props = element.props as Record<string, unknown>;
|
|
69
|
-
if ((element.type === AccessGate || element.type === MetadataCollector) && props.children) {
|
|
70
|
-
const inner = replaceOutermostSegmentOutlet(props.children as React.ReactElement, replacement);
|
|
71
|
-
/* eslint-disable react/no-children-prop -- createElement API */
|
|
72
|
-
return createElement(element.type as React.FunctionComponent<Record<string, unknown>>, {
|
|
73
|
-
...props,
|
|
74
|
-
children: inner,
|
|
75
|
-
});
|
|
76
|
-
/* eslint-enable react/no-children-prop */
|
|
77
|
-
}
|
|
78
|
-
return element;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Whether a layout's returned tree still contains its children boundary.
|
|
83
|
-
*
|
|
84
|
-
* Layouts may forward `children` through a non-children prop or clone it with
|
|
85
|
-
* React.Children/cloneElement. Checking only `props.children` or object
|
|
86
|
-
* identity would incorrectly classify those layouts as suppressing their
|
|
87
|
-
* descendants and let shell settlement complete before an async child deny.
|
|
88
|
-
*/
|
|
89
|
-
export function containsLayoutChildren(
|
|
90
|
-
node: unknown,
|
|
91
|
-
boundaryType: React.ElementType,
|
|
92
|
-
seen = new WeakSet<object>()
|
|
93
|
-
): boolean {
|
|
94
|
-
if (node == null || typeof node !== 'object') return false;
|
|
95
|
-
if (seen.has(node)) return false;
|
|
96
|
-
seen.add(node);
|
|
97
|
-
try {
|
|
98
|
-
if (Array.isArray(node)) {
|
|
99
|
-
return node.some((child) => containsLayoutChildren(child, boundaryType, seen));
|
|
100
|
-
}
|
|
101
|
-
if ('$$typeof' in node && 'type' in node && 'props' in node) {
|
|
102
|
-
if (node.type === boundaryType) return true;
|
|
103
|
-
// React's development element also exposes `_owner`; following it can
|
|
104
|
-
// walk back into the currently rendering layout's pending props and
|
|
105
|
-
// falsely "find" a child that the returned tree actually suppressed.
|
|
106
|
-
return containsLayoutChildren(node.props, boundaryType, seen);
|
|
107
|
-
}
|
|
108
|
-
const descriptors = Object.getOwnPropertyDescriptors(node);
|
|
109
|
-
return Object.values(descriptors).some(
|
|
110
|
-
(descriptor) =>
|
|
111
|
-
'value' in descriptor &&
|
|
112
|
-
descriptor.enumerable === true &&
|
|
113
|
-
containsLayoutChildren(descriptor.value, boundaryType, seen)
|
|
114
|
-
);
|
|
115
|
-
} catch {
|
|
116
|
-
// An opaque return value cannot safely prove that it retained the
|
|
117
|
-
// boundary. Treat it as suppression so an uninspectable Proxy cannot
|
|
118
|
-
// strand every descendant settlement promise and hang the response.
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function descendantSettleCallbacks(
|
|
124
|
-
callbacks: Array<() => void>,
|
|
125
|
-
layoutSettled: (() => void) | undefined
|
|
126
|
-
): Array<() => void> {
|
|
127
|
-
// A tracked layout appended its own callback last; descendants are every
|
|
128
|
-
// callback before it. An untracked layout appended nothing, so all current
|
|
129
|
-
// callbacks belong to descendants. `slice(0, undefined)` is not equivalent
|
|
130
|
-
// to `slice()` — it returns an empty array.
|
|
131
|
-
return layoutSettled ? callbacks.slice(0, -1) : callbacks.slice();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ─── Client Reference Detection ──────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Symbol used by React Flight to mark client references.
|
|
138
|
-
* Client references are proxy objects created by @vitejs/plugin-rsc for
|
|
139
|
-
* 'use client' modules in the RSC environment. They must be passed to
|
|
140
|
-
* createElement() — calling them as functions throws:
|
|
141
|
-
* "Unexpectedly client reference export 'default' is called on server"
|
|
142
|
-
*/
|
|
143
|
-
const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Detect whether a component is a React client reference.
|
|
147
|
-
* Client references have $$typeof set to Symbol.for('react.client.reference')
|
|
148
|
-
* by registerClientReference() in the React Flight server runtime.
|
|
149
|
-
*
|
|
150
|
-
* Used to skip OTEL tracing wrappers that would call the component as a
|
|
151
|
-
* function. Client components must go through createElement only — they are
|
|
152
|
-
* serialized as references in the RSC Flight stream, not executed on the server.
|
|
153
|
-
*/
|
|
154
52
|
/**
|
|
155
53
|
* The `segments` array handed to a layout's `SegmentProvider` — the value
|
|
156
54
|
* `useSelectedLayoutSegment(s)` reads.
|
|
@@ -173,27 +71,6 @@ export function computeLayoutSegmentPath(
|
|
|
173
71
|
return parts.length === 0 ? [''] : ['', ...parts];
|
|
174
72
|
}
|
|
175
73
|
|
|
176
|
-
export function isClientReference(component: unknown): boolean {
|
|
177
|
-
return (
|
|
178
|
-
component != null &&
|
|
179
|
-
typeof component === 'function' &&
|
|
180
|
-
(component as unknown as Record<string, unknown>).$$typeof === CLIENT_REFERENCE_TAG
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ─── Typed Errors ────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Thrown when an app/schema.ts segment-param codec's parse() fails.
|
|
188
|
-
* The pipeline catches this and responds with 404.
|
|
189
|
-
*/
|
|
190
|
-
export class ParamCoercionError extends Error {
|
|
191
|
-
constructor(message: string) {
|
|
192
|
-
super(message);
|
|
193
|
-
this.name = 'ParamCoercionError';
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
74
|
/**
|
|
198
75
|
* Thrown when a matched route has no page component in its leaf segment.
|
|
199
76
|
* The pipeline catches this and renders the custom 404 page.
|
|
@@ -234,51 +111,73 @@ export interface RouteElementResult {
|
|
|
234
111
|
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
235
112
|
*/
|
|
236
113
|
skippedSegments: string[];
|
|
237
|
-
/**
|
|
238
|
-
* Resolves when every deny-capable shell component (PageDenyBoundary and
|
|
239
|
-
* each TracedLayout with a deny page chain) has finished executing, OR
|
|
240
|
-
* immediately when any of them catches a DenySignal in-tree (the HTTP
|
|
241
|
-
* status is known at that point; inner components may never run).
|
|
242
|
-
* Undefined when the route has no deny-capable server components.
|
|
243
|
-
* See TIM-1045 (pages), TIM-1208 (layouts).
|
|
244
|
-
*/
|
|
245
|
-
shellSettled?: Promise<void>;
|
|
246
114
|
/**
|
|
247
115
|
* Slot metadata for the X-Timber-Segments header. Contains info about
|
|
248
116
|
* all parallel route slots encountered during element tree construction,
|
|
249
117
|
* enabling the client to populate its segment cache with slot entries.
|
|
250
118
|
*/
|
|
251
119
|
slotSkipInfo: SlotSkipEntry[];
|
|
120
|
+
/**
|
|
121
|
+
* The deny page chain built from the leaf segment outward to root.
|
|
122
|
+
* Threaded into `renderRscStream` for late addressing of nested-component
|
|
123
|
+
* denies in Flight's `onError`. See TIM-1450.
|
|
124
|
+
*/
|
|
125
|
+
leafDenyChain?: DenyPageEntry[];
|
|
252
126
|
}
|
|
253
127
|
|
|
254
|
-
// ───
|
|
128
|
+
// ─── Wrapping Helpers ────────────────────────────────────────────────────
|
|
255
129
|
|
|
256
130
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
131
|
+
* Wrap an element in MetadataCollector(s) for one segment's metadata sources.
|
|
132
|
+
*
|
|
133
|
+
* The loop wraps innermost-to-outermost, so the last-wrapped collector
|
|
134
|
+
* renders first. `.reverse()` ensures layout metadata is pushed before
|
|
135
|
+
* page metadata, preserving the root→page merge order required by
|
|
136
|
+
* resolveMetadata (page-authoritative). See TIM-1363.
|
|
259
137
|
*/
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
138
|
+
function wrapMetadataCollectors(
|
|
139
|
+
element: React.ReactElement,
|
|
140
|
+
sources: MetadataSource[],
|
|
141
|
+
h: (...args: unknown[]) => React.ReactElement
|
|
142
|
+
): React.ReactElement {
|
|
143
|
+
for (const meta of sources) {
|
|
144
|
+
element = h(MetadataCollector, {
|
|
145
|
+
metadataExport: meta.metadataExport,
|
|
146
|
+
segmentName: meta.segmentName,
|
|
147
|
+
isPage: meta.isPage,
|
|
148
|
+
children: element,
|
|
149
|
+
});
|
|
270
150
|
}
|
|
151
|
+
return element;
|
|
271
152
|
}
|
|
272
153
|
|
|
273
154
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
155
|
+
* Wrap an element in an AccessGate for one segment.
|
|
156
|
+
*
|
|
157
|
+
* Skipped segments pass `skippedSegment: true` and omit `denyPages`.
|
|
158
|
+
* Rendered segments pass `denyPages` and omit `skippedSegment`.
|
|
276
159
|
*/
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
160
|
+
function wrapAccessGate(
|
|
161
|
+
element: React.ReactElement,
|
|
162
|
+
opts: {
|
|
163
|
+
accessFn: () => unknown;
|
|
164
|
+
segment: ManifestSegmentNode;
|
|
165
|
+
segmentKey: string;
|
|
166
|
+
segmentIndex: number;
|
|
167
|
+
denyPages?: DenyPageEntry[];
|
|
168
|
+
skippedSegment?: boolean;
|
|
169
|
+
},
|
|
170
|
+
h: (...args: unknown[]) => React.ReactElement
|
|
171
|
+
): React.ReactElement {
|
|
172
|
+
return h(AccessGate, {
|
|
173
|
+
accessFn: opts.accessFn,
|
|
174
|
+
segmentName: opts.segment.segmentName,
|
|
175
|
+
segmentKey: opts.segmentKey,
|
|
176
|
+
segmentIndex: opts.segmentIndex,
|
|
177
|
+
...(opts.skippedSegment ? { skippedSegment: true } : {}),
|
|
178
|
+
...(opts.denyPages ? { denyPages: opts.denyPages } : {}),
|
|
179
|
+
children: element,
|
|
180
|
+
});
|
|
282
181
|
}
|
|
283
182
|
|
|
284
183
|
// ─── Builder ──────────────────────────────────────────────────────────────
|
|
@@ -286,10 +185,9 @@ function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number):
|
|
|
286
185
|
/**
|
|
287
186
|
* Build a React element tree from a matched route.
|
|
288
187
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* as AccessGate verdicts for synchronous replay during render.
|
|
188
|
+
* Loads route modules, resolves metadata (skipping denied segments via
|
|
189
|
+
* AccessGate), and constructs the element tree. Access checks run during
|
|
190
|
+
* render inside AccessGate — denied segments' metadata never executes.
|
|
293
191
|
*
|
|
294
192
|
* Does NOT serialize to RSC Flight — the caller decides whether to render
|
|
295
193
|
* to a stream or use the element directly (e.g., for action revalidation).
|
|
@@ -314,74 +212,8 @@ export async function buildRouteElement(
|
|
|
314
212
|
// placed below each AccessGate — denied segments' metadata() never
|
|
315
213
|
// executes because AccessGate prevents MetadataCollector from rendering.
|
|
316
214
|
// No pre-render access pass needed. See TIM-1363.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
segmentName: string;
|
|
320
|
-
isPage: boolean;
|
|
321
|
-
segmentIndex: number;
|
|
322
|
-
}
|
|
323
|
-
const metadataSources: MetadataSource[] = [];
|
|
324
|
-
const layoutComponents: LayoutComponentEntry[] = [];
|
|
325
|
-
let PageComponent: ((...args: unknown[]) => unknown) | null = null;
|
|
326
|
-
let deferSuspenseFor = 0;
|
|
327
|
-
|
|
328
|
-
for (let i = 0; i < segments.length; i++) {
|
|
329
|
-
const segment = segments[i];
|
|
330
|
-
const isLeaf = i === segments.length - 1;
|
|
331
|
-
|
|
332
|
-
// Load layout
|
|
333
|
-
if (segment.layout) {
|
|
334
|
-
const mod = await loadModule(segment.layout);
|
|
335
|
-
if (mod.default) {
|
|
336
|
-
layoutComponents.push({
|
|
337
|
-
component: mod.default as (...args: unknown[]) => unknown,
|
|
338
|
-
segment,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
|
|
343
|
-
if (mod.metadata) {
|
|
344
|
-
metadataSources.push({
|
|
345
|
-
metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
|
|
346
|
-
segmentName: segment.segmentName ?? segment.urlPath,
|
|
347
|
-
isPage: false,
|
|
348
|
-
segmentIndex: i,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Load page (leaf segment only)
|
|
355
|
-
if (isLeaf && segment.page) {
|
|
356
|
-
const mod = await loadModule(segment.page);
|
|
357
|
-
|
|
358
|
-
if (mod.default) {
|
|
359
|
-
PageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
360
|
-
}
|
|
361
|
-
rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
|
|
362
|
-
if (mod.metadata) {
|
|
363
|
-
metadataSources.push({
|
|
364
|
-
metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
|
|
365
|
-
segmentName: segment.segmentName ?? segment.urlPath,
|
|
366
|
-
isPage: true,
|
|
367
|
-
segmentIndex: i,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (!PageComponent) {
|
|
375
|
-
const segmentInfo = segments
|
|
376
|
-
.map(
|
|
377
|
-
(s, i) =>
|
|
378
|
-
` [${i}] ${s.segmentName} (page: ${s.page ? 'yes' : 'no'}, layout: ${s.layout ? 'yes' : 'no'}, children: ${s.children?.length ?? 0})${i === segments.length - 1 ? ' ← leaf' : ''}`
|
|
379
|
-
)
|
|
380
|
-
.join('\n');
|
|
381
|
-
throw new NoPageComponentError(
|
|
382
|
-
`No page component found for route: ${new URL(req.url).pathname}\nMatched segments:\n${segmentInfo}`
|
|
383
|
-
);
|
|
384
|
-
}
|
|
215
|
+
const { metadataSources, layoutComponents, PageComponent, deferSuspenseFor, accessFns } =
|
|
216
|
+
await loadRouteModules(segments, req.url);
|
|
385
217
|
|
|
386
218
|
// Compute state-tree keys for all segments. Route groups accumulate
|
|
387
219
|
// ancestor group names for globally unique keys (e.g., "/(a)/(shared)"
|
|
@@ -414,31 +246,8 @@ export async function buildRouteElement(
|
|
|
414
246
|
}
|
|
415
247
|
|
|
416
248
|
// Build element tree: page wrapped in layouts (innermost to outermost)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// Shell settlement tracking (TIM-1045 pages, TIM-1208 layouts).
|
|
420
|
-
// Multiple deny-capable components can suspend independently. We need all
|
|
421
|
-
// of them to settle before committing the HTTP status.
|
|
422
|
-
//
|
|
423
|
-
// Each tracked component gets its own deferred. shellSettled resolves
|
|
424
|
-
// when ALL settle (Promise.all) OR when any in-tree deny fires
|
|
425
|
-
// (short-circuit via onDenyStatus — covers AccessGate, TracedLayout,
|
|
426
|
-
// and PageDenyBoundary without threading callbacks into each).
|
|
427
|
-
const shellPromises: Promise<void>[] = [];
|
|
428
|
-
const shellSettleCallbacks: Array<() => void> = [];
|
|
429
|
-
let resolveShortCircuit: (() => void) | undefined;
|
|
430
|
-
const trackShellComponent = (): (() => void) => {
|
|
431
|
-
let resolve!: () => void;
|
|
432
|
-
shellPromises.push(new Promise<void>((r) => (resolve = r)));
|
|
433
|
-
let settled = false;
|
|
434
|
-
const settle = () => {
|
|
435
|
-
if (settled) return;
|
|
436
|
-
settled = true;
|
|
437
|
-
resolve();
|
|
438
|
-
};
|
|
439
|
-
shellSettleCallbacks.push(settle);
|
|
440
|
-
return settle;
|
|
441
|
-
};
|
|
249
|
+
type CreateElementFn = (...args: unknown[]) => React.ReactElement;
|
|
250
|
+
const h: CreateElementFn = createElement as CreateElementFn;
|
|
442
251
|
|
|
443
252
|
// Build the page element.
|
|
444
253
|
// Server component pages are wrapped in PageDenyBoundary which calls
|
|
@@ -454,13 +263,11 @@ export async function buildRouteElement(
|
|
|
454
263
|
if (isClientReference(PageComponent)) {
|
|
455
264
|
element = h(PageComponent, {});
|
|
456
265
|
} else if (leafDenyPages && leafDenyPages.length > 0) {
|
|
457
|
-
const onSettled = trackShellComponent();
|
|
458
266
|
element = h(PageDenyBoundary, {
|
|
459
267
|
Page: PageComponent,
|
|
460
268
|
route: match.segments[leafIndex]?.urlPath ?? '/',
|
|
461
269
|
denyPages: leafDenyPages,
|
|
462
270
|
segmentKey: segmentKeys[leafIndex],
|
|
463
|
-
onSettled,
|
|
464
271
|
});
|
|
465
272
|
} else {
|
|
466
273
|
// Server component page WITHOUT deny page chain — trace only
|
|
@@ -503,86 +310,15 @@ export async function buildRouteElement(
|
|
|
503
310
|
const skippedSegments: string[] = [];
|
|
504
311
|
|
|
505
312
|
// Pre-compute the contiguous skippable prefix (top-down).
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// outlet there to receive the partial payload).
|
|
516
|
-
const skippableSet = new Set<number>();
|
|
517
|
-
if (clientStateTree) {
|
|
518
|
-
for (let i = 0; i < segments.length; i++) {
|
|
519
|
-
const isLeaf = i === segments.length - 1;
|
|
520
|
-
const layoutComponent = layoutBySegment.get(segments[i]);
|
|
521
|
-
|
|
522
|
-
if (shouldSkipSegment(segmentKeys[i], layoutComponent, isLeaf, clientStateTree)) {
|
|
523
|
-
skippableSet.add(i);
|
|
524
|
-
} else if (layoutComponent) {
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Ensure there's a merge point: a non-skipped layout whose key is
|
|
530
|
-
// in the client's state tree (so the client has a mounted outlet).
|
|
531
|
-
// If all layouts are in the prefix, keep the innermost as merge point.
|
|
532
|
-
if (skippableSet.size > 0) {
|
|
533
|
-
let mergePointIndex = -1;
|
|
534
|
-
for (let i = 0; i < segments.length; i++) {
|
|
535
|
-
if (skippableSet.has(i)) continue;
|
|
536
|
-
if (layoutBySegment.has(segments[i])) {
|
|
537
|
-
mergePointIndex = i;
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (mergePointIndex === -1) {
|
|
543
|
-
// All layouts are skippable — keep the innermost as merge point
|
|
544
|
-
for (let i = segments.length - 1; i >= 0; i--) {
|
|
545
|
-
if (skippableSet.has(i) && layoutBySegment.has(segments[i])) {
|
|
546
|
-
skippableSet.delete(i);
|
|
547
|
-
mergePointIndex = i;
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Cross-section guard: at least one rendered layout below the
|
|
554
|
-
// prefix must exist in the client's state tree (the client needs
|
|
555
|
-
// a mounted outlet to receive the partial payload). Check ALL
|
|
556
|
-
// non-skipped layouts, not just the merge point — async layouts
|
|
557
|
-
// are excluded from the client state tree but may sit above a
|
|
558
|
-
// sync layout that IS in the tree.
|
|
559
|
-
let hasClientOutlet = false;
|
|
560
|
-
for (let j = 0; j < segments.length; j++) {
|
|
561
|
-
if (skippableSet.has(j)) continue;
|
|
562
|
-
if (layoutBySegment.has(segments[j]) && clientStateTree.has(segmentKeys[j])) {
|
|
563
|
-
hasClientOutlet = true;
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (!hasClientOutlet) {
|
|
568
|
-
skippableSet.clear();
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Slot-change guard (TIM-1232): if a skippable layout has parallel
|
|
574
|
-
// slots whose content changed, the layout cannot be skipped — it must
|
|
575
|
-
// render so its slots are included in the payload.
|
|
576
|
-
if (skippableSet.size > 0 && clientStateTree) {
|
|
577
|
-
truncateSkippableForChangedSlots(
|
|
578
|
-
skippableSet,
|
|
579
|
-
segments,
|
|
580
|
-
segmentKeys,
|
|
581
|
-
match,
|
|
582
|
-
clientStateTree,
|
|
583
|
-
req
|
|
584
|
-
);
|
|
585
|
-
}
|
|
313
|
+
// See skippable-prefix.ts for the three sub-rules and slot-change guard.
|
|
314
|
+
const skippableSet = computeSkippableSet(
|
|
315
|
+
segments,
|
|
316
|
+
segmentKeys,
|
|
317
|
+
layoutBySegment,
|
|
318
|
+
clientStateTree,
|
|
319
|
+
match,
|
|
320
|
+
req
|
|
321
|
+
);
|
|
586
322
|
|
|
587
323
|
let outermostSegmentProvider: React.ReactElement | null = null;
|
|
588
324
|
|
|
@@ -630,16 +366,7 @@ export async function buildRouteElement(
|
|
|
630
366
|
// root layout title templates). MetadataCollector wraps the element
|
|
631
367
|
// inside AccessGate — if access denies, metadata is excluded.
|
|
632
368
|
const skippedMeta = metadataSources.filter((s) => s.segmentIndex === i).reverse();
|
|
633
|
-
|
|
634
|
-
const isDynamic = typeof meta.metadataExport === 'function';
|
|
635
|
-
element = h(MetadataCollector, {
|
|
636
|
-
metadataExport: meta.metadataExport,
|
|
637
|
-
segmentName: meta.segmentName,
|
|
638
|
-
isPage: meta.isPage,
|
|
639
|
-
...(isDynamic ? { onSettled: trackShellComponent() } : {}),
|
|
640
|
-
children: element,
|
|
641
|
-
});
|
|
642
|
-
}
|
|
369
|
+
element = wrapMetadataCollectors(element, skippedMeta, h);
|
|
643
370
|
|
|
644
371
|
// SECURITY: Even though the layout is skipped, AccessGate MUST still
|
|
645
372
|
// wrap the element tree. access.ts runs on every navigation regardless
|
|
@@ -648,25 +375,28 @@ export async function buildRouteElement(
|
|
|
648
375
|
//
|
|
649
376
|
// Skipped segments omit denyPages and set skippedSegment=true. If
|
|
650
377
|
// access denies at a skipped segment, AccessGate records the deny in
|
|
651
|
-
// ALS and rethrows
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
378
|
+
// ALS and rethrows. Rendering the deny page in-tree here would appear
|
|
379
|
+
// inside the client's cached layout chrome (TIM-1074), so the throw
|
|
380
|
+
// goes to Flight's onError instead: addressed there against the leaf
|
|
381
|
+
// chain, its row is claimed by the boundary the client still has
|
|
382
|
+
// mounted at the owner (TIM-1450); with no addressable file it stays
|
|
383
|
+
// unplaced, and the response path answers with rscErrorEnvelope so the
|
|
384
|
+
// client hard-navigates for a clean full render — when the deny lands
|
|
385
|
+
// inside Flight's opening window. After it, the unaddressed row reaches
|
|
386
|
+
// the terminal boundary the last full render left mounted. See TIM-1363.
|
|
387
|
+
const accessFn = accessFns[i];
|
|
388
|
+
if (accessFn) {
|
|
389
|
+
element = wrapAccessGate(
|
|
390
|
+
element,
|
|
391
|
+
{
|
|
661
392
|
accessFn,
|
|
662
|
-
|
|
393
|
+
segment,
|
|
663
394
|
segmentKey: segmentKeys[i],
|
|
664
395
|
segmentIndex: i,
|
|
665
396
|
skippedSegment: true,
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
397
|
+
},
|
|
398
|
+
h
|
|
399
|
+
);
|
|
670
400
|
}
|
|
671
401
|
|
|
672
402
|
continue;
|
|
@@ -682,16 +412,7 @@ export async function buildRouteElement(
|
|
|
682
412
|
// before page metadata, preserving the root→page merge order required
|
|
683
413
|
// by resolveMetadata (page-authoritative). See TIM-1363.
|
|
684
414
|
const segmentMetadata = metadataSources.filter((s) => s.segmentIndex === i).reverse();
|
|
685
|
-
|
|
686
|
-
const isDynamic = typeof meta.metadataExport === 'function';
|
|
687
|
-
element = h(MetadataCollector, {
|
|
688
|
-
metadataExport: meta.metadataExport,
|
|
689
|
-
segmentName: meta.segmentName,
|
|
690
|
-
isPage: meta.isPage,
|
|
691
|
-
...(isDynamic ? { onSettled: trackShellComponent() } : {}),
|
|
692
|
-
children: element,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
415
|
+
element = wrapMetadataCollectors(element, segmentMetadata, h);
|
|
695
416
|
|
|
696
417
|
// Wrap with error boundaries from this segment (inside layout).
|
|
697
418
|
// Keep ALL error boundaries (including 4xx) — they're the safety net for
|
|
@@ -729,7 +450,7 @@ export async function buildRouteElement(
|
|
|
729
450
|
interception,
|
|
730
451
|
parentTreePath,
|
|
731
452
|
destinationUrl,
|
|
732
|
-
clientStateTree: clientStateTree
|
|
453
|
+
clientStateTree: clientStateTree ?? null,
|
|
733
454
|
slotSkipInfo,
|
|
734
455
|
});
|
|
735
456
|
|
|
@@ -759,41 +480,48 @@ export async function buildRouteElement(
|
|
|
759
480
|
// fallback path. See TIM-668, design/04-authorization.md.
|
|
760
481
|
const layoutComponentRef = layoutComponent;
|
|
761
482
|
const layoutDenyPages = denyPageChains.get(i);
|
|
762
|
-
const hasLayoutDenyChain = !!layoutDenyPages && layoutDenyPages.length > 0;
|
|
763
|
-
const onLayoutSettled = hasLayoutDenyChain ? trackShellComponent() : undefined;
|
|
764
|
-
const descendantSettlers = descendantSettleCallbacks(shellSettleCallbacks, onLayoutSettled);
|
|
765
|
-
const LayoutChildrenBoundary = ({ children }: { children: React.ReactNode }) => children;
|
|
766
483
|
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
767
484
|
try {
|
|
768
|
-
|
|
485
|
+
return await withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
|
|
769
486
|
(layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
|
|
770
487
|
);
|
|
771
|
-
// A layout is allowed to suppress children. Descendant deny-capable
|
|
772
|
-
// components will never execute in that case, so settle their
|
|
773
|
-
// bookkeeping rather than leaving the RSC response waiting forever.
|
|
774
|
-
if (!containsLayoutChildren(rendered, LayoutChildrenBoundary)) {
|
|
775
|
-
for (const settle of descendantSettlers) settle();
|
|
776
|
-
}
|
|
777
|
-
return rendered;
|
|
778
488
|
} catch (error: unknown) {
|
|
779
489
|
// A layout that denied never renders its own chrome — it threw
|
|
780
490
|
// instead of returning it — so an entry this segment owns is
|
|
781
491
|
// already correctly placed here. Anything owned further up is
|
|
782
492
|
// hoisted (handleCaughtDeny throws). See TIM-1356.
|
|
783
|
-
if (error
|
|
493
|
+
if (isDenySignal(error)) {
|
|
784
494
|
setFirstDeniedSegmentIndex(i);
|
|
785
495
|
if (layoutDenyPages) {
|
|
786
496
|
return handleCaughtDeny(layoutDenyPages, error, segmentId);
|
|
787
497
|
}
|
|
788
498
|
}
|
|
789
499
|
throw error;
|
|
790
|
-
} finally {
|
|
791
|
-
onLayoutSettled?.();
|
|
792
500
|
}
|
|
793
501
|
};
|
|
794
502
|
layoutElement = h(TracedLayout, {
|
|
795
503
|
...slotProps,
|
|
796
|
-
children:
|
|
504
|
+
children: element,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// A second, deny-only copy of this segment's boundaries OUTSIDE its
|
|
508
|
+
// layout. The copy above (inside the layout) is the one a page error
|
|
509
|
+
// reaches; a server component this layout renders in its own chrome
|
|
510
|
+
// is a sibling of `children`, so its throw never passes that copy —
|
|
511
|
+
// it passes this one. Claiming is by the same key, so the segment
|
|
512
|
+
// answers a nested deny in its chrome the way `TracedLayout` answers
|
|
513
|
+
// a deny in the layout body: with its own file, replacing the layout.
|
|
514
|
+
// A hoisted deny addressed further up passes through unclaimed.
|
|
515
|
+
//
|
|
516
|
+
// Inside SegmentProvider so a partial navigation, which splices the
|
|
517
|
+
// provider into the client's mounted outlet, carries it along.
|
|
518
|
+
// See TIM-1450, design/04-authorization.md §"Where a Deny Page Renders".
|
|
519
|
+
layoutElement = await wrapSegmentWithErrorBoundaries(segment, layoutElement, {
|
|
520
|
+
loadModule,
|
|
521
|
+
createElement: h,
|
|
522
|
+
errorBoundaryComponent: TimberErrorBoundary,
|
|
523
|
+
segmentKey: segmentId,
|
|
524
|
+
denyOnly: true,
|
|
797
525
|
});
|
|
798
526
|
}
|
|
799
527
|
|
|
@@ -822,21 +550,19 @@ export async function buildRouteElement(
|
|
|
822
550
|
// Parent layouts (from outer iterations) form the shell. Access runs
|
|
823
551
|
// exactly once, during render, inside React.cache scope — layouts
|
|
824
552
|
// share the cache. See TIM-1363, design/04-authorization.md.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
element = h(AccessGate, {
|
|
553
|
+
const accessFn = accessFns[i];
|
|
554
|
+
if (accessFn) {
|
|
555
|
+
element = wrapAccessGate(
|
|
556
|
+
element,
|
|
557
|
+
{
|
|
831
558
|
accessFn,
|
|
832
|
-
|
|
559
|
+
segment,
|
|
833
560
|
segmentKey: segmentKeys[i],
|
|
834
561
|
segmentIndex: i,
|
|
835
562
|
denyPages: denyPageChains.get(i),
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
563
|
+
},
|
|
564
|
+
h
|
|
565
|
+
);
|
|
840
566
|
}
|
|
841
567
|
}
|
|
842
568
|
|
|
@@ -849,16 +575,27 @@ export async function buildRouteElement(
|
|
|
849
575
|
element = replaceOutermostSegmentOutlet(element, outermostSegmentProvider);
|
|
850
576
|
}
|
|
851
577
|
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
578
|
+
// The deny chain's terminal, for a deny that streamed as an error row and
|
|
579
|
+
// that no segment boundary — inner or outer — could claim. With the outer
|
|
580
|
+
// copies above, that is one shape only: a server component in a layout's
|
|
581
|
+
// chrome denied and no status file exists at or above that layout. Files
|
|
582
|
+
// below it are not candidates (rendering them re-runs the layout that
|
|
583
|
+
// denied), so this renders the framework default page, as the document
|
|
584
|
+
// path does for a deny with no file. A non-deny row nothing claimed ends
|
|
585
|
+
// here too: the terminal leaves the SPA so the document request renders
|
|
586
|
+
// the real error page (see TimberErrorBoundary). Outside every segment
|
|
587
|
+
// boundary, inside the payload root so hydration agrees.
|
|
588
|
+
//
|
|
589
|
+
// Skipped on partial navigations: the terminal from the last full render
|
|
590
|
+
// is still mounted above the merge point, and a copy here would sit below
|
|
591
|
+
// the skipped ancestors' boundaries and intercept denies addressed to them.
|
|
592
|
+
// See TIM-1450.
|
|
593
|
+
if (skippedSegments.length === 0) {
|
|
594
|
+
element = h(TimberErrorBoundary, {
|
|
595
|
+
lastResort: true,
|
|
596
|
+
fallbackComponent: (await import('../client/deny-last-resort.tsx')).DenyLastResort,
|
|
597
|
+
children: element,
|
|
598
|
+
});
|
|
862
599
|
}
|
|
863
600
|
|
|
864
601
|
// Publish the request's params beside the finished tree, as the payload's
|
|
@@ -872,7 +609,7 @@ export async function buildRouteElement(
|
|
|
872
609
|
segments,
|
|
873
610
|
deferSuspenseFor,
|
|
874
611
|
skippedSegments,
|
|
875
|
-
shellSettled,
|
|
876
612
|
slotSkipInfo,
|
|
613
|
+
leafDenyChain: leafDenyPages,
|
|
877
614
|
};
|
|
878
615
|
}
|