@timber-js/app 0.1.23 → 0.1.25

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 (61) hide show
  1. package/dist/_chunks/{ssr-data-B2yikEEB.js → ssr-data-DLnbYpj1.js} +2 -4
  2. package/dist/_chunks/{ssr-data-B2yikEEB.js.map → ssr-data-DLnbYpj1.js.map} +1 -1
  3. package/dist/_chunks/{use-cookie-D5aS4slY.js → use-cookie-dDbpCTx-.js} +2 -2
  4. package/dist/_chunks/{use-cookie-D5aS4slY.js.map → use-cookie-dDbpCTx-.js.map} +1 -1
  5. package/dist/adapters/nitro.d.ts.map +1 -1
  6. package/dist/adapters/nitro.js +4 -3
  7. package/dist/adapters/nitro.js.map +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client/browser-dev.d.ts +29 -0
  11. package/dist/client/browser-dev.d.ts.map +1 -0
  12. package/dist/client/browser-links.d.ts +32 -0
  13. package/dist/client/browser-links.d.ts.map +1 -0
  14. package/dist/client/error-boundary.js +1 -1
  15. package/dist/client/index.d.ts +2 -0
  16. package/dist/client/index.d.ts.map +1 -1
  17. package/dist/client/index.js +150 -122
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/navigation-context.d.ts +52 -0
  20. package/dist/client/navigation-context.d.ts.map +1 -0
  21. package/dist/client/router.d.ts.map +1 -1
  22. package/dist/client/transition-root.d.ts +54 -0
  23. package/dist/client/transition-root.d.ts.map +1 -0
  24. package/dist/client/use-params.d.ts +35 -25
  25. package/dist/client/use-params.d.ts.map +1 -1
  26. package/dist/client/use-pathname.d.ts +11 -4
  27. package/dist/client/use-pathname.d.ts.map +1 -1
  28. package/dist/client/use-router.d.ts +14 -0
  29. package/dist/client/use-router.d.ts.map +1 -1
  30. package/dist/cookies/index.js +2 -2
  31. package/dist/server/index.js +264 -218
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/server/metadata-platform.d.ts +34 -0
  34. package/dist/server/metadata-platform.d.ts.map +1 -0
  35. package/dist/server/metadata-render.d.ts.map +1 -1
  36. package/dist/server/metadata-social.d.ts +24 -0
  37. package/dist/server/metadata-social.d.ts.map +1 -0
  38. package/dist/server/pipeline-interception.d.ts +32 -0
  39. package/dist/server/pipeline-interception.d.ts.map +1 -0
  40. package/dist/server/pipeline-metadata.d.ts +31 -0
  41. package/dist/server/pipeline-metadata.d.ts.map +1 -0
  42. package/dist/server/pipeline.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/adapters/nitro.ts +9 -7
  45. package/src/cli.ts +9 -2
  46. package/src/client/browser-dev.ts +142 -0
  47. package/src/client/browser-entry.ts +73 -223
  48. package/src/client/browser-links.ts +90 -0
  49. package/src/client/index.ts +4 -0
  50. package/src/client/navigation-context.ts +118 -0
  51. package/src/client/router.ts +37 -33
  52. package/src/client/transition-root.tsx +86 -0
  53. package/src/client/use-params.ts +50 -54
  54. package/src/client/use-pathname.ts +31 -24
  55. package/src/client/use-router.ts +17 -15
  56. package/src/server/metadata-platform.ts +229 -0
  57. package/src/server/metadata-render.ts +9 -363
  58. package/src/server/metadata-social.ts +184 -0
  59. package/src/server/pipeline-interception.ts +76 -0
  60. package/src/server/pipeline-metadata.ts +90 -0
  61. package/src/server/pipeline.ts +2 -148
@@ -1,11 +1,10 @@
1
1
  "use client";
2
- import { a as _setCurrentParams, c as cachedSearchParams, d as paramsListeners, i as _setCachedSearch, l as currentParams, n as getSsrData, o as _setGlobalRouter, r as setSsrData, s as cachedSearch, t as clearSsrData, u as globalRouter } from "../_chunks/ssr-data-B2yikEEB.js";
2
+ import { a as _setCurrentParams, c as cachedSearchParams, i as _setCachedSearch, l as currentParams, n as getSsrData, o as _setGlobalRouter, r as setSsrData, s as cachedSearch, t as clearSsrData, u as globalRouter } from "../_chunks/ssr-data-DLnbYpj1.js";
3
3
  import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-DAhgj8Gx.js";
4
- import { t as useCookie } from "../_chunks/use-cookie-D5aS4slY.js";
4
+ import { t as useCookie } from "../_chunks/use-cookie-dDbpCTx-.js";
5
5
  import { TimberErrorBoundary } from "./error-boundary.js";
6
- import { createContext, createElement, startTransition, useActionState as useActionState$1, useContext, useEffect, useMemo, useRef, useSyncExternalStore, useTransition } from "react";
6
+ import React, { createContext, createElement, useActionState as useActionState$1, useContext, useEffect, useMemo, useRef, useSyncExternalStore, useTransition } from "react";
7
7
  import { jsxDEV } from "react/jsx-dev-runtime";
8
- import { flushSync } from "react-dom";
9
8
  //#region src/client/link-navigate-interceptor.tsx
10
9
  var _jsxFileName$2 = "/Users/dsaewitz/y/timber-js-fresh/packages/timber-app/src/client/link-navigate-interceptor.tsx";
11
10
  /** Symbol used to store the onNavigate callback on anchor elements. */
@@ -380,94 +379,112 @@ var HistoryStack = class {
380
379
  }
381
380
  };
382
381
  //#endregion
383
- //#region src/client/use-params.ts
382
+ //#region src/client/navigation-context.ts
384
383
  /**
385
- * useParams()client-side hook for accessing route params.
386
- *
387
- * Returns the dynamic route parameters for the current URL.
388
- * When called with a route pattern argument, TypeScript narrows
389
- * the return type to the exact params shape for that route.
390
- *
391
- * Two layers of type narrowing work together:
392
- * 1. The generic overload here uses the Routes interface directly —
393
- * `useParams<R>()` returns `Routes[R]['params']`.
394
- * 2. Build-time codegen generates per-route string-literal overloads
395
- * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).
396
- *
397
- * When the Routes interface is empty (no codegen yet), the generic
398
- * overload has `keyof Routes = never`, so only the fallback matches.
399
- *
400
- * During SSR, params are read from the ALS-backed SSR data context
401
- * (populated by ssr-entry.ts) to ensure correct per-request isolation
402
- * across concurrent requests with streaming Suspense.
403
- *
404
- * Reactivity: useParams() uses useSyncExternalStore so that components
405
- * in unchanged layouts (e.g., sidebar items) re-render atomically when
406
- * params change during client-side navigation. This matches the pattern
407
- * used by usePathname() and useSearchParams().
408
- *
409
- * All mutable state is delegated to client/state.ts for singleton guarantees.
410
- * See design/18-build-system.md §"Singleton State Registry"
411
- *
412
- * Design doc: design/09-typescript.md §"Typed Routes"
384
+ * NavigationContextReact context for navigation state.
385
+ *
386
+ * Holds the current route params and pathname, updated atomically
387
+ * with the RSC tree on each navigation. This replaces the previous
388
+ * useSyncExternalStore approach for useParams() and usePathname(),
389
+ * which suffered from a timing gap: the new tree could commit before
390
+ * the external store re-renders fired, causing a frame where both
391
+ * old and new active states were visible simultaneously.
392
+ *
393
+ * By wrapping the RSC payload element in NavigationProvider inside
394
+ * renderRoot(), the context value and the element tree are passed to
395
+ * reactRoot.render() in the same call — atomic by construction.
396
+ * All consumers (useParams, usePathname) see the new values in the
397
+ * same render pass as the new tree.
398
+ *
399
+ * During SSR, no NavigationProvider is mounted. Hooks fall back to
400
+ * the ALS-backed getSsrData() for per-request isolation.
401
+ *
402
+ * IMPORTANT: createContext and useContext are NOT available in the RSC
403
+ * environment (React Server Components use a stripped-down React).
404
+ * The context is lazily initialized on first access, and all functions
405
+ * that depend on these APIs are safe to call from any environment —
406
+ * they return null or no-op when the APIs aren't available.
407
+ *
408
+ * See design/19-client-navigation.md §"NavigationContext"
413
409
  */
414
410
  /**
415
- * Subscribe to params changes. Called by useSyncExternalStore.
416
- * Exported for testing not intended for direct use by app code.
411
+ * The context is created lazily to avoid calling createContext at module
412
+ * level. In the RSC environment, React.createContext doesn't exist
413
+ * calling it at import time would crash the server.
417
414
  */
418
- function subscribe$2(callback) {
419
- paramsListeners.add(callback);
420
- return () => paramsListeners.delete(callback);
415
+ var _context;
416
+ function getOrCreateContext() {
417
+ if (_context !== void 0) return _context;
418
+ if (typeof React.createContext === "function") _context = React.createContext(null);
419
+ return _context;
421
420
  }
422
421
  /**
423
- * Get the current params snapshot (client).
424
- * Exported for testing not intended for direct use by app code.
422
+ * Read the navigation context. Returns null during SSR (no provider)
423
+ * or in the RSC environment (no context available).
424
+ * Internal — used by useParams() and usePathname().
425
425
  */
426
- function getSnapshot() {
427
- return currentParams;
426
+ function useNavigationContext() {
427
+ const ctx = getOrCreateContext();
428
+ if (!ctx) return null;
429
+ if (typeof React.useContext !== "function") return null;
430
+ return React.useContext(ctx);
428
431
  }
429
432
  /**
430
- * Get the server-side params snapshot (SSR).
431
- * Falls back to the module-level currentParams if no SSR context
432
- * is available (shouldn't happen, but defensive).
433
+ * Wraps children with NavigationContext.Provider.
434
+ *
435
+ * Used in browser-entry.ts renderRoot to wrap the RSC payload element
436
+ * so that navigation state updates atomically with the tree render.
433
437
  */
434
- function getServerSnapshot() {
435
- return getSsrData()?.params ?? currentParams;
438
+ function NavigationProvider({ value, children }) {
439
+ const ctx = getOrCreateContext();
440
+ if (!ctx) return children;
441
+ return createElement(ctx.Provider, { value }, children);
442
+ }
443
+ /**
444
+ * Module-level navigation state. Updated by the router before calling
445
+ * renderRoot(). The renderRoot callback reads this to create the
446
+ * NavigationProvider with the correct values.
447
+ *
448
+ * This is NOT used by hooks directly — hooks read from React context.
449
+ * This exists only as a communication channel between the router
450
+ * (which knows the new nav state) and renderRoot (which wraps the element).
451
+ */
452
+ var _currentNavState = {
453
+ params: {},
454
+ pathname: "/"
455
+ };
456
+ function setNavigationState(state) {
457
+ _currentNavState = state;
436
458
  }
459
+ function getNavigationState() {
460
+ return _currentNavState;
461
+ }
462
+ //#endregion
463
+ //#region src/client/use-params.ts
437
464
  /**
438
- * Set the current route params WITHOUT notifying subscribers.
439
- * Called by the router before renderPayload() so that new components
440
- * in the RSC tree see the updated params via getSnapshot(), but
441
- * preserved layout components don't re-render prematurely with
442
- * {old tree, new params}.
465
+ * Set the current route params in the module-level store.
466
+ *
467
+ * Called by the router on each navigation. This updates the fallback
468
+ * snapshot used by tests and by the hook when called outside a React
469
+ * component (no NavigationContext available).
443
470
  *
444
- * After the React render commits, the router calls notifyParamsListeners()
445
- * to trigger re-renders in preserved layouts that read useParams().
471
+ * On the client, the primary reactivity path is NavigationContext —
472
+ * the router calls setNavigationState() then renderRoot() which wraps
473
+ * the element in NavigationProvider. setCurrentParams is still called
474
+ * for the module-level fallback.
446
475
  *
447
- * On the client, the segment router calls this on each navigation.
448
476
  * During SSR, params are also available via getSsrData().params
449
- * (ALS-backed), but setCurrentParams is still called for the
450
- * module-level fallback path.
477
+ * (ALS-backed).
451
478
  */
452
479
  function setCurrentParams(params) {
453
480
  _setCurrentParams(params);
454
481
  }
455
- /**
456
- * Notify all useSyncExternalStore subscribers that params have changed.
457
- * Called by the router AFTER renderPayload() so that preserved layout
458
- * components re-render only after the new tree is committed — producing
459
- * an atomic {new tree, new params} update instead of a stale
460
- * {old tree, new params} intermediate state.
461
- */
462
- function notifyParamsListeners() {
463
- for (const listener of paramsListeners) listener();
464
- }
465
482
  function useParams(_route) {
466
483
  try {
467
- return useSyncExternalStore(subscribe$2, getSnapshot, getServerSnapshot);
468
- } catch {
469
- return getServerSnapshot();
470
- }
484
+ const navContext = useNavigationContext();
485
+ if (navContext !== null) return navContext.params;
486
+ } catch {}
487
+ return getSsrData()?.params ?? currentParams;
471
488
  }
472
489
  //#endregion
473
490
  //#region src/client/router.ts
@@ -639,9 +656,21 @@ function createRouter(deps) {
639
656
  function renderPayload(payload) {
640
657
  if (deps.renderRoot) deps.renderRoot(payload);
641
658
  }
642
- /** Update useParams() with route params from the server response. */
643
- function updateParams(params) {
644
- setCurrentParams(params ?? {});
659
+ /**
660
+ * Update navigation state (params + pathname) for the next render.
661
+ *
662
+ * Sets both the module-level fallback (for tests and SSR) and the
663
+ * navigation context state (read by renderRoot to wrap the element
664
+ * in NavigationProvider). The context update is atomic with the tree
665
+ * render — both are passed to reactRoot.render() in the same call.
666
+ */
667
+ function updateNavigationState(params, url) {
668
+ const resolvedParams = params ?? {};
669
+ setCurrentParams(resolvedParams);
670
+ setNavigationState({
671
+ params: resolvedParams,
672
+ pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/"
673
+ });
645
674
  }
646
675
  /** Apply head elements (title, meta tags) to the DOM if available. */
647
676
  function applyHead(elements) {
@@ -682,11 +711,8 @@ function createRouter(deps) {
682
711
  params: result.params
683
712
  });
684
713
  updateSegmentCache(result.segmentInfo);
685
- updateParams(result.params);
686
- flushSync(() => {
687
- renderPayload(result.payload);
688
- notifyParamsListeners();
689
- });
714
+ updateNavigationState(result.params, url);
715
+ renderPayload(result.payload);
690
716
  applyHead(result.headElements);
691
717
  window.dispatchEvent(new Event("timber:navigation-end"));
692
718
  afterPaint(() => {
@@ -717,11 +743,8 @@ function createRouter(deps) {
717
743
  params: result.params
718
744
  });
719
745
  updateSegmentCache(result.segmentInfo);
720
- updateParams(result.params);
721
- flushSync(() => {
722
- renderPayload(result.payload);
723
- notifyParamsListeners();
724
- });
746
+ updateNavigationState(result.params, currentUrl);
747
+ renderPayload(result.payload);
725
748
  applyHead(result.headElements);
726
749
  } finally {
727
750
  setPending(false);
@@ -730,11 +753,8 @@ function createRouter(deps) {
730
753
  async function handlePopState(url, scrollY = 0) {
731
754
  const entry = historyStack.get(url);
732
755
  if (entry && entry.payload !== null) {
733
- updateParams(entry.params);
734
- flushSync(() => {
735
- renderPayload(entry.payload);
736
- notifyParamsListeners();
737
- });
756
+ updateNavigationState(entry.params, url);
757
+ renderPayload(entry.payload);
738
758
  applyHead(entry.headElements);
739
759
  afterPaint(() => {
740
760
  deps.scrollTo(0, scrollY);
@@ -745,16 +765,13 @@ function createRouter(deps) {
745
765
  try {
746
766
  const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree());
747
767
  updateSegmentCache(result.segmentInfo);
748
- updateParams(result.params);
768
+ updateNavigationState(result.params, url);
749
769
  historyStack.push(url, {
750
770
  payload: result.payload,
751
771
  headElements: result.headElements,
752
772
  params: result.params
753
773
  });
754
- flushSync(() => {
755
- renderPayload(result.payload);
756
- notifyParamsListeners();
757
- });
774
+ renderPayload(result.payload);
758
775
  applyHead(result.headElements);
759
776
  afterPaint(() => {
760
777
  deps.scrollTo(0, scrollY);
@@ -850,6 +867,20 @@ function useNavigationPending() {
850
867
  *
851
868
  * This wraps timber's internal RouterInstance in the Next.js-compatible
852
869
  * AppRouterInstance shape that ecosystem libraries expect.
870
+ *
871
+ * NOTE: Unlike Next.js, these methods do NOT wrap navigation in
872
+ * startTransition. In Next.js, router state is React state (useReducer)
873
+ * so startTransition defers the update and provides isPending tracking.
874
+ * In timber, navigation calls reactRoot.render() which is a root-level
875
+ * render — startTransition has no effect on root renders.
876
+ *
877
+ * Navigation state (params, pathname) is delivered atomically via
878
+ * NavigationContext embedded in the element tree passed to
879
+ * reactRoot.render(). See design/19-client-navigation.md §"NavigationContext".
880
+ *
881
+ * For loading UI during navigation, use:
882
+ * - useLinkStatus() — per-link pending indicator (inside <Link>)
883
+ * - useNavigationPending() — global navigation pending state
853
884
  */
854
885
  /**
855
886
  * Get a router instance for programmatic navigation.
@@ -876,9 +907,7 @@ function useRouter() {
876
907
  if (process.env.NODE_ENV === "development") console.error("[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.");
877
908
  return;
878
909
  }
879
- startTransition(async () => {
880
- await router.navigate(href, { scroll: options?.scroll });
881
- });
910
+ router.navigate(href, { scroll: options?.scroll });
882
911
  },
883
912
  replace(href, options) {
884
913
  const router = getRouterOrNull();
@@ -886,11 +915,9 @@ function useRouter() {
886
915
  if (process.env.NODE_ENV === "development") console.error("[timber] useRouter().replace() called but router is not initialized.");
887
916
  return;
888
917
  }
889
- startTransition(async () => {
890
- await router.navigate(href, {
891
- scroll: options?.scroll,
892
- replace: true
893
- });
918
+ router.navigate(href, {
919
+ scroll: options?.scroll,
920
+ replace: true
894
921
  });
895
922
  },
896
923
  refresh() {
@@ -899,9 +926,7 @@ function useRouter() {
899
926
  if (process.env.NODE_ENV === "development") console.error("[timber] useRouter().refresh() called but router is not initialized.");
900
927
  return;
901
928
  }
902
- startTransition(async () => {
903
- await router.refresh();
904
- });
929
+ router.refresh();
905
930
  },
906
931
  back() {
907
932
  if (typeof window !== "undefined") window.history.back();
@@ -924,31 +949,34 @@ function useRouter() {
924
949
  * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').
925
950
  * Updates when client-side navigation changes the URL.
926
951
  *
927
- * This is a thin wrapper over window.location.pathname, provided for
928
- * Next.js API compatibility (libraries like nuqs import usePathname
929
- * from next/navigation).
952
+ * On the client, reads from NavigationContext which is updated atomically
953
+ * with the RSC tree render. This replaces the previous useSyncExternalStore
954
+ * approach which only subscribed to popstate events — meaning usePathname()
955
+ * did NOT re-render on forward navigation (pushState). The context approach
956
+ * fixes this: pathname updates in the same render pass as the new tree.
930
957
  *
931
958
  * During SSR, reads the request pathname from the SSR ALS context
932
959
  * (populated by ssr-entry.ts) instead of window.location.
960
+ *
961
+ * Compatible with Next.js's `usePathname()` from `next/navigation`.
933
962
  */
934
- function getPathname() {
935
- if (typeof window !== "undefined") return window.location.pathname;
936
- return getSsrData()?.pathname ?? "/";
937
- }
938
- function getServerPathname() {
939
- return getSsrData()?.pathname ?? "/";
940
- }
941
- function subscribe$1(callback) {
942
- window.addEventListener("popstate", callback);
943
- return () => window.removeEventListener("popstate", callback);
944
- }
945
963
  /**
946
964
  * Read the current URL pathname.
947
965
  *
948
- * Compatible with Next.js's `usePathname()` from `next/navigation`.
966
+ * On the client, reads from NavigationContext (provided by
967
+ * NavigationProvider in renderRoot). During SSR, reads from the
968
+ * ALS-backed SSR data context. Falls back to window.location.pathname
969
+ * when called outside a React component (e.g., in tests).
949
970
  */
950
971
  function usePathname() {
951
- return useSyncExternalStore(subscribe$1, getPathname, getServerPathname);
972
+ try {
973
+ const navContext = useNavigationContext();
974
+ if (navContext !== null) return navContext.pathname;
975
+ } catch {}
976
+ const ssrData = getSsrData();
977
+ if (ssrData) return ssrData.pathname ?? "/";
978
+ if (typeof window !== "undefined") return window.location.pathname;
979
+ return "/";
952
980
  }
953
981
  //#endregion
954
982
  //#region src/client/use-search-params.ts
@@ -1247,6 +1275,6 @@ function useFormErrors(result) {
1247
1275
  };
1248
1276
  }
1249
1277
  //#endregion
1250
- export { HistoryStack, Link, LinkStatusContext, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getRouter, getSsrData, interpolateParams, resolveHref, setCurrentParams, setGlobalRouter, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, useParams, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
1278
+ export { HistoryStack, Link, LinkStatusContext, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getNavigationState, getRouter, getSsrData, interpolateParams, resolveHref, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, useParams, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
1251
1279
 
1252
1280
  //# sourceMappingURL=index.js.map