@timber-js/app 0.1.4 → 0.1.6

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 (48) 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 +3 -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/error-formatter.ts +12 -0
  39. package/src/server/primitives.ts +24 -1
  40. package/src/server/request-context.ts +7 -1
  41. package/src/server/rsc-entry/index.ts +53 -1
  42. package/src/server/slot-resolver.ts +20 -1
  43. package/src/server/ssr-entry.ts +21 -5
  44. package/src/shims/headers.ts +5 -7
  45. package/src/shims/link.ts +2 -0
  46. package/src/shims/navigation.ts +7 -17
  47. package/dist/_chunks/error-boundary-dj-WO5uq.js.map +0 -1
  48. 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;AAkpBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;8BA9hBpD,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AAgiBhD,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.6",
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
  }
@@ -147,6 +147,18 @@ function extractErrorHint(message: string): string | null {
147
147
  return 'A component resolved to undefined/null — check default exports and import paths';
148
148
  }
149
149
 
150
+ // "Invalid hook call" — hooks called outside React's render context.
151
+ // In RSC, this typically means a 'use client' component was executed as a
152
+ // server component instead of being serialized as a client reference.
153
+ if (message.includes('Invalid hook call')) {
154
+ return (
155
+ 'A hook was called outside of a React component render. ' +
156
+ 'If this is a \'use client\' component, ensure the directive is at the very top of the file ' +
157
+ '(before any imports) and that @vitejs/plugin-rsc is loaded correctly. ' +
158
+ 'Barrel re-exports from non-\'use client\' files do not propagate the directive.'
159
+ );
160
+ }
161
+
150
162
  return null;
151
163
  }
152
164
 
@@ -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,
@@ -407,6 +409,30 @@ async function renderRoute(
407
409
  status: error.status,
408
410
  });
409
411
  }
412
+ // Dev diagnostic: detect "Invalid hook call" errors which indicate
413
+ // a 'use client' component is being executed during RSC rendering
414
+ // instead of being serialized as a client reference. This happens when
415
+ // the RSC plugin's transform doesn't detect the directive — e.g., the
416
+ // directive isn't at the very top of the file, or the component is
417
+ // re-exported through a barrel file without 'use client'.
418
+ // See LOCAL-297.
419
+ if (
420
+ process.env.NODE_ENV !== 'production' &&
421
+ error instanceof Error &&
422
+ error.message.includes('Invalid hook call')
423
+ ) {
424
+ console.error(
425
+ '[timber] A React hook was called during RSC rendering. This usually means a ' +
426
+ "'use client' component is being executed as a server component instead of " +
427
+ 'being serialized as a client reference.\n\n' +
428
+ 'Common causes:\n' +
429
+ " 1. The 'use client' directive is not the FIRST statement in the file (before any imports)\n" +
430
+ " 2. The component is re-exported through a barrel file (index.ts) that lacks 'use client'\n" +
431
+ ' 3. @vitejs/plugin-rsc is not loaded or is misconfigured\n\n' +
432
+ `Request: ${_req.method} ${new URL(_req.url).pathname}`
433
+ );
434
+ }
435
+
410
436
  // Track unhandled errors for pre-flush handling (500 status)
411
437
  if (!renderError) {
412
438
  renderError = { error, status: 500 };
@@ -675,6 +701,32 @@ async function renderRoute(
675
701
  return new Response(null, { status: 499 });
676
702
  }
677
703
 
704
+ // SsrStreamError: SSR's renderToReadableStream failed because the RSC
705
+ // stream contained an uncontained error (e.g., slot without error boundary).
706
+ // Render the deny/error page WITHOUT layout wrapping to avoid re-executing
707
+ // server components (which call headers()/cookies() and fail in SSR's
708
+ // separate ALS scope). See LOCAL-293.
709
+ if (ssrError instanceof SsrStreamError) {
710
+ const sig = redirectSignal as RedirectSignal | null;
711
+ if (sig) return buildRedirectResponse(_req, sig, responseHeaders);
712
+ if (denySignal) {
713
+ // Render deny page without layouts — pass empty layout list
714
+ return renderDenyPage(
715
+ denySignal, segments, [] as LayoutEntry[],
716
+ _req, match, responseHeaders, clientBootstrap, createDebugChannelSink, callSsr
717
+ );
718
+ }
719
+ const err = renderError as { error: unknown; status: number } | null;
720
+ if (err) {
721
+ return renderErrorPage(
722
+ err.error, err.status, segments, [] as LayoutEntry[],
723
+ _req, match, responseHeaders, clientBootstrap
724
+ );
725
+ }
726
+ // No captured signal — return bare 500
727
+ return new Response(null, { status: 500, headers: responseHeaders });
728
+ }
729
+
678
730
  // SSR shell rendering failed — the error was outside Suspense.
679
731
  // Check captured signals (redirect, deny, render error).
680
732
  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';