@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.
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +4 -3
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/client/index.js +90 -102
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-status-provider.d.ts +1 -1
- package/dist/client/link-status-provider.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +13 -0
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +6 -8
- package/dist/client/transition-root.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/nitro.ts +4 -3
- package/src/client/link-status-provider.tsx +2 -2
- package/src/client/navigation-context.ts +54 -0
- package/src/client/transition-root.tsx +24 -20
- package/src/client/use-navigation-pending.ts +1 -1
- package/dist/client/pending-navigation-context.d.ts +0 -32
- package/dist/client/pending-navigation-context.d.ts.map +0 -1
- package/src/client/pending-navigation-context.ts +0 -66
package/src/adapters/nitro.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 './
|
|
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
|
|
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`
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 './
|
|
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 [
|
|
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.
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
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:
|
|
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 './
|
|
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
|
-
}
|