@btst/stack 1.0.1 → 1.1.1

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 (282) hide show
  1. package/README.md +156 -709
  2. package/dist/api/index.cjs +2 -1
  3. package/dist/api/index.d.cts +4 -3
  4. package/dist/api/index.d.mts +4 -3
  5. package/dist/api/index.d.ts +4 -3
  6. package/dist/api/index.mjs +1 -1
  7. package/dist/client/components/compose.cjs +68 -0
  8. package/dist/client/components/compose.mjs +65 -0
  9. package/dist/client/components/error-boundary.cjs +24 -0
  10. package/dist/client/components/error-boundary.mjs +22 -0
  11. package/dist/client/components/index.cjs +10 -0
  12. package/dist/client/components/index.d.cts +52 -0
  13. package/dist/client/components/index.d.mts +52 -0
  14. package/dist/client/components/index.d.ts +52 -0
  15. package/dist/client/components/index.mjs +2 -0
  16. package/dist/client/index.cjs +24 -5
  17. package/dist/client/index.d.cts +125 -8
  18. package/dist/client/index.d.mts +125 -8
  19. package/dist/client/index.d.ts +125 -8
  20. package/dist/client/index.mjs +21 -4
  21. package/dist/client/meta-utils.cjs +162 -0
  22. package/dist/client/meta-utils.mjs +160 -0
  23. package/dist/client/path-utils.cjs +15 -0
  24. package/dist/client/path-utils.mjs +13 -0
  25. package/dist/client/sitemap-utils.cjs +14 -0
  26. package/dist/client/sitemap-utils.mjs +12 -0
  27. package/dist/context/index.cjs +6 -63
  28. package/dist/context/index.d.cts +21 -24
  29. package/dist/context/index.d.mts +21 -24
  30. package/dist/context/index.d.ts +21 -24
  31. package/dist/context/index.mjs +1 -61
  32. package/dist/context/provider.cjs +51 -0
  33. package/dist/context/provider.mjs +46 -0
  34. package/dist/index.cjs +2 -3
  35. package/dist/index.d.cts +3 -2
  36. package/dist/index.d.mts +3 -2
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.mjs +1 -2
  39. package/dist/plugins/api/index.cjs +13 -0
  40. package/dist/plugins/api/index.d.cts +40 -0
  41. package/dist/plugins/api/index.d.mts +40 -0
  42. package/dist/plugins/api/index.d.ts +40 -0
  43. package/dist/plugins/api/index.mjs +8 -0
  44. package/dist/plugins/blog/api/index.cjs +11 -0
  45. package/dist/plugins/blog/api/index.d.cts +7 -0
  46. package/dist/plugins/blog/api/index.d.mts +7 -0
  47. package/dist/plugins/blog/api/index.d.ts +7 -0
  48. package/dist/plugins/blog/api/index.mjs +2 -0
  49. package/dist/plugins/blog/api/plugin.cjs +569 -0
  50. package/dist/plugins/blog/api/plugin.mjs +565 -0
  51. package/dist/plugins/blog/client/components/forms/image-field.cjs +133 -0
  52. package/dist/plugins/blog/client/components/forms/image-field.mjs +131 -0
  53. package/dist/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
  54. package/dist/plugins/blog/client/components/forms/markdown-editor.cjs +106 -0
  55. package/dist/plugins/blog/client/components/forms/markdown-editor.mjs +104 -0
  56. package/dist/plugins/blog/client/components/forms/post-forms.cjs +401 -0
  57. package/dist/plugins/blog/client/components/forms/post-forms.mjs +398 -0
  58. package/dist/plugins/blog/client/components/forms/tags-multiselect.cjs +71 -0
  59. package/dist/plugins/blog/client/components/forms/tags-multiselect.mjs +65 -0
  60. package/dist/plugins/blog/client/components/index.cjs +17 -0
  61. package/dist/plugins/blog/client/components/index.d.cts +22 -0
  62. package/dist/plugins/blog/client/components/index.d.mts +22 -0
  63. package/dist/plugins/blog/client/components/index.d.ts +22 -0
  64. package/dist/plugins/blog/client/components/index.mjs +12 -0
  65. package/dist/plugins/blog/client/components/loading/form-page-skeleton.cjs +62 -0
  66. package/dist/plugins/blog/client/components/loading/form-page-skeleton.mjs +60 -0
  67. package/dist/plugins/blog/client/components/loading/index.cjs +20 -0
  68. package/dist/plugins/blog/client/components/loading/index.mjs +16 -0
  69. package/dist/plugins/blog/client/components/loading/list-page-skeleton.cjs +26 -0
  70. package/dist/plugins/blog/client/components/loading/list-page-skeleton.mjs +24 -0
  71. package/dist/plugins/blog/client/components/loading/page-header-skeleton.cjs +13 -0
  72. package/dist/plugins/blog/client/components/loading/page-header-skeleton.mjs +11 -0
  73. package/dist/plugins/blog/client/components/loading/post-card-skeleton.cjs +22 -0
  74. package/dist/plugins/blog/client/components/loading/post-card-skeleton.mjs +20 -0
  75. package/dist/plugins/blog/client/components/loading/post-page-skeleton.cjs +56 -0
  76. package/dist/plugins/blog/client/components/loading/post-page-skeleton.mjs +54 -0
  77. package/dist/plugins/blog/client/components/pages/404-page.cjs +19 -0
  78. package/dist/plugins/blog/client/components/pages/404-page.mjs +17 -0
  79. package/dist/plugins/blog/client/components/pages/edit-post-page.cjs +41 -0
  80. package/dist/plugins/blog/client/components/pages/edit-post-page.internal.cjs +57 -0
  81. package/dist/plugins/blog/client/components/pages/edit-post-page.internal.mjs +55 -0
  82. package/dist/plugins/blog/client/components/pages/edit-post-page.mjs +39 -0
  83. package/dist/plugins/blog/client/components/pages/home-page.cjs +41 -0
  84. package/dist/plugins/blog/client/components/pages/home-page.internal.cjs +61 -0
  85. package/dist/plugins/blog/client/components/pages/home-page.internal.mjs +59 -0
  86. package/dist/plugins/blog/client/components/pages/home-page.mjs +39 -0
  87. package/dist/plugins/blog/client/components/pages/new-post-page.cjs +37 -0
  88. package/dist/plugins/blog/client/components/pages/new-post-page.internal.cjs +53 -0
  89. package/dist/plugins/blog/client/components/pages/new-post-page.internal.mjs +51 -0
  90. package/dist/plugins/blog/client/components/pages/new-post-page.mjs +35 -0
  91. package/dist/plugins/blog/client/components/pages/post-page.cjs +39 -0
  92. package/dist/plugins/blog/client/components/pages/post-page.internal.cjs +101 -0
  93. package/dist/plugins/blog/client/components/pages/post-page.internal.mjs +99 -0
  94. package/dist/plugins/blog/client/components/pages/post-page.mjs +37 -0
  95. package/dist/plugins/blog/client/components/pages/tag-page.cjs +39 -0
  96. package/dist/plugins/blog/client/components/pages/tag-page.internal.cjs +61 -0
  97. package/dist/plugins/blog/client/components/pages/tag-page.internal.mjs +59 -0
  98. package/dist/plugins/blog/client/components/pages/tag-page.mjs +37 -0
  99. package/dist/plugins/blog/client/components/shared/better-blog-attribution.cjs +24 -0
  100. package/dist/plugins/blog/client/components/shared/better-blog-attribution.mjs +22 -0
  101. package/dist/plugins/blog/client/components/shared/default-error.cjs +18 -0
  102. package/dist/plugins/blog/client/components/shared/default-error.mjs +16 -0
  103. package/dist/plugins/blog/client/components/shared/defaults.cjs +13 -0
  104. package/dist/plugins/blog/client/components/shared/defaults.mjs +10 -0
  105. package/dist/plugins/blog/client/components/shared/empty-list.cjs +21 -0
  106. package/dist/plugins/blog/client/components/shared/empty-list.mjs +19 -0
  107. package/dist/plugins/blog/client/components/shared/error-placeholder.cjs +24 -0
  108. package/dist/plugins/blog/client/components/shared/error-placeholder.mjs +22 -0
  109. package/dist/plugins/blog/client/components/shared/highlight-text.cjs +53 -0
  110. package/dist/plugins/blog/client/components/shared/highlight-text.mjs +51 -0
  111. package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
  112. package/dist/plugins/blog/client/components/shared/markdown-content.cjs +324 -0
  113. package/dist/plugins/blog/client/components/shared/markdown-content.mjs +315 -0
  114. package/dist/plugins/blog/client/components/shared/on-this-page.cjs +161 -0
  115. package/dist/plugins/blog/client/components/shared/on-this-page.mjs +158 -0
  116. package/dist/plugins/blog/client/components/shared/page-header.cjs +40 -0
  117. package/dist/plugins/blog/client/components/shared/page-header.mjs +38 -0
  118. package/dist/plugins/blog/client/components/shared/page-layout.cjs +24 -0
  119. package/dist/plugins/blog/client/components/shared/page-layout.mjs +22 -0
  120. package/dist/plugins/blog/client/components/shared/page-wrapper.cjs +23 -0
  121. package/dist/plugins/blog/client/components/shared/page-wrapper.mjs +21 -0
  122. package/dist/plugins/blog/client/components/shared/post-card.cjs +279 -0
  123. package/dist/plugins/blog/client/components/shared/post-card.mjs +277 -0
  124. package/dist/plugins/blog/client/components/shared/post-navigation.cjs +74 -0
  125. package/dist/plugins/blog/client/components/shared/post-navigation.mjs +72 -0
  126. package/dist/plugins/blog/client/components/shared/posts-list.cjs +48 -0
  127. package/dist/plugins/blog/client/components/shared/posts-list.mjs +46 -0
  128. package/dist/plugins/blog/client/components/shared/recent-posts-carousel.cjs +59 -0
  129. package/dist/plugins/blog/client/components/shared/recent-posts-carousel.mjs +57 -0
  130. package/dist/plugins/blog/client/components/shared/search-input.cjs +136 -0
  131. package/dist/plugins/blog/client/components/shared/search-input.mjs +117 -0
  132. package/dist/plugins/blog/client/components/shared/search-modal.cjs +135 -0
  133. package/dist/plugins/blog/client/components/shared/search-modal.mjs +116 -0
  134. package/dist/plugins/blog/client/components/shared/tags-list.cjs +22 -0
  135. package/dist/plugins/blog/client/components/shared/tags-list.mjs +20 -0
  136. package/dist/plugins/blog/client/components/shared/use-route-lifecycle.cjs +50 -0
  137. package/dist/plugins/blog/client/components/shared/use-route-lifecycle.mjs +48 -0
  138. package/dist/plugins/blog/client/hooks/blog-hooks.cjs +380 -0
  139. package/dist/plugins/blog/client/hooks/blog-hooks.mjs +368 -0
  140. package/dist/plugins/blog/client/hooks/index.cjs +17 -0
  141. package/dist/plugins/blog/client/hooks/index.d.cts +150 -0
  142. package/dist/plugins/blog/client/hooks/index.d.mts +150 -0
  143. package/dist/plugins/blog/client/hooks/index.d.ts +150 -0
  144. package/dist/plugins/blog/client/hooks/index.mjs +1 -0
  145. package/dist/plugins/blog/client/hooks/use-debounce.cjs +16 -0
  146. package/dist/plugins/blog/client/hooks/use-debounce.mjs +14 -0
  147. package/dist/plugins/blog/client/index.cjs +7 -0
  148. package/dist/plugins/blog/client/index.d.cts +414 -0
  149. package/dist/plugins/blog/client/index.d.mts +414 -0
  150. package/dist/plugins/blog/client/index.d.ts +414 -0
  151. package/dist/plugins/blog/client/index.mjs +1 -0
  152. package/dist/plugins/blog/client/localization/blog-card.cjs +7 -0
  153. package/dist/plugins/blog/client/localization/blog-card.mjs +5 -0
  154. package/dist/plugins/blog/client/localization/blog-common.cjs +10 -0
  155. package/dist/plugins/blog/client/localization/blog-common.mjs +8 -0
  156. package/dist/plugins/blog/client/localization/blog-forms.cjs +40 -0
  157. package/dist/plugins/blog/client/localization/blog-forms.mjs +38 -0
  158. package/dist/plugins/blog/client/localization/blog-list.cjs +18 -0
  159. package/dist/plugins/blog/client/localization/blog-list.mjs +16 -0
  160. package/dist/plugins/blog/client/localization/blog-post.cjs +13 -0
  161. package/dist/plugins/blog/client/localization/blog-post.mjs +11 -0
  162. package/dist/plugins/blog/client/localization/index.cjs +17 -0
  163. package/dist/plugins/blog/client/localization/index.mjs +15 -0
  164. package/dist/plugins/blog/client/plugin.cjs +462 -0
  165. package/dist/plugins/blog/client/plugin.mjs +460 -0
  166. package/dist/plugins/blog/client.css +3 -0
  167. package/dist/plugins/blog/db.cjs +90 -0
  168. package/dist/plugins/blog/db.mjs +88 -0
  169. package/dist/plugins/blog/query-keys.cjs +181 -0
  170. package/dist/plugins/blog/query-keys.d.cts +530 -0
  171. package/dist/plugins/blog/query-keys.d.mts +530 -0
  172. package/dist/plugins/blog/query-keys.d.ts +530 -0
  173. package/dist/plugins/blog/query-keys.mjs +179 -0
  174. package/dist/plugins/blog/schemas.cjs +39 -0
  175. package/dist/plugins/blog/schemas.mjs +35 -0
  176. package/dist/plugins/blog/style.css +22 -0
  177. package/dist/plugins/blog/utils.cjs +97 -0
  178. package/dist/plugins/blog/utils.mjs +87 -0
  179. package/dist/plugins/client/index.cjs +15 -0
  180. package/dist/plugins/client/index.d.cts +57 -0
  181. package/dist/plugins/client/index.d.mts +57 -0
  182. package/dist/plugins/client/index.d.ts +57 -0
  183. package/dist/plugins/client/index.mjs +9 -0
  184. package/dist/{shared/stack.3OUyGp_E.mjs → plugins/utils.mjs} +1 -1
  185. package/dist/shared/{stack.DORw_1ps.d.cts → stack.ByOugz9d.d.cts} +17 -1
  186. package/dist/shared/{stack.DORw_1ps.d.mts → stack.ByOugz9d.d.mts} +17 -1
  187. package/dist/shared/{stack.DORw_1ps.d.ts → stack.ByOugz9d.d.ts} +17 -1
  188. package/dist/shared/stack.CoPoHVfV.d.cts +76 -0
  189. package/dist/shared/stack.CoPoHVfV.d.mts +76 -0
  190. package/dist/shared/stack.CoPoHVfV.d.ts +76 -0
  191. package/package.json +102 -14
  192. package/src/__tests__/plugins.test.tsx +539 -0
  193. package/src/__tests__/sitemap.test.ts +60 -0
  194. package/src/api/index.ts +75 -0
  195. package/src/client/components/compose.tsx +116 -0
  196. package/src/client/components/error-boundary.tsx +30 -0
  197. package/src/client/components/index.tsx +2 -0
  198. package/src/client/index.ts +109 -0
  199. package/src/client/meta-utils.ts +228 -0
  200. package/src/client/path-utils.ts +38 -0
  201. package/src/client/sitemap-utils.ts +46 -0
  202. package/src/context/index.ts +1 -0
  203. package/src/context/provider.tsx +157 -0
  204. package/src/index.ts +1 -0
  205. package/src/plugins/api/index.ts +50 -0
  206. package/src/plugins/blog/api/index.ts +2 -0
  207. package/src/plugins/blog/api/plugin.ts +759 -0
  208. package/src/plugins/blog/client/components/forms/image-field.tsx +165 -0
  209. package/src/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
  210. package/src/plugins/blog/client/components/forms/markdown-editor.tsx +136 -0
  211. package/src/plugins/blog/client/components/forms/post-forms.tsx +531 -0
  212. package/src/plugins/blog/client/components/forms/tags-multiselect.tsx +79 -0
  213. package/src/plugins/blog/client/components/index.tsx +11 -0
  214. package/src/plugins/blog/client/components/loading/form-page-skeleton.tsx +75 -0
  215. package/src/plugins/blog/client/components/loading/index.tsx +27 -0
  216. package/src/plugins/blog/client/components/loading/list-page-skeleton.tsx +38 -0
  217. package/src/plugins/blog/client/components/loading/page-header-skeleton.tsx +10 -0
  218. package/src/plugins/blog/client/components/loading/post-card-skeleton.tsx +30 -0
  219. package/src/plugins/blog/client/components/loading/post-page-skeleton.tsx +75 -0
  220. package/src/plugins/blog/client/components/pages/404-page.tsx +23 -0
  221. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +60 -0
  222. package/src/plugins/blog/client/components/pages/edit-post-page.tsx +40 -0
  223. package/src/plugins/blog/client/components/pages/home-page.internal.tsx +71 -0
  224. package/src/plugins/blog/client/components/pages/home-page.tsx +42 -0
  225. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +59 -0
  226. package/src/plugins/blog/client/components/pages/new-post-page.tsx +36 -0
  227. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +142 -0
  228. package/src/plugins/blog/client/components/pages/post-page.tsx +38 -0
  229. package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +74 -0
  230. package/src/plugins/blog/client/components/pages/tag-page.tsx +38 -0
  231. package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +19 -0
  232. package/src/plugins/blog/client/components/shared/default-error.tsx +20 -0
  233. package/src/plugins/blog/client/components/shared/defaults.tsx +9 -0
  234. package/src/plugins/blog/client/components/shared/empty-list.tsx +25 -0
  235. package/src/plugins/blog/client/components/shared/error-placeholder.tsx +20 -0
  236. package/src/plugins/blog/client/components/shared/highlight-text.tsx +80 -0
  237. package/src/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
  238. package/src/plugins/blog/client/components/shared/markdown-content.tsx +448 -0
  239. package/src/plugins/blog/client/components/shared/on-this-page.tsx +234 -0
  240. package/src/plugins/blog/client/components/shared/page-header.tsx +35 -0
  241. package/src/plugins/blog/client/components/shared/page-layout.tsx +23 -0
  242. package/src/plugins/blog/client/components/shared/page-wrapper.tsx +32 -0
  243. package/src/plugins/blog/client/components/shared/post-card.tsx +308 -0
  244. package/src/plugins/blog/client/components/shared/post-navigation.tsx +98 -0
  245. package/src/plugins/blog/client/components/shared/posts-list.tsx +67 -0
  246. package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +79 -0
  247. package/src/plugins/blog/client/components/shared/search-input.tsx +146 -0
  248. package/src/plugins/blog/client/components/shared/search-modal.tsx +162 -0
  249. package/src/plugins/blog/client/components/shared/tags-list.tsx +34 -0
  250. package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +68 -0
  251. package/src/plugins/blog/client/hooks/blog-hooks.tsx +623 -0
  252. package/src/plugins/blog/client/hooks/index.tsx +1 -0
  253. package/src/plugins/blog/client/hooks/use-debounce.ts +43 -0
  254. package/src/plugins/blog/client/index.ts +9 -0
  255. package/src/plugins/blog/client/localization/blog-card.ts +3 -0
  256. package/src/plugins/blog/client/localization/blog-common.ts +7 -0
  257. package/src/plugins/blog/client/localization/blog-forms.ts +45 -0
  258. package/src/plugins/blog/client/localization/blog-list.ts +14 -0
  259. package/src/plugins/blog/client/localization/blog-post.ts +9 -0
  260. package/src/plugins/blog/client/localization/index.ts +15 -0
  261. package/src/plugins/blog/client/overrides.ts +123 -0
  262. package/src/plugins/blog/client/plugin.tsx +672 -0
  263. package/src/plugins/blog/client.css +3 -0
  264. package/src/plugins/blog/db.ts +90 -0
  265. package/src/plugins/blog/query-keys.ts +267 -0
  266. package/src/plugins/blog/schemas.ts +39 -0
  267. package/src/plugins/blog/style.css +22 -0
  268. package/src/plugins/blog/types.ts +37 -0
  269. package/src/plugins/blog/utils.ts +144 -0
  270. package/src/plugins/client/index.ts +53 -0
  271. package/src/plugins/index.ts +0 -0
  272. package/src/plugins/utils.ts +35 -0
  273. package/src/types.ts +209 -0
  274. package/dist/plugins/index.cjs +0 -15
  275. package/dist/plugins/index.d.cts +0 -64
  276. package/dist/plugins/index.d.mts +0 -64
  277. package/dist/plugins/index.d.ts +0 -64
  278. package/dist/plugins/index.mjs +0 -11
  279. package/dist/shared/stack.DrUAVfIH.d.cts +0 -17
  280. package/dist/shared/stack.DrUAVfIH.d.mts +0 -17
  281. package/dist/shared/stack.DrUAVfIH.d.ts +0 -17
  282. /package/dist/{shared/stack.CktCg4PJ.cjs → plugins/utils.cjs} +0 -0
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import React, { Suspense, type ErrorInfo } from "react";
4
+ import { type FallbackProps } from "react-error-boundary";
5
+ import type { createRouter } from "@btst/yar";
6
+ import { ErrorBoundary } from "./error-boundary";
7
+
8
+ /**
9
+ * Route type with optional components
10
+ */
11
+ export type RouteWithComponents =
12
+ | {
13
+ PageComponent?: React.ComponentType;
14
+ ErrorComponent?: React.ComponentType<FallbackProps>;
15
+ LoadingComponent?: React.ComponentType;
16
+ }
17
+ | null
18
+ | undefined;
19
+
20
+ /**
21
+ * Composes the route content with Suspense and Error Boundary
22
+ * Resolves the route on the client-side where component references are available
23
+ *
24
+ * This is marked "use client" so it can access component references safely
25
+ */
26
+ export function RouteRenderer({
27
+ router,
28
+ path,
29
+ NotFoundComponent,
30
+ onNotFound,
31
+ onError,
32
+ props,
33
+ }: {
34
+ router: ReturnType<typeof createRouter>;
35
+ path: string;
36
+ NotFoundComponent?: React.ComponentType<{ message: string }>;
37
+ onNotFound?: () => never;
38
+ onError: (error: Error, info: ErrorInfo) => void;
39
+ props?: any;
40
+ }) {
41
+ // Resolve route on the client where components are available
42
+ const route = router.getRoute(path);
43
+
44
+ return (
45
+ <ComposedRoute
46
+ path={path}
47
+ PageComponent={route?.PageComponent}
48
+ ErrorComponent={route?.ErrorComponent}
49
+ LoadingComponent={route?.LoadingComponent}
50
+ onNotFound={onNotFound}
51
+ NotFoundComponent={NotFoundComponent}
52
+ onError={onError}
53
+ props={props}
54
+ />
55
+ );
56
+ }
57
+
58
+ export function ComposedRoute({
59
+ path,
60
+ PageComponent,
61
+ ErrorComponent,
62
+ LoadingComponent,
63
+ onNotFound,
64
+ NotFoundComponent,
65
+ props,
66
+ onError,
67
+ }: {
68
+ path: string;
69
+ PageComponent: React.ComponentType<any>;
70
+ ErrorComponent?: React.ComponentType<FallbackProps>;
71
+ LoadingComponent: React.ComponentType;
72
+ onNotFound?: () => never;
73
+ NotFoundComponent?: React.ComponentType<{ message: string }>;
74
+ props?: any;
75
+ onError: (error: Error, info: ErrorInfo) => void;
76
+ }) {
77
+ if (PageComponent) {
78
+ const content = <PageComponent {...props} />;
79
+ // Avoid server-side skeletons: only show loading fallback in the browser
80
+ const isBrowser = typeof window !== "undefined";
81
+ const suspenseFallback =
82
+ isBrowser && LoadingComponent ? <LoadingComponent /> : null;
83
+
84
+ // If an ErrorComponent is provided (which itself may be lazy), ensure we have
85
+ // a Suspense boundary that can handle both the page content and the lazy error UI
86
+ if (ErrorComponent) {
87
+ return (
88
+ <Suspense key={`outer-${path}`} fallback={suspenseFallback}>
89
+ <ErrorBoundary
90
+ FallbackComponent={ErrorComponent}
91
+ resetKeys={[path]}
92
+ onError={onError}
93
+ >
94
+ <Suspense key={`inner-${path}`} fallback={suspenseFallback}>
95
+ {content}
96
+ </Suspense>
97
+ </ErrorBoundary>
98
+ </Suspense>
99
+ );
100
+ }
101
+
102
+ return (
103
+ <Suspense key={path} fallback={suspenseFallback}>
104
+ {content}
105
+ </Suspense>
106
+ );
107
+ } else {
108
+ if (onNotFound) {
109
+ onNotFound();
110
+ }
111
+
112
+ if (NotFoundComponent) {
113
+ return <NotFoundComponent message={`Unknown route: ${path}`} />;
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import type { ErrorInfo } from "react";
3
+ import {
4
+ ErrorBoundary as ReactErrorBoundary,
5
+ type FallbackProps,
6
+ } from "react-error-boundary";
7
+
8
+ export type { FallbackProps } from "react-error-boundary";
9
+
10
+ export function ErrorBoundary({
11
+ children,
12
+ FallbackComponent,
13
+ resetKeys,
14
+ onError,
15
+ }: {
16
+ children: React.ReactNode;
17
+ FallbackComponent: React.ComponentType<FallbackProps>;
18
+ resetKeys?: Array<string | number | boolean | null | undefined>;
19
+ onError: (error: Error, info: ErrorInfo) => void;
20
+ }) {
21
+ return (
22
+ <ReactErrorBoundary
23
+ FallbackComponent={FallbackComponent}
24
+ onError={onError}
25
+ resetKeys={resetKeys}
26
+ >
27
+ {children}
28
+ </ReactErrorBoundary>
29
+ );
30
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./compose";
2
+ export * from "./error-boundary";
@@ -0,0 +1,109 @@
1
+ import { createRouter } from "@btst/yar";
2
+
3
+ import type {
4
+ ClientLibConfig,
5
+ ClientLib,
6
+ ClientPlugin,
7
+ PluginRoutes,
8
+ Sitemap,
9
+ } from "../types";
10
+ export type { ClientPlugin } from "../types";
11
+
12
+ /**
13
+ * Creates the client library with plugin support
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // For Next.js with SSR:
18
+ * const lib = createStackClient({
19
+ * plugins: {
20
+ * blog: blogPlugin.client
21
+ * }
22
+ * });
23
+ *
24
+ * // SPA usage - just render the route
25
+ * function Page() {
26
+ * return lib.resolveRoute('/blog');
27
+ * }
28
+ *
29
+ * // SSR usage - prefetch data with loader, then render
30
+ * async function Page({ params }) {
31
+ * const path = '/blog';
32
+ *
33
+ * // Load data server-side if loader exists
34
+ * const loader = lib.getLoader(path);
35
+ * if (loader) await loader(queryClient, baseURL, basePath);
36
+ *
37
+ * // Render with built-in Suspense + Error Boundary
38
+ * return lib.resolveRoute(path);
39
+ * }
40
+ *
41
+ * // Next.js with notFound() function
42
+ * import { notFound } from 'next/navigation';
43
+ *
44
+ * async function Page({ params }) {
45
+ * const path = '/blog';
46
+ * const loader = lib.getLoader(path);
47
+ * if (loader) await loader(queryClient, baseURL);
48
+ *
49
+ * return lib.resolveRoute(path, {
50
+ * onNotFound: notFound // Calls Next.js notFound() instead of rendering
51
+ * });
52
+ * }
53
+ *
54
+ * ```
55
+ *
56
+ * @template TPlugins - The exact plugins map (inferred from config)
57
+ * @template TRoutes - All routes from all plugins, merged (computed automatically)
58
+ */
59
+ export function createStackClient<
60
+ TPlugins extends Record<string, ClientPlugin<any, any>>,
61
+ TRoutes extends PluginRoutes<TPlugins> = PluginRoutes<TPlugins>,
62
+ >(config: ClientLibConfig<TPlugins>): ClientLib<TRoutes> {
63
+ const { plugins } = config;
64
+
65
+ // Collect all routes from all plugins
66
+ // We build this with type assertions to preserve literal keys
67
+ const allRoutes = {} as TRoutes;
68
+
69
+ for (const [pluginKey, plugin] of Object.entries(plugins)) {
70
+ // Add routes
71
+ const pluginRoutes = plugin.routes();
72
+ Object.assign(allRoutes, pluginRoutes);
73
+ }
74
+
75
+ // Create the composed router - TypeScript will infer the router type
76
+ // The router's getRoute method will return the union of all route return types
77
+ const router = createRouter<TRoutes, {}>(allRoutes);
78
+
79
+ return {
80
+ router,
81
+ async generateSitemap() {
82
+ const sitemapEntries: Sitemap = [];
83
+ for (const plugin of Object.values(plugins)) {
84
+ if (typeof plugin.sitemap === "function") {
85
+ // Allow each plugin to return a partial sitemap
86
+ const entries = await plugin.sitemap();
87
+ if (Array.isArray(entries)) sitemapEntries.push(...entries);
88
+ }
89
+ }
90
+ // De-duplicate by URL while preserving lastModified/priorities preferring the first occurrence
91
+ const seen = new Set<string>();
92
+ const deduped: Sitemap = [];
93
+ for (const entry of sitemapEntries) {
94
+ if (!entry?.url || seen.has(entry.url)) continue;
95
+ seen.add(entry.url);
96
+ deduped.push(entry);
97
+ }
98
+ return deduped;
99
+ },
100
+ };
101
+ }
102
+
103
+ export type { ClientLib, ClientLibConfig };
104
+
105
+ export { sitemapEntryToXmlString } from "./sitemap-utils";
106
+
107
+ export { metaElementsToObject } from "./meta-utils";
108
+
109
+ export { normalizePath } from "./path-utils";
@@ -0,0 +1,228 @@
1
+ interface Metadata {
2
+ title: string;
3
+ description?: string;
4
+ keywords?: string[];
5
+ applicationName?: string;
6
+ generator?: string;
7
+ referrer?:
8
+ | "no-referrer"
9
+ | "origin"
10
+ | "no-referrer-when-downgrade"
11
+ | "origin-when-cross-origin"
12
+ | "same-origin"
13
+ | "strict-origin"
14
+ | "strict-origin-when-cross-origin";
15
+ themeColor?: string;
16
+ viewport?: string;
17
+ creator?: string;
18
+ publisher?: string;
19
+ authors?: { name: string }[];
20
+ abstract?: string;
21
+ robots?: string;
22
+ alternates?: Partial<{ canonical: string }>;
23
+ twitter?: Partial<{
24
+ title: string;
25
+ description: string;
26
+ site: string;
27
+ creator: string;
28
+ images?: string[];
29
+ }>;
30
+ openGraph?: Partial<{
31
+ title: string;
32
+ description: string;
33
+ url: string;
34
+ siteName: string;
35
+ locale: string;
36
+ images?: string[];
37
+ videos?: string[];
38
+ audio?: string[];
39
+ }>;
40
+ }
41
+
42
+ /**
43
+ * Converts an array of meta elements to a metadata object
44
+ * @param metaElements - An array of meta elements
45
+ * @example
46
+ * ```ts
47
+ * const metaElements = [
48
+ * { name: "title", content: "My Page" },
49
+ * { name: "description", content: "This is my page" },
50
+ * ];
51
+ * const metadata = metaElementsToObject(metaElements);
52
+ * console.log(metadata);
53
+ * ```
54
+ */
55
+ export function metaElementsToObject(
56
+ metaElements: Array<React.JSX.IntrinsicElements["meta"] | undefined>,
57
+ ) {
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const metadata: Metadata = { title: "" };
60
+
61
+ // Handlers for meta name= mappings
62
+ const nameHandlers: Record<string, (content: string) => void> = {
63
+ title: (c) => {
64
+ metadata.title = c;
65
+ },
66
+ description: (c) => {
67
+ metadata.description = c;
68
+ },
69
+ keywords: (c) => {
70
+ const parts = c
71
+ .split(",")
72
+ .map((k) => k.trim())
73
+ .filter(Boolean);
74
+ metadata.keywords = parts.length > 0 ? parts : undefined;
75
+ },
76
+ "application-name": (c) => {
77
+ metadata.applicationName = c;
78
+ },
79
+ generator: (c) => {
80
+ metadata.generator = c;
81
+ },
82
+ referrer: (c) => {
83
+ const allowedReferrers = new Set([
84
+ "no-referrer",
85
+ "origin",
86
+ "no-referrer-when-downgrade",
87
+ "origin-when-cross-origin",
88
+ "same-origin",
89
+ "strict-origin",
90
+ "strict-origin-when-cross-origin",
91
+ "unsafe-url",
92
+ ]);
93
+ if (allowedReferrers.has(c)) {
94
+ metadata.referrer = c as never;
95
+ }
96
+ },
97
+ "theme-color": (c) => {
98
+ metadata.themeColor = c;
99
+ },
100
+ viewport: (c) => {
101
+ metadata.viewport = c;
102
+ },
103
+ creator: (c) => {
104
+ metadata.creator = c;
105
+ },
106
+ publisher: (c) => {
107
+ metadata.publisher = c;
108
+ },
109
+ author: (c) => {
110
+ metadata.authors = [{ name: c }];
111
+ },
112
+ abstract: (c) => {
113
+ metadata.abstract = c;
114
+ },
115
+ robots: (c) => {
116
+ metadata.robots = c;
117
+ },
118
+ // Twitter
119
+ "twitter:title": (c) => {
120
+ if (!metadata.twitter) metadata.twitter = {};
121
+ metadata.twitter.title = c;
122
+ },
123
+ "twitter:description": (c) => {
124
+ if (!metadata.twitter) metadata.twitter = {};
125
+ metadata.twitter.description = c;
126
+ },
127
+ "twitter:site": (c) => {
128
+ if (!metadata.twitter) metadata.twitter = {};
129
+ metadata.twitter.site = c;
130
+ },
131
+ "twitter:creator": (c) => {
132
+ if (!metadata.twitter) metadata.twitter = {};
133
+ metadata.twitter.creator = c;
134
+ },
135
+ "twitter:image": (c) => {
136
+ if (!metadata.twitter) metadata.twitter = {};
137
+ const currentImages = metadata.twitter.images;
138
+ if (!currentImages) {
139
+ metadata.twitter.images = [c];
140
+ } else if (Array.isArray(currentImages)) {
141
+ metadata.twitter.images = [...currentImages, c];
142
+ } else {
143
+ metadata.twitter.images = [currentImages, c];
144
+ }
145
+ },
146
+ };
147
+
148
+ // Handlers for meta property= (Open Graph)
149
+ const propertyHandlers: Record<string, (content: string) => void> = {
150
+ "og:title": (c) => {
151
+ if (!metadata.openGraph) metadata.openGraph = {};
152
+ metadata.openGraph.title = c;
153
+ },
154
+ "og:description": (c) => {
155
+ if (!metadata.openGraph) metadata.openGraph = {};
156
+ metadata.openGraph.description = c;
157
+ },
158
+ "og:url": (c) => {
159
+ if (!metadata.openGraph) metadata.openGraph = {};
160
+ metadata.openGraph.url = c;
161
+ metadata.alternates = {
162
+ ...(metadata.alternates ?? {}),
163
+ canonical: c,
164
+ };
165
+ },
166
+ "og:site_name": (c) => {
167
+ if (!metadata.openGraph) metadata.openGraph = {};
168
+ metadata.openGraph.siteName = c;
169
+ },
170
+ "og:locale": (c) => {
171
+ if (!metadata.openGraph) metadata.openGraph = {};
172
+ metadata.openGraph.locale = c;
173
+ },
174
+ "og:image": (c) => {
175
+ if (!metadata.openGraph) metadata.openGraph = {};
176
+ const currentImages = metadata.openGraph.images;
177
+ if (!currentImages) {
178
+ metadata.openGraph.images = [c];
179
+ } else if (Array.isArray(currentImages)) {
180
+ metadata.openGraph.images = [...currentImages, c];
181
+ } else {
182
+ metadata.openGraph.images = [currentImages, c];
183
+ }
184
+ },
185
+ "og:video": (c) => {
186
+ if (!metadata.openGraph) metadata.openGraph = {};
187
+ const currentVideos = metadata.openGraph.videos;
188
+ if (!currentVideos) {
189
+ metadata.openGraph.videos = [c];
190
+ } else if (Array.isArray(currentVideos)) {
191
+ metadata.openGraph.videos = [...currentVideos, c];
192
+ } else {
193
+ metadata.openGraph.videos = [currentVideos, c];
194
+ }
195
+ },
196
+ "og:audio": (c) => {
197
+ if (!metadata.openGraph) metadata.openGraph = {};
198
+ const currentAudio = metadata.openGraph.audio;
199
+ if (!currentAudio) {
200
+ metadata.openGraph.audio = [c];
201
+ } else if (Array.isArray(currentAudio)) {
202
+ metadata.openGraph.audio = [...currentAudio, c];
203
+ } else {
204
+ metadata.openGraph.audio = [currentAudio, c];
205
+ }
206
+ },
207
+ };
208
+
209
+ for (const meta of metaElements) {
210
+ if (!meta) continue;
211
+
212
+ // name-based
213
+ if ("name" in meta && "content" in meta) {
214
+ const handler = nameHandlers[String(meta.name)];
215
+ if (handler) handler(String(meta.content));
216
+ continue;
217
+ }
218
+
219
+ // property-based (Open Graph)
220
+ if ("property" in meta && "content" in meta) {
221
+ const handler = propertyHandlers[String(meta.property)];
222
+ if (handler) handler(String(meta.content));
223
+ continue;
224
+ }
225
+ }
226
+
227
+ return metadata;
228
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Normalizes path segments from framework route params into a consistent path string.
3
+ *
4
+ * Handles different framework param formats:
5
+ * - Next.js: `pathParams.all` (string[])
6
+ * - React Router: `params["*"]` (string)
7
+ * - TanStack Router: `params._splat` (string)
8
+ *
9
+ * @param path - Path segments as string, string array, or undefined
10
+ * @returns Normalized path string starting with "/" (or "/" for empty/undefined)
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Next.js
15
+ * normalizePath(pathParams?.all) // ["blog", "post"] => "/blog/post"
16
+ *
17
+ * // React Router / TanStack Router
18
+ * normalizePath(params["*"]) // "blog/post" => "/blog/post"
19
+ * normalizePath(undefined) // => "/"
20
+ * ```
21
+ */
22
+ export function normalizePath(
23
+ path?: string | Array<string> | undefined,
24
+ ): string {
25
+ if (!path) {
26
+ return "/";
27
+ }
28
+
29
+ if (Array.isArray(path)) {
30
+ // Handle Next.js format: pathParams.all (string[])
31
+ const segments = path.filter(Boolean);
32
+ return segments.length > 0 ? `/${segments.join("/")}` : "/";
33
+ }
34
+
35
+ // Handle React Router / TanStack Router format: params["*"] or params._splat (string)
36
+ const segments = path.split("/").filter(Boolean);
37
+ return segments.length > 0 ? `/${segments.join("/")}` : "/";
38
+ }
@@ -0,0 +1,46 @@
1
+ import type { Sitemap } from "../types";
2
+
3
+ /**
4
+ * Converts an array of sitemap entries into an XML string following the sitemap.org protocol.
5
+ *
6
+ * @param entries - Array of sitemap entries from `lib.generateSitemap()`
7
+ * @returns Complete XML string for the sitemap
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const entries = await lib.generateSitemap();
12
+ * const xml = sitemapEntryToXmlString(entries);
13
+ * return new Response(xml, {
14
+ * headers: { "Content-Type": "application/xml; charset=utf-8" }
15
+ * });
16
+ * ```
17
+ */
18
+ export function sitemapEntryToXmlString(entries: Sitemap): string {
19
+ const xml =
20
+ `<?xml version="1.0" encoding="UTF-8"?>` +
21
+ `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">` +
22
+ entries
23
+ .map((entry) => {
24
+ const url = `<loc>${entry.url}</loc>`;
25
+ const lastModified = entry.lastModified
26
+ ? `<lastmod>${
27
+ entry.lastModified instanceof Date
28
+ ? entry.lastModified.toISOString()
29
+ : entry.lastModified
30
+ }</lastmod>`
31
+ : "";
32
+ const changeFrequency = entry.changeFrequency
33
+ ? `<changefreq>${entry.changeFrequency}</changefreq>`
34
+ : "";
35
+ const priority =
36
+ entry.priority !== undefined
37
+ ? `<priority>${entry.priority}</priority>`
38
+ : "";
39
+
40
+ return `<url>${url}${lastModified}${changeFrequency}${priority}</url>`;
41
+ })
42
+ .join("") +
43
+ `</urlset>`;
44
+
45
+ return xml;
46
+ }
@@ -0,0 +1 @@
1
+ export * from "./provider";