@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
|
@@ -26,7 +26,8 @@ import { flightInitScript, htmlEscapeJsonString } from '../flight-scripts.js';
|
|
|
26
26
|
import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.js';
|
|
27
27
|
import type { DefaultPageRenderer, LayoutEntry } from '../deny-renderer.js';
|
|
28
28
|
import type { NavContext } from '../ssr-bridge-types.js';
|
|
29
|
-
import { createDebugChannelSink
|
|
29
|
+
import { createDebugChannelSink } from './helpers.js';
|
|
30
|
+
import { isRscRequest } from '../../shared/rsc-media-type.js';
|
|
30
31
|
import { withPublishedParams } from '../publish-params.js';
|
|
31
32
|
import { getCookiesForSsr } from '../cookie-context.js';
|
|
32
33
|
import { callSsr } from './ssr-bridge.js';
|
|
@@ -72,9 +73,12 @@ interface ErrorFileResolution {
|
|
|
72
73
|
*/
|
|
73
74
|
async function resolveErrorFile(
|
|
74
75
|
status: number,
|
|
75
|
-
segments: ManifestSegmentNode[]
|
|
76
|
+
segments: ManifestSegmentNode[],
|
|
77
|
+
maxIndex?: number
|
|
76
78
|
): Promise<ErrorFileResolution | null> {
|
|
77
|
-
|
|
79
|
+
const start =
|
|
80
|
+
maxIndex !== undefined ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
81
|
+
for (let i = start; i >= 0; i--) {
|
|
78
82
|
const segment = segments[i];
|
|
79
83
|
|
|
80
84
|
if (segment.statusFiles) {
|
|
@@ -131,9 +135,14 @@ async function resolveErrorFile(
|
|
|
131
135
|
function getLayoutsForErrorFile(
|
|
132
136
|
segments: ManifestSegmentNode[],
|
|
133
137
|
layoutComponents: LayoutEntry[],
|
|
134
|
-
foundSegmentIndex: number
|
|
138
|
+
foundSegmentIndex: number,
|
|
139
|
+
firstDeniedIndex = Infinity
|
|
135
140
|
): LayoutEntry[] {
|
|
136
|
-
|
|
141
|
+
// Clamp so a denied segment's layout never wraps the error page.
|
|
142
|
+
// The file resolution is unbounded (errors may use leaf files), but the
|
|
143
|
+
// layout wrapping must still respect the denial boundary. TIM-1358.
|
|
144
|
+
const ceiling = Math.min(foundSegmentIndex, firstDeniedIndex - 1);
|
|
145
|
+
const resolvedSegments = new Set(segments.slice(0, ceiling + 1));
|
|
137
146
|
return layoutComponents.filter((lc) => resolvedSegments.has(lc.segment));
|
|
138
147
|
}
|
|
139
148
|
|
|
@@ -182,12 +191,13 @@ export async function renderErrorPage(
|
|
|
182
191
|
match: RouteMatch,
|
|
183
192
|
responseHeaders: Headers,
|
|
184
193
|
clientBootstrap: ClientBootstrapConfig,
|
|
185
|
-
globalError?: GlobalErrorFile
|
|
194
|
+
globalError?: GlobalErrorFile,
|
|
195
|
+
firstDeniedIndex = Infinity
|
|
186
196
|
): Promise<Response> {
|
|
187
197
|
// The error itself is not logged here — every producer of a `renderError`
|
|
188
198
|
// signal already logged it at the point it was caught (`rsc-stream.ts`'s
|
|
189
199
|
// `onError`, `outcomeToResponse`), so a log on this exit would double up.
|
|
190
|
-
if (
|
|
200
|
+
if (isRscRequest(req)) {
|
|
191
201
|
return rscErrorEnvelope(status, responseHeaders);
|
|
192
202
|
}
|
|
193
203
|
|
|
@@ -195,8 +205,13 @@ export async function renderErrorPage(
|
|
|
195
205
|
// shows original source positions instead of transpiled/bundled ones.
|
|
196
206
|
// See TIM-811.
|
|
197
207
|
sourceMapError(error);
|
|
198
|
-
// Walk segments from leaf to root to find the error component
|
|
199
|
-
|
|
208
|
+
// Walk segments from leaf to root to find the error component.
|
|
209
|
+
// When firstDeniedIndex is finite, bound the walk so error files below
|
|
210
|
+
// the denied segment are not selected — matching the deny renderer's
|
|
211
|
+
// behavior. For plain errors (firstDeniedIndex = Infinity), the full
|
|
212
|
+
// chain is searched. See TIM-1358.
|
|
213
|
+
const errorMaxIndex = firstDeniedIndex < Infinity ? firstDeniedIndex : undefined;
|
|
214
|
+
const resolution = await resolveErrorFile(status, segments, errorMaxIndex);
|
|
200
215
|
|
|
201
216
|
// Tier 2: No segment-level error file — try global-error.tsx
|
|
202
217
|
if (!resolution && globalError) {
|
|
@@ -221,7 +236,12 @@ export async function renderErrorPage(
|
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
const { component: errorComponent, segmentIndex, isMdx } = resolution;
|
|
224
|
-
const layoutsToWrap = getLayoutsForErrorFile(
|
|
239
|
+
const layoutsToWrap = getLayoutsForErrorFile(
|
|
240
|
+
segments,
|
|
241
|
+
layoutComponents,
|
|
242
|
+
segmentIndex,
|
|
243
|
+
firstDeniedIndex
|
|
244
|
+
);
|
|
225
245
|
|
|
226
246
|
return renderErrorPageViaRsc(
|
|
227
247
|
error,
|
|
@@ -407,7 +427,7 @@ export async function renderNoMatchPage(
|
|
|
407
427
|
const deny = new DenySignal(404);
|
|
408
428
|
const match: RouteMatch = { segments, segmentParams: {}, middlewareChain: [] };
|
|
409
429
|
|
|
410
|
-
if (
|
|
430
|
+
if (isRscRequest(req)) {
|
|
411
431
|
return renderDenyPageAsRsc(
|
|
412
432
|
deny,
|
|
413
433
|
segments,
|
|
@@ -12,9 +12,6 @@ import type { SlotSkipEntry } from '../slot-resolver.js';
|
|
|
12
12
|
import { isClientReference } from '../route-element-builder.js';
|
|
13
13
|
import { isStaticRequestDependent } from '../request-dep.js';
|
|
14
14
|
|
|
15
|
-
/** RSC content type for client navigation payload requests. */
|
|
16
|
-
export const RSC_CONTENT_TYPE = 'text/x-component';
|
|
17
|
-
|
|
18
15
|
/**
|
|
19
16
|
* Create a debug channel sink that discards all debug data.
|
|
20
17
|
*
|
|
@@ -205,6 +202,8 @@ export function buildSegmentInfo(
|
|
|
205
202
|
const skippedSet = skippedSegmentKeys ? new Set(skippedSegmentKeys) : null;
|
|
206
203
|
const result: SegmentInfo[] = [];
|
|
207
204
|
|
|
205
|
+
// Deepest segment that actually mounted, exclusive.
|
|
206
|
+
//
|
|
208
207
|
for (let i = 0; i < segments.length; i++) {
|
|
209
208
|
// A denied segment's AccessGate renders the deny page in place of the
|
|
210
209
|
// layout, so this segment and everything below it never mounted. Their
|
|
@@ -259,15 +258,6 @@ export function buildSegmentInfo(
|
|
|
259
258
|
return result;
|
|
260
259
|
}
|
|
261
260
|
|
|
262
|
-
/**
|
|
263
|
-
* Check if a request is asking for an RSC payload (client navigation)
|
|
264
|
-
* rather than full HTML. Client-side navigation sends Accept: text/x-component.
|
|
265
|
-
*/
|
|
266
|
-
export function isRscPayloadRequest(req: Request): boolean {
|
|
267
|
-
const accept = req.headers.get('Accept') ?? '';
|
|
268
|
-
return accept.includes(RSC_CONTENT_TYPE);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
261
|
// Re-export consolidated helpers from their single source of truth.
|
|
272
262
|
// Callers within rsc-entry/ import from this file; the implementations
|
|
273
263
|
// live in pipeline-helpers.ts (buildRedirectResponse) and render-utils.ts
|
|
@@ -25,11 +25,6 @@
|
|
|
25
25
|
|
|
26
26
|
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
27
27
|
import routeManifest from 'virtual:timber-route-manifest';
|
|
28
|
-
// TIM-830: Populate the search-params registry eagerly so <Link> can
|
|
29
|
-
// serialize flat `Partial<T>` values synchronously in RSC renders.
|
|
30
|
-
// The module has side effects only — no exports consumed here.
|
|
31
|
-
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
32
|
-
import 'virtual:timber-search-params-registry';
|
|
33
28
|
// @ts-expect-error — virtual module provided by timber-entries plugin
|
|
34
29
|
import config from 'virtual:timber-config';
|
|
35
30
|
// @ts-expect-error — virtual module provided by timber-build-manifest plugin
|
|
@@ -41,9 +41,11 @@ import { recordTiming } from '../server-timing.js';
|
|
|
41
41
|
import { requestContextAls } from '../als-registry.js';
|
|
42
42
|
import { parseClientStateTree } from '../state-tree-diff.js';
|
|
43
43
|
|
|
44
|
+
import { getFirstDeniedSegmentIndex } from '../deny-boundary.js';
|
|
44
45
|
import { handleApiRoute } from './api-handler.js';
|
|
45
46
|
import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
|
|
46
|
-
import { buildRedirectResponse, createDebugChannelSink
|
|
47
|
+
import { buildRedirectResponse, createDebugChannelSink } from './helpers.js';
|
|
48
|
+
import { isRscRequest } from '../../shared/rsc-media-type.js';
|
|
47
49
|
import { buildRscPayloadResponse } from './rsc-payload.js';
|
|
48
50
|
import { renderRscStream } from './rsc-stream.js';
|
|
49
51
|
import { renderSsrResponse } from './ssr-renderer.js';
|
|
@@ -97,7 +99,7 @@ export async function renderRoute(
|
|
|
97
99
|
// Only used for RSC requests — HTML requests always get a full render.
|
|
98
100
|
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
99
101
|
const clientStateTree =
|
|
100
|
-
deps.clientSegmentCache &&
|
|
102
|
+
deps.clientSegmentCache && isRscRequest(req) ? parseClientStateTree(req) : null;
|
|
101
103
|
|
|
102
104
|
// Build the React element tree — runs access checks eagerly to gate
|
|
103
105
|
// metadata resolution (TIM-1027), then loads modules and resolves
|
|
@@ -146,7 +148,6 @@ export async function renderRoute(
|
|
|
146
148
|
skippedSegments,
|
|
147
149
|
shellSettled,
|
|
148
150
|
slotSkipInfo,
|
|
149
|
-
firstDeniedIndex,
|
|
150
151
|
} = routeResult;
|
|
151
152
|
|
|
152
153
|
// Build head HTML for injection into the SSR output.
|
|
@@ -225,14 +226,15 @@ export async function renderRoute(
|
|
|
225
226
|
// Synchronous deny — deny() in a non-async component throws during
|
|
226
227
|
// renderToReadableStream creation, caught in the try/catch above.
|
|
227
228
|
if (signals.denySignal) {
|
|
228
|
-
if (
|
|
229
|
+
if (isRscRequest(req)) {
|
|
229
230
|
return renderDenyPageAsRsc(
|
|
230
231
|
signals.denySignal,
|
|
231
232
|
segments,
|
|
232
233
|
layoutComponents as LayoutEntry[],
|
|
233
234
|
req,
|
|
234
235
|
responseHeaders,
|
|
235
|
-
createDebugChannelSink
|
|
236
|
+
createDebugChannelSink,
|
|
237
|
+
getFirstDeniedSegmentIndex()
|
|
236
238
|
);
|
|
237
239
|
}
|
|
238
240
|
return renderDenyPage(
|
|
@@ -244,7 +246,9 @@ export async function renderRoute(
|
|
|
244
246
|
responseHeaders,
|
|
245
247
|
clientBootstrap,
|
|
246
248
|
createDebugChannelSink,
|
|
247
|
-
callSsr
|
|
249
|
+
callSsr,
|
|
250
|
+
undefined,
|
|
251
|
+
getFirstDeniedSegmentIndex()
|
|
248
252
|
);
|
|
249
253
|
}
|
|
250
254
|
|
|
@@ -264,14 +268,15 @@ export async function renderRoute(
|
|
|
264
268
|
match,
|
|
265
269
|
responseHeaders,
|
|
266
270
|
clientBootstrap,
|
|
267
|
-
globalError
|
|
271
|
+
globalError,
|
|
272
|
+
getFirstDeniedSegmentIndex()
|
|
268
273
|
);
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
// For RSC payload requests (client navigation), return the RSC Flight
|
|
272
277
|
// stream directly — skip SSR HTML rendering entirely.
|
|
273
278
|
// See design/19-client-navigation.md §"RSC Payload Handling"
|
|
274
|
-
if (
|
|
279
|
+
if (isRscRequest(req)) {
|
|
275
280
|
return buildRscPayloadResponse(
|
|
276
281
|
req,
|
|
277
282
|
rscStream!,
|
|
@@ -280,8 +285,7 @@ export async function renderRoute(
|
|
|
280
285
|
layoutComponents,
|
|
281
286
|
responseHeaders,
|
|
282
287
|
skippedSegments,
|
|
283
|
-
slotSkipInfo
|
|
284
|
-
firstDeniedIndex
|
|
288
|
+
slotSkipInfo
|
|
285
289
|
);
|
|
286
290
|
}
|
|
287
291
|
|
|
@@ -300,6 +304,5 @@ export async function renderRoute(
|
|
|
300
304
|
deferSuspenseFor,
|
|
301
305
|
globalError,
|
|
302
306
|
slotSkipInfo,
|
|
303
|
-
firstDeniedIndex,
|
|
304
307
|
});
|
|
305
308
|
}
|
|
@@ -40,6 +40,7 @@ import { buildRouteElement } from '../route-element-builder.js';
|
|
|
40
40
|
import type { RevalidateRenderer, RevalidationResult } from '../actions.js';
|
|
41
41
|
|
|
42
42
|
import { runMiddlewareForAction, type CoerceSegmentParamsFn } from './action-middleware-runner.js';
|
|
43
|
+
import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.js';
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Sentinel thrown when revalidation cannot proceed because middleware
|
|
@@ -214,7 +215,7 @@ export function createRevalidateRendererFactory(
|
|
|
214
215
|
revalidateUrl.pathname = validation.pathname;
|
|
215
216
|
revalidateUrl.search = validation.search;
|
|
216
217
|
const revalidateHeaders = new Headers(req.headers);
|
|
217
|
-
revalidateHeaders.set('Accept',
|
|
218
|
+
revalidateHeaders.set('Accept', RSC_CONTENT_TYPE);
|
|
218
219
|
// Snapshot the action's post-mutation cookie map (deletions
|
|
219
220
|
// included) and serialize it into a `Cookie:` header value the
|
|
220
221
|
// target middleware can read off `ctx.req.headers.get('cookie')`.
|
|
@@ -15,17 +15,22 @@ import type { LayoutComponentEntry } from '../route-element-builder.js';
|
|
|
15
15
|
import type { ManifestSegmentNode } from '../route-matcher.js';
|
|
16
16
|
import type { SlotSkipEntry } from '../slot-resolver.js';
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
buildSegmentInfo,
|
|
21
|
-
createDebugChannelSink,
|
|
22
|
-
RSC_CONTENT_TYPE,
|
|
23
|
-
} from './helpers.js';
|
|
18
|
+
import { buildRedirectResponse, buildSegmentInfo, createDebugChannelSink } from './helpers.js';
|
|
19
|
+
import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.js';
|
|
24
20
|
|
|
25
21
|
import { rscErrorEnvelope } from '../../shared/rsc-error-envelope.js';
|
|
26
22
|
|
|
27
23
|
import type { RenderSignals } from './rsc-stream.js';
|
|
28
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Ceiling on waiting for a first chunk that is already in flight, used only
|
|
27
|
+
* when an addressed deny won the race against it. Matches the 50ms ceiling
|
|
28
|
+
* documented for the race itself: long enough that the error row — which is
|
|
29
|
+
* what the render just produced — arrives, short enough that a stalled stream
|
|
30
|
+
* cannot hold the response open.
|
|
31
|
+
*/
|
|
32
|
+
const FIRST_CHUNK_CEILING_MS = 50;
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
35
|
* Build an RSC payload Response for a client-side navigation request.
|
|
31
36
|
*
|
|
@@ -43,9 +48,7 @@ export async function buildRscPayloadResponse(
|
|
|
43
48
|
layoutComponents: LayoutComponentEntry[],
|
|
44
49
|
responseHeaders: Headers,
|
|
45
50
|
skippedSegments?: string[],
|
|
46
|
-
slotSkipInfo?: SlotSkipEntry[]
|
|
47
|
-
/** Index of the first denied segment — see RouteElementResult.firstDeniedIndex. */
|
|
48
|
-
firstDeniedIndex = Infinity
|
|
51
|
+
slotSkipInfo?: SlotSkipEntry[]
|
|
49
52
|
): Promise<Response> {
|
|
50
53
|
// Read the first chunk from the RSC stream before committing headers.
|
|
51
54
|
// Race the first read against signal detection — if an async component
|
|
@@ -72,10 +75,18 @@ export async function buildRscPayloadResponse(
|
|
|
72
75
|
| { type: 'data'; chunk: ReadableStreamReadResult<Uint8Array> }
|
|
73
76
|
| { type: 'signal' };
|
|
74
77
|
|
|
78
|
+
// Retained rather than inlined into the race below. When a signal wins, the
|
|
79
|
+
// usual answer is to cancel — but an ADDRESSED deny is one whose error row
|
|
80
|
+
// the client's boundary is going to render, so that payload has to survive
|
|
81
|
+
// (TIM-1356). Taking delivery of the read already in flight is not the
|
|
82
|
+
// "bare read() after a signal won" the async rules forbid; issuing a second
|
|
83
|
+
// one would be. It still gets a ceiling below.
|
|
84
|
+
const pendingRead = reader.read();
|
|
85
|
+
|
|
75
86
|
let first: RaceResult;
|
|
76
87
|
try {
|
|
77
88
|
first = await Promise.race([
|
|
78
|
-
|
|
89
|
+
pendingRead.then((chunk) => ({ type: 'data' as const, chunk })),
|
|
79
90
|
signalDetected.then(() => ({ type: 'signal' as const })),
|
|
80
91
|
]);
|
|
81
92
|
} catch (_readError) {
|
|
@@ -121,15 +132,42 @@ export async function buildRscPayloadResponse(
|
|
|
121
132
|
reader.cancel().catch(() => {});
|
|
122
133
|
return buildRedirectResponse(trackedRedirect, req, responseHeaders);
|
|
123
134
|
}
|
|
124
|
-
|
|
135
|
+
// An addressed deny needs no fallback: the payload already carries its error
|
|
136
|
+
// row, aimed at the boundary the client has mounted at that segment, which
|
|
137
|
+
// renders it. Cancelling would throw a finished render away and re-run the
|
|
138
|
+
// whole route — the exact cost TIM-666 removed. The status still lands,
|
|
139
|
+
// because the catch site called setDenyStatus() before re-throwing (read
|
|
140
|
+
// below via getDenyStatus).
|
|
141
|
+
//
|
|
142
|
+
// The question is asked of `unplacedDeny`, not of `denySignal.ownerKey`,
|
|
143
|
+
// because one render can produce both kinds and `denySignal` holds only one
|
|
144
|
+
// of them. A deny nothing could place has no boundary that may claim it, so
|
|
145
|
+
// its row would otherwise stream to a client with nothing to render it —
|
|
146
|
+
// and it needs the fallback whether or not some other deny was addressed
|
|
147
|
+
// first. See TIM-1356.
|
|
148
|
+
if (signals.unplacedDeny) {
|
|
125
149
|
reader.cancel().catch(() => {});
|
|
150
|
+
|
|
151
|
+
// A skipped segment whose access just flipped to deny marks the signal
|
|
152
|
+
// for hard-navigate: rendering a deny page here would execute the denied
|
|
153
|
+
// segment's protected layout and place it inside cached client chrome
|
|
154
|
+
// (TIM-1074). Return rscErrorEnvelope so the client hard-navigates for
|
|
155
|
+
// a clean full render with correct deny page placement. See TIM-1363.
|
|
156
|
+
if (signals.unplacedDeny._hardNavigate) {
|
|
157
|
+
return rscErrorEnvelope(signals.unplacedDeny.status, responseHeaders);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Read the denied segment index from ALS — set by AccessGate during
|
|
161
|
+
// render. See TIM-1358, TIM-1365.
|
|
162
|
+
const { getFirstDeniedSegmentIndex: getDeniedIdx } = await import('../deny-boundary.js');
|
|
126
163
|
return renderDenyPageAsRsc(
|
|
127
|
-
signals.
|
|
164
|
+
signals.unplacedDeny,
|
|
128
165
|
segments,
|
|
129
166
|
layoutComponents as LayoutEntry[],
|
|
130
167
|
req,
|
|
131
168
|
responseHeaders,
|
|
132
|
-
createDebugChannelSink
|
|
169
|
+
createDebugChannelSink,
|
|
170
|
+
getDeniedIdx()
|
|
133
171
|
);
|
|
134
172
|
}
|
|
135
173
|
|
|
@@ -151,6 +189,41 @@ export async function buildRscPayloadResponse(
|
|
|
151
189
|
let firstRead: ReadableStreamReadResult<Uint8Array>;
|
|
152
190
|
if (first.type === 'data') {
|
|
153
191
|
firstRead = first.chunk;
|
|
192
|
+
} else if (signals.denySignal?.ownerKey) {
|
|
193
|
+
// An addressed deny won the race before its own error row surfaced.
|
|
194
|
+
// Cancelling here would hand the client a 4xx payload with nothing in it
|
|
195
|
+
// — no row for the owning boundary to render, so the navigation shows
|
|
196
|
+
// nothing at all. Take delivery of the read already in flight instead,
|
|
197
|
+
// under a ceiling so a stalled stream still cannot hang the response.
|
|
198
|
+
const TIMED_OUT = Symbol('first-chunk-timeout');
|
|
199
|
+
const raced = await Promise.race([
|
|
200
|
+
pendingRead,
|
|
201
|
+
new Promise<typeof TIMED_OUT>((resolve) =>
|
|
202
|
+
setTimeout(() => resolve(TIMED_OUT), FIRST_CHUNK_CEILING_MS)
|
|
203
|
+
),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Missing the ceiling must not be reported as a finished stream. Under
|
|
207
|
+
// event-loop contention or slow serialization the row is late rather than
|
|
208
|
+
// absent, and fabricating `done: true` here would produce the very empty
|
|
209
|
+
// 4xx this branch exists to prevent.
|
|
210
|
+
//
|
|
211
|
+
// Nor can it fall to `renderDenyPageAsRsc`. That renderer re-resolves from
|
|
212
|
+
// the FULL leaf chain and wraps whatever it finds, ignoring `ownerKey` —
|
|
213
|
+
// so an `/admin` denial addressed to a root `4xx.tsx` would come back as
|
|
214
|
+
// `/admin/secret/403.tsx` with the denied layouts executed, which is the
|
|
215
|
+
// TIM-1074 leak TIM-1358 still owns. A deny this path already placed
|
|
216
|
+
// correctly must not be handed to it.
|
|
217
|
+
//
|
|
218
|
+
// So: hard-navigate. The document request renders the same deny through
|
|
219
|
+
// the boundary at its owner, with the placement intact. A full page load
|
|
220
|
+
// is a real cost, on a path that needs a status page and a stalled stream
|
|
221
|
+
// to reach at all.
|
|
222
|
+
if (raced === TIMED_OUT) {
|
|
223
|
+
reader.cancel().catch(() => {});
|
|
224
|
+
return rscErrorEnvelope(signals.denySignal.status, responseHeaders);
|
|
225
|
+
}
|
|
226
|
+
firstRead = raced;
|
|
154
227
|
} else {
|
|
155
228
|
await reader.cancel();
|
|
156
229
|
firstRead = { done: true, value: undefined };
|
|
@@ -189,12 +262,29 @@ export async function buildRscPayloadResponse(
|
|
|
189
262
|
// for state tree diffing on subsequent navigations. Request-dependency
|
|
190
263
|
// is determined by static module-graph analysis (not runtime taint).
|
|
191
264
|
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
265
|
+
// Read the deny state before describing the tree — what mounted depends on it.
|
|
266
|
+
// Both values are recorded in ALS during render by AccessGate's catch path.
|
|
267
|
+
const { getDenyStatus, getFirstDeniedSegmentIndex } = await import('../deny-boundary.js');
|
|
268
|
+
const denyStatus = getDenyStatus();
|
|
269
|
+
const resolvedDenyIndex = getFirstDeniedSegmentIndex();
|
|
270
|
+
|
|
271
|
+
// Emitted in full even when a deny rendered, because this header answers two
|
|
272
|
+
// questions and they diverge here. The client picks its merge point out of
|
|
273
|
+
// this list (`buildSegmentUpdates`), and a partial payload with no merge
|
|
274
|
+
// point is never inserted at all — the URL commits over the stale previous
|
|
275
|
+
// UI. So it has to keep describing the chain the payload was BUILT against.
|
|
276
|
+
//
|
|
277
|
+
// What it must not also do on a deny is seed the segment cache, since the
|
|
278
|
+
// deny page replaced a subtree and part of that chain never mounted. That
|
|
279
|
+
// half is handled at the commit site from the response status, where the
|
|
280
|
+
// merge point has already been chosen. See `client/router-pipeline.ts` and
|
|
281
|
+
// TIM-1356.
|
|
192
282
|
const segmentInfo = buildSegmentInfo(
|
|
193
283
|
segments,
|
|
194
284
|
layoutComponents,
|
|
195
285
|
slotSkipInfo,
|
|
196
286
|
skippedSegments,
|
|
197
|
-
|
|
287
|
+
resolvedDenyIndex
|
|
198
288
|
);
|
|
199
289
|
responseHeaders.set('X-Timber-Segments', JSON.stringify(segmentInfo));
|
|
200
290
|
|
|
@@ -204,12 +294,6 @@ export async function buildRscPayloadResponse(
|
|
|
204
294
|
responseHeaders.set('X-Timber-Skipped-Segments', JSON.stringify(skippedSegments));
|
|
205
295
|
}
|
|
206
296
|
|
|
207
|
-
// Check if a DenySignal was caught in-tree by AccessGate or
|
|
208
|
-
// PageDenyBoundary. The deny page is in the Flight stream as a normal
|
|
209
|
-
// element — use the deny status for the HTTP response. See TIM-666.
|
|
210
|
-
const { getDenyStatus } = await import('../deny-boundary.js');
|
|
211
|
-
const denyStatus = getDenyStatus();
|
|
212
|
-
|
|
213
297
|
return new Response(patchedStream, {
|
|
214
298
|
status: denyStatus ?? 200,
|
|
215
299
|
headers: responseHeaders,
|
|
@@ -40,6 +40,18 @@ import { isDevMode } from '../debug.js';
|
|
|
40
40
|
*/
|
|
41
41
|
export interface RenderSignals {
|
|
42
42
|
denySignal: DenySignal | null;
|
|
43
|
+
/**
|
|
44
|
+
* The first deny that no in-tree catch site could place — no `ownerKey`, so
|
|
45
|
+
* no boundary may claim its error row and only the re-render fallback can
|
|
46
|
+
* produce a page for it.
|
|
47
|
+
*
|
|
48
|
+
* Tracked apart from `denySignal` because one render can produce both. A
|
|
49
|
+
* deep page can hoist a 404 to a boundary that will render it while an
|
|
50
|
+
* outer layout separately throws a 403 nothing matches; whichever lands in
|
|
51
|
+
* `denySignal` first, the unplaced one still needs the fallback, and
|
|
52
|
+
* deciding from `denySignal.ownerKey` alone would strand it. See TIM-1356.
|
|
53
|
+
*/
|
|
54
|
+
unplacedDeny: DenySignal | null;
|
|
43
55
|
redirectSignal: RedirectSignal | null;
|
|
44
56
|
renderError: { error: unknown; status: number } | null;
|
|
45
57
|
/**
|
|
@@ -88,6 +100,7 @@ export interface RscStreamResult {
|
|
|
88
100
|
export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStreamResult {
|
|
89
101
|
const signals: RenderSignals = {
|
|
90
102
|
denySignal: null,
|
|
103
|
+
unplacedDeny: null,
|
|
91
104
|
redirectSignal: null,
|
|
92
105
|
renderError: null,
|
|
93
106
|
lastUnhandledError: null,
|
|
@@ -110,9 +123,18 @@ export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStrea
|
|
|
110
123
|
if (isAbortError(error) || req.signal?.aborted) return;
|
|
111
124
|
if (error instanceof DenySignal) {
|
|
112
125
|
signals.denySignal = error;
|
|
126
|
+
if (!error.ownerKey) signals.unplacedDeny ??= error;
|
|
113
127
|
signals.onSignal?.();
|
|
114
|
-
// Return structured digest for client-side error boundaries
|
|
115
|
-
|
|
128
|
+
// Return structured digest for client-side error boundaries.
|
|
129
|
+
// `ownerKey` is set when an in-tree catch site hoisted the deny
|
|
130
|
+
// to the segment owning the matched file — it addresses the
|
|
131
|
+
// boundary that may render it. See TIM-1356.
|
|
132
|
+
return JSON.stringify({
|
|
133
|
+
type: 'deny',
|
|
134
|
+
status: error.status,
|
|
135
|
+
data: error.data,
|
|
136
|
+
ownerKey: error.ownerKey,
|
|
137
|
+
});
|
|
116
138
|
}
|
|
117
139
|
if (error instanceof RedirectSignal) {
|
|
118
140
|
signals.redirectSignal = error;
|
|
@@ -218,6 +240,7 @@ export function renderRscStream(payload: RscPayloadRoot, req: Request): RscStrea
|
|
|
218
240
|
} catch (error) {
|
|
219
241
|
if (error instanceof DenySignal) {
|
|
220
242
|
signals.denySignal = error;
|
|
243
|
+
if (!error.ownerKey) signals.unplacedDeny ??= error;
|
|
221
244
|
} else if (error instanceof RedirectSignal) {
|
|
222
245
|
signals.redirectSignal = error;
|
|
223
246
|
} else {
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
isAbortError,
|
|
34
34
|
} from './helpers.js';
|
|
35
35
|
import { getCookiesForSsr } from '../cookie-context.js';
|
|
36
|
-
import { getDenyStatus } from '../deny-boundary.js';
|
|
36
|
+
import { getDenyStatus, getFirstDeniedSegmentIndex } from '../deny-boundary.js';
|
|
37
37
|
import { renderErrorPage, type GlobalErrorFile } from './error-renderer.js';
|
|
38
38
|
import { callSsr } from './ssr-bridge.js';
|
|
39
39
|
import type { RenderSignals } from './rsc-stream.js';
|
|
@@ -104,8 +104,6 @@ interface SsrRenderOptions {
|
|
|
104
104
|
/** Tier 2 global-error.tsx file, if present in app/. */
|
|
105
105
|
globalError?: GlobalErrorFile;
|
|
106
106
|
slotSkipInfo?: SlotSkipEntry[];
|
|
107
|
-
/** Index of the first denied segment — see RouteElementResult.firstDeniedIndex. */
|
|
108
|
-
firstDeniedIndex?: number;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
/**
|
|
@@ -168,13 +166,11 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
168
166
|
const errorAbort = new AbortController();
|
|
169
167
|
signals.onUnhandledError = () => errorAbort.abort();
|
|
170
168
|
|
|
171
|
-
//
|
|
172
|
-
// The
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
? ''
|
|
177
|
-
: `<script>self.__timber_segments=${htmlEscapeJsonString(JSON.stringify(buildSegmentInfo(segments, layoutComponents, opts.slotSkipInfo, undefined, opts.firstDeniedIndex)))}</script>`;
|
|
169
|
+
// Segment metadata for initial hydration: deferred to shell-flush time.
|
|
170
|
+
// The SSR entry invokes this thunk AFTER renderSsrNodeStream /
|
|
171
|
+
// renderSsrStream returns (shell ready ⇒ AccessGates settled), so
|
|
172
|
+
// getFirstDeniedSegmentIndex() returns the authoritative ALS value
|
|
173
|
+
// rather than the pre-render Infinity. See TIM-1365, TIM-1359.
|
|
178
174
|
|
|
179
175
|
// Params reach the browser inside the payload — a field on the root row of
|
|
180
176
|
// the same Flight stream that is inlined for hydration. SSR itself renders
|
|
@@ -193,10 +189,13 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
193
189
|
headHtml:
|
|
194
190
|
headHtml +
|
|
195
191
|
clientBootstrap.preloadLinks +
|
|
196
|
-
segmentScript +
|
|
197
192
|
// Initialize __timber_f in <head> so it exists before any streaming
|
|
198
193
|
// chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
|
|
199
194
|
(clientJsDisabled ? '' : flightInitScript()),
|
|
195
|
+
getHeadSegmentScript: clientJsDisabled
|
|
196
|
+
? undefined
|
|
197
|
+
: () =>
|
|
198
|
+
`<script>self.__timber_segments=${htmlEscapeJsonString(JSON.stringify(buildSegmentInfo(segments, layoutComponents, opts.slotSkipInfo, undefined, getFirstDeniedSegmentIndex())))}</script>`,
|
|
200
199
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
201
200
|
// Skip RSC inline stream when client JS is disabled — no client to hydrate.
|
|
202
201
|
rscStream: clientJsDisabled ? undefined : inlineStream,
|
|
@@ -215,6 +214,12 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
215
214
|
// The boundary renders the deny page as its fallback AND sets the correct
|
|
216
215
|
// HTTP status code on navContext — no redundant renderDenyPage() needed.
|
|
217
216
|
// See TIM-664, LOCAL-298.
|
|
217
|
+
// Read the denied segment index from ALS — set by AccessGate during render
|
|
218
|
+
// (setFirstDeniedSegmentIndex). See TIM-1358, TIM-1365.
|
|
219
|
+
function resolvedDeniedIndex(): number {
|
|
220
|
+
return getFirstDeniedSegmentIndex();
|
|
221
|
+
}
|
|
222
|
+
|
|
218
223
|
function checkCapturedSignals(skipHandledDeny = false): Response | Promise<Response> | null {
|
|
219
224
|
if (signals.redirectSignal) {
|
|
220
225
|
return buildRedirectResponse(signals.redirectSignal, req, responseHeaders);
|
|
@@ -233,7 +238,9 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
233
238
|
responseHeaders,
|
|
234
239
|
clientBootstrap,
|
|
235
240
|
createDebugChannelSink,
|
|
236
|
-
callSsr
|
|
241
|
+
callSsr,
|
|
242
|
+
undefined,
|
|
243
|
+
resolvedDeniedIndex()
|
|
237
244
|
);
|
|
238
245
|
}
|
|
239
246
|
if (signals.renderError) {
|
|
@@ -246,7 +253,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
246
253
|
match,
|
|
247
254
|
responseHeaders,
|
|
248
255
|
clientBootstrap,
|
|
249
|
-
globalError
|
|
256
|
+
globalError,
|
|
257
|
+
resolvedDeniedIndex()
|
|
250
258
|
);
|
|
251
259
|
}
|
|
252
260
|
return null;
|
|
@@ -421,7 +429,9 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
421
429
|
responseHeaders,
|
|
422
430
|
clientBootstrap,
|
|
423
431
|
createDebugChannelSink,
|
|
424
|
-
callSsr
|
|
432
|
+
callSsr,
|
|
433
|
+
undefined,
|
|
434
|
+
resolvedDeniedIndex()
|
|
425
435
|
);
|
|
426
436
|
}
|
|
427
437
|
if (signals.renderError) {
|
|
@@ -436,7 +446,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
436
446
|
match,
|
|
437
447
|
responseHeaders,
|
|
438
448
|
clientBootstrap,
|
|
439
|
-
globalError
|
|
449
|
+
globalError,
|
|
450
|
+
resolvedDeniedIndex()
|
|
440
451
|
);
|
|
441
452
|
}
|
|
442
453
|
// No captured signal — unhandled error in the RSC stream.
|
|
@@ -454,7 +465,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
454
465
|
match,
|
|
455
466
|
responseHeaders,
|
|
456
467
|
clientBootstrap,
|
|
457
|
-
globalError
|
|
468
|
+
globalError,
|
|
469
|
+
resolvedDeniedIndex()
|
|
458
470
|
);
|
|
459
471
|
}
|
|
460
472
|
|
|
@@ -198,7 +198,11 @@ export async function resolveSlotElement(
|
|
|
198
198
|
element = await wrapWithAccessGate(slotNode, element, h, accessVerdicts, scope);
|
|
199
199
|
|
|
200
200
|
// Wrap with slot root's error boundaries (outermost)
|
|
201
|
-
element = await wrapSegmentWithErrorBoundaries(slotNode, element,
|
|
201
|
+
element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
|
|
202
|
+
loadModule,
|
|
203
|
+
createElement: h,
|
|
204
|
+
errorBoundaryComponent: TimberErrorBoundary,
|
|
205
|
+
});
|
|
202
206
|
|
|
203
207
|
// Catch-all error boundary: ensures slot errors NEVER propagate to the
|
|
204
208
|
// parent layout. Without this, a slot without error.tsx that throws
|
|
@@ -278,7 +282,11 @@ async function wrapWithIntermediateSegments(
|
|
|
278
282
|
): Promise<React.ReactElement> {
|
|
279
283
|
for (let i = chain.length - 1; i > 0; i--) {
|
|
280
284
|
const seg = chain[i];
|
|
281
|
-
element = await wrapSegmentWithErrorBoundaries(seg, element,
|
|
285
|
+
element = await wrapSegmentWithErrorBoundaries(seg, element, {
|
|
286
|
+
loadModule,
|
|
287
|
+
createElement: h,
|
|
288
|
+
errorBoundaryComponent: TimberErrorBoundary,
|
|
289
|
+
});
|
|
282
290
|
element = await wrapWithLayout(seg, element, h, scope);
|
|
283
291
|
element = await wrapWithAccessGate(seg, element, h, accessVerdicts, scope);
|
|
284
292
|
}
|
|
@@ -70,6 +70,18 @@ export interface NavContext {
|
|
|
70
70
|
/** Request cookies as name→value pairs. Used by useCookie() during SSR
|
|
71
71
|
* to return correct cookie values before hydration. */
|
|
72
72
|
cookies?: Map<string, string>;
|
|
73
|
+
/**
|
|
74
|
+
* Lazy thunk that produces the `<script>self.__timber_segments=…</script>`
|
|
75
|
+
* tag. Invoked by the SSR entry AFTER `renderSsrNodeStream` /
|
|
76
|
+
* `renderSsrStream` returns (shell ready ⇒ AccessGates settled), so the
|
|
77
|
+
* `getFirstDeniedSegmentIndex()` ALS value is authoritative. The result
|
|
78
|
+
* is appended to headHtml before the head injector runs.
|
|
79
|
+
*
|
|
80
|
+
* Undefined when client JS is disabled (no client to consume it).
|
|
81
|
+
* See TIM-1365, TIM-1359.
|
|
82
|
+
*/
|
|
83
|
+
getHeadSegmentScript?: () => string;
|
|
84
|
+
|
|
73
85
|
/**
|
|
74
86
|
* Mutable flag: set by TimberErrorBoundary during SSR when it catches
|
|
75
87
|
* a DenySignal (via digest). This tells the RSC entry that the denial
|