@timber-js/app 0.2.0-alpha.52 → 0.2.0-alpha.54

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 (57) hide show
  1. package/dist/_chunks/{segment-context-DBn-nrMN.js → segment-context-Bmugn-ao.js} +8 -5
  2. package/dist/_chunks/segment-context-Bmugn-ao.js.map +1 -0
  3. package/dist/client/index.js +23 -1
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/router.d.ts.map +1 -1
  6. package/dist/client/rsc-fetch.d.ts +13 -0
  7. package/dist/client/rsc-fetch.d.ts.map +1 -1
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/server/default-logger.d.ts.map +1 -1
  11. package/dist/server/flight-scripts.d.ts +5 -2
  12. package/dist/server/flight-scripts.d.ts.map +1 -1
  13. package/dist/server/index.js +1 -3
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/server/logger.d.ts +1 -0
  16. package/dist/server/logger.d.ts.map +1 -1
  17. package/dist/server/pipeline.d.ts +11 -0
  18. package/dist/server/pipeline.d.ts.map +1 -1
  19. package/dist/server/rsc-entry/error-renderer.d.ts +12 -8
  20. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  21. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  22. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  23. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  24. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  25. package/dist/server/rsc-entry/ssr-error-bridge.d.ts +12 -0
  26. package/dist/server/rsc-entry/ssr-error-bridge.d.ts.map +1 -0
  27. package/dist/server/rsc-entry/ssr-renderer.d.ts +12 -0
  28. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  29. package/dist/server/slot-resolver.d.ts.map +1 -1
  30. package/dist/server/ssr-error-entry.d.ts +65 -0
  31. package/dist/server/ssr-error-entry.d.ts.map +1 -0
  32. package/dist/shared/merge-search-params.d.ts.map +1 -1
  33. package/dist/shims/navigation.d.ts +1 -1
  34. package/dist/shims/navigation.d.ts.map +1 -1
  35. package/package.json +7 -6
  36. package/src/cli.ts +0 -0
  37. package/src/client/browser-entry.ts +7 -6
  38. package/src/client/router.ts +13 -1
  39. package/src/client/rsc-fetch.ts +25 -0
  40. package/src/plugins/routing.ts +7 -1
  41. package/src/server/default-logger.ts +4 -3
  42. package/src/server/flight-scripts.ts +6 -3
  43. package/src/server/logger.ts +6 -1
  44. package/src/server/pipeline.ts +1 -1
  45. package/src/server/rsc-entry/error-renderer.ts +201 -45
  46. package/src/server/rsc-entry/helpers.ts +12 -2
  47. package/src/server/rsc-entry/index.ts +4 -1
  48. package/src/server/rsc-entry/rsc-payload.ts +37 -4
  49. package/src/server/rsc-entry/rsc-stream.ts +21 -2
  50. package/src/server/rsc-entry/ssr-error-bridge.ts +20 -0
  51. package/src/server/rsc-entry/ssr-renderer.ts +85 -15
  52. package/src/server/slot-resolver.ts +7 -1
  53. package/src/server/ssr-error-entry.ts +237 -0
  54. package/src/shared/merge-search-params.ts +11 -4
  55. package/src/shims/navigation.ts +1 -0
  56. package/LICENSE +0 -8
  57. package/dist/_chunks/segment-context-DBn-nrMN.js.map +0 -1
@@ -23,15 +23,18 @@ import { createContext, createElement, useContext, useMemo } from "react";
23
23
  function mergePreservedSearchParams(targetHref, currentSearch, preserve) {
24
24
  const currentParams = new URLSearchParams(currentSearch);
25
25
  if (currentParams.size === 0) return targetHref;
26
- const qIdx = targetHref.indexOf("?");
27
- const targetPath = qIdx >= 0 ? targetHref.slice(0, qIdx) : targetHref;
28
- const targetParams = new URLSearchParams(qIdx >= 0 ? targetHref.slice(qIdx + 1) : "");
26
+ const hashIdx = targetHref.indexOf("#");
27
+ const hrefWithoutHash = hashIdx >= 0 ? targetHref.slice(0, hashIdx) : targetHref;
28
+ const hash = hashIdx >= 0 ? targetHref.slice(hashIdx) : "";
29
+ const qIdx = hrefWithoutHash.indexOf("?");
30
+ const targetPath = qIdx >= 0 ? hrefWithoutHash.slice(0, qIdx) : hrefWithoutHash;
31
+ const targetParams = new URLSearchParams(qIdx >= 0 ? hrefWithoutHash.slice(qIdx + 1) : "");
29
32
  const merged = new URLSearchParams(targetParams);
30
33
  for (const [key, value] of currentParams) if (!targetParams.has(key)) {
31
34
  if (preserve === true || preserve.includes(key)) merged.append(key, value);
32
35
  }
33
36
  const qs = merged.toString();
34
- return qs ? `${targetPath}?${qs}` : targetPath;
37
+ return (qs ? `${targetPath}?${qs}` : targetPath) + hash;
35
38
  }
36
39
  //#endregion
37
40
  //#region src/client/segment-context.ts
@@ -66,4 +69,4 @@ function SegmentProvider({ segments, segmentId: _segmentId, parallelRouteKeys, c
66
69
  //#endregion
67
70
  export { useSegmentContext as n, mergePreservedSearchParams as r, SegmentProvider as t };
68
71
 
69
- //# sourceMappingURL=segment-context-DBn-nrMN.js.map
72
+ //# sourceMappingURL=segment-context-Bmugn-ao.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-context-Bmugn-ao.js","names":[],"sources":["../../src/shared/merge-search-params.ts","../../src/client/segment-context.ts"],"sourcesContent":["/**\n * Shared utility for merging preserved search params into a target URL.\n *\n * Used by both <Link> (client) and redirect() (server). Extracted to a shared\n * module to avoid importing client code ('use client') from server modules.\n */\n\n/**\n * Merge preserved search params from the current URL into a target href.\n *\n * When `preserve` is `true`, all current search params are merged.\n * When `preserve` is a `string[]`, only the named params are merged.\n *\n * The target href's own search params take precedence — preserved params\n * are only added if the target doesn't already define them.\n *\n * @param targetHref - The resolved target href (may already contain query string)\n * @param currentSearch - The current URL's search string (e.g. \"?private=access&page=2\")\n * @param preserve - `true` to preserve all, or `string[]` to preserve specific params\n * @returns The target href with preserved search params merged in\n */\nexport function mergePreservedSearchParams(\n targetHref: string,\n currentSearch: string,\n preserve: true | string[]\n): string {\n const currentParams = new URLSearchParams(currentSearch);\n if (currentParams.size === 0) return targetHref;\n\n // Split hash fragment from target before processing query params.\n // Hash must come after query string: /path?query=value#hash\n const hashIdx = targetHref.indexOf('#');\n const hrefWithoutHash = hashIdx >= 0 ? targetHref.slice(0, hashIdx) : targetHref;\n const hash = hashIdx >= 0 ? targetHref.slice(hashIdx) : '';\n\n // Split target into path and existing query\n const qIdx = hrefWithoutHash.indexOf('?');\n const targetPath = qIdx >= 0 ? hrefWithoutHash.slice(0, qIdx) : hrefWithoutHash;\n const targetParams = new URLSearchParams(qIdx >= 0 ? hrefWithoutHash.slice(qIdx + 1) : '');\n\n // Collect params to preserve (that aren't already in the target)\n const merged = new URLSearchParams(targetParams);\n for (const [key, value] of currentParams) {\n // Only preserve if: (a) not already in target, and (b) included in preserve list\n if (!targetParams.has(key)) {\n if (preserve === true || preserve.includes(key)) {\n merged.append(key, value);\n }\n }\n }\n\n const qs = merged.toString();\n const pathWithQuery = qs ? `${targetPath}?${qs}` : targetPath;\n return pathWithQuery + hash;\n}\n","/**\n * Segment Context — provides layout segment position for useSelectedLayoutSegment hooks.\n *\n * Each layout in the segment tree is wrapped with a SegmentProvider that stores\n * the URL segments from root to the current layout level. The hooks read this\n * context to determine which child segments are active below the calling layout.\n *\n * The context value is intentionally minimal: just the segment path array and\n * parallel route keys. No internal cache details are exposed.\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { createContext, useContext, createElement, useMemo } from 'react';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface SegmentContextValue {\n /** URL segments from root to this layout (e.g. ['', 'dashboard', 'settings']) */\n segments: string[];\n /** Parallel route slot keys available at this layout level (e.g. ['sidebar', 'modal']) */\n parallelRouteKeys: string[];\n}\n\n// ─── Context ─────────────────────────────────────────────────────\n\nconst SegmentContext = createContext<SegmentContextValue | null>(null);\n\n/** Read the segment context. Returns null if no provider is above this component. */\nexport function useSegmentContext(): SegmentContextValue | null {\n return useContext(SegmentContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface SegmentProviderProps {\n segments: string[];\n /**\n * Unique identifier for this segment, used by the client-side segment\n * merger for element caching. For route groups this includes the group\n * name (e.g., \"/(marketing)\") since groups share their parent's urlPath.\n * Falls back to the reconstructed path from `segments` if not provided.\n */\n segmentId?: string;\n parallelRouteKeys: string[];\n children: React.ReactNode;\n}\n\n/**\n * Wraps each layout to provide segment position context.\n * Injected by rsc-entry.ts during element tree construction.\n */\nexport function SegmentProvider({\n segments,\n segmentId: _segmentId,\n parallelRouteKeys,\n children,\n}: SegmentProviderProps) {\n const value = useMemo(\n () => ({ segments, parallelRouteKeys }),\n // segments and parallelRouteKeys are static per layout — they don't change\n // across navigations. The layout's position in the tree is fixed.\n // Intentionally using derived keys — segments/parallelRouteKeys are static per layout\n [segments.join('/'), parallelRouteKeys.join(',')]\n );\n return createElement(SegmentContext.Provider, { value }, children);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,2BACd,YACA,eACA,UACQ;CACR,MAAM,gBAAgB,IAAI,gBAAgB,cAAc;AACxD,KAAI,cAAc,SAAS,EAAG,QAAO;CAIrC,MAAM,UAAU,WAAW,QAAQ,IAAI;CACvC,MAAM,kBAAkB,WAAW,IAAI,WAAW,MAAM,GAAG,QAAQ,GAAG;CACtE,MAAM,OAAO,WAAW,IAAI,WAAW,MAAM,QAAQ,GAAG;CAGxD,MAAM,OAAO,gBAAgB,QAAQ,IAAI;CACzC,MAAM,aAAa,QAAQ,IAAI,gBAAgB,MAAM,GAAG,KAAK,GAAG;CAChE,MAAM,eAAe,IAAI,gBAAgB,QAAQ,IAAI,gBAAgB,MAAM,OAAO,EAAE,GAAG,GAAG;CAG1F,MAAM,SAAS,IAAI,gBAAgB,aAAa;AAChD,MAAK,MAAM,CAAC,KAAK,UAAU,cAEzB,KAAI,CAAC,aAAa,IAAI,IAAI;MACpB,aAAa,QAAQ,SAAS,SAAS,IAAI,CAC7C,QAAO,OAAO,KAAK,MAAM;;CAK/B,MAAM,KAAK,OAAO,UAAU;AAE5B,SADsB,KAAK,GAAG,WAAW,GAAG,OAAO,cAC5B;;;;;;;;;;;;;;;;ACzBzB,IAAM,iBAAiB,cAA0C,KAAK;;AAGtE,SAAgB,oBAAgD;AAC9D,QAAO,WAAW,eAAe;;;;;;AAsBnC,SAAgB,gBAAgB,EAC9B,UACA,WAAW,YACX,mBACA,YACuB;CACvB,MAAM,QAAQ,eACL;EAAE;EAAU;EAAmB,GAItC,CAAC,SAAS,KAAK,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC,CAClD;AACD,QAAO,cAAc,eAAe,UAAU,EAAE,OAAO,EAAE,SAAS"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { n as __exportAll } from "../_chunks/chunk-DYhsFzuS.js";
3
3
  import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-wEXY2JQB.js";
4
- import { n as useSegmentContext, r as mergePreservedSearchParams, t as SegmentProvider } from "../_chunks/segment-context-DBn-nrMN.js";
4
+ import { n as useSegmentContext, r as mergePreservedSearchParams, t as SegmentProvider } from "../_chunks/segment-context-Bmugn-ao.js";
5
5
  import { a as _setCachedSearch, c as cachedSearch, d as globalRouter, i as setSsrData, l as cachedSearchParams, n as clearSsrData, o as _setCurrentParams, r as getSsrData, s as _setGlobalRouter, t as TimberErrorBoundary, u as currentParams } from "../_chunks/error-boundary-B9vT_YK_.js";
6
6
  import { t as _registerUseCookieModule } from "../_chunks/define-cookie-k9btcEfI.js";
7
7
  import React, { cloneElement, createContext, createElement, isValidElement, useActionState as useActionState$1, useContext, useSyncExternalStore, useTransition } from "react";
@@ -886,6 +886,23 @@ var VersionSkewError = class extends Error {
886
886
  }
887
887
  };
888
888
  /**
889
+ * Thrown when the server returns a 5xx error for an RSC payload request.
890
+ * The server sends X-Timber-Error header and a JSON body instead of a
891
+ * broken RSC stream. Caught in navigate() to trigger a hard navigation
892
+ * so the server can render the error page as HTML.
893
+ *
894
+ * See design/10-error-handling.md §"Error Page Rendering for Client Navigation"
895
+ */
896
+ var ServerErrorResponse = class extends Error {
897
+ status;
898
+ url;
899
+ constructor(status, url) {
900
+ super(`Server error ${status} during navigation to ${url}`);
901
+ this.status = status;
902
+ this.url = url;
903
+ }
904
+ };
905
+ /**
889
906
  * Fetch an RSC payload from the server. If a decodeRsc function is provided,
890
907
  * the response is decoded into a React element tree via createFromFetch.
891
908
  * Otherwise, the raw response text is returned (test mode).
@@ -909,6 +926,7 @@ async function fetchRscPayload(url, deps, stateTree, currentUrl) {
909
926
  if (checkReloadSignal(response)) throw new VersionSkewError();
910
927
  const redirectLocation = response.headers.get("X-Timber-Redirect") || (response.status >= 300 && response.status < 400 ? response.headers.get("Location") : null);
911
928
  if (redirectLocation) throw new RedirectError(redirectLocation);
929
+ if (response.headers.get("X-Timber-Error") === "1") throw new ServerErrorResponse(response.status, url);
912
930
  headElements = extractHeadElements(response);
913
931
  segmentInfo = extractSegmentInfo(response);
914
932
  params = extractParams(response);
@@ -1114,6 +1132,10 @@ function createRouter(deps) {
1114
1132
  await navigate(error.redirectUrl, { replace: true });
1115
1133
  return;
1116
1134
  }
1135
+ if (error instanceof ServerErrorResponse) {
1136
+ window.location.href = error.url;
1137
+ return new Promise(() => {});
1138
+ }
1117
1139
  if (isAbortError(error)) return;
1118
1140
  throw error;
1119
1141
  } finally {