@timber-js/app 0.1.1 → 0.1.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.
Files changed (143) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +11 -7
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugins/dev-server.d.ts.map +1 -1
  5. package/dist/plugins/entries.d.ts.map +1 -1
  6. package/package.json +5 -4
  7. package/src/adapters/cloudflare.ts +325 -0
  8. package/src/adapters/nitro.ts +366 -0
  9. package/src/adapters/types.ts +63 -0
  10. package/src/cache/index.ts +91 -0
  11. package/src/cache/redis-handler.ts +91 -0
  12. package/src/cache/register-cached-function.ts +99 -0
  13. package/src/cache/singleflight.ts +26 -0
  14. package/src/cache/stable-stringify.ts +21 -0
  15. package/src/cache/timber-cache.ts +116 -0
  16. package/src/cli.ts +201 -0
  17. package/src/client/browser-entry.ts +663 -0
  18. package/src/client/error-boundary.tsx +209 -0
  19. package/src/client/form.tsx +200 -0
  20. package/src/client/head.ts +61 -0
  21. package/src/client/history.ts +46 -0
  22. package/src/client/index.ts +60 -0
  23. package/src/client/link-navigate-interceptor.tsx +62 -0
  24. package/src/client/link-status-provider.tsx +40 -0
  25. package/src/client/link.tsx +310 -0
  26. package/src/client/nuqs-adapter.tsx +117 -0
  27. package/src/client/router-ref.ts +25 -0
  28. package/src/client/router.ts +563 -0
  29. package/src/client/segment-cache.ts +194 -0
  30. package/src/client/segment-context.ts +57 -0
  31. package/src/client/ssr-data.ts +95 -0
  32. package/src/client/types.ts +4 -0
  33. package/src/client/unload-guard.ts +34 -0
  34. package/src/client/use-cookie.ts +122 -0
  35. package/src/client/use-link-status.ts +46 -0
  36. package/src/client/use-navigation-pending.ts +47 -0
  37. package/src/client/use-params.ts +71 -0
  38. package/src/client/use-pathname.ts +43 -0
  39. package/src/client/use-query-states.ts +133 -0
  40. package/src/client/use-router.ts +77 -0
  41. package/src/client/use-search-params.ts +74 -0
  42. package/src/client/use-selected-layout-segment.ts +110 -0
  43. package/src/content/index.ts +13 -0
  44. package/src/cookies/define-cookie.ts +137 -0
  45. package/src/cookies/index.ts +9 -0
  46. package/src/fonts/ast.ts +359 -0
  47. package/src/fonts/css.ts +68 -0
  48. package/src/fonts/fallbacks.ts +248 -0
  49. package/src/fonts/google.ts +332 -0
  50. package/src/fonts/local.ts +177 -0
  51. package/src/fonts/types.ts +88 -0
  52. package/src/index.ts +420 -0
  53. package/src/plugins/adapter-build.ts +118 -0
  54. package/src/plugins/build-manifest.ts +323 -0
  55. package/src/plugins/build-report.ts +353 -0
  56. package/src/plugins/cache-transform.ts +199 -0
  57. package/src/plugins/chunks.ts +90 -0
  58. package/src/plugins/content.ts +136 -0
  59. package/src/plugins/dev-error-overlay.ts +230 -0
  60. package/src/plugins/dev-logs.ts +280 -0
  61. package/src/plugins/dev-server.ts +391 -0
  62. package/src/plugins/dynamic-transform.ts +161 -0
  63. package/src/plugins/entries.ts +214 -0
  64. package/src/plugins/fonts.ts +581 -0
  65. package/src/plugins/mdx.ts +179 -0
  66. package/src/plugins/react-prod.ts +56 -0
  67. package/src/plugins/routing.ts +419 -0
  68. package/src/plugins/server-action-exports.ts +220 -0
  69. package/src/plugins/server-bundle.ts +113 -0
  70. package/src/plugins/shims.ts +168 -0
  71. package/src/plugins/static-build.ts +207 -0
  72. package/src/routing/codegen.ts +396 -0
  73. package/src/routing/index.ts +14 -0
  74. package/src/routing/interception.ts +173 -0
  75. package/src/routing/scanner.ts +487 -0
  76. package/src/routing/status-file-lint.ts +114 -0
  77. package/src/routing/types.ts +100 -0
  78. package/src/search-params/analyze.ts +192 -0
  79. package/src/search-params/codecs.ts +153 -0
  80. package/src/search-params/create.ts +314 -0
  81. package/src/search-params/index.ts +23 -0
  82. package/src/search-params/registry.ts +31 -0
  83. package/src/server/access-gate.tsx +142 -0
  84. package/src/server/action-client.ts +473 -0
  85. package/src/server/action-handler.ts +325 -0
  86. package/src/server/actions.ts +236 -0
  87. package/src/server/asset-headers.ts +81 -0
  88. package/src/server/body-limits.ts +102 -0
  89. package/src/server/build-manifest.ts +234 -0
  90. package/src/server/canonicalize.ts +90 -0
  91. package/src/server/client-module-map.ts +58 -0
  92. package/src/server/csrf.ts +79 -0
  93. package/src/server/deny-renderer.ts +302 -0
  94. package/src/server/dev-logger.ts +419 -0
  95. package/src/server/dev-span-processor.ts +78 -0
  96. package/src/server/dev-warnings.ts +282 -0
  97. package/src/server/early-hints-sender.ts +55 -0
  98. package/src/server/early-hints.ts +142 -0
  99. package/src/server/error-boundary-wrapper.ts +69 -0
  100. package/src/server/error-formatter.ts +184 -0
  101. package/src/server/flush.ts +182 -0
  102. package/src/server/form-data.ts +176 -0
  103. package/src/server/form-flash.ts +93 -0
  104. package/src/server/html-injectors.ts +445 -0
  105. package/src/server/index.ts +222 -0
  106. package/src/server/instrumentation.ts +136 -0
  107. package/src/server/logger.ts +145 -0
  108. package/src/server/manifest-status-resolver.ts +215 -0
  109. package/src/server/metadata-render.ts +527 -0
  110. package/src/server/metadata-routes.ts +189 -0
  111. package/src/server/metadata.ts +263 -0
  112. package/src/server/middleware-runner.ts +32 -0
  113. package/src/server/nuqs-ssr-provider.tsx +63 -0
  114. package/src/server/pipeline.ts +555 -0
  115. package/src/server/prerender.ts +139 -0
  116. package/src/server/primitives.ts +264 -0
  117. package/src/server/proxy.ts +43 -0
  118. package/src/server/request-context.ts +554 -0
  119. package/src/server/route-element-builder.ts +395 -0
  120. package/src/server/route-handler.ts +153 -0
  121. package/src/server/route-matcher.ts +316 -0
  122. package/src/server/rsc-entry/api-handler.ts +112 -0
  123. package/src/server/rsc-entry/error-renderer.ts +177 -0
  124. package/src/server/rsc-entry/helpers.ts +147 -0
  125. package/src/server/rsc-entry/index.ts +688 -0
  126. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  127. package/src/server/slot-resolver.ts +359 -0
  128. package/src/server/ssr-entry.ts +161 -0
  129. package/src/server/ssr-render.ts +200 -0
  130. package/src/server/status-code-resolver.ts +282 -0
  131. package/src/server/tracing.ts +281 -0
  132. package/src/server/tree-builder.ts +354 -0
  133. package/src/server/types.ts +150 -0
  134. package/src/shims/font-google.ts +67 -0
  135. package/src/shims/headers.ts +11 -0
  136. package/src/shims/image.ts +48 -0
  137. package/src/shims/link.ts +9 -0
  138. package/src/shims/navigation-client.ts +52 -0
  139. package/src/shims/navigation.ts +31 -0
  140. package/src/shims/server-only-noop.js +5 -0
  141. package/src/utils/directive-parser.ts +529 -0
  142. package/src/utils/format.ts +10 -0
  143. package/src/utils/startup-timer.ts +102 -0
@@ -0,0 +1,310 @@
1
+ // Link component — client-side navigation with progressive enhancement
2
+ // See design/19-client-navigation.md § Progressive Enhancement
3
+ //
4
+ // Without JavaScript, <Link> renders as a plain <a> tag — standard browser
5
+ // navigation. With JavaScript, the client runtime intercepts clicks on links
6
+ // marked with data-timber-link, fetches RSC payloads, and reconciles the DOM.
7
+ //
8
+ // Typed Link: design/09-typescript.md §"Typed Link"
9
+ // - href validated against known routes (via codegen overloads, not runtime)
10
+ // - params prop typed per-route, URL interpolated at runtime
11
+ // - searchParams prop serialized via SearchParamsDefinition
12
+ // - params and fully-resolved string href are mutually exclusive
13
+ // - searchParams and inline query string are mutually exclusive
14
+
15
+ import type { AnchorHTMLAttributes, ReactNode } from 'react';
16
+ import type { SearchParamsDefinition } from '#/search-params/create.js';
17
+ import type { OnNavigateHandler } from './link-navigate-interceptor.js';
18
+ import { LinkNavigateInterceptor } from './link-navigate-interceptor.js';
19
+ import { LinkStatusProvider } from './link-status-provider.js';
20
+
21
+ // ─── Types ───────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Base props shared by all Link variants.
25
+ */
26
+ interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
27
+ /** Prefetch the RSC payload on hover */
28
+ prefetch?: boolean;
29
+ /**
30
+ * Scroll to top on navigation. Defaults to true.
31
+ * Set to false for tabbed interfaces where content changes within a fixed layout.
32
+ */
33
+ scroll?: boolean;
34
+ /**
35
+ * Called before client-side navigation commits. Call `e.preventDefault()`
36
+ * to cancel the default navigation — the caller is then responsible for
37
+ * navigating (e.g. via `router.push()`).
38
+ *
39
+ * Only fires for client-side SPA navigations, not full page loads.
40
+ * Has no effect during SSR.
41
+ */
42
+ onNavigate?: OnNavigateHandler;
43
+ children?: ReactNode;
44
+ }
45
+
46
+ /**
47
+ * Link with a fully-resolved string href.
48
+ * When using a string href with params already interpolated,
49
+ * the params prop is not available.
50
+ */
51
+ export interface LinkPropsWithHref extends LinkBaseProps {
52
+ href: string;
53
+ params?: never;
54
+ /**
55
+ * Typed search params — serialized via the route's SearchParamsDefinition.
56
+ * Mutually exclusive with an inline query string in href.
57
+ */
58
+ searchParams?: {
59
+ definition: SearchParamsDefinition<Record<string, unknown>>;
60
+ values: Record<string, unknown>;
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Link with a route pattern + params for interpolation.
66
+ * e.g. <Link href="/products/[id]" params={{ id: "123" }}>
67
+ * <Link href="/products/[id]" params={{ id: 123 }}>
68
+ */
69
+ export interface LinkPropsWithParams extends LinkBaseProps {
70
+ /** Route pattern with dynamic segments (e.g. "/products/[id]") */
71
+ href: string;
72
+ /**
73
+ * Dynamic segment values to interpolate into the href.
74
+ * Single dynamic segments accept string | number (numbers are stringified).
75
+ * Catch-all segments accept string[].
76
+ */
77
+ params: Record<string, string | number | string[]>;
78
+ /**
79
+ * Typed search params — serialized via the route's SearchParamsDefinition.
80
+ */
81
+ searchParams?: {
82
+ definition: SearchParamsDefinition<Record<string, unknown>>;
83
+ values: Record<string, unknown>;
84
+ };
85
+ }
86
+
87
+ export type LinkProps = LinkPropsWithHref | LinkPropsWithParams;
88
+
89
+ // ─── Dangerous URL Scheme Detection ──────────────────────────────
90
+
91
+ /**
92
+ * Reject dangerous URL schemes that could execute script.
93
+ * Security: design/13-security.md § Link scheme injection (test #9)
94
+ */
95
+ const DANGEROUS_SCHEMES = /^\s*(javascript|data|vbscript):/i;
96
+
97
+ export function validateLinkHref(href: string): void {
98
+ if (DANGEROUS_SCHEMES.test(href)) {
99
+ throw new Error(
100
+ `<Link> received a dangerous href: "${href}". ` +
101
+ 'javascript:, data:, and vbscript: URLs are not allowed.'
102
+ );
103
+ }
104
+ }
105
+
106
+ // ─── Internal Link Detection ─────────────────────────────────────
107
+
108
+ /** Returns true if the href is an internal path (not an external URL) */
109
+ function isInternalHref(href: string): boolean {
110
+ // Relative paths, root-relative paths, and hash links are internal
111
+ if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {
112
+ return true;
113
+ }
114
+ // Anything with a protocol scheme is external
115
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {
116
+ return false;
117
+ }
118
+ // Bare relative paths (e.g., "dashboard") are internal
119
+ return true;
120
+ }
121
+
122
+ // ─── URL Interpolation ──────────────────────────────────────────
123
+
124
+ /**
125
+ * Interpolate dynamic segments in a route pattern with actual values.
126
+ * e.g. interpolateParams("/products/[id]", { id: "123" }) → "/products/123"
127
+ *
128
+ * Supports:
129
+ * - [param] → single segment
130
+ * - [...param] → catch-all (joined with /)
131
+ * - [[...param]] → optional catch-all (omitted if undefined/empty)
132
+ */
133
+ export function interpolateParams(
134
+ pattern: string,
135
+ params: Record<string, string | number | string[]>
136
+ ): string {
137
+ return (
138
+ pattern
139
+ .replace(
140
+ /\[\[\.\.\.(\w+)\]\]|\[\.\.\.(\w+)\]|\[(\w+)\]/g,
141
+ (_match, optionalCatchAll, catchAll, single) => {
142
+ if (optionalCatchAll) {
143
+ const value = params[optionalCatchAll];
144
+ if (value === undefined || (Array.isArray(value) && value.length === 0)) {
145
+ return '';
146
+ }
147
+ const segments = Array.isArray(value) ? value : [value];
148
+ return segments.map(encodeURIComponent).join('/');
149
+ }
150
+
151
+ if (catchAll) {
152
+ const value = params[catchAll];
153
+ if (value === undefined) {
154
+ throw new Error(
155
+ `<Link> missing required catch-all param "${catchAll}" for pattern "${pattern}".`
156
+ );
157
+ }
158
+ const segments = Array.isArray(value) ? value : [value];
159
+ if (segments.length === 0) {
160
+ throw new Error(
161
+ `<Link> catch-all param "${catchAll}" must have at least one segment for pattern "${pattern}".`
162
+ );
163
+ }
164
+ return segments.map(encodeURIComponent).join('/');
165
+ }
166
+
167
+ // single dynamic segment
168
+ const value = params[single];
169
+ if (value === undefined) {
170
+ throw new Error(`<Link> missing required param "${single}" for pattern "${pattern}".`);
171
+ }
172
+ if (Array.isArray(value)) {
173
+ throw new Error(
174
+ `<Link> param "${single}" expected a string but received an array for pattern "${pattern}".`
175
+ );
176
+ }
177
+ // Accept numbers — coerce to string for URL interpolation
178
+ return encodeURIComponent(String(value));
179
+ }
180
+ )
181
+ // Clean up trailing slash from empty optional catch-all
182
+ .replace(/\/+$/, '') || '/'
183
+ );
184
+ }
185
+
186
+ // ─── Resolve Href ───────────────────────────────────────────────
187
+
188
+ /**
189
+ * Resolve the final href string from Link props.
190
+ *
191
+ * Handles:
192
+ * - params interpolation into route patterns
193
+ * - searchParams serialization via SearchParamsDefinition
194
+ * - Validation that searchParams and inline query strings are exclusive
195
+ */
196
+ export function resolveHref(
197
+ href: string,
198
+ params?: Record<string, string | number | string[]>,
199
+ searchParams?: {
200
+ definition: SearchParamsDefinition<Record<string, unknown>>;
201
+ values: Record<string, unknown>;
202
+ }
203
+ ): string {
204
+ let resolvedPath = href;
205
+
206
+ // Interpolate params if provided
207
+ if (params) {
208
+ resolvedPath = interpolateParams(href, params);
209
+ }
210
+
211
+ // Serialize searchParams if provided
212
+ if (searchParams) {
213
+ // Validate: searchParams prop and inline query string are mutually exclusive
214
+ if (resolvedPath.includes('?')) {
215
+ throw new Error(
216
+ '<Link> received both a searchParams prop and a query string in href. ' +
217
+ 'These are mutually exclusive — use one or the other.'
218
+ );
219
+ }
220
+
221
+ const qs = searchParams.definition.serialize(searchParams.values);
222
+ if (qs) {
223
+ resolvedPath = `${resolvedPath}?${qs}`;
224
+ }
225
+ }
226
+
227
+ return resolvedPath;
228
+ }
229
+
230
+ // ─── Build Props ─────────────────────────────────────────────────
231
+
232
+ interface LinkOutputProps {
233
+ 'href': string;
234
+ 'data-timber-link'?: boolean;
235
+ 'data-timber-prefetch'?: boolean;
236
+ 'data-timber-scroll'?: string;
237
+ }
238
+
239
+ /**
240
+ * Build the HTML attributes for a Link. Separated from the component
241
+ * for testability — the component just spreads these onto an <a>.
242
+ */
243
+ export function buildLinkProps(
244
+ props: Pick<LinkPropsWithHref, 'href' | 'prefetch' | 'scroll'> & {
245
+ params?: Record<string, string | number | string[]>;
246
+ searchParams?: {
247
+ definition: SearchParamsDefinition<Record<string, unknown>>;
248
+ values: Record<string, unknown>;
249
+ };
250
+ }
251
+ ): LinkOutputProps {
252
+ const resolvedHref = resolveHref(props.href, props.params, props.searchParams);
253
+
254
+ validateLinkHref(resolvedHref);
255
+
256
+ const output: LinkOutputProps = { href: resolvedHref };
257
+ const internal = isInternalHref(resolvedHref);
258
+
259
+ if (internal) {
260
+ output['data-timber-link'] = true;
261
+
262
+ if (props.prefetch) {
263
+ output['data-timber-prefetch'] = true;
264
+ }
265
+
266
+ if (props.scroll === false) {
267
+ output['data-timber-scroll'] = 'false';
268
+ }
269
+ }
270
+
271
+ return output;
272
+ }
273
+
274
+ // ─── Link Component ──────────────────────────────────────────────
275
+
276
+ /**
277
+ * Navigation link with progressive enhancement.
278
+ *
279
+ * Renders as a plain `<a>` tag — works without JavaScript. When the client
280
+ * runtime is active, it intercepts clicks on links marked with
281
+ * `data-timber-link` to perform RSC-based client navigation.
282
+ *
283
+ * Supports typed routes via codegen overloads. At runtime:
284
+ * - `params` prop interpolates dynamic segments in the href pattern
285
+ * - `searchParams` prop serializes query parameters via a SearchParamsDefinition
286
+ */
287
+ export function Link({
288
+ href,
289
+ prefetch,
290
+ scroll,
291
+ params,
292
+ searchParams,
293
+ onNavigate,
294
+ children,
295
+ ...rest
296
+ }: LinkProps) {
297
+ const linkProps = buildLinkProps({ href, prefetch, scroll, params, searchParams });
298
+
299
+ const inner = <LinkStatusProvider href={linkProps.href}>{children}</LinkStatusProvider>;
300
+
301
+ return (
302
+ <a {...rest} {...linkProps}>
303
+ {onNavigate ? (
304
+ <LinkNavigateInterceptor onNavigate={onNavigate}>{inner}</LinkNavigateInterceptor>
305
+ ) : (
306
+ inner
307
+ )}
308
+ </a>
309
+ );
310
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Timber nuqs adapter — connects nuqs URL state management to timber's
3
+ * RSC-aware client navigation.
4
+ *
5
+ * nuqs uses framework adapters to control how URL updates are applied.
6
+ * This adapter implements nuqs's UseAdapterHook interface and:
7
+ * - Calls router.navigate() for non-shallow updates (the default)
8
+ * - Uses pushState/replaceState directly for shallow updates
9
+ * - Sets timber defaults: shallow: false, scroll: true, history: 'push'
10
+ *
11
+ * Design doc: design/23-search-params.md §"Custom Adapter"
12
+ */
13
+ 'use client';
14
+
15
+ import { useState, useEffect, useMemo, type ReactNode } from 'react';
16
+ import {
17
+ unstable_createAdapterProvider as createAdapterProvider,
18
+ renderQueryString,
19
+ type unstable_AdapterInterface as AdapterInterface,
20
+ type unstable_AdapterOptions as AdapterOptions,
21
+ } from 'nuqs/adapters/custom';
22
+ import { getRouter } from './router-ref.js';
23
+
24
+ // ─── Adapter Hook ─────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Custom adapter hook for nuqs. Returns the current search params
28
+ * and an updateUrl function that integrates with timber's router.
29
+ *
30
+ * @param _watchKeys - param keys this hook instance cares about
31
+ * (used by nuqs for selective re-rendering)
32
+ */
33
+ function useTimberAdapter(_watchKeys: string[]): AdapterInterface {
34
+ const [searchParams, setSearchParams] = useState(
35
+ () => new URLSearchParams(window.location.search)
36
+ );
37
+
38
+ // Sync search params on popstate (back/forward) and after
39
+ // timber navigations that change the URL.
40
+ useEffect(() => {
41
+ function sync() {
42
+ setSearchParams(new URLSearchParams(window.location.search));
43
+ }
44
+
45
+ window.addEventListener('popstate', sync);
46
+ // timber dispatches a custom event after client navigations complete
47
+ window.addEventListener('timber:navigation-end', sync);
48
+
49
+ return () => {
50
+ window.removeEventListener('popstate', sync);
51
+ window.removeEventListener('timber:navigation-end', sync);
52
+ };
53
+ }, []);
54
+
55
+ const updateUrl = useMemo(() => {
56
+ return (search: URLSearchParams, options: Required<AdapterOptions>) => {
57
+ const url = new URL(window.location.href);
58
+ url.search = renderQueryString(search);
59
+
60
+ if (options.shallow) {
61
+ // Shallow: update URL only, no server roundtrip.
62
+ const method =
63
+ options.history === 'push' ? window.history.pushState : window.history.replaceState;
64
+ method.call(window.history, window.history.state, '', url.toString());
65
+
66
+ // Update local state to reflect the new URL
67
+ setSearchParams(new URLSearchParams(url.search));
68
+ } else {
69
+ // Non-shallow (timber default): trigger RSC navigation to fetch
70
+ // fresh server data for the new search params.
71
+ const router = getRouter();
72
+ void router.navigate(url.pathname + url.search + url.hash, {
73
+ scroll: options.scroll,
74
+ replace: options.history === 'replace',
75
+ });
76
+ }
77
+
78
+ if (options.scroll) {
79
+ window.scrollTo({ top: 0 });
80
+ }
81
+ };
82
+ }, []);
83
+
84
+ return {
85
+ searchParams,
86
+ updateUrl,
87
+ getSearchParamsSnapshot: () => new URLSearchParams(window.location.search),
88
+ };
89
+ }
90
+
91
+ // ─── Provider Component ───────────────────────────────────────────
92
+
93
+ // Lazily created — createAdapterProvider calls React.createElement internally,
94
+ // so it must NOT run at module scope. In Rolldown SSR bundles, React's
95
+ // __esmMin lazy initializer may not have run yet at module-init time.
96
+ let _TimberNuqsProvider: ReturnType<typeof createAdapterProvider> | undefined;
97
+
98
+ /**
99
+ * Wraps the React tree with nuqs's adapter context, configured with
100
+ * timber's default options.
101
+ *
102
+ * Auto-injected in browser-entry.ts — no user setup required.
103
+ */
104
+ export function TimberNuqsAdapter({ children }: { children: ReactNode }) {
105
+ const Provider = (_TimberNuqsProvider ??= createAdapterProvider(useTimberAdapter));
106
+ return (
107
+ <Provider
108
+ defaultOptions={{
109
+ shallow: false,
110
+ scroll: true,
111
+ clearOnDefault: true,
112
+ }}
113
+ >
114
+ {children}
115
+ </Provider>
116
+ );
117
+ }
@@ -0,0 +1,25 @@
1
+ // Global router reference — shared between browser-entry and client hooks.
2
+ // This module has no dependencies on virtual modules, so it can be safely
3
+ // imported by client hooks without pulling in browser-entry's virtual imports.
4
+
5
+ import type { RouterInstance } from './router.js';
6
+
7
+ let globalRouter: RouterInstance | null = null;
8
+
9
+ /**
10
+ * Set the global router instance. Called once during bootstrap.
11
+ */
12
+ export function setGlobalRouter(router: RouterInstance): void {
13
+ globalRouter = router;
14
+ }
15
+
16
+ /**
17
+ * Get the global router instance. Throws if called before bootstrap.
18
+ * Used by client-side hooks (useNavigationPending, etc.)
19
+ */
20
+ export function getRouter(): RouterInstance {
21
+ if (!globalRouter) {
22
+ throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');
23
+ }
24
+ return globalRouter;
25
+ }