@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
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* `DenyPageEntry` records ordered by specificity (specific status →
|
|
9
9
|
* category catch-all → `error.tsx`).
|
|
10
10
|
*
|
|
11
|
-
* 2. **Runtime matching** (`
|
|
12
|
-
* chain entry whose status filter
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 2. **Runtime matching and placement** (`matchDenyEntry`, `placeDenyPage`,
|
|
12
|
+
* `handleCaughtDeny`) — picks the first chain entry whose status filter
|
|
13
|
+
* matches the thrown DenySignal, then decides whether the catch site may
|
|
14
|
+
* render it where it stands. A chain spans the catcher outward to root,
|
|
15
|
+
* so a match may belong to an ancestor segment; rendering it in place
|
|
16
|
+
* would wrap it in every layout in between. Ancestor-owned matches are
|
|
17
|
+
* re-thrown with `ownerKey` stamped on the signal so the error boundary
|
|
18
|
+
* at the owning segment renders it. See TIM-1356.
|
|
15
19
|
*
|
|
16
20
|
* 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
|
|
17
21
|
* component that wraps a server-component page, calls it, and catches
|
|
@@ -36,7 +40,9 @@ import type { SerializableError } from '../client/error-reconstituter.js';
|
|
|
36
40
|
import { requestContextAls } from './als-registry.js';
|
|
37
41
|
import { DenySignal } from './primitives.js';
|
|
38
42
|
import { loadModule } from './safe-load.js';
|
|
43
|
+
import type { ManifestLoader } from './safe-load.js';
|
|
39
44
|
import { withSpan } from './tracing.js';
|
|
45
|
+
import { isValidElementType, type LoadedComponent } from './utils/element-type.js';
|
|
40
46
|
import { isMdxFilePath } from './utils/mdx-file.js';
|
|
41
47
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
42
48
|
|
|
@@ -56,27 +62,106 @@ export interface DenyPageEntry {
|
|
|
56
62
|
kind: 'status' | 'error';
|
|
57
63
|
/** MDX files are server components — rendered with plain props, never ErrorReconstituter. */
|
|
58
64
|
isMdx: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Position in the segment chain (0 = root, ascending toward leaf).
|
|
67
|
+
* Used by `matchDenyEntry` to reconstruct per-segment ordering for 5xx
|
|
68
|
+
* statuses — `resolve5xx` walks per-segment (status files then error.tsx
|
|
69
|
+
* at each segment before moving outward), while the chain is stored in
|
|
70
|
+
* the 4xx two-pass order. Without this, `deny(503)` resolves to a
|
|
71
|
+
* different file depending on which path serves it. See TIM-1360.
|
|
72
|
+
*/
|
|
73
|
+
segmentIndex: number;
|
|
74
|
+
/**
|
|
75
|
+
* Segment key of the segment whose directory holds this file — where the
|
|
76
|
+
* deny page must render, which is not necessarily where it was caught.
|
|
77
|
+
*
|
|
78
|
+
* A chain flattens every segment from the catcher outward to root, so
|
|
79
|
+
* without this a catcher cannot tell a file it owns from an ancestor's,
|
|
80
|
+
* and renders both at its own position — wrapping the deny page in every
|
|
81
|
+
* layout in between (TIM-1356).
|
|
82
|
+
*
|
|
83
|
+
* A **key**, not an index: `computeSegmentKeys` produces the identity the
|
|
84
|
+
* client segment cache already navigates by, and it survives the trip
|
|
85
|
+
* through the Flight digest to a boundary the client mounted on an
|
|
86
|
+
* earlier navigation. A chain position would not — depth varies with
|
|
87
|
+
* route groups and interception.
|
|
88
|
+
*
|
|
89
|
+
* It inherits that key's known collision rather than fixing it: keys reset
|
|
90
|
+
* to `urlPath` at each URL-visible segment, so `(browse)/feed` and
|
|
91
|
+
* `(landing)/feed` are both `/feed` (see `routing/segment-keys.ts`). Two
|
|
92
|
+
* such branches are already indistinguishable to the skip decision and the
|
|
93
|
+
* client cache, so a deny addressed across them can land in the wrong
|
|
94
|
+
* branch's layout. Fixing that means fixing the key for every reader, not
|
|
95
|
+
* minting a second identity here.
|
|
96
|
+
*/
|
|
97
|
+
ownerKey: string;
|
|
59
98
|
}
|
|
60
99
|
|
|
100
|
+
/**
|
|
101
|
+
* What `renderDenyEntry` needs to build the element: which component, and
|
|
102
|
+
* which props contract it expects. Deliberately excludes `ownerKey` — the
|
|
103
|
+
* re-render fallback path builds an entry from a `resolveStatusFile`
|
|
104
|
+
* resolution that already carries the position, and would otherwise have to
|
|
105
|
+
* invent an owner it never reads.
|
|
106
|
+
*/
|
|
107
|
+
export type DenyEntryShape = Pick<DenyPageEntry, 'component' | 'kind' | 'isMdx'>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where a matched deny page must render, relative to the catcher that
|
|
111
|
+
* matched it.
|
|
112
|
+
*
|
|
113
|
+
* `render` — the catcher owns the file and is already at the right
|
|
114
|
+
* position. `hoist` — an ancestor owns it; the catcher must re-throw so the
|
|
115
|
+
* boundary at `ownerKey` renders it, outside the layouts in between.
|
|
116
|
+
*/
|
|
117
|
+
export type DenyPlacement =
|
|
118
|
+
| { kind: 'render'; element: React.ReactElement }
|
|
119
|
+
| { kind: 'hoist'; ownerKey: string };
|
|
120
|
+
|
|
61
121
|
// ─── Chain Construction ──────────────────────────────────────────────────
|
|
62
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Load a candidate's default export, or null if it cannot be a component.
|
|
125
|
+
*
|
|
126
|
+
* Gated on `isValidElementType` — the same predicate `error-boundary-wrapper.ts`
|
|
127
|
+
* uses to decide whether to create a boundary at all, and deliberately not a
|
|
128
|
+
* second expression of it. A file this walk accepts but that one rejects
|
|
129
|
+
* yields a chain entry whose owner has no boundary, so a deny addressed to it
|
|
130
|
+
* passes every boundary and escapes with nothing to catch it. (Before the two
|
|
131
|
+
* agreed, such an entry instead crashed inside `createElement` when rendered
|
|
132
|
+
* in place.)
|
|
133
|
+
*
|
|
134
|
+
* Agreement is per-file, which is not the same as per-segment: this walk runs
|
|
135
|
+
* over every segment in the chain, while the boundary walk is skipped for a
|
|
136
|
+
* segment the client already has cached. See the skip branch in
|
|
137
|
+
* `route-element-builder.ts` for why that case still lands.
|
|
138
|
+
*
|
|
139
|
+
* See TIM-1356.
|
|
140
|
+
*/
|
|
141
|
+
async function loadChainComponent(file: ManifestLoader): Promise<LoadedComponent | null> {
|
|
142
|
+
const mod = await loadModule(file).catch(() => null);
|
|
143
|
+
return isValidElementType(mod?.default) ? mod.default : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
63
146
|
/**
|
|
64
147
|
* Build the deny page fallback chain from the segment chain.
|
|
65
148
|
*
|
|
66
149
|
* Walks segments from `startIndex` outward (toward root) and collects
|
|
67
150
|
* status-code file components in fallback order:
|
|
68
151
|
* 1. Specific status files (403.tsx, 404.tsx) — exact match
|
|
69
|
-
* 2. Category catch-alls (4xx.tsx) — matches any
|
|
152
|
+
* 2. Category catch-alls (4xx.tsx, 5xx.tsx) — matches any in category
|
|
70
153
|
* 3. error.tsx — catches everything
|
|
71
154
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
155
|
+
* The chain is stored in the 4xx two-pass order (all status files across
|
|
156
|
+
* segments, then all error.tsx), which mirrors `resolve4xx`. For 5xx,
|
|
157
|
+
* `matchDenyEntry` reconstructs the per-segment walk from each entry's
|
|
158
|
+
* `segmentIndex` — matching `resolve5xx`'s single-pass ordering where a
|
|
159
|
+
* leaf `error.tsx` beats a root `5xx.tsx`. See TIM-1360.
|
|
76
160
|
*/
|
|
77
161
|
export async function buildDenyPageChain(
|
|
78
162
|
segments: ManifestSegmentNode[],
|
|
79
|
-
startIndex: number
|
|
163
|
+
startIndex: number,
|
|
164
|
+
segmentKeys: string[]
|
|
80
165
|
): Promise<DenyPageEntry[]> {
|
|
81
166
|
const chain: DenyPageEntry[] = [];
|
|
82
167
|
|
|
@@ -93,13 +178,15 @@ export async function buildDenyPageChain(
|
|
|
93
178
|
if (key !== '4xx' && key !== '5xx') {
|
|
94
179
|
const status = parseInt(key, 10);
|
|
95
180
|
if (!isNaN(status)) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
181
|
+
const component = await loadChainComponent(file);
|
|
182
|
+
if (component) {
|
|
98
183
|
chain.push({
|
|
99
184
|
status,
|
|
100
|
-
component
|
|
185
|
+
component,
|
|
101
186
|
kind: 'status',
|
|
102
187
|
isMdx: isMdxFilePath(file.filePath),
|
|
188
|
+
segmentIndex: i,
|
|
189
|
+
ownerKey: segmentKeys[i],
|
|
103
190
|
});
|
|
104
191
|
}
|
|
105
192
|
}
|
|
@@ -109,14 +196,16 @@ export async function buildDenyPageChain(
|
|
|
109
196
|
// Category catch-alls (4xx.tsx, 5xx.tsx)
|
|
110
197
|
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
111
198
|
if (key === '4xx' || key === '5xx') {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
199
|
+
const component = await loadChainComponent(file);
|
|
200
|
+
if (component) {
|
|
114
201
|
const categoryStatus = key === '4xx' ? 400 : 500;
|
|
115
202
|
chain.push({
|
|
116
203
|
status: categoryStatus,
|
|
117
|
-
component
|
|
204
|
+
component,
|
|
118
205
|
kind: 'status',
|
|
119
206
|
isMdx: isMdxFilePath(file.filePath),
|
|
207
|
+
segmentIndex: i,
|
|
208
|
+
ownerKey: segmentKeys[i],
|
|
120
209
|
});
|
|
121
210
|
}
|
|
122
211
|
}
|
|
@@ -129,13 +218,15 @@ export async function buildDenyPageChain(
|
|
|
129
218
|
for (let i = startIndex; i >= 0; i--) {
|
|
130
219
|
const segment = segments[i];
|
|
131
220
|
if (segment.error) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
221
|
+
const component = await loadChainComponent(segment.error);
|
|
222
|
+
if (component) {
|
|
134
223
|
chain.push({
|
|
135
224
|
status: null,
|
|
136
|
-
component
|
|
225
|
+
component,
|
|
137
226
|
kind: 'error',
|
|
138
227
|
isMdx: isMdxFilePath(segment.error.filePath),
|
|
228
|
+
segmentIndex: i,
|
|
229
|
+
ownerKey: segmentKeys[i],
|
|
139
230
|
});
|
|
140
231
|
}
|
|
141
232
|
}
|
|
@@ -147,31 +238,106 @@ export async function buildDenyPageChain(
|
|
|
147
238
|
// ─── Runtime Matcher ──────────────────────────────────────────────────────
|
|
148
239
|
|
|
149
240
|
/**
|
|
150
|
-
* Find the first deny page in the chain
|
|
151
|
-
* Returns
|
|
241
|
+
* Find the first deny page in the chain whose status filter matches.
|
|
242
|
+
* Returns null when nothing in the chain accepts this status.
|
|
243
|
+
*
|
|
244
|
+
* For 4xx statuses, the chain's stored order is correct: every status file
|
|
245
|
+
* across all segments (inner→outer) before any error.tsx — so a root
|
|
246
|
+
* `404.tsx` beats a leaf `error.tsx`. This matches `resolve4xx`.
|
|
247
|
+
*
|
|
248
|
+
* For 5xx statuses, the match walks per-segment (inner→outer): at each
|
|
249
|
+
* segment, status files (`503.tsx` → `5xx.tsx`) then `error.tsx`, before
|
|
250
|
+
* moving to the next segment outward. A leaf `error.tsx` therefore beats a
|
|
251
|
+
* root `5xx.tsx` — the intentional inverse of the 4xx chain, matching
|
|
252
|
+
* `resolve5xx`. See TIM-1360.
|
|
152
253
|
*/
|
|
153
|
-
export function
|
|
154
|
-
chain
|
|
155
|
-
|
|
156
|
-
data: unknown
|
|
157
|
-
): React.ReactElement | null {
|
|
254
|
+
export function matchDenyEntry(chain: DenyPageEntry[], status: number): DenyPageEntry | null {
|
|
255
|
+
if (status >= 500) return matchDenyEntry5xx(chain, status);
|
|
256
|
+
|
|
158
257
|
for (const entry of chain) {
|
|
159
|
-
if (entry.status === status)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
258
|
+
if (entry.status === status) return entry;
|
|
259
|
+
if (entry.status === 400 && status >= 400 && status <= 499) return entry;
|
|
260
|
+
if (entry.status === null) return entry;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 5xx per-segment walk: at each segment (inner→outer), check status files
|
|
267
|
+
* then error.tsx before moving outward. Mirrors `resolve5xx`.
|
|
268
|
+
*/
|
|
269
|
+
function matchDenyEntry5xx(chain: DenyPageEntry[], status: number): DenyPageEntry | null {
|
|
270
|
+
// Collect unique segment indices, sorted descending (inner→outer).
|
|
271
|
+
const segmentIndices: number[] = [];
|
|
272
|
+
for (const entry of chain) {
|
|
273
|
+
if (!segmentIndices.includes(entry.segmentIndex)) {
|
|
274
|
+
segmentIndices.push(entry.segmentIndex);
|
|
164
275
|
}
|
|
165
|
-
|
|
166
|
-
|
|
276
|
+
}
|
|
277
|
+
segmentIndices.sort((a, b) => b - a);
|
|
278
|
+
|
|
279
|
+
for (const seg of segmentIndices) {
|
|
280
|
+
// Status files at this segment (exact then category).
|
|
281
|
+
for (const entry of chain) {
|
|
282
|
+
if (entry.segmentIndex !== seg || entry.kind === 'error') continue;
|
|
283
|
+
if (entry.status === status) return entry;
|
|
284
|
+
if (entry.status === 500 && status >= 500 && status <= 599) return entry;
|
|
167
285
|
}
|
|
168
|
-
|
|
169
|
-
|
|
286
|
+
// error.tsx at this segment.
|
|
287
|
+
for (const entry of chain) {
|
|
288
|
+
if (entry.segmentIndex !== seg || entry.kind !== 'error') continue;
|
|
289
|
+
return entry;
|
|
170
290
|
}
|
|
171
291
|
}
|
|
172
292
|
return null;
|
|
173
293
|
}
|
|
174
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Decide where the deny page for `status` renders, from the perspective of
|
|
297
|
+
* the catch site at `selfKey`.
|
|
298
|
+
*
|
|
299
|
+
* The match itself is unchanged — first entry in fallback order wins, so
|
|
300
|
+
* every path picks the same file. What this adds is the position: a chain
|
|
301
|
+
* built at segment i spans i outward to root, so a matched entry is owned
|
|
302
|
+
* either by the catcher or by one of its ancestors.
|
|
303
|
+
*
|
|
304
|
+
* - Owned by the catcher → `render`. The catcher already sits at the
|
|
305
|
+
* owning segment's position.
|
|
306
|
+
* - Owned by an ancestor → `hoist`. Rendering here would wrap the deny page
|
|
307
|
+
* in every layout between the owner and the catcher. The caller re-throws
|
|
308
|
+
* with `ownerKey` stamped on the signal so the boundary at that segment
|
|
309
|
+
* renders it instead — inside the owner's layout, outside the rest.
|
|
310
|
+
*
|
|
311
|
+
* Returns null when no entry matches; the caller re-throws unannotated and
|
|
312
|
+
* the re-render fallback resolves it.
|
|
313
|
+
*
|
|
314
|
+
* See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
|
|
315
|
+
*/
|
|
316
|
+
export function placeDenyPage(
|
|
317
|
+
chain: DenyPageEntry[],
|
|
318
|
+
status: number,
|
|
319
|
+
data: unknown,
|
|
320
|
+
selfKey: string | undefined
|
|
321
|
+
): DenyPlacement | null {
|
|
322
|
+
const entry = matchDenyEntry(chain, status);
|
|
323
|
+
if (!entry) return null;
|
|
324
|
+
|
|
325
|
+
if (entry.ownerKey !== selfKey) {
|
|
326
|
+
// An MDX owner cannot be addressed. Its boundary holds a `fallbackElement`
|
|
327
|
+
// pre-rendered at tree-build time — before this status or its data
|
|
328
|
+
// existed — so hoisting to it would render a page that never learns what
|
|
329
|
+
// was denied (a category `4xx.mdx` gets no `status` prop at all). Leave it
|
|
330
|
+
// unaddressed so the re-render fallback serves it, which is the documented
|
|
331
|
+
// MDX path and resolves the position correctly on its own. See TIM-503.
|
|
332
|
+
if (entry.isMdx) return null;
|
|
333
|
+
return { kind: 'hoist', ownerKey: entry.ownerKey };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Rendering in place is unaffected: `renderDenyEntry` builds the MDX element
|
|
337
|
+
// here, at catch time, with the real status.
|
|
338
|
+
return { kind: 'render', element: renderDenyEntry(entry, status, data) };
|
|
339
|
+
}
|
|
340
|
+
|
|
175
341
|
/**
|
|
176
342
|
* Build the element for a matched deny chain entry with the props contract
|
|
177
343
|
* the file's convention documents:
|
|
@@ -184,7 +350,7 @@ export function renderMatchingDenyPage(
|
|
|
184
350
|
* - error.mdx: plain { status } — server component, static content.
|
|
185
351
|
*/
|
|
186
352
|
export function renderDenyEntry(
|
|
187
|
-
entry:
|
|
353
|
+
entry: DenyEntryShape,
|
|
188
354
|
status: number,
|
|
189
355
|
data: unknown
|
|
190
356
|
): React.ReactElement {
|
|
@@ -216,6 +382,43 @@ export function renderDenyEntry(
|
|
|
216
382
|
});
|
|
217
383
|
}
|
|
218
384
|
|
|
385
|
+
/**
|
|
386
|
+
* The whole of what an in-tree catch site does with a `DenySignal`, shared
|
|
387
|
+
* by all three of them (`PageDenyBoundary`, `TracedLayout`, `AccessGate`).
|
|
388
|
+
*
|
|
389
|
+
* Returns the deny page element when this catch site owns the matched file.
|
|
390
|
+
* Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
|
|
391
|
+
* file, bare when nothing matched at all.
|
|
392
|
+
*
|
|
393
|
+
* `setDenyStatus` runs on the hoist path too, and must run *before* the
|
|
394
|
+
* throw: on the RSC payload path no boundary executes server-side, so the
|
|
395
|
+
* ALS status is the only thing standing between the response and a 200
|
|
396
|
+
* (`rsc-payload.ts` reads it after the stream settles).
|
|
397
|
+
*
|
|
398
|
+
* Either branch leaves part of the matched chain unmounted — hoisting sends
|
|
399
|
+
* the render to a boundary that discards everything below it, and a layout
|
|
400
|
+
* that denies returns the deny page instead of its own child outlet. What the
|
|
401
|
+
* response then says about the tree is decided from the deny status alone in
|
|
402
|
+
* `rsc-payload.ts`, not tracked here; see the note there for why.
|
|
403
|
+
*/
|
|
404
|
+
export function handleCaughtDeny(
|
|
405
|
+
chain: DenyPageEntry[] | undefined,
|
|
406
|
+
error: DenySignal,
|
|
407
|
+
selfKey: string | undefined
|
|
408
|
+
): React.ReactElement {
|
|
409
|
+
const placement = chain ? placeDenyPage(chain, error.status, error.data, selfKey) : null;
|
|
410
|
+
|
|
411
|
+
if (placement?.kind === 'render') {
|
|
412
|
+
setDenyStatus(error.status);
|
|
413
|
+
return placement.element;
|
|
414
|
+
}
|
|
415
|
+
if (placement?.kind === 'hoist') {
|
|
416
|
+
setDenyStatus(error.status);
|
|
417
|
+
error.ownerKey = placement.ownerKey;
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
|
|
219
422
|
// ─── Page Boundary ────────────────────────────────────────────────────────
|
|
220
423
|
|
|
221
424
|
/**
|
|
@@ -236,6 +439,7 @@ export async function PageDenyBoundary({
|
|
|
236
439
|
Page,
|
|
237
440
|
route,
|
|
238
441
|
denyPages,
|
|
442
|
+
segmentKey,
|
|
239
443
|
onSettled,
|
|
240
444
|
}: {
|
|
241
445
|
/** The page server component function. */
|
|
@@ -244,6 +448,12 @@ export async function PageDenyBoundary({
|
|
|
244
448
|
route: string;
|
|
245
449
|
/** Deny page fallback chain from the segment chain. */
|
|
246
450
|
denyPages: DenyPageEntry[];
|
|
451
|
+
/**
|
|
452
|
+
* Segment key of the leaf. The page renders inside the leaf's layout, so
|
|
453
|
+
* a leaf-owned deny page is already at the right position; anything the
|
|
454
|
+
* chain matched further up is hoisted to its owner.
|
|
455
|
+
*/
|
|
456
|
+
segmentKey: string;
|
|
247
457
|
/** Called when the page component finishes (success or deny). See TIM-1045. */
|
|
248
458
|
onSettled?: () => void;
|
|
249
459
|
}): Promise<React.ReactElement> {
|
|
@@ -252,11 +462,7 @@ export async function PageDenyBoundary({
|
|
|
252
462
|
return result as React.ReactElement;
|
|
253
463
|
} catch (error: unknown) {
|
|
254
464
|
if (error instanceof DenySignal) {
|
|
255
|
-
|
|
256
|
-
if (denyElement) {
|
|
257
|
-
setDenyStatus(error.status);
|
|
258
|
-
return denyElement;
|
|
259
|
-
}
|
|
465
|
+
return handleCaughtDeny(denyPages, error, segmentKey);
|
|
260
466
|
}
|
|
261
467
|
throw error;
|
|
262
468
|
} finally {
|
|
@@ -286,3 +492,25 @@ export function setDenyStatus(status: number): void {
|
|
|
286
492
|
export function getDenyStatus(): number | undefined {
|
|
287
493
|
return requestContextAls.getStore()?.denyStatus;
|
|
288
494
|
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Record the first denied segment index in the request context ALS.
|
|
498
|
+
* Called from AccessGate when a DenySignal is caught during render.
|
|
499
|
+
* Only the first (shallowest) denial is recorded — deeper denials are
|
|
500
|
+
* unreachable because the gate short-circuits the subtree.
|
|
501
|
+
* See TIM-1363.
|
|
502
|
+
*/
|
|
503
|
+
export function setFirstDeniedSegmentIndex(index: number): void {
|
|
504
|
+
const store = requestContextAls.getStore();
|
|
505
|
+
if (store && store._firstDeniedSegmentIndex === undefined) {
|
|
506
|
+
store._firstDeniedSegmentIndex = index;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Read the first denied segment index from the request context ALS.
|
|
512
|
+
* Returns Infinity if no segment denied during render.
|
|
513
|
+
*/
|
|
514
|
+
export function getFirstDeniedSegmentIndex(): number {
|
|
515
|
+
return requestContextAls.getStore()?._firstDeniedSegmentIndex ?? Infinity;
|
|
516
|
+
}
|
|
@@ -22,7 +22,7 @@ import { DenySignal } from './primitives.js';
|
|
|
22
22
|
import { appVisibleSearch } from '../shared/rsc-cache-key.js';
|
|
23
23
|
import { getSlotParamsForClient } from './request-context.js';
|
|
24
24
|
import { withPublishedParams } from './publish-params.js';
|
|
25
|
-
import { renderDenyEntry, type
|
|
25
|
+
import { renderDenyEntry, type DenyEntryShape } from './deny-boundary.js';
|
|
26
26
|
import { getCookiesForSsr } from './cookie-context.js';
|
|
27
27
|
import { logRenderError } from './logger.js';
|
|
28
28
|
import { loadModule } from './safe-load.js';
|
|
@@ -40,9 +40,7 @@ import { flightInitScript } from './flight-scripts.js';
|
|
|
40
40
|
import type { ClientBootstrapConfig } from './html-injectors.js';
|
|
41
41
|
import type { Metadata } from './types.js';
|
|
42
42
|
import { teeWithErrorPropagation } from './stream-utils.js';
|
|
43
|
-
|
|
44
|
-
/** RSC content type for client navigation payload requests. */
|
|
45
|
-
const RSC_CONTENT_TYPE = 'text/x-component';
|
|
43
|
+
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
|
|
46
44
|
|
|
47
45
|
/** Layout component entry for deny page wrapping. */
|
|
48
46
|
export interface LayoutEntry {
|
|
@@ -84,8 +82,7 @@ function buildDenyElement(
|
|
|
84
82
|
resolutionKind: 'exact' | 'category' | 'error',
|
|
85
83
|
filePath: string
|
|
86
84
|
): React.ReactElement {
|
|
87
|
-
const entry:
|
|
88
|
-
status: deny.status,
|
|
85
|
+
const entry: DenyEntryShape = {
|
|
89
86
|
component,
|
|
90
87
|
kind: resolutionKind === 'error' ? 'error' : 'status',
|
|
91
88
|
isMdx: isMdxFilePath(filePath),
|
|
@@ -122,21 +119,29 @@ export async function renderDenyPage(
|
|
|
122
119
|
clientBootstrap: ClientBootstrapConfig,
|
|
123
120
|
createDebugChannelSink: DebugChannelFactory,
|
|
124
121
|
callSsr: CallSsrFn,
|
|
125
|
-
defaultPage?: DefaultPageRenderer
|
|
122
|
+
defaultPage?: DefaultPageRenderer,
|
|
123
|
+
firstDeniedIndex = Infinity
|
|
126
124
|
): Promise<Response> {
|
|
125
|
+
// Clamp the resolution walk to the denied segment — a status file below
|
|
126
|
+
// the denial is invisible to the in-tree chain and must not be selected
|
|
127
|
+
// by the fallback. The denying segment itself IS included (matching
|
|
128
|
+
// buildDenyPageChain's start-at-i semantics), but its layout is not
|
|
129
|
+
// (the gate-outside-layout rule from TIM-1074). See TIM-1358.
|
|
130
|
+
const maxIndex = firstDeniedIndex < Infinity ? firstDeniedIndex : undefined;
|
|
131
|
+
|
|
127
132
|
// API routes (route.ts) → JSON only, never render components
|
|
128
133
|
if (isApiRoute(segments)) {
|
|
129
|
-
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
|
|
134
|
+
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders, maxIndex);
|
|
130
135
|
if (jsonResponse) return jsonResponse;
|
|
131
136
|
return bareJsonResponse(deny.status, responseHeaders);
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
// Page routes → component chain first, JSON fallback only if no component found.
|
|
135
|
-
const resolution = resolveStatusFile(deny.status, segments, 'component');
|
|
140
|
+
const resolution = resolveStatusFile(deny.status, segments, 'component', maxIndex);
|
|
136
141
|
|
|
137
142
|
// No component status file — try JSON chain, then the framework default page.
|
|
138
143
|
if (!resolution) {
|
|
139
|
-
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
|
|
144
|
+
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders, maxIndex);
|
|
140
145
|
if (jsonResponse) return jsonResponse;
|
|
141
146
|
return (
|
|
142
147
|
defaultPage?.(deny.status, responseHeaders) ??
|
|
@@ -166,7 +171,7 @@ export async function renderDenyPage(
|
|
|
166
171
|
// error-file resolver, which both swallow the load failure.
|
|
167
172
|
const mod = await loadModule(resolution.file).catch(() => null);
|
|
168
173
|
if (!mod?.default) {
|
|
169
|
-
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
|
|
174
|
+
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders, maxIndex);
|
|
170
175
|
if (jsonResponse) return jsonResponse;
|
|
171
176
|
return (
|
|
172
177
|
defaultPage?.(deny.status, responseHeaders) ??
|
|
@@ -187,10 +192,12 @@ export async function renderDenyPage(
|
|
|
187
192
|
resolution.file.filePath
|
|
188
193
|
);
|
|
189
194
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
195
|
+
// Wrap in layout shell, clamped so a denied segment's layout never
|
|
196
|
+
// executes — the gate-outside-layout rule from TIM-1074. The denied
|
|
197
|
+
// segment's layout was loaded into `layoutComponents` but must not
|
|
198
|
+
// appear in the fallback response. See TIM-1358.
|
|
199
|
+
const layoutCeiling = Math.min(resolution.segmentIndex, firstDeniedIndex - 1);
|
|
200
|
+
const resolvedSegments = new Set(segments.slice(0, layoutCeiling + 1));
|
|
194
201
|
const layoutsToWrap = layoutComponents.filter((lc) => resolvedSegments.has(lc.segment));
|
|
195
202
|
for (let i = layoutsToWrap.length - 1; i >= 0; i--) {
|
|
196
203
|
const { component } = layoutsToWrap[i];
|
|
@@ -273,9 +280,11 @@ export async function renderDenyPageAsRsc(
|
|
|
273
280
|
layoutComponents: LayoutEntry[],
|
|
274
281
|
req: Request,
|
|
275
282
|
responseHeaders: Headers,
|
|
276
|
-
createDebugChannelSink: DebugChannelFactory
|
|
283
|
+
createDebugChannelSink: DebugChannelFactory,
|
|
284
|
+
firstDeniedIndex = Infinity
|
|
277
285
|
): Promise<Response> {
|
|
278
|
-
const
|
|
286
|
+
const maxIndex = firstDeniedIndex < Infinity ? firstDeniedIndex : undefined;
|
|
287
|
+
const resolution = resolveStatusFile(deny.status, segments, 'component', maxIndex);
|
|
279
288
|
|
|
280
289
|
if (!resolution) {
|
|
281
290
|
return denyHardNavigateResponse(deny.status, responseHeaders);
|
|
@@ -296,8 +305,9 @@ export async function renderDenyPageAsRsc(
|
|
|
296
305
|
resolution.file.filePath
|
|
297
306
|
);
|
|
298
307
|
|
|
299
|
-
//
|
|
300
|
-
const
|
|
308
|
+
// Wrap in layout shell, clamped to exclude denied segment layouts. TIM-1358.
|
|
309
|
+
const layoutCeiling = Math.min(resolution.segmentIndex, firstDeniedIndex - 1);
|
|
310
|
+
const resolvedSegments = new Set(segments.slice(0, layoutCeiling + 1));
|
|
301
311
|
const layoutsToWrap = layoutComponents.filter((lc) => resolvedSegments.has(lc.segment));
|
|
302
312
|
for (let i = layoutsToWrap.length - 1; i >= 0; i--) {
|
|
303
313
|
const { component } = layoutsToWrap[i];
|
|
@@ -332,9 +342,10 @@ export async function renderDenyPageAsRsc(
|
|
|
332
342
|
async function renderDenyPageJson(
|
|
333
343
|
deny: DenySignal,
|
|
334
344
|
segments: ManifestSegmentNode[],
|
|
335
|
-
responseHeaders: Headers
|
|
345
|
+
responseHeaders: Headers,
|
|
346
|
+
maxIndex?: number
|
|
336
347
|
): Promise<Response | null> {
|
|
337
|
-
const resolution = resolveStatusFile(deny.status, segments, 'json');
|
|
348
|
+
const resolution = resolveStatusFile(deny.status, segments, 'json', maxIndex);
|
|
338
349
|
|
|
339
350
|
if (!resolution) {
|
|
340
351
|
return null;
|