@timber-js/app 0.2.0-alpha.33 → 0.2.0-alpha.35

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 (236) hide show
  1. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  2. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  3. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  4. package/dist/_chunks/{debug-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
  5. package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
  6. package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
  7. package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
  8. package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
  9. package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
  10. package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
  11. package/dist/_chunks/{format-RyoGQL74.js.map → format-cX7wzEp2.js.map} +1 -1
  12. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  13. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  14. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  16. package/dist/_chunks/{request-context-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
  17. package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
  18. package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
  19. package/dist/_chunks/segment-context-Dpq2XOKg.js.map +1 -0
  20. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  22. package/dist/_chunks/{tracing-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
  23. package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.js.map} +1 -1
  24. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  26. package/dist/_chunks/wrappers-C1SN725w.js +331 -0
  27. package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
  28. package/dist/adapters/compress-module.d.ts.map +1 -1
  29. package/dist/adapters/nitro.js +5 -2
  30. package/dist/adapters/nitro.js.map +1 -1
  31. package/dist/cache/index.js +1 -2
  32. package/dist/cache/index.js.map +1 -1
  33. package/dist/client/error-boundary.d.ts +10 -1
  34. package/dist/client/error-boundary.d.ts.map +1 -1
  35. package/dist/client/error-boundary.js +1 -125
  36. package/dist/client/index.d.ts +2 -2
  37. package/dist/client/index.d.ts.map +1 -1
  38. package/dist/client/index.js +193 -90
  39. package/dist/client/index.js.map +1 -1
  40. package/dist/client/link.d.ts +8 -8
  41. package/dist/client/link.d.ts.map +1 -1
  42. package/dist/client/navigation-context.d.ts +2 -2
  43. package/dist/client/router.d.ts +25 -3
  44. package/dist/client/router.d.ts.map +1 -1
  45. package/dist/client/rsc-fetch.d.ts +23 -2
  46. package/dist/client/rsc-fetch.d.ts.map +1 -1
  47. package/dist/client/segment-cache.d.ts +1 -1
  48. package/dist/client/segment-cache.d.ts.map +1 -1
  49. package/dist/client/stale-reload.d.ts +15 -0
  50. package/dist/client/stale-reload.d.ts.map +1 -1
  51. package/dist/client/top-loader.d.ts +1 -1
  52. package/dist/client/top-loader.d.ts.map +1 -1
  53. package/dist/client/use-params.d.ts +2 -2
  54. package/dist/client/use-params.d.ts.map +1 -1
  55. package/dist/client/use-query-states.d.ts +1 -1
  56. package/dist/codec.d.ts +21 -0
  57. package/dist/codec.d.ts.map +1 -0
  58. package/dist/cookies/define-cookie.d.ts +33 -12
  59. package/dist/cookies/define-cookie.d.ts.map +1 -1
  60. package/dist/cookies/index.js +1 -83
  61. package/dist/index.d.ts +87 -12
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +356 -215
  64. package/dist/index.js.map +1 -1
  65. package/dist/params/define.d.ts +76 -0
  66. package/dist/params/define.d.ts.map +1 -0
  67. package/dist/params/index.d.ts +8 -0
  68. package/dist/params/index.d.ts.map +1 -0
  69. package/dist/params/index.js +104 -0
  70. package/dist/params/index.js.map +1 -0
  71. package/dist/plugins/adapter-build.d.ts.map +1 -1
  72. package/dist/plugins/build-manifest.d.ts.map +1 -1
  73. package/dist/plugins/client-chunks.d.ts +32 -0
  74. package/dist/plugins/client-chunks.d.ts.map +1 -0
  75. package/dist/plugins/entries.d.ts.map +1 -1
  76. package/dist/plugins/routing.d.ts.map +1 -1
  77. package/dist/plugins/server-bundle.d.ts.map +1 -1
  78. package/dist/plugins/static-build.d.ts.map +1 -1
  79. package/dist/routing/codegen.d.ts +2 -2
  80. package/dist/routing/codegen.d.ts.map +1 -1
  81. package/dist/routing/index.js +1 -1
  82. package/dist/routing/scanner.d.ts.map +1 -1
  83. package/dist/routing/status-file-lint.d.ts +2 -1
  84. package/dist/routing/status-file-lint.d.ts.map +1 -1
  85. package/dist/routing/types.d.ts +6 -4
  86. package/dist/routing/types.d.ts.map +1 -1
  87. package/dist/rsc-runtime/rsc.d.ts +1 -1
  88. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  89. package/dist/search-params/codecs.d.ts +1 -1
  90. package/dist/search-params/define.d.ts +153 -0
  91. package/dist/search-params/define.d.ts.map +1 -0
  92. package/dist/search-params/index.d.ts +4 -5
  93. package/dist/search-params/index.d.ts.map +1 -1
  94. package/dist/search-params/index.js +3 -474
  95. package/dist/search-params/registry.d.ts +1 -1
  96. package/dist/search-params/wrappers.d.ts +53 -0
  97. package/dist/search-params/wrappers.d.ts.map +1 -0
  98. package/dist/server/access-gate.d.ts +4 -0
  99. package/dist/server/access-gate.d.ts.map +1 -1
  100. package/dist/server/action-encryption.d.ts +76 -0
  101. package/dist/server/action-encryption.d.ts.map +1 -0
  102. package/dist/server/action-handler.d.ts.map +1 -1
  103. package/dist/server/als-registry.d.ts +4 -4
  104. package/dist/server/als-registry.d.ts.map +1 -1
  105. package/dist/server/build-manifest.d.ts +2 -2
  106. package/dist/server/early-hints.d.ts +13 -5
  107. package/dist/server/early-hints.d.ts.map +1 -1
  108. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  109. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  110. package/dist/server/flight-injection-state.d.ts +78 -0
  111. package/dist/server/flight-injection-state.d.ts.map +1 -0
  112. package/dist/server/form-data.d.ts +29 -0
  113. package/dist/server/form-data.d.ts.map +1 -1
  114. package/dist/server/html-injectors.d.ts.map +1 -1
  115. package/dist/server/index.d.ts +1 -1
  116. package/dist/server/index.d.ts.map +1 -1
  117. package/dist/server/index.js +1819 -1629
  118. package/dist/server/index.js.map +1 -1
  119. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  120. package/dist/server/pipeline.d.ts.map +1 -1
  121. package/dist/server/request-context.d.ts +28 -40
  122. package/dist/server/request-context.d.ts.map +1 -1
  123. package/dist/server/route-element-builder.d.ts +7 -0
  124. package/dist/server/route-element-builder.d.ts.map +1 -1
  125. package/dist/server/route-matcher.d.ts +2 -2
  126. package/dist/server/route-matcher.d.ts.map +1 -1
  127. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  128. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  129. package/dist/server/slot-resolver.d.ts.map +1 -1
  130. package/dist/server/ssr-entry.d.ts.map +1 -1
  131. package/dist/server/ssr-render.d.ts +3 -0
  132. package/dist/server/ssr-render.d.ts.map +1 -1
  133. package/dist/server/tree-builder.d.ts +12 -8
  134. package/dist/server/tree-builder.d.ts.map +1 -1
  135. package/dist/server/types.d.ts +1 -3
  136. package/dist/server/types.d.ts.map +1 -1
  137. package/dist/server/version-skew.d.ts +61 -0
  138. package/dist/server/version-skew.d.ts.map +1 -0
  139. package/dist/shims/navigation-client.d.ts +1 -1
  140. package/dist/shims/navigation-client.d.ts.map +1 -1
  141. package/dist/shims/navigation.d.ts +1 -1
  142. package/dist/shims/navigation.d.ts.map +1 -1
  143. package/dist/utils/state-machine.d.ts +80 -0
  144. package/dist/utils/state-machine.d.ts.map +1 -0
  145. package/package.json +12 -8
  146. package/src/adapters/compress-module.ts +5 -2
  147. package/src/client/browser-entry.ts +94 -85
  148. package/src/client/error-boundary.tsx +18 -1
  149. package/src/client/index.ts +9 -1
  150. package/src/client/link.tsx +9 -9
  151. package/src/client/navigation-context.ts +2 -2
  152. package/src/client/router.ts +102 -55
  153. package/src/client/rsc-fetch.ts +63 -2
  154. package/src/client/segment-cache.ts +1 -1
  155. package/src/client/stale-reload.ts +28 -0
  156. package/src/client/top-loader.tsx +2 -2
  157. package/src/client/use-params.ts +3 -3
  158. package/src/client/use-query-states.ts +1 -1
  159. package/src/codec.ts +21 -0
  160. package/src/cookies/define-cookie.ts +69 -18
  161. package/src/index.ts +255 -65
  162. package/src/params/define.ts +260 -0
  163. package/src/params/index.ts +28 -0
  164. package/src/plugins/adapter-build.ts +6 -0
  165. package/src/plugins/build-manifest.ts +11 -0
  166. package/src/plugins/client-chunks.ts +65 -0
  167. package/src/plugins/entries.ts +3 -6
  168. package/src/plugins/routing.ts +40 -14
  169. package/src/plugins/server-bundle.ts +32 -1
  170. package/src/plugins/shims.ts +1 -1
  171. package/src/plugins/static-build.ts +8 -4
  172. package/src/routing/codegen.ts +109 -88
  173. package/src/routing/scanner.ts +55 -6
  174. package/src/routing/status-file-lint.ts +2 -1
  175. package/src/routing/types.ts +7 -4
  176. package/src/rsc-runtime/rsc.ts +2 -0
  177. package/src/search-params/codecs.ts +1 -1
  178. package/src/search-params/define.ts +504 -0
  179. package/src/search-params/index.ts +12 -18
  180. package/src/search-params/registry.ts +1 -1
  181. package/src/search-params/wrappers.ts +85 -0
  182. package/src/server/access-gate.tsx +38 -8
  183. package/src/server/action-encryption.ts +144 -0
  184. package/src/server/action-handler.ts +16 -0
  185. package/src/server/als-registry.ts +4 -4
  186. package/src/server/build-manifest.ts +4 -4
  187. package/src/server/compress.ts +25 -7
  188. package/src/server/early-hints.ts +36 -15
  189. package/src/server/error-boundary-wrapper.ts +57 -14
  190. package/src/server/flight-injection-state.ts +152 -0
  191. package/src/server/form-data.ts +76 -0
  192. package/src/server/html-injectors.ts +42 -26
  193. package/src/server/index.ts +2 -4
  194. package/src/server/node-stream-transforms.ts +91 -46
  195. package/src/server/pipeline.ts +98 -26
  196. package/src/server/request-context.ts +49 -124
  197. package/src/server/route-element-builder.ts +102 -99
  198. package/src/server/route-matcher.ts +2 -2
  199. package/src/server/rsc-entry/error-renderer.ts +3 -2
  200. package/src/server/rsc-entry/index.ts +26 -11
  201. package/src/server/rsc-entry/rsc-payload.ts +2 -2
  202. package/src/server/rsc-entry/ssr-renderer.ts +4 -4
  203. package/src/server/slot-resolver.ts +204 -206
  204. package/src/server/ssr-entry.ts +3 -1
  205. package/src/server/ssr-render.ts +3 -0
  206. package/src/server/tree-builder.ts +84 -48
  207. package/src/server/types.ts +1 -3
  208. package/src/server/version-skew.ts +104 -0
  209. package/src/shims/navigation-client.ts +1 -1
  210. package/src/shims/navigation.ts +1 -1
  211. package/src/utils/state-machine.ts +111 -0
  212. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  213. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  214. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  215. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  216. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  217. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  218. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  219. package/dist/client/error-boundary.js.map +0 -1
  220. package/dist/cookies/index.js.map +0 -1
  221. package/dist/plugins/dynamic-transform.d.ts +0 -72
  222. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  223. package/dist/search-params/analyze.d.ts +0 -54
  224. package/dist/search-params/analyze.d.ts.map +0 -1
  225. package/dist/search-params/builtin-codecs.d.ts +0 -105
  226. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  227. package/dist/search-params/create.d.ts +0 -106
  228. package/dist/search-params/create.d.ts.map +0 -1
  229. package/dist/search-params/index.js.map +0 -1
  230. package/dist/server/prerender.d.ts +0 -77
  231. package/dist/server/prerender.d.ts.map +0 -1
  232. package/src/plugins/dynamic-transform.ts +0 -161
  233. package/src/search-params/analyze.ts +0 -192
  234. package/src/search-params/builtin-codecs.ts +0 -228
  235. package/src/search-params/create.ts +0 -321
  236. package/src/server/prerender.ts +0 -139
@@ -26,6 +26,119 @@ import type { ManifestSegmentNode } from './route-matcher.js';
26
26
 
27
27
  type CreateElementFn = (...args: unknown[]) => React.ReactElement;
28
28
 
29
+ // ─── Module Loading Helpers ─────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Load a module and extract its default export as a component function.
33
+ * Returns undefined if no default export exists.
34
+ */
35
+ async function loadComponent(loader: {
36
+ load: () => Promise<unknown>;
37
+ }): Promise<((...args: unknown[]) => unknown) | undefined> {
38
+ const mod = (await loader.load()) as Record<string, unknown>;
39
+ return mod.default as ((...args: unknown[]) => unknown) | undefined;
40
+ }
41
+
42
+ /**
43
+ * Load and render the default.tsx fallback for a slot node.
44
+ * Returns null if the slot has no default.tsx or it has no default export.
45
+ */
46
+ async function renderDefaultFallback(
47
+ slotNode: ManifestSegmentNode,
48
+ paramsPromise: Promise<Record<string, string | string[]>>,
49
+ h: CreateElementFn
50
+ ): Promise<React.ReactElement | null> {
51
+ if (!slotNode.default) return null;
52
+ const DefaultComp = await loadComponent(slotNode.default);
53
+ if (!DefaultComp) return null;
54
+ return h(DefaultComp, { params: paramsPromise });
55
+ }
56
+
57
+ // ─── Segment Tree Matching ──────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Find a matching child node for a URL segment name.
61
+ *
62
+ * Tries matches in priority order:
63
+ * 1. Static segment (exact name match)
64
+ * 2. Dynamic segment ([param])
65
+ * 3. Catch-all or optional-catch-all ([...param] / [[...param]])
66
+ * 4. Group children (transparent wrappers)
67
+ *
68
+ * Returns `{ node, consumesRest }` where `consumesRest` is true for catch-all
69
+ * segments that consume all remaining URL parts.
70
+ */
71
+ function findMatchingChild(
72
+ children: ManifestSegmentNode[],
73
+ segmentName: string
74
+ ): { node: ManifestSegmentNode; consumesRest: boolean } | null {
75
+ // 1. Static match
76
+ for (const child of children) {
77
+ if (child.segmentType === 'static' && child.segmentName === segmentName) {
78
+ return { node: child, consumesRest: false };
79
+ }
80
+ }
81
+
82
+ // 2. Dynamic match
83
+ for (const child of children) {
84
+ if (child.segmentType === 'dynamic') {
85
+ return { node: child, consumesRest: false };
86
+ }
87
+ }
88
+
89
+ // 3. Catch-all match — consumes all remaining segments
90
+ for (const child of children) {
91
+ if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
92
+ return { node: child, consumesRest: true };
93
+ }
94
+ }
95
+
96
+ // 4. Group children (transparent)
97
+ for (const child of children) {
98
+ if (child.segmentType === 'group') {
99
+ for (const groupChild of child.children ?? []) {
100
+ if (groupChild.segmentName === segmentName) {
101
+ return { node: groupChild, consumesRest: false };
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Walk a segment tree from `startNode`, matching each part in `parts` against
112
+ * child nodes. Returns the chain of matched nodes (including startNode) and the
113
+ * final node, or null if any part fails to match.
114
+ */
115
+ function walkSegmentTree(
116
+ startNode: ManifestSegmentNode,
117
+ parts: { segmentName: string }[] | string[],
118
+ initialChain: ManifestSegmentNode[] = [startNode]
119
+ ): { chain: ManifestSegmentNode[]; leaf: ManifestSegmentNode } | null {
120
+ const chain = [...initialChain];
121
+ let currentNode = startNode;
122
+
123
+ for (const part of parts) {
124
+ const segName = typeof part === 'string' ? part : part.segmentName;
125
+ const directChildren = currentNode.children ?? [];
126
+ const match = findMatchingChild(directChildren, segName);
127
+
128
+ if (!match) return null;
129
+
130
+ chain.push(match.node);
131
+ currentNode = match.node;
132
+
133
+ // Catch-all segments consume all remaining parts
134
+ if (match.consumesRest) break;
135
+ }
136
+
137
+ return { chain, leaf: currentNode };
138
+ }
139
+
140
+ // ─── Slot Element Resolution ────────────────────────────────────────────────
141
+
29
142
  /**
30
143
  * Resolve the element for a parallel slot.
31
144
  *
@@ -54,36 +167,26 @@ export async function resolveSlotElement(
54
167
  : findSlotMatch(slotNode, match);
55
168
 
56
169
  if (slotMatch) {
57
- const mod = (await slotMatch.page.load()) as Record<string, unknown>;
58
- if (mod.default) {
59
- const SlotPage = mod.default as (...args: unknown[]) => unknown;
60
-
170
+ const SlotPage = await loadComponent(slotMatch.page);
171
+ if (SlotPage) {
61
172
  // Load default.tsx fallback for notFound() handling in the slot page.
62
173
  // When a slot page calls notFound() or deny(), it should gracefully
63
174
  // degrade to default.tsx or null — not crash the page. This matches
64
175
  // Next.js behavior. See design/02-rendering-pipeline.md
65
176
  // §"Slot Access Failure = Graceful Degradation"
66
- let denyFallback: React.ReactElement | null = null;
67
- if (slotNode.default) {
68
- const defaultMod = (await slotNode.default.load()) as Record<string, unknown>;
69
- const DefaultComp = defaultMod.default as ((...args: unknown[]) => unknown) | undefined;
70
- if (DefaultComp) {
71
- denyFallback = h(DefaultComp, { params: paramsPromise, searchParams: {} });
72
- }
73
- }
177
+ const denyFallback = await renderDefaultFallback(slotNode, paramsPromise, h);
74
178
 
75
179
  // Wrap the slot page to catch DenySignal (from notFound() or deny())
76
180
  // at the component level. This prevents the signal from reaching the
77
181
  // RSC onError callback and being tracked as a page-level denial, which
78
182
  // would cause the pipeline to replace the entire successful SSR response
79
183
  // with a deny page. Instead, the slot gracefully degrades.
80
- const denyFallbackCapture = denyFallback;
81
184
  const SafeSlotPage = async (props: Record<string, unknown>) => {
82
185
  try {
83
186
  return await (SlotPage as (props: Record<string, unknown>) => unknown)(props);
84
187
  } catch (error) {
85
188
  if (error instanceof DenySignal) {
86
- return denyFallbackCapture;
189
+ return denyFallback;
87
190
  }
88
191
  throw error;
89
192
  }
@@ -91,88 +194,21 @@ export async function resolveSlotElement(
91
194
 
92
195
  let element: React.ReactElement = h(SafeSlotPage, {
93
196
  params: paramsPromise,
94
- searchParams: {},
95
197
  });
96
198
 
97
199
  // Wrap with error boundaries and layouts from intermediate slot segments
98
200
  // (everything between slot root and leaf). Process innermost-first, same
99
201
  // order as route-element-builder.ts handles main segments. The slot root
100
202
  // (index 0) is handled separately after the access gate below.
101
- for (let i = slotMatch.chain.length - 1; i > 0; i--) {
102
- const seg = slotMatch.chain[i];
103
-
104
- // Error boundaries from this segment
105
- element = await wrapSegmentWithErrorBoundaries(seg, element, h);
106
-
107
- // Layout from this segment
108
- if (seg.layout) {
109
- const layoutMod = (await seg.layout.load()) as Record<string, unknown>;
110
- if (layoutMod.default) {
111
- const Layout = layoutMod.default as (...args: unknown[]) => unknown;
112
- element = h(Layout, {
113
- params: paramsPromise,
114
- searchParams: {},
115
- children: element,
116
- });
117
- }
118
- }
119
- }
203
+ element = await wrapWithIntermediateSegments(slotMatch.chain, element, paramsPromise, h);
120
204
 
121
205
  // Wrap in SlotAccessGate if slot root has access.ts.
122
206
  // On denial: denied.tsx → default.tsx → null (graceful degradation).
123
207
  // See design/04-authorization.md §"Slot-Level Auth".
124
- if (slotNode.access) {
125
- const accessMod = (await slotNode.access.load()) as Record<string, unknown>;
126
- const accessFn = accessMod.default as
127
- | ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
128
- | undefined;
129
- if (accessFn) {
130
- // Load denied.tsx fallback
131
- let deniedFallback: React.ReactElement | null = null;
132
- if (slotNode.denied) {
133
- const deniedMod = (await slotNode.denied.load()) as Record<string, unknown>;
134
- const DeniedComponent = deniedMod.default as
135
- | ((...args: unknown[]) => unknown)
136
- | undefined;
137
- if (DeniedComponent) {
138
- deniedFallback = h(DeniedComponent, {});
139
- }
140
- }
141
-
142
- // Load default.tsx fallback
143
- let defaultFallback: React.ReactElement | null = null;
144
- if (slotNode.default) {
145
- const defaultMod = (await slotNode.default.load()) as Record<string, unknown>;
146
- const DefaultComp = defaultMod.default as ((...args: unknown[]) => unknown) | undefined;
147
- if (DefaultComp) {
148
- defaultFallback = h(DefaultComp, { params: paramsPromise, searchParams: {} });
149
- }
150
- }
151
-
152
- const params = await paramsPromise;
153
- element = h(SlotAccessGate, {
154
- accessFn,
155
- params,
156
- searchParams: {},
157
- deniedFallback,
158
- defaultFallback,
159
- children: element,
160
- });
161
- }
162
- }
208
+ element = await wrapWithAccessGate(slotNode, element, paramsPromise, h);
163
209
 
164
210
  // Wrap with slot root's layout (outermost, outside access gate)
165
- if (slotNode.layout) {
166
- const layoutMod = (await slotNode.layout.load()) as Record<string, unknown>;
167
- if (layoutMod.default) {
168
- const Layout = layoutMod.default as (...args: unknown[]) => unknown;
169
- element = h(Layout, {
170
- params: paramsPromise,
171
- searchParams: {},
172
- children: element,
173
- });
174
- }
175
- }
211
+ element = await wrapWithLayout(slotNode, element, paramsPromise, h);
176
212
 
177
213
  // Wrap with slot root's error boundaries (outermost)
178
214
  element = await wrapSegmentWithErrorBoundaries(slotNode, element, h);
@@ -195,18 +231,86 @@ export async function resolveSlotElement(
195
231
  }
196
232
 
197
233
  // No matching page — render default.tsx fallback
198
- if (slotNode.default) {
199
- const mod = (await slotNode.default.load()) as Record<string, unknown>;
200
- if (mod.default) {
201
- const DefaultComponent = mod.default as (...args: unknown[]) => unknown;
202
- return h(DefaultComponent, { params: paramsPromise, searchParams: {} });
203
- }
234
+ return renderDefaultFallback(slotNode, paramsPromise, h);
235
+ }
236
+
237
+ // ─── Element Wrapping Helpers ───────────────────────────────────────────────
238
+
239
+ /**
240
+ * Wrap an element with error boundaries and layouts from intermediate
241
+ * slot segments (indices 1..n, skipping the slot root at index 0).
242
+ * Processes innermost-first to match route-element-builder.ts ordering.
243
+ */
244
+ async function wrapWithIntermediateSegments(
245
+ chain: ManifestSegmentNode[],
246
+ element: React.ReactElement,
247
+ paramsPromise: Promise<Record<string, string | string[]>>,
248
+ h: CreateElementFn
249
+ ): Promise<React.ReactElement> {
250
+ for (let i = chain.length - 1; i > 0; i--) {
251
+ const seg = chain[i];
252
+ element = await wrapSegmentWithErrorBoundaries(seg, element, h);
253
+ element = await wrapWithLayout(seg, element, paramsPromise, h);
204
254
  }
255
+ return element;
256
+ }
205
257
 
206
- // No page and no default — slot renders nothing
207
- return null;
258
+ /**
259
+ * Wrap an element with a segment's layout component, if present.
260
+ */
261
+ async function wrapWithLayout(
262
+ node: ManifestSegmentNode,
263
+ element: React.ReactElement,
264
+ paramsPromise: Promise<Record<string, string | string[]>>,
265
+ h: CreateElementFn
266
+ ): Promise<React.ReactElement> {
267
+ if (!node.layout) return element;
268
+ const Layout = await loadComponent(node.layout);
269
+ if (!Layout) return element;
270
+ return h(Layout, { params: paramsPromise, children: element });
271
+ }
272
+
273
+ /**
274
+ * Wrap an element with a SlotAccessGate if the node has access.ts.
275
+ * On denial: denied.tsx → default.tsx → null (graceful degradation).
276
+ */
277
+ async function wrapWithAccessGate(
278
+ slotNode: ManifestSegmentNode,
279
+ element: React.ReactElement,
280
+ paramsPromise: Promise<Record<string, string | string[]>>,
281
+ h: CreateElementFn
282
+ ): Promise<React.ReactElement> {
283
+ if (!slotNode.access) return element;
284
+
285
+ const accessFn = await loadComponent(slotNode.access);
286
+ if (!accessFn) return element;
287
+
288
+ // Pass the component (not pre-built element) so SlotAccessGate can
289
+ // forward DenySignal.data as dangerouslyPassData dynamically. See TIM-488.
290
+ let DeniedComponent: ((...args: unknown[]) => unknown) | null = null;
291
+ if (slotNode.denied) {
292
+ DeniedComponent = (await loadComponent(slotNode.denied)) ?? null;
293
+ }
294
+
295
+ // Extract slot name from the directory name (strip @ prefix)
296
+ const slotName = slotNode.segmentName?.replace(/^@/, '') ?? '';
297
+
298
+ const defaultFallback = await renderDefaultFallback(slotNode, paramsPromise, h);
299
+ const params = await paramsPromise;
300
+
301
+ return h(SlotAccessGate, {
302
+ accessFn,
303
+ params,
304
+ DeniedComponent,
305
+ slotName,
306
+ createElement: h,
307
+ defaultFallback,
308
+ children: element,
309
+ });
208
310
  }
209
311
 
312
+ // ─── Slot Matching ──────────────────────────────────────────────────────────
313
+
210
314
  /** Result of matching a slot's sub-tree against the current route. */
211
315
  interface SlotMatchResult {
212
316
  /** The page file at the matched leaf. */
@@ -267,74 +371,10 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
267
371
  return null;
268
372
  }
269
373
 
270
- // Walk the slot's children to match remaining URL segments.
271
- // Track the chain so we can apply error boundaries and layouts.
272
- const chain: ManifestSegmentNode[] = [slotNode];
273
- let currentNode = slotNode;
274
- for (const seg of remainingSegments) {
275
- const childName = seg.segmentName;
276
- const directChildren = currentNode.children ?? [];
277
-
278
- let found: ManifestSegmentNode | null = null;
279
- for (const child of directChildren) {
280
- // Exact static match
281
- if (child.segmentType === 'static' && child.segmentName === childName) {
282
- found = child;
283
- break;
284
- }
285
- }
286
-
287
- // Try dynamic segments if no static match
288
- if (!found) {
289
- for (const child of directChildren) {
290
- if (child.segmentType === 'dynamic') {
291
- found = child;
292
- break;
293
- }
294
- }
295
- }
296
-
297
- // Try catch-all segments — these consume ALL remaining URL segments,
298
- // so we break out of the outer loop immediately.
299
- if (!found) {
300
- for (const child of directChildren) {
301
- if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
302
- found = child;
303
- break;
304
- }
305
- }
306
- if (found) {
307
- chain.push(found);
308
- currentNode = found;
309
- break;
310
- }
311
- }
312
-
313
- // Try group children (transparent)
314
- if (!found) {
315
- for (const child of directChildren) {
316
- if (child.segmentType === 'group') {
317
- for (const groupChild of child.children ?? []) {
318
- if (groupChild.segmentName === childName) {
319
- found = groupChild;
320
- break;
321
- }
322
- }
323
- if (found) break;
324
- }
325
- }
326
- }
327
-
328
- if (!found) {
329
- // No matching child in slot tree — slot doesn't match this URL
330
- return null;
331
- }
332
- chain.push(found);
333
- currentNode = found;
334
- }
335
-
336
- if (currentNode.page) {
337
- return { page: currentNode.page, chain };
374
+ // Walk the slot's children to match remaining URL segments
375
+ const result = walkSegmentTree(slotNode, remainingSegments);
376
+ if (result && result.leaf.page) {
377
+ return { page: result.leaf.page, chain: result.chain };
338
378
  }
339
379
  return null;
340
380
  }
@@ -374,59 +414,17 @@ function findInterceptingMatch(
374
414
 
375
415
  // Walk the intercepting child's sub-tree to match remaining target parts
376
416
  const remaining = targetParts.slice(matchIdx + 1);
377
- const chain: ManifestSegmentNode[] = [slotNode, child];
378
417
 
379
418
  if (remaining.length === 0) {
380
419
  if (child.page) {
381
- return { page: child.page, chain };
420
+ return { page: child.page, chain: [slotNode, child] };
382
421
  }
383
422
  continue;
384
423
  }
385
424
 
386
- let currentNode = child;
387
- let matched = true;
388
- for (const part of remaining) {
389
- const children = currentNode.children ?? [];
390
- let found: ManifestSegmentNode | null = null;
391
-
392
- // Static match
393
- for (const c of children) {
394
- if (c.segmentType === 'static' && c.segmentName === part) {
395
- found = c;
396
- break;
397
- }
398
- }
399
-
400
- // Dynamic match
401
- if (!found) {
402
- for (const c of children) {
403
- if (c.segmentType === 'dynamic') {
404
- found = c;
405
- break;
406
- }
407
- }
408
- }
409
-
410
- // Catch-all match
411
- if (!found) {
412
- for (const c of children) {
413
- if (c.segmentType === 'catch-all' || c.segmentType === 'optional-catch-all') {
414
- found = c;
415
- break;
416
- }
417
- }
418
- }
419
-
420
- if (!found) {
421
- matched = false;
422
- break;
423
- }
424
- chain.push(found);
425
- currentNode = found;
426
- }
427
-
428
- if (matched && currentNode.page) {
429
- return { page: currentNode.page, chain };
425
+ const result = walkSegmentTree(child, remaining, [slotNode, child]);
426
+ if (result && result.leaf.page) {
427
+ return { page: result.leaf.page, chain: result.chain };
430
428
  }
431
429
  }
432
430
 
@@ -195,7 +195,7 @@ export async function handleSsr(
195
195
  // Client hooks read from getSsrData() which delegates to this
196
196
  // ALS store via the registered provider.
197
197
  return ssrDataAls.run(ssrData, async () => {
198
- // Also set the module-level currentParams for useParams().
198
+ // Also set the module-level currentParams for useSegmentParams().
199
199
  // useParams reads from getSsrData() during SSR (ALS-backed),
200
200
  // but setCurrentParams is kept for the client-side path where
201
201
  // the segment router updates params on navigation.
@@ -262,6 +262,8 @@ export async function handleSsr(
262
262
  }
263
263
  const _renderEnd = performance.now();
264
264
 
265
+ // React 19.3+ emits <!DOCTYPE html> automatically when the root
266
+ // element is <html>, so no framework-level doctype prepend needed.
265
267
  const errorHandler = createNodeErrorHandler(navContext.signal);
266
268
  const headInjector = createNodeHeadInjector(navContext.headHtml);
267
269
  const flightInjector = createNodeFlightInjector(navContext.rscStream);
@@ -289,6 +289,9 @@ function isAbortError(error: unknown): boolean {
289
289
  * Build a Response from the SSR HTML stream with the correct
290
290
  * status code and headers from the navigation context.
291
291
  *
292
+ * React 19.3+ automatically emits `<!DOCTYPE html>` when the root
293
+ * element is `<html>`, so no framework-level doctype prepending is needed.
294
+ *
292
295
  * Sets content-type to text/html if not already set by middleware.
293
296
  */
294
297
  export function buildSsrResponse(