@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
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
|
}
|
|
@@ -1716,75 +1870,131 @@ function wrapInSlotsProvider(shell, slotValues) {
|
|
|
1716
1870
|
});
|
|
1717
1871
|
}
|
|
1718
1872
|
//#endregion
|
|
1873
|
+
//#region src/server/error-boundary-wrapper.ts
|
|
1874
|
+
/**
|
|
1875
|
+
* Load a status/error file's component, or `null` if the boundary should be skipped.
|
|
1876
|
+
*
|
|
1877
|
+
* Two ways to get `null`, both deliberate:
|
|
1878
|
+
* - the module fails to load (syntax error in the error page) — building an
|
|
1879
|
+
* error boundary must not itself throw, so the boundary is skipped (TIM-584);
|
|
1880
|
+
* - the default export is not a valid React component type, which would
|
|
1881
|
+
* otherwise crash inside `createElement`.
|
|
1882
|
+
*/
|
|
1883
|
+
async function loadBoundaryComponent(file, loadModule) {
|
|
1884
|
+
let mod;
|
|
1885
|
+
try {
|
|
1886
|
+
mod = await loadModule(file);
|
|
1887
|
+
} catch {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
return isValidElementType(mod?.default) ? mod.default : null;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Wrap `element` in one error boundary rendering `Component` on failure.
|
|
1894
|
+
*
|
|
1895
|
+
* MDX status files are server components and cannot be passed as function
|
|
1896
|
+
* props to `TimberErrorBoundary` (a 'use client' component). They are
|
|
1897
|
+
* pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
|
|
1898
|
+
*
|
|
1899
|
+
* @param status boundary status filter — omitted entirely for `error.tsx`
|
|
1900
|
+
* @param fallbackProps props handed to an MDX fallback when it is pre-rendered
|
|
1901
|
+
*/
|
|
1902
|
+
function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
|
|
1903
|
+
const props = isMdxFilePath(file.filePath) ? {
|
|
1904
|
+
fallbackElement: config.createElement(Component, fallbackProps),
|
|
1905
|
+
children: element
|
|
1906
|
+
} : {
|
|
1907
|
+
fallbackComponent: Component,
|
|
1908
|
+
children: element
|
|
1909
|
+
};
|
|
1910
|
+
if (status !== void 0) props.status = status;
|
|
1911
|
+
if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
|
|
1912
|
+
return config.createElement(config.errorBoundaryComponent, props);
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Wrap an element in the error boundaries declared by a route segment.
|
|
1916
|
+
*
|
|
1917
|
+
* Wrapping is applied inside-out — the last wrap produces the outermost
|
|
1918
|
+
* boundary — so the processing order below is innermost to outermost:
|
|
1919
|
+
*
|
|
1920
|
+
* 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
|
|
1921
|
+
* 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
|
|
1922
|
+
* 3. `error.tsx` — catches anything not matched by a status file
|
|
1923
|
+
*
|
|
1924
|
+
* The order is load-bearing: it decides which boundary catches a given throw.
|
|
1925
|
+
*
|
|
1926
|
+
* See design/10-error-handling.md §"Format-Aware Fallback Chains".
|
|
1927
|
+
*/
|
|
1928
|
+
async function wrapSegmentWithErrorBoundaries(segment, element, config) {
|
|
1929
|
+
const { loadModule, skipDenyBoundaries = false } = config;
|
|
1930
|
+
if (segment.statusFiles) {
|
|
1931
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1932
|
+
if (key === "4xx" || key === "5xx") continue;
|
|
1933
|
+
const status = parseInt(key, 10);
|
|
1934
|
+
if (isNaN(status)) continue;
|
|
1935
|
+
if (skipDenyBoundaries && status >= 400 && status <= 499) continue;
|
|
1936
|
+
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1937
|
+
if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
|
|
1938
|
+
}
|
|
1939
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1940
|
+
if (key !== "4xx" && key !== "5xx") continue;
|
|
1941
|
+
if (skipDenyBoundaries && key === "4xx") continue;
|
|
1942
|
+
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1943
|
+
if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (segment.error) {
|
|
1947
|
+
const Component = await loadBoundaryComponent(segment.error, loadModule);
|
|
1948
|
+
if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
|
|
1949
|
+
}
|
|
1950
|
+
return element;
|
|
1951
|
+
}
|
|
1952
|
+
//#endregion
|
|
1719
1953
|
//#region src/server/param-coercion.ts
|
|
1720
1954
|
/** Global schema codecs, set once at startup from virtual:timber-schema. */
|
|
1721
1955
|
var _globalCodecs = null;
|
|
1722
1956
|
/**
|
|
1723
1957
|
* Run segment param coercion on the matched route's segments.
|
|
1724
1958
|
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1959
|
+
* Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
|
|
1960
|
+
* design/41-global-params.md migration step 9). A route with no schema, or
|
|
1961
|
+
* a param the schema does not declare, keeps its raw string — which is the
|
|
1962
|
+
* honest type for an uninterpreted URL segment.
|
|
1728
1963
|
*
|
|
1729
1964
|
* Throws ParamCoercionError if any codec fails (→ 404).
|
|
1730
1965
|
*
|
|
1731
1966
|
* This runs BEFORE middleware, so ctx.segmentParams is already typed.
|
|
1732
1967
|
* See design/07-routing.md §"Where Coercion Runs"
|
|
1733
1968
|
* See design/41-global-params.md §"Pipeline Integration"
|
|
1969
|
+
*
|
|
1970
|
+
* Stays `async` with nothing to await: it is injected as a
|
|
1971
|
+
* `CoerceSegmentParamsFn` into the revalidation renderer and the action
|
|
1972
|
+
* middleware runner, both of which await it. Narrowing the return type to
|
|
1973
|
+
* `void` would break those contracts and remove a microtask boundary the
|
|
1974
|
+
* pipeline's ordering has always had, for no gain.
|
|
1734
1975
|
*/
|
|
1735
1976
|
async function coerceSegmentParams(match) {
|
|
1736
1977
|
const globalCodecs = _globalCodecs;
|
|
1737
1978
|
const mergeTarget = Object.create(null);
|
|
1738
1979
|
for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
|
|
1739
1980
|
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;
|
|
1981
|
+
if (globalCodecs) for (const node of match.segments) {
|
|
1982
|
+
const segment = effectiveUrlSegment(node);
|
|
1983
|
+
if (!segment.paramName) continue;
|
|
1984
|
+
const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
|
|
1985
|
+
const codec = globalCodecs[bracketKey];
|
|
1986
|
+
if (!codec) continue;
|
|
1987
|
+
const key = segment.paramName;
|
|
1988
|
+
if (!(key in mergeTarget)) continue;
|
|
1989
|
+
let parsed;
|
|
1774
1990
|
try {
|
|
1775
|
-
|
|
1991
|
+
parsed = codec.parse(mergeTarget[key]);
|
|
1776
1992
|
} catch (err) {
|
|
1777
1993
|
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
|
-
}
|
|
1994
|
+
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
1995
|
throw new ParamCoercionError(message);
|
|
1783
1996
|
}
|
|
1784
|
-
|
|
1785
|
-
if (key === "__proto__") continue;
|
|
1786
|
-
mergeTarget[key] = normalizeParamValue(coerced[key], key);
|
|
1787
|
-
}
|
|
1997
|
+
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
1788
1998
|
}
|
|
1789
1999
|
}
|
|
1790
2000
|
//#endregion
|
|
@@ -2007,7 +2217,7 @@ function parseClientStateTree(req) {
|
|
|
2007
2217
|
//#endregion
|
|
2008
2218
|
//#region src/server/route-element-builder.ts
|
|
2009
2219
|
/**
|
|
2010
|
-
* Thrown when
|
|
2220
|
+
* Thrown when an app/schema.ts segment-param codec's parse() fails.
|
|
2011
2221
|
* The pipeline catches this and responds with 404.
|
|
2012
2222
|
*/
|
|
2013
2223
|
var ParamCoercionError = class extends Error {
|
|
@@ -2150,7 +2360,7 @@ async function outcomeToResponse(config, outcome, ctx) {
|
|
|
2150
2360
|
});
|
|
2151
2361
|
}
|
|
2152
2362
|
case "error": {
|
|
2153
|
-
const isRsc = (ctx.req
|
|
2363
|
+
const isRsc = isRscRequest(ctx.req);
|
|
2154
2364
|
if (outcome.phase === "proxy") {
|
|
2155
2365
|
logProxyError({ error: outcome.error });
|
|
2156
2366
|
await fireOnRequestError(outcome.error, ctx.req, "proxy");
|
|
@@ -2437,8 +2647,8 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2437
2647
|
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
|
|
2438
2648
|
return new Response(null, { status: 500 });
|
|
2439
2649
|
}
|
|
2440
|
-
const
|
|
2441
|
-
if (
|
|
2650
|
+
const isRsc = isRscRequest(req);
|
|
2651
|
+
if (isRsc) {
|
|
2442
2652
|
if (!checkVersionSkew(req).ok) {
|
|
2443
2653
|
const reloadHeaders = new Headers();
|
|
2444
2654
|
applyReloadHeaders(reloadHeaders);
|
|
@@ -2450,7 +2660,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2450
2660
|
}
|
|
2451
2661
|
let match = config.matchRoute(canonicalPathname);
|
|
2452
2662
|
let interception;
|
|
2453
|
-
if (
|
|
2663
|
+
if (isRsc && config.interceptionRewrites?.length) {
|
|
2454
2664
|
const rawSourceUrl = req.headers.get("X-Timber-URL");
|
|
2455
2665
|
const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
|
|
2456
2666
|
if (validatedSourceUrl) {
|
|
@@ -2486,7 +2696,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2486
2696
|
responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
|
|
2487
2697
|
const leafSegment = match.segments[match.segments.length - 1];
|
|
2488
2698
|
const isApiRoute = leafSegment?.route && !leafSegment?.page;
|
|
2489
|
-
if (config.earlyHints && !
|
|
2699
|
+
if (config.earlyHints && !isRsc && !isApiRoute) try {
|
|
2490
2700
|
await config.earlyHints(match, req, responseHeaders);
|
|
2491
2701
|
} catch (err) {
|
|
2492
2702
|
swallow(err, "early hints hook threw");
|
|
@@ -2498,7 +2708,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2498
2708
|
if (error instanceof ParamCoercionError) {
|
|
2499
2709
|
if (isDebug()) {
|
|
2500
2710
|
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
|
|
2711
|
+
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
2712
|
}
|
|
2503
2713
|
const leafSegment = match.segments[match.segments.length - 1];
|
|
2504
2714
|
if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
|
|
@@ -2610,12 +2820,6 @@ async function hasValidRscCacheKey(req) {
|
|
|
2610
2820
|
return claims.every((claim) => claim === expected);
|
|
2611
2821
|
}
|
|
2612
2822
|
/**
|
|
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
2823
|
* Ensure a Flight payload can never be interpreted as a document.
|
|
2620
2824
|
*
|
|
2621
2825
|
* Applied here, at the pipeline's single exit, rather than where each
|
|
@@ -2633,7 +2837,7 @@ var RSC_MEDIA_TYPE = "text/x-component";
|
|
|
2633
2837
|
function hardenRscPayloadResponse(response) {
|
|
2634
2838
|
const contentType = response.headers.get("content-type");
|
|
2635
2839
|
if (!contentType) return;
|
|
2636
|
-
if (contentType.split(";")[0].trim().toLowerCase() !==
|
|
2840
|
+
if (contentType.split(";")[0].trim().toLowerCase() !== "text/x-component") return;
|
|
2637
2841
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
2638
2842
|
}
|
|
2639
2843
|
/**
|
|
@@ -2958,40 +3162,6 @@ function sendEarlyHints103(links) {
|
|
|
2958
3162
|
}
|
|
2959
3163
|
//#endregion
|
|
2960
3164
|
//#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
3165
|
/**
|
|
2996
3166
|
* Build the unified element tree from a matched segment chain.
|
|
2997
3167
|
*
|
|
@@ -3017,7 +3187,11 @@ async function buildElementTree(config) {
|
|
|
3017
3187
|
let element = createElement(PageComponent, {});
|
|
3018
3188
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
3019
3189
|
const segment = segments[i];
|
|
3020
|
-
element = await
|
|
3190
|
+
element = await wrapSegmentWithErrorBoundaries(segment, element, {
|
|
3191
|
+
loadModule,
|
|
3192
|
+
createElement,
|
|
3193
|
+
errorBoundaryComponent
|
|
3194
|
+
});
|
|
3021
3195
|
if (segment.access) {
|
|
3022
3196
|
const accessFn = (await loadModule(segment.access)).default;
|
|
3023
3197
|
element = createElement("timber:access-gate", {
|
|
@@ -3058,7 +3232,11 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
|
|
|
3058
3232
|
const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
|
|
3059
3233
|
if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
|
|
3060
3234
|
let element = createElement(PageComponent, {});
|
|
3061
|
-
element = await
|
|
3235
|
+
element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
|
|
3236
|
+
loadModule,
|
|
3237
|
+
createElement,
|
|
3238
|
+
errorBoundaryComponent
|
|
3239
|
+
});
|
|
3062
3240
|
if (slotNode.access) {
|
|
3063
3241
|
const accessFn = (await loadModule(slotNode.access)).default;
|
|
3064
3242
|
const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
|
|
@@ -3074,81 +3252,6 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
|
|
|
3074
3252
|
}
|
|
3075
3253
|
return element;
|
|
3076
3254
|
}
|
|
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
3255
|
//#endregion
|
|
3153
3256
|
//#region src/server/status-code-resolver.ts
|
|
3154
3257
|
/**
|
|
@@ -3186,12 +3289,19 @@ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
|
|
|
3186
3289
|
* @param status - The HTTP status code (4xx or 5xx).
|
|
3187
3290
|
* @param segments - The matched segment chain from root (index 0) to leaf (last).
|
|
3188
3291
|
* @param format - The response format family ('component' or 'json'). Defaults to 'component'.
|
|
3189
|
-
|
|
3190
|
-
|
|
3292
|
+
* @param maxIndex - Optional upper bound on the segment index to search.
|
|
3293
|
+
* When set, the resolver only considers segments `0..maxIndex` (inclusive),
|
|
3294
|
+
* skipping any segments deeper in the tree. Used by the deny renderers to
|
|
3295
|
+
* prevent resolving a status file below a denied segment — matching
|
|
3296
|
+
* `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
|
|
3297
|
+
* this parameter, so the full chain is searched — a 500 from a leaf page
|
|
3298
|
+
* should use the leaf's own `5xx.tsx`. See TIM-1358.
|
|
3299
|
+
*/
|
|
3300
|
+
function resolveStatusFile(status, segments, format = "component", maxIndex) {
|
|
3191
3301
|
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);
|
|
3302
|
+
if (format === "json") return resolveJson(status, segments, maxIndex);
|
|
3303
|
+
if (status <= 499) return resolve4xx(status, segments, maxIndex);
|
|
3304
|
+
return resolve5xx(status, segments, maxIndex);
|
|
3195
3305
|
}
|
|
3196
3306
|
/**
|
|
3197
3307
|
* 4xx component fallback chain — two separate full passes leaf→root.
|
|
@@ -3205,13 +3315,14 @@ function resolveStatusFile(status, segments, format = "component") {
|
|
|
3205
3315
|
* Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
|
|
3206
3316
|
* Pass 2 — error.tsx (error)
|
|
3207
3317
|
*/
|
|
3208
|
-
function resolve4xx(status, segments) {
|
|
3318
|
+
function resolve4xx(status, segments, maxIndex) {
|
|
3209
3319
|
const statusStr = String(status);
|
|
3210
|
-
|
|
3320
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3321
|
+
for (let i = start; i >= 0; i--) {
|
|
3211
3322
|
const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
|
|
3212
3323
|
if (r) return r;
|
|
3213
3324
|
}
|
|
3214
|
-
for (let i =
|
|
3325
|
+
for (let i = start; i >= 0; i--) {
|
|
3215
3326
|
const errorFile = segments[i].error;
|
|
3216
3327
|
if (errorFile) return {
|
|
3217
3328
|
file: errorFile,
|
|
@@ -3229,9 +3340,10 @@ function resolve4xx(status, segments) {
|
|
|
3229
3340
|
* `error.tsx` therefore beats a root's `5xx.tsx`, which is the
|
|
3230
3341
|
* intentional inverse of the 4xx chain.
|
|
3231
3342
|
*/
|
|
3232
|
-
function resolve5xx(status, segments) {
|
|
3343
|
+
function resolve5xx(status, segments, maxIndex) {
|
|
3233
3344
|
const statusStr = String(status);
|
|
3234
|
-
|
|
3345
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3346
|
+
for (let i = start; i >= 0; i--) {
|
|
3235
3347
|
const segment = segments[i];
|
|
3236
3348
|
const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
|
|
3237
3349
|
if (r) return r;
|
|
@@ -3251,10 +3363,11 @@ function resolve5xx(status, segments) {
|
|
|
3251
3363
|
* no error.tsx — the JSON chain terminates at the category catch-all
|
|
3252
3364
|
* and the caller falls back to a bare-JSON framework default.
|
|
3253
3365
|
*/
|
|
3254
|
-
function resolveJson(status, segments) {
|
|
3366
|
+
function resolveJson(status, segments, maxIndex) {
|
|
3255
3367
|
const statusStr = String(status);
|
|
3256
3368
|
const categoryKey = status >= 500 ? "5xx" : "4xx";
|
|
3257
|
-
|
|
3369
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3370
|
+
for (let i = start; i >= 0; i--) {
|
|
3258
3371
|
const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
|
|
3259
3372
|
if (r) return r;
|
|
3260
3373
|
}
|