@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.188
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/segment-keys-BawYuNFO.js.map +1 -1
- 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 +33 -17
- 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 +14 -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/segment-keys.d.ts +22 -0
- package/dist/routing/segment-keys.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/chain-url-parts.d.ts +2 -3
- package/dist/server/chain-url-parts.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 +344 -237
- 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/skippable-prefix.d.ts +3 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +18 -19
- 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/state-tree-diff.d.ts +9 -16
- package/dist/server/state-tree-diff.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 -43
- 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/dist/shared/segment-info.d.ts +7 -0
- package/dist/shared/segment-info.d.ts.map +1 -1
- 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 +26 -5
- 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/segment-keys.ts +37 -0
- 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 +61 -68
- package/src/server/action-handler.ts +1 -4
- package/src/server/als-registry.ts +16 -0
- package/src/server/chain-url-parts.ts +2 -3
- 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 +169 -194
- 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 +3 -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/skippable-prefix.ts +21 -39
- package/src/server/slot-resolver.ts +69 -189
- package/src/server/ssr-bridge-types.ts +12 -0
- package/src/server/ssr-entry.ts +8 -6
- package/src/server/state-tree-diff.ts +11 -62
- package/src/server/static-generator.ts +3 -2
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -228
- 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/src/shared/segment-info.ts +7 -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
package/dist/server/internal.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { n as isRscRequest } from "../_chunks/rsc-media-type-DRqE_lD_.js";
|
|
1
2
|
import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-BawYuNFO.js";
|
|
2
3
|
import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
|
|
3
|
-
import {
|
|
4
|
+
import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
|
|
5
|
+
import { A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-D8xJZXIN.js";
|
|
4
6
|
import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
|
|
7
|
+
import { t as isMdxFilePath } from "../_chunks/mdx-file-C005ay-P.js";
|
|
5
8
|
import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
|
|
6
|
-
import { i as coerce, t as executeAction } from "../_chunks/actions-
|
|
7
|
-
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-
|
|
8
|
-
import "../_chunks/error-boundary-
|
|
9
|
-
import { n as toBracketKey } from "../_chunks/resolve-schema-
|
|
10
|
-
import {
|
|
11
|
-
import "../_chunks/segment-context-ZDnXDkbz.js";
|
|
9
|
+
import { i as coerce, t as executeAction } from "../_chunks/actions-35jnMdeJ.js";
|
|
10
|
+
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-DjNrIWRR.js";
|
|
11
|
+
import "../_chunks/error-boundary-D-ODYX41.js";
|
|
12
|
+
import { n as toBracketKey } from "../_chunks/resolve-schema-5ma5pp1b.js";
|
|
13
|
+
import { r as normalizeParamValue } from "../_chunks/segment-context-CjOlyB8Y.js";
|
|
12
14
|
import { n as rscErrorEnvelope } from "../_chunks/rsc-error-envelope-tT5PJs4q.js";
|
|
13
15
|
import { createHash } from "node:crypto";
|
|
14
16
|
import { readFile } from "node:fs/promises";
|
|
@@ -287,7 +289,7 @@ function cloneWithMutableHeaders(response) {
|
|
|
287
289
|
* createFromFetch. See design/19-client-navigation.md.
|
|
288
290
|
*/
|
|
289
291
|
function buildRedirectResponse(signal, req, headers) {
|
|
290
|
-
if ((req
|
|
292
|
+
if (isRscRequest(req)) {
|
|
291
293
|
headers.set("X-Timber-Redirect", signal.location);
|
|
292
294
|
return new Response(null, {
|
|
293
295
|
status: 204,
|
|
@@ -1136,6 +1138,42 @@ async function loadModule(loader) {
|
|
|
1136
1138
|
}
|
|
1137
1139
|
}
|
|
1138
1140
|
//#endregion
|
|
1141
|
+
//#region src/server/utils/element-type.ts
|
|
1142
|
+
var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
|
|
1143
|
+
Symbol.for("react.forward_ref"),
|
|
1144
|
+
Symbol.for("react.memo"),
|
|
1145
|
+
Symbol.for("react.lazy"),
|
|
1146
|
+
Symbol.for("react.provider"),
|
|
1147
|
+
Symbol.for("react.context"),
|
|
1148
|
+
Symbol.for("react.suspense"),
|
|
1149
|
+
Symbol.for("react.suspense_list"),
|
|
1150
|
+
Symbol.for("react.client.reference")
|
|
1151
|
+
]);
|
|
1152
|
+
/**
|
|
1153
|
+
* Validate that a loaded module's `default` export is something React
|
|
1154
|
+
* accepts as the first argument to `createElement` — i.e. a valid component
|
|
1155
|
+
* type. React doesn't export `isValidElementType` (only `isValidElement`,
|
|
1156
|
+
* which checks for *elements*, not *component types*), so this mirrors
|
|
1157
|
+
* React's internal check:
|
|
1158
|
+
*
|
|
1159
|
+
* - functions → function or class components
|
|
1160
|
+
* - objects with a `$$typeof` matching one of React's known component
|
|
1161
|
+
* markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
|
|
1162
|
+
* suspense, client references via `@vitejs/plugin-rsc`)
|
|
1163
|
+
*
|
|
1164
|
+
* Strings (HTML tag names) are valid for `createElement` but never appear
|
|
1165
|
+
* as a route module's default export, so they're not recognized here.
|
|
1166
|
+
*
|
|
1167
|
+
* Anything else (numbers, plain config objects, JSON, etc.) is rejected so
|
|
1168
|
+
* the boundary wrapper is skipped rather than crashing inside React.
|
|
1169
|
+
*/
|
|
1170
|
+
function isValidElementType(value) {
|
|
1171
|
+
if (typeof value === "function") return true;
|
|
1172
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1173
|
+
const marker = value.$$typeof;
|
|
1174
|
+
return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
|
|
1175
|
+
}
|
|
1176
|
+
//#endregion
|
|
1139
1177
|
//#region src/server/deny-boundary.ts
|
|
1140
1178
|
/**
|
|
1141
1179
|
* Deny boundary subsystem — the in-tree DenySignal flow.
|
|
@@ -1147,10 +1185,14 @@ async function loadModule(loader) {
|
|
|
1147
1185
|
* `DenyPageEntry` records ordered by specificity (specific status →
|
|
1148
1186
|
* category catch-all → `error.tsx`).
|
|
1149
1187
|
*
|
|
1150
|
-
* 2. **Runtime matching** (`
|
|
1151
|
-
* chain entry whose status filter
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1188
|
+
* 2. **Runtime matching and placement** (`matchDenyEntry`, `placeDenyPage`,
|
|
1189
|
+
* `handleCaughtDeny`) — picks the first chain entry whose status filter
|
|
1190
|
+
* matches the thrown DenySignal, then decides whether the catch site may
|
|
1191
|
+
* render it where it stands. A chain spans the catcher outward to root,
|
|
1192
|
+
* so a match may belong to an ancestor segment; rendering it in place
|
|
1193
|
+
* would wrap it in every layout in between. Ancestor-owned matches are
|
|
1194
|
+
* re-thrown with `ownerKey` stamped on the signal so the error boundary
|
|
1195
|
+
* at the owning segment renders it. See TIM-1356.
|
|
1154
1196
|
*
|
|
1155
1197
|
* 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
|
|
1156
1198
|
* component that wraps a server-component page, calls it, and catches
|
|
@@ -1168,19 +1210,86 @@ async function loadModule(loader) {
|
|
|
1168
1210
|
* See design/04-authorization.md, design/10-error-handling.md, TIM-666.
|
|
1169
1211
|
*/
|
|
1170
1212
|
/**
|
|
1171
|
-
* Find the first deny page in the chain
|
|
1172
|
-
* Returns
|
|
1213
|
+
* Find the first deny page in the chain whose status filter matches.
|
|
1214
|
+
* Returns null when nothing in the chain accepts this status.
|
|
1215
|
+
*
|
|
1216
|
+
* For 4xx statuses, the chain's stored order is correct: every status file
|
|
1217
|
+
* across all segments (inner→outer) before any error.tsx — so a root
|
|
1218
|
+
* `404.tsx` beats a leaf `error.tsx`. This matches `resolve4xx`.
|
|
1219
|
+
*
|
|
1220
|
+
* For 5xx statuses, the match walks per-segment (inner→outer): at each
|
|
1221
|
+
* segment, status files (`503.tsx` → `5xx.tsx`) then `error.tsx`, before
|
|
1222
|
+
* moving to the next segment outward. A leaf `error.tsx` therefore beats a
|
|
1223
|
+
* root `5xx.tsx` — the intentional inverse of the 4xx chain, matching
|
|
1224
|
+
* `resolve5xx`. See TIM-1360.
|
|
1173
1225
|
*/
|
|
1174
|
-
function
|
|
1226
|
+
function matchDenyEntry(chain, status) {
|
|
1227
|
+
if (status >= 500) return matchDenyEntry5xx(chain, status);
|
|
1175
1228
|
for (const entry of chain) {
|
|
1176
|
-
if (entry.status === status) return
|
|
1177
|
-
if (entry.status === 400 && status >= 400 && status <= 499) return
|
|
1178
|
-
if (entry.status ===
|
|
1179
|
-
|
|
1229
|
+
if (entry.status === status) return entry;
|
|
1230
|
+
if (entry.status === 400 && status >= 400 && status <= 499) return entry;
|
|
1231
|
+
if (entry.status === null) return entry;
|
|
1232
|
+
}
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* 5xx per-segment walk: at each segment (inner→outer), check status files
|
|
1237
|
+
* then error.tsx before moving outward. Mirrors `resolve5xx`.
|
|
1238
|
+
*/
|
|
1239
|
+
function matchDenyEntry5xx(chain, status) {
|
|
1240
|
+
const segmentIndices = [];
|
|
1241
|
+
for (const entry of chain) if (!segmentIndices.includes(entry.segmentIndex)) segmentIndices.push(entry.segmentIndex);
|
|
1242
|
+
segmentIndices.sort((a, b) => b - a);
|
|
1243
|
+
for (const seg of segmentIndices) {
|
|
1244
|
+
for (const entry of chain) {
|
|
1245
|
+
if (entry.segmentIndex !== seg || entry.kind === "error") continue;
|
|
1246
|
+
if (entry.status === status) return entry;
|
|
1247
|
+
if (entry.status === 500 && status >= 500 && status <= 599) return entry;
|
|
1248
|
+
}
|
|
1249
|
+
for (const entry of chain) {
|
|
1250
|
+
if (entry.segmentIndex !== seg || entry.kind !== "error") continue;
|
|
1251
|
+
return entry;
|
|
1252
|
+
}
|
|
1180
1253
|
}
|
|
1181
1254
|
return null;
|
|
1182
1255
|
}
|
|
1183
1256
|
/**
|
|
1257
|
+
* Decide where the deny page for `status` renders, from the perspective of
|
|
1258
|
+
* the catch site at `selfKey`.
|
|
1259
|
+
*
|
|
1260
|
+
* The match itself is unchanged — first entry in fallback order wins, so
|
|
1261
|
+
* every path picks the same file. What this adds is the position: a chain
|
|
1262
|
+
* built at segment i spans i outward to root, so a matched entry is owned
|
|
1263
|
+
* either by the catcher or by one of its ancestors.
|
|
1264
|
+
*
|
|
1265
|
+
* - Owned by the catcher → `render`. The catcher already sits at the
|
|
1266
|
+
* owning segment's position.
|
|
1267
|
+
* - Owned by an ancestor → `hoist`. Rendering here would wrap the deny page
|
|
1268
|
+
* in every layout between the owner and the catcher. The caller re-throws
|
|
1269
|
+
* with `ownerKey` stamped on the signal so the boundary at that segment
|
|
1270
|
+
* renders it instead — inside the owner's layout, outside the rest.
|
|
1271
|
+
*
|
|
1272
|
+
* Returns null when no entry matches; the caller re-throws unannotated and
|
|
1273
|
+
* the re-render fallback resolves it.
|
|
1274
|
+
*
|
|
1275
|
+
* See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
|
|
1276
|
+
*/
|
|
1277
|
+
function placeDenyPage(chain, status, data, selfKey) {
|
|
1278
|
+
const entry = matchDenyEntry(chain, status);
|
|
1279
|
+
if (!entry) return null;
|
|
1280
|
+
if (entry.ownerKey !== selfKey) {
|
|
1281
|
+
if (entry.isMdx) return null;
|
|
1282
|
+
return {
|
|
1283
|
+
kind: "hoist",
|
|
1284
|
+
ownerKey: entry.ownerKey
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
kind: "render",
|
|
1289
|
+
element: renderDenyEntry(entry, status, data)
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1184
1293
|
* Build the element for a matched deny chain entry with the props contract
|
|
1185
1294
|
* the file's convention documents:
|
|
1186
1295
|
*
|
|
@@ -1211,6 +1320,37 @@ function renderDenyEntry(entry, status, data) {
|
|
|
1211
1320
|
});
|
|
1212
1321
|
}
|
|
1213
1322
|
/**
|
|
1323
|
+
* The whole of what an in-tree catch site does with a `DenySignal`, shared
|
|
1324
|
+
* by all three of them (`PageDenyBoundary`, `TracedLayout`, `AccessGate`).
|
|
1325
|
+
*
|
|
1326
|
+
* Returns the deny page element when this catch site owns the matched file.
|
|
1327
|
+
* Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
|
|
1328
|
+
* file, bare when nothing matched at all.
|
|
1329
|
+
*
|
|
1330
|
+
* `setDenyStatus` runs on the hoist path too, and must run *before* the
|
|
1331
|
+
* throw: on the RSC payload path no boundary executes server-side, so the
|
|
1332
|
+
* ALS status is the only thing standing between the response and a 200
|
|
1333
|
+
* (`rsc-payload.ts` reads it after the stream settles).
|
|
1334
|
+
*
|
|
1335
|
+
* Either branch leaves part of the matched chain unmounted — hoisting sends
|
|
1336
|
+
* the render to a boundary that discards everything below it, and a layout
|
|
1337
|
+
* that denies returns the deny page instead of its own child outlet. What the
|
|
1338
|
+
* response then says about the tree is decided from the deny status alone in
|
|
1339
|
+
* `rsc-payload.ts`, not tracked here; see the note there for why.
|
|
1340
|
+
*/
|
|
1341
|
+
function handleCaughtDeny(chain, error, selfKey) {
|
|
1342
|
+
const placement = chain ? placeDenyPage(chain, error.status, error.data, selfKey) : null;
|
|
1343
|
+
if (placement?.kind === "render") {
|
|
1344
|
+
setDenyStatus(error.status);
|
|
1345
|
+
return placement.element;
|
|
1346
|
+
}
|
|
1347
|
+
if (placement?.kind === "hoist") {
|
|
1348
|
+
setDenyStatus(error.status);
|
|
1349
|
+
error.ownerKey = placement.ownerKey;
|
|
1350
|
+
}
|
|
1351
|
+
throw error;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1214
1354
|
* Set the deny status in the request context ALS.
|
|
1215
1355
|
* Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
|
|
1216
1356
|
* The pipeline reads this after render to set the HTTP status code.
|
|
@@ -1222,6 +1362,17 @@ function setDenyStatus(status) {
|
|
|
1222
1362
|
store.onDenyStatus?.();
|
|
1223
1363
|
}
|
|
1224
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Record the first denied segment index in the request context ALS.
|
|
1367
|
+
* Called from AccessGate when a DenySignal is caught during render.
|
|
1368
|
+
* Only the first (shallowest) denial is recorded — deeper denials are
|
|
1369
|
+
* unreachable because the gate short-circuits the subtree.
|
|
1370
|
+
* See TIM-1363.
|
|
1371
|
+
*/
|
|
1372
|
+
function setFirstDeniedSegmentIndex(index) {
|
|
1373
|
+
const store = requestContextAls.getStore();
|
|
1374
|
+
if (store && store._firstDeniedSegmentIndex === void 0) store._firstDeniedSegmentIndex = index;
|
|
1375
|
+
}
|
|
1225
1376
|
//#endregion
|
|
1226
1377
|
//#region src/server/access-gate.tsx
|
|
1227
1378
|
/**
|
|
@@ -1241,37 +1392,37 @@ function setDenyStatus(status) {
|
|
|
1241
1392
|
/**
|
|
1242
1393
|
* Framework-injected access gate for segments.
|
|
1243
1394
|
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
* When no verdict is provided (backward compat with tree-builder.ts),
|
|
1250
|
-
* AccessGate calls accessFn directly with OTEL instrumentation.
|
|
1395
|
+
* Calls accessFn during render — inside `renderToReadableStream`, sharing
|
|
1396
|
+
* the same `React.cache` scope as layouts and pages. A `requireUser()`
|
|
1397
|
+
* call in access.ts populates the cache; the same call in any layout is
|
|
1398
|
+
* a free hit. Access executes exactly once per request. See TIM-1363.
|
|
1251
1399
|
*
|
|
1252
1400
|
* access.ts is a pure gate — return values are discarded. The layout below
|
|
1253
1401
|
* gets the same data by calling the same cached functions (React.cache dedup).
|
|
1402
|
+
*
|
|
1403
|
+
* On denial the gate renders the matching deny page only when this segment
|
|
1404
|
+
* owns it. Because the gate is wrapped OUTSIDE its own layout, that render
|
|
1405
|
+
* omits the denying segment's layout — deliberate, and the one place the
|
|
1406
|
+
* in-tree path diverges from `renderDenyPage`'s inclusive slice: a layout
|
|
1407
|
+
* whose access.ts denied must never run and serialize protected data into
|
|
1408
|
+
* the Flight stream (TIM-1074). An ancestor-owned page is hoisted instead,
|
|
1409
|
+
* because the gate also sits inside every layout above it and rendering
|
|
1410
|
+
* here would wrap the deny page in all of them (TIM-1356).
|
|
1411
|
+
*
|
|
1412
|
+
* For skipped segments (skippedSegment=true), deny pages are NOT rendered
|
|
1413
|
+
* in-tree — the client's cached layout chrome would wrap them incorrectly.
|
|
1414
|
+
* Instead, the deny is recorded in ALS and rethrown so the response path
|
|
1415
|
+
* can return rscErrorEnvelope for a client hard-navigate.
|
|
1254
1416
|
*/
|
|
1255
1417
|
function AccessGate(props) {
|
|
1256
|
-
const { accessFn, segmentName,
|
|
1257
|
-
|
|
1258
|
-
if (verdict === "pass") return children;
|
|
1259
|
-
if (verdict instanceof DenySignal && denyPages) {
|
|
1260
|
-
const denyElement = renderMatchingDenyPage(denyPages, verdict.status, verdict.data);
|
|
1261
|
-
if (denyElement) {
|
|
1262
|
-
setDenyStatus(verdict.status);
|
|
1263
|
-
return denyElement;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
throw verdict;
|
|
1267
|
-
}
|
|
1268
|
-
return accessGateFallback(accessFn, segmentName, denyPages, children);
|
|
1418
|
+
const { accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, onSettled, children } = props;
|
|
1419
|
+
return accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, skippedSegment ? void 0 : denyPages, skippedSegment, onSettled, children);
|
|
1269
1420
|
}
|
|
1270
1421
|
/**
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1422
|
+
* Calls accessFn during render with OTEL instrumentation.
|
|
1423
|
+
* Records the first denied segment index in ALS for post-render consumers.
|
|
1273
1424
|
*/
|
|
1274
|
-
async function accessGateFallback(accessFn, segmentName, denyPages, children) {
|
|
1425
|
+
async function accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, onSettled, children) {
|
|
1275
1426
|
try {
|
|
1276
1427
|
await withSpan("timber.access", { "timber.segment": segmentName ?? "unknown" }, async () => {
|
|
1277
1428
|
try {
|
|
@@ -1287,14 +1438,17 @@ async function accessGateFallback(accessFn, segmentName, denyPages, children) {
|
|
|
1287
1438
|
}
|
|
1288
1439
|
});
|
|
1289
1440
|
} catch (error) {
|
|
1290
|
-
if (error instanceof DenySignal
|
|
1291
|
-
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1441
|
+
if (error instanceof DenySignal) {
|
|
1442
|
+
if (segmentIndex !== void 0) setFirstDeniedSegmentIndex(segmentIndex);
|
|
1443
|
+
if (skippedSegment) {
|
|
1444
|
+
error._hardNavigate = true;
|
|
1445
|
+
throw error;
|
|
1295
1446
|
}
|
|
1447
|
+
if (denyPages) return handleCaughtDeny(denyPages, error, segmentKey);
|
|
1296
1448
|
}
|
|
1297
1449
|
throw error;
|
|
1450
|
+
} finally {
|
|
1451
|
+
onSettled?.();
|
|
1298
1452
|
}
|
|
1299
1453
|
return children;
|
|
1300
1454
|
}
|
|
@@ -1313,13 +1467,7 @@ async function accessGateFallback(accessFn, segmentName, denyPages, children) {
|
|
|
1313
1467
|
* slot doesn't make architectural sense.
|
|
1314
1468
|
*/
|
|
1315
1469
|
async function SlotAccessGate(props) {
|
|
1316
|
-
const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children
|
|
1317
|
-
if (verdict !== void 0) {
|
|
1318
|
-
if (verdict === "pass") return children;
|
|
1319
|
-
if (verdict instanceof DenySignal) return buildDeniedFallback(DeniedComponent, slotName, verdict.data, createElement) ?? defaultFallback ?? null;
|
|
1320
|
-
if (isDebug()) console.error("[timber] redirect() is not allowed in slot access.ts. Slots use deny() for graceful degradation — denied.tsx → default.tsx → null. If you need to redirect, move the logic to the parent segment's access.ts.");
|
|
1321
|
-
return buildDeniedFallback(DeniedComponent, slotName, void 0, createElement) ?? defaultFallback ?? null;
|
|
1322
|
-
}
|
|
1470
|
+
const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
|
|
1323
1471
|
try {
|
|
1324
1472
|
await accessFn();
|
|
1325
1473
|
} catch (error) {
|
|
@@ -1716,75 +1864,131 @@ function wrapInSlotsProvider(shell, slotValues) {
|
|
|
1716
1864
|
});
|
|
1717
1865
|
}
|
|
1718
1866
|
//#endregion
|
|
1867
|
+
//#region src/server/error-boundary-wrapper.ts
|
|
1868
|
+
/**
|
|
1869
|
+
* Load a status/error file's component, or `null` if the boundary should be skipped.
|
|
1870
|
+
*
|
|
1871
|
+
* Two ways to get `null`, both deliberate:
|
|
1872
|
+
* - the module fails to load (syntax error in the error page) — building an
|
|
1873
|
+
* error boundary must not itself throw, so the boundary is skipped (TIM-584);
|
|
1874
|
+
* - the default export is not a valid React component type, which would
|
|
1875
|
+
* otherwise crash inside `createElement`.
|
|
1876
|
+
*/
|
|
1877
|
+
async function loadBoundaryComponent(file, loadModule) {
|
|
1878
|
+
let mod;
|
|
1879
|
+
try {
|
|
1880
|
+
mod = await loadModule(file);
|
|
1881
|
+
} catch {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
return isValidElementType(mod?.default) ? mod.default : null;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Wrap `element` in one error boundary rendering `Component` on failure.
|
|
1888
|
+
*
|
|
1889
|
+
* MDX status files are server components and cannot be passed as function
|
|
1890
|
+
* props to `TimberErrorBoundary` (a 'use client' component). They are
|
|
1891
|
+
* pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
|
|
1892
|
+
*
|
|
1893
|
+
* @param status boundary status filter — omitted entirely for `error.tsx`
|
|
1894
|
+
* @param fallbackProps props handed to an MDX fallback when it is pre-rendered
|
|
1895
|
+
*/
|
|
1896
|
+
function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
|
|
1897
|
+
const props = isMdxFilePath(file.filePath) ? {
|
|
1898
|
+
fallbackElement: config.createElement(Component, fallbackProps),
|
|
1899
|
+
children: element
|
|
1900
|
+
} : {
|
|
1901
|
+
fallbackComponent: Component,
|
|
1902
|
+
children: element
|
|
1903
|
+
};
|
|
1904
|
+
if (status !== void 0) props.status = status;
|
|
1905
|
+
if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
|
|
1906
|
+
return config.createElement(config.errorBoundaryComponent, props);
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Wrap an element in the error boundaries declared by a route segment.
|
|
1910
|
+
*
|
|
1911
|
+
* Wrapping is applied inside-out — the last wrap produces the outermost
|
|
1912
|
+
* boundary — so the processing order below is innermost to outermost:
|
|
1913
|
+
*
|
|
1914
|
+
* 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
|
|
1915
|
+
* 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
|
|
1916
|
+
* 3. `error.tsx` — catches anything not matched by a status file
|
|
1917
|
+
*
|
|
1918
|
+
* The order is load-bearing: it decides which boundary catches a given throw.
|
|
1919
|
+
*
|
|
1920
|
+
* See design/10-error-handling.md §"Format-Aware Fallback Chains".
|
|
1921
|
+
*/
|
|
1922
|
+
async function wrapSegmentWithErrorBoundaries(segment, element, config) {
|
|
1923
|
+
const { loadModule, skipDenyBoundaries = false } = config;
|
|
1924
|
+
if (segment.statusFiles) {
|
|
1925
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1926
|
+
if (key === "4xx" || key === "5xx") continue;
|
|
1927
|
+
const status = parseInt(key, 10);
|
|
1928
|
+
if (isNaN(status)) continue;
|
|
1929
|
+
if (skipDenyBoundaries && status >= 400 && status <= 499) continue;
|
|
1930
|
+
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1931
|
+
if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
|
|
1932
|
+
}
|
|
1933
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1934
|
+
if (key !== "4xx" && key !== "5xx") continue;
|
|
1935
|
+
if (skipDenyBoundaries && key === "4xx") continue;
|
|
1936
|
+
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1937
|
+
if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (segment.error) {
|
|
1941
|
+
const Component = await loadBoundaryComponent(segment.error, loadModule);
|
|
1942
|
+
if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
|
|
1943
|
+
}
|
|
1944
|
+
return element;
|
|
1945
|
+
}
|
|
1946
|
+
//#endregion
|
|
1719
1947
|
//#region src/server/param-coercion.ts
|
|
1720
1948
|
/** Global schema codecs, set once at startup from virtual:timber-schema. */
|
|
1721
1949
|
var _globalCodecs = null;
|
|
1722
1950
|
/**
|
|
1723
1951
|
* Run segment param coercion on the matched route's segments.
|
|
1724
1952
|
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1953
|
+
* Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
|
|
1954
|
+
* design/41-global-params.md migration step 9). A route with no schema, or
|
|
1955
|
+
* a param the schema does not declare, keeps its raw string — which is the
|
|
1956
|
+
* honest type for an uninterpreted URL segment.
|
|
1728
1957
|
*
|
|
1729
1958
|
* Throws ParamCoercionError if any codec fails (→ 404).
|
|
1730
1959
|
*
|
|
1731
1960
|
* This runs BEFORE middleware, so ctx.segmentParams is already typed.
|
|
1732
1961
|
* See design/07-routing.md §"Where Coercion Runs"
|
|
1733
1962
|
* See design/41-global-params.md §"Pipeline Integration"
|
|
1963
|
+
*
|
|
1964
|
+
* Stays `async` with nothing to await: it is injected as a
|
|
1965
|
+
* `CoerceSegmentParamsFn` into the revalidation renderer and the action
|
|
1966
|
+
* middleware runner, both of which await it. Narrowing the return type to
|
|
1967
|
+
* `void` would break those contracts and remove a microtask boundary the
|
|
1968
|
+
* pipeline's ordering has always had, for no gain.
|
|
1734
1969
|
*/
|
|
1735
1970
|
async function coerceSegmentParams(match) {
|
|
1736
1971
|
const globalCodecs = _globalCodecs;
|
|
1737
1972
|
const mergeTarget = Object.create(null);
|
|
1738
1973
|
for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
|
|
1739
1974
|
match.segmentParams = mergeTarget;
|
|
1740
|
-
if (globalCodecs) {
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
let parsed;
|
|
1750
|
-
try {
|
|
1751
|
-
parsed = codec.parse(mergeTarget[key]);
|
|
1752
|
-
} catch (err) {
|
|
1753
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1754
|
-
if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
|
|
1755
|
-
throw new ParamCoercionError(message);
|
|
1756
|
-
}
|
|
1757
|
-
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
1758
|
-
}
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
|
-
for (const segment of match.segments) {
|
|
1762
|
-
if (!segment.params) continue;
|
|
1763
|
-
let mod;
|
|
1764
|
-
try {
|
|
1765
|
-
mod = await loadModule(segment.params);
|
|
1766
|
-
} catch (err) {
|
|
1767
|
-
const message = `Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`;
|
|
1768
|
-
if (isDebug()) console.warn(`[timber] Param coercion error: ${message}\n Segment: ${segment.segmentName}\n Params file: ${segment.params}`);
|
|
1769
|
-
throw new ParamCoercionError(message);
|
|
1770
|
-
}
|
|
1771
|
-
const segmentParamsDef = mod.segmentParams;
|
|
1772
|
-
if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
|
|
1773
|
-
let coerced;
|
|
1975
|
+
if (globalCodecs) for (const node of match.segments) {
|
|
1976
|
+
const segment = effectiveUrlSegment(node);
|
|
1977
|
+
if (!segment.paramName) continue;
|
|
1978
|
+
const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
|
|
1979
|
+
const codec = globalCodecs[bracketKey];
|
|
1980
|
+
if (!codec) continue;
|
|
1981
|
+
const key = segment.paramName;
|
|
1982
|
+
if (!(key in mergeTarget)) continue;
|
|
1983
|
+
let parsed;
|
|
1774
1984
|
try {
|
|
1775
|
-
|
|
1985
|
+
parsed = codec.parse(mergeTarget[key]);
|
|
1776
1986
|
} catch (err) {
|
|
1777
1987
|
const message = err instanceof Error ? err.message : String(err);
|
|
1778
|
-
if (isDebug()) {
|
|
1779
|
-
const rawKeys = Object.keys(match.segmentParams).join(", ");
|
|
1780
|
-
console.warn(`[timber] Param codec rejected values for segment "${segment.segmentName}"\n Error: ${message}\n Available raw params: { ${rawKeys} }\n Params file: ${segment.params}\n Hint: this usually means a codec threw for the raw URL value.\n Check that the regex/schema accepts the actual path segment string.`);
|
|
1781
|
-
}
|
|
1988
|
+
if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
|
|
1782
1989
|
throw new ParamCoercionError(message);
|
|
1783
1990
|
}
|
|
1784
|
-
|
|
1785
|
-
if (key === "__proto__") continue;
|
|
1786
|
-
mergeTarget[key] = normalizeParamValue(coerced[key], key);
|
|
1787
|
-
}
|
|
1991
|
+
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
1788
1992
|
}
|
|
1789
1993
|
}
|
|
1790
1994
|
//#endregion
|
|
@@ -2007,7 +2211,7 @@ function parseClientStateTree(req) {
|
|
|
2007
2211
|
//#endregion
|
|
2008
2212
|
//#region src/server/route-element-builder.ts
|
|
2009
2213
|
/**
|
|
2010
|
-
* Thrown when
|
|
2214
|
+
* Thrown when an app/schema.ts segment-param codec's parse() fails.
|
|
2011
2215
|
* The pipeline catches this and responds with 404.
|
|
2012
2216
|
*/
|
|
2013
2217
|
var ParamCoercionError = class extends Error {
|
|
@@ -2150,7 +2354,7 @@ async function outcomeToResponse(config, outcome, ctx) {
|
|
|
2150
2354
|
});
|
|
2151
2355
|
}
|
|
2152
2356
|
case "error": {
|
|
2153
|
-
const isRsc = (ctx.req
|
|
2357
|
+
const isRsc = isRscRequest(ctx.req);
|
|
2154
2358
|
if (outcome.phase === "proxy") {
|
|
2155
2359
|
logProxyError({ error: outcome.error });
|
|
2156
2360
|
await fireOnRequestError(outcome.error, ctx.req, "proxy");
|
|
@@ -2437,8 +2641,8 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2437
2641
|
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
|
|
2438
2642
|
return new Response(null, { status: 500 });
|
|
2439
2643
|
}
|
|
2440
|
-
const
|
|
2441
|
-
if (
|
|
2644
|
+
const isRsc = isRscRequest(req);
|
|
2645
|
+
if (isRsc) {
|
|
2442
2646
|
if (!checkVersionSkew(req).ok) {
|
|
2443
2647
|
const reloadHeaders = new Headers();
|
|
2444
2648
|
applyReloadHeaders(reloadHeaders);
|
|
@@ -2450,7 +2654,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2450
2654
|
}
|
|
2451
2655
|
let match = config.matchRoute(canonicalPathname);
|
|
2452
2656
|
let interception;
|
|
2453
|
-
if (
|
|
2657
|
+
if (isRsc && config.interceptionRewrites?.length) {
|
|
2454
2658
|
const rawSourceUrl = req.headers.get("X-Timber-URL");
|
|
2455
2659
|
const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
|
|
2456
2660
|
if (validatedSourceUrl) {
|
|
@@ -2486,7 +2690,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2486
2690
|
responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
|
|
2487
2691
|
const leafSegment = match.segments[match.segments.length - 1];
|
|
2488
2692
|
const isApiRoute = leafSegment?.route && !leafSegment?.page;
|
|
2489
|
-
if (config.earlyHints && !
|
|
2693
|
+
if (config.earlyHints && !isRsc && !isApiRoute) try {
|
|
2490
2694
|
await config.earlyHints(match, req, responseHeaders);
|
|
2491
2695
|
} catch (err) {
|
|
2492
2696
|
swallow(err, "early hints hook threw");
|
|
@@ -2498,7 +2702,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2498
2702
|
if (error instanceof ParamCoercionError) {
|
|
2499
2703
|
if (isDebug()) {
|
|
2500
2704
|
const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
|
|
2501
|
-
console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a
|
|
2705
|
+
console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a codec in app/schema.ts rejected the URL value.\n Check the codec for the bracket key of the segment named above.`);
|
|
2502
2706
|
}
|
|
2503
2707
|
const leafSegment = match.segments[match.segments.length - 1];
|
|
2504
2708
|
if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
|
|
@@ -2610,12 +2814,6 @@ async function hasValidRscCacheKey(req) {
|
|
|
2610
2814
|
return claims.every((claim) => claim === expected);
|
|
2611
2815
|
}
|
|
2612
2816
|
/**
|
|
2613
|
-
* Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
|
|
2614
|
-
* `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
|
|
2615
|
-
* duplication predates this module and is tracked separately.
|
|
2616
|
-
*/
|
|
2617
|
-
var RSC_MEDIA_TYPE = "text/x-component";
|
|
2618
|
-
/**
|
|
2619
2817
|
* Ensure a Flight payload can never be interpreted as a document.
|
|
2620
2818
|
*
|
|
2621
2819
|
* Applied here, at the pipeline's single exit, rather than where each
|
|
@@ -2633,7 +2831,7 @@ var RSC_MEDIA_TYPE = "text/x-component";
|
|
|
2633
2831
|
function hardenRscPayloadResponse(response) {
|
|
2634
2832
|
const contentType = response.headers.get("content-type");
|
|
2635
2833
|
if (!contentType) return;
|
|
2636
|
-
if (contentType.split(";")[0].trim().toLowerCase() !==
|
|
2834
|
+
if (contentType.split(";")[0].trim().toLowerCase() !== "text/x-component") return;
|
|
2637
2835
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
2638
2836
|
}
|
|
2639
2837
|
/**
|
|
@@ -2958,40 +3156,6 @@ function sendEarlyHints103(links) {
|
|
|
2958
3156
|
}
|
|
2959
3157
|
//#endregion
|
|
2960
3158
|
//#region src/server/tree-builder.ts
|
|
2961
|
-
var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
|
|
2962
|
-
Symbol.for("react.forward_ref"),
|
|
2963
|
-
Symbol.for("react.memo"),
|
|
2964
|
-
Symbol.for("react.lazy"),
|
|
2965
|
-
Symbol.for("react.provider"),
|
|
2966
|
-
Symbol.for("react.context"),
|
|
2967
|
-
Symbol.for("react.suspense"),
|
|
2968
|
-
Symbol.for("react.suspense_list"),
|
|
2969
|
-
Symbol.for("react.client.reference")
|
|
2970
|
-
]);
|
|
2971
|
-
/**
|
|
2972
|
-
* Validate that a loaded module's `default` export is something React
|
|
2973
|
-
* accepts as the first argument to `createElement` — i.e. a valid component
|
|
2974
|
-
* type. React doesn't export `isValidElementType` (only `isValidElement`,
|
|
2975
|
-
* which checks for *elements*, not *component types*), so this mirrors
|
|
2976
|
-
* React's internal check:
|
|
2977
|
-
*
|
|
2978
|
-
* - functions → function or class components
|
|
2979
|
-
* - objects with a `$$typeof` matching one of React's known component
|
|
2980
|
-
* markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
|
|
2981
|
-
* suspense, client references via `@vitejs/plugin-rsc`)
|
|
2982
|
-
*
|
|
2983
|
-
* Strings (HTML tag names) are valid for `createElement` but never appear
|
|
2984
|
-
* as a route module's default export, so they're not recognized here.
|
|
2985
|
-
*
|
|
2986
|
-
* Anything else (numbers, plain config objects, JSON, etc.) is rejected so
|
|
2987
|
-
* the boundary wrapper is skipped rather than crashing inside React.
|
|
2988
|
-
*/
|
|
2989
|
-
function isValidElementType(value) {
|
|
2990
|
-
if (typeof value === "function") return true;
|
|
2991
|
-
if (typeof value !== "object" || value === null) return false;
|
|
2992
|
-
const marker = value.$$typeof;
|
|
2993
|
-
return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
|
|
2994
|
-
}
|
|
2995
3159
|
/**
|
|
2996
3160
|
* Build the unified element tree from a matched segment chain.
|
|
2997
3161
|
*
|
|
@@ -3017,7 +3181,11 @@ async function buildElementTree(config) {
|
|
|
3017
3181
|
let element = createElement(PageComponent, {});
|
|
3018
3182
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
3019
3183
|
const segment = segments[i];
|
|
3020
|
-
element = await
|
|
3184
|
+
element = await wrapSegmentWithErrorBoundaries(segment, element, {
|
|
3185
|
+
loadModule,
|
|
3186
|
+
createElement,
|
|
3187
|
+
errorBoundaryComponent
|
|
3188
|
+
});
|
|
3021
3189
|
if (segment.access) {
|
|
3022
3190
|
const accessFn = (await loadModule(segment.access)).default;
|
|
3023
3191
|
element = createElement("timber:access-gate", {
|
|
@@ -3058,7 +3226,11 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
|
|
|
3058
3226
|
const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
|
|
3059
3227
|
if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
|
|
3060
3228
|
let element = createElement(PageComponent, {});
|
|
3061
|
-
element = await
|
|
3229
|
+
element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
|
|
3230
|
+
loadModule,
|
|
3231
|
+
createElement,
|
|
3232
|
+
errorBoundaryComponent
|
|
3233
|
+
});
|
|
3062
3234
|
if (slotNode.access) {
|
|
3063
3235
|
const accessFn = (await loadModule(slotNode.access)).default;
|
|
3064
3236
|
const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
|
|
@@ -3074,81 +3246,6 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
|
|
|
3074
3246
|
}
|
|
3075
3247
|
return element;
|
|
3076
3248
|
}
|
|
3077
|
-
/** MDX/markdown extensions — these are server components that cannot be passed as function props. */
|
|
3078
|
-
var MDX_EXTENSIONS = /* @__PURE__ */ new Set(["mdx", "md"]);
|
|
3079
|
-
/**
|
|
3080
|
-
* Check if a route file is an MDX/markdown file based on its extension.
|
|
3081
|
-
* MDX components are server components by default and cannot cross the
|
|
3082
|
-
* RSC→client boundary as function props. They must be pre-rendered as
|
|
3083
|
-
* elements and passed as fallbackElement instead of fallbackComponent.
|
|
3084
|
-
*/
|
|
3085
|
-
function isMdxFile(file) {
|
|
3086
|
-
return MDX_EXTENSIONS.has(file.extension);
|
|
3087
|
-
}
|
|
3088
|
-
/**
|
|
3089
|
-
* Wrap an element with error boundaries from a segment's status-code files.
|
|
3090
|
-
*
|
|
3091
|
-
* Wrapping order (innermost to outermost):
|
|
3092
|
-
* 1. Specific status files (503.tsx, 429.tsx, etc.)
|
|
3093
|
-
* 2. Category catch-alls (4xx.tsx, 5xx.tsx)
|
|
3094
|
-
* 3. error.tsx (general error boundary)
|
|
3095
|
-
*
|
|
3096
|
-
* This creates the fallback chain described in design/10-error-handling.md.
|
|
3097
|
-
*
|
|
3098
|
-
* MDX status files are server components and cannot be passed as function
|
|
3099
|
-
* props to TimberErrorBoundary (a 'use client' component). Instead, they
|
|
3100
|
-
* are pre-rendered as elements and passed as fallbackElement. The error
|
|
3101
|
-
* boundary renders the element directly when an error is caught.
|
|
3102
|
-
* See TIM-503.
|
|
3103
|
-
*/
|
|
3104
|
-
async function wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent) {
|
|
3105
|
-
if (segment.statusFiles) {
|
|
3106
|
-
for (const [key, file] of Object.entries(segment.statusFiles)) if (key !== "4xx" && key !== "5xx") {
|
|
3107
|
-
const status = parseInt(key, 10);
|
|
3108
|
-
if (!isNaN(status)) {
|
|
3109
|
-
const mod = await loadModule(file);
|
|
3110
|
-
const Component = isValidElementType(mod.default) ? mod.default : null;
|
|
3111
|
-
if (Component) element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
|
|
3112
|
-
fallbackElement: createElement(Component, { status }),
|
|
3113
|
-
status,
|
|
3114
|
-
children: element
|
|
3115
|
-
} : {
|
|
3116
|
-
fallbackComponent: Component,
|
|
3117
|
-
status,
|
|
3118
|
-
children: element
|
|
3119
|
-
});
|
|
3120
|
-
}
|
|
3121
|
-
}
|
|
3122
|
-
for (const [key, file] of Object.entries(segment.statusFiles)) if (key === "4xx" || key === "5xx") {
|
|
3123
|
-
const mod = await loadModule(file);
|
|
3124
|
-
const Component = isValidElementType(mod.default) ? mod.default : null;
|
|
3125
|
-
if (Component) {
|
|
3126
|
-
const categoryStatus = key === "4xx" ? 400 : 500;
|
|
3127
|
-
element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
|
|
3128
|
-
fallbackElement: createElement(Component, {}),
|
|
3129
|
-
status: categoryStatus,
|
|
3130
|
-
children: element
|
|
3131
|
-
} : {
|
|
3132
|
-
fallbackComponent: Component,
|
|
3133
|
-
status: categoryStatus,
|
|
3134
|
-
children: element
|
|
3135
|
-
});
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
if (segment.error) {
|
|
3140
|
-
const errorModule = await loadModule(segment.error);
|
|
3141
|
-
const ErrorComponent = isValidElementType(errorModule.default) ? errorModule.default : null;
|
|
3142
|
-
if (ErrorComponent) element = createElement(errorBoundaryComponent, isMdxFile(segment.error) ? {
|
|
3143
|
-
fallbackElement: createElement(ErrorComponent, {}),
|
|
3144
|
-
children: element
|
|
3145
|
-
} : {
|
|
3146
|
-
fallbackComponent: ErrorComponent,
|
|
3147
|
-
children: element
|
|
3148
|
-
});
|
|
3149
|
-
}
|
|
3150
|
-
return element;
|
|
3151
|
-
}
|
|
3152
3249
|
//#endregion
|
|
3153
3250
|
//#region src/server/status-code-resolver.ts
|
|
3154
3251
|
/**
|
|
@@ -3186,12 +3283,19 @@ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
|
|
|
3186
3283
|
* @param status - The HTTP status code (4xx or 5xx).
|
|
3187
3284
|
* @param segments - The matched segment chain from root (index 0) to leaf (last).
|
|
3188
3285
|
* @param format - The response format family ('component' or 'json'). Defaults to 'component'.
|
|
3189
|
-
|
|
3190
|
-
|
|
3286
|
+
* @param maxIndex - Optional upper bound on the segment index to search.
|
|
3287
|
+
* When set, the resolver only considers segments `0..maxIndex` (inclusive),
|
|
3288
|
+
* skipping any segments deeper in the tree. Used by the deny renderers to
|
|
3289
|
+
* prevent resolving a status file below a denied segment — matching
|
|
3290
|
+
* `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
|
|
3291
|
+
* this parameter, so the full chain is searched — a 500 from a leaf page
|
|
3292
|
+
* should use the leaf's own `5xx.tsx`. See TIM-1358.
|
|
3293
|
+
*/
|
|
3294
|
+
function resolveStatusFile(status, segments, format = "component", maxIndex) {
|
|
3191
3295
|
if (status < 400 || status > 599) return null;
|
|
3192
|
-
if (format === "json") return resolveJson(status, segments);
|
|
3193
|
-
if (status <= 499) return resolve4xx(status, segments);
|
|
3194
|
-
return resolve5xx(status, segments);
|
|
3296
|
+
if (format === "json") return resolveJson(status, segments, maxIndex);
|
|
3297
|
+
if (status <= 499) return resolve4xx(status, segments, maxIndex);
|
|
3298
|
+
return resolve5xx(status, segments, maxIndex);
|
|
3195
3299
|
}
|
|
3196
3300
|
/**
|
|
3197
3301
|
* 4xx component fallback chain — two separate full passes leaf→root.
|
|
@@ -3205,13 +3309,14 @@ function resolveStatusFile(status, segments, format = "component") {
|
|
|
3205
3309
|
* Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
|
|
3206
3310
|
* Pass 2 — error.tsx (error)
|
|
3207
3311
|
*/
|
|
3208
|
-
function resolve4xx(status, segments) {
|
|
3312
|
+
function resolve4xx(status, segments, maxIndex) {
|
|
3209
3313
|
const statusStr = String(status);
|
|
3210
|
-
|
|
3314
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3315
|
+
for (let i = start; i >= 0; i--) {
|
|
3211
3316
|
const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
|
|
3212
3317
|
if (r) return r;
|
|
3213
3318
|
}
|
|
3214
|
-
for (let i =
|
|
3319
|
+
for (let i = start; i >= 0; i--) {
|
|
3215
3320
|
const errorFile = segments[i].error;
|
|
3216
3321
|
if (errorFile) return {
|
|
3217
3322
|
file: errorFile,
|
|
@@ -3229,9 +3334,10 @@ function resolve4xx(status, segments) {
|
|
|
3229
3334
|
* `error.tsx` therefore beats a root's `5xx.tsx`, which is the
|
|
3230
3335
|
* intentional inverse of the 4xx chain.
|
|
3231
3336
|
*/
|
|
3232
|
-
function resolve5xx(status, segments) {
|
|
3337
|
+
function resolve5xx(status, segments, maxIndex) {
|
|
3233
3338
|
const statusStr = String(status);
|
|
3234
|
-
|
|
3339
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3340
|
+
for (let i = start; i >= 0; i--) {
|
|
3235
3341
|
const segment = segments[i];
|
|
3236
3342
|
const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
|
|
3237
3343
|
if (r) return r;
|
|
@@ -3251,10 +3357,11 @@ function resolve5xx(status, segments) {
|
|
|
3251
3357
|
* no error.tsx — the JSON chain terminates at the category catch-all
|
|
3252
3358
|
* and the caller falls back to a bare-JSON framework default.
|
|
3253
3359
|
*/
|
|
3254
|
-
function resolveJson(status, segments) {
|
|
3360
|
+
function resolveJson(status, segments, maxIndex) {
|
|
3255
3361
|
const statusStr = String(status);
|
|
3256
3362
|
const categoryKey = status >= 500 ? "5xx" : "4xx";
|
|
3257
|
-
|
|
3363
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3364
|
+
for (let i = start; i >= 0; i--) {
|
|
3258
3365
|
const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
|
|
3259
3366
|
if (r) return r;
|
|
3260
3367
|
}
|