@timber-js/app 0.1.4 → 0.1.5

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 (47) hide show
  1. package/dist/_chunks/{registry-DUIpYD_x.js → registry-BfPM41ri.js} +1 -1
  2. package/dist/_chunks/{registry-DUIpYD_x.js.map → registry-BfPM41ri.js.map} +1 -1
  3. package/dist/_chunks/{request-context-D6XHINkR.js → request-context-BzES06i1.js} +2 -1
  4. package/dist/_chunks/request-context-BzES06i1.js.map +1 -0
  5. package/dist/_chunks/{use-cookie-8ZlA0rr3.js → use-cookie-HcvNlW4L.js} +1 -1
  6. package/dist/_chunks/{use-cookie-8ZlA0rr3.js.map → use-cookie-HcvNlW4L.js.map} +1 -1
  7. package/dist/{_chunks/error-boundary-dj-WO5uq.js → client/error-boundary.js} +4 -2
  8. package/dist/client/error-boundary.js.map +1 -0
  9. package/dist/client/index.js +7 -6
  10. package/dist/client/index.js.map +1 -1
  11. package/dist/client/link.d.ts.map +1 -1
  12. package/dist/client/slot-error-fallback.d.ts +13 -0
  13. package/dist/client/slot-error-fallback.d.ts.map +1 -0
  14. package/dist/cookies/index.js +2 -2
  15. package/dist/index.js +17 -16
  16. package/dist/index.js.map +1 -1
  17. package/dist/plugins/shims.d.ts +5 -0
  18. package/dist/plugins/shims.d.ts.map +1 -1
  19. package/dist/search-params/index.js +1 -1
  20. package/dist/server/index.js +2 -2
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/primitives.d.ts +15 -0
  23. package/dist/server/primitives.d.ts.map +1 -1
  24. package/dist/server/request-context.d.ts +32 -0
  25. package/dist/server/request-context.d.ts.map +1 -1
  26. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  27. package/dist/server/slot-resolver.d.ts.map +1 -1
  28. package/dist/server/ssr-entry.d.ts.map +1 -1
  29. package/dist/shims/headers.d.ts +5 -7
  30. package/dist/shims/headers.d.ts.map +1 -1
  31. package/dist/shims/link.d.ts.map +1 -1
  32. package/dist/shims/navigation.d.ts +5 -15
  33. package/dist/shims/navigation.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/client/link.tsx +2 -0
  36. package/src/client/slot-error-fallback.tsx +16 -0
  37. package/src/plugins/shims.ts +48 -22
  38. package/src/server/primitives.ts +24 -1
  39. package/src/server/request-context.ts +7 -1
  40. package/src/server/rsc-entry/index.ts +29 -1
  41. package/src/server/slot-resolver.ts +20 -1
  42. package/src/server/ssr-entry.ts +21 -5
  43. package/src/shims/headers.ts +5 -7
  44. package/src/shims/link.ts +2 -0
  45. package/src/shims/navigation.ts +7 -17
  46. package/dist/_chunks/error-boundary-dj-WO5uq.js.map +0 -1
  47. package/dist/_chunks/request-context-D6XHINkR.js.map +0 -1
@@ -128,4 +128,19 @@ export declare function waitUntil(promise: Promise<unknown>, adapter: WaitUntilA
128
128
  * @internal
129
129
  */
130
130
  export declare function _resetWaitUntilWarning(): void;
131
+ /**
132
+ * Error thrown when SSR's renderToReadableStream fails due to an error
133
+ * in the decoded RSC stream (e.g., uncontained slot errors).
134
+ *
135
+ * The RSC entry checks for this error type in its catch block to avoid
136
+ * re-executing server components via renderDenyPage. Instead, it renders
137
+ * a bare deny/error page without layout wrapping.
138
+ *
139
+ * Defined in primitives.ts (not ssr-entry.ts) because ssr-entry.ts imports
140
+ * react-dom/server which cannot be loaded in the RSC environment.
141
+ */
142
+ export declare class SsrStreamError extends Error {
143
+ readonly cause: unknown;
144
+ constructor(message: string, cause: unknown);
145
+ }
131
146
  //# sourceMappingURL=primitives.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/server/primitives.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBAEX,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;IAO1C;;;;OAIG;IACH,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAqBnC;CACF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,MAAM,GAAE,MAAY,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK,CAQhE;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,IAAI,KAAK,CAEhC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY;;;CAGf,CAAC;AAIX;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAM7C;AAKD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAY,GAAG,KAAK,CAWlE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAErD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,MAAM,GAAE,MAAY,GAAG,KAAK,CAoB9F;AAID;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO;IAC/E,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,CAAE,SAAQ,KAAK;IACpF,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAYpE;AAID,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAapF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/server/primitives.ts"],"names":[],"mappings":"AAOA;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBAEX,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;IAO1C;;;;OAIG;IACH,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAqBnC;CACF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,MAAM,GAAE,MAAY,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK,CAQhE;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,IAAI,KAAK,CAEhC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY;;;CAGf,CAAC;AAIX;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAM7C;AAKD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAY,GAAG,KAAK,CAWlE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAErD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,MAAM,GAAE,MAAY,GAAG,KAAK,CAoB9F;AAID;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO;IAC/E,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAW,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,CAAE,SAAQ,KAAK;IACpF,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAYpE;AAID,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAapF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAID;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,SAAQ,KAAK;aAGrB,KAAK,EAAE,OAAO;gBAD9B,OAAO,EAAE,MAAM,EACC,KAAK,EAAE,OAAO;CAKjC"}
@@ -9,7 +9,38 @@
9
9
  * and design/11-platform.md §"AsyncLocalStorage".
10
10
  * See design/29-cookies.md for cookie mutation semantics.
11
11
  */
12
+ import { AsyncLocalStorage } from 'node:async_hooks';
12
13
  import type { Routes } from '#/index.js';
14
+ interface RequestContextStore {
15
+ /** Incoming request headers (read-only view). */
16
+ headers: Headers;
17
+ /** Raw cookie header string, parsed lazily into a Map on first access. */
18
+ cookieHeader: string;
19
+ /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */
20
+ parsedCookies?: Map<string, string>;
21
+ /** Original (pre-overlay) frozen headers, kept for overlay merging. */
22
+ originalHeaders: Headers;
23
+ /**
24
+ * Promise resolving to the route's typed search params (when search-params.ts
25
+ * exists) or to the raw URLSearchParams. Stored as a Promise so the framework
26
+ * can later support partial pre-rendering where param resolution is deferred.
27
+ */
28
+ searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;
29
+ /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */
30
+ cookieJar: Map<string, CookieEntry>;
31
+ /** Whether the response has flushed (headers committed). */
32
+ flushed: boolean;
33
+ /** Whether the current context allows cookie mutation. */
34
+ mutableContext: boolean;
35
+ }
36
+ /** A single outgoing cookie entry in the cookie jar. */
37
+ interface CookieEntry {
38
+ name: string;
39
+ value: string;
40
+ options: CookieOptions;
41
+ }
42
+ /** @internal */
43
+ export declare const requestContextAls: AsyncLocalStorage<RequestContextStore>;
13
44
  /**
14
45
  * Configure the cookie signing secrets.
15
46
  *
@@ -172,4 +203,5 @@ export declare function getSetCookieHeaders(): string[];
172
203
  * See design/07-routing.md §"Request Header Injection"
173
204
  */
174
205
  export declare function applyRequestHeaderOverlay(overlay: Headers): void;
206
+ export {};
175
207
  //# sourceMappingURL=request-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA0EA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AA+lBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BA3epD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA6ehD,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;AA0nBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BAtgBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAwgBhD,wBAAiE"}
@@ -1 +1 @@
1
- {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAIrE,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,CAqHpC"}
1
+ {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAMrE,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,CAiIpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA2BH;;;;;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,CA0DnB;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;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAwEnB;AAED,eAAe,SAAS,CAAC"}
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Shim: next/headers → timber request context
2
+ * Shim: next/headers → timber server
3
3
  *
4
- * Re-exports timber's ALS-backed headers() and cookies() for libraries
5
- * that import from next/headers. These are real implementations backed
6
- * by AsyncLocalStorage, not stubs.
7
- *
8
- * See design/14-ecosystem.md §"next/headers" for the full shim audit.
4
+ * Imports from @timber-js/app/server which Vite resolves to dist/server/index.js
5
+ * via native package.json exports. This ensures the same ALS singleton as the
6
+ * pipeline (both import from the same shared request-context chunk in dist/).
9
7
  */
10
- export { headers, cookies } from '#/server/request-context.js';
8
+ export { headers, cookies } from '@timber-js/app/server';
11
9
  //# sourceMappingURL=headers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/shims/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/shims/headers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/shims/link.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/shims/link.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,25 +1,15 @@
1
1
  /**
2
2
  * Shim: next/navigation → timber navigation primitives
3
3
  *
4
- * Re-exports timber's navigation hooks and functions for libraries
5
- * that import from next/navigation. Covers the App Router API surface
6
- * used by ecosystem libraries (nuqs, next-intl, etc.).
7
- *
8
- * Note: nuqs imports next/navigation.js (with .js extension).
9
- * The timber-shims plugin strips .js before matching.
10
- *
11
- * Intentional divergences from Next.js:
12
- * - useRouter().replace() currently uses pushState (same as push) —
13
- * timber's router doesn't distinguish push/replace yet.
14
- * - redirect() does not accept a RedirectType argument — timber
15
- * always uses replace semantics for redirects.
16
- * - permanentRedirect() delegates to redirect(path, 308).
17
- * See design/14-ecosystem.md for the full shim audit.
4
+ * Client hooks use #/ source imports (individual files with 'use client' directives
5
+ * that the RSC plugin detects).
6
+ * Server functions use @timber-js/app/server (resolved to dist/ via native exports)
7
+ * for ALS singleton consistency.
18
8
  */
19
9
  export { useParams } from '#/client/use-params.js';
20
10
  export { usePathname } from '#/client/use-pathname.js';
21
11
  export { useSearchParams } from '#/client/use-search-params.js';
22
12
  export { useRouter } from '#/client/use-router.js';
23
13
  export { useSelectedLayoutSegment, useSelectedLayoutSegments, } from '#/client/use-selected-layout-segment.js';
24
- export { redirect, permanentRedirect, notFound, RedirectType } from '#/server/primitives.js';
14
+ export { redirect, permanentRedirect, notFound, RedirectType } from '@timber-js/app/server';
25
15
  //# sourceMappingURL=navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/shims/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,yCAAyC,CAAC;AAGjD,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/shims/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,yCAAyC,CAAC;AAGjD,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  // Link component — client-side navigation with progressive enhancement
2
4
  // See design/19-client-navigation.md § Progressive Enhancement
3
5
  //
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Null fallback component for the slot catch-all error boundary.
5
+ *
6
+ * When a slot throws an error that isn't caught by a user-defined error.tsx,
7
+ * this boundary renders nothing — the slot gracefully degrades per
8
+ * design/02-rendering-pipeline.md §"Slot Access Failure = Graceful Degradation".
9
+ *
10
+ * This must be a 'use client' component because TimberErrorBoundary passes it
11
+ * as a prop (fallbackComponent), and server component functions cannot be
12
+ * passed directly to client components.
13
+ */
14
+ export default function SlotErrorFallback(): null {
15
+ return null;
16
+ }
@@ -5,6 +5,11 @@
5
5
  * shim implementations. This enables Next.js-compatible libraries
6
6
  * (nuqs, next-intl, etc.) to work unmodified.
7
7
  *
8
+ * NOTE: This plugin does NOT resolve @timber-js/app/* subpath imports.
9
+ * Those are handled by Vite's native package.json `exports` resolution,
10
+ * which maps them to dist/ files. This ensures a single module instance
11
+ * for shared modules like request-context (ALS singleton).
12
+ *
8
13
  * Design doc: 18-build-system.md §"Shim Map"
9
14
  */
10
15
 
@@ -14,7 +19,14 @@ import { fileURLToPath } from 'node:url';
14
19
  import type { PluginContext } from '#/index.js';
15
20
 
16
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
- const SHIMS_DIR = resolve(__dirname, '..', 'shims');
22
+ // Detect whether we're running from source (src/plugins/) or dist (dist/).
23
+ // From src/plugins/: go up 2 levels to package root.
24
+ // From dist/: go up 1 level to package root.
25
+ // When Rollup bundles into dist/index.js, __dirname is dist/, not src/plugins/.
26
+ const PKG_ROOT = __dirname.endsWith('plugins')
27
+ ? resolve(__dirname, '..', '..')
28
+ : resolve(__dirname, '..');
29
+ const SHIMS_DIR = resolve(PKG_ROOT, 'src', 'shims');
18
30
 
19
31
  /**
20
32
  * Virtual module IDs for server-only and client-only poison pills.
@@ -55,20 +67,6 @@ const CLIENT_SHIM_OVERRIDES: Record<string, string> = {
55
67
  'next/navigation': resolve(SHIMS_DIR, 'navigation-client.ts'),
56
68
  };
57
69
 
58
- /**
59
- * Map from @timber-js/app/* subpath imports to real source files.
60
- *
61
- * These resolve subpath imports like `@timber-js/app/server` to the
62
- * real entry files in the package source.
63
- */
64
- const TIMBER_SUBPATH_MAP: Record<string, string> = {
65
- '@timber-js/app/server': resolve(__dirname, '..', 'server', 'index.ts'),
66
- '@timber-js/app/client': resolve(__dirname, '..', 'client', 'index.ts'),
67
- '@timber-js/app/cache': resolve(__dirname, '..', 'cache', 'index.ts'),
68
- '@timber-js/app/search-params': resolve(__dirname, '..', 'search-params', 'index.ts'),
69
- '@timber-js/app/routing': resolve(__dirname, '..', 'routing', 'index.ts'),
70
- };
71
-
72
70
  /**
73
71
  * Strip .js extension from an import specifier.
74
72
  *
@@ -94,14 +92,20 @@ export function timberShims(_ctx: PluginContext): Plugin {
94
92
  enforce: 'pre',
95
93
 
96
94
  /**
97
- * Resolve next/* and @timber-js/app/* imports to shim/source files.
95
+ * Resolve next/* imports to shim files.
98
96
  *
99
97
  * Resolution order:
100
98
  * 1. Check server-only / client-only poison pill packages
101
99
  * 2. Strip .js extension from the import specifier
102
100
  * 3. Check next/* shim map
103
- * 4. Check @timber-js/app/* subpath map
104
- * 5. Return null (pass through) for unrecognized imports
101
+ * 4. Return null (pass through) for everything else
102
+ *
103
+ * @timber-js/app/server is resolved to src/ so it shares the same module
104
+ * instance as framework internals (which import via #/). This ensures
105
+ * a single requestContextAls and _getRscFallback variable.
106
+ *
107
+ * @timber-js/app/client is NOT mapped here — it resolves to dist/ via
108
+ * package.json exports, where 'use client' is preserved on the entry.
105
109
  */
106
110
  resolveId(id: string) {
107
111
  // Poison pill packages — resolve to virtual modules handled by load()
@@ -121,9 +125,16 @@ export function timberShims(_ctx: PluginContext): Plugin {
121
125
  return SHIM_MAP[cleanId];
122
126
  }
123
127
 
124
- // Check @timber-js/app/* subpath map
125
- if (cleanId in TIMBER_SUBPATH_MAP) {
126
- return TIMBER_SUBPATH_MAP[cleanId];
128
+ // @timber-js/app/server src/ in server environments so user code
129
+ // shares the same module instance as framework internals (single ALS).
130
+ // In the client environment, return a virtual empty module — server
131
+ // code must never be bundled into the browser.
132
+ if (cleanId === '@timber-js/app/server') {
133
+ const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
134
+ if (envName === 'client') {
135
+ return '\0timber:server-empty';
136
+ }
137
+ return resolve(PKG_ROOT, 'src', 'server', 'index.ts');
127
138
  }
128
139
 
129
140
  return null;
@@ -162,7 +173,22 @@ export function timberShims(_ctx: PluginContext): Plugin {
162
173
  return 'export {};';
163
174
  }
164
175
 
165
- return null;
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.
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
+ `;
191
+ }
166
192
  },
167
193
  };
168
194
  }
@@ -1,4 +1,4 @@
1
- // Server-side primitives: deny, redirect, redirectExternal, RenderError, waitUntil
1
+ // Server-side primitives: deny, redirect, redirectExternal, RenderError, waitUntil, SsrStreamError
2
2
  //
3
3
  // These are the core runtime signals that components, middleware, and access gates
4
4
  // use to control request flow. See design/10-error-handling.md.
@@ -262,3 +262,26 @@ export function waitUntil(promise: Promise<unknown>, adapter: WaitUntilAdapter):
262
262
  export function _resetWaitUntilWarning(): void {
263
263
  _waitUntilWarned = false;
264
264
  }
265
+
266
+ // ─── SsrStreamError ─────────────────────────────────────────────────────────
267
+
268
+ /**
269
+ * Error thrown when SSR's renderToReadableStream fails due to an error
270
+ * in the decoded RSC stream (e.g., uncontained slot errors).
271
+ *
272
+ * The RSC entry checks for this error type in its catch block to avoid
273
+ * re-executing server components via renderDenyPage. Instead, it renders
274
+ * a bare deny/error page without layout wrapping.
275
+ *
276
+ * Defined in primitives.ts (not ssr-entry.ts) because ssr-entry.ts imports
277
+ * react-dom/server which cannot be loaded in the RSC environment.
278
+ */
279
+ export class SsrStreamError extends Error {
280
+ constructor(
281
+ message: string,
282
+ public readonly cause: unknown
283
+ ) {
284
+ super(message);
285
+ this.name = 'SsrStreamError';
286
+ }
287
+ }
@@ -46,7 +46,13 @@ interface CookieEntry {
46
46
  options: CookieOptions;
47
47
  }
48
48
 
49
- const requestContextAls = new AsyncLocalStorage<RequestContextStore>();
49
+ /** @internal */
50
+ export const requestContextAls = new AsyncLocalStorage<RequestContextStore>();
51
+
52
+ // No fallback needed — we use enterWith() instead of run() to ensure
53
+ // the ALS context persists for the entire request lifecycle including
54
+ // async stream consumption by React's renderToReadableStream.
55
+
50
56
 
51
57
  // ─── Cookie Signing Secrets ──────────────────────────────────────────────
52
58
 
@@ -24,6 +24,7 @@ import buildManifest from 'virtual:timber-build-manifest';
24
24
 
25
25
  import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc';
26
26
 
27
+ import React, { createElement } from 'react';
27
28
  import { createPipeline } from '#/server/pipeline.js';
28
29
  import { initDevTracing } from '#/server/tracing.js';
29
30
  import type { PipelineConfig, RouteMatch, InterceptionContext } from '#/server/pipeline.js';
@@ -31,7 +32,7 @@ import { logRenderError } from '#/server/logger.js';
31
32
  import { resolveLogMode } from '#/server/dev-logger.js';
32
33
  import { createRouteMatcher, createMetadataRouteMatcher } from '#/server/route-matcher.js';
33
34
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
34
- import { DenySignal, RedirectSignal, RenderError } from '#/server/primitives.js';
35
+ import { DenySignal, RedirectSignal, RenderError, SsrStreamError } from '#/server/primitives.js';
35
36
  import { buildClientScripts } from '#/server/html-injectors.js';
36
37
  import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
37
38
  import { renderDenyPage, renderDenyPageAsRsc } from '#/server/deny-renderer.js';
@@ -372,6 +373,7 @@ async function renderRoute(
372
373
  let redirectSignal: RedirectSignal | null = null;
373
374
  let renderError: { error: unknown; status: number } | null = null;
374
375
  let rscStream: ReadableStream<Uint8Array> | undefined;
376
+
375
377
  try {
376
378
  rscStream = renderToReadableStream(
377
379
  element,
@@ -675,6 +677,32 @@ async function renderRoute(
675
677
  return new Response(null, { status: 499 });
676
678
  }
677
679
 
680
+ // SsrStreamError: SSR's renderToReadableStream failed because the RSC
681
+ // stream contained an uncontained error (e.g., slot without error boundary).
682
+ // Render the deny/error page WITHOUT layout wrapping to avoid re-executing
683
+ // server components (which call headers()/cookies() and fail in SSR's
684
+ // separate ALS scope). See LOCAL-293.
685
+ if (ssrError instanceof SsrStreamError) {
686
+ const sig = redirectSignal as RedirectSignal | null;
687
+ if (sig) return buildRedirectResponse(_req, sig, responseHeaders);
688
+ if (denySignal) {
689
+ // Render deny page without layouts — pass empty layout list
690
+ return renderDenyPage(
691
+ denySignal, segments, [] as LayoutEntry[],
692
+ _req, match, responseHeaders, clientBootstrap, createDebugChannelSink, callSsr
693
+ );
694
+ }
695
+ const err = renderError as { error: unknown; status: number } | null;
696
+ if (err) {
697
+ return renderErrorPage(
698
+ err.error, err.status, segments, [] as LayoutEntry[],
699
+ _req, match, responseHeaders, clientBootstrap
700
+ );
701
+ }
702
+ // No captured signal — return bare 500
703
+ return new Response(null, { status: 500, headers: responseHeaders });
704
+ }
705
+
678
706
  // SSR shell rendering failed — the error was outside Suspense.
679
707
  // Check captured signals (redirect, deny, render error).
680
708
  const signalResponse = checkCapturedSignals();
@@ -20,6 +20,8 @@ import type { ManifestSegmentNode } from './route-matcher.js';
20
20
  import type { RouteMatch, InterceptionContext } from './pipeline.js';
21
21
  import { SlotAccessGate } from './access-gate.js';
22
22
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
23
+ import { TimberErrorBoundary } from '#/client/error-boundary.js';
24
+ import SlotErrorFallback from '#/client/slot-error-fallback.js';
23
25
 
24
26
  type CreateElementFn = (...args: unknown[]) => React.ReactElement;
25
27
 
@@ -142,6 +144,18 @@ export async function resolveSlotElement(
142
144
  // Wrap with slot root's error boundaries (outermost)
143
145
  element = await wrapSegmentWithErrorBoundaries(slotNode, element, h);
144
146
 
147
+ // Catch-all error boundary: ensures slot errors NEVER propagate to the
148
+ // parent layout. Without this, a slot without error.tsx that throws
149
+ // causes SSR's renderToReadableStream to reject, triggering renderDenyPage
150
+ // which re-executes all layout server components (including headers() calls
151
+ // that fail in the SSR environment). The null fallback means the slot
152
+ // degrades to nothing — consistent with the slot access denial behavior.
153
+ // See design/02-rendering-pipeline.md §"Slot Access Failure = Graceful Degradation"
154
+ element = h(TimberErrorBoundary, {
155
+ fallbackComponent: SlotErrorFallback,
156
+ children: element,
157
+ });
158
+
145
159
  return element;
146
160
  }
147
161
  }
@@ -187,9 +201,14 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
187
201
 
188
202
  // Find the parent segment that owns this slot by comparing urlPaths.
189
203
  // The slot's urlPath matches its parent's urlPath (slots don't add URL depth).
204
+ // Search BACKWARDS to find the deepest (last) matching segment. Multiple
205
+ // segments can share the same urlPath when route groups are involved (e.g.,
206
+ // Root urlPath='/' and (browse) urlPath='/'). The slot's parent is always
207
+ // the deepest one — searching forward would incorrectly pick the root,
208
+ // making remainingSegments too long and breaking slot matching.
190
209
  const slotUrlPath = slotNode.urlPath;
191
210
  let parentIndex = -1;
192
- for (let i = 0; i < segments.length; i++) {
211
+ for (let i = segments.length - 1; i >= 0; i--) {
193
212
  if (segments[i].urlPath === slotUrlPath) {
194
213
  parentIndex = i;
195
214
  break;
@@ -18,6 +18,8 @@ import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
18
18
  import { AsyncLocalStorage } from 'node:async_hooks';
19
19
 
20
20
  import { renderSsrStream, buildSsrResponse } from './ssr-render.js';
21
+ import { formatSsrError } from './error-formatter.js';
22
+ import { SsrStreamError } from './primitives.js';
21
23
  import { injectHead, injectRscPayload } from './html-injectors.js';
22
24
  import { withNuqsSsrAdapter } from './nuqs-ssr-provider.js';
23
25
  import { withSpan } from './tracing.js';
@@ -141,11 +143,25 @@ export async function handleSsr(
141
143
  // in the shell HTML. This executes immediately during parsing — even
142
144
  // while Suspense boundaries are still streaming — triggering module
143
145
  // loading via dynamic import() so hydration can start early.
144
- const htmlStream = await renderSsrStream(wrappedElement, {
145
- bootstrapScriptContent: navContext.bootstrapScriptContent || undefined,
146
- deferSuspenseFor: navContext.deferSuspenseFor,
147
- signal: navContext.signal,
148
- });
146
+ let htmlStream: ReadableStream<Uint8Array>;
147
+ try {
148
+ htmlStream = await renderSsrStream(wrappedElement, {
149
+ bootstrapScriptContent: navContext.bootstrapScriptContent || undefined,
150
+ deferSuspenseFor: navContext.deferSuspenseFor,
151
+ signal: navContext.signal,
152
+ });
153
+ } catch (renderError) {
154
+ // SSR shell rendering failed — the RSC stream contained an error
155
+ // that wasn't caught by any error boundary in the decoded tree.
156
+ // Wrap in SsrStreamError so the RSC entry can handle it without
157
+ // re-executing server components via renderDenyPage.
158
+ // See LOCAL-293.
159
+ console.error('[timber] SSR shell failed from RSC stream error:', formatSsrError(renderError));
160
+ throw new SsrStreamError(
161
+ 'SSR renderToReadableStream failed due to RSC stream error',
162
+ renderError
163
+ );
164
+ }
149
165
 
150
166
  // Inject metadata into <head>, then interleave RSC payload chunks
151
167
  // into the body as they arrive from the tee'd RSC stream.
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Shim: next/headers → timber request context
2
+ * Shim: next/headers → timber server
3
3
  *
4
- * Re-exports timber's ALS-backed headers() and cookies() for libraries
5
- * that import from next/headers. These are real implementations backed
6
- * by AsyncLocalStorage, not stubs.
7
- *
8
- * See design/14-ecosystem.md §"next/headers" for the full shim audit.
4
+ * Imports from @timber-js/app/server which Vite resolves to dist/server/index.js
5
+ * via native package.json exports. This ensures the same ALS singleton as the
6
+ * pipeline (both import from the same shared request-context chunk in dist/).
9
7
  */
10
8
 
11
- export { headers, cookies } from '#/server/request-context.js';
9
+ export { headers, cookies } from '@timber-js/app/server';
package/src/shims/link.ts CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  /**
2
4
  * Shim: next/link → @timber-js/app/client Link
3
5
  *
@@ -1,23 +1,13 @@
1
1
  /**
2
2
  * Shim: next/navigation → timber navigation primitives
3
3
  *
4
- * Re-exports timber's navigation hooks and functions for libraries
5
- * that import from next/navigation. Covers the App Router API surface
6
- * used by ecosystem libraries (nuqs, next-intl, etc.).
7
- *
8
- * Note: nuqs imports next/navigation.js (with .js extension).
9
- * The timber-shims plugin strips .js before matching.
10
- *
11
- * Intentional divergences from Next.js:
12
- * - useRouter().replace() currently uses pushState (same as push) —
13
- * timber's router doesn't distinguish push/replace yet.
14
- * - redirect() does not accept a RedirectType argument — timber
15
- * always uses replace semantics for redirects.
16
- * - permanentRedirect() delegates to redirect(path, 308).
17
- * See design/14-ecosystem.md for the full shim audit.
4
+ * Client hooks use #/ source imports (individual files with 'use client' directives
5
+ * that the RSC plugin detects).
6
+ * Server functions use @timber-js/app/server (resolved to dist/ via native exports)
7
+ * for ALS singleton consistency.
18
8
  */
19
9
 
20
- // Hooks (client-side)
10
+ // Hooks (client-side — must use source imports for RSC 'use client' detection)
21
11
  export { useParams } from '#/client/use-params.js';
22
12
  export { usePathname } from '#/client/use-pathname.js';
23
13
  export { useSearchParams } from '#/client/use-search-params.js';
@@ -27,5 +17,5 @@ export {
27
17
  useSelectedLayoutSegments,
28
18
  } from '#/client/use-selected-layout-segment.js';
29
19
 
30
- // Functions (server-side)
31
- export { redirect, permanentRedirect, notFound, RedirectType } from '#/server/primitives.js';
20
+ // Functions (server-side — resolved to dist/ for ALS singleton consistency)
21
+ export { redirect, permanentRedirect, notFound, RedirectType } from '@timber-js/app/server';
@@ -1 +0,0 @@
1
- {"version":3,"file":"error-boundary-dj-WO5uq.js","names":[],"sources":["../../src/client/error-boundary.tsx"],"sourcesContent":["'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\n\n// ─── Page Unload Detection ───────────────────────────────────────────────────\n// Track whether the page is being unloaded (user refreshed or navigated away).\n// When this is true, error boundaries suppress activation — the error is from\n// the aborted connection, not an application error.\nlet _isUnloading = false;\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _isUnloading = true;\n });\n window.addEventListener('pagehide', () => {\n _isUnloading = true;\n });\n}\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent: (...args: unknown[]) => ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (_isUnloading) {\n return { hasError: false, error: null };\n }\n return { hasError: true, error };\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal errors must propagate through all error boundaries\n // so the SSR shell fails and the pipeline catch block can produce a\n // proper HTTP redirect response. See design/04-authorization.md.\n if (parsed?.type === 'redirect') {\n throw error;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch {\n // Not JSON — legacy or unknown digest format\n }\n return null;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA0BA,IAAI,eAAe;AACnB,IAAI,OAAO,WAAW,aAAa;AACjC,QAAO,iBAAiB,sBAAsB;AAC5C,iBAAe;GACf;AACF,QAAO,iBAAiB,kBAAkB;AACxC,iBAAe;GACf;;AAiDJ,IAAa,sBAAb,cAAyC,UAGvC;CACA,YAAY,OAAiC;AAC3C,QAAM,MAAM;AACZ,OAAK,QAAQ;GAAE,UAAU;GAAO,OAAO;GAAM;;CAG/C,OAAO,yBAAyB,OAAwC;AAKtE,MAAI,aACF,QAAO;GAAE,UAAU;GAAO,OAAO;GAAM;AAEzC,SAAO;GAAE,UAAU;GAAM;GAAO;;CAGlC,mBAAmB,WAA2C;AAI5D,MAAI,KAAK,MAAM,YAAY,UAAU,aAAa,KAAK,MAAM,SAC3D,MAAK,SAAS;GAAE,UAAU;GAAO,OAAO;GAAM,CAAC;;;CAKnD,cAAsB;AACpB,OAAK,SAAS;GAAE,UAAU;GAAO,OAAO;GAAM,CAAC;;CAGjD,SAAoB;AAClB,MAAI,CAAC,KAAK,MAAM,YAAY,CAAC,KAAK,MAAM,MACtC,QAAO,KAAK,MAAM;EAGpB,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,SAAS,YAAY,MAAM;AAKjC,MAAI,QAAQ,SAAS,WACnB,OAAM;AAKR,MAAI,KAAK,MAAM,UAAU,MAAM;GAC7B,MAAM,cAAc,eAAe,QAAQ,MAAM;AACjD,OAAI,eAAe,QAAQ,CAAC,cAAc,KAAK,MAAM,QAAQ,YAAY,CAEvE,OAAM;;AAKV,MAAI,QAAQ,SAAS,OACnB,QAAO,cAAc,KAAK,MAAM,mBAA4B;GAC1D,QAAQ,OAAO;GACf,qBAAqB,OAAO;GAC7B,CAAC;EAIJ,MAAM,SACJ,QAAQ,SAAS,iBAAiB;GAAE,MAAM,OAAO;GAAM,MAAM,OAAO;GAAM,GAAG;AAE/E,SAAO,cAAc,KAAK,MAAM,mBAA4B;GAC1D;GACA;GACA,OAAO,KAAK;GACb,CAAC;;;;;;;AAUN,SAAS,YAAY,OAAmC;CACtD,MAAM,MAAO,MAA8B;AAC3C,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,KAAI;EACF,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MAAI,UAAU,OAAO,WAAW,YAAY,OAAO,OAAO,SAAS,SACjE,QAAO;SAEH;AAGR,QAAO;;;;;;AAOT,SAAS,eAAe,QAA6B,OAA6B;AAChF,KAAI,QAAQ,SAAS,OAAQ,QAAO,OAAO;AAC3C,KAAI,QAAQ,SAAS,eAAgB,QAAO,OAAO;AACnD,KAAI,QAAQ,SAAS,WAAY,QAAO,OAAO;CAG/C,MAAM,QAAQ,MAAM,QAAQ,MAAM,oCAAoC;AACtE,KAAI,MAAO,QAAO,SAAS,MAAM,IAAI,GAAG;AAGxC,QAAO;;;;;;AAOT,SAAS,cAAc,gBAAwB,aAA8B;AAE3E,KAAI,mBAAmB,IAAK,QAAO,eAAe,OAAO,eAAe;AACxE,KAAI,mBAAmB,IAAK,QAAO,eAAe,OAAO,eAAe;AAExE,QAAO,mBAAmB"}