@timber-js/app 0.1.29 → 0.1.30
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/client/index.js +128 -124
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-status-provider.d.ts +10 -4
- package/dist/client/link-status-provider.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +8 -0
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/use-navigation-pending.d.ts.map +1 -1
- package/dist/index.js +120 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/chunks.d.ts +17 -6
- package/dist/plugins/chunks.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +3 -0
- package/src/client/link-status-provider.tsx +18 -24
- package/src/client/navigation-context.ts +9 -1
- package/src/client/router.ts +23 -4
- package/src/client/use-navigation-pending.ts +10 -17
- package/src/plugins/chunks.ts +145 -17
package/dist/client/index.js
CHANGED
|
@@ -67,53 +67,104 @@ function useLinkStatus() {
|
|
|
67
67
|
return useContext(LinkStatusContext);
|
|
68
68
|
}
|
|
69
69
|
//#endregion
|
|
70
|
-
//#region src/client/
|
|
70
|
+
//#region src/client/navigation-context.ts
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
72
|
+
* NavigationContext — React context for navigation state.
|
|
73
|
+
*
|
|
74
|
+
* Holds the current route params and pathname, updated atomically
|
|
75
|
+
* with the RSC tree on each navigation. This replaces the previous
|
|
76
|
+
* useSyncExternalStore approach for useParams() and usePathname(),
|
|
77
|
+
* which suffered from a timing gap: the new tree could commit before
|
|
78
|
+
* the external store re-renders fired, causing a frame where both
|
|
79
|
+
* old and new active states were visible simultaneously.
|
|
80
|
+
*
|
|
81
|
+
* By wrapping the RSC payload element in NavigationProvider inside
|
|
82
|
+
* renderRoot(), the context value and the element tree are passed to
|
|
83
|
+
* reactRoot.render() in the same call — atomic by construction.
|
|
84
|
+
* All consumers (useParams, usePathname) see the new values in the
|
|
85
|
+
* same render pass as the new tree.
|
|
86
|
+
*
|
|
87
|
+
* During SSR, no NavigationProvider is mounted. Hooks fall back to
|
|
88
|
+
* the ALS-backed getSsrData() for per-request isolation.
|
|
89
|
+
*
|
|
90
|
+
* IMPORTANT: createContext and useContext are NOT available in the RSC
|
|
91
|
+
* environment (React Server Components use a stripped-down React).
|
|
92
|
+
* The context is lazily initialized on first access, and all functions
|
|
93
|
+
* that depend on these APIs are safe to call from any environment —
|
|
94
|
+
* they return null or no-op when the APIs aren't available.
|
|
95
|
+
*
|
|
96
|
+
* See design/19-client-navigation.md §"NavigationContext"
|
|
73
97
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The context is created lazily to avoid calling createContext at module
|
|
100
|
+
* level. In the RSC environment, React.createContext doesn't exist —
|
|
101
|
+
* calling it at import time would crash the server.
|
|
102
|
+
*/
|
|
103
|
+
var _context;
|
|
104
|
+
function getOrCreateContext() {
|
|
105
|
+
if (_context !== void 0) return _context;
|
|
106
|
+
if (typeof React.createContext === "function") _context = React.createContext(null);
|
|
107
|
+
return _context;
|
|
76
108
|
}
|
|
77
109
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
110
|
+
* Read the navigation context. Returns null during SSR (no provider)
|
|
111
|
+
* or in the RSC environment (no context available).
|
|
112
|
+
* Internal — used by useParams() and usePathname().
|
|
80
113
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
return
|
|
114
|
+
function useNavigationContext() {
|
|
115
|
+
const ctx = getOrCreateContext();
|
|
116
|
+
if (!ctx) return null;
|
|
117
|
+
if (typeof React.useContext !== "function") return null;
|
|
118
|
+
return React.useContext(ctx);
|
|
84
119
|
}
|
|
85
120
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
121
|
+
* Wraps children with NavigationContext.Provider.
|
|
122
|
+
*
|
|
123
|
+
* Used in browser-entry.ts renderRoot to wrap the RSC payload element
|
|
124
|
+
* so that navigation state updates atomically with the tree render.
|
|
89
125
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
126
|
+
function NavigationProvider({ value, children }) {
|
|
127
|
+
const ctx = getOrCreateContext();
|
|
128
|
+
if (!ctx) return children;
|
|
129
|
+
return createElement(ctx.Provider, { value }, children);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Module-level navigation state. Updated by the router before calling
|
|
133
|
+
* renderRoot(). The renderRoot callback reads this to create the
|
|
134
|
+
* NavigationProvider with the correct values.
|
|
135
|
+
*
|
|
136
|
+
* This is NOT used by hooks directly — hooks read from React context.
|
|
137
|
+
* This exists only as a communication channel between the router
|
|
138
|
+
* (which knows the new nav state) and renderRoot (which wraps the element).
|
|
139
|
+
*/
|
|
140
|
+
var _currentNavState = {
|
|
141
|
+
params: {},
|
|
142
|
+
pathname: "/",
|
|
143
|
+
pendingUrl: null
|
|
144
|
+
};
|
|
145
|
+
function setNavigationState(state) {
|
|
146
|
+
_currentNavState = state;
|
|
147
|
+
}
|
|
148
|
+
function getNavigationState() {
|
|
149
|
+
return _currentNavState;
|
|
92
150
|
}
|
|
93
151
|
//#endregion
|
|
94
152
|
//#region src/client/link-status-provider.tsx
|
|
95
153
|
var NOT_PENDING = { pending: false };
|
|
96
154
|
var IS_PENDING = { pending: true };
|
|
97
155
|
/**
|
|
98
|
-
* Client component that
|
|
99
|
-
* a scoped LinkStatusContext to children. Renders no extra DOM —
|
|
100
|
-
* context provider around children.
|
|
156
|
+
* Client component that reads the pending URL from NavigationContext and
|
|
157
|
+
* provides a scoped LinkStatusContext to children. Renders no extra DOM —
|
|
158
|
+
* just a context provider around children.
|
|
159
|
+
*
|
|
160
|
+
* Because pendingUrl lives in NavigationContext alongside params and pathname,
|
|
161
|
+
* all three update in the same React commit via renderRoot(). This eliminates
|
|
162
|
+
* the two-commit timing gap that existed when pendingUrl was read via
|
|
163
|
+
* useSyncExternalStore (external module-level state) while params came from
|
|
164
|
+
* NavigationContext (React context).
|
|
101
165
|
*/
|
|
102
166
|
function LinkStatusProvider({ href, children }) {
|
|
103
|
-
const status =
|
|
104
|
-
try {
|
|
105
|
-
return getRouter().onPendingChange(callback);
|
|
106
|
-
} catch {
|
|
107
|
-
return () => {};
|
|
108
|
-
}
|
|
109
|
-
}, () => {
|
|
110
|
-
try {
|
|
111
|
-
if (getRouter().getPendingUrl() === href) return IS_PENDING;
|
|
112
|
-
return NOT_PENDING;
|
|
113
|
-
} catch {
|
|
114
|
-
return NOT_PENDING;
|
|
115
|
-
}
|
|
116
|
-
}, () => NOT_PENDING);
|
|
167
|
+
const status = useNavigationContext()?.pendingUrl === href ? IS_PENDING : NOT_PENDING;
|
|
117
168
|
return /* @__PURE__ */ jsx(LinkStatusContext.Provider, {
|
|
118
169
|
value: status,
|
|
119
170
|
children
|
|
@@ -356,87 +407,6 @@ var HistoryStack = class {
|
|
|
356
407
|
}
|
|
357
408
|
};
|
|
358
409
|
//#endregion
|
|
359
|
-
//#region src/client/navigation-context.ts
|
|
360
|
-
/**
|
|
361
|
-
* NavigationContext — React context for navigation state.
|
|
362
|
-
*
|
|
363
|
-
* Holds the current route params and pathname, updated atomically
|
|
364
|
-
* with the RSC tree on each navigation. This replaces the previous
|
|
365
|
-
* useSyncExternalStore approach for useParams() and usePathname(),
|
|
366
|
-
* which suffered from a timing gap: the new tree could commit before
|
|
367
|
-
* the external store re-renders fired, causing a frame where both
|
|
368
|
-
* old and new active states were visible simultaneously.
|
|
369
|
-
*
|
|
370
|
-
* By wrapping the RSC payload element in NavigationProvider inside
|
|
371
|
-
* renderRoot(), the context value and the element tree are passed to
|
|
372
|
-
* reactRoot.render() in the same call — atomic by construction.
|
|
373
|
-
* All consumers (useParams, usePathname) see the new values in the
|
|
374
|
-
* same render pass as the new tree.
|
|
375
|
-
*
|
|
376
|
-
* During SSR, no NavigationProvider is mounted. Hooks fall back to
|
|
377
|
-
* the ALS-backed getSsrData() for per-request isolation.
|
|
378
|
-
*
|
|
379
|
-
* IMPORTANT: createContext and useContext are NOT available in the RSC
|
|
380
|
-
* environment (React Server Components use a stripped-down React).
|
|
381
|
-
* The context is lazily initialized on first access, and all functions
|
|
382
|
-
* that depend on these APIs are safe to call from any environment —
|
|
383
|
-
* they return null or no-op when the APIs aren't available.
|
|
384
|
-
*
|
|
385
|
-
* See design/19-client-navigation.md §"NavigationContext"
|
|
386
|
-
*/
|
|
387
|
-
/**
|
|
388
|
-
* The context is created lazily to avoid calling createContext at module
|
|
389
|
-
* level. In the RSC environment, React.createContext doesn't exist —
|
|
390
|
-
* calling it at import time would crash the server.
|
|
391
|
-
*/
|
|
392
|
-
var _context;
|
|
393
|
-
function getOrCreateContext() {
|
|
394
|
-
if (_context !== void 0) return _context;
|
|
395
|
-
if (typeof React.createContext === "function") _context = React.createContext(null);
|
|
396
|
-
return _context;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Read the navigation context. Returns null during SSR (no provider)
|
|
400
|
-
* or in the RSC environment (no context available).
|
|
401
|
-
* Internal — used by useParams() and usePathname().
|
|
402
|
-
*/
|
|
403
|
-
function useNavigationContext() {
|
|
404
|
-
const ctx = getOrCreateContext();
|
|
405
|
-
if (!ctx) return null;
|
|
406
|
-
if (typeof React.useContext !== "function") return null;
|
|
407
|
-
return React.useContext(ctx);
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Wraps children with NavigationContext.Provider.
|
|
411
|
-
*
|
|
412
|
-
* Used in browser-entry.ts renderRoot to wrap the RSC payload element
|
|
413
|
-
* so that navigation state updates atomically with the tree render.
|
|
414
|
-
*/
|
|
415
|
-
function NavigationProvider({ value, children }) {
|
|
416
|
-
const ctx = getOrCreateContext();
|
|
417
|
-
if (!ctx) return children;
|
|
418
|
-
return createElement(ctx.Provider, { value }, children);
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Module-level navigation state. Updated by the router before calling
|
|
422
|
-
* renderRoot(). The renderRoot callback reads this to create the
|
|
423
|
-
* NavigationProvider with the correct values.
|
|
424
|
-
*
|
|
425
|
-
* This is NOT used by hooks directly — hooks read from React context.
|
|
426
|
-
* This exists only as a communication channel between the router
|
|
427
|
-
* (which knows the new nav state) and renderRoot (which wraps the element).
|
|
428
|
-
*/
|
|
429
|
-
var _currentNavState = {
|
|
430
|
-
params: {},
|
|
431
|
-
pathname: "/"
|
|
432
|
-
};
|
|
433
|
-
function setNavigationState(state) {
|
|
434
|
-
_currentNavState = state;
|
|
435
|
-
}
|
|
436
|
-
function getNavigationState() {
|
|
437
|
-
return _currentNavState;
|
|
438
|
-
}
|
|
439
|
-
//#endregion
|
|
440
410
|
//#region src/client/use-params.ts
|
|
441
411
|
/**
|
|
442
412
|
* Set the current route params in the module-level store.
|
|
@@ -616,12 +586,21 @@ function createRouter(deps) {
|
|
|
616
586
|
let pending = false;
|
|
617
587
|
let pendingUrl = null;
|
|
618
588
|
const pendingListeners = /* @__PURE__ */ new Set();
|
|
589
|
+
/** Last rendered payload — used to re-render at navigation start with pendingUrl set. */
|
|
590
|
+
let lastRenderedPayload = null;
|
|
619
591
|
function setPending(value, url) {
|
|
620
592
|
const newPendingUrl = value && url ? url : null;
|
|
621
593
|
if (pending === value && pendingUrl === newPendingUrl) return;
|
|
622
594
|
pending = value;
|
|
623
595
|
pendingUrl = newPendingUrl;
|
|
624
596
|
for (const listener of pendingListeners) listener(value);
|
|
597
|
+
if (value && lastRenderedPayload !== null) {
|
|
598
|
+
setNavigationState({
|
|
599
|
+
...getNavigationState(),
|
|
600
|
+
pendingUrl: newPendingUrl
|
|
601
|
+
});
|
|
602
|
+
renderPayload(lastRenderedPayload);
|
|
603
|
+
}
|
|
625
604
|
}
|
|
626
605
|
/** Update the segment cache from server-provided segment metadata. */
|
|
627
606
|
function updateSegmentCache(segmentInfo) {
|
|
@@ -631,22 +610,29 @@ function createRouter(deps) {
|
|
|
631
610
|
}
|
|
632
611
|
/** Render a decoded RSC payload into the DOM if a renderer is available. */
|
|
633
612
|
function renderPayload(payload) {
|
|
613
|
+
lastRenderedPayload = payload;
|
|
634
614
|
if (deps.renderRoot) deps.renderRoot(payload);
|
|
635
615
|
}
|
|
636
616
|
/**
|
|
637
|
-
* Update navigation state (params + pathname) for the next render.
|
|
617
|
+
* Update navigation state (params + pathname + pendingUrl) for the next render.
|
|
638
618
|
*
|
|
639
619
|
* Sets both the module-level fallback (for tests and SSR) and the
|
|
640
620
|
* navigation context state (read by renderRoot to wrap the element
|
|
641
621
|
* in NavigationProvider). The context update is atomic with the tree
|
|
642
622
|
* render — both are passed to reactRoot.render() in the same call.
|
|
623
|
+
*
|
|
624
|
+
* pendingUrl is included so that LinkStatusProvider (which reads from
|
|
625
|
+
* NavigationContext) sees the pending state change in the same React
|
|
626
|
+
* commit as params/pathname — preventing the gap where the spinner
|
|
627
|
+
* disappears before the active state updates.
|
|
643
628
|
*/
|
|
644
|
-
function updateNavigationState(params, url) {
|
|
629
|
+
function updateNavigationState(params, url, navPendingUrl = null) {
|
|
645
630
|
const resolvedParams = params ?? {};
|
|
646
631
|
setCurrentParams(resolvedParams);
|
|
647
632
|
setNavigationState({
|
|
648
633
|
params: resolvedParams,
|
|
649
|
-
pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/"
|
|
634
|
+
pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/",
|
|
635
|
+
pendingUrl: navPendingUrl
|
|
650
636
|
});
|
|
651
637
|
}
|
|
652
638
|
/** Apply head elements (title, meta tags) to the DOM if available. */
|
|
@@ -823,15 +809,33 @@ function createRouter(deps) {
|
|
|
823
809
|
* ```
|
|
824
810
|
*/
|
|
825
811
|
function useNavigationPending() {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
812
|
+
const navState = useNavigationContext();
|
|
813
|
+
if (!navState) return false;
|
|
814
|
+
return navState.pendingUrl !== null;
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/client/router-ref.ts
|
|
818
|
+
/**
|
|
819
|
+
* Set the global router instance. Called once during bootstrap.
|
|
820
|
+
*/
|
|
821
|
+
function setGlobalRouter(router) {
|
|
822
|
+
_setGlobalRouter(router);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Get the global router instance. Throws if called before bootstrap.
|
|
826
|
+
* Used by client-side hooks (useNavigationPending, etc.)
|
|
827
|
+
*/
|
|
828
|
+
function getRouter() {
|
|
829
|
+
if (!globalRouter) throw new Error("[timber] Router not initialized. getRouter() was called before bootstrap().");
|
|
830
|
+
return globalRouter;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Get the global router instance or null if not yet initialized.
|
|
834
|
+
* Used by useRouter() methods to avoid silent failures — callers
|
|
835
|
+
* can log a meaningful warning instead of silently no-oping.
|
|
836
|
+
*/
|
|
837
|
+
function getRouterOrNull() {
|
|
838
|
+
return globalRouter;
|
|
835
839
|
}
|
|
836
840
|
//#endregion
|
|
837
841
|
//#region src/client/use-router.ts
|