@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.187
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-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
- package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
- package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
- package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
- package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
- package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
- package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
- package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
- package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
- package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
- package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
- package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
- package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
- package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
- package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
- package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
- package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
- package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
- package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
- package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
- package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
- package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
- package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
- package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/compress-module.d.ts +12 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.js +54 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cdn/cloudflare-purge.js +30 -0
- package/dist/cdn/cloudflare-purge.js.map +1 -0
- package/dist/cdn/fastly-purge.js +33 -0
- package/dist/cdn/fastly-purge.js.map +1 -0
- package/dist/cdn/index.js +94 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/workers-cache-purge.js +35 -0
- package/dist/cdn/workers-cache-purge.js.map +1 -0
- package/dist/cli-check.d.ts +153 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli.d.ts +34 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -21
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +1 -1
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +6 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +27 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +21 -14
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +1 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +16 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +9 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +8 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +9 -3
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -2
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -477
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugin-context.d.ts +15 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +0 -9
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +24 -0
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +3 -44
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +10 -31
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +51 -0
- package/dist/routing/codegen-write.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts +18 -4
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +16 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +3 -2
- package/dist/routing/link-codegen.d.ts +19 -4
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -7
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/types.d.ts +0 -6
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +60 -9
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +62 -8
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +0 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +66 -29
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +70 -0
- package/dist/search-params/parse-total.d.ts.map +1 -0
- package/dist/search-params/wrappers.d.ts +26 -3
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +19 -8
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +16 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/compress.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +148 -15
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +85 -15
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +3 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +343 -230
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +52 -0
- package/dist/server/metadata-collector.d.ts.map +1 -0
- package/dist/server/param-coercion.d.ts +12 -5
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +23 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +1 -12
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -7
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +0 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +11 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +0 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +8 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +28 -36
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +40 -0
- package/dist/server/utils/element-type.d.ts.map +1 -0
- package/dist/shared/rsc-media-type.d.ts +40 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/33-api-search-params.mdx +38 -16
- package/docs/api/35-api-typescript.mdx +3 -3
- package/docs/api/36-cli.mdx +34 -7
- package/docs/learn/00-introduction.mdx +1 -1
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/05-typed-params.mdx +8 -8
- package/docs/learn/07-typed-routes.mdx +12 -7
- package/docs/learn/11-error-handling.mdx +16 -0
- package/docs/more/01-advanced-routing.mdx +1 -1
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/50-ai-agent-instructions.mdx +6 -4
- package/package.json +8 -5
- package/src/adapters/cloudflare.ts +4 -1
- package/src/adapters/compress-module.ts +79 -1
- package/src/cli-check.ts +458 -0
- package/src/cli.ts +59 -24
- package/src/client/browser-entry/action-dispatch.ts +2 -1
- package/src/client/browser-entry/index.ts +0 -5
- package/src/client/error-boundary.tsx +65 -1
- package/src/client/index.ts +14 -3
- package/src/client/link.tsx +65 -64
- package/src/client/navigation-commit.ts +27 -4
- package/src/client/params-context.ts +4 -4
- package/src/client/router-pipeline.ts +4 -0
- package/src/client/router.ts +1 -0
- package/src/client/rsc-fetch.ts +14 -4
- package/src/client/segment-cache.ts +8 -0
- package/src/client/use-query-states.ts +102 -39
- package/src/cookies/define-cookie.ts +6 -1
- package/src/index.ts +20 -3
- package/src/plugin-context.ts +26 -0
- package/src/plugins/routing.ts +84 -146
- package/src/plugins/shims.ts +0 -1
- package/src/plugins/static-build.ts +78 -24
- package/src/routing/codegen-shared.ts +3 -79
- package/src/routing/codegen-types.ts +10 -31
- package/src/routing/codegen-write.ts +139 -0
- package/src/routing/codegen.ts +56 -182
- package/src/routing/convention-lint.ts +139 -40
- package/src/routing/export-detect.ts +151 -7
- package/src/routing/link-codegen.ts +32 -65
- package/src/routing/manifest-codegen.ts +1 -59
- package/src/routing/scanner.ts +3 -3
- package/src/routing/types.ts +0 -6
- package/src/schema-bridge.ts +180 -58
- package/src/search-params/define.ts +102 -37
- package/src/search-params/index.ts +0 -1
- package/src/search-params/parse-total.ts +78 -0
- package/src/search-params/wrappers.ts +60 -11
- package/src/server/access-gate.tsx +60 -40
- package/src/server/action-handler.ts +1 -4
- package/src/server/als-registry.ts +16 -0
- package/src/server/compress.ts +9 -1
- package/src/server/deny-boundary.ts +269 -41
- package/src/server/deny-renderer.ts +32 -21
- package/src/server/error-boundary-wrapper.ts +166 -79
- package/src/server/index.ts +1 -3
- package/src/server/internal.ts +2 -2
- package/src/server/metadata-collector.ts +115 -0
- package/src/server/param-coercion.ts +13 -61
- package/src/server/pipeline-helpers.ts +2 -2
- package/src/server/pipeline-outcome.ts +2 -1
- package/src/server/pipeline-phases.ts +9 -9
- package/src/server/primitives.ts +25 -0
- package/src/server/route-element-builder.ts +167 -170
- package/src/server/rsc-cache-key-guard.ts +2 -8
- package/src/server/rsc-entry/deny-fallback.ts +3 -2
- package/src/server/rsc-entry/error-renderer.ts +31 -11
- package/src/server/rsc-entry/helpers.ts +2 -12
- package/src/server/rsc-entry/index.ts +0 -5
- package/src/server/rsc-entry/render-route.ts +14 -11
- package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
- package/src/server/rsc-entry/rsc-payload.ts +104 -20
- package/src/server/rsc-entry/rsc-stream.ts +25 -2
- package/src/server/rsc-entry/ssr-renderer.ts +28 -16
- package/src/server/slot-resolver.ts +10 -2
- package/src/server/ssr-bridge-types.ts +12 -0
- package/src/server/ssr-entry.ts +8 -6
- package/src/server/static-generator.ts +3 -2
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -218
- package/src/server/types.ts +12 -7
- package/src/server/utils/element-type.ts +72 -0
- package/src/shared/rsc-media-type.ts +43 -0
- package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
- package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
- package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
- package/dist/_chunks/registry-DbJPKoBp.js +0 -20
- package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
- package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
- package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
- package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
- package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
- package/dist/search-params/registry.d.ts +0 -20
- package/dist/search-params/registry.d.ts.map +0 -1
- package/dist/segment-params/define.d.ts +0 -83
- package/dist/segment-params/define.d.ts.map +0 -1
- package/dist/segment-params/index.d.ts +0 -3
- package/dist/segment-params/index.d.ts.map +0 -1
- package/dist/segment-params/index.js +0 -70
- package/dist/segment-params/index.js.map +0 -1
- package/src/search-params/registry.ts +0 -31
- package/src/segment-params/define.ts +0 -226
- package/src/segment-params/index.ts +0 -9
|
@@ -20,18 +20,12 @@ import { createElement, Fragment } from 'react';
|
|
|
20
20
|
import { withSpan } from './tracing.js';
|
|
21
21
|
import type { RouteMatch } from './pipeline.js';
|
|
22
22
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
23
|
-
import { resolveMetadata, renderMetadataToElements } from './metadata.js';
|
|
24
23
|
import type { Metadata } from './types.js';
|
|
25
|
-
import { collectMetadataRouteHeadElements } from './metadata-routes.js';
|
|
26
24
|
import { DenySignal, RedirectSignal } from './primitives.js';
|
|
25
|
+
import { MetadataCollector, MetadataHead } from './metadata-collector.js';
|
|
27
26
|
import { AccessGate } from './access-gate.js';
|
|
28
27
|
import { requestContextAls } from './als-registry.js';
|
|
29
|
-
import {
|
|
30
|
-
PageDenyBoundary,
|
|
31
|
-
buildDenyPageChain,
|
|
32
|
-
renderMatchingDenyPage,
|
|
33
|
-
setDenyStatus,
|
|
34
|
-
} from './deny-boundary.js';
|
|
28
|
+
import { PageDenyBoundary, buildDenyPageChain, handleCaughtDeny } from './deny-boundary.js';
|
|
35
29
|
import type { DenyPageEntry } from './deny-boundary.js';
|
|
36
30
|
import { LAYOUT_CHILDREN_SLOT } from './prebuilt/slots.js';
|
|
37
31
|
import { resolveSlotProps, publishSlotSegmentParams, type SlotSkipEntry } from './slot-resolver.js';
|
|
@@ -43,6 +37,7 @@ import { SegmentOutlet } from '../client/segment-outlet.js';
|
|
|
43
37
|
import { SlotOutlet } from '../client/slot-outlet.js';
|
|
44
38
|
import { SlotsProvider } from '../client/slot-provider.js';
|
|
45
39
|
|
|
40
|
+
import { TimberErrorBoundary } from '../client/error-boundary.js';
|
|
46
41
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
|
|
47
42
|
import type { InterceptionContext } from './pipeline.js';
|
|
48
43
|
import { shouldSkipSegment, type ClientStateTree } from './state-tree-diff.js';
|
|
@@ -52,7 +47,7 @@ import {
|
|
|
52
47
|
computeSegmentTreePaths,
|
|
53
48
|
computeSlotKey,
|
|
54
49
|
} from '../routing/segment-keys.js';
|
|
55
|
-
import {
|
|
50
|
+
import { setFirstDeniedSegmentIndex } from './deny-boundary.js';
|
|
56
51
|
import { loadModule } from './safe-load.js';
|
|
57
52
|
|
|
58
53
|
/**
|
|
@@ -67,9 +62,9 @@ function replaceOutermostSegmentOutlet(
|
|
|
67
62
|
if (element.type === SegmentOutlet) {
|
|
68
63
|
return replacement;
|
|
69
64
|
}
|
|
70
|
-
// AccessGate
|
|
65
|
+
// AccessGate and MetadataCollector wrap outside SegmentOutlet — recurse through them.
|
|
71
66
|
const props = element.props as Record<string, unknown>;
|
|
72
|
-
if (element.type === AccessGate && props.children) {
|
|
67
|
+
if ((element.type === AccessGate || element.type === MetadataCollector) && props.children) {
|
|
73
68
|
const inner = replaceOutermostSegmentOutlet(props.children as React.ReactElement, replacement);
|
|
74
69
|
/* eslint-disable react/no-children-prop -- createElement API */
|
|
75
70
|
return createElement(element.type as React.FunctionComponent<Record<string, unknown>>, {
|
|
@@ -134,7 +129,7 @@ export function isClientReference(component: unknown): boolean {
|
|
|
134
129
|
// ─── Typed Errors ────────────────────────────────────────────────────────
|
|
135
130
|
|
|
136
131
|
/**
|
|
137
|
-
* Thrown when
|
|
132
|
+
* Thrown when an app/schema.ts segment-param codec's parse() fails.
|
|
138
133
|
* The pipeline catches this and responds with 404.
|
|
139
134
|
*/
|
|
140
135
|
export class ParamCoercionError extends Error {
|
|
@@ -199,17 +194,6 @@ export interface RouteElementResult {
|
|
|
199
194
|
* enabling the client to populate its segment cache with slot entries.
|
|
200
195
|
*/
|
|
201
196
|
slotSkipInfo: SlotSkipEntry[];
|
|
202
|
-
/**
|
|
203
|
-
* Index of the first segment whose `access.ts` denied, or `Infinity`.
|
|
204
|
-
*
|
|
205
|
-
* AccessGate sits OUTSIDE the layout, so a denial means this segment and
|
|
206
|
-
* everything below it never rendered — no layout, no slots. It is the
|
|
207
|
-
* boundary of the tree that actually exists, which is why
|
|
208
|
-
* `collectMetadataRouteHeadElements` already filters on it and why
|
|
209
|
-
* `buildSegmentInfo` must too: the layouts were still *loaded*, so a list
|
|
210
|
-
* built from `layoutComponents` describes more than what mounted.
|
|
211
|
-
*/
|
|
212
|
-
firstDeniedIndex: number;
|
|
213
197
|
}
|
|
214
198
|
|
|
215
199
|
// ─── Module Processing Helpers ─────────────────────────────────────────────
|
|
@@ -297,47 +281,19 @@ export async function buildRouteElement(
|
|
|
297
281
|
): Promise<RouteElementResult> {
|
|
298
282
|
const segments = match.segments;
|
|
299
283
|
|
|
300
|
-
// ──
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// See TIM-1027, design/04-authorization.md, design/13-security.md.
|
|
312
|
-
const accessVerdicts = new Map<number, DenySignal | RedirectSignal>();
|
|
313
|
-
let firstDeniedIndex = Infinity;
|
|
314
|
-
|
|
315
|
-
for (let i = 0; i < segments.length; i++) {
|
|
316
|
-
const segment = segments[i];
|
|
317
|
-
if (!segment.access || i >= firstDeniedIndex) continue;
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const accessMod = await loadModule(segment.access);
|
|
321
|
-
const accessFn = accessMod.default as (() => Promise<void>) | undefined;
|
|
322
|
-
if (accessFn) {
|
|
323
|
-
await withSpan(
|
|
324
|
-
'timber.access.pre',
|
|
325
|
-
{ 'timber.segment': segment.segmentName ?? 'unknown' },
|
|
326
|
-
() => accessFn()
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
} catch (error: unknown) {
|
|
330
|
-
if (error instanceof DenySignal || error instanceof RedirectSignal) {
|
|
331
|
-
accessVerdicts.set(i, error);
|
|
332
|
-
firstDeniedIndex = i;
|
|
333
|
-
} else {
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
284
|
+
// ── Module loading ──────────────────────────────────────────────────
|
|
285
|
+
// Load layout/page modules and collect metadata sources. Metadata
|
|
286
|
+
// resolution is deferred to render time via MetadataCollector components
|
|
287
|
+
// placed below each AccessGate — denied segments' metadata() never
|
|
288
|
+
// executes because AccessGate prevents MetadataCollector from rendering.
|
|
289
|
+
// No pre-render access pass needed. See TIM-1363.
|
|
290
|
+
interface MetadataSource {
|
|
291
|
+
metadataExport: Metadata | (() => Promise<Metadata>);
|
|
292
|
+
segmentName: string;
|
|
293
|
+
isPage: boolean;
|
|
294
|
+
segmentIndex: number;
|
|
337
295
|
}
|
|
338
|
-
|
|
339
|
-
// ── Module loading + metadata resolution ─────────────────────────────
|
|
340
|
-
const metadataEntries: Array<{ metadata: Metadata; isPage: boolean }> = [];
|
|
296
|
+
const metadataSources: MetadataSource[] = [];
|
|
341
297
|
const layoutComponents: LayoutComponentEntry[] = [];
|
|
342
298
|
let PageComponent: ((...args: unknown[]) => unknown) | null = null;
|
|
343
299
|
let deferSuspenseFor = 0;
|
|
@@ -345,7 +301,6 @@ export async function buildRouteElement(
|
|
|
345
301
|
for (let i = 0; i < segments.length; i++) {
|
|
346
302
|
const segment = segments[i];
|
|
347
303
|
const isLeaf = i === segments.length - 1;
|
|
348
|
-
const isDenied = i >= firstDeniedIndex;
|
|
349
304
|
|
|
350
305
|
// Load layout
|
|
351
306
|
if (segment.layout) {
|
|
@@ -357,15 +312,14 @@ export async function buildRouteElement(
|
|
|
357
312
|
});
|
|
358
313
|
}
|
|
359
314
|
|
|
360
|
-
// Param coercion is handled in the pipeline (Stage 2c) before
|
|
361
|
-
// middleware and rendering. See coerceSegmentParams() in pipeline.ts.
|
|
362
|
-
|
|
363
315
|
rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
316
|
+
if (mod.metadata) {
|
|
317
|
+
metadataSources.push({
|
|
318
|
+
metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
|
|
319
|
+
segmentName: segment.segmentName ?? segment.urlPath,
|
|
320
|
+
isPage: false,
|
|
321
|
+
segmentIndex: i,
|
|
322
|
+
});
|
|
369
323
|
}
|
|
370
324
|
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
371
325
|
}
|
|
@@ -374,18 +328,17 @@ export async function buildRouteElement(
|
|
|
374
328
|
if (isLeaf && segment.page) {
|
|
375
329
|
const mod = await loadModule(segment.page);
|
|
376
330
|
|
|
377
|
-
// Param coercion is handled in the pipeline (Stage 2c) before
|
|
378
|
-
// middleware and rendering. See coerceSegmentParams() in pipeline.ts.
|
|
379
|
-
|
|
380
331
|
if (mod.default) {
|
|
381
332
|
PageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
382
333
|
}
|
|
383
334
|
rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
335
|
+
if (mod.metadata) {
|
|
336
|
+
metadataSources.push({
|
|
337
|
+
metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
|
|
338
|
+
segmentName: segment.segmentName ?? segment.urlPath,
|
|
339
|
+
isPage: true,
|
|
340
|
+
segmentIndex: i,
|
|
341
|
+
});
|
|
389
342
|
}
|
|
390
343
|
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
391
344
|
}
|
|
@@ -403,45 +356,36 @@ export async function buildRouteElement(
|
|
|
403
356
|
);
|
|
404
357
|
}
|
|
405
358
|
|
|
359
|
+
// Compute state-tree keys for all segments. Route groups accumulate
|
|
360
|
+
// ancestor group names for globally unique keys (e.g., "/(a)/(shared)"
|
|
361
|
+
// instead of just "/(shared)"). This is the single key computation —
|
|
362
|
+
// the same keys are used for skip decisions, SegmentOutlet props, the
|
|
363
|
+
// X-Timber-Segments header (via buildSegmentInfo), the error boundaries,
|
|
364
|
+
// and deny page ownership.
|
|
365
|
+
//
|
|
366
|
+
// Computed here, before the deny chains below, because each chain entry
|
|
367
|
+
// records the key of the segment that owns it.
|
|
368
|
+
const segmentKeys = computeSegmentKeys(segments);
|
|
369
|
+
// The directory path of each segment — the key `setSlotParams` and codegen
|
|
370
|
+
// both use for a slot beneath it, and the identity interception scoping
|
|
371
|
+
// compares against. One derivation, so a slot cannot be keyed two ways.
|
|
372
|
+
const segmentTreePaths = computeSegmentTreePaths(segments);
|
|
373
|
+
|
|
406
374
|
// Build deny page fallback chains for each segment position.
|
|
407
375
|
// When AccessGate or PageDenyBoundary catches a DenySignal, they render
|
|
408
376
|
// the matching deny page in-tree instead of throwing into React Flight.
|
|
409
377
|
// The chain walks from the current segment outward to root, collecting
|
|
410
|
-
// status-code files (403.tsx → 4xx.tsx → error.tsx) in fallback order
|
|
411
|
-
//
|
|
378
|
+
// status-code files (403.tsx → 4xx.tsx → error.tsx) in fallback order,
|
|
379
|
+
// each tagged with the key of the segment it came from so the catch site
|
|
380
|
+
// can tell a file it owns from an ancestor's. See TIM-666, TIM-1356.
|
|
412
381
|
const denyPageChains = new Map<number, DenyPageEntry[]>();
|
|
413
382
|
for (let i = 0; i < segments.length; i++) {
|
|
414
|
-
const chain = await buildDenyPageChain(segments, i);
|
|
383
|
+
const chain = await buildDenyPageChain(segments, i, segmentKeys);
|
|
415
384
|
if (chain.length > 0) {
|
|
416
385
|
denyPageChains.set(i, chain);
|
|
417
386
|
}
|
|
418
387
|
}
|
|
419
388
|
|
|
420
|
-
// Resolve metadata and convert to React elements that React 19 Float
|
|
421
|
-
// hoists into <head> during both SSR and client rendering. This replaces
|
|
422
|
-
// the previous HeadElement descriptor system (string splice for SSR,
|
|
423
|
-
// X-Timber-Head header for RSC nav, imperative DOM updates on client).
|
|
424
|
-
// React manages <title>/<meta>/<link> lifecycle across navigations.
|
|
425
|
-
// See design/16-metadata.md, TIM-1151.
|
|
426
|
-
const resolvedMetadata = resolveMetadata(metadataEntries);
|
|
427
|
-
const headElements = renderMetadataToElements(resolvedMetadata);
|
|
428
|
-
|
|
429
|
-
// Auto-link metadata route files (icon, apple-icon, manifest, opengraph-image).
|
|
430
|
-
// See design/16-metadata.md §"Auto-Linking"
|
|
431
|
-
const autoLinkedElements = collectMetadataRouteHeadElements(
|
|
432
|
-
segments,
|
|
433
|
-
firstDeniedIndex,
|
|
434
|
-
resolvedMetadata,
|
|
435
|
-
new URL(req.url),
|
|
436
|
-
metadataRouteHashes
|
|
437
|
-
);
|
|
438
|
-
headElements.push(...autoLinkedElements);
|
|
439
|
-
|
|
440
|
-
// Convert HeadElement descriptors to React elements for Float hoisting.
|
|
441
|
-
// Placed at the page segment level so they're always re-rendered on
|
|
442
|
-
// SPA navigation (sync layouts may be skipped by segment tree diffing).
|
|
443
|
-
const metadataElement = headElementsToReact(headElements);
|
|
444
|
-
|
|
445
389
|
// Build element tree: page wrapped in layouts (innermost to outermost)
|
|
446
390
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
447
391
|
|
|
@@ -482,6 +426,7 @@ export async function buildRouteElement(
|
|
|
482
426
|
Page: PageComponent,
|
|
483
427
|
route: match.segments[leafIndex]?.urlPath ?? '/',
|
|
484
428
|
denyPages: leafDenyPages,
|
|
429
|
+
segmentKey: segmentKeys[leafIndex],
|
|
485
430
|
onSettled,
|
|
486
431
|
});
|
|
487
432
|
} else {
|
|
@@ -496,11 +441,18 @@ export async function buildRouteElement(
|
|
|
496
441
|
element = h(TracedPage, {});
|
|
497
442
|
}
|
|
498
443
|
|
|
499
|
-
// Wrap page
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
444
|
+
// Wrap page in MetadataHead — a server component that reads all
|
|
445
|
+
// metadata entries collected by MetadataCollector components above it,
|
|
446
|
+
// merges them, and renders Float-hoisted head elements as siblings of
|
|
447
|
+
// the page. Placed at the page level so metadata is always re-rendered
|
|
448
|
+
// on SPA navigation (never skipped by segment tree diffing).
|
|
449
|
+
// See TIM-1363, design/16-metadata.md.
|
|
450
|
+
element = h(MetadataHead, {
|
|
451
|
+
segments,
|
|
452
|
+
requestUrl: req.url,
|
|
453
|
+
metadataRouteHashes,
|
|
454
|
+
children: element,
|
|
455
|
+
});
|
|
504
456
|
|
|
505
457
|
// Build a lookup of layout components by segment for O(1) access.
|
|
506
458
|
const layoutBySegment = new Map(
|
|
@@ -511,17 +463,6 @@ export async function buildRouteElement(
|
|
|
511
463
|
// The client uses this to merge the partial payload with its cached segments.
|
|
512
464
|
const skippedSegments: string[] = [];
|
|
513
465
|
|
|
514
|
-
// Compute state-tree keys for all segments. Route groups accumulate
|
|
515
|
-
// ancestor group names for globally unique keys (e.g., "/(a)/(shared)"
|
|
516
|
-
// instead of just "/(shared)"). This is the single key computation —
|
|
517
|
-
// the same keys are used for skip decisions, SegmentOutlet props, and
|
|
518
|
-
// the X-Timber-Segments header (via buildSegmentInfo).
|
|
519
|
-
const segmentKeys = computeSegmentKeys(segments);
|
|
520
|
-
// The directory path of each segment — the key `setSlotParams` and codegen
|
|
521
|
-
// both use for a slot beneath it, and the identity interception scoping
|
|
522
|
-
// compares against. One derivation, so a slot cannot be keyed two ways.
|
|
523
|
-
const segmentTreePaths = computeSegmentTreePaths(segments);
|
|
524
|
-
|
|
525
466
|
// Pre-compute the contiguous skippable prefix (top-down).
|
|
526
467
|
//
|
|
527
468
|
// Skipped segments must form a contiguous prefix from root to avoid
|
|
@@ -539,10 +480,6 @@ export async function buildRouteElement(
|
|
|
539
480
|
const isLeaf = i === segments.length - 1;
|
|
540
481
|
const layoutComponent = layoutBySegment.get(segments[i]);
|
|
541
482
|
|
|
542
|
-
// Never skip denied segments — their AccessGate must render the
|
|
543
|
-
// deny page, not reuse the client's cached (passing) layout.
|
|
544
|
-
if (i >= firstDeniedIndex) break;
|
|
545
|
-
|
|
546
483
|
if (shouldSkipSegment(segmentKeys[i], layoutComponent, isLeaf, clientStateTree)) {
|
|
547
484
|
skippableSet.add(i);
|
|
548
485
|
} else if (layoutComponent) {
|
|
@@ -655,20 +592,55 @@ export async function buildRouteElement(
|
|
|
655
592
|
// because an unchanged slot is what made this layout skippable.
|
|
656
593
|
publishSlotSegmentParams(segment, match, destinationUrl, segmentTreePaths[i]);
|
|
657
594
|
|
|
595
|
+
// No error boundaries are wrapped for a skipped segment — the `continue`
|
|
596
|
+
// below returns before that walk. A deny addressed to this segment
|
|
597
|
+
// therefore finds no boundary in the payload, and since TIM-1356 the
|
|
598
|
+
// payload path no longer falls back to a re-render for an addressed
|
|
599
|
+
// deny. What makes that sound is the precondition for skipping at all:
|
|
600
|
+
// `shouldSkipSegment` only returns true when this exact segment key is
|
|
601
|
+
// in the client's state tree, which means the client rendered this
|
|
602
|
+
// segment on an earlier navigation and still has its boundaries mounted,
|
|
603
|
+
// carrying the same key this build would have stamped. The payload
|
|
604
|
+
// splices in below them, so the throw passes through them on its way up.
|
|
605
|
+
//
|
|
606
|
+
// Metadata from skipped segments still needs to be collected (e.g.,
|
|
607
|
+
// root layout title templates). MetadataCollector wraps the element
|
|
608
|
+
// inside AccessGate — if access denies, metadata is excluded.
|
|
609
|
+
const skippedMeta = metadataSources.filter((s) => s.segmentIndex === i).reverse();
|
|
610
|
+
for (const meta of skippedMeta) {
|
|
611
|
+
const isDynamic = typeof meta.metadataExport === 'function';
|
|
612
|
+
element = h(MetadataCollector, {
|
|
613
|
+
metadataExport: meta.metadataExport,
|
|
614
|
+
segmentName: meta.segmentName,
|
|
615
|
+
isPage: meta.isPage,
|
|
616
|
+
...(isDynamic ? { onSettled: trackShellComponent() } : {}),
|
|
617
|
+
children: element,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
658
621
|
// SECURITY: Even though the layout is skipped, AccessGate MUST still
|
|
659
622
|
// wrap the element tree. access.ts runs on every navigation regardless
|
|
660
623
|
// of cached layouts or state tree content.
|
|
661
624
|
// See design/13-security.md §"Auth always runs" (test #11).
|
|
625
|
+
//
|
|
626
|
+
// Skipped segments omit denyPages and set skippedSegment=true. If
|
|
627
|
+
// access denies at a skipped segment, AccessGate records the deny in
|
|
628
|
+
// ALS and rethrows — the response path returns rscErrorEnvelope so the
|
|
629
|
+
// client hard-navigates for a clean full render with correct deny page
|
|
630
|
+
// placement. Rendering the deny page in-tree here would appear inside
|
|
631
|
+
// the client's cached layout chrome (TIM-1074). See TIM-1363.
|
|
662
632
|
if (segment.access) {
|
|
663
633
|
const accessMod = await loadModule(segment.access);
|
|
664
634
|
const accessFn = accessMod.default as (() => unknown) | undefined;
|
|
665
635
|
if (accessFn) {
|
|
666
|
-
const
|
|
636
|
+
const onGateSettled = trackShellComponent();
|
|
667
637
|
element = h(AccessGate, {
|
|
668
638
|
accessFn,
|
|
669
639
|
segmentName: segment.segmentName,
|
|
670
|
-
|
|
671
|
-
|
|
640
|
+
segmentKey: segmentKeys[i],
|
|
641
|
+
segmentIndex: i,
|
|
642
|
+
skippedSegment: true,
|
|
643
|
+
onSettled: onGateSettled,
|
|
672
644
|
children: element,
|
|
673
645
|
});
|
|
674
646
|
}
|
|
@@ -677,12 +649,42 @@ export async function buildRouteElement(
|
|
|
677
649
|
continue;
|
|
678
650
|
}
|
|
679
651
|
|
|
652
|
+
// Wrap with MetadataCollector(s) INSIDE error boundaries so a
|
|
653
|
+
// metadata() throw is caught by this segment's status-file boundaries,
|
|
654
|
+
// not an ancestor's. Still inside AccessGate (placed below) — when
|
|
655
|
+
// access denies, neither MetadataCollector nor error boundaries render.
|
|
656
|
+
//
|
|
657
|
+
// Reversed: the loop wraps innermost-to-outermost, so the last-wrapped
|
|
658
|
+
// collector renders first. Reversing ensures layout metadata is pushed
|
|
659
|
+
// before page metadata, preserving the root→page merge order required
|
|
660
|
+
// by resolveMetadata (page-authoritative). See TIM-1363.
|
|
661
|
+
const segmentMetadata = metadataSources.filter((s) => s.segmentIndex === i).reverse();
|
|
662
|
+
for (const meta of segmentMetadata) {
|
|
663
|
+
const isDynamic = typeof meta.metadataExport === 'function';
|
|
664
|
+
element = h(MetadataCollector, {
|
|
665
|
+
metadataExport: meta.metadataExport,
|
|
666
|
+
segmentName: meta.segmentName,
|
|
667
|
+
isPage: meta.isPage,
|
|
668
|
+
...(isDynamic ? { onSettled: trackShellComponent() } : {}),
|
|
669
|
+
children: element,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
680
673
|
// Wrap with error boundaries from this segment (inside layout).
|
|
681
674
|
// Keep ALL error boundaries (including 4xx) — they're the safety net for
|
|
682
675
|
// DenySignal from nested server components that escape AccessGate/PageDenyBoundary
|
|
683
676
|
// try/catch. Status-code files must be 'use client' TSX or MDX to serialize
|
|
684
677
|
// as error boundary fallbacks. See TIM-666.
|
|
685
|
-
|
|
678
|
+
// `segmentKey` makes each boundary addressable: a deny hoisted to this
|
|
679
|
+
// segment carries its owner's key, and every other boundary declines it
|
|
680
|
+
// rather than catching it first and rendering at the wrong depth
|
|
681
|
+
// (TIM-1356).
|
|
682
|
+
element = await wrapSegmentWithErrorBoundaries(segment, element, {
|
|
683
|
+
loadModule,
|
|
684
|
+
createElement: h,
|
|
685
|
+
errorBoundaryComponent: TimberErrorBoundary,
|
|
686
|
+
segmentKey: segmentKeys[i],
|
|
687
|
+
});
|
|
686
688
|
|
|
687
689
|
// Wrap with layout BEFORE AccessGate — AccessGate is OUTSIDE the layout.
|
|
688
690
|
// When AccessGate denies, the layout never renders. The deny page appears
|
|
@@ -690,31 +692,24 @@ export async function buildRouteElement(
|
|
|
690
692
|
// This prevents leaking layout UI (sidebars, nav) on denied pages.
|
|
691
693
|
// See design/04-authorization.md §"Access Failure".
|
|
692
694
|
if (layoutComponent) {
|
|
693
|
-
// Resolve parallel slots for this layout
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
// Skip slot resolution for denied segments (TIM-1229): when a
|
|
698
|
-
// parent segment's AccessGate denies, the layout never renders,
|
|
699
|
-
// so slot props are never consumed. Resolving them is wasted work
|
|
700
|
-
// and could trigger side effects in slot access.ts functions.
|
|
695
|
+
// Resolve parallel slots for this layout. If a parent AccessGate
|
|
696
|
+
// denies during render, the layout never renders and slot props
|
|
697
|
+
// are never consumed. Slot access functions may still fire eagerly
|
|
698
|
+
// for skip-candidate slots — a follow-up will make that lazy.
|
|
701
699
|
const segmentId = segmentKeys[i];
|
|
702
700
|
const parentTreePath = segmentTreePaths[i];
|
|
703
|
-
const slotProps =
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
clientStateTree: clientStateTree as ClientStateTree | null,
|
|
716
|
-
slotSkipInfo,
|
|
717
|
-
});
|
|
701
|
+
const slotProps = await resolveSlotProps({
|
|
702
|
+
segment,
|
|
703
|
+
segmentId,
|
|
704
|
+
match,
|
|
705
|
+
h,
|
|
706
|
+
interception,
|
|
707
|
+
parentTreePath,
|
|
708
|
+
departingUrl,
|
|
709
|
+
destinationUrl,
|
|
710
|
+
clientStateTree: clientStateTree as ClientStateTree | null,
|
|
711
|
+
slotSkipInfo,
|
|
712
|
+
});
|
|
718
713
|
|
|
719
714
|
const segmentPath = computeLayoutSegmentPath(segments, i, match);
|
|
720
715
|
const parallelRouteKeys = Object.keys(segment.slots ?? {});
|
|
@@ -750,11 +745,14 @@ export async function buildRouteElement(
|
|
|
750
745
|
(layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
|
|
751
746
|
);
|
|
752
747
|
} catch (error: unknown) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
748
|
+
// A layout that denied never renders its own chrome — it threw
|
|
749
|
+
// instead of returning it — so an entry this segment owns is
|
|
750
|
+
// already correctly placed here. Anything owned further up is
|
|
751
|
+
// hoisted (handleCaughtDeny throws). See TIM-1356.
|
|
752
|
+
if (error instanceof DenySignal) {
|
|
753
|
+
setFirstDeniedSegmentIndex(i);
|
|
754
|
+
if (layoutDenyPages) {
|
|
755
|
+
return handleCaughtDeny(layoutDenyPages, error, segmentId);
|
|
758
756
|
}
|
|
759
757
|
}
|
|
760
758
|
throw error;
|
|
@@ -795,23 +793,23 @@ export async function buildRouteElement(
|
|
|
795
793
|
outermostSegmentProvider = segmentProviderElement;
|
|
796
794
|
}
|
|
797
795
|
|
|
798
|
-
// Wrap in AccessGate OUTSIDE the layout.
|
|
799
|
-
// If access denies, the
|
|
800
|
-
//
|
|
801
|
-
//
|
|
796
|
+
// Wrap in AccessGate OUTSIDE the layout, error boundaries, and MetadataCollector.
|
|
797
|
+
// If access denies, neither the layout nor MetadataCollector renders.
|
|
798
|
+
// Parent layouts (from outer iterations) form the shell. Access runs
|
|
799
|
+
// exactly once, during render, inside React.cache scope — layouts
|
|
800
|
+
// share the cache. See TIM-1363, design/04-authorization.md.
|
|
802
801
|
if (segment.access) {
|
|
803
802
|
const accessMod = await loadModule(segment.access);
|
|
804
803
|
const accessFn = accessMod.default as (() => unknown) | undefined;
|
|
805
804
|
if (accessFn) {
|
|
806
|
-
|
|
807
|
-
// without re-execution. Passing segments omit verdict so AccessGate
|
|
808
|
-
// re-runs access during render for React.cache population.
|
|
809
|
-
const verdict = accessVerdicts.get(i);
|
|
805
|
+
const onGateSettled = trackShellComponent();
|
|
810
806
|
element = h(AccessGate, {
|
|
811
807
|
accessFn,
|
|
812
808
|
segmentName: segment.segmentName,
|
|
809
|
+
segmentKey: segmentKeys[i],
|
|
810
|
+
segmentIndex: i,
|
|
813
811
|
denyPages: denyPageChains.get(i),
|
|
814
|
-
|
|
812
|
+
onSettled: onGateSettled,
|
|
815
813
|
children: element,
|
|
816
814
|
});
|
|
817
815
|
}
|
|
@@ -852,6 +850,5 @@ export async function buildRouteElement(
|
|
|
852
850
|
skippedSegments,
|
|
853
851
|
shellSettled,
|
|
854
852
|
slotSkipInfo,
|
|
855
|
-
firstDeniedIndex,
|
|
856
853
|
};
|
|
857
854
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { rscCacheKey, isRscCacheKeyShape, RSC_KEY_PARAM } from '../shared/rsc-cache-key.js';
|
|
29
|
+
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Legacy shared-cache control fields that predate RFC 9213 and so do not
|
|
@@ -105,13 +106,6 @@ export async function hasValidRscCacheKey(req: Request): Promise<boolean> {
|
|
|
105
106
|
return claims.every((claim) => claim === expected);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
/**
|
|
109
|
-
* Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
|
|
110
|
-
* `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
|
|
111
|
-
* duplication predates this module and is tracked separately.
|
|
112
|
-
*/
|
|
113
|
-
const RSC_MEDIA_TYPE = 'text/x-component';
|
|
114
|
-
|
|
115
109
|
/**
|
|
116
110
|
* Ensure a Flight payload can never be interpreted as a document.
|
|
117
111
|
*
|
|
@@ -130,7 +124,7 @@ const RSC_MEDIA_TYPE = 'text/x-component';
|
|
|
130
124
|
export function hardenRscPayloadResponse(response: Response): void {
|
|
131
125
|
const contentType = response.headers.get('content-type');
|
|
132
126
|
if (!contentType) return;
|
|
133
|
-
if (contentType.split(';')[0].trim().toLowerCase() !==
|
|
127
|
+
if (contentType.split(';')[0].trim().toLowerCase() !== RSC_CONTENT_TYPE) return;
|
|
134
128
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
135
129
|
}
|
|
136
130
|
|
|
@@ -17,7 +17,8 @@ import { logRenderError } from '../logger.js';
|
|
|
17
17
|
import type { PipelineConfig, RouteMatch } from '../pipeline.js';
|
|
18
18
|
import type { ManifestSegmentNode } from '../route-matcher.js';
|
|
19
19
|
import { loadModule } from '../safe-load.js';
|
|
20
|
-
import { createDebugChannelSink
|
|
20
|
+
import { createDebugChannelSink } from './helpers.js';
|
|
21
|
+
import { isRscRequest } from '../../shared/rsc-media-type.js';
|
|
21
22
|
import { callSsr } from './ssr-bridge.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -67,7 +68,7 @@ export function createDenyFallbackRenderer(options: {
|
|
|
67
68
|
// decoder breaks navigation, so dispatch to the RSC renderer when the
|
|
68
69
|
// request is an RSC payload request — mirroring the normal render path.
|
|
69
70
|
// See design/19-client-navigation.md §"RSC Payload Handling" and TIM-823.
|
|
70
|
-
if (
|
|
71
|
+
if (isRscRequest(req)) {
|
|
71
72
|
return renderDenyPageAsRsc(
|
|
72
73
|
deny,
|
|
73
74
|
chain,
|