@timber-js/app 0.1.32 → 0.1.34

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.
@@ -391,9 +391,10 @@ const MIME_TYPES = {
391
391
 
392
392
  const publicDir = join(__dirname, '${publicDir}');
393
393
  const port = parseInt(process.env.PORT || '3000', 10);
394
+ const host = process.env.HOST || process.env.HOSTNAME || 'localhost';
394
395
 
395
396
  const server = createServer(async (req, res) => {
396
- const url = new URL(req.url || '/', \`http://localhost:\${port}\`);
397
+ const url = new URL(req.url || '/', \`http://\${host}:\${port}\`);
397
398
 
398
399
  // Try serving static files from the public directory first.
399
400
  const filePath = join(publicDir, url.pathname);
@@ -485,11 +486,11 @@ const server = createServer(async (req, res) => {
485
486
  }
486
487
  });
487
488
 
488
- server.listen(port, () => {
489
+ server.listen(port, host, () => {
489
490
  console.log();
490
491
  console.log(' ⚡ timber preview server running at:');
491
492
  console.log();
492
- console.log(\` ➜ http://localhost:\${port}\`);
493
+ console.log(\` ➜ http://\${host}:\${port}\`);
493
494
  console.log();
494
495
  });
495
496
  `;
@@ -12,7 +12,7 @@
12
12
 
13
13
  import type { ReactNode } from 'react';
14
14
  import { LinkStatusContext, type LinkStatus } from './use-link-status.js';
15
- import { usePendingNavigationUrl } from './pending-navigation-context.js';
15
+ import { usePendingNavigationUrl } from './navigation-context.js';
16
16
 
17
17
  const NOT_PENDING: LinkStatus = { pending: false };
18
18
  const IS_PENDING: LinkStatus = { pending: true };
@@ -22,7 +22,7 @@ const IS_PENDING: LinkStatus = { pending: true };
22
22
  * and provides a scoped LinkStatusContext to children. Renders no extra DOM —
23
23
  * just a context provider around children.
24
24
  */
25
- export function LinkStatusProvider({ href, children }: { href: string; children: ReactNode }) {
25
+ export function LinkStatusProvider({ href, children }: { href: string; children?: ReactNode }) {
26
26
  const pendingUrl = usePendingNavigationUrl();
27
27
  const status = pendingUrl === href ? IS_PENDING : NOT_PENDING;
28
28
 
@@ -116,3 +116,57 @@ export function setNavigationState(state: NavigationState): void {
116
116
  export function getNavigationState(): NavigationState {
117
117
  return _currentNavState;
118
118
  }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Pending Navigation Context (same module for singleton guarantee)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /**
125
+ * Separate context for the in-flight navigation URL. Provided by
126
+ * TransitionRoot (useOptimistic state), consumed by LinkStatusProvider
127
+ * and useNavigationPending.
128
+ *
129
+ * Lives in this module (not a separate file) to guarantee singleton
130
+ * identity across chunks. The `'use client'` LinkStatusProvider and
131
+ * the non-directive TransitionRoot both import from this module —
132
+ * if they were in separate files, the bundler could duplicate the
133
+ * module-level context variable across chunks.
134
+ */
135
+ let _pendingContext: React.Context<string | null> | undefined;
136
+
137
+ function getOrCreatePendingContext(): React.Context<string | null> | undefined {
138
+ if (_pendingContext !== undefined) return _pendingContext;
139
+ if (typeof React.createContext === 'function') {
140
+ _pendingContext = React.createContext<string | null>(null);
141
+ }
142
+ return _pendingContext;
143
+ }
144
+
145
+ /**
146
+ * Read the pending navigation URL from context.
147
+ * Returns null during SSR (no provider) or in the RSC environment.
148
+ */
149
+ export function usePendingNavigationUrl(): string | null {
150
+ const ctx = getOrCreatePendingContext();
151
+ if (!ctx) return null;
152
+ if (typeof React.useContext !== 'function') return null;
153
+ return React.useContext(ctx);
154
+ }
155
+
156
+ /**
157
+ * Provider for the pending navigation URL. Wraps children with
158
+ * the pending context Provider.
159
+ */
160
+ export function PendingNavigationProvider({
161
+ value,
162
+ children,
163
+ }: {
164
+ value: string | null;
165
+ children?: ReactNode;
166
+ }): React.ReactElement {
167
+ const ctx = getOrCreatePendingContext();
168
+ if (!ctx) {
169
+ return children as React.ReactElement;
170
+ }
171
+ return createElement(ctx.Provider, { value }, children);
172
+ }
@@ -11,14 +11,12 @@
11
11
  * a transition update. React keeps the old committed tree visible while
12
12
  * any new Suspense boundaries in the transition resolve.
13
13
  *
14
- * Also manages `pendingUrl` via `useOptimistic`. During a navigation
15
- * transition, the optimistic value (the target URL) shows immediately
16
- * while the transition is pending, and automatically reverts to null
17
- * when the transition commits. This ensures useLinkStatus and
18
- * useNavigationPending show the pending state immediately and clear
19
- * atomically with the new tree same pattern Next.js uses with
20
- * useOptimistic per Link instance, adapted for timber's server-component
21
- * Link with global click delegation.
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.
22
20
  *
23
21
  * See design/05-streaming.md §"deferSuspenseFor"
24
22
  * See design/19-client-navigation.md §"NavigationContext"
@@ -26,12 +24,11 @@
26
24
 
27
25
  import {
28
26
  useState,
29
- useOptimistic,
30
27
  useTransition,
31
28
  createElement,
32
29
  type ReactNode,
33
30
  } from 'react';
34
- import { PendingNavigationProvider } from './pending-navigation-context.js';
31
+ import { PendingNavigationProvider } from './navigation-context.js';
35
32
 
36
33
  // ─── Module-level functions ──────────────────────────────────────
37
34
 
@@ -72,10 +69,7 @@ let _navigateTransition: ((
72
69
  */
73
70
  export function TransitionRoot({ initial }: { initial: ReactNode }): ReactNode {
74
71
  const [element, setElement] = useState<ReactNode>(initial);
75
- const [optimisticPendingUrl, setOptimisticPendingUrl] = useOptimistic<string | null>(null);
76
- // useTransition's startTransition (not the standalone import) creates an
77
- // action context that useOptimistic can track. The standalone startTransition
78
- // doesn't — optimistic values would never show.
72
+ const [pendingUrl, setPendingUrl] = useState<string | null>(null);
79
73
  const [, startTransition] = useTransition();
80
74
 
81
75
  // Non-navigation render (revalidation, popstate cached replay).
@@ -85,25 +79,35 @@ export function TransitionRoot({ initial }: { initial: ReactNode }): ReactNode {
85
79
  });
86
80
  };
87
81
 
88
- // Full navigation transition. The entire navigation (fetch + state updates)
89
- // runs inside startTransition. useOptimistic shows the pending URL immediately
90
- // (urgent) and reverts to null when the transition commits (atomic with new tree).
91
- _navigateTransition = (pendingUrl: string, perform: () => Promise<ReactNode>) => {
82
+ // Full navigation transition.
83
+ // setPendingUrl(url) is an URGENT update React commits it before the next
84
+ // paint, so the pending spinner appears immediately when navigation starts.
85
+ // Inside startTransition: the async fetch + setElement + setPendingUrl(null)
86
+ // are deferred. When the transition commits, the new tree and pendingUrl=null
87
+ // both apply in the same React commit — making the pending→active transition
88
+ // atomic (no frame where pending is false but the old tree is still visible).
89
+ _navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {
90
+ // Urgent: show pending state immediately
91
+ setPendingUrl(url);
92
+
92
93
  return new Promise<void>((resolve, reject) => {
93
94
  startTransition(async () => {
94
95
  try {
95
- setOptimisticPendingUrl(pendingUrl);
96
96
  const newElement = await perform();
97
97
  setElement(newElement);
98
+ // Clear pending inside the transition — commits atomically with new tree
99
+ setPendingUrl(null);
98
100
  resolve();
99
101
  } catch (err) {
102
+ // Clear pending on error too
103
+ setPendingUrl(null);
100
104
  reject(err);
101
105
  }
102
106
  });
103
107
  });
104
108
  };
105
109
 
106
- return createElement(PendingNavigationProvider, { value: optimisticPendingUrl }, element);
110
+ return createElement(PendingNavigationProvider, { value: pendingUrl }, element);
107
111
  }
108
112
 
109
113
  // ─── Public API ──────────────────────────────────────────────────
@@ -5,7 +5,7 @@
5
5
  // pending state shows immediately (urgent update) and clears atomically
6
6
  // with the new tree (same startTransition commit).
7
7
 
8
- import { usePendingNavigationUrl } from './pending-navigation-context.js';
8
+ import { usePendingNavigationUrl } from './navigation-context.js';
9
9
 
10
10
  /**
11
11
  * Returns true while an RSC navigation is in flight.
@@ -1,32 +0,0 @@
1
- /**
2
- * PendingNavigationContext — React context for the in-flight navigation URL.
3
- *
4
- * Provided by TransitionRoot. The value is the URL being navigated to,
5
- * or null when idle. Used by:
6
- * - LinkStatusProvider to show per-link pending spinners
7
- * - useNavigationPending to return a global pending boolean
8
- *
9
- * The pending URL is set as an URGENT update (shows immediately) and
10
- * cleared inside startTransition (commits atomically with the new tree).
11
- * This ensures pending state appears instantly on navigation start and
12
- * disappears in the same React commit as the new params/tree.
13
- *
14
- * Separate from NavigationContext (which holds params + pathname) because
15
- * the pending URL is managed as React state in TransitionRoot, while
16
- * params/pathname are set via module-level state read by renderRoot.
17
- * Both contexts commit together in the same transition.
18
- *
19
- * See design/19-client-navigation.md §"NavigationContext"
20
- */
21
- import React, { type ReactNode } from 'react';
22
- /**
23
- * Read the pending navigation URL from context.
24
- * Returns null during SSR (no provider) or in the RSC environment.
25
- * Internal — used by LinkStatusProvider and useNavigationPending.
26
- */
27
- export declare function usePendingNavigationUrl(): string | null;
28
- export declare function PendingNavigationProvider({ value, children, }: {
29
- value: string | null;
30
- children?: ReactNode;
31
- }): React.ReactElement;
32
- //# sourceMappingURL=pending-navigation-context.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pending-navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/pending-navigation-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAgB7D;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAKvD;AAMD,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAMrB"}
@@ -1,66 +0,0 @@
1
- /**
2
- * PendingNavigationContext — React context for the in-flight navigation URL.
3
- *
4
- * Provided by TransitionRoot. The value is the URL being navigated to,
5
- * or null when idle. Used by:
6
- * - LinkStatusProvider to show per-link pending spinners
7
- * - useNavigationPending to return a global pending boolean
8
- *
9
- * The pending URL is set as an URGENT update (shows immediately) and
10
- * cleared inside startTransition (commits atomically with the new tree).
11
- * This ensures pending state appears instantly on navigation start and
12
- * disappears in the same React commit as the new params/tree.
13
- *
14
- * Separate from NavigationContext (which holds params + pathname) because
15
- * the pending URL is managed as React state in TransitionRoot, while
16
- * params/pathname are set via module-level state read by renderRoot.
17
- * Both contexts commit together in the same transition.
18
- *
19
- * See design/19-client-navigation.md §"NavigationContext"
20
- */
21
-
22
- import React, { createElement, type ReactNode } from 'react';
23
-
24
- // ---------------------------------------------------------------------------
25
- // Lazy context initialization (same pattern as NavigationContext)
26
- // ---------------------------------------------------------------------------
27
-
28
- let _context: React.Context<string | null> | undefined;
29
-
30
- function getOrCreateContext(): React.Context<string | null> | undefined {
31
- if (_context !== undefined) return _context;
32
- if (typeof React.createContext === 'function') {
33
- _context = React.createContext<string | null>(null);
34
- }
35
- return _context;
36
- }
37
-
38
- /**
39
- * Read the pending navigation URL from context.
40
- * Returns null during SSR (no provider) or in the RSC environment.
41
- * Internal — used by LinkStatusProvider and useNavigationPending.
42
- */
43
- export function usePendingNavigationUrl(): string | null {
44
- const ctx = getOrCreateContext();
45
- if (!ctx) return null;
46
- if (typeof React.useContext !== 'function') return null;
47
- return React.useContext(ctx);
48
- }
49
-
50
- // ---------------------------------------------------------------------------
51
- // Provider component
52
- // ---------------------------------------------------------------------------
53
-
54
- export function PendingNavigationProvider({
55
- value,
56
- children,
57
- }: {
58
- value: string | null;
59
- children?: ReactNode;
60
- }): React.ReactElement {
61
- const ctx = getOrCreateContext();
62
- if (!ctx) {
63
- return children as React.ReactElement;
64
- }
65
- return createElement(ctx.Provider, { value }, children);
66
- }