@timber-js/app 0.2.0-alpha.49 → 0.2.0-alpha.50

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA8EH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE;QACZ,gFAAgF;QAChF,QAAQ,EAAE,MAAM,CAAC;QACjB,uDAAuD;QACvD,OAAO,EAAE,MAAM,CAAC;QAChB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC;QACnB,+CAA+C;QAC/C,OAAO,EAAE,MAAM,CAAC;QAChB,sDAAsD;QACtD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAmLnB;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA8EH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACvC;;;0DAGsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;4DACwD;IACxD,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE;QACZ,gFAAgF;QAChF,QAAQ,EAAE,MAAM,CAAC;QACjB,uDAAuD;QACvD,OAAO,EAAE,MAAM,CAAC;QAChB,gEAAgE;QAChE,MAAM,EAAE,MAAM,CAAC;QACf,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC;QACnB,+CAA+C;QAC/C,OAAO,EAAE,MAAM,CAAC;QAChB,sDAAsD;QACtD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAuLnB;AAED,eAAe,SAAS,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SSR wrapper components that match the client-side component tree depth.
3
+ *
4
+ * During hydration, React's useId() generates deterministic IDs based on
5
+ * the component's position in the fiber tree. If the SSR tree has fewer
6
+ * wrapper components than the client hydration tree, every useId() call
7
+ * produces different IDs — causing hydration mismatches in libraries like
8
+ * Radix UI that rely on useId() internally.
9
+ *
10
+ * The client tree (browser-entry.ts) wraps the RSC element with:
11
+ * TransitionRoot → PendingNavigationProvider → Fragment(TopLoader, ...) →
12
+ * TimberNuqsAdapter → NuqsAdapterProvider → NavigationProvider → element
13
+ *
14
+ * The SSR tree must produce the same component boundaries. These wrappers
15
+ * are no-op components that render children directly but exist as fiber
16
+ * nodes to match the client's tree depth.
17
+ *
18
+ * Related issues: facebook/react#24669, radix-ui/primitives#3700
19
+ * See design/02-rendering-pipeline.md §"RSC → SSR → Client Hydration"
20
+ */
21
+ import { type ReactNode } from 'react';
22
+ /**
23
+ * Wrap the SSR element tree with the same component boundaries as the
24
+ * client hydration tree. This ensures useId() generates matching IDs
25
+ * on both sides.
26
+ *
27
+ * Client tree (browser-entry.ts):
28
+ * TransitionRoot
29
+ * → PendingNavigationProvider
30
+ * → Fragment(TopLoader, element)
31
+ * → TimberNuqsAdapter
32
+ * → NuqsAdapterProvider (from createAdapterProvider)
33
+ * → NavigationProvider
34
+ * → [RSC element]
35
+ *
36
+ * SSR tree (this function):
37
+ * SsrTransitionRoot
38
+ * → SsrPendingProvider
39
+ * → Fragment(SsrTopLoader, element)
40
+ * → SsrNuqsWrapper
41
+ * → NuqsAdapterProvider (from withNuqsSsrAdapter → createAdapterProvider)
42
+ * → SsrNavigationProvider
43
+ * → [RSC element]
44
+ *
45
+ * @param element - The decoded RSC element
46
+ * @param searchParams - Request search params for nuqs SSR adapter
47
+ * @param hasTopLoader - Whether the top loader is enabled (must match client config)
48
+ */
49
+ export declare function wrapSsrElement(element: ReactNode, searchParams: Record<string, string>, hasTopLoader: boolean): ReactNode;
50
+ //# sourceMappingURL=ssr-wrappers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr-wrappers.d.ts","sourceRoot":"","sources":["../../src/server/ssr-wrappers.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAwEhE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,EAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,YAAY,EAAE,OAAO,GACpB,SAAS,CAcX"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.49",
3
+ "version": "0.2.0-alpha.50",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -31,7 +31,7 @@ import {
31
31
  import { logRenderError } from './logger.js';
32
32
  import { SsrStreamError } from './primitives.js';
33
33
  import { createBufferedTransformStream, injectHead, injectRscPayload } from './html-injectors.js';
34
- import { withNuqsSsrAdapter } from './nuqs-ssr-provider.js';
34
+ import { wrapSsrElement } from './ssr-wrappers.js';
35
35
  import { withSpan } from './tracing.js';
36
36
  import { setCurrentParams } from '#/client/use-params.js';
37
37
  import { registerSsrDataProvider, type SsrData } from '#/client/ssr-data.js';
@@ -226,9 +226,13 @@ export async function handleSsr(
226
226
  }
227
227
  const _decodeEnd = performance.now();
228
228
 
229
- // Wrap with a server-safe nuqs adapter so that 'use client' components
230
- // that call nuqs hooks (useQueryStates, useQueryState) can SSR correctly.
231
- const wrappedElement = withNuqsSsrAdapter(navContext.searchParams, element);
229
+ // Wrap with the same component tree structure as the client hydration
230
+ // tree (TransitionRoot PendingNavigationProvider TopLoader
231
+ // TimberNuqsAdapter NuqsAdapterProvider → NavigationProvider).
232
+ // This ensures useId() produces matching IDs on both sides, preventing
233
+ // hydration mismatches in libraries like Radix UI. See TIM-532.
234
+ const hasTopLoader = _runtimeConfig.topLoader?.enabled !== false;
235
+ const wrappedElement = wrapSsrElement(element, navContext.searchParams, hasTopLoader);
232
236
 
233
237
  // Render to HTML stream (waits for onShellReady).
234
238
  //
@@ -0,0 +1,139 @@
1
+ /**
2
+ * SSR wrapper components that match the client-side component tree depth.
3
+ *
4
+ * During hydration, React's useId() generates deterministic IDs based on
5
+ * the component's position in the fiber tree. If the SSR tree has fewer
6
+ * wrapper components than the client hydration tree, every useId() call
7
+ * produces different IDs — causing hydration mismatches in libraries like
8
+ * Radix UI that rely on useId() internally.
9
+ *
10
+ * The client tree (browser-entry.ts) wraps the RSC element with:
11
+ * TransitionRoot → PendingNavigationProvider → Fragment(TopLoader, ...) →
12
+ * TimberNuqsAdapter → NuqsAdapterProvider → NavigationProvider → element
13
+ *
14
+ * The SSR tree must produce the same component boundaries. These wrappers
15
+ * are no-op components that render children directly but exist as fiber
16
+ * nodes to match the client's tree depth.
17
+ *
18
+ * Related issues: facebook/react#24669, radix-ui/primitives#3700
19
+ * See design/02-rendering-pipeline.md §"RSC → SSR → Client Hydration"
20
+ */
21
+
22
+ import { createElement, Fragment, type ReactNode } from 'react';
23
+ import { withNuqsSsrAdapter } from './nuqs-ssr-provider.js';
24
+
25
+ /**
26
+ * SSR equivalent of TransitionRoot.
27
+ *
28
+ * On the client, TransitionRoot uses useState + useTransition and renders:
29
+ * PendingNavigationProvider(Fragment(TopLoader, element))
30
+ *
31
+ * This SSR version matches the component boundary depth without client
32
+ * hooks. It renders SsrPendingProvider → Fragment(SsrTopLoader, children).
33
+ */
34
+ function SsrTransitionRoot({
35
+ children,
36
+ hasTopLoader,
37
+ }: {
38
+ children: ReactNode;
39
+ hasTopLoader: boolean;
40
+ }): ReactNode {
41
+ if (hasTopLoader) {
42
+ return createElement(
43
+ SsrPendingProvider,
44
+ null,
45
+ createElement(Fragment, null, createElement(SsrTopLoader, null), children)
46
+ );
47
+ }
48
+ return createElement(SsrPendingProvider, null, children);
49
+ }
50
+
51
+ /**
52
+ * SSR equivalent of PendingNavigationProvider.
53
+ * Matches the context.Provider component boundary on the client.
54
+ */
55
+ function SsrPendingProvider({ children }: { children: ReactNode }): ReactNode {
56
+ return children as ReactNode;
57
+ }
58
+
59
+ /**
60
+ * SSR equivalent of TopLoader.
61
+ * Exists as a fiber node to match the client tree depth; renders nothing.
62
+ */
63
+ function SsrTopLoader(): ReactNode {
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * SSR equivalent of NavigationProvider.
69
+ * Matches the context.Provider component boundary on the client.
70
+ */
71
+ function SsrNavigationProvider({ children }: { children: ReactNode }): ReactNode {
72
+ return children as ReactNode;
73
+ }
74
+
75
+ /**
76
+ * SSR equivalent of TimberNuqsAdapter component boundary.
77
+ * The actual NuqsAdapterProvider (from createAdapterProvider) is added
78
+ * inside this wrapper via withNuqsSsrAdapter, matching the client's
79
+ * TimberNuqsAdapter → NuqsAdapterProvider nesting.
80
+ */
81
+ function SsrNuqsWrapper({
82
+ searchParams,
83
+ children,
84
+ }: {
85
+ searchParams: Record<string, string>;
86
+ children: ReactNode;
87
+ }): ReactNode {
88
+ // withNuqsSsrAdapter creates a NuqsAdapterProvider (from createAdapterProvider),
89
+ // which is the same component factory used by TimberNuqsAdapter on the client.
90
+ // Both produce the same internal component boundary depth.
91
+ return withNuqsSsrAdapter(searchParams, children);
92
+ }
93
+
94
+ /**
95
+ * Wrap the SSR element tree with the same component boundaries as the
96
+ * client hydration tree. This ensures useId() generates matching IDs
97
+ * on both sides.
98
+ *
99
+ * Client tree (browser-entry.ts):
100
+ * TransitionRoot
101
+ * → PendingNavigationProvider
102
+ * → Fragment(TopLoader, element)
103
+ * → TimberNuqsAdapter
104
+ * → NuqsAdapterProvider (from createAdapterProvider)
105
+ * → NavigationProvider
106
+ * → [RSC element]
107
+ *
108
+ * SSR tree (this function):
109
+ * SsrTransitionRoot
110
+ * → SsrPendingProvider
111
+ * → Fragment(SsrTopLoader, element)
112
+ * → SsrNuqsWrapper
113
+ * → NuqsAdapterProvider (from withNuqsSsrAdapter → createAdapterProvider)
114
+ * → SsrNavigationProvider
115
+ * → [RSC element]
116
+ *
117
+ * @param element - The decoded RSC element
118
+ * @param searchParams - Request search params for nuqs SSR adapter
119
+ * @param hasTopLoader - Whether the top loader is enabled (must match client config)
120
+ */
121
+ export function wrapSsrElement(
122
+ element: ReactNode,
123
+ searchParams: Record<string, string>,
124
+ hasTopLoader: boolean
125
+ ): ReactNode {
126
+ // Build inside-out to match the client's createElement chain:
127
+ // NavigationProvider(TimberNuqsAdapter(element))
128
+ // → passed as initial to TransitionRoot
129
+ // → TransitionRoot renders PendingNavigationProvider(Fragment(TopLoader, initial))
130
+
131
+ // 1. Innermost: NavigationProvider equivalent
132
+ const withNav = createElement(SsrNavigationProvider, null, element);
133
+
134
+ // 2. TimberNuqsAdapter equivalent (wraps withNuqsSsrAdapter for the actual nuqs provider)
135
+ const withNuqs = createElement(SsrNuqsWrapper, { searchParams, children: withNav });
136
+
137
+ // 3. Outermost: TransitionRoot equivalent (PendingNavigationProvider + TopLoader)
138
+ return createElement(SsrTransitionRoot, { hasTopLoader, children: withNuqs });
139
+ }