@btst/stack 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -2
- package/dist/api/index.cjs +2 -2
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.mjs +2 -2
- package/dist/client/components/compose.cjs +68 -0
- package/dist/client/components/compose.mjs +65 -0
- package/dist/client/components/error-boundary.cjs +24 -0
- package/dist/client/components/error-boundary.mjs +22 -0
- package/dist/client/components/index.cjs +10 -0
- package/dist/client/components/index.d.cts +52 -0
- package/dist/client/components/index.d.mts +52 -0
- package/dist/client/components/index.d.ts +52 -0
- package/dist/client/components/index.mjs +2 -0
- package/dist/client/index.cjs +21 -16
- package/dist/client/index.d.cts +102 -14
- package/dist/client/index.d.mts +102 -14
- package/dist/client/index.d.ts +102 -14
- package/dist/client/index.mjs +19 -10
- package/dist/client/meta-utils.cjs +162 -0
- package/dist/client/meta-utils.mjs +160 -0
- package/dist/client/sitemap-utils.cjs +14 -0
- package/dist/client/sitemap-utils.mjs +12 -0
- package/dist/context/index.cjs +6 -51
- package/dist/context/index.d.cts +26 -26
- package/dist/context/index.d.mts +26 -26
- package/dist/context/index.d.ts +26 -26
- package/dist/context/index.mjs +1 -50
- package/dist/context/provider.cjs +51 -0
- package/dist/context/provider.mjs +46 -0
- package/dist/index.cjs +0 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +0 -2
- package/dist/plugins/api/index.cjs +15 -0
- package/dist/plugins/api/index.d.cts +41 -0
- package/dist/plugins/api/index.d.mts +41 -0
- package/dist/plugins/api/index.d.ts +41 -0
- package/dist/plugins/api/index.mjs +9 -0
- package/dist/plugins/blog/api/index.cjs +11 -0
- package/dist/plugins/blog/api/index.d.cts +7 -0
- package/dist/plugins/blog/api/index.d.mts +7 -0
- package/dist/plugins/blog/api/index.d.ts +7 -0
- package/dist/plugins/blog/api/index.mjs +2 -0
- package/dist/plugins/blog/api/plugin.cjs +569 -0
- package/dist/plugins/blog/api/plugin.mjs +565 -0
- package/dist/plugins/blog/client/components/forms/image-field.cjs +133 -0
- package/dist/plugins/blog/client/components/forms/image-field.mjs +131 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor.cjs +106 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor.mjs +104 -0
- package/dist/plugins/blog/client/components/forms/post-forms.cjs +401 -0
- package/dist/plugins/blog/client/components/forms/post-forms.mjs +398 -0
- package/dist/plugins/blog/client/components/forms/tags-multiselect.cjs +71 -0
- package/dist/plugins/blog/client/components/forms/tags-multiselect.mjs +65 -0
- package/dist/plugins/blog/client/components/index.cjs +17 -0
- package/dist/plugins/blog/client/components/index.d.cts +22 -0
- package/dist/plugins/blog/client/components/index.d.mts +22 -0
- package/dist/plugins/blog/client/components/index.d.ts +22 -0
- package/dist/plugins/blog/client/components/index.mjs +12 -0
- package/dist/plugins/blog/client/components/loading/form-page-skeleton.cjs +62 -0
- package/dist/plugins/blog/client/components/loading/form-page-skeleton.mjs +60 -0
- package/dist/plugins/blog/client/components/loading/index.cjs +20 -0
- package/dist/plugins/blog/client/components/loading/index.mjs +16 -0
- package/dist/plugins/blog/client/components/loading/list-page-skeleton.cjs +26 -0
- package/dist/plugins/blog/client/components/loading/list-page-skeleton.mjs +24 -0
- package/dist/plugins/blog/client/components/loading/page-header-skeleton.cjs +13 -0
- package/dist/plugins/blog/client/components/loading/page-header-skeleton.mjs +11 -0
- package/dist/plugins/blog/client/components/loading/post-card-skeleton.cjs +22 -0
- package/dist/plugins/blog/client/components/loading/post-card-skeleton.mjs +20 -0
- package/dist/plugins/blog/client/components/loading/post-page-skeleton.cjs +56 -0
- package/dist/plugins/blog/client/components/loading/post-page-skeleton.mjs +54 -0
- package/dist/plugins/blog/client/components/pages/404-page.cjs +19 -0
- package/dist/plugins/blog/client/components/pages/404-page.mjs +17 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.cjs +41 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.internal.cjs +57 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.internal.mjs +55 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.mjs +39 -0
- package/dist/plugins/blog/client/components/pages/home-page.cjs +41 -0
- package/dist/plugins/blog/client/components/pages/home-page.internal.cjs +61 -0
- package/dist/plugins/blog/client/components/pages/home-page.internal.mjs +59 -0
- package/dist/plugins/blog/client/components/pages/home-page.mjs +39 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.cjs +37 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.internal.cjs +53 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.internal.mjs +51 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.mjs +35 -0
- package/dist/plugins/blog/client/components/pages/post-page.cjs +39 -0
- package/dist/plugins/blog/client/components/pages/post-page.internal.cjs +101 -0
- package/dist/plugins/blog/client/components/pages/post-page.internal.mjs +99 -0
- package/dist/plugins/blog/client/components/pages/post-page.mjs +37 -0
- package/dist/plugins/blog/client/components/pages/tag-page.cjs +39 -0
- package/dist/plugins/blog/client/components/pages/tag-page.internal.cjs +61 -0
- package/dist/plugins/blog/client/components/pages/tag-page.internal.mjs +59 -0
- package/dist/plugins/blog/client/components/pages/tag-page.mjs +37 -0
- package/dist/plugins/blog/client/components/shared/better-blog-attribution.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/better-blog-attribution.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/default-error.cjs +18 -0
- package/dist/plugins/blog/client/components/shared/default-error.mjs +16 -0
- package/dist/plugins/blog/client/components/shared/defaults.cjs +13 -0
- package/dist/plugins/blog/client/components/shared/defaults.mjs +10 -0
- package/dist/plugins/blog/client/components/shared/empty-list.cjs +21 -0
- package/dist/plugins/blog/client/components/shared/empty-list.mjs +19 -0
- package/dist/plugins/blog/client/components/shared/error-placeholder.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/error-placeholder.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/highlight-text.cjs +53 -0
- package/dist/plugins/blog/client/components/shared/highlight-text.mjs +51 -0
- package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
- package/dist/plugins/blog/client/components/shared/markdown-content.cjs +324 -0
- package/dist/plugins/blog/client/components/shared/markdown-content.mjs +315 -0
- package/dist/plugins/blog/client/components/shared/on-this-page.cjs +161 -0
- package/dist/plugins/blog/client/components/shared/on-this-page.mjs +158 -0
- package/dist/plugins/blog/client/components/shared/page-header.cjs +40 -0
- package/dist/plugins/blog/client/components/shared/page-header.mjs +38 -0
- package/dist/plugins/blog/client/components/shared/page-layout.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/page-layout.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/page-wrapper.cjs +23 -0
- package/dist/plugins/blog/client/components/shared/page-wrapper.mjs +21 -0
- package/dist/plugins/blog/client/components/shared/post-card.cjs +279 -0
- package/dist/plugins/blog/client/components/shared/post-card.mjs +277 -0
- package/dist/plugins/blog/client/components/shared/post-navigation.cjs +74 -0
- package/dist/plugins/blog/client/components/shared/post-navigation.mjs +72 -0
- package/dist/plugins/blog/client/components/shared/posts-list.cjs +48 -0
- package/dist/plugins/blog/client/components/shared/posts-list.mjs +46 -0
- package/dist/plugins/blog/client/components/shared/recent-posts-carousel.cjs +59 -0
- package/dist/plugins/blog/client/components/shared/recent-posts-carousel.mjs +57 -0
- package/dist/plugins/blog/client/components/shared/search-input.cjs +136 -0
- package/dist/plugins/blog/client/components/shared/search-input.mjs +117 -0
- package/dist/plugins/blog/client/components/shared/search-modal.cjs +135 -0
- package/dist/plugins/blog/client/components/shared/search-modal.mjs +116 -0
- package/dist/plugins/blog/client/components/shared/tags-list.cjs +22 -0
- package/dist/plugins/blog/client/components/shared/tags-list.mjs +20 -0
- package/dist/plugins/blog/client/components/shared/use-route-lifecycle.cjs +50 -0
- package/dist/plugins/blog/client/components/shared/use-route-lifecycle.mjs +48 -0
- package/dist/plugins/blog/client/hooks/blog-hooks.cjs +380 -0
- package/dist/plugins/blog/client/hooks/blog-hooks.mjs +368 -0
- package/dist/plugins/blog/client/hooks/index.cjs +17 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +150 -0
- package/dist/plugins/blog/client/hooks/index.d.mts +150 -0
- package/dist/plugins/blog/client/hooks/index.d.ts +150 -0
- package/dist/plugins/blog/client/hooks/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/use-debounce.cjs +16 -0
- package/dist/plugins/blog/client/hooks/use-debounce.mjs +14 -0
- package/dist/plugins/blog/client/index.cjs +7 -0
- package/dist/plugins/blog/client/index.d.cts +414 -0
- package/dist/plugins/blog/client/index.d.mts +414 -0
- package/dist/plugins/blog/client/index.d.ts +414 -0
- package/dist/plugins/blog/client/index.mjs +1 -0
- package/dist/plugins/blog/client/localization/blog-card.cjs +7 -0
- package/dist/plugins/blog/client/localization/blog-card.mjs +5 -0
- package/dist/plugins/blog/client/localization/blog-common.cjs +10 -0
- package/dist/plugins/blog/client/localization/blog-common.mjs +8 -0
- package/dist/plugins/blog/client/localization/blog-forms.cjs +40 -0
- package/dist/plugins/blog/client/localization/blog-forms.mjs +38 -0
- package/dist/plugins/blog/client/localization/blog-list.cjs +18 -0
- package/dist/plugins/blog/client/localization/blog-list.mjs +16 -0
- package/dist/plugins/blog/client/localization/blog-post.cjs +13 -0
- package/dist/plugins/blog/client/localization/blog-post.mjs +11 -0
- package/dist/plugins/blog/client/localization/index.cjs +17 -0
- package/dist/plugins/blog/client/localization/index.mjs +15 -0
- package/dist/plugins/blog/client/plugin.cjs +462 -0
- package/dist/plugins/blog/client/plugin.mjs +460 -0
- package/dist/plugins/blog/client.css +3 -0
- package/dist/plugins/blog/db.cjs +90 -0
- package/dist/plugins/blog/db.mjs +88 -0
- package/dist/plugins/blog/query-keys.cjs +181 -0
- package/dist/plugins/blog/query-keys.d.cts +530 -0
- package/dist/plugins/blog/query-keys.d.mts +530 -0
- package/dist/plugins/blog/query-keys.d.ts +530 -0
- package/dist/plugins/blog/query-keys.mjs +179 -0
- package/dist/plugins/blog/schemas.cjs +39 -0
- package/dist/plugins/blog/schemas.mjs +35 -0
- package/dist/plugins/blog/style.css +22 -0
- package/dist/plugins/blog/utils.cjs +97 -0
- package/dist/plugins/blog/utils.mjs +87 -0
- package/dist/plugins/client/index.cjs +15 -0
- package/dist/plugins/client/index.d.cts +57 -0
- package/dist/plugins/client/index.d.mts +57 -0
- package/dist/plugins/client/index.d.ts +57 -0
- package/dist/plugins/client/index.mjs +9 -0
- package/dist/{shared/stack.Br2KMECJ.cjs → plugins/utils.cjs} +1 -8
- package/dist/{shared/stack.CwGEQ10b.mjs → plugins/utils.mjs} +2 -8
- package/dist/shared/{stack.Dva9muUy.d.cts → stack.ByOugz9d.d.cts} +17 -23
- package/dist/shared/{stack.Dva9muUy.d.mts → stack.ByOugz9d.d.mts} +17 -23
- package/dist/shared/{stack.Dva9muUy.d.ts → stack.ByOugz9d.d.ts} +17 -23
- package/dist/shared/stack.Cr2JoQdo.d.cts +76 -0
- package/dist/shared/stack.Cr2JoQdo.d.mts +76 -0
- package/dist/shared/stack.Cr2JoQdo.d.ts +76 -0
- package/package.json +104 -16
- package/src/__tests__/plugins.test.tsx +539 -0
- package/src/__tests__/sitemap.test.ts +60 -0
- package/src/api/index.ts +73 -0
- package/src/client/components/compose.tsx +116 -0
- package/src/client/components/error-boundary.tsx +30 -0
- package/src/client/components/index.tsx +2 -0
- package/src/client/index.ts +107 -0
- package/src/client/meta-utils.ts +228 -0
- package/src/client/sitemap-utils.ts +46 -0
- package/src/context/index.ts +1 -0
- package/src/context/provider.tsx +157 -0
- package/src/index.ts +1 -0
- package/src/plugins/api/index.ts +51 -0
- package/src/plugins/blog/api/index.ts +2 -0
- package/src/plugins/blog/api/plugin.ts +759 -0
- package/src/plugins/blog/client/components/forms/image-field.tsx +165 -0
- package/src/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +136 -0
- package/src/plugins/blog/client/components/forms/post-forms.tsx +531 -0
- package/src/plugins/blog/client/components/forms/tags-multiselect.tsx +79 -0
- package/src/plugins/blog/client/components/index.tsx +11 -0
- package/src/plugins/blog/client/components/loading/form-page-skeleton.tsx +75 -0
- package/src/plugins/blog/client/components/loading/index.tsx +27 -0
- package/src/plugins/blog/client/components/loading/list-page-skeleton.tsx +38 -0
- package/src/plugins/blog/client/components/loading/page-header-skeleton.tsx +10 -0
- package/src/plugins/blog/client/components/loading/post-card-skeleton.tsx +30 -0
- package/src/plugins/blog/client/components/loading/post-page-skeleton.tsx +75 -0
- package/src/plugins/blog/client/components/pages/404-page.tsx +23 -0
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +60 -0
- package/src/plugins/blog/client/components/pages/edit-post-page.tsx +40 -0
- package/src/plugins/blog/client/components/pages/home-page.internal.tsx +71 -0
- package/src/plugins/blog/client/components/pages/home-page.tsx +42 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +59 -0
- package/src/plugins/blog/client/components/pages/new-post-page.tsx +36 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +142 -0
- package/src/plugins/blog/client/components/pages/post-page.tsx +38 -0
- package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +74 -0
- package/src/plugins/blog/client/components/pages/tag-page.tsx +38 -0
- package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +19 -0
- package/src/plugins/blog/client/components/shared/default-error.tsx +20 -0
- package/src/plugins/blog/client/components/shared/defaults.tsx +9 -0
- package/src/plugins/blog/client/components/shared/empty-list.tsx +25 -0
- package/src/plugins/blog/client/components/shared/error-placeholder.tsx +20 -0
- package/src/plugins/blog/client/components/shared/highlight-text.tsx +80 -0
- package/src/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
- package/src/plugins/blog/client/components/shared/markdown-content.tsx +448 -0
- package/src/plugins/blog/client/components/shared/on-this-page.tsx +234 -0
- package/src/plugins/blog/client/components/shared/page-header.tsx +35 -0
- package/src/plugins/blog/client/components/shared/page-layout.tsx +23 -0
- package/src/plugins/blog/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/blog/client/components/shared/post-card.tsx +308 -0
- package/src/plugins/blog/client/components/shared/post-navigation.tsx +98 -0
- package/src/plugins/blog/client/components/shared/posts-list.tsx +67 -0
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +79 -0
- package/src/plugins/blog/client/components/shared/search-input.tsx +146 -0
- package/src/plugins/blog/client/components/shared/search-modal.tsx +162 -0
- package/src/plugins/blog/client/components/shared/tags-list.tsx +34 -0
- package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +68 -0
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +623 -0
- package/src/plugins/blog/client/hooks/index.tsx +1 -0
- package/src/plugins/blog/client/hooks/use-debounce.ts +43 -0
- package/src/plugins/blog/client/index.ts +9 -0
- package/src/plugins/blog/client/localization/blog-card.ts +3 -0
- package/src/plugins/blog/client/localization/blog-common.ts +7 -0
- package/src/plugins/blog/client/localization/blog-forms.ts +45 -0
- package/src/plugins/blog/client/localization/blog-list.ts +14 -0
- package/src/plugins/blog/client/localization/blog-post.ts +9 -0
- package/src/plugins/blog/client/localization/index.ts +15 -0
- package/src/plugins/blog/client/overrides.ts +123 -0
- package/src/plugins/blog/client/plugin.tsx +672 -0
- package/src/plugins/blog/client.css +3 -0
- package/src/plugins/blog/db.ts +90 -0
- package/src/plugins/blog/query-keys.ts +267 -0
- package/src/plugins/blog/schemas.ts +39 -0
- package/src/plugins/blog/style.css +22 -0
- package/src/plugins/blog/types.ts +37 -0
- package/src/plugins/blog/utils.ts +144 -0
- package/src/plugins/client/index.ts +53 -0
- package/src/plugins/index.ts +0 -0
- package/src/plugins/utils.ts +35 -0
- package/src/types.ts +209 -0
- package/dist/plugins/index.cjs +0 -16
- package/dist/plugins/index.d.cts +0 -64
- package/dist/plugins/index.d.mts +0 -64
- package/dist/plugins/index.d.ts +0 -64
- package/dist/plugins/index.mjs +0 -11
- package/dist/shared/stack.DvFqFlOV.d.cts +0 -22
- package/dist/shared/stack.DvFqFlOV.d.mts +0 -22
- package/dist/shared/stack.DvFqFlOV.d.ts +0 -22
|
@@ -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,107 @@
|
|
|
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";
|
|
@@ -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,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";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context value that provides plugin-specific overrides
|
|
6
|
+
* Generic over the shape of all plugin overrides
|
|
7
|
+
*/
|
|
8
|
+
interface BetterStackContextValue<
|
|
9
|
+
TPluginOverrides extends Record<string, any>,
|
|
10
|
+
> {
|
|
11
|
+
/**
|
|
12
|
+
* The overrides for the plugin.
|
|
13
|
+
*/
|
|
14
|
+
overrides: TPluginOverrides;
|
|
15
|
+
/**
|
|
16
|
+
* The base path where the client router is mounted.
|
|
17
|
+
*/
|
|
18
|
+
basePath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BetterStackContext = createContext<BetterStackContextValue<any> | null>(
|
|
22
|
+
null,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provider component for BetterStack context
|
|
27
|
+
* Provides type-safe access to plugin-specific overrides
|
|
28
|
+
*
|
|
29
|
+
* Only requires override values, not plugin objects - keeps bundle size minimal!
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* // Define the type shape (no import of plugin values needed!)
|
|
34
|
+
* type MyPluginOverrides = {
|
|
35
|
+
* todos: TodosPluginOverrides;
|
|
36
|
+
* messages: MessagesPluginOverrides;
|
|
37
|
+
* };
|
|
38
|
+
*
|
|
39
|
+
* <BetterStackProvider<MyPluginOverrides>
|
|
40
|
+
* overrides={{
|
|
41
|
+
* todos: {
|
|
42
|
+
* Link: (props) => <NextLink {...props} />,
|
|
43
|
+
* navigate: (path) => router.push(path),
|
|
44
|
+
* },
|
|
45
|
+
* messages: {
|
|
46
|
+
* MarkdownRenderer: (props) => <ReactMarkdown {...props} />,
|
|
47
|
+
* }
|
|
48
|
+
* }}
|
|
49
|
+
* >
|
|
50
|
+
* {children}
|
|
51
|
+
* </BetterStackProvider>
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function BetterStackProvider<
|
|
55
|
+
TPluginOverrides extends Record<string, any> = Record<string, any>,
|
|
56
|
+
>({
|
|
57
|
+
children,
|
|
58
|
+
overrides,
|
|
59
|
+
basePath,
|
|
60
|
+
}: {
|
|
61
|
+
children?: ReactNode;
|
|
62
|
+
overrides: TPluginOverrides;
|
|
63
|
+
basePath: string;
|
|
64
|
+
}) {
|
|
65
|
+
const value: BetterStackContextValue<TPluginOverrides> = {
|
|
66
|
+
overrides,
|
|
67
|
+
basePath,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<BetterStackContext.Provider value={value}>
|
|
72
|
+
{children}
|
|
73
|
+
</BetterStackContext.Provider>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Hook to access the entire context
|
|
79
|
+
* Useful if you need access to multiple plugins or the full context
|
|
80
|
+
*/
|
|
81
|
+
export function useBetterStack<
|
|
82
|
+
TPluginOverrides extends Record<string, any> = Record<string, any>,
|
|
83
|
+
>() {
|
|
84
|
+
const context = useContext(
|
|
85
|
+
BetterStackContext,
|
|
86
|
+
) as BetterStackContextValue<TPluginOverrides> | null;
|
|
87
|
+
|
|
88
|
+
if (!context) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"useBetterStack must be used within BetterStackProvider. " +
|
|
91
|
+
"Wrap your app with <BetterStackProvider> in your layout file.",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Helper type: merge TOverrides with TDefaults, making defaulted properties required
|
|
99
|
+
type OverridesResult<TOverrides, TDefaults> = undefined extends TDefaults
|
|
100
|
+
? TOverrides
|
|
101
|
+
: TOverrides & Required<Pick<TDefaults & {}, keyof TDefaults>>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Hook to access overrides for a specific plugin
|
|
105
|
+
* This is type-safe and will only expose the overrides defined by that plugin
|
|
106
|
+
*
|
|
107
|
+
* When default values are provided, properties with defaults are guaranteed to be non-null.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```tsx
|
|
111
|
+
* // Without defaults - trusts plugin is configured
|
|
112
|
+
* function TodosList() {
|
|
113
|
+
* const { navigate } = usePluginOverrides<TodosPluginOverrides>("todos");
|
|
114
|
+
* // navigate is (path: string) => void (required fields are non-nullable)
|
|
115
|
+
* navigate("/todos/add");
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* // With defaults - optional fields with defaults become required
|
|
119
|
+
* function TodosList() {
|
|
120
|
+
* const { localization } = usePluginOverrides<TodosPluginOverrides, Partial<TodosPluginOverrides>>("todos", {
|
|
121
|
+
* localization: DEFAULT_LOCALIZATION
|
|
122
|
+
* });
|
|
123
|
+
* // localization is Localization (guaranteed to exist because we provided a default)
|
|
124
|
+
* console.log(localization.SOME_KEY);
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function usePluginOverrides<
|
|
129
|
+
TOverrides = any,
|
|
130
|
+
TDefaults extends Partial<TOverrides> | undefined = undefined,
|
|
131
|
+
>(
|
|
132
|
+
pluginName: string,
|
|
133
|
+
defaultValues?: TDefaults,
|
|
134
|
+
): OverridesResult<TOverrides, TDefaults> {
|
|
135
|
+
const context = useBetterStack();
|
|
136
|
+
|
|
137
|
+
const pluginOverrides = context.overrides[pluginName];
|
|
138
|
+
|
|
139
|
+
// If defaults are provided, merge them with plugin overrides
|
|
140
|
+
// This ensures default properties exist even if plugin is partially configured
|
|
141
|
+
const overrides = defaultValues
|
|
142
|
+
? { ...defaultValues, ...pluginOverrides }
|
|
143
|
+
: pluginOverrides;
|
|
144
|
+
|
|
145
|
+
return overrides as OverridesResult<TOverrides, TDefaults>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function useBasePath() {
|
|
149
|
+
const context = useBetterStack();
|
|
150
|
+
if (!context) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
"useBasePath must be used within BetterStackProvider. " +
|
|
153
|
+
"Wrap your app with <BetterStackProvider> in your layout file.",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return context.basePath;
|
|
157
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./api";
|