@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199
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-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `route-element-builder.ts` — tree inspection and rewriting
|
|
3
|
+
* that need no request state. Split out so the builder stays within the
|
|
4
|
+
* 500-line limit as its wrapping steps grew (TIM-1450).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createElement } from 'react';
|
|
8
|
+
|
|
9
|
+
import { AccessGate } from './access-gate.tsx';
|
|
10
|
+
import { MetadataCollector } from './metadata-collector.ts';
|
|
11
|
+
import { SegmentOutlet } from '../client/segment-outlet.tsx';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Replace the outermost SegmentOutlet in a partial payload with its
|
|
15
|
+
* SegmentProvider child. Walks through AccessGate wrappers (which sit
|
|
16
|
+
* outside SegmentOutlet) to find the outlet.
|
|
17
|
+
*/
|
|
18
|
+
export function replaceOutermostSegmentOutlet(
|
|
19
|
+
element: React.ReactElement,
|
|
20
|
+
replacement: React.ReactElement
|
|
21
|
+
): React.ReactElement {
|
|
22
|
+
if (element.type === SegmentOutlet) {
|
|
23
|
+
return replacement;
|
|
24
|
+
}
|
|
25
|
+
// AccessGate and MetadataCollector wrap outside SegmentOutlet — recurse through them.
|
|
26
|
+
const props = element.props as Record<string, unknown>;
|
|
27
|
+
if ((element.type === AccessGate || element.type === MetadataCollector) && props.children) {
|
|
28
|
+
const inner = replaceOutermostSegmentOutlet(props.children as React.ReactElement, replacement);
|
|
29
|
+
/* eslint-disable react/no-children-prop -- createElement API */
|
|
30
|
+
return createElement(element.type as React.FunctionComponent<Record<string, unknown>>, {
|
|
31
|
+
...props,
|
|
32
|
+
children: inner,
|
|
33
|
+
});
|
|
34
|
+
/* eslint-enable react/no-children-prop */
|
|
35
|
+
}
|
|
36
|
+
return element;
|
|
37
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { RouteContext } from './types.ts';
|
|
12
12
|
import { logRouteError } from './logger.ts';
|
|
13
|
-
import {
|
|
13
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
14
14
|
import { stripHeadBody } from './head-response.ts';
|
|
15
15
|
|
|
16
16
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
@@ -127,7 +127,7 @@ async function runHandler(handler: RouteHandler, ctx: RouteContext): Promise<Res
|
|
|
127
127
|
// Control-flow signals — propagate to the API route dispatcher so deny()
|
|
128
128
|
// gets the JSON status-file chain and redirect() gets a proper 3xx with
|
|
129
129
|
// Location. Not errors; don't log them.
|
|
130
|
-
if (error
|
|
130
|
+
if (isDenySignal(error) || isRedirectSignal(error)) {
|
|
131
131
|
throw error;
|
|
132
132
|
}
|
|
133
133
|
logRouteError({ method: ctx.req.method, path: new URL(ctx.req.url).pathname, error });
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route module loading — loads layout, page, and access.ts modules from
|
|
3
|
+
* a matched route's segment chain in a single pass.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `route-element-builder.ts` to keep that file focused on
|
|
6
|
+
* element-tree construction.
|
|
7
|
+
*
|
|
8
|
+
* See design/02-rendering-pipeline.md, design/07-routing.md.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ManifestSegmentNode } from './route-matcher.ts';
|
|
12
|
+
import type { Metadata } from './types.ts';
|
|
13
|
+
import { loadModule } from './safe-load.ts';
|
|
14
|
+
import { NoPageComponentError, type LayoutComponentEntry } from './route-element-builder.ts';
|
|
15
|
+
|
|
16
|
+
/** Metadata export from a layout or page module, with its segment context. */
|
|
17
|
+
export interface MetadataSource {
|
|
18
|
+
metadataExport: Metadata | (() => Promise<Metadata>);
|
|
19
|
+
segmentName: string;
|
|
20
|
+
isPage: boolean;
|
|
21
|
+
segmentIndex: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Access function loaded from a segment's access.ts. */
|
|
25
|
+
export type AccessFn = (() => unknown) | undefined;
|
|
26
|
+
|
|
27
|
+
/** Result of the module-loading phase. */
|
|
28
|
+
export interface LoadedRouteModules {
|
|
29
|
+
metadataSources: MetadataSource[];
|
|
30
|
+
layoutComponents: LayoutComponentEntry[];
|
|
31
|
+
PageComponent: (...args: unknown[]) => unknown;
|
|
32
|
+
deferSuspenseFor: number;
|
|
33
|
+
accessFns: Array<AccessFn>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reject the legacy `generateMetadata` export with a helpful migration message.
|
|
38
|
+
* Throws if the module exports `generateMetadata` instead of `metadata`.
|
|
39
|
+
*/
|
|
40
|
+
function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: string): void {
|
|
41
|
+
if ('generateMetadata' in mod) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${filePath}: "generateMetadata" is not a valid export. ` +
|
|
44
|
+
`Export an async function named "metadata" instead.\n\n` +
|
|
45
|
+
` // Before\n` +
|
|
46
|
+
` export async function generateMetadata({ params }) { ... }\n\n` +
|
|
47
|
+
` // After\n` +
|
|
48
|
+
` export async function metadata() { ... }`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract `deferSuspenseFor` from a module and return the maximum
|
|
55
|
+
* of the current value and the module's value.
|
|
56
|
+
*/
|
|
57
|
+
function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number): number {
|
|
58
|
+
if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > current) {
|
|
59
|
+
return mod.deferSuspenseFor;
|
|
60
|
+
}
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collect metadata and deferSuspenseFor from a loaded module.
|
|
66
|
+
* Shared between layout and page loading paths.
|
|
67
|
+
*/
|
|
68
|
+
function collectModuleExports(
|
|
69
|
+
mod: Record<string, unknown>,
|
|
70
|
+
segment: ManifestSegmentNode,
|
|
71
|
+
segmentIndex: number,
|
|
72
|
+
isPage: boolean,
|
|
73
|
+
metadataSources: MetadataSource[],
|
|
74
|
+
currentDefer: number
|
|
75
|
+
): number {
|
|
76
|
+
const filePath = (isPage ? segment.page?.filePath : segment.layout?.filePath) ?? segment.urlPath;
|
|
77
|
+
rejectLegacyGenerateMetadata(mod, filePath);
|
|
78
|
+
if (mod.metadata) {
|
|
79
|
+
metadataSources.push({
|
|
80
|
+
metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
|
|
81
|
+
segmentName: segment.segmentName ?? segment.urlPath,
|
|
82
|
+
isPage,
|
|
83
|
+
segmentIndex,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return extractDeferSuspenseFor(mod, currentDefer);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load all route modules (layouts, page, access functions) in a single pass.
|
|
91
|
+
*
|
|
92
|
+
* Access functions are loaded eagerly here so the wrapping loop does not need
|
|
93
|
+
* to await `loadModule(segment.access)` at each iteration.
|
|
94
|
+
*/
|
|
95
|
+
export async function loadRouteModules(
|
|
96
|
+
segments: ManifestSegmentNode[],
|
|
97
|
+
requestUrl: string
|
|
98
|
+
): Promise<LoadedRouteModules> {
|
|
99
|
+
const metadataSources: MetadataSource[] = [];
|
|
100
|
+
const layoutComponents: LayoutComponentEntry[] = [];
|
|
101
|
+
const accessFns: Array<AccessFn> = [];
|
|
102
|
+
let PageComponent: ((...args: unknown[]) => unknown) | null = null;
|
|
103
|
+
let deferSuspenseFor = 0;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < segments.length; i++) {
|
|
106
|
+
const segment = segments[i];
|
|
107
|
+
const isLeaf = i === segments.length - 1;
|
|
108
|
+
|
|
109
|
+
if (segment.layout) {
|
|
110
|
+
const mod = await loadModule(segment.layout);
|
|
111
|
+
if (mod.default) {
|
|
112
|
+
layoutComponents.push({
|
|
113
|
+
component: mod.default as (...args: unknown[]) => unknown,
|
|
114
|
+
segment,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
deferSuspenseFor = collectModuleExports(
|
|
118
|
+
mod,
|
|
119
|
+
segment,
|
|
120
|
+
i,
|
|
121
|
+
false,
|
|
122
|
+
metadataSources,
|
|
123
|
+
deferSuspenseFor
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isLeaf && segment.page) {
|
|
128
|
+
const mod = await loadModule(segment.page);
|
|
129
|
+
if (mod.default) {
|
|
130
|
+
PageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
131
|
+
}
|
|
132
|
+
deferSuspenseFor = collectModuleExports(
|
|
133
|
+
mod,
|
|
134
|
+
segment,
|
|
135
|
+
i,
|
|
136
|
+
true,
|
|
137
|
+
metadataSources,
|
|
138
|
+
deferSuspenseFor
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (segment.access) {
|
|
143
|
+
const accessMod = await loadModule(segment.access);
|
|
144
|
+
accessFns[i] = accessMod.default as AccessFn;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!PageComponent) {
|
|
149
|
+
const segmentInfo = segments
|
|
150
|
+
.map(
|
|
151
|
+
(s, i) =>
|
|
152
|
+
` [${i}] ${s.segmentName} (page: ${s.page ? 'yes' : 'no'}, layout: ${s.layout ? 'yes' : 'no'}, children: ${s.children?.length ?? 0})${i === segments.length - 1 ? ' ← leaf' : ''}`
|
|
153
|
+
)
|
|
154
|
+
.join('\n');
|
|
155
|
+
throw new NoPageComponentError(
|
|
156
|
+
`No page component found for route: ${new URL(requestUrl).pathname}\nMatched segments:\n${segmentInfo}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { metadataSources, layoutComponents, PageComponent, deferSuspenseFor, accessFns };
|
|
161
|
+
}
|
|
@@ -27,38 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { rscCacheKey, isRscCacheKeyShape, RSC_KEY_PARAM } from '../shared/rsc-cache-key.ts';
|
|
29
29
|
import { isRscContentType } from '../shared/rsc-media-type.ts';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Legacy shared-cache control fields that predate RFC 9213 and so do not
|
|
33
|
-
* follow the `<target>-Cache-Control` naming pattern — the one part of this
|
|
34
|
-
* rule that cannot be matched by shape. `X-Accel-Expires` is nginx
|
|
35
|
-
* `proxy_cache`, which gives it precedence over `Cache-Control`.
|
|
36
|
-
*
|
|
37
|
-
* A closed historical set, unlike the vendor `*-Cache-Control` names, which
|
|
38
|
-
* keep being minted and are handled by pattern below.
|
|
39
|
-
*/
|
|
40
|
-
const LEGACY_CDN_CACHE_HEADERS = ['Surrogate-Control', 'Edge-Control', 'X-Accel-Expires'];
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Whether a response header overrides `Cache-Control` at a shared cache.
|
|
44
|
-
*
|
|
45
|
-
* Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
|
|
46
|
-
* as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
|
|
47
|
-
* `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
|
|
48
|
-
* next. A vendor list here is a denylist with an unbounded tail — it was
|
|
49
|
-
* extended twice during review, each time by one name, which is the signal
|
|
50
|
-
* that enumeration is the wrong shape. The suffix rule is total.
|
|
51
|
-
*
|
|
52
|
-
* `Cache-Control` itself does not match (no `-` prefix) and is replaced
|
|
53
|
-
* separately.
|
|
54
|
-
*/
|
|
55
|
-
function isSharedCacheOverride(name: string): boolean {
|
|
56
|
-
const lowered = name.toLowerCase();
|
|
57
|
-
return (
|
|
58
|
-
lowered.endsWith('-cache-control') ||
|
|
59
|
-
LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered)
|
|
60
|
-
);
|
|
61
|
-
}
|
|
30
|
+
import { preventResponseCaching } from './response-cache-policy.ts';
|
|
62
31
|
|
|
63
32
|
/**
|
|
64
33
|
* Whether the request's `_rsc` parameter matches the key derived from its
|
|
@@ -149,14 +118,5 @@ export function hardenRscPayloadResponse(response: Response): void {
|
|
|
149
118
|
*/
|
|
150
119
|
export async function enforceRscCacheKey(req: Request, response: Response): Promise<void> {
|
|
151
120
|
if (await hasValidRscCacheKey(req)) return;
|
|
152
|
-
|
|
153
|
-
// key iterator can skip entries.
|
|
154
|
-
const overrides: string[] = [];
|
|
155
|
-
for (const name of response.headers.keys()) {
|
|
156
|
-
if (isSharedCacheOverride(name)) overrides.push(name);
|
|
157
|
-
}
|
|
158
|
-
for (const name of overrides) {
|
|
159
|
-
response.headers.delete(name);
|
|
160
|
-
}
|
|
161
|
-
response.headers.set('Cache-Control', 'private, no-store');
|
|
121
|
+
preventResponseCaching(response.headers);
|
|
162
122
|
}
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
setSegmentParams,
|
|
27
27
|
} from '../request-context.ts';
|
|
28
28
|
import { runMiddlewareChain } from '../middleware-runner.ts';
|
|
29
|
-
import { DenySignal, RedirectSignal } from '../primitives.ts';
|
|
30
|
-
import { ParamCoercionError } from '../
|
|
29
|
+
import { isDenySignal, isRedirectSignal, DenySignal, RedirectSignal } from '../primitives.ts';
|
|
30
|
+
import { ParamCoercionError } from '../param-coercion.ts';
|
|
31
31
|
import type { RouteMatch } from '../pipeline.ts';
|
|
32
32
|
import type { MiddlewareContext } from '../types.ts';
|
|
33
33
|
import { requestCspNonce } from '../csp-nonce.ts';
|
|
@@ -128,14 +128,14 @@ export async function runMiddlewareForAction(
|
|
|
128
128
|
try {
|
|
129
129
|
middlewareResponse = await runMiddlewareChain(match.middlewareChain, ctx);
|
|
130
130
|
} catch (err) {
|
|
131
|
-
if (err
|
|
131
|
+
if (isRedirectSignal(err)) {
|
|
132
132
|
return {
|
|
133
133
|
kind: 'redirect',
|
|
134
134
|
signal: err,
|
|
135
135
|
setCookieHeaders: getSetCookieHeaders(),
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
-
if (err
|
|
138
|
+
if (isDenySignal(err)) {
|
|
139
139
|
return {
|
|
140
140
|
kind: 'deny',
|
|
141
141
|
signal: err,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { withSpan, setSpanAttribute } from '../tracing.ts';
|
|
10
10
|
import type { ManifestSegmentNode } from '../route-matcher.ts';
|
|
11
11
|
import { loadModule } from '../safe-load.ts';
|
|
12
|
-
import { DenySignal, RedirectSignal } from '../primitives.ts';
|
|
12
|
+
import { isDenySignal, isRedirectSignal, DenySignal, RedirectSignal } from '../primitives.ts';
|
|
13
13
|
import { handleRouteRequest } from '../route-handler.ts';
|
|
14
14
|
import { stripHeadBody } from '../head-response.ts';
|
|
15
15
|
import { isDebug } from '../debug.ts';
|
|
@@ -49,13 +49,13 @@ export async function handleApiRoute(
|
|
|
49
49
|
await accessFn();
|
|
50
50
|
await setSpanAttribute('timber.result', 'pass');
|
|
51
51
|
} catch (error) {
|
|
52
|
-
if (error
|
|
52
|
+
if (isDenySignal(error)) {
|
|
53
53
|
await setSpanAttribute('timber.result', 'deny');
|
|
54
54
|
await setSpanAttribute('timber.deny_status', error.status);
|
|
55
55
|
if (error.sourceFile) {
|
|
56
56
|
await setSpanAttribute('timber.deny_file', error.sourceFile);
|
|
57
57
|
}
|
|
58
|
-
} else if (error
|
|
58
|
+
} else if (isRedirectSignal(error)) {
|
|
59
59
|
await setSpanAttribute('timber.result', 'redirect');
|
|
60
60
|
}
|
|
61
61
|
throw error;
|
|
@@ -104,10 +104,10 @@ async function translateApiSignal(
|
|
|
104
104
|
segments: ManifestSegmentNode[],
|
|
105
105
|
responseHeaders: Headers
|
|
106
106
|
): Promise<Response | null> {
|
|
107
|
-
if (error
|
|
107
|
+
if (isDenySignal(error)) {
|
|
108
108
|
return stripHeadBody(await renderApiDeny(error, segments, responseHeaders), req.method);
|
|
109
109
|
}
|
|
110
|
-
if (error
|
|
110
|
+
if (isRedirectSignal(error)) {
|
|
111
111
|
responseHeaders.set('Location', error.location);
|
|
112
112
|
return new Response(null, { status: error.status, headers: responseHeaders });
|
|
113
113
|
}
|
|
@@ -20,7 +20,7 @@ import { logRenderError } from '../logger.ts';
|
|
|
20
20
|
import { appVisibleSearch } from '../../shared/rsc-cache-key.ts';
|
|
21
21
|
import { getSlotParamsForClient } from '../request-context.ts';
|
|
22
22
|
import type { ManifestSegmentNode } from '../route-matcher.ts';
|
|
23
|
-
import { DenySignal, RenderError } from '../primitives.ts';
|
|
23
|
+
import { isRenderError, DenySignal, RenderError } from '../primitives.ts';
|
|
24
24
|
import type { ClientBootstrapConfig } from '../html-injectors.ts';
|
|
25
25
|
import { flightInitScript, htmlEscapeJsonString } from '../flight-scripts.ts';
|
|
26
26
|
import { getCspNonce } from '../csp-nonce.ts';
|
|
@@ -40,7 +40,7 @@ import { loadModule } from '../safe-load.ts';
|
|
|
40
40
|
import { sourceMapError } from '../dev-source-map.ts';
|
|
41
41
|
import { isMdxFilePath as isMdxFile } from '../utils/mdx-file.ts';
|
|
42
42
|
import { defaultStatusPageResponse } from '../default-status-page.ts';
|
|
43
|
-
import { rscErrorEnvelope } from '
|
|
43
|
+
import { rscErrorEnvelope } from '../rsc-error-envelope.ts';
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* A manifest file reference with lazy import and path.
|
|
@@ -289,8 +289,7 @@ async function renderErrorPageViaRsc(
|
|
|
289
289
|
// SerializableError (plain object) and reconstitutes a real Error
|
|
290
290
|
// before passing to the user's error component.
|
|
291
291
|
const serializedError = toSerializableError(error);
|
|
292
|
-
const digest =
|
|
293
|
-
error instanceof RenderError ? { code: error.code, data: error.digest.data } : null;
|
|
292
|
+
const digest = isRenderError(error) ? { code: error.code, data: error.digest.data } : null;
|
|
294
293
|
|
|
295
294
|
element = h(ErrorReconstituter, {
|
|
296
295
|
error: serializedError,
|
|
@@ -9,7 +9,7 @@ import { swallow } from '../logger.ts';
|
|
|
9
9
|
import { computeSegmentKeys, computeSegmentTreePaths } from '../../routing/segment-keys.ts';
|
|
10
10
|
import type { SegmentInfo } from '../../shared/segment-info.ts';
|
|
11
11
|
import type { SlotSkipEntry } from '../slot-resolver.ts';
|
|
12
|
-
import { isClientReference } from '../
|
|
12
|
+
import { isClientReference } from '../utils/element-type.ts';
|
|
13
13
|
import { isStaticRequestDependent } from '../request-dep.ts';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -72,15 +72,15 @@ import { generateStaticSite } from '../static-generator.ts';
|
|
|
72
72
|
* Resolve the Server-Timing mode from timber.config.ts.
|
|
73
73
|
*
|
|
74
74
|
* If the user set `serverTiming` explicitly, use that value.
|
|
75
|
-
* Otherwise
|
|
75
|
+
* Otherwise default to `'total'`.
|
|
76
76
|
*/
|
|
77
77
|
function resolveServerTimingMode(
|
|
78
78
|
config: Record<string, unknown>,
|
|
79
|
-
|
|
79
|
+
_isDev: boolean
|
|
80
80
|
): 'detailed' | 'total' | false {
|
|
81
81
|
const userValue = config.serverTiming as 'detailed' | 'total' | false | undefined;
|
|
82
82
|
if (userValue !== undefined) return userValue;
|
|
83
|
-
return
|
|
83
|
+
return 'total';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -31,11 +31,8 @@ import type { LayoutEntry } from '../deny-renderer.ts';
|
|
|
31
31
|
import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.ts';
|
|
32
32
|
import type { ClientBootstrapConfig } from '../html-injectors.ts';
|
|
33
33
|
import type { InterceptionContext, RouteMatch } from '../pipeline.ts';
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
NoPageComponentError,
|
|
37
|
-
ParamCoercionError,
|
|
38
|
-
} from '../route-element-builder.ts';
|
|
34
|
+
import { buildRouteElement, NoPageComponentError } from '../route-element-builder.ts';
|
|
35
|
+
import { ParamCoercionError } from '../param-coercion.ts';
|
|
39
36
|
import type { ManifestSegmentNode } from '../route-matcher.ts';
|
|
40
37
|
import { recordTiming } from '../server-timing.ts';
|
|
41
38
|
import { requestContextAls } from '../als-registry.ts';
|
|
@@ -146,8 +143,8 @@ export async function renderRoute(
|
|
|
146
143
|
layoutComponents,
|
|
147
144
|
deferSuspenseFor,
|
|
148
145
|
skippedSegments,
|
|
149
|
-
shellSettled,
|
|
150
146
|
slotSkipInfo,
|
|
147
|
+
leafDenyChain,
|
|
151
148
|
} = routeResult;
|
|
152
149
|
|
|
153
150
|
// Build head HTML for injection into the SSR output.
|
|
@@ -201,8 +198,7 @@ export async function renderRoute(
|
|
|
201
198
|
|
|
202
199
|
// Render to RSC Flight stream with signal tracking.
|
|
203
200
|
const _rscStart = performance.now();
|
|
204
|
-
const { rscStream, signals, getDebugComponents } = renderRscStream(payload, req);
|
|
205
|
-
if (shellSettled) signals.shellSettled = shellSettled;
|
|
201
|
+
const { rscStream, signals, getDebugComponents } = renderRscStream(payload, req, leafDenyChain);
|
|
206
202
|
|
|
207
203
|
// Store the debug components getter in ALS so onPipelineError can
|
|
208
204
|
// include component tree context for render-phase errors (dev mode only).
|
|
@@ -5,7 +5,18 @@
|
|
|
5
5
|
* returned directly without SSR HTML rendering. The client decodes it via
|
|
6
6
|
* `createFromFetch` and renders into the hydrated React root.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The response's HTTP status carries no deny semantics: a payload response
|
|
9
|
+
* commits 200 when Flight's opening window closes (the first chunk plus one
|
|
10
|
+
* macrotask), and a deny, redirect, or error that lands after that rides
|
|
11
|
+
* the payload as a Flight error row for the client's boundaries — the same
|
|
12
|
+
* way a nested component's already did. The status
|
|
13
|
+
* code contract (design/05) is a document-path contract, enforced by Fizz's
|
|
14
|
+
* `onShellReady`; Flight has no such checkpoint, and the only reader of a
|
|
15
|
+
* payload response's status is the static generator's check for an error
|
|
16
|
+
* envelope — the client router reads the body. See TIM-1495.
|
|
17
|
+
*
|
|
18
|
+
* Design docs: 19-client-navigation.md §"RSC Payload Handling",
|
|
19
|
+
* 04-authorization.md §"Status codes on the RSC payload path"
|
|
9
20
|
*/
|
|
10
21
|
|
|
11
22
|
import type { LayoutEntry } from '../deny-renderer.ts';
|
|
@@ -18,7 +29,7 @@ import type { SlotSkipEntry } from '../slot-resolver.ts';
|
|
|
18
29
|
import { buildRedirectResponse, buildSegmentInfo, createDebugChannelSink } from './helpers.ts';
|
|
19
30
|
import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.ts';
|
|
20
31
|
|
|
21
|
-
import { rscErrorEnvelope } from '
|
|
32
|
+
import { rscErrorEnvelope } from '../rsc-error-envelope.ts';
|
|
22
33
|
|
|
23
34
|
import type { RenderSignals } from './rsc-stream.ts';
|
|
24
35
|
|
|
@@ -39,6 +50,16 @@ const FIRST_CHUNK_CEILING_MS = 50;
|
|
|
39
50
|
* renderToReadableStream. Reading one chunk triggers rendering of the
|
|
40
51
|
* initial component tree, allowing onError to capture DenySignal/
|
|
41
52
|
* RedirectSignal before we commit the response. See TIM-344.
|
|
53
|
+
*
|
|
54
|
+
* The commit point is the close of Flight's opening window: the first chunk
|
|
55
|
+
* plus one macrotask turn. Nothing is held for it. A signal that lands
|
|
56
|
+
* inside the window still gets a server-level answer — a redirect response,
|
|
57
|
+
* the fallback deny page, the hard-navigate envelope — because the race
|
|
58
|
+
* below and the one timer turn after it deliver it for free. A signal that
|
|
59
|
+
* lands after it is a row in the stream, and the client's boundaries answer
|
|
60
|
+
* it. Waiting for the shell components to settle (TIM-1045, TIM-1208) made
|
|
61
|
+
* the payload path pretend to a status contract it could not keep for
|
|
62
|
+
* nested components, and was removed in TIM-1495.
|
|
42
63
|
*/
|
|
43
64
|
export async function buildRscPayloadResponse(
|
|
44
65
|
req: Request,
|
|
@@ -98,36 +119,25 @@ export async function buildRscPayloadResponse(
|
|
|
98
119
|
return rscErrorEnvelope(500, responseHeaders);
|
|
99
120
|
}
|
|
100
121
|
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
122
|
+
// The first chunk arrived with no signal yet. Give Flight's opening window
|
|
123
|
+
// one macrotask to close before committing: Flight pings continuation
|
|
124
|
+
// tasks as microtasks only during that window (`startWork` closes it with
|
|
125
|
+
// a `setTimeout(0)`) and as macrotasks after it, so everything that throws
|
|
126
|
+
// synchronously or after microtask-only work — a page body calling
|
|
127
|
+
// `redirect()`, a hundred nested components awaiting `Promise.resolve()` —
|
|
128
|
+
// has reached `onError` by the time this timer fires, and gets the server
|
|
129
|
+
// answer below (the 204 redirect, the fallback deny page, the envelope)
|
|
130
|
+
// rather than a row the client has to answer. Anything that awaited a
|
|
131
|
+
// timer or I/O has left the window and rides the payload. Nothing is
|
|
132
|
+
// tracked and nothing is waited for: the cost is one timer turn, and the
|
|
133
|
+
// status is 200 either way — this decides which mechanism answers, not
|
|
134
|
+
// what the response claims.
|
|
105
135
|
if (first.type === 'data' && !signals.redirectSignal && !signals.denySignal) {
|
|
106
|
-
|
|
107
|
-
// renderToReadableStream — before the first chunk is read. Skip the
|
|
108
|
-
// wait entirely: components below the denied gate never execute.
|
|
109
|
-
const { getDenyStatus: earlyDenyCheck } = await import('../deny-boundary.ts');
|
|
110
|
-
if (signals.shellSettled && !earlyDenyCheck()) {
|
|
111
|
-
// Wait for all deny-capable shell components to finish, or for an
|
|
112
|
-
// in-tree layout deny to short-circuit (setDenyStatus already
|
|
113
|
-
// called), or for a signal to escape to onError. TIM-1045, TIM-1208.
|
|
114
|
-
await Promise.race([signals.shellSettled, signalDetected]);
|
|
115
|
-
// onSettled fires from the raw metadata promise's rejection handler,
|
|
116
|
-
// which runs in the same microtask as the rejection. React Flight's
|
|
117
|
-
// onError fires synchronously inside that microtask (serializeThenable
|
|
118
|
-
// rejection → erroredTask → logRecoverableError → onError). One
|
|
119
|
-
// macrotask yield is enough for onError to set signals.denySignal /
|
|
120
|
-
// signals.redirectSignal / signals.renderError before we check.
|
|
121
|
-
// Pinned by tests/react-server/metadata-signal-ordering.test.ts.
|
|
122
|
-
await new Promise<void>((r) => setTimeout(r, 0));
|
|
123
|
-
} else if (!earlyDenyCheck()) {
|
|
124
|
-
// No deny-capable shell components (client component page, no deny
|
|
125
|
-
// chain) — brief yield to let in-flight microtask rejections complete.
|
|
126
|
-
await new Promise<void>((r) => setTimeout(r, 0));
|
|
127
|
-
}
|
|
136
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
// Detach the callback —
|
|
139
|
+
// Detach the callback — the window has closed, and a signal arriving after
|
|
140
|
+
// it is a row in the stream the client answers.
|
|
131
141
|
signals.onSignal = undefined;
|
|
132
142
|
|
|
133
143
|
// Check for redirect/deny signals detected during initial rendering
|
|
@@ -139,11 +149,9 @@ export async function buildRscPayloadResponse(
|
|
|
139
149
|
// An addressed deny needs no fallback: the payload already carries its error
|
|
140
150
|
// row, aimed at the boundary the client has mounted at that segment, which
|
|
141
151
|
// renders it. Cancelling would throw a finished render away and re-run the
|
|
142
|
-
// whole route — the exact cost TIM-666 removed.
|
|
143
|
-
// because the catch site called setDenyStatus() before re-throwing (read
|
|
144
|
-
// below via getDenyStatus).
|
|
152
|
+
// whole route — the exact cost TIM-666 removed.
|
|
145
153
|
//
|
|
146
|
-
// The question is asked of `unplacedDeny`, not of `denySignal.
|
|
154
|
+
// The question is asked of `unplacedDeny`, not of `denySignal.owners`,
|
|
147
155
|
// because one render can produce both kinds and `denySignal` holds only one
|
|
148
156
|
// of them. A deny nothing could place has no boundary that may claim it, so
|
|
149
157
|
// its row would otherwise stream to a client with nothing to render it —
|
|
@@ -193,7 +201,7 @@ export async function buildRscPayloadResponse(
|
|
|
193
201
|
let firstRead: ReadableStreamReadResult<Uint8Array>;
|
|
194
202
|
if (first.type === 'data') {
|
|
195
203
|
firstRead = first.chunk;
|
|
196
|
-
} else if (signals.denySignal?.
|
|
204
|
+
} else if (signals.denySignal?.owners) {
|
|
197
205
|
// An addressed deny won the race before its own error row surfaced.
|
|
198
206
|
// Cancelling here would hand the client a 4xx payload with nothing in it
|
|
199
207
|
// — no row for the owning boundary to render, so the navigation shows
|
|
@@ -266,10 +274,11 @@ export async function buildRscPayloadResponse(
|
|
|
266
274
|
// for state tree diffing on subsequent navigations. Request-dependency
|
|
267
275
|
// is determined by static module-graph analysis (not runtime taint).
|
|
268
276
|
// See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
269
|
-
// Read the deny
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
// Read the deny index before describing the tree — what mounted depends on
|
|
278
|
+
// it. Recorded in ALS during render by AccessGate's catch path; a gate that
|
|
279
|
+
// denies after this point leaves it unset, and the header then describes
|
|
280
|
+
// the full chain, which is the merge-point answer either way.
|
|
281
|
+
const { getFirstDeniedSegmentIndex } = await import('../deny-boundary.ts');
|
|
273
282
|
const resolvedDenyIndex = getFirstDeniedSegmentIndex();
|
|
274
283
|
|
|
275
284
|
// Emitted in full even when a deny rendered, because this header answers two
|
|
@@ -280,9 +289,10 @@ export async function buildRscPayloadResponse(
|
|
|
280
289
|
//
|
|
281
290
|
// What it must not also do on a deny is seed the segment cache, since the
|
|
282
291
|
// deny page replaced a subtree and part of that chain never mounted. That
|
|
283
|
-
// half is
|
|
284
|
-
//
|
|
285
|
-
//
|
|
292
|
+
// half is decided on the client by the boundary that renders the status
|
|
293
|
+
// page, at the commit that puts it on screen — the only place that knows,
|
|
294
|
+
// since the row may arrive after this response has been returned. See
|
|
295
|
+
// `client/navigation-commit.ts` and TIM-1356, TIM-1495.
|
|
286
296
|
const segmentInfo = buildSegmentInfo(
|
|
287
297
|
segments,
|
|
288
298
|
layoutComponents,
|
|
@@ -298,8 +308,15 @@ export async function buildRscPayloadResponse(
|
|
|
298
308
|
responseHeaders.set('X-Timber-Skipped-Segments', JSON.stringify(skippedSegments));
|
|
299
309
|
}
|
|
300
310
|
|
|
311
|
+
// Always 200. A deny that already streamed — addressed at its owner's
|
|
312
|
+
// boundary — is in the body, not the status line, and the one consumer of
|
|
313
|
+
// this response, the client router, reads the body. A status here would be
|
|
314
|
+
// right only for denies that happened to land before this line and wrong
|
|
315
|
+
// for every nested component that denies after I/O, and a status that is
|
|
316
|
+
// sometimes right is a contract nothing may rely on. So it makes no claim.
|
|
317
|
+
// See design/04-authorization.md §"Status codes on the RSC payload path".
|
|
301
318
|
return new Response(patchedStream, {
|
|
302
|
-
status:
|
|
319
|
+
status: 200,
|
|
303
320
|
headers: responseHeaders,
|
|
304
321
|
});
|
|
305
322
|
}
|