@timber-js/app 0.1.11 → 0.1.13

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 (35) hide show
  1. package/dist/_chunks/ssr-data-BgSwMbN9.js +38 -0
  2. package/dist/_chunks/ssr-data-BgSwMbN9.js.map +1 -0
  3. package/dist/_chunks/{use-cookie-HcvNlW4L.js → use-cookie-D2cZu0jK.js} +3 -37
  4. package/dist/_chunks/use-cookie-D2cZu0jK.js.map +1 -0
  5. package/dist/client/error-boundary.d.ts +7 -0
  6. package/dist/client/error-boundary.d.ts.map +1 -1
  7. package/dist/client/error-boundary.js +5 -0
  8. package/dist/client/error-boundary.js.map +1 -1
  9. package/dist/client/index.js +15 -8
  10. package/dist/client/index.js.map +1 -1
  11. package/dist/client/ssr-data.d.ts +9 -0
  12. package/dist/client/ssr-data.d.ts.map +1 -1
  13. package/dist/client/use-router.d.ts.map +1 -1
  14. package/dist/cookies/index.js +1 -1
  15. package/dist/index.js +5 -11
  16. package/dist/index.js.map +1 -1
  17. package/dist/plugins/shims.d.ts.map +1 -1
  18. package/dist/server/index.js +9 -10
  19. package/dist/server/index.js.map +1 -1
  20. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  21. package/dist/server/slot-resolver.d.ts.map +1 -1
  22. package/dist/server/ssr-entry.d.ts +7 -0
  23. package/dist/server/ssr-entry.d.ts.map +1 -1
  24. package/dist/server/tree-builder.d.ts +10 -0
  25. package/dist/server/tree-builder.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/client/error-boundary.tsx +23 -0
  28. package/src/client/ssr-data.ts +7 -0
  29. package/src/client/use-router.ts +15 -3
  30. package/src/plugins/shims.ts +8 -14
  31. package/src/server/rsc-entry/index.ts +10 -3
  32. package/src/server/slot-resolver.ts +24 -1
  33. package/src/server/ssr-entry.ts +8 -0
  34. package/src/server/tree-builder.ts +35 -10
  35. package/dist/_chunks/use-cookie-HcvNlW4L.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AA8qBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BAxjBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA0jBhD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AAqrBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BA/jBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAikBhD,wBAAiE"}
@@ -1 +1 @@
1
- {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAElE;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,EACzD,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAiKpC"}
1
+ {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAElE;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,EACzD,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAkKpC"}
@@ -46,6 +46,13 @@ export interface NavContext {
46
46
  /** Request cookies as name→value pairs. Used by useCookie() during SSR
47
47
  * to return correct cookie values before hydration. */
48
48
  cookies?: Map<string, string>;
49
+ /**
50
+ * Mutable flag: set by TimberErrorBoundary during SSR when it catches
51
+ * a DenySignal (via digest). This tells the RSC entry that the denial
52
+ * was contained by a slot error boundary and should NOT be promoted
53
+ * to a page-level deny. See LOCAL-298.
54
+ */
55
+ _denyHandledByBoundary?: boolean;
49
56
  }
50
57
  /**
51
58
  * Handle SSR: decode an RSC stream and render it to hydration-ready HTML.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CA2EnB;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA6BH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CA4EnB;AAED,eAAe,SAAS,CAAC"}
@@ -40,6 +40,16 @@ export interface TreeBuilderConfig {
40
40
  loadModule: ModuleLoader;
41
41
  /** React.createElement or equivalent. */
42
42
  createElement: CreateElement;
43
+ /**
44
+ * Error boundary component for wrapping segments.
45
+ *
46
+ * This is injected by the caller rather than imported directly to avoid
47
+ * pulling 'use client' code into the server barrel (@timber-js/app/server).
48
+ * In the RSC environment, the RSC plugin transforms this import to a
49
+ * client reference proxy — the caller handles the import so the server
50
+ * barrel stays free of client dependencies.
51
+ */
52
+ errorBoundaryComponent?: unknown;
43
53
  }
44
54
  /**
45
55
  * Framework-injected access gate component.
@@ -1 +1 @@
1
- {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { Component, createElement, type ReactNode } from 'react';
22
+ import { getSsrData } from './ssr-data.js';
22
23
 
23
24
  // ─── Page Unload Detection ───────────────────────────────────────────────────
24
25
  // Track whether the page is being unloaded (user refreshed or navigated away).
@@ -70,6 +71,13 @@ export interface TimberErrorBoundaryProps {
70
71
  * 400 = any 4xx, 500 = any 5xx, specific number = exact match.
71
72
  */
72
73
  status?: number;
74
+ /**
75
+ * When true, catching a DenySignal sets _denyHandledByBoundary on the
76
+ * nav context to prevent page-level deny promotion. Only slot catch-all
77
+ * boundaries should set this — segment boundaries (403.tsx, 4xx.tsx,
78
+ * error.tsx) must NOT, otherwise normal page denies get swallowed.
79
+ */
80
+ isSlotBoundary?: boolean;
73
81
  children: ReactNode;
74
82
  }
75
83
 
@@ -97,6 +105,7 @@ export class TimberErrorBoundary extends Component<
97
105
  if (_isUnloading) {
98
106
  return { hasError: false, error: null };
99
107
  }
108
+
100
109
  return { hasError: true, error };
101
110
  }
102
111
 
@@ -139,6 +148,20 @@ export class TimberErrorBoundary extends Component<
139
148
  }
140
149
  }
141
150
 
151
+ // Report DenySignal handling to prevent page-level promotion — but only
152
+ // for slot boundaries. Segment boundaries (403.tsx, 4xx.tsx, error.tsx)
153
+ // must NOT set this flag, otherwise normal page/hold-window denies get
154
+ // swallowed as 200 with boundary HTML instead of the intended 4xx.
155
+ // Runs here in render() (not getDerivedStateFromError) so the status
156
+ // filter has already been applied — non-matching boundaries re-threw above.
157
+ // See LOCAL-298.
158
+ if (parsed?.type === 'deny' && this.props.isSlotBoundary) {
159
+ const ssrData = getSsrData();
160
+ if (ssrData?._navContext) {
161
+ ssrData._navContext._denyHandledByBoundary = true;
162
+ }
163
+ }
164
+
142
165
  // Render the fallback component with the right props shape.
143
166
  if (parsed?.type === 'deny') {
144
167
  return createElement(this.props.fallbackComponent as never, {
@@ -30,6 +30,13 @@ export interface SsrData {
30
30
  cookies: Map<string, string>;
31
31
  /** The request's route params (e.g. { id: '123' }) */
32
32
  params: Record<string, string | string[]>;
33
+ /**
34
+ * Mutable reference to NavContext for error boundary → RSC communication.
35
+ * When TimberErrorBoundary catches a DenySignal, it sets
36
+ * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry
37
+ * from promoting the denial to page-level. See LOCAL-298.
38
+ */
39
+ _navContext?: { _denyHandledByBoundary?: boolean };
33
40
  }
34
41
 
35
42
  // ─── ALS-Backed Provider ─────────────────────────────────────────
@@ -9,6 +9,7 @@
9
9
  * AppRouterInstance shape that ecosystem libraries expect.
10
10
  */
11
11
 
12
+ import { startTransition } from 'react';
12
13
  import { getRouter } from './router-ref.js';
13
14
 
14
15
  export interface AppRouterInstance {
@@ -56,13 +57,24 @@ export function useRouter(): AppRouterInstance {
56
57
 
57
58
  return {
58
59
  push(href: string, options?: { scroll?: boolean }) {
59
- void router.navigate(href, { scroll: options?.scroll });
60
+ // Wrap in startTransition so React 19 tracks the async navigation.
61
+ // React 19's startTransition accepts async callbacks — it keeps
62
+ // isPending=true until the returned promise resolves. This means
63
+ // useTransition's isPending reflects the full RSC fetch + render
64
+ // lifecycle when wrapping router.push() in startTransition.
65
+ startTransition(async () => {
66
+ await router.navigate(href, { scroll: options?.scroll });
67
+ });
60
68
  },
61
69
  replace(href: string, options?: { scroll?: boolean }) {
62
- void router.navigate(href, { scroll: options?.scroll, replace: true });
70
+ startTransition(async () => {
71
+ await router.navigate(href, { scroll: options?.scroll, replace: true });
72
+ });
63
73
  },
64
74
  refresh() {
65
- void router.refresh();
75
+ startTransition(async () => {
76
+ await router.refresh();
77
+ });
66
78
  },
67
79
  back() {
68
80
  window.history.back();
@@ -173,21 +173,15 @@ export function timberShims(_ctx: PluginContext): Plugin {
173
173
  return 'export {};';
174
174
  }
175
175
 
176
- // Stub for @timber-js/app/server in client environment.
177
- // Exports throw-on-call stubs so named imports resolve but
178
- // calling them gives a clear error instead of crashing the bundle.
176
+ // Error module for @timber-js/app/server in client environment.
177
+ // Server modules must never be bundled into the browser — if this
178
+ // module is reached, there is a broken import chain that needs fixing.
179
179
  if (id === '\0timber:server-empty') {
180
- return `
181
- const stub = (name) => () => { throw new Error(name + "() is a server-only function and cannot be called in client code."); };
182
- export const headers = stub("headers");
183
- export const cookies = stub("cookies");
184
- export const notFound = stub("notFound");
185
- export const redirect = stub("redirect");
186
- export const permanentRedirect = stub("permanentRedirect");
187
- export const deny = stub("deny");
188
- export const searchParams = stub("searchParams");
189
- export const RedirectType = { push: "push", replace: "replace" };
190
- `;
180
+ return `throw new Error(
181
+ "[timber] @timber-js/app/server was imported from client code. " +
182
+ "Server modules (headers, cookies, redirect, deny, etc.) cannot be used in client components. " +
183
+ "If you need these APIs, move the import to a server component or middleware."
184
+ );`;
191
185
  }
192
186
  },
193
187
  };
@@ -661,10 +661,17 @@ async function renderRoute(
661
661
  // Helper: check if render-phase signals were captured and return the
662
662
  // appropriate HTTP response. Used after both successful SSR (signal
663
663
  // promotion from Suspense) and failed SSR (signal outside Suspense).
664
- function checkCapturedSignals(): Response | Promise<Response> | null {
664
+ //
665
+ // When `skipHandledDeny` is true (SSR success path), skip DenySignal
666
+ // promotion if the denial was already handled by a TimberErrorBoundary
667
+ // (e.g., slot error boundary). The boundary sets navContext._denyHandledByBoundary
668
+ // during SSR rendering. See LOCAL-298.
669
+ function checkCapturedSignals(
670
+ skipHandledDeny = false
671
+ ): Response | Promise<Response> | null {
665
672
  const sig = redirectSignal as RedirectSignal | null;
666
673
  if (sig) return buildRedirectResponse(_req, sig, responseHeaders);
667
- if (denySignal) {
674
+ if (denySignal && !(skipHandledDeny && navContext._denyHandledByBoundary)) {
668
675
  return renderDenyPage(
669
676
  denySignal,
670
677
  segments,
@@ -703,7 +710,7 @@ async function renderRoute(
703
710
  // See design/05-streaming.md §"deferSuspenseFor and the Hold Window"
704
711
  await new Promise<void>((r) => setTimeout(r, 0));
705
712
 
706
- const promoted = checkCapturedSignals();
713
+ const promoted = checkCapturedSignals(/* skipHandledDeny */ true);
707
714
  if (promoted) {
708
715
  ssrResponse.body?.cancel();
709
716
  return promoted;
@@ -186,6 +186,7 @@ export async function resolveSlotElement(
186
186
  // See design/02-rendering-pipeline.md §"Slot Access Failure = Graceful Degradation"
187
187
  element = h(TimberErrorBoundary, {
188
188
  fallbackComponent: SlotErrorFallback,
189
+ isSlotBoundary: true,
189
190
  children: element,
190
191
  });
191
192
 
@@ -257,6 +258,12 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
257
258
  if (slotNode.page) {
258
259
  return { page: slotNode.page, chain: [slotNode] };
259
260
  }
261
+ // Check for optional-catch-all child — [[...slug]] matches zero segments
262
+ for (const child of slotNode.children ?? []) {
263
+ if (child.segmentType === 'optional-catch-all' && child.page) {
264
+ return { page: child.page, chain: [slotNode, child] };
265
+ }
266
+ }
260
267
  return null;
261
268
  }
262
269
 
@@ -280,11 +287,27 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
280
287
  // Try dynamic segments if no static match
281
288
  if (!found) {
282
289
  for (const child of directChildren) {
283
- if (child.segmentType === 'dynamic' || child.segmentType === 'catch-all') {
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') {
284
302
  found = child;
285
303
  break;
286
304
  }
287
305
  }
306
+ if (found) {
307
+ chain.push(found);
308
+ currentNode = found;
309
+ break;
310
+ }
288
311
  }
289
312
 
290
313
  // Try group children (transparent)
@@ -74,6 +74,13 @@ export interface NavContext {
74
74
  /** Request cookies as name→value pairs. Used by useCookie() during SSR
75
75
  * to return correct cookie values before hydration. */
76
76
  cookies?: Map<string, string>;
77
+ /**
78
+ * Mutable flag: set by TimberErrorBoundary during SSR when it catches
79
+ * a DenySignal (via digest). This tells the RSC entry that the denial
80
+ * was contained by a slot error boundary and should NOT be promoted
81
+ * to a page-level deny. See LOCAL-298.
82
+ */
83
+ _denyHandledByBoundary?: boolean;
77
84
  }
78
85
 
79
86
  /**
@@ -111,6 +118,7 @@ export async function handleSsr(
111
118
  searchParams: navContext.searchParams,
112
119
  cookies: navContext.cookies ?? new Map(),
113
120
  params: navContext.params,
121
+ _navContext: navContext,
114
122
  };
115
123
 
116
124
  // Run the entire render inside the SSR data ALS scope.
@@ -11,7 +11,6 @@
11
11
  */
12
12
 
13
13
  import type { SegmentNode, RouteFile } from '#/routing/types.js';
14
- import { TimberErrorBoundary } from '#/client/error-boundary.js';
15
14
 
16
15
  // ─── Types ───────────────────────────────────────────────────────────────────
17
16
 
@@ -55,6 +54,16 @@ export interface TreeBuilderConfig {
55
54
  loadModule: ModuleLoader;
56
55
  /** React.createElement or equivalent. */
57
56
  createElement: CreateElement;
57
+ /**
58
+ * Error boundary component for wrapping segments.
59
+ *
60
+ * This is injected by the caller rather than imported directly to avoid
61
+ * pulling 'use client' code into the server barrel (@timber-js/app/server).
62
+ * In the RSC environment, the RSC plugin transforms this import to a
63
+ * client reference proxy — the caller handles the import so the server
64
+ * barrel stays free of client dependencies.
65
+ */
66
+ errorBoundaryComponent?: unknown;
58
67
  }
59
68
 
60
69
  // ─── Component wrappers ──────────────────────────────────────────────────────
@@ -134,7 +143,8 @@ export interface TreeBuildResult {
134
143
  * Parallel slots are resolved at each layout level and composed as named props.
135
144
  */
136
145
  export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
137
- const { segments, params, searchParams, loadModule, createElement } = config;
146
+ const { segments, params, searchParams, loadModule, createElement, errorBoundaryComponent } =
147
+ config;
138
148
 
139
149
  if (segments.length === 0) {
140
150
  throw new Error('[timber] buildElementTree: empty segment chain');
@@ -166,7 +176,13 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
166
176
  const segment = segments[i];
167
177
 
168
178
  // Wrap in error boundaries (status-code files + error.tsx)
169
- element = await wrapWithErrorBoundaries(segment, element, loadModule, createElement);
179
+ element = await wrapWithErrorBoundaries(
180
+ segment,
181
+ element,
182
+ loadModule,
183
+ createElement,
184
+ errorBoundaryComponent
185
+ );
170
186
 
171
187
  // Wrap in AccessGate if segment has access.ts
172
188
  if (segment.access) {
@@ -198,7 +214,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
198
214
  params,
199
215
  searchParams,
200
216
  loadModule,
201
- createElement
217
+ createElement,
218
+ errorBoundaryComponent
202
219
  );
203
220
  }
204
221
  }
@@ -229,7 +246,8 @@ async function buildSlotElement(
229
246
  params: Record<string, string | string[]>,
230
247
  searchParams: unknown,
231
248
  loadModule: ModuleLoader,
232
- createElement: CreateElement
249
+ createElement: CreateElement,
250
+ errorBoundaryComponent: unknown
233
251
  ): Promise<ReactElement> {
234
252
  // Load slot page
235
253
  const pageModule = slotNode.page ? await loadModule(slotNode.page) : null;
@@ -249,7 +267,13 @@ async function buildSlotElement(
249
267
  let element: ReactElement = createElement(PageComponent, { params, searchParams });
250
268
 
251
269
  // Wrap in error boundaries
252
- element = await wrapWithErrorBoundaries(slotNode, element, loadModule, createElement);
270
+ element = await wrapWithErrorBoundaries(
271
+ slotNode,
272
+ element,
273
+ loadModule,
274
+ createElement,
275
+ errorBoundaryComponent
276
+ );
253
277
 
254
278
  // Wrap in SlotAccessGate if slot has access.ts
255
279
  if (slotNode.access) {
@@ -301,7 +325,8 @@ async function wrapWithErrorBoundaries(
301
325
  segment: SegmentNode,
302
326
  element: ReactElement,
303
327
  loadModule: ModuleLoader,
304
- createElement: CreateElement
328
+ createElement: CreateElement,
329
+ errorBoundaryComponent: unknown
305
330
  ): Promise<ReactElement> {
306
331
  // Wrapping is applied inside-out. The last wrap call produces the outermost boundary.
307
332
  // Order: specific status → category → error.tsx (outermost)
@@ -315,7 +340,7 @@ async function wrapWithErrorBoundaries(
315
340
  const mod = await loadModule(file);
316
341
  const Component = mod.default;
317
342
  if (Component) {
318
- element = createElement(TimberErrorBoundary, {
343
+ element = createElement(errorBoundaryComponent, {
319
344
  fallbackComponent: Component,
320
345
  status,
321
346
  children: element,
@@ -331,7 +356,7 @@ async function wrapWithErrorBoundaries(
331
356
  const mod = await loadModule(file);
332
357
  const Component = mod.default;
333
358
  if (Component) {
334
- element = createElement(TimberErrorBoundary, {
359
+ element = createElement(errorBoundaryComponent, {
335
360
  fallbackComponent: Component,
336
361
  status: key === '4xx' ? 400 : 500, // category marker
337
362
  children: element,
@@ -346,7 +371,7 @@ async function wrapWithErrorBoundaries(
346
371
  const errorModule = await loadModule(segment.error);
347
372
  const ErrorComponent = errorModule.default;
348
373
  if (ErrorComponent) {
349
- element = createElement(TimberErrorBoundary, {
374
+ element = createElement(errorBoundaryComponent, {
350
375
  fallbackComponent: ErrorComponent,
351
376
  children: element,
352
377
  } satisfies ErrorBoundaryProps);
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-cookie-HcvNlW4L.js","names":[],"sources":["../../src/client/ssr-data.ts","../../src/client/use-cookie.ts"],"sourcesContent":["/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n */\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n\nlet _ssrDataProvider: (() => SsrData | undefined) | undefined;\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _ssrDataProvider = provider;\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\nlet currentSsrData: SsrData | undefined;\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n currentSsrData = data;\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n currentSsrData = undefined;\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (_ssrDataProvider) {\n return _ssrDataProvider();\n }\n return currentSsrData;\n}\n","/**\n * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nfunction getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nfunction serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n const merged = { ...defaultOptions, ...options };\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n"],"mappings":";;AAwCA,IAAI;AAkBJ,IAAI;;;;;;;AAQJ,SAAgB,WAAW,MAAqB;AAC9C,kBAAiB;;;;;;;;AASnB,SAAgB,eAAqB;AACnC,kBAAiB,KAAA;;;;;;;;;;;AAYnB,SAAgB,aAAkC;AAChD,KAAI,iBACF,QAAO,kBAAkB;AAE3B,QAAO;;;;;;;;;;;;;;ACxDT,IAAM,4BAAY,IAAI,KAA4B;;AAGlD,SAAS,eAAe,MAAkC;AACxD,KAAI,OAAO,aAAa,YAAa,QAAO,KAAA;CAC5C,MAAM,QAAQ,SAAS,OAAO,MAC5B,IAAI,OAAO,gBAAgB,KAAK,QAAQ,uBAAuB,OAAO,GAAG,mBAAmB,CAC7F;AACD,QAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG,KAAA;;;AAIhD,SAAS,iBAAiB,SAAuC;AAC/D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,QAAkB,EAAE;AAC1B,OAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM;AACzC,KAAI,QAAQ,OAAQ,OAAM,KAAK,UAAU,QAAQ,SAAS;AAC1D,KAAI,QAAQ,WAAW,KAAA,EAAW,OAAM,KAAK,WAAW,QAAQ,SAAS;AACzE,KAAI,QAAQ,QAAS,OAAM,KAAK,WAAW,QAAQ,QAAQ,aAAa,GAAG;CAC3E,MAAM,WAAW,QAAQ,YAAY;AACrC,OAAM,KAAK,YAAY,SAAS,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG;AAC9E,KAAI,QAAQ,OAAQ,OAAM,KAAK,SAAS;AACxC,QAAO,OAAO,MAAM,KAAK,KAAK;;;AAIhC,SAAS,OAAO,MAAoB;CAClC,MAAM,OAAO,UAAU,IAAI,KAAK;AAChC,KAAI,KACF,MAAK,MAAM,MAAM,KAAM,KAAI;;;;;;;;;;;;;AAiB/B,SAAgB,UACd,MACA,gBACgF;CAChF,MAAM,aAAa,aAAqC;EACtD,IAAI,OAAO,UAAU,IAAI,KAAK;AAC9B,MAAI,CAAC,MAAM;AACT,0BAAO,IAAI,KAAK;AAChB,aAAU,IAAI,MAAM,KAAK;;AAE3B,OAAK,IAAI,SAAS;AAClB,eAAa;AACX,QAAM,OAAO,SAAS;AACtB,OAAI,KAAM,SAAS,EAAG,WAAU,OAAO,KAAK;;;CAIhD,MAAM,oBAAwC,eAAe,KAAK;CAClE,MAAM,0BAA8C,YAAY,EAAE,QAAQ,IAAI,KAAK;CAEnF,MAAM,QAAQ,qBAAqB,WAAW,aAAa,kBAAkB;CAE7E,MAAM,aAA2B,UAAkB,YAAkC;EACnF,MAAM,SAAS;GAAE,GAAG;GAAgB,GAAG;GAAS;AAChD,WAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,SAAS,GAAG,iBAAiB,OAAO;AACpF,SAAO,KAAK;;CAGd,MAAM,qBAA2B;EAC/B,MAAM,OAAO,gBAAgB,QAAQ;EACrC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,YAAY,GAAG,KAAK,4DAA4D;AACpF,MAAI,OAAQ,cAAa,YAAY;AACrC,WAAS,SAAS;AAClB,SAAO,KAAK;;AAGd,QAAO;EAAC;EAAO;EAAW;EAAa"}