@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.
Files changed (249) hide show
  1. package/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
  2. package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
  5. package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
  6. package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
  7. package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
  8. package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
  9. package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
  11. package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
  14. package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
  15. package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
  16. package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
  17. package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
  18. package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
  19. package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
  20. package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
  21. package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
  22. package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
  23. package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
  24. package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
  25. package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
  26. package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
  27. package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
  28. package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
  29. package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
  30. package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
  31. package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
  32. package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
  33. package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
  34. package/dist/_chunks/state-FippDgxN.js +52 -0
  35. package/dist/_chunks/state-FippDgxN.js.map +1 -0
  36. package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
  37. package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
  38. package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
  39. package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
  40. package/dist/analyze/crawl-entry.js +2 -2
  41. package/dist/analyze/graph-command.js +2 -2
  42. package/dist/cache/index.js +1 -1
  43. package/dist/cli.js +2 -2
  44. package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
  45. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  46. package/dist/client/browser-entry/action-queue.d.ts +44 -0
  47. package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
  48. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  49. package/dist/client/deny-last-resort.d.ts +29 -0
  50. package/dist/client/deny-last-resort.d.ts.map +1 -0
  51. package/dist/client/error-boundary.d.ts +48 -2
  52. package/dist/client/error-boundary.d.ts.map +1 -1
  53. package/dist/client/error-boundary.js +2 -2
  54. package/dist/client/history.d.ts +21 -2
  55. package/dist/client/history.d.ts.map +1 -1
  56. package/dist/client/index.js +34 -14
  57. package/dist/client/index.js.map +1 -1
  58. package/dist/client/internal.d.ts +1 -0
  59. package/dist/client/internal.d.ts.map +1 -1
  60. package/dist/client/internal.js +272 -1225
  61. package/dist/client/internal.js.map +1 -1
  62. package/dist/client/link.d.ts.map +1 -1
  63. package/dist/client/navigation-commit.d.ts +12 -19
  64. package/dist/client/navigation-commit.d.ts.map +1 -1
  65. package/dist/client/navigation-transition.d.ts +62 -11
  66. package/dist/client/navigation-transition.d.ts.map +1 -1
  67. package/dist/client/router-effects.d.ts +9 -8
  68. package/dist/client/router-effects.d.ts.map +1 -1
  69. package/dist/client/router-lifecycle.d.ts +60 -17
  70. package/dist/client/router-lifecycle.d.ts.map +1 -1
  71. package/dist/client/router-pipeline.d.ts +7 -4
  72. package/dist/client/router-pipeline.d.ts.map +1 -1
  73. package/dist/client/router-types.d.ts +63 -8
  74. package/dist/client/router-types.d.ts.map +1 -1
  75. package/dist/client/router.d.ts.map +1 -1
  76. package/dist/client/rsc-fetch.d.ts +0 -9
  77. package/dist/client/rsc-fetch.d.ts.map +1 -1
  78. package/dist/client/segment-cache.d.ts +23 -8
  79. package/dist/client/segment-cache.d.ts.map +1 -1
  80. package/dist/client/state.d.ts +16 -0
  81. package/dist/client/state.d.ts.map +1 -1
  82. package/dist/client/status-page-marker.d.ts +25 -0
  83. package/dist/client/status-page-marker.d.ts.map +1 -0
  84. package/dist/cookies/index.js +1 -1
  85. package/dist/dev-tools/holding-server.d.ts +4 -17
  86. package/dist/dev-tools/holding-server.d.ts.map +1 -1
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +74 -175
  89. package/dist/index.js.map +1 -1
  90. package/dist/plugins/dev-server.d.ts.map +1 -1
  91. package/dist/routing/codegen-write.d.ts.map +1 -1
  92. package/dist/routing/index.js +2 -2
  93. package/dist/routing/interception.d.ts +2 -2
  94. package/dist/routing/slot-placement.d.ts +2 -2
  95. package/dist/server/access-gate.d.ts +73 -1
  96. package/dist/server/access-gate.d.ts.map +1 -1
  97. package/dist/server/action-handler.d.ts.map +1 -1
  98. package/dist/server/actions.d.ts +16 -1
  99. package/dist/server/actions.d.ts.map +1 -1
  100. package/dist/server/als-registry.d.ts +3 -9
  101. package/dist/server/als-registry.d.ts.map +1 -1
  102. package/dist/server/children-interception.d.ts +1 -1
  103. package/dist/server/default-status-page.d.ts +2 -2
  104. package/dist/server/default-status-page.d.ts.map +1 -1
  105. package/dist/server/deny-boundary.d.ts +15 -9
  106. package/dist/server/deny-boundary.d.ts.map +1 -1
  107. package/dist/server/deny-renderer.d.ts.map +1 -1
  108. package/dist/server/error-boundary-wrapper.d.ts +21 -4
  109. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  110. package/dist/server/error-response-headers.d.ts +3 -0
  111. package/dist/server/error-response-headers.d.ts.map +1 -0
  112. package/dist/server/index.js +3 -3
  113. package/dist/server/index.js.map +1 -1
  114. package/dist/server/internal.d.ts +1 -2
  115. package/dist/server/internal.d.ts.map +1 -1
  116. package/dist/server/internal.js +2339 -2506
  117. package/dist/server/internal.js.map +1 -1
  118. package/dist/server/metadata-collector.d.ts +2 -5
  119. package/dist/server/metadata-collector.d.ts.map +1 -1
  120. package/dist/server/param-coercion.d.ts +10 -3
  121. package/dist/server/param-coercion.d.ts.map +1 -1
  122. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  123. package/dist/server/pipeline-phases.d.ts +11 -0
  124. package/dist/server/pipeline-phases.d.ts.map +1 -1
  125. package/dist/server/port-resolution.d.ts +3 -89
  126. package/dist/server/port-resolution.d.ts.map +1 -1
  127. package/dist/server/primitives.d.ts +38 -10
  128. package/dist/server/primitives.d.ts.map +1 -1
  129. package/dist/server/response-cache-policy.d.ts +3 -0
  130. package/dist/server/response-cache-policy.d.ts.map +1 -0
  131. package/dist/server/route-element-builder.d.ts +11 -41
  132. package/dist/server/route-element-builder.d.ts.map +1 -1
  133. package/dist/server/route-element-helpers.d.ts +12 -0
  134. package/dist/server/route-element-helpers.d.ts.map +1 -0
  135. package/dist/server/route-module-loader.d.ts +37 -0
  136. package/dist/server/route-module-loader.d.ts.map +1 -0
  137. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  138. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  139. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  140. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  141. package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
  142. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  143. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
  144. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  145. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  146. package/dist/server/rsc-error-envelope.d.ts +11 -0
  147. package/dist/server/rsc-error-envelope.d.ts.map +1 -0
  148. package/dist/server/skippable-prefix.d.ts +18 -15
  149. package/dist/server/skippable-prefix.d.ts.map +1 -1
  150. package/dist/server/slot-resolver.d.ts.map +1 -1
  151. package/dist/server/slot-subtree-contain.d.ts +54 -0
  152. package/dist/server/slot-subtree-contain.d.ts.map +1 -0
  153. package/dist/server/stream-utils.d.ts.map +1 -1
  154. package/dist/server/utils/element-type.d.ts +10 -0
  155. package/dist/server/utils/element-type.d.ts.map +1 -1
  156. package/dist/shared/rsc-error-envelope.d.ts +0 -9
  157. package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
  158. package/dist/shared/status-reason-phrase.d.ts +26 -0
  159. package/dist/shared/status-reason-phrase.d.ts.map +1 -0
  160. package/docs/api/30-api-server.mdx +4 -2
  161. package/docs/api/31-api-client.mdx +5 -1
  162. package/docs/api/36-cli.mdx +5 -3
  163. package/docs/learn/12-error-handling.mdx +5 -1
  164. package/package.json +10 -10
  165. package/src/client/browser-entry/action-dispatch.ts +166 -99
  166. package/src/client/browser-entry/action-queue.ts +90 -0
  167. package/src/client/browser-entry/router-init.ts +60 -35
  168. package/src/client/deny-last-resort.tsx +54 -0
  169. package/src/client/error-boundary.tsx +144 -42
  170. package/src/client/history.ts +52 -3
  171. package/src/client/internal.ts +1 -0
  172. package/src/client/link.tsx +70 -35
  173. package/src/client/navigation-commit.ts +79 -27
  174. package/src/client/navigation-transition.ts +176 -127
  175. package/src/client/router-effects.ts +14 -17
  176. package/src/client/router-lifecycle.ts +181 -115
  177. package/src/client/router-pipeline.ts +94 -71
  178. package/src/client/router-types.ts +61 -7
  179. package/src/client/router.ts +147 -74
  180. package/src/client/rsc-fetch.ts +0 -13
  181. package/src/client/segment-cache.ts +43 -10
  182. package/src/client/state.ts +26 -0
  183. package/src/client/status-page-marker.tsx +32 -0
  184. package/src/dev-tools/holding-server.ts +4 -17
  185. package/src/index.ts +18 -34
  186. package/src/plugins/dev-server.ts +2 -1
  187. package/src/react-canary.d.ts +2 -0
  188. package/src/routing/codegen-write.ts +2 -0
  189. package/src/routing/interception.ts +2 -2
  190. package/src/routing/slot-placement.ts +2 -2
  191. package/src/server/access-gate.tsx +89 -21
  192. package/src/server/action-client.ts +2 -2
  193. package/src/server/action-handler.ts +23 -10
  194. package/src/server/actions.ts +81 -34
  195. package/src/server/als-registry.ts +3 -9
  196. package/src/server/children-interception.ts +1 -1
  197. package/src/server/default-status-page.ts +7 -47
  198. package/src/server/deny-boundary.ts +45 -28
  199. package/src/server/deny-renderer.ts +6 -2
  200. package/src/server/error-boundary-wrapper.ts +23 -4
  201. package/src/server/error-response-headers.ts +18 -0
  202. package/src/server/internal.ts +2 -10
  203. package/src/server/metadata-collector.ts +3 -18
  204. package/src/server/param-coercion.ts +13 -4
  205. package/src/server/pipeline-outcome.ts +35 -13
  206. package/src/server/pipeline-phases.ts +22 -16
  207. package/src/server/port-resolution.ts +3 -165
  208. package/src/server/prebuilt-builder.ts +4 -4
  209. package/src/server/primitives.ts +75 -11
  210. package/src/server/response-cache-policy.ts +45 -0
  211. package/src/server/route-element-builder.ts +149 -412
  212. package/src/server/route-element-helpers.ts +37 -0
  213. package/src/server/route-handler.ts +2 -2
  214. package/src/server/route-module-loader.ts +161 -0
  215. package/src/server/rsc-cache-key-guard.ts +2 -42
  216. package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
  217. package/src/server/rsc-entry/api-handler.ts +5 -5
  218. package/src/server/rsc-entry/error-renderer.ts +3 -4
  219. package/src/server/rsc-entry/helpers.ts +1 -1
  220. package/src/server/rsc-entry/index.ts +3 -3
  221. package/src/server/rsc-entry/render-route.ts +4 -8
  222. package/src/server/rsc-entry/rsc-payload.ts +59 -42
  223. package/src/server/rsc-entry/rsc-stream.ts +48 -27
  224. package/src/server/rsc-entry/ssr-renderer.ts +6 -10
  225. package/src/server/rsc-error-envelope.ts +18 -0
  226. package/src/server/skippable-prefix.ts +105 -7
  227. package/src/server/slot-resolver.ts +43 -12
  228. package/src/server/slot-subtree-contain.ts +255 -0
  229. package/src/server/stream-utils.ts +12 -8
  230. package/src/server/utils/element-type.ts +18 -2
  231. package/src/shared/rsc-error-envelope.ts +0 -15
  232. package/src/shared/status-reason-phrase.ts +61 -0
  233. package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
  234. package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
  235. package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
  236. package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
  237. package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
  238. package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
  239. package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
  240. package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
  241. package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
  242. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
  243. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
  244. package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
  245. package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
  246. package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
  247. package/dist/server/tree-builder.d.ts +0 -150
  248. package/dist/server/tree-builder.d.ts.map +0 -1
  249. 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 { DenySignal, RedirectSignal } from './primitives.ts';
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 instanceof DenySignal || error instanceof RedirectSignal) {
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
- // Collected before deleting — mutating Headers while iterating its live
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 '../route-element-builder.ts';
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 instanceof RedirectSignal) {
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 instanceof DenySignal) {
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 instanceof DenySignal) {
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 instanceof RedirectSignal) {
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 instanceof DenySignal) {
107
+ if (isDenySignal(error)) {
108
108
  return stripHeadBody(await renderApiDeny(error, segments, responseHeaders), req.method);
109
109
  }
110
- if (error instanceof RedirectSignal) {
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 '../../shared/rsc-error-envelope.ts';
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 '../route-element-builder.ts';
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: `'detailed'` in dev, `'total'` in production.
75
+ * Otherwise default to `'total'`.
76
76
  */
77
77
  function resolveServerTimingMode(
78
78
  config: Record<string, unknown>,
79
- isDev: boolean
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 isDev ? 'detailed' : 'total';
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
- buildRouteElement,
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
- * Design docs: 19-client-navigation.md §"RSC Payload Handling"
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 '../../shared/rsc-error-envelope.ts';
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
- // If data arrived first, still check signals — they may have fired
102
- // concurrently. Wait for all deny-capable shell components (layouts
103
- // with deny chains + page) to settle so async deny() calls preceded
104
- // by I/O are captured. See TIM-1045, TIM-1208.
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
- // Sync deny (AccessGate pre-pass replay) sets getDenyStatus() during
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 — no longer needed after this point.
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. The status still lands,
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.ownerKey`,
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?.ownerKey) {
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 state before describing the tree — what mounted depends on it.
270
- // Both values are recorded in ALS during render by AccessGate's catch path.
271
- const { getDenyStatus, getFirstDeniedSegmentIndex } = await import('../deny-boundary.ts');
272
- const denyStatus = getDenyStatus();
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 handled at the commit site from the response status, where the
284
- // merge point has already been chosen. See `client/router-pipeline.ts` and
285
- // TIM-1356.
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: denyStatus ?? 200,
319
+ status: 200,
303
320
  headers: responseHeaders,
304
321
  });
305
322
  }