@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.
- package/dist/_chunks/{registry-DUIpYD_x.js → registry-BfPM41ri.js} +1 -1
- package/dist/_chunks/{registry-DUIpYD_x.js.map → registry-BfPM41ri.js.map} +1 -1
- package/dist/_chunks/{request-context-D6XHINkR.js → request-context-BzES06i1.js} +2 -1
- package/dist/_chunks/request-context-BzES06i1.js.map +1 -0
- package/dist/_chunks/{use-cookie-8ZlA0rr3.js → use-cookie-HcvNlW4L.js} +1 -1
- package/dist/_chunks/{use-cookie-8ZlA0rr3.js.map → use-cookie-HcvNlW4L.js.map} +1 -1
- package/dist/{_chunks/error-boundary-dj-WO5uq.js → client/error-boundary.js} +4 -2
- package/dist/client/error-boundary.js.map +1 -0
- package/dist/client/index.js +7 -6
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/slot-error-fallback.d.ts +13 -0
- package/dist/client/slot-error-fallback.d.ts.map +1 -0
- package/dist/cookies/index.js +2 -2
- package/dist/index.js +17 -16
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts +5 -0
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/search-params/index.js +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/primitives.d.ts +15 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +32 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/shims/headers.d.ts +5 -7
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +5 -15
- package/dist/shims/navigation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/link.tsx +2 -0
- package/src/client/slot-error-fallback.tsx +16 -0
- package/src/plugins/shims.ts +48 -22
- package/src/server/error-formatter.ts +12 -0
- package/src/server/primitives.ts +24 -1
- package/src/server/request-context.ts +7 -1
- package/src/server/rsc-entry/index.ts +53 -1
- package/src/server/slot-resolver.ts +20 -1
- package/src/server/ssr-entry.ts +21 -5
- package/src/shims/headers.ts +5 -7
- package/src/shims/link.ts +2 -0
- package/src/shims/navigation.ts +7 -17
- package/dist/_chunks/error-boundary-dj-WO5uq.js.map +0 -1
- 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":"AAAA;;;;;;;;;;GAUG;
|
|
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":"
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/shims/headers.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shim: next/headers → timber
|
|
2
|
+
* Shim: next/headers → timber server
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 '
|
|
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
|
|
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"}
|
package/dist/shims/link.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/shims/link.ts"],"names":[],"mappings":"
|
|
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
|
-
*
|
|
5
|
-
* that
|
|
6
|
-
*
|
|
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 '
|
|
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
|
|
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.
|
|
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",
|
package/src/client/link.tsx
CHANGED
|
@@ -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
|
+
}
|
package/src/plugins/shims.ts
CHANGED
|
@@ -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
|
-
|
|
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/*
|
|
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.
|
|
104
|
-
*
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
package/src/server/primitives.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
211
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
193
212
|
if (segments[i].urlPath === slotUrlPath) {
|
|
194
213
|
parentIndex = i;
|
|
195
214
|
break;
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
package/src/shims/headers.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shim: next/headers → timber
|
|
2
|
+
* Shim: next/headers → timber server
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 '
|
|
9
|
+
export { headers, cookies } from '@timber-js/app/server';
|
package/src/shims/link.ts
CHANGED
package/src/shims/navigation.ts
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shim: next/navigation → timber navigation primitives
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* that
|
|
6
|
-
*
|
|
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 '
|
|
20
|
+
// Functions (server-side — resolved to dist/ for ALS singleton consistency)
|
|
21
|
+
export { redirect, permanentRedirect, notFound, RedirectType } from '@timber-js/app/server';
|