@timber-js/app 0.2.0-alpha.67 → 0.2.0-alpha.69

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 (51) hide show
  1. package/LICENSE +8 -0
  2. package/dist/client/history.d.ts +19 -4
  3. package/dist/client/history.d.ts.map +1 -1
  4. package/dist/client/index.js +321 -167
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/link-pending-store.d.ts +3 -3
  7. package/dist/client/link.d.ts.map +1 -1
  8. package/dist/client/nav-link-store.d.ts +36 -0
  9. package/dist/client/nav-link-store.d.ts.map +1 -0
  10. package/dist/client/navigation-api-types.d.ts +90 -0
  11. package/dist/client/navigation-api-types.d.ts.map +1 -0
  12. package/dist/client/navigation-api.d.ts +115 -0
  13. package/dist/client/navigation-api.d.ts.map +1 -0
  14. package/dist/client/navigation-context.d.ts +11 -0
  15. package/dist/client/navigation-context.d.ts.map +1 -1
  16. package/dist/client/{transition-root.d.ts → navigation-root.d.ts} +31 -9
  17. package/dist/client/navigation-root.d.ts.map +1 -0
  18. package/dist/client/nuqs-adapter.d.ts.map +1 -1
  19. package/dist/client/router.d.ts +46 -2
  20. package/dist/client/router.d.ts.map +1 -1
  21. package/dist/client/rsc-fetch.d.ts +1 -1
  22. package/dist/client/rsc-fetch.d.ts.map +1 -1
  23. package/dist/client/top-loader.d.ts +2 -2
  24. package/dist/client/top-loader.d.ts.map +1 -1
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/server/route-element-builder.d.ts +10 -0
  27. package/dist/server/route-element-builder.d.ts.map +1 -1
  28. package/dist/server/slot-resolver.d.ts.map +1 -1
  29. package/dist/server/ssr-wrappers.d.ts +3 -3
  30. package/package.json +6 -7
  31. package/src/cli.ts +0 -0
  32. package/src/client/browser-entry.ts +92 -19
  33. package/src/client/history.ts +26 -4
  34. package/src/client/link-pending-store.ts +3 -3
  35. package/src/client/link.tsx +31 -9
  36. package/src/client/nav-link-store.ts +47 -0
  37. package/src/client/navigation-api-types.ts +112 -0
  38. package/src/client/navigation-api.ts +315 -0
  39. package/src/client/navigation-context.ts +22 -2
  40. package/src/client/navigation-root.tsx +346 -0
  41. package/src/client/nuqs-adapter.tsx +16 -3
  42. package/src/client/router.ts +186 -18
  43. package/src/client/rsc-fetch.ts +4 -3
  44. package/src/client/top-loader.tsx +12 -4
  45. package/src/client/use-navigation-pending.ts +1 -1
  46. package/src/server/route-element-builder.ts +69 -21
  47. package/src/server/slot-resolver.ts +37 -35
  48. package/src/server/ssr-entry.ts +1 -1
  49. package/src/server/ssr-wrappers.tsx +10 -10
  50. package/dist/client/transition-root.d.ts.map +0 -1
  51. package/src/client/transition-root.tsx +0 -205
@@ -8,7 +8,7 @@
8
8
  * Radix UI that rely on useId() internally.
9
9
  *
10
10
  * The client tree (browser-entry.ts) wraps the RSC element with:
11
- * TransitionRoot → PendingNavigationProvider → Fragment(TopLoader, ...) →
11
+ * NavigationRoot → PendingNavigationProvider → Fragment(TopLoader, ...) →
12
12
  * TimberNuqsAdapter → NuqsAdapterProvider → NavigationProvider → element
13
13
  *
14
14
  * The SSR tree must produce the same component boundaries. These wrappers
@@ -23,15 +23,15 @@ import { createElement, Fragment, type ReactNode } from 'react';
23
23
  import { withNuqsSsrAdapter } from './nuqs-ssr-provider.js';
24
24
 
25
25
  /**
26
- * SSR equivalent of TransitionRoot.
26
+ * SSR equivalent of NavigationRoot.
27
27
  *
28
- * On the client, TransitionRoot uses useState + useTransition and renders:
28
+ * On the client, NavigationRoot uses useState and standalone startTransition, rendering:
29
29
  * PendingNavigationProvider(Fragment(TopLoader, element))
30
30
  *
31
31
  * This SSR version matches the component boundary depth without client
32
32
  * hooks. It renders SsrPendingProvider → Fragment(SsrTopLoader, children).
33
33
  */
34
- function SsrTransitionRoot({
34
+ function SsrNavigationRoot({
35
35
  children,
36
36
  hasTopLoader,
37
37
  }: {
@@ -97,7 +97,7 @@ function SsrNuqsWrapper({
97
97
  * on both sides.
98
98
  *
99
99
  * Client tree (browser-entry.ts):
100
- * TransitionRoot
100
+ * NavigationRoot
101
101
  * → PendingNavigationProvider
102
102
  * → Fragment(TopLoader, element)
103
103
  * → TimberNuqsAdapter
@@ -106,7 +106,7 @@ function SsrNuqsWrapper({
106
106
  * → [RSC element]
107
107
  *
108
108
  * SSR tree (this function):
109
- * SsrTransitionRoot
109
+ * SsrNavigationRoot
110
110
  * → SsrPendingProvider
111
111
  * → Fragment(SsrTopLoader, element)
112
112
  * → SsrNuqsWrapper
@@ -125,8 +125,8 @@ export function wrapSsrElement(
125
125
  ): ReactNode {
126
126
  // Build inside-out to match the client's createElement chain:
127
127
  // NavigationProvider(TimberNuqsAdapter(element))
128
- // → passed as initial to TransitionRoot
129
- // → TransitionRoot renders PendingNavigationProvider(Fragment(TopLoader, initial))
128
+ // → passed as initial to NavigationRoot
129
+ // → NavigationRoot renders PendingNavigationProvider(Fragment(TopLoader, initial))
130
130
 
131
131
  // 1. Innermost: NavigationProvider equivalent
132
132
  const withNav = createElement(SsrNavigationProvider, null, element);
@@ -134,6 +134,6 @@ export function wrapSsrElement(
134
134
  // 2. TimberNuqsAdapter equivalent (wraps withNuqsSsrAdapter for the actual nuqs provider)
135
135
  const withNuqs = createElement(SsrNuqsWrapper, { searchParams, children: withNav });
136
136
 
137
- // 3. Outermost: TransitionRoot equivalent (PendingNavigationProvider + TopLoader)
138
- return createElement(SsrTransitionRoot, { hasTopLoader, children: withNuqs });
137
+ // 3. Outermost: NavigationRoot equivalent (PendingNavigationProvider + TopLoader)
138
+ return createElement(SsrNavigationRoot, { hasTopLoader, children: withNuqs });
139
139
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"transition-root.d.ts","sourceRoot":"","sources":["../../src/client/transition-root.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAsBlE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,GAAG,SAAS,CA2DZ;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,CAIzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI,CAc5F"}
@@ -1,205 +0,0 @@
1
- /**
2
- * TransitionRoot — Wrapper component for transition-based rendering.
3
- *
4
- * Solves the "new boundary has no old content" problem for client-side
5
- * navigation. When React renders a completely new Suspense boundary via
6
- * root.render(), it shows the fallback immediately — root.render() is
7
- * always an urgent update regardless of startTransition.
8
- *
9
- * TransitionRoot holds the current element in React state. Navigation
10
- * updates call startTransition(() => setState(newElement)), which IS
11
- * a transition update. React keeps the old committed tree visible while
12
- * any new Suspense boundaries in the transition resolve.
13
- *
14
- * Also manages `pendingUrl` as React state with an urgent/transition split:
15
- * - Navigation START: `setPendingUrl(url)` is an urgent update — React
16
- * commits it before the next paint, showing the spinner immediately.
17
- * - Navigation END: `setPendingUrl(null)` is inside `startTransition`
18
- * alongside `setElement(newTree)` — both commit atomically, so the
19
- * spinner disappears in the same frame as the new content appears.
20
- *
21
- * See design/05-streaming.md §"deferSuspenseFor"
22
- * See design/19-client-navigation.md §"NavigationContext"
23
- */
24
-
25
- import { useState, useTransition, createElement, Fragment, type ReactNode } from 'react';
26
- import { PendingNavigationProvider } from './navigation-context.js';
27
- import { TopLoader, type TopLoaderConfig } from './top-loader.js';
28
- import { getCurrentNavId, resetLinkPending } from './link-pending-store.js';
29
-
30
- // ─── Module-level functions ──────────────────────────────────────
31
-
32
- /**
33
- * Module-level reference to the state setter wrapped in startTransition.
34
- * Used for non-navigation renders (applyRevalidation, popstate replay).
35
- */
36
- let _transitionRender: ((element: ReactNode) => void) | null = null;
37
-
38
- /**
39
- * Module-level reference to the navigation transition function.
40
- * Wraps a full navigation (fetch + render) in a single startTransition
41
- * with useOptimistic for the pending URL.
42
- */
43
- let _navigateTransition:
44
- | ((pendingUrl: string, perform: () => Promise<ReactNode>) => Promise<void>)
45
- | null = null;
46
-
47
- // ─── Component ───────────────────────────────────────────────────
48
-
49
- /**
50
- * Root wrapper component that enables transition-based rendering.
51
- *
52
- * Renders PendingNavigationProvider around children for the pending URL
53
- * context. The DOM tree matches the server-rendered HTML during hydration
54
- * (the provider renders no extra DOM elements).
55
- *
56
- * Usage in browser-entry.ts:
57
- * const rootEl = createElement(TransitionRoot, { initial: wrapped });
58
- * reactRoot = hydrateRoot(document, rootEl);
59
- *
60
- * Subsequent navigations:
61
- * navigateTransition(url, async () => { fetch; return wrappedElement; });
62
- *
63
- * Non-navigation renders:
64
- * transitionRender(newWrappedElement);
65
- */
66
- export function TransitionRoot({
67
- initial,
68
- topLoaderConfig,
69
- }: {
70
- initial: ReactNode;
71
- topLoaderConfig?: TopLoaderConfig;
72
- }): ReactNode {
73
- const [element, setElement] = useState<ReactNode>(initial);
74
- const [pendingUrl, setPendingUrl] = useState<string | null>(null);
75
- const [, startTransition] = useTransition();
76
-
77
- // Non-navigation render (revalidation, popstate cached replay).
78
- _transitionRender = (newElement: ReactNode) => {
79
- startTransition(() => {
80
- setElement(newElement);
81
- });
82
- };
83
-
84
- // Full navigation transition.
85
- // setPendingUrl(url) is an URGENT update — React commits it before the next
86
- // paint, so the pending spinner appears immediately when navigation starts.
87
- // Inside startTransition: the async fetch + setElement + setPendingUrl(null)
88
- // are deferred. When the transition commits, the new tree and pendingUrl=null
89
- // both apply in the same React commit — making the pending→active transition
90
- // atomic (no frame where pending is false but the old tree is still visible).
91
- _navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {
92
- // Urgent: show pending state immediately (for TopLoader / useNavigationPending)
93
- setPendingUrl(url);
94
-
95
- return new Promise<void>((resolve, reject) => {
96
- startTransition(async () => {
97
- // Capture the current nav ID before async work begins.
98
- // Used to guard against stale clears when a newer navigation
99
- // supersedes this one.
100
- const navId = getCurrentNavId();
101
- try {
102
- const newElement = await perform();
103
- setElement(newElement);
104
- // Clear pending inside the transition — commits atomically with new tree
105
- setPendingUrl(null);
106
- // Reset per-link pending state. The navId guard ensures a stale
107
- // transition (T1) doesn't clear a newer navigation's (T2) link.
108
- // The setter call is a transition update — batched with setElement
109
- // and setPendingUrl, so pending clears atomically with new tree.
110
- // See design/19-client-navigation.md §"Per-Link Pending State"
111
- resetLinkPending(navId);
112
- resolve();
113
- } catch (err) {
114
- // Clear pending on error too
115
- setPendingUrl(null);
116
- resetLinkPending(navId);
117
- reject(err);
118
- }
119
- });
120
- });
121
- };
122
-
123
- // Inject TopLoader alongside the element tree inside PendingNavigationProvider.
124
- // The TopLoader reads pendingUrl from context to show/hide the progress bar.
125
- // It is rendered only when not explicitly disabled via config.
126
- const showTopLoader = topLoaderConfig?.enabled !== false;
127
- const children = showTopLoader
128
- ? createElement(Fragment, null, createElement(TopLoader, { config: topLoaderConfig }), element)
129
- : element;
130
- return createElement(PendingNavigationProvider, { value: pendingUrl }, children);
131
- }
132
-
133
- // ─── Public API ──────────────────────────────────────────────────
134
-
135
- /**
136
- * Trigger a transition render for non-navigation updates.
137
- * React keeps the old committed tree visible while any new Suspense
138
- * boundaries in the update resolve.
139
- *
140
- * Used for: applyRevalidation, popstate replay with cached payload.
141
- */
142
- export function transitionRender(element: ReactNode): void {
143
- if (_transitionRender) {
144
- _transitionRender(element);
145
- }
146
- }
147
-
148
- /**
149
- * Run a full navigation inside a React transition with optimistic pending URL.
150
- *
151
- * The `perform` callback runs inside `startTransition` — it should fetch the
152
- * RSC payload, update router state, and return the wrapped React element.
153
- * The pending URL shows immediately (useOptimistic urgent update) and reverts
154
- * to null when the transition commits (atomic with the new tree).
155
- *
156
- * Returns a Promise that resolves when the async work completes (note: the
157
- * React transition may not have committed yet, but all state updates are done).
158
- *
159
- * Used for: navigate(), refresh(), popstate with fetch.
160
- */
161
- export function navigateTransition(
162
- pendingUrl: string,
163
- perform: () => Promise<ReactNode>
164
- ): Promise<void> {
165
- if (_navigateTransition) {
166
- return _navigateTransition(pendingUrl, perform);
167
- }
168
- // Fallback: no TransitionRoot mounted (shouldn't happen in production)
169
- return perform().then(() => {});
170
- }
171
-
172
- /**
173
- * Check if the TransitionRoot is mounted and ready for renders.
174
- * Used by browser-entry.ts to guard against renders before hydration.
175
- */
176
- export function isTransitionRootReady(): boolean {
177
- return _transitionRender !== null;
178
- }
179
-
180
- /**
181
- * Install one-shot deferred callbacks for the no-RSC bootstrap path (TIM-600).
182
- *
183
- * When there's no RSC payload, we can't create a React root immediately —
184
- * `createRoot(document).render(...)` would blank the SSR HTML. Instead,
185
- * this sets up `_transitionRender` and `_navigateTransition` so that the
186
- * first client navigation triggers root creation via `createAndMount`.
187
- *
188
- * After `createAndMount` runs, TransitionRoot renders and overwrites these
189
- * callbacks with its real `startTransition`-based implementations.
190
- */
191
- export function installDeferredNavigation(createAndMount: (initial: ReactNode) => void): void {
192
- let mounted = false;
193
- const mountOnce = (element: ReactNode) => {
194
- if (mounted) return;
195
- mounted = true;
196
- createAndMount(element);
197
- };
198
- _transitionRender = (element: ReactNode) => {
199
- mountOnce(element);
200
- };
201
- _navigateTransition = async (_pendingUrl: string, perform: () => Promise<ReactNode>) => {
202
- const element = await perform();
203
- mountOnce(element);
204
- };
205
- }