@timber-js/app 0.1.29 → 0.1.31
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 +138 -86
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-status-provider.d.ts +4 -4
- package/dist/client/link-status-provider.d.ts.map +1 -1
- package/dist/client/pending-navigation-context.d.ts +32 -0
- package/dist/client/pending-navigation-context.d.ts.map +1 -0
- package/dist/client/router.d.ts +12 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +33 -13
- package/dist/client/transition-root.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 +26 -16
- package/src/client/link-status-provider.tsx +14 -24
- package/src/client/pending-navigation-context.ts +66 -0
- package/src/client/router.ts +127 -75
- package/src/client/transition-root.tsx +84 -20
- package/src/client/use-navigation-pending.ts +8 -17
- package/src/plugins/chunks.ts +145 -17
package/dist/client/index.js
CHANGED
|
@@ -67,53 +67,55 @@ function useLinkStatus() {
|
|
|
67
67
|
return useContext(LinkStatusContext);
|
|
68
68
|
}
|
|
69
69
|
//#endregion
|
|
70
|
-
//#region src/client/
|
|
70
|
+
//#region src/client/pending-navigation-context.ts
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
72
|
+
* PendingNavigationContext — React context for the in-flight navigation URL.
|
|
73
|
+
*
|
|
74
|
+
* Provided by TransitionRoot. The value is the URL being navigated to,
|
|
75
|
+
* or null when idle. Used by:
|
|
76
|
+
* - LinkStatusProvider to show per-link pending spinners
|
|
77
|
+
* - useNavigationPending to return a global pending boolean
|
|
78
|
+
*
|
|
79
|
+
* The pending URL is set as an URGENT update (shows immediately) and
|
|
80
|
+
* cleared inside startTransition (commits atomically with the new tree).
|
|
81
|
+
* This ensures pending state appears instantly on navigation start and
|
|
82
|
+
* disappears in the same React commit as the new params/tree.
|
|
83
|
+
*
|
|
84
|
+
* Separate from NavigationContext (which holds params + pathname) because
|
|
85
|
+
* the pending URL is managed as React state in TransitionRoot, while
|
|
86
|
+
* params/pathname are set via module-level state read by renderRoot.
|
|
87
|
+
* Both contexts commit together in the same transition.
|
|
88
|
+
*
|
|
89
|
+
* See design/19-client-navigation.md §"NavigationContext"
|
|
80
90
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
91
|
+
var _context$1;
|
|
92
|
+
function getOrCreateContext$1() {
|
|
93
|
+
if (_context$1 !== void 0) return _context$1;
|
|
94
|
+
if (typeof React.createContext === "function") _context$1 = React.createContext(null);
|
|
95
|
+
return _context$1;
|
|
84
96
|
}
|
|
85
97
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
98
|
+
* Read the pending navigation URL from context.
|
|
99
|
+
* Returns null during SSR (no provider) or in the RSC environment.
|
|
100
|
+
* Internal — used by LinkStatusProvider and useNavigationPending.
|
|
89
101
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
102
|
+
function usePendingNavigationUrl() {
|
|
103
|
+
const ctx = getOrCreateContext$1();
|
|
104
|
+
if (!ctx) return null;
|
|
105
|
+
if (typeof React.useContext !== "function") return null;
|
|
106
|
+
return React.useContext(ctx);
|
|
92
107
|
}
|
|
93
108
|
//#endregion
|
|
94
109
|
//#region src/client/link-status-provider.tsx
|
|
95
110
|
var NOT_PENDING = { pending: false };
|
|
96
111
|
var IS_PENDING = { pending: true };
|
|
97
112
|
/**
|
|
98
|
-
* Client component that
|
|
99
|
-
* a scoped LinkStatusContext to children. Renders no extra DOM —
|
|
100
|
-
* context provider around children.
|
|
113
|
+
* Client component that reads the pending URL from PendingNavigationContext
|
|
114
|
+
* and provides a scoped LinkStatusContext to children. Renders no extra DOM —
|
|
115
|
+
* just a context provider around children.
|
|
101
116
|
*/
|
|
102
117
|
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);
|
|
118
|
+
const status = usePendingNavigationUrl() === href ? IS_PENDING : NOT_PENDING;
|
|
117
119
|
return /* @__PURE__ */ jsx(LinkStatusContext.Provider, {
|
|
118
120
|
value: status,
|
|
119
121
|
children
|
|
@@ -649,6 +651,26 @@ function createRouter(deps) {
|
|
|
649
651
|
pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/"
|
|
650
652
|
});
|
|
651
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Render a payload via navigateTransition (production) or renderRoot (tests).
|
|
656
|
+
* The perform callback should fetch data, update state, and return the payload.
|
|
657
|
+
* In production, the entire callback runs inside a React transition with
|
|
658
|
+
* useOptimistic for the pending URL. In tests, the payload is rendered directly.
|
|
659
|
+
*/
|
|
660
|
+
async function renderViaTransition(pendingUrl, perform) {
|
|
661
|
+
if (deps.navigateTransition) {
|
|
662
|
+
let headElements = null;
|
|
663
|
+
await deps.navigateTransition(pendingUrl, async (wrapPayload) => {
|
|
664
|
+
const result = await perform();
|
|
665
|
+
headElements = result.headElements;
|
|
666
|
+
return wrapPayload(result.payload);
|
|
667
|
+
});
|
|
668
|
+
return headElements;
|
|
669
|
+
}
|
|
670
|
+
const result = await perform();
|
|
671
|
+
renderPayload(result.payload);
|
|
672
|
+
return result.headElements;
|
|
673
|
+
}
|
|
652
674
|
/** Apply head elements (title, meta tags) to the DOM if available. */
|
|
653
675
|
function applyHead(elements) {
|
|
654
676
|
if (elements && deps.applyHead) deps.applyHead(elements);
|
|
@@ -658,6 +680,40 @@ function createRouter(deps) {
|
|
|
658
680
|
if (deps.afterPaint) deps.afterPaint(callback);
|
|
659
681
|
else callback();
|
|
660
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Core navigation logic shared between the transition and fallback paths.
|
|
685
|
+
* Fetches the RSC payload, updates all state, and returns the result.
|
|
686
|
+
*/
|
|
687
|
+
async function performNavigationFetch(url, options) {
|
|
688
|
+
const prefetched = prefetchCache.consume(url);
|
|
689
|
+
let result = prefetched ? {
|
|
690
|
+
payload: prefetched.payload,
|
|
691
|
+
headElements: prefetched.headElements,
|
|
692
|
+
segmentInfo: prefetched.segmentInfo ?? null,
|
|
693
|
+
params: prefetched.params ?? null
|
|
694
|
+
} : void 0;
|
|
695
|
+
if (result === void 0) {
|
|
696
|
+
const stateTree = segmentCache.serializeStateTree();
|
|
697
|
+
const rawCurrentUrl = deps.getCurrentUrl();
|
|
698
|
+
result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname);
|
|
699
|
+
}
|
|
700
|
+
if (options.replace) deps.replaceState({
|
|
701
|
+
timber: true,
|
|
702
|
+
scrollY: 0
|
|
703
|
+
}, "", url);
|
|
704
|
+
else deps.pushState({
|
|
705
|
+
timber: true,
|
|
706
|
+
scrollY: 0
|
|
707
|
+
}, "", url);
|
|
708
|
+
historyStack.push(url, {
|
|
709
|
+
payload: result.payload,
|
|
710
|
+
headElements: result.headElements,
|
|
711
|
+
params: result.params
|
|
712
|
+
});
|
|
713
|
+
updateSegmentCache(result.segmentInfo);
|
|
714
|
+
updateNavigationState(result.params, url);
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
661
717
|
async function navigate(url, options = {}) {
|
|
662
718
|
const scroll = options.scroll !== false;
|
|
663
719
|
const replace = options.replace === true;
|
|
@@ -668,29 +724,7 @@ function createRouter(deps) {
|
|
|
668
724
|
}, "", deps.getCurrentUrl());
|
|
669
725
|
setPending(true, url);
|
|
670
726
|
try {
|
|
671
|
-
|
|
672
|
-
if (result === void 0) {
|
|
673
|
-
const stateTree = segmentCache.serializeStateTree();
|
|
674
|
-
const rawCurrentUrl = deps.getCurrentUrl();
|
|
675
|
-
result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname);
|
|
676
|
-
}
|
|
677
|
-
if (replace) deps.replaceState({
|
|
678
|
-
timber: true,
|
|
679
|
-
scrollY: 0
|
|
680
|
-
}, "", url);
|
|
681
|
-
else deps.pushState({
|
|
682
|
-
timber: true,
|
|
683
|
-
scrollY: 0
|
|
684
|
-
}, "", url);
|
|
685
|
-
historyStack.push(url, {
|
|
686
|
-
payload: result.payload,
|
|
687
|
-
headElements: result.headElements,
|
|
688
|
-
params: result.params
|
|
689
|
-
});
|
|
690
|
-
updateSegmentCache(result.segmentInfo);
|
|
691
|
-
updateNavigationState(result.params, url);
|
|
692
|
-
renderPayload(result.payload);
|
|
693
|
-
applyHead(result.headElements);
|
|
727
|
+
applyHead(await renderViaTransition(url, () => performNavigationFetch(url, { replace })));
|
|
694
728
|
window.dispatchEvent(new Event("timber:navigation-end"));
|
|
695
729
|
afterPaint(() => {
|
|
696
730
|
if (scroll) deps.scrollTo(0, 0);
|
|
@@ -713,16 +747,17 @@ function createRouter(deps) {
|
|
|
713
747
|
const currentUrl = deps.getCurrentUrl();
|
|
714
748
|
setPending(true, currentUrl);
|
|
715
749
|
try {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
750
|
+
applyHead(await renderViaTransition(currentUrl, async () => {
|
|
751
|
+
const result = await fetchRscPayload(currentUrl, deps);
|
|
752
|
+
historyStack.push(currentUrl, {
|
|
753
|
+
payload: result.payload,
|
|
754
|
+
headElements: result.headElements,
|
|
755
|
+
params: result.params
|
|
756
|
+
});
|
|
757
|
+
updateSegmentCache(result.segmentInfo);
|
|
758
|
+
updateNavigationState(result.params, currentUrl);
|
|
759
|
+
return result;
|
|
760
|
+
}));
|
|
726
761
|
} finally {
|
|
727
762
|
setPending(false);
|
|
728
763
|
}
|
|
@@ -740,16 +775,17 @@ function createRouter(deps) {
|
|
|
740
775
|
} else {
|
|
741
776
|
setPending(true, url);
|
|
742
777
|
try {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
778
|
+
applyHead(await renderViaTransition(url, async () => {
|
|
779
|
+
const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree());
|
|
780
|
+
updateSegmentCache(result.segmentInfo);
|
|
781
|
+
updateNavigationState(result.params, url);
|
|
782
|
+
historyStack.push(url, {
|
|
783
|
+
payload: result.payload,
|
|
784
|
+
headElements: result.headElements,
|
|
785
|
+
params: result.params
|
|
786
|
+
});
|
|
787
|
+
return result;
|
|
788
|
+
}));
|
|
753
789
|
afterPaint(() => {
|
|
754
790
|
deps.scrollTo(0, scrollY);
|
|
755
791
|
window.dispatchEvent(new Event("timber:scroll-restored"));
|
|
@@ -823,15 +859,31 @@ function createRouter(deps) {
|
|
|
823
859
|
* ```
|
|
824
860
|
*/
|
|
825
861
|
function useNavigationPending() {
|
|
826
|
-
return
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
862
|
+
return usePendingNavigationUrl() !== null;
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/client/router-ref.ts
|
|
866
|
+
/**
|
|
867
|
+
* Set the global router instance. Called once during bootstrap.
|
|
868
|
+
*/
|
|
869
|
+
function setGlobalRouter(router) {
|
|
870
|
+
_setGlobalRouter(router);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Get the global router instance. Throws if called before bootstrap.
|
|
874
|
+
* Used by client-side hooks (useNavigationPending, etc.)
|
|
875
|
+
*/
|
|
876
|
+
function getRouter() {
|
|
877
|
+
if (!globalRouter) throw new Error("[timber] Router not initialized. getRouter() was called before bootstrap().");
|
|
878
|
+
return globalRouter;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Get the global router instance or null if not yet initialized.
|
|
882
|
+
* Used by useRouter() methods to avoid silent failures — callers
|
|
883
|
+
* can log a meaningful warning instead of silently no-oping.
|
|
884
|
+
*/
|
|
885
|
+
function getRouterOrNull() {
|
|
886
|
+
return globalRouter;
|
|
835
887
|
}
|
|
836
888
|
//#endregion
|
|
837
889
|
//#region src/client/use-router.ts
|