@timber-js/app 0.2.0-alpha.1 → 0.2.0-alpha.3
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/browser-entry.d.ts +1 -2
- package/dist/client/browser-entry.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +133 -91
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +12 -9
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +9 -3
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/chunks.d.ts +9 -1
- package/dist/plugins/chunks.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +1 -1
- package/src/client/browser-entry.ts +6 -18
- package/src/client/index.ts +1 -1
- package/src/client/link.tsx +92 -34
- package/src/client/navigation-context.ts +54 -14
- package/src/client/top-loader.tsx +18 -15
- package/src/plugins/chunks.ts +9 -1
- package/dist/client/browser-links.d.ts +0 -32
- package/dist/client/browser-links.d.ts.map +0 -1
- package/dist/client/link-navigate-interceptor.d.ts +0 -28
- package/dist/client/link-navigate-interceptor.d.ts.map +0 -1
- package/src/client/browser-links.ts +0 -90
- package/src/client/link-navigate-interceptor.tsx +0 -62
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* 3. Setting up client-side navigation for subsequent page transitions
|
|
13
13
|
*
|
|
14
14
|
* After hydration, the browser entry:
|
|
15
|
-
* -
|
|
16
|
-
* - Listens for mouseenter on <a data-timber-prefetch> for hover prefetch
|
|
15
|
+
* - Link click handling is per-component (Link's onClick), not global delegation
|
|
17
16
|
* - Listens for popstate events for back/forward navigation
|
|
18
17
|
*
|
|
19
18
|
* Design docs: 18-build-system.md §"Entry Files", 19-client-navigation.md
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-entry.d.ts","sourceRoot":"","sources":["../../src/client/browser-entry.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"browser-entry.d.ts","sourceRoot":"","sources":["../../src/client/browser-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { JsonSerializable, RenderErrorDigest } from './types';
|
|
2
2
|
export { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';
|
|
3
3
|
export type { LinkProps, LinkPropsWithHref, LinkPropsWithParams } from './link';
|
|
4
|
-
export type { OnNavigateHandler, OnNavigateEvent } from './link
|
|
4
|
+
export type { OnNavigateHandler, OnNavigateEvent } from './link';
|
|
5
5
|
export { createRouter } from './router';
|
|
6
6
|
export type { RouterInstance, NavigationOptions, RouterDeps, RscDecoder, RootRenderer, } from './router';
|
|
7
7
|
export { useNavigationPending } from './use-navigation-pending';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAClG,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGtE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/client/index.js
CHANGED
|
@@ -3,35 +3,8 @@ import { a as _setCurrentParams, c as cachedSearchParams, i as _setCachedSearch,
|
|
|
3
3
|
import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-DAhgj8Gx.js";
|
|
4
4
|
import { t as useCookie } from "../_chunks/use-cookie-dDbpCTx-.js";
|
|
5
5
|
import { TimberErrorBoundary } from "./error-boundary.js";
|
|
6
|
-
import React, { cloneElement, createContext, createElement, isValidElement, useActionState as useActionState$1, useContext,
|
|
6
|
+
import React, { cloneElement, createContext, createElement, isValidElement, useActionState as useActionState$1, useContext, useMemo, useSyncExternalStore, useTransition } from "react";
|
|
7
7
|
import { jsx } from "react/jsx-runtime";
|
|
8
|
-
//#region src/client/link-navigate-interceptor.tsx
|
|
9
|
-
/** Symbol used to store the onNavigate callback on anchor elements. */
|
|
10
|
-
var ON_NAVIGATE_KEY = "__timberOnNavigate";
|
|
11
|
-
/**
|
|
12
|
-
* Client component rendered inside <Link> that attaches the onNavigate
|
|
13
|
-
* callback to the closest <a> ancestor via a DOM property. The callback
|
|
14
|
-
* is cleaned up on unmount.
|
|
15
|
-
*
|
|
16
|
-
* Renders no extra DOM — just a transparent wrapper.
|
|
17
|
-
*/
|
|
18
|
-
function LinkNavigateInterceptor({ onNavigate, children }) {
|
|
19
|
-
const ref = useRef(null);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const anchor = ref.current?.closest("a");
|
|
22
|
-
if (!anchor) return;
|
|
23
|
-
anchor[ON_NAVIGATE_KEY] = onNavigate;
|
|
24
|
-
return () => {
|
|
25
|
-
delete anchor[ON_NAVIGATE_KEY];
|
|
26
|
-
};
|
|
27
|
-
}, [onNavigate]);
|
|
28
|
-
return /* @__PURE__ */ jsx("span", {
|
|
29
|
-
ref,
|
|
30
|
-
style: { display: "contents" },
|
|
31
|
-
children
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
8
|
//#region src/client/use-link-status.ts
|
|
36
9
|
/**
|
|
37
10
|
* React context provided by <Link>. Holds the pending status
|
|
@@ -93,9 +66,15 @@ function useLinkStatus() {
|
|
|
93
66
|
* that depend on these APIs are safe to call from any environment —
|
|
94
67
|
* they return null or no-op when the APIs aren't available.
|
|
95
68
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
69
|
+
* SINGLETON GUARANTEE: All shared mutable state uses globalThis via
|
|
70
|
+
* Symbol.for keys. The RSC client bundler can duplicate this module
|
|
71
|
+
* across chunks (browser-entry graph + client-reference graph). With
|
|
72
|
+
* ESM output, each chunk gets its own module scope — module-level
|
|
73
|
+
* variables would create separate singleton instances per chunk.
|
|
74
|
+
* globalThis guarantees a single instance regardless of duplication.
|
|
75
|
+
*
|
|
76
|
+
* This workaround will be removed when Rolldown ships `format: 'app'`
|
|
77
|
+
* (module registry format that deduplicates like webpack/Turbopack).
|
|
99
78
|
* See design/27-chunking-strategy.md.
|
|
100
79
|
*
|
|
101
80
|
* See design/19-client-navigation.md §"NavigationContext"
|
|
@@ -104,14 +83,25 @@ function useLinkStatus() {
|
|
|
104
83
|
* The context is created lazily to avoid calling createContext at module
|
|
105
84
|
* level. In the RSC environment, React.createContext doesn't exist —
|
|
106
85
|
* calling it at import time would crash the server.
|
|
86
|
+
*
|
|
87
|
+
* Context instances are stored on globalThis (NOT in module-level
|
|
88
|
+
* variables) because the ESM bundler can duplicate this module across
|
|
89
|
+
* chunks. Module-level variables would create separate instances per
|
|
90
|
+
* chunk — the provider in TransitionRoot (index chunk) would use
|
|
91
|
+
* context A while the consumer in LinkStatusProvider (shared chunk)
|
|
92
|
+
* reads from context B. globalThis guarantees a single instance.
|
|
93
|
+
*
|
|
94
|
+
* See design/27-chunking-strategy.md §"Singleton Safety"
|
|
107
95
|
*/
|
|
108
|
-
var
|
|
109
|
-
var
|
|
96
|
+
var NAV_CTX_KEY = Symbol.for("__timber_nav_ctx");
|
|
97
|
+
var PENDING_CTX_KEY = Symbol.for("__timber_pending_nav_ctx");
|
|
110
98
|
function getOrCreateContext() {
|
|
111
|
-
|
|
99
|
+
const existing = globalThis[NAV_CTX_KEY];
|
|
100
|
+
if (existing !== void 0) return existing;
|
|
112
101
|
if (typeof React.createContext === "function") {
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
const ctx = React.createContext(null);
|
|
103
|
+
globalThis[NAV_CTX_KEY] = ctx;
|
|
104
|
+
return ctx;
|
|
115
105
|
}
|
|
116
106
|
}
|
|
117
107
|
/**
|
|
@@ -144,27 +134,42 @@ function NavigationProvider({ value, children }) {
|
|
|
144
134
|
* NavigationProvider with the correct params/pathname.
|
|
145
135
|
*
|
|
146
136
|
* This is NOT used by hooks directly — hooks read from React context.
|
|
137
|
+
*
|
|
138
|
+
* Stored on globalThis (like the context instances above) because the
|
|
139
|
+
* router lives in one chunk while renderRoot lives in another. Module-
|
|
140
|
+
* level variables would be separate per chunk.
|
|
147
141
|
*/
|
|
148
|
-
var
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
var NAV_STATE_KEY = Symbol.for("__timber_nav_state");
|
|
143
|
+
function _getNavStateStore() {
|
|
144
|
+
const g = globalThis;
|
|
145
|
+
if (!g[NAV_STATE_KEY]) g[NAV_STATE_KEY] = { current: {
|
|
146
|
+
params: {},
|
|
147
|
+
pathname: "/"
|
|
148
|
+
} };
|
|
149
|
+
return g[NAV_STATE_KEY];
|
|
150
|
+
}
|
|
152
151
|
function setNavigationState(state) {
|
|
153
|
-
|
|
152
|
+
_getNavStateStore().current = state;
|
|
154
153
|
}
|
|
155
154
|
function getNavigationState() {
|
|
156
|
-
return
|
|
155
|
+
return _getNavStateStore().current;
|
|
157
156
|
}
|
|
158
157
|
/**
|
|
159
158
|
* Separate context for the in-flight navigation URL. Provided by
|
|
160
159
|
* TransitionRoot (urgent useState), consumed by LinkStatusProvider
|
|
161
160
|
* and useNavigationPending.
|
|
161
|
+
*
|
|
162
|
+
* Uses globalThis via Symbol.for for the same reason as NavigationContext
|
|
163
|
+
* above — the bundler may duplicate this module across chunks, and module-
|
|
164
|
+
* level variables would create separate context instances.
|
|
162
165
|
*/
|
|
163
166
|
function getOrCreatePendingContext() {
|
|
164
|
-
|
|
167
|
+
const existing = globalThis[PENDING_CTX_KEY];
|
|
168
|
+
if (existing !== void 0) return existing;
|
|
165
169
|
if (typeof React.createContext === "function") {
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
const ctx = React.createContext(null);
|
|
171
|
+
globalThis[PENDING_CTX_KEY] = ctx;
|
|
172
|
+
return ctx;
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
/**
|
|
@@ -194,6 +199,30 @@ function LinkStatusProvider({ href, children }) {
|
|
|
194
199
|
});
|
|
195
200
|
}
|
|
196
201
|
//#endregion
|
|
202
|
+
//#region src/client/router-ref.ts
|
|
203
|
+
/**
|
|
204
|
+
* Set the global router instance. Called once during bootstrap.
|
|
205
|
+
*/
|
|
206
|
+
function setGlobalRouter(router) {
|
|
207
|
+
_setGlobalRouter(router);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get the global router instance. Throws if called before bootstrap.
|
|
211
|
+
* Used by client-side hooks (useNavigationPending, etc.)
|
|
212
|
+
*/
|
|
213
|
+
function getRouter() {
|
|
214
|
+
if (!globalRouter) throw new Error("[timber] Router not initialized. getRouter() was called before bootstrap().");
|
|
215
|
+
return globalRouter;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get the global router instance or null if not yet initialized.
|
|
219
|
+
* Used by useRouter() methods to avoid silent failures — callers
|
|
220
|
+
* can log a meaningful warning instead of silently no-oping.
|
|
221
|
+
*/
|
|
222
|
+
function getRouterOrNull() {
|
|
223
|
+
return globalRouter;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
197
226
|
//#region src/client/link.tsx
|
|
198
227
|
/**
|
|
199
228
|
* Reject dangerous URL schemes that could execute script.
|
|
@@ -263,44 +292,81 @@ function resolveHref(href, params, searchParams) {
|
|
|
263
292
|
function buildLinkProps(props) {
|
|
264
293
|
const resolvedHref = resolveHref(props.href, props.params, props.searchParams);
|
|
265
294
|
validateLinkHref(resolvedHref);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
295
|
+
return { href: resolvedHref };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Should this click be intercepted for SPA navigation?
|
|
299
|
+
*
|
|
300
|
+
* Returns false (pass through to browser) when:
|
|
301
|
+
* - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab
|
|
302
|
+
* - The click is not the primary button
|
|
303
|
+
* - The event was already prevented by a parent handler
|
|
304
|
+
* - The link has target="_blank" or similar
|
|
305
|
+
* - The link has a download attribute
|
|
306
|
+
* - The href is external
|
|
307
|
+
*/
|
|
308
|
+
function shouldInterceptClick(event, resolvedHref) {
|
|
309
|
+
if (event.button !== 0) return false;
|
|
310
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;
|
|
311
|
+
if (event.defaultPrevented) return false;
|
|
312
|
+
const anchor = event.currentTarget;
|
|
313
|
+
if (anchor.target && anchor.target !== "_self") return false;
|
|
314
|
+
if (anchor.hasAttribute("download")) return false;
|
|
315
|
+
if (!isInternalHref(resolvedHref)) return false;
|
|
316
|
+
return true;
|
|
273
317
|
}
|
|
274
318
|
/**
|
|
275
319
|
* Navigation link with progressive enhancement.
|
|
276
320
|
*
|
|
277
321
|
* Renders as a plain `<a>` tag — works without JavaScript. When the client
|
|
278
|
-
* runtime is active,
|
|
279
|
-
*
|
|
322
|
+
* runtime is active, the Link's onClick handler triggers RSC-based client
|
|
323
|
+
* navigation via the router. No global event delegation — each Link owns
|
|
324
|
+
* its own click handling.
|
|
280
325
|
*
|
|
281
326
|
* Supports typed routes via codegen overloads. At runtime:
|
|
282
327
|
* - `params` prop interpolates dynamic segments in the href pattern
|
|
283
328
|
* - `searchParams` prop serializes query parameters via a SearchParamsDefinition
|
|
284
329
|
*/
|
|
285
|
-
function Link({ href, prefetch, scroll, params, searchParams, onNavigate, children, ...rest }) {
|
|
286
|
-
const
|
|
330
|
+
function Link({ href, prefetch, scroll, params, searchParams, onNavigate, onClick: userOnClick, onMouseEnter: userOnMouseEnter, children, ...rest }) {
|
|
331
|
+
const { href: resolvedHref } = buildLinkProps({
|
|
287
332
|
href,
|
|
288
|
-
prefetch,
|
|
289
|
-
scroll,
|
|
290
333
|
params,
|
|
291
334
|
searchParams
|
|
292
335
|
});
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
336
|
+
const internal = isInternalHref(resolvedHref);
|
|
337
|
+
const handleClick = internal ? (event) => {
|
|
338
|
+
userOnClick?.(event);
|
|
339
|
+
if (!shouldInterceptClick(event, resolvedHref)) return;
|
|
340
|
+
if (onNavigate) {
|
|
341
|
+
let prevented = false;
|
|
342
|
+
onNavigate({ preventDefault: () => {
|
|
343
|
+
prevented = true;
|
|
344
|
+
} });
|
|
345
|
+
if (prevented) {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const router = getRouterOrNull();
|
|
351
|
+
if (!router) return;
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
const shouldScroll = scroll !== false;
|
|
354
|
+
router.navigate(resolvedHref, { scroll: shouldScroll });
|
|
355
|
+
} : userOnClick;
|
|
356
|
+
const handleMouseEnter = internal && prefetch ? (event) => {
|
|
357
|
+
userOnMouseEnter?.(event);
|
|
358
|
+
const router = getRouterOrNull();
|
|
359
|
+
if (router) router.prefetch(resolvedHref);
|
|
360
|
+
} : userOnMouseEnter;
|
|
297
361
|
return /* @__PURE__ */ jsx("a", {
|
|
298
362
|
...rest,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
363
|
+
href: resolvedHref,
|
|
364
|
+
onClick: handleClick,
|
|
365
|
+
onMouseEnter: handleMouseEnter,
|
|
366
|
+
children: /* @__PURE__ */ jsx(LinkStatusProvider, {
|
|
367
|
+
href: resolvedHref,
|
|
368
|
+
children
|
|
369
|
+
})
|
|
304
370
|
});
|
|
305
371
|
}
|
|
306
372
|
//#endregion
|
|
@@ -1105,30 +1171,6 @@ function useNavigationPending() {
|
|
|
1105
1171
|
return usePendingNavigationUrl() !== null;
|
|
1106
1172
|
}
|
|
1107
1173
|
//#endregion
|
|
1108
|
-
//#region src/client/router-ref.ts
|
|
1109
|
-
/**
|
|
1110
|
-
* Set the global router instance. Called once during bootstrap.
|
|
1111
|
-
*/
|
|
1112
|
-
function setGlobalRouter(router) {
|
|
1113
|
-
_setGlobalRouter(router);
|
|
1114
|
-
}
|
|
1115
|
-
/**
|
|
1116
|
-
* Get the global router instance. Throws if called before bootstrap.
|
|
1117
|
-
* Used by client-side hooks (useNavigationPending, etc.)
|
|
1118
|
-
*/
|
|
1119
|
-
function getRouter() {
|
|
1120
|
-
if (!globalRouter) throw new Error("[timber] Router not initialized. getRouter() was called before bootstrap().");
|
|
1121
|
-
return globalRouter;
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Get the global router instance or null if not yet initialized.
|
|
1125
|
-
* Used by useRouter() methods to avoid silent failures — callers
|
|
1126
|
-
* can log a meaningful warning instead of silently no-oping.
|
|
1127
|
-
*/
|
|
1128
|
-
function getRouterOrNull() {
|
|
1129
|
-
return globalRouter;
|
|
1130
|
-
}
|
|
1131
|
-
//#endregion
|
|
1132
1174
|
//#region src/client/use-router.ts
|
|
1133
1175
|
/**
|
|
1134
1176
|
* useRouter() — client-side hook for programmatic navigation.
|